summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/OpenSearchEngine.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/search/OpenSearchEngine.sys.mjs')
-rw-r--r--toolkit/components/search/OpenSearchEngine.sys.mjs257
1 files changed, 257 insertions, 0 deletions
diff --git a/toolkit/components/search/OpenSearchEngine.sys.mjs b/toolkit/components/search/OpenSearchEngine.sys.mjs
new file mode 100644
index 0000000000..e1b9b976fb
--- /dev/null
+++ b/toolkit/components/search/OpenSearchEngine.sys.mjs
@@ -0,0 +1,257 @@
+/* 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 */
+
+import {
+ EngineURL,
+ SearchEngine,
+} from "resource://gre/modules/SearchEngine.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ loadAndParseOpenSearchEngine:
+ "resource://gre/modules/OpenSearchLoader.sys.mjs",
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
+ return console.createInstance({
+ prefix: "OpenSearchEngine",
+ maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
+ });
+});
+
+// The default engine update interval, in days. This is only used if an engine
+// specifies an updateURL, but not an updateInterval.
+const OPENSEARCH_DEFAULT_UPDATE_INTERVAL = 7;
+
+/**
+ * OpenSearchEngine represents an OpenSearch base search engine.
+ */
+export class OpenSearchEngine extends SearchEngine {
+ // The data describing the engine, in the form of an XML document element.
+ _data = null;
+ // The number of days between update checks for new versions
+ _updateInterval = null;
+ // The url to check at for a new update
+ _updateURL = null;
+ // The url to check for a new icon
+ _iconUpdateURL = null;
+
+ /**
+ * Creates a OpenSearchEngine.
+ *
+ * @param {object} [options]
+ * The options object
+ * @param {object} [options.json]
+ * An object that represents the saved JSON settings for the engine.
+ * @param {OpenSearchProperties} [options.engineData]
+ * The engine data for this search engine that will have been loaded via
+ * `OpenSearchLoader`.
+ */
+ constructor(options = {}) {
+ super({
+ loadPath:
+ options.json?._loadPath ??
+ OpenSearchEngine.getAnonymizedLoadPath(
+ lazy.SearchUtils.sanitizeName(options.engineData.name),
+ options.engineData.installURL
+ ),
+ });
+
+ if (options.engineData) {
+ this.#setEngineData(options.engineData);
+
+ // As this is a new engine, we must set the verification hash for the load
+ // path set in the constructor.
+ this.setAttr(
+ "loadPathHash",
+ lazy.SearchUtils.getVerificationHash(this._loadPath)
+ );
+
+ if (this.hasUpdates) {
+ this.#setNextUpdateTime();
+ }
+ } else {
+ this._initWithJSON(options.json);
+ this._updateInterval = options.json._updateInterval ?? null;
+ this._updateURL = options.json._updateURL ?? null;
+ this._iconUpdateURL = options.json._iconUpdateURL ?? null;
+ }
+ }
+
+ /**
+ * Creates a JavaScript object that represents this engine.
+ *
+ * @returns {object}
+ * An object suitable for serialization as JSON.
+ */
+ toJSON() {
+ let json = super.toJSON();
+ json._updateInterval = this._updateInterval;
+ json._updateURL = this._updateURL;
+ json._iconUpdateURL = this._iconUpdateURL;
+ return json;
+ }
+
+ /**
+ * Determines if this search engine has updates url.
+ *
+ * @returns {boolean}
+ * Returns true if this search engine may update itself.
+ */
+ get hasUpdates() {
+ // Whether or not the engine has an update URL
+ let selfURL = this._getURLOfType(
+ lazy.SearchUtils.URL_TYPE.OPENSEARCH,
+ "self"
+ );
+ return !!(this._updateURL || this._iconUpdateURL || selfURL);
+ }
+
+ /**
+ * Returns the engine's updateURI if it exists and returns null otherwise
+ *
+ * @returns {?string}
+ */
+ get updateURI() {
+ let updateURL = this._getURLOfType(lazy.SearchUtils.URL_TYPE.OPENSEARCH);
+ let updateURI =
+ updateURL && updateURL._hasRelation("self")
+ ? updateURL.getSubmission("", this).uri
+ : lazy.SearchUtils.makeURI(this._updateURL);
+ return updateURI;
+ }
+
+ /**
+ * Considers if this engine needs to be updated, and updates it if necessary.
+ */
+ async maybeUpdate() {
+ if (!this.hasUpdates) {
+ return;
+ }
+
+ let currentTime = Date.now();
+
+ let expireTime = this.getAttr("updateexpir");
+
+ if (!expireTime || !(expireTime <= currentTime)) {
+ lazy.logConsole.debug(this.name, "Skipping update, not expired yet.");
+ return;
+ }
+
+ await this.#update();
+
+ this.#setNextUpdateTime();
+ }
+
+ /**
+ * Updates the OpenSearch engine details from the server.
+ */
+ async #update() {
+ let updateURI = this.updateURI;
+ if (updateURI) {
+ let data = await lazy.loadAndParseOpenSearchEngine(
+ updateURI,
+ this.getAttr("updatelastmodified")
+ );
+
+ this.#setEngineData(data);
+
+ lazy.SearchUtils.notifyAction(
+ this,
+ lazy.SearchUtils.MODIFIED_TYPE.CHANGED
+ );
+
+ // Keep track of the last modified date, so that we can make conditional
+ // server requests for future updates.
+ this.setAttr("updatelastmodified", new Date().toUTCString());
+ }
+
+ if (this._iconUpdateURL) {
+ // Force update of the icon from the icon URL.
+ this._setIcon(this._iconUpdateURL, true);
+ }
+ }
+
+ /**
+ * Sets the data for this engine based on the OpenSearch properties.
+ *
+ * @param {OpenSearchProperties} data
+ * The OpenSearch data.
+ */
+ #setEngineData(data) {
+ let name = data.name.trim();
+ if (!this._engineToUpdate) {
+ if (Services.search.getEngineByName(name)) {
+ throw Components.Exception(
+ "Found a duplicate engine",
+ Ci.nsISearchService.ERROR_DUPLICATE_ENGINE
+ );
+ }
+ }
+
+ this._name = name;
+ this._description = data.description ?? "";
+ this._searchForm = data.searchForm ?? "";
+ this._queryCharset = data.queryCharset ?? "UTF-8";
+
+ for (let url of data.urls) {
+ let engineURL;
+ try {
+ engineURL = new EngineURL(url.type, url.method, url.template);
+ } catch (ex) {
+ throw Components.Exception(
+ `Failed to add ${url.template} as an Engine URL`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ if (url.rels.length) {
+ engineURL.rels = url.rels;
+ }
+
+ for (let param of url.params) {
+ try {
+ engineURL.addParam(param.name, param.value);
+ } catch (ex) {
+ // Ignore failure
+ lazy.logConsole.error("OpenSearch url has an invalid param", param);
+ }
+ }
+
+ this._urls.push(engineURL);
+ }
+
+ for (let image of data.images) {
+ this._setIcon(image.url, image.isPrefered, image.width, image.height);
+ }
+ }
+
+ /**
+ * Sets the next update time for this engine.
+ */
+ #setNextUpdateTime() {
+ var interval = this._updateInterval || OPENSEARCH_DEFAULT_UPDATE_INTERVAL;
+ var milliseconds = interval * 86400000; // |interval| is in days
+ this.setAttr("updateexpir", Date.now() + milliseconds);
+ }
+
+ /**
+ * This indicates where we found the .xml file to load the engine,
+ * and attempts to hide user-identifiable data (such as username).
+ *
+ * @param {string} sanitizedName
+ * The sanitized name of the engine.
+ * @param {nsIURI} uri
+ * The uri the engine was loaded from.
+ * @returns {string}
+ * A load path with reduced data.
+ */
+ static getAnonymizedLoadPath(sanitizedName, uri) {
+ return `[${uri.scheme}]${uri.host}/${sanitizedName}.xml`;
+ }
+}