mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-11 16:32:59 +00:00
3760df94f0
Differential Revision: https://phabricator.services.mozilla.com/D107790
404 lines
12 KiB
JavaScript
404 lines
12 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/. */
|
|
|
|
/* eslint no-shadow: error, mozilla/no-aArgs: error */
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
EngineURL: "resource://gre/modules/SearchEngine.jsm",
|
|
SearchEngine: "resource://gre/modules/SearchEngine.jsm",
|
|
SearchUtils: "resource://gre/modules/SearchUtils.jsm",
|
|
Services: "resource://gre/modules/Services.jsm",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "logConsole", () => {
|
|
return console.createInstance({
|
|
prefix: "OpenSearchEngine",
|
|
maxLogLevel: SearchUtils.loggingEnabled ? "Debug" : "Warn",
|
|
});
|
|
});
|
|
|
|
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 http://opensearch.a9.com/spec/1.1/description/
|
|
// gives the namespace names defined above, many existing OpenSearch engines
|
|
// are using the following versions. We therefore allow either.
|
|
const OPENSEARCH_NAMESPACES = [
|
|
OPENSEARCH_NS_11,
|
|
OPENSEARCH_NS_10,
|
|
"http://a9.com/-/spec/opensearchdescription/1.1/",
|
|
"http://a9.com/-/spec/opensearchdescription/1.0/",
|
|
];
|
|
|
|
const OPENSEARCH_LOCALNAME = "OpenSearchDescription";
|
|
|
|
const MOZSEARCH_NS_10 = "http://www.mozilla.org/2006/browser/search/";
|
|
const MOZSEARCH_LOCALNAME = "SearchPlugin";
|
|
|
|
/**
|
|
* Ensures an assertion is met before continuing. Should be used to indicate
|
|
* fatal errors.
|
|
* @param {*} assertion
|
|
* An assertion that must be met
|
|
* @param {string} message
|
|
* A message to display if the assertion is not met
|
|
* @param {number} resultCode
|
|
* The NS_ERROR_* value to throw if the assertion is not met
|
|
* @throws resultCode
|
|
* If the assertion fails.
|
|
*/
|
|
function ENSURE_WARN(assertion, message, resultCode) {
|
|
if (!assertion) {
|
|
throw Components.Exception(message, resultCode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* OpenSearchEngine represents an OpenSearch base search engine.
|
|
*/
|
|
class OpenSearchEngine extends SearchEngine {
|
|
// The data describing the engine, in the form of an XML document element.
|
|
_data = null;
|
|
|
|
constructor() {
|
|
super({
|
|
isAppProvided: false,
|
|
// We don't know what this is until after it has loaded, so add a placeholder.
|
|
loadPath: "[opensearch]loading",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieves the engine data from a URI. Initializes the engine, flushes to
|
|
* disk, and notifies the search service once initialization is complete.
|
|
*
|
|
* @param {string|nsIURI} uri
|
|
* The uri to load the search plugin from.
|
|
* @param {function} [callback]
|
|
* A callback to receive any details of errors.
|
|
*/
|
|
_install(uri, callback) {
|
|
let loadURI = uri instanceof Ci.nsIURI ? uri : SearchUtils.makeURI(uri);
|
|
if (!loadURI) {
|
|
throw Components.Exception(
|
|
loadURI,
|
|
"Must have URI when calling _install!",
|
|
Cr.NS_ERROR_UNEXPECTED
|
|
);
|
|
}
|
|
if (!/^https?$/i.test(loadURI.scheme)) {
|
|
throw Components.Exception(
|
|
"Invalid URI passed to SearchEngine constructor",
|
|
Cr.NS_ERROR_INVALID_ARG
|
|
);
|
|
}
|
|
|
|
logConsole.debug("_install: Downloading engine from:", loadURI.spec);
|
|
|
|
var chan = SearchUtils.makeChannel(loadURI);
|
|
|
|
if (this._engineToUpdate && chan instanceof Ci.nsIHttpChannel) {
|
|
var lastModified = this._engineToUpdate.getAttr("updatelastmodified");
|
|
if (lastModified) {
|
|
chan.setRequestHeader("If-Modified-Since", lastModified, false);
|
|
}
|
|
}
|
|
this._uri = loadURI;
|
|
|
|
var listener = new SearchUtils.LoadListener(
|
|
chan,
|
|
/(^text\/|xml$)/,
|
|
this._onLoad.bind(this, callback)
|
|
);
|
|
chan.notificationCallbacks = listener;
|
|
chan.asyncOpen(listener);
|
|
}
|
|
|
|
/**
|
|
* Handle the successful download of an engine. Initializes the engine and
|
|
* triggers parsing of the data. The engine is then flushed to disk. Notifies
|
|
* the search service once initialization is complete.
|
|
*
|
|
* @param {function} callback
|
|
* A callback to receive success or failure notifications. May be null.
|
|
* @param {array} bytes
|
|
* The loaded search engine data.
|
|
*/
|
|
_onLoad(callback, bytes) {
|
|
let onError = errorCode => {
|
|
if (this._engineToUpdate) {
|
|
logConsole.warn("Failed to update", this._engineToUpdate.name);
|
|
}
|
|
callback?.(errorCode);
|
|
};
|
|
|
|
if (!bytes) {
|
|
onError(Ci.nsISearchService.ERROR_DOWNLOAD_FAILURE);
|
|
return;
|
|
}
|
|
|
|
var parser = new DOMParser();
|
|
var doc = parser.parseFromBuffer(bytes, "text/xml");
|
|
this._data = doc.documentElement;
|
|
|
|
try {
|
|
this._initFromData();
|
|
} catch (ex) {
|
|
logConsole.error("_onLoad: Failed to init engine!", ex);
|
|
|
|
if (ex.result == Cr.NS_ERROR_FILE_CORRUPTED) {
|
|
onError(Ci.nsISearchService.ERROR_ENGINE_CORRUPTED);
|
|
} else {
|
|
onError(Ci.nsISearchService.ERROR_DOWNLOAD_FAILURE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this._engineToUpdate) {
|
|
let engineToUpdate = this._engineToUpdate.wrappedJSObject;
|
|
|
|
// Preserve metadata and loadPath.
|
|
Object.keys(engineToUpdate._metaData).forEach(key => {
|
|
this.setAttr(key, engineToUpdate.getAttr(key));
|
|
});
|
|
this._loadPath = engineToUpdate._loadPath;
|
|
|
|
// Keep track of the last modified date, so that we can make conditional
|
|
// requests for future updates.
|
|
this.setAttr("updatelastmodified", new Date().toUTCString());
|
|
|
|
// Set the new engine's icon, if it doesn't yet have one.
|
|
if (!this._iconURI && engineToUpdate._iconURI) {
|
|
this._iconURI = engineToUpdate._iconURI;
|
|
}
|
|
} else {
|
|
// Check that when adding a new engine (e.g., not updating an
|
|
// existing one), a duplicate engine does not already exist.
|
|
if (Services.search.getEngineByName(this.name)) {
|
|
onError(Ci.nsISearchService.ERROR_DUPLICATE_ENGINE);
|
|
logConsole.debug("_onLoad: duplicate engine found, bailing");
|
|
return;
|
|
}
|
|
|
|
this._loadPath = OpenSearchEngine.getAnonymizedLoadPath(
|
|
SearchUtils.sanitizeName(this.name),
|
|
this._uri
|
|
);
|
|
if (this._extensionID) {
|
|
this._loadPath += ":" + this._extensionID;
|
|
}
|
|
this.setAttr(
|
|
"loadPathHash",
|
|
SearchUtils.getVerificationHash(this._loadPath)
|
|
);
|
|
}
|
|
|
|
// Notify the search service of the successful load. It will deal with
|
|
// updates by checking this._engineToUpdate.
|
|
SearchUtils.notifyAction(this, SearchUtils.MODIFIED_TYPE.LOADED);
|
|
|
|
callback?.();
|
|
}
|
|
|
|
/**
|
|
* Initialize this Engine object from the collected data.
|
|
*/
|
|
_initFromData() {
|
|
ENSURE_WARN(
|
|
this._data,
|
|
"Can't init an engine with no data!",
|
|
Cr.NS_ERROR_UNEXPECTED
|
|
);
|
|
|
|
// Ensure we have a supported engine type before attempting to parse it.
|
|
let element = this._data;
|
|
if (
|
|
(element.localName == MOZSEARCH_LOCALNAME &&
|
|
element.namespaceURI == MOZSEARCH_NS_10) ||
|
|
(element.localName == OPENSEARCH_LOCALNAME &&
|
|
OPENSEARCH_NAMESPACES.includes(element.namespaceURI))
|
|
) {
|
|
logConsole.debug("Initing search plugin from", this._location);
|
|
|
|
this._parse();
|
|
} else {
|
|
Cu.reportError("Invalid search plugin due to namespace not matching.");
|
|
throw Components.Exception(
|
|
this._location + " is not a valid search plugin.",
|
|
Cr.NS_ERROR_FILE_CORRUPTED
|
|
);
|
|
}
|
|
// No need to keep a ref to our data (which in some cases can be a document
|
|
// element) past this point
|
|
this._data = null;
|
|
}
|
|
|
|
/**
|
|
* Extracts data from an OpenSearch URL element and creates an EngineURL
|
|
* object which is then added to the engine's list of URLs.
|
|
*
|
|
* @param {HTMLLinkElement} element
|
|
* The OpenSearch URL element.
|
|
* @throws NS_ERROR_FAILURE if a URL object could not be created.
|
|
*
|
|
* @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag.
|
|
* @see EngineURL()
|
|
*/
|
|
_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 = SearchUtils.URL_TYPE.SUGGEST_JSON;
|
|
}
|
|
|
|
try {
|
|
var url = new EngineURL(type, method, template);
|
|
} catch (ex) {
|
|
throw Components.Exception(
|
|
"_parseURL: failed to add " + template + " as a URL",
|
|
Cr.NS_ERROR_FAILURE
|
|
);
|
|
}
|
|
|
|
if (rels.length) {
|
|
url.rels = rels;
|
|
}
|
|
|
|
for (var i = 0; i < element.children.length; ++i) {
|
|
var param = element.children[i];
|
|
if (param.localName == "Param") {
|
|
try {
|
|
url.addParam(param.getAttribute("name"), param.getAttribute("value"));
|
|
} catch (ex) {
|
|
// Ignore failure
|
|
logConsole.error("_parseURL: Url element has an invalid param");
|
|
}
|
|
}
|
|
// Note: MozParams are not supported for OpenSearch engines as they
|
|
// cannot be app-provided engines.
|
|
}
|
|
|
|
this._urls.push(url);
|
|
}
|
|
|
|
/**
|
|
* Get the icon from an OpenSearch Image element.
|
|
*
|
|
* @param {HTMLLinkElement} element
|
|
* The OpenSearch URL element.
|
|
* @see http://opensearch.a9.com/spec/1.1/description/#image
|
|
*/
|
|
_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) {
|
|
logConsole.warn(
|
|
"OpenSearch image element must have positive width and height."
|
|
);
|
|
return;
|
|
}
|
|
|
|
this._setIcon(element.textContent, isPrefered, width, height);
|
|
}
|
|
|
|
/**
|
|
* Extract search engine information from the collected data to initialize
|
|
* the engine object.
|
|
*/
|
|
_parse() {
|
|
var doc = this._data;
|
|
|
|
for (var i = 0; i < doc.children.length; ++i) {
|
|
var child = doc.children[i];
|
|
switch (child.localName) {
|
|
case "ShortName":
|
|
this._name = child.textContent;
|
|
break;
|
|
case "Description":
|
|
this._description = child.textContent;
|
|
break;
|
|
case "Url":
|
|
try {
|
|
this._parseURL(child);
|
|
} catch (ex) {
|
|
// Parsing of the element failed, just skip it.
|
|
logConsole.error("Failed to parse URL child:", ex);
|
|
}
|
|
break;
|
|
case "Image":
|
|
this._parseImage(child);
|
|
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.
|
|
this._queryCharset = child.textContent;
|
|
break;
|
|
|
|
// Non-OpenSearch elements
|
|
case "SearchForm":
|
|
this._searchForm = child.textContent;
|
|
break;
|
|
case "UpdateUrl":
|
|
this._updateURL = child.textContent;
|
|
break;
|
|
case "UpdateInterval":
|
|
this._updateInterval = parseInt(child.textContent);
|
|
break;
|
|
case "IconUpdateUrl":
|
|
this._iconUpdateURL = child.textContent;
|
|
break;
|
|
case "ExtensionID":
|
|
this._extensionID = child.textContent;
|
|
break;
|
|
}
|
|
}
|
|
if (!this.name || !this._urls.length) {
|
|
throw Components.Exception(
|
|
"_parse: No name, or missing URL!",
|
|
Cr.NS_ERROR_FAILURE
|
|
);
|
|
}
|
|
if (!this.supportsResponseType(SearchUtils.URL_TYPE.SEARCH)) {
|
|
throw Components.Exception(
|
|
"_parse: No text/html result type!",
|
|
Cr.NS_ERROR_FAILURE
|
|
);
|
|
}
|
|
}
|
|
|
|
get _hasUpdates() {
|
|
// Whether or not the engine has an update URL
|
|
let selfURL = this._getURLOfType(SearchUtils.URL_TYPE.OPENSEARCH, "self");
|
|
return !!(this._updateURL || this._iconUpdateURL || selfURL);
|
|
}
|
|
|
|
// This indicates where we found the .xml file to load the engine,
|
|
// and attempts to hide user-identifiable data (such as username).
|
|
static getAnonymizedLoadPath(shortName, uri) {
|
|
return `[${uri.scheme}]${uri.host}/${shortName}.xml`;
|
|
}
|
|
}
|
|
|
|
var EXPORTED_SYMBOLS = ["OpenSearchEngine"];
|