diff options
Diffstat (limited to 'toolkit/components/search/AddonSearchEngine.sys.mjs')
-rw-r--r-- | toolkit/components/search/AddonSearchEngine.sys.mjs | 454 |
1 files changed, 454 insertions, 0 deletions
diff --git a/toolkit/components/search/AddonSearchEngine.sys.mjs b/toolkit/components/search/AddonSearchEngine.sys.mjs new file mode 100644 index 0000000000..4a469eaa02 --- /dev/null +++ b/toolkit/components/search/AddonSearchEngine.sys.mjs @@ -0,0 +1,454 @@ +/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { SearchEngine } from "resource://gre/modules/SearchEngine.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "logConsole", () => { + return console.createInstance({ + prefix: "AddonSearchEngine", + maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn", + }); +}); + +/** + * AddonSearchEngine represents a search engine defined by an add-on. + */ +export class AddonSearchEngine extends SearchEngine { + // Whether the engine is provided by the application. + #isAppProvided = false; + // The extension ID if added by an extension. + _extensionID = null; + // The locale, or "DEFAULT", if required. + _locale = null; + + /** + * Creates a AddonSearchEngine. + * + * @param {object} options + * The options object + * @param {boolean} options.isAppProvided + * Indicates whether the engine is provided by Firefox, either + * shipped in omni.ja or via Normandy. If it is, it will + * be treated as read-only. + * @param {object} [options.details] + * An object that simulates the manifest object from a WebExtension. + * @param {object} [options.json] + * An object that represents the saved JSON settings for the engine. + */ + constructor({ isAppProvided, details, json } = {}) { + let extensionId = + details?.extensionID ?? json.extensionID ?? json._extensionID; + let id = extensionId + (details?.locale ?? json._locale); + + super({ + loadPath: "[addon]" + extensionId, + id, + }); + + this._extensionID = extensionId; + this.#isAppProvided = isAppProvided; + + if (json) { + this._initWithJSON(json); + } + } + + _initWithJSON(json) { + super._initWithJSON(json); + this._extensionID = json.extensionID || json._extensionID || null; + this._locale = json.extensionLocale || json._locale || null; + } + + /** + * Call to initalise an instance with extension details. Does not need to be + * called if json has been passed to the constructor. + * + * @param {object} options + * The options object. + * @param {Extension} options.extension + * The extension object representing the add-on. + * @param {object} options.locale + * The locale to use from the extension for getting details of the search + * engine. + * @param {object} [options.config] + * The search engine configuration for application provided engines, that + * may be overriding some of the WebExtension's settings. + */ + async init({ extension, locale, config }) { + let { baseURI, manifest } = await this.#getExtensionDetailsForLocale( + extension, + locale + ); + + this.#initFromManifest(baseURI, manifest, locale, config); + } + + /** + * Manages updates to this engine. + * + * @param {object} options + * The options object. + * @param {object} [options.configuration] + * The search engine configuration for application provided engines, that + * may be overriding some of the WebExtension's settings. + * @param {object} [options.extension] + * The extension associated with this search engine, if known. + * @param {object} [options.manifest] + * The extension's manifest associated with this search engine, if known. + * @param {string} [options.locale] + * The locale to use from the extension for getting details of the search + * engine. + */ + async update({ configuration, extension, manifest, locale } = {}) { + let baseURI = extension?.baseURI; + if (!manifest) { + ({ baseURI, manifest } = await this.#getExtensionDetailsForLocale( + extension, + locale + )); + } + let originalName = this.name; + let name = manifest.chrome_settings_overrides.search_provider.name.trim(); + if (originalName != name && Services.search.getEngineByName(name)) { + throw new Error("Can't upgrade to the same name as an existing engine"); + } + + this.#updateFromManifest(baseURI, manifest, locale, configuration); + } + + /** + * This will update the add-on search engine if there is no name change. + * + * @param {object} options + * The options object. + * @param {object} [options.configuration] + * The search engine configuration for application provided engines, that + * may be overriding some of the WebExtension's settings. + * @param {string} [options.locale] + * The locale to use from the extension for getting details of the search + * engine. + * @returns {boolean} + * Returns true if the engine was updated, false otherwise. + */ + async updateIfNoNameChange({ configuration, locale }) { + let { baseURI, manifest } = await this.#getExtensionDetailsForLocale( + null, + locale + ); + + if ( + this.name != + manifest.chrome_settings_overrides.search_provider.name.trim() + ) { + return false; + } + + this.#updateFromManifest(baseURI, manifest, locale, configuration); + return true; + } + + /** + * Whether or not this engine is provided by the application, e.g. it is + * in the list of configured search engines. Overrides the definition in + * `SearchEngine`. + * + * @returns {boolean} + */ + get isAppProvided() { + return this.#isAppProvided; + } + + /** + * Whether or not this engine is an in-memory only search engine. + * These engines are typically application provided or policy engines, + * where they are loaded every time on SearchService initialization + * using the policy JSON or the extension manifest. Minimal details of the + * in-memory engines are saved to disk, but they are never loaded + * from the user's saved settings file. + * + * @returns {boolean} + * Only returns true for application provided engines. + */ + get inMemory() { + return this.#isAppProvided; + } + + get isGeneralPurposeEngine() { + return !!( + this._extensionID && + lazy.SearchUtils.GENERAL_SEARCH_ENGINE_IDS.has(this._extensionID) + ); + } + + /** + * Creates a JavaScript object that represents this engine. + * + * @returns {object} + * An object suitable for serialization as JSON. + */ + toJSON() { + // For built-in engines we don't want to store all their data in the settings + // file so just store the relevant metadata. + if (this.#isAppProvided) { + return { + id: this.id, + _name: this.name, + _isAppProvided: true, + _metaData: this._metaData, + }; + } + let json = super.toJSON(); + json._extensionID = this._extensionID; + json._locale = this._locale; + return json; + } + + /** + * Checks to see if this engine's settings are in sync with what the add-on + * manager has, and reports the results to telemetry. + */ + async checkAndReportIfSettingsValid() { + let addon = await lazy.AddonManager.getAddonByID(this._extensionID); + + if (!addon) { + lazy.logConsole.debug( + `Add-on ${this._extensionID} for search engine ${this.name} is not installed!` + ); + Services.telemetry.keyedScalarSet( + "browser.searchinit.engine_invalid_webextension", + this._extensionID, + 1 + ); + } else if (!addon.isActive) { + lazy.logConsole.debug( + `Add-on ${this._extensionID} for search engine ${this.name} is not active!` + ); + Services.telemetry.keyedScalarSet( + "browser.searchinit.engine_invalid_webextension", + this._extensionID, + 2 + ); + } else { + let policy = await AddonSearchEngine.getWebExtensionPolicy( + this._extensionID + ); + let providerSettings = + policy.extension.manifest?.chrome_settings_overrides?.search_provider; + + if (!providerSettings) { + lazy.logConsole.debug( + `Add-on ${this._extensionID} for search engine ${this.name} no longer has an engine defined` + ); + Services.telemetry.keyedScalarSet( + "browser.searchinit.engine_invalid_webextension", + this._extensionID, + 4 + ); + } else if (this.name != providerSettings.name) { + lazy.logConsole.debug( + `Add-on ${this._extensionID} for search engine ${this.name} has a different name!` + ); + Services.telemetry.keyedScalarSet( + "browser.searchinit.engine_invalid_webextension", + this._extensionID, + 5 + ); + } else if (!this.checkSearchUrlMatchesManifest(providerSettings)) { + lazy.logConsole.debug( + `Add-on ${this._extensionID} for search engine ${this.name} has out-of-date manifest!` + ); + Services.telemetry.keyedScalarSet( + "browser.searchinit.engine_invalid_webextension", + this._extensionID, + 6 + ); + } + } + } + + /** + * Initializes the engine based on the manifest and other values. + * + * @param {string} extensionBaseURI + * The Base URI of the WebExtension. + * @param {object} manifest + * An object representing the WebExtensions' manifest. + * @param {string} locale + * The locale that is being used for the WebExtension. + * @param {object} [configuration] + * The search engine configuration for application provided engines, that + * may be overriding some of the WebExtension's settings. + */ + #initFromManifest(extensionBaseURI, manifest, locale, configuration = {}) { + let searchProvider = manifest.chrome_settings_overrides.search_provider; + + this._locale = locale; + + // We only set _telemetryId for app-provided engines. See also telemetryId + // getter. + if (this.#isAppProvided) { + if (configuration.telemetryId) { + this._telemetryId = configuration.telemetryId; + } else { + let telemetryId = this._extensionID.split("@")[0]; + if (locale != lazy.SearchUtils.DEFAULT_TAG) { + telemetryId += "-" + locale; + } + this._telemetryId = telemetryId; + } + } + + // Set the main icon URL for the engine. + let iconURL = searchProvider.favicon_url; + + if (!iconURL) { + iconURL = + manifest.icons && + extensionBaseURI.resolve( + lazy.ExtensionParent.IconDetails.getPreferredIcon(manifest.icons).icon + ); + } + + // Record other icons that the WebExtension has. + if (manifest.icons) { + let iconList = Object.entries(manifest.icons).map(icon => { + return { + width: icon[0], + height: icon[0], + url: extensionBaseURI.resolve(icon[1]), + }; + }); + for (let icon of iconList) { + this._addIconToMap(icon.size, icon.size, icon.url); + } + } + + // Filter out any untranslated parameters, the extension has to list all + // possible mozParams for each engine where a 'locale' may only provide + // actual values for some (or none). + if (searchProvider.params) { + searchProvider.params = searchProvider.params.filter(param => { + return !(param.value && param.value.startsWith("__MSG_")); + }); + } + + this._initWithDetails( + { ...searchProvider, iconURL, description: manifest.description }, + configuration + ); + } + + /** + * Update this engine based on new manifest, used during + * webextension upgrades. + * + * @param {string} extensionBaseURI + * The Base URI of the WebExtension. + * @param {object} manifest + * An object representing the WebExtensions' manifest. + * @param {string} locale + * The locale that is being used for the WebExtension. + * @param {object} [configuration] + * The search engine configuration for application provided engines, that + * may be overriding some of the WebExtension's settings. + */ + #updateFromManifest(extensionBaseURI, manifest, locale, configuration = {}) { + this._urls = []; + this._iconMapObj = null; + this.#initFromManifest(extensionBaseURI, manifest, locale, configuration); + lazy.SearchUtils.notifyAction(this, lazy.SearchUtils.MODIFIED_TYPE.CHANGED); + } + + /** + * Get the localized manifest from the WebExtension for the given locale or + * manifest default locale. + * + * The search service configuration overloads the add-on manager concepts of + * locales, and forces particular locales within the WebExtension to be used, + * ignoring the user's current locale. The user's current locale is taken into + * account within the configuration, just not in the WebExtension. + * + * @param {object} [extension] + * The extension to get the manifest from. + * @param {string} locale + * The locale to load from the WebExtension. If this is `DEFAULT_TAG`, then + * the default locale is loaded. + * @returns {object} + * The loaded manifest. + */ + async #getExtensionDetailsForLocale(extension, locale) { + // If we haven't been passed an extension object, then go and find it. + if (!extension) { + extension = ( + await AddonSearchEngine.getWebExtensionPolicy(this._extensionID) + ).extension; + } + + let manifest = extension.manifest; + + let localeToLoad; + if (this.#isAppProvided) { + // If the locale we want from the WebExtension is the extension's default + // then we get that from the manifest here. We do this because if we + // are reloading due to the locale change, the add-on manager might not + // have updated the WebExtension's manifest to the new version by the + // time we hit this code. + localeToLoad = + locale == lazy.SearchUtils.DEFAULT_TAG + ? manifest.default_locale + : locale; + } else { + // For user installed add-ons, we have to simulate the add-on manager + // code for loading the correct locale. + // We do this, as in the case of a live language switch, the add-on manager + // may not have yet reloaded the extension, and there's no way for us to + // listen for that reload to complete. + // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1781768#c3 for + // more background. + localeToLoad = Services.locale.negotiateLanguages( + Services.locale.appLocalesAsBCP47, + [...extension.localeData.locales.keys()], + manifest.default_locale + )[0]; + } + + if (localeToLoad) { + manifest = await extension.getLocalizedManifest(localeToLoad); + } + return { baseURI: extension.baseURI, manifest }; + } + + /** + * Gets the WebExtensionPolicy for an add-on. + * + * @param {string} id + * The WebExtension id. + * @returns {WebExtensionPolicy} + */ + static async getWebExtensionPolicy(id) { + let policy = WebExtensionPolicy.getByID(id); + if (!policy) { + let idPrefix = id.split("@")[0]; + let path = `resource://search-extensions/${idPrefix}/`; + await lazy.AddonManager.installBuiltinAddon(path); + policy = WebExtensionPolicy.getByID(id); + } + // On startup the extension may have not finished parsing the + // manifest, wait for that here. + await policy.readyPromise; + return policy; + } +} |