summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/OpenSearchLoader.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/search/OpenSearchLoader.sys.mjs')
-rw-r--r--toolkit/components/search/OpenSearchLoader.sys.mjs393
1 files changed, 393 insertions, 0 deletions
diff --git a/toolkit/components/search/OpenSearchLoader.sys.mjs b/toolkit/components/search/OpenSearchLoader.sys.mjs
new file mode 100644
index 0000000000..e1d820b463
--- /dev/null
+++ b/toolkit/components/search/OpenSearchLoader.sys.mjs
@@ -0,0 +1,393 @@
+/* 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: "resource://gre/modules/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} [description]
+ * The description of the engine.
+ * @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 {string} [IconUpdateUrl]
+ * Non-standard. The update URL for the icon.
+ * @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 {boolean} isPrefered
+ * If this image is of the preferred 16x16 size.
+ * @property {width} width
+ * The reported width of the image.
+ * @property {height} height
+ * The reported 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 {OpenSearchProperties}
+ * The properties of the loaded OpenSearch engine.
+ */
+export async function loadAndParseOpenSearchEngine(sourceURI, lastModified) {
+ if (!sourceURI) {
+ throw Components.Exception(
+ sourceURI,
+ "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
+ );
+
+ 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.
+ * @returns {OpenSearchProperties}
+ * The properties of the OpenSearch engine.
+ */
+function processXMLDocument(xmlDocument) {
+ let result = { 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 "Description":
+ result.description = 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;
+ case "IconUpdateUrl":
+ result.iconUpdateURL = 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);
+ let isPrefered = width == 16 && height == 16;
+
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
+ lazy.logConsole.warn(
+ "OpenSearch image element must have positive width and height."
+ );
+ return null;
+ }
+
+ return {
+ url: element.textContent,
+ isPrefered,
+ width,
+ height,
+ };
+}
+
+/**
+ * Confirms if the document has the expected namespace.
+ *
+ * @param {DOMElement} 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))
+ );
+}