379 lines
11 KiB
JavaScript
379 lines
11 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/**
|
|
* OpenSearchLoader is used for loading OpenSearch definitions from content.
|
|
*/
|
|
|
|
/* eslint no-shadow: error, mozilla/no-aArgs: error */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
|
|
return console.createInstance({
|
|
prefix: "OpenSearchLoader",
|
|
maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
|
|
});
|
|
});
|
|
|
|
// The namespaces from the specification at
|
|
// https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#namespace
|
|
const OPENSEARCH_NS_10 = "http://a9.com/-/spec/opensearch/1.0/";
|
|
const OPENSEARCH_NS_11 = "http://a9.com/-/spec/opensearch/1.1/";
|
|
|
|
// Although the specification at gives the namespace names defined above, many
|
|
// existing OpenSearch engines are using the following versions. We therefore
|
|
// allow any one of these.
|
|
const OPENSEARCH_NAMESPACES = [
|
|
OPENSEARCH_NS_11,
|
|
OPENSEARCH_NS_10,
|
|
"http://a9.com/-/spec/opensearchdescription/1.1/",
|
|
"http://a9.com/-/spec/opensearchdescription/1.0/",
|
|
];
|
|
|
|
// The name of the element defining the OpenSearch definition.
|
|
const OPENSEARCH_LOCALNAME = "OpenSearchDescription";
|
|
|
|
// These were OpenSearch definitions for engines used internally by Mozilla.
|
|
// It may be possible to deprecate/remove these in future.
|
|
const MOZSEARCH_NS_10 = "http://www.mozilla.org/2006/browser/search/";
|
|
const MOZSEARCH_LOCALNAME = "SearchPlugin";
|
|
|
|
/**
|
|
* @typedef {object} OpenSearchProperties
|
|
* @property {string} name
|
|
* The display name of the engine.
|
|
* @property {nsIURI} [installURL]
|
|
* The URL that the engine was initially loaded from.
|
|
* @property {string} [queryCharset]
|
|
* The character set to use for encoding query values.
|
|
* @property {string} [searchForm]
|
|
* Non-standard. The search form URL.
|
|
* @property {string} [updateURL]
|
|
* Non-standard. The update URL for the engine.
|
|
* @property {number} [updateInterval]
|
|
* Non-standard. The update interval for the engine.
|
|
* @property {OpenSearchURL[]} urls
|
|
* An array of URLs associated with the engine.
|
|
* @property {OpenSearchImage[]} images
|
|
* An array of images assocaiated with the engine.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} OpenSearchURL
|
|
* @property {string} type
|
|
* The OpenSearch based type of the URL see SearchUtils.URL_TYPE.
|
|
* @property {string} method
|
|
* The method of submission for the URL: GET or POST.
|
|
* @property {string} template
|
|
* The template for the URL.
|
|
* @property {object[]} params
|
|
* An array of additional properties of name/value pairs. These are not part
|
|
* of the OpenSearch specification, but were used in Firefox prior to Firefox 78.
|
|
* @property {string[]} rels
|
|
* An array of strings that define the relationship of this URL.
|
|
*
|
|
* @see SearchUtils.URL_TYPE
|
|
* @see https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#url-rel-values
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} OpenSearchImage
|
|
* @property {string} url
|
|
* The source URL of the image.
|
|
* @property {number} size
|
|
* The reported width and height of the image.
|
|
*/
|
|
|
|
/**
|
|
* Retrieves the engine data from a URI and returns it.
|
|
*
|
|
* @param {nsIURI} sourceURI
|
|
* The uri from which to load the OpenSearch engine data.
|
|
* @param {string} [lastModified]
|
|
* The UTC date when the engine was last updated, if any.
|
|
* @returns {Promise<OpenSearchProperties>}
|
|
* The properties of the loaded OpenSearch engine.
|
|
*/
|
|
export async function loadAndParseOpenSearchEngine(sourceURI, lastModified) {
|
|
if (!sourceURI) {
|
|
throw Components.Exception(
|
|
"Must have URI when calling _install!",
|
|
Cr.NS_ERROR_UNEXPECTED
|
|
);
|
|
}
|
|
if (!/^https?$/i.test(sourceURI.scheme)) {
|
|
throw Components.Exception(
|
|
"Invalid URI passed to SearchEngine constructor",
|
|
Cr.NS_ERROR_INVALID_ARG
|
|
);
|
|
}
|
|
|
|
lazy.logConsole.debug("Downloading OpenSearch engine from:", sourceURI.spec);
|
|
|
|
let xmlData = await loadEngineXML(sourceURI, lastModified);
|
|
let xmlDocument = await parseXML(xmlData);
|
|
|
|
lazy.logConsole.debug("Loading search plugin");
|
|
|
|
let engineData;
|
|
try {
|
|
engineData = processXMLDocument(xmlDocument);
|
|
} catch (ex) {
|
|
lazy.logConsole.error("parseData: Failed to init engine!", ex);
|
|
|
|
if (ex.result == Cr.NS_ERROR_FILE_CORRUPTED) {
|
|
throw Components.Exception(
|
|
"",
|
|
Ci.nsISearchService.ERROR_ENGINE_CORRUPTED
|
|
);
|
|
}
|
|
throw Components.Exception("", Ci.nsISearchService.ERROR_DOWNLOAD_FAILURE);
|
|
}
|
|
|
|
engineData.installURL = sourceURI;
|
|
return engineData;
|
|
}
|
|
|
|
/**
|
|
* Loads the engine XML from the given URI.
|
|
*
|
|
* @param {nsIURI} sourceURI
|
|
* The uri from which to load the OpenSearch engine data.
|
|
* @param {string} [lastModified]
|
|
* The UTC date when the engine was last updated, if any.
|
|
* @returns {Promise}
|
|
* A promise that is resolved with the data if the engine is successfully loaded
|
|
* and rejected otherwise.
|
|
*/
|
|
function loadEngineXML(sourceURI, lastModified) {
|
|
var chan = lazy.SearchUtils.makeChannel(
|
|
sourceURI,
|
|
// OpenSearchEngine is loading a definition file for a search engine,
|
|
// TYPE_DOCUMENT captures that load best.
|
|
Ci.nsIContentPolicy.TYPE_DOCUMENT
|
|
);
|
|
|
|
// we collect https telemetry for all top-level (document) loads.
|
|
chan.loadInfo.httpsUpgradeTelemetry = sourceURI.schemeIs("https")
|
|
? Ci.nsILoadInfo.ALREADY_HTTPS
|
|
: Ci.nsILoadInfo.NO_UPGRADE;
|
|
|
|
if (lastModified && chan instanceof Ci.nsIHttpChannel) {
|
|
chan.setRequestHeader("If-Modified-Since", lastModified, false);
|
|
}
|
|
let loadPromise = Promise.withResolvers();
|
|
|
|
let loadHandler = data => {
|
|
if (!data) {
|
|
loadPromise.reject(
|
|
Components.Exception("", Ci.nsISearchService.ERROR_DOWNLOAD_FAILURE)
|
|
);
|
|
return;
|
|
}
|
|
loadPromise.resolve(data);
|
|
};
|
|
|
|
var listener = new lazy.SearchUtils.LoadListener(
|
|
chan,
|
|
/(^text\/|xml$)/,
|
|
loadHandler
|
|
);
|
|
chan.notificationCallbacks = listener;
|
|
chan.asyncOpen(listener);
|
|
|
|
return loadPromise.promise;
|
|
}
|
|
|
|
/**
|
|
* Parses an engines XML data into a document element.
|
|
*
|
|
* @param {number[]} xmlData
|
|
* The loaded search engine data.
|
|
* @returns {Element}
|
|
* A document element containing the parsed data.
|
|
*/
|
|
function parseXML(xmlData) {
|
|
var parser = new DOMParser();
|
|
var doc = parser.parseFromBuffer(xmlData, "text/xml");
|
|
|
|
if (!doc?.documentElement) {
|
|
throw Components.Exception(
|
|
"Could not parse file",
|
|
Ci.nsISearchService.ERROR_ENGINE_CORRUPTED
|
|
);
|
|
}
|
|
|
|
if (!hasExpectedNamspeace(doc.documentElement)) {
|
|
throw Components.Exception(
|
|
"Not a valid OpenSearch xml file",
|
|
Ci.nsISearchService.ERROR_ENGINE_CORRUPTED
|
|
);
|
|
}
|
|
return doc.documentElement;
|
|
}
|
|
|
|
/**
|
|
* Extract search engine information from the given document into a form that
|
|
* can be passed to an OpenSearchEngine.
|
|
*
|
|
* @param {Element} xmlDocument
|
|
* The document to examine.
|
|
*/
|
|
function processXMLDocument(xmlDocument) {
|
|
/** @type {OpenSearchProperties} */
|
|
let result = { name: "", urls: [], images: [] };
|
|
|
|
for (let i = 0; i < xmlDocument.children.length; ++i) {
|
|
var child = xmlDocument.children[i];
|
|
switch (child.localName) {
|
|
case "ShortName":
|
|
result.name = child.textContent;
|
|
break;
|
|
case "Url":
|
|
try {
|
|
result.urls.push(parseURL(child));
|
|
} catch (ex) {
|
|
// Parsing of the element failed, just skip it.
|
|
lazy.logConsole.error("Failed to parse URL child:", ex);
|
|
}
|
|
break;
|
|
case "Image": {
|
|
let imageData = parseImage(child);
|
|
if (imageData) {
|
|
result.images.push(imageData);
|
|
}
|
|
break;
|
|
}
|
|
case "InputEncoding":
|
|
// If this is not specified we fallback to the SearchEngine constructor
|
|
// which currently uses SearchUtils.DEFAULT_QUERY_CHARSET which is
|
|
// UTF-8 - the same as for OpenSearch.
|
|
result.queryCharset = child.textContent;
|
|
break;
|
|
|
|
// Non-OpenSearch elements
|
|
case "SearchForm":
|
|
result.searchForm = child.textContent;
|
|
break;
|
|
case "UpdateUrl":
|
|
result.updateURL = child.textContent;
|
|
break;
|
|
case "UpdateInterval":
|
|
result.updateInterval = parseInt(child.textContent);
|
|
break;
|
|
}
|
|
}
|
|
if (!result.name || !result.urls.length) {
|
|
throw Components.Exception(
|
|
"_parse: No name, or missing URL!",
|
|
Cr.NS_ERROR_FAILURE
|
|
);
|
|
}
|
|
if (!result.urls.find(url => url.type == lazy.SearchUtils.URL_TYPE.SEARCH)) {
|
|
throw Components.Exception(
|
|
"_parse: No text/html result type!",
|
|
Cr.NS_ERROR_FAILURE
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Extracts data from an OpenSearch URL element and creates an object which can
|
|
* be used to create an OpenSearchEngine's URL.
|
|
*
|
|
* @param {Element} element
|
|
* The OpenSearch URL element.
|
|
* @returns {OpenSearchURL}
|
|
* The extracted URL data.
|
|
* @throws NS_ERROR_FAILURE if a URL object could not be created.
|
|
*
|
|
* @see https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#the-url-element
|
|
*/
|
|
function parseURL(element) {
|
|
var type = element.getAttribute("type");
|
|
// According to the spec, method is optional, defaulting to "GET" if not
|
|
// specified.
|
|
var method = element.getAttribute("method") || "GET";
|
|
var template = element.getAttribute("template");
|
|
|
|
let rels = [];
|
|
if (element.hasAttribute("rel")) {
|
|
rels = element.getAttribute("rel").toLowerCase().split(/\s+/);
|
|
}
|
|
|
|
// Support an alternate suggestion type, see bug 1425827 for details.
|
|
if (type == "application/json" && rels.includes("suggestions")) {
|
|
type = lazy.SearchUtils.URL_TYPE.SUGGEST_JSON;
|
|
}
|
|
|
|
let url = {
|
|
type,
|
|
method,
|
|
template,
|
|
params: [],
|
|
rels,
|
|
};
|
|
|
|
// Non-standard. Used to be for Mozilla search engine files.
|
|
for (var i = 0; i < element.children.length; ++i) {
|
|
var param = element.children[i];
|
|
if (param.localName == "Param") {
|
|
url.params.push({
|
|
name: param.getAttribute("name"),
|
|
value: param.getAttribute("value"),
|
|
});
|
|
}
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Extracts an icon from an OpenSearch Image element.
|
|
*
|
|
* @param {Element} element
|
|
* The OpenSearch URL element.
|
|
* @returns {OpenSearchImage}
|
|
* The properties of the image.
|
|
* @see https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#the-image-element
|
|
*/
|
|
function parseImage(element) {
|
|
let width = parseInt(element.getAttribute("width"), 10);
|
|
let height = parseInt(element.getAttribute("height"), 10);
|
|
|
|
if (isNaN(width) || isNaN(height) || width <= 0 || width != height) {
|
|
lazy.logConsole.warn(
|
|
"OpenSearch image element must have equal and positive width and height."
|
|
);
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
url: element.textContent,
|
|
size: width,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Confirms if the document has the expected namespace.
|
|
*
|
|
* @param {Element} element
|
|
* The document to check.
|
|
* @returns {boolean}
|
|
* True if the document matches the namespace.
|
|
*/
|
|
function hasExpectedNamspeace(element) {
|
|
return (
|
|
(element.localName == MOZSEARCH_LOCALNAME &&
|
|
element.namespaceURI == MOZSEARCH_NS_10) ||
|
|
(element.localName == OPENSEARCH_LOCALNAME &&
|
|
OPENSEARCH_NAMESPACES.includes(element.namespaceURI))
|
|
);
|
|
}
|