* @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. * @param {OriginAttributesDictionary} [originAttributes] * The origin attributes of the site loading the manifest. If none are * specified, the origin attributes will be formed of the first party domain * based on the domain of the manifest. * @returns {Promise} * The properties of the loaded OpenSearch engine. */ export async function loadAndParseOpenSearchEngine( sourceURI, lastModified, originAttributes ) { 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, originAttributes); 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. * @param {object} [originAttributes] * The origin attributes to use to load the manifest. * @returns {Promise} * A promise that is resolved with the data if the engine is successfully loaded * and rejected otherwise. */ function loadEngineXML(sourceURI, lastModified, originAttributes = null) { 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, originAttributes ); // 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)) ); } PK