diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/search/OpenSearchLoader.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/search/OpenSearchLoader.sys.mjs')
-rw-r--r-- | toolkit/components/search/OpenSearchLoader.sys.mjs | 393 |
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)) + ); +} |