summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/SearchService.jsm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/search/SearchService.jsm
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/search/SearchService.jsm')
-rw-r--r--toolkit/components/search/SearchService.jsm2838
1 files changed, 2838 insertions, 0 deletions
diff --git a/toolkit/components/search/SearchService.jsm b/toolkit/components/search/SearchService.jsm
new file mode 100644
index 0000000000..f9083e0213
--- /dev/null
+++ b/toolkit/components/search/SearchService.jsm
@@ -0,0 +1,2838 @@
+/* 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"
+);
+const { PromiseUtils } = ChromeUtils.import(
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ AddonManager: "resource://gre/modules/AddonManager.jsm",
+ IgnoreLists: "resource://gre/modules/IgnoreLists.jsm",
+ OpenSearchEngine: "resource://gre/modules/OpenSearchEngine.jsm",
+ OS: "resource://gre/modules/osfile.jsm",
+ Region: "resource://gre/modules/Region.jsm",
+ RemoteSettings: "resource://services-settings/remote-settings.js",
+ SearchEngine: "resource://gre/modules/SearchEngine.jsm",
+ SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.jsm",
+ SearchSettings: "resource://gre/modules/SearchSettings.jsm",
+ SearchStaticData: "resource://gre/modules/SearchStaticData.jsm",
+ SearchUtils: "resource://gre/modules/SearchUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gExperiment",
+ SearchUtils.BROWSER_SEARCH_PREF + "experiment",
+ false,
+ () => {
+ Services.search.wrappedJSObject._maybeReloadEngines();
+ }
+);
+
+XPCOMUtils.defineLazyGetter(this, "logConsole", () => {
+ return console.createInstance({
+ prefix: "SearchService",
+ maxLogLevel: SearchUtils.loggingEnabled ? "Debug" : "Warn",
+ });
+});
+
+const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
+const QUIT_APPLICATION_TOPIC = "quit-application";
+
+// The default engine update interval, in days. This is only used if an engine
+// specifies an updateURL, but not an updateInterval.
+const SEARCH_DEFAULT_UPDATE_INTERVAL = 7;
+
+// This is the amount of time we'll be idle for before applying any configuration
+// changes.
+const RECONFIG_IDLE_TIME_SEC = 5 * 60;
+
+// nsISearchParseSubmissionResult
+function ParseSubmissionResult(
+ engine,
+ terms,
+ termsParameterName,
+ termsOffset,
+ termsLength
+) {
+ this._engine = engine;
+ this._terms = terms;
+ this._termsParameterName = termsParameterName;
+ this._termsOffset = termsOffset;
+ this._termsLength = termsLength;
+}
+ParseSubmissionResult.prototype = {
+ get engine() {
+ return this._engine;
+ },
+ get terms() {
+ return this._terms;
+ },
+ get termsParameterName() {
+ return this._termsParameterName;
+ },
+ get termsOffset() {
+ return this._termsOffset;
+ },
+ get termsLength() {
+ return this._termsLength;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsISearchParseSubmissionResult"]),
+};
+
+const gEmptyParseSubmissionResult = Object.freeze(
+ new ParseSubmissionResult(null, "", "", -1, 0)
+);
+
+/**
+ * The search service handles loading and maintaining of search engines. It will
+ * also work out the default lists for each locale/region.
+ *
+ * @implements {nsISearchService}
+ */
+function SearchService() {
+ this._initObservers = PromiseUtils.defer();
+ this._engines = new Map();
+ this._settings = new SearchSettings(this);
+}
+
+SearchService.prototype = {
+ classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"),
+
+ // The current status of initialization. Note that it does not determine if
+ // initialization is complete, only if an error has been encountered so far.
+ _initRV: Cr.NS_OK,
+
+ // The boolean indicates that the initialization has started or not.
+ _initStarted: false,
+
+ // The boolean that indicates if initialization has been completed (successful
+ // or not).
+ _initialized: false,
+
+ // Indicates if we're already waiting for maybeReloadEngines to be called.
+ _maybeReloadDebounce: false,
+
+ // Indicates if we're currently in maybeReloadEngines.
+ _reloadingEngines: false,
+
+ // The engine selector singleton that is managing the engine configuration.
+ _engineSelector: null,
+
+ /**
+ * Various search engines may be ignored if their submission urls contain a
+ * string that is in the list. The list is controlled via remote settings.
+ */
+ _submissionURLIgnoreList: [],
+
+ /**
+ * Various search engines may be ignored if their load path is contained
+ * in this list. The list is controlled via remote settings.
+ */
+ _loadPathIgnoreList: [],
+
+ /**
+ * A map of engine display names to `SearchEngine`.
+ */
+ _engines: null,
+
+ /**
+ * An array of engine short names sorted into display order.
+ */
+ __sortedEngines: null,
+
+ /**
+ * A flag to prevent setting of useSavedOrder when there's non-user
+ * activity happening.
+ */
+ _dontSetUseSavedOrder: false,
+
+ /**
+ * An object containing the {id, locale} of the WebExtension for the default
+ * engine, as suggested by the configuration.
+ * For the legacy configuration, this is the user visible name.
+ */
+ _searchDefault: null,
+
+ /**
+ * An object containing the {id, locale} of the WebExtension for the default
+ * engine for private browsing mode, as suggested by the configuration.
+ * For the legacy configuration, this is the user visible name.
+ */
+ _searchPrivateDefault: null,
+
+ /**
+ * A Set of installed search extensions reported by AddonManager
+ * startup before SearchSevice has started. Will be installed
+ * during init().
+ */
+ _startupExtensions: new Set(),
+
+ /**
+ * A Set of removed search extensions reported by AddonManager
+ * startup before SearchSevice has started. Will be removed
+ * during init().
+ */
+ _startupRemovedExtensions: new Set(),
+
+ // A reference to the handler for the default override allow list.
+ _defaultOverrideAllowlist: null,
+
+ // This reflects the combined values of the prefs for enabling the separate
+ // private default UI, and for the user choosing a separate private engine.
+ // If either one is disabled, then we don't enable the separate private default.
+ get _separatePrivateDefault() {
+ return (
+ this._separatePrivateDefaultPrefValue &&
+ this._separatePrivateDefaultEnabledPrefValue
+ );
+ },
+
+ // If initialization has not been completed yet, perform synchronous
+ // initialization.
+ // Throws in case of initialization error.
+ _ensureInitialized() {
+ if (this._initialized) {
+ if (!Components.isSuccessCode(this._initRV)) {
+ logConsole.debug("_ensureInitialized: failure");
+ throw Components.Exception(
+ "SearchService previously failed to initialize",
+ this._initRV
+ );
+ }
+ return;
+ }
+
+ let err = new Error(
+ "Something tried to use the search service before it's been " +
+ "properly intialized. Please examine the stack trace to figure out what and " +
+ "where to fix it:\n"
+ );
+ err.message += err.stack;
+ throw err;
+ },
+
+ /**
+ * Asynchronous implementation of the initializer.
+ *
+ * @returns {number}
+ * A Components.results success code on success, otherwise a failure code.
+ */
+ async _init() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_separatePrivateDefaultPrefValue",
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ false,
+ this._onSeparateDefaultPrefChanged.bind(this)
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_separatePrivateDefaultEnabledPrefValue",
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
+ false,
+ this._onSeparateDefaultPrefChanged.bind(this)
+ );
+
+ // We need to catch the region being updated
+ // during initialisation so we start listening
+ // straight away.
+ Services.obs.addObserver(this, Region.REGION_TOPIC);
+
+ try {
+ // Create the search engine selector.
+ this._engineSelector = new SearchEngineSelector(
+ this._handleConfigurationUpdated.bind(this)
+ );
+
+ // See if we have a settings file so we don't have to parse a bunch of XML.
+ let settings = await this._settings.get();
+
+ this._setupRemoteSettings().catch(Cu.reportError);
+
+ await this._loadEngines(settings);
+
+ // If we've got this far, but the application is now shutting down,
+ // then we need to abandon any further work, especially not writing
+ // the settings. We do this, because the add-on manager has also
+ // started shutting down and as a result, we might have an incomplete
+ // picture of the installed search engines. Writing the settings at
+ // this stage would potentially mean the user would loose their engine
+ // data.
+ // We will however, rebuild the settings on next start up if we detect
+ // it is necessary.
+ if (Services.startup.shuttingDown) {
+ logConsole.warn("_init: abandoning init due to shutting down");
+ this._initRV = Cr.NS_ERROR_ABORT;
+ this._initObservers.reject(this._initRV);
+ return this._initRV;
+ }
+
+ // Make sure the current list of engines is persisted, without the need to wait.
+ logConsole.debug("_init: engines loaded, writing settings");
+ this._addObservers();
+ } catch (ex) {
+ this._initRV = ex.result !== undefined ? ex.result : Cr.NS_ERROR_FAILURE;
+ logConsole.error("_init: failure initializing search:", ex.result);
+ }
+
+ this._initialized = true;
+ if (Components.isSuccessCode(this._initRV)) {
+ this._initObservers.resolve(this._initRV);
+ } else {
+ this._initObservers.reject(this._initRV);
+ }
+ Services.obs.notifyObservers(
+ null,
+ SearchUtils.TOPIC_SEARCH_SERVICE,
+ "init-complete"
+ );
+
+ logConsole.debug("Completed _init");
+ return this._initRV;
+ },
+
+ /**
+ * Obtains the remote settings for the search service. This should only be
+ * called from init(). Any subsequent updates to the remote settings are
+ * handled via a sync listener.
+ *
+ * For desktop, the initial remote settings are obtained from dumps in
+ * `services/settings/dumps/main/`.
+ *
+ * When enabling for Android, be aware the dumps are not shipped there, and
+ * hence the `get` may take a while to return.
+ */
+ async _setupRemoteSettings() {
+ // Now we have the values, listen for future updates.
+ let listener = this._handleIgnoreListUpdated.bind(this);
+
+ const current = await IgnoreLists.getAndSubscribe(listener);
+ // Only save the listener after the subscribe, otherwise for tests it might
+ // not be fully set up by the time we remove it again.
+ this._ignoreListListener = listener;
+
+ await this._handleIgnoreListUpdated({ data: { current } });
+ Services.obs.notifyObservers(
+ null,
+ SearchUtils.TOPIC_SEARCH_SERVICE,
+ "settings-update-complete"
+ );
+ },
+
+ /**
+ * This handles updating of the ignore list settings, and removing any ignored
+ * engines.
+ *
+ * @param {object} eventData
+ * The event in the format received from RemoteSettings.
+ */
+ async _handleIgnoreListUpdated(eventData) {
+ logConsole.debug("_handleIgnoreListUpdated");
+ const {
+ data: { current },
+ } = eventData;
+
+ for (const entry of current) {
+ if (entry.id == "load-paths") {
+ this._loadPathIgnoreList = [...entry.matches];
+ } else if (entry.id == "submission-urls") {
+ this._submissionURLIgnoreList = [...entry.matches];
+ }
+ }
+
+ // If we have not finished initializing, then we wait for the initialization
+ // to complete.
+ if (!this.isInitialized) {
+ await this._initObservers;
+ }
+ // We try to remove engines manually, as this should be more efficient and
+ // we don't really want to cause a re-init as this upsets unit tests.
+ let engineRemoved = false;
+ for (let engine of this._engines.values()) {
+ if (this._engineMatchesIgnoreLists(engine)) {
+ await this.removeEngine(engine);
+ engineRemoved = true;
+ }
+ }
+ // If we've removed an engine, and we don't have any left, we need to
+ // reload the engines - it is possible the settings just had one engine in it,
+ // and that is now empty, so we need to load from our main list.
+ if (engineRemoved && !this._engines.size) {
+ this._maybeReloadEngines().catch(Cu.reportError);
+ }
+ },
+
+ /**
+ * Determines if a given engine matches the ignorelists or not.
+ *
+ * @param {Engine} engine
+ * The engine to check against the ignorelists.
+ * @returns {boolean}
+ * Returns true if the engine matches a ignorelists entry.
+ */
+ _engineMatchesIgnoreLists(engine) {
+ if (this._loadPathIgnoreList.includes(engine._loadPath)) {
+ return true;
+ }
+ let url = engine
+ ._getURLOfType("text/html")
+ .getSubmission("dummy", engine)
+ .uri.spec.toLowerCase();
+ if (
+ this._submissionURLIgnoreList.some(code =>
+ url.includes(code.toLowerCase())
+ )
+ ) {
+ return true;
+ }
+ return false;
+ },
+
+ async maybeSetAndOverrideDefault(extension) {
+ let searchProvider =
+ extension.manifest.chrome_settings_overrides.search_provider;
+ let engine = this._engines.get(searchProvider.name);
+ if (!engine || !engine.isAppProvided || engine.hidden) {
+ // If the engine is not application provided, then we shouldn't simply
+ // set default to it.
+ // If the engine is application provided, but hidden, then we don't
+ // switch to it, nor do we try to install it.
+ return {
+ canChangeToAppProvided: false,
+ canInstallEngine: !engine?.hidden,
+ };
+ }
+
+ if (!this._defaultOverrideAllowlist) {
+ this._defaultOverrideAllowlist = new SearchDefaultOverrideAllowlistHandler();
+ }
+
+ if (
+ extension.startupReason === "ADDON_INSTALL" ||
+ extension.startupReason === "ADDON_ENABLE"
+ ) {
+ // Don't allow an extension to set the default if it is already the default.
+ if (this.defaultEngine.name == searchProvider.name) {
+ return {
+ canChangeToAppProvided: false,
+ canInstallEngine: false,
+ };
+ }
+ if (
+ !(await this._defaultOverrideAllowlist.canOverride(
+ extension,
+ engine._extensionID
+ ))
+ ) {
+ logConsole.debug(
+ "Allowing default engine to be set to app-provided.",
+ extension.id
+ );
+ // We don't allow overriding the engine in this case, but we can allow
+ // the extension to change the default engine.
+ return {
+ canChangeToAppProvided: true,
+ canInstallEngine: false,
+ };
+ }
+ // We're ok to override.
+ engine.overrideWithExtension(extension.id, extension.manifest);
+ logConsole.debug(
+ "Allowing default engine to be set to app-provided and overridden.",
+ extension.id
+ );
+ return {
+ canChangeToAppProvided: true,
+ canInstallEngine: false,
+ };
+ }
+
+ if (
+ engine.getAttr("overriddenBy") == extension.id &&
+ (await this._defaultOverrideAllowlist.canOverride(
+ extension,
+ engine._extensionID
+ ))
+ ) {
+ engine.overrideWithExtension(extension.id, extension.manifest);
+ logConsole.debug(
+ "Re-enabling overriding of core extension by",
+ extension.id
+ );
+ return {
+ canChangeToAppProvided: true,
+ canInstallEngine: false,
+ };
+ }
+
+ return {
+ canChangeToAppProvided: false,
+ canInstallEngine: false,
+ };
+ },
+
+ /**
+ * Handles the search configuration being - adds a wait on the user
+ * being idle, before the search engine update gets handled.
+ */
+ _handleConfigurationUpdated() {
+ if (this._queuedIdle) {
+ return;
+ }
+
+ this._queuedIdle = true;
+
+ this.idleService.addIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
+ },
+
+ get _sortedEngines() {
+ if (!this.__sortedEngines) {
+ return this._buildSortedEngineList();
+ }
+ return this.__sortedEngines;
+ },
+
+ /**
+ * Returns the engine that is the default for this locale/region, ignoring any
+ * user changes to the default engine.
+ *
+ * @param {boolean} privateMode
+ * Set to true to return the default engine in private mode,
+ * false for normal mode.
+ * @returns {SearchEngine}
+ * The engine that is default.
+ */
+ _originalDefaultEngine(privateMode = false) {
+ let defaultEngine = this._getEngineByWebExtensionDetails(
+ privateMode && this._searchPrivateDefault
+ ? this._searchPrivateDefault
+ : this._searchDefault
+ );
+
+ if (defaultEngine) {
+ return defaultEngine;
+ }
+
+ if (privateMode) {
+ // If for some reason we can't find the private mode engine, fall back
+ // to the non-private one.
+ return this._originalDefaultEngine(false);
+ }
+
+ // Something unexpected as happened. In order to recover the original
+ // default engine, use the first visible engine which is the best we can do.
+ return this._getSortedEngines(false)[0];
+ },
+
+ /**
+ * @returns {SearchEngine}
+ * The engine that is the default for this locale/region, ignoring any
+ * user changes to the default engine.
+ */
+ get originalDefaultEngine() {
+ return this._originalDefaultEngine();
+ },
+
+ /**
+ * @returns {SearchEngine}
+ * The engine that is the default for this locale/region in private browsing
+ * mode, ignoring any user changes to the default engine.
+ * Note: if there is no default for this locale/region, then the non-private
+ * browsing engine will be returned.
+ */
+ get originalPrivateDefaultEngine() {
+ return this._originalDefaultEngine(this._separatePrivateDefault);
+ },
+
+ resetToOriginalDefaultEngine() {
+ let originalDefaultEngine = this.originalDefaultEngine;
+ originalDefaultEngine.hidden = false;
+ this.defaultEngine = originalDefaultEngine;
+ },
+
+ /**
+ * Loads engines asynchronously.
+ *
+ * @param {object} settings
+ * An object representing the search engine settings.
+ */
+ async _loadEngines(settings) {
+ logConsole.debug("_loadEngines: start");
+ let { engines, privateDefault } = await this._fetchEngineSelectorEngines();
+ this._setDefaultAndOrdersFromSelector(engines, privateDefault);
+
+ let newEngines = await this._loadEnginesFromConfig(engines);
+ for (let engine of newEngines) {
+ this._addEngineToStore(engine);
+ }
+
+ logConsole.debug(
+ "_loadEngines: loading",
+ this._startupExtensions.size,
+ "engines reported by AddonManager startup"
+ );
+ for (let extension of this._startupExtensions) {
+ await this._installExtensionEngine(
+ extension,
+ [SearchUtils.DEFAULT_TAG],
+ true
+ );
+ }
+ this._startupExtensions.clear();
+
+ this._loadEnginesFromSettings(settings.engines);
+
+ this._loadEnginesMetadataFromSettings(settings.engines);
+
+ logConsole.debug("_loadEngines: done");
+ },
+
+ /**
+ * Loads engines as specified by the configuration. We only expect
+ * configured engines here, user engines should not be listed.
+ *
+ * @param {array} engineConfigs
+ * An array of engines configurations based on the schema.
+ * @returns {array.<nsISearchEngine>}
+ * Returns an array of the loaded search engines. This may be
+ * smaller than the original list if not all engines can be loaded.
+ */
+ async _loadEnginesFromConfig(engineConfigs) {
+ logConsole.debug("_loadEnginesFromConfig");
+ let engines = [];
+ for (let config of engineConfigs) {
+ try {
+ let engine = await this.makeEngineFromConfig(config);
+ engines.push(engine);
+ } catch (ex) {
+ console.error(
+ `Could not load engine ${
+ "webExtension" in config ? config.webExtension.id : "unknown"
+ }: ${ex}`
+ );
+ }
+ }
+ return engines;
+ },
+
+ /**
+ * Reloads engines asynchronously, but only when
+ * the service has already been initialized.
+ */
+ async _maybeReloadEngines() {
+ if (this._maybeReloadDebounce) {
+ logConsole.debug("We're already waiting to reload engines.");
+ return;
+ }
+
+ if (!this._initialized || this._reloadingEngines) {
+ this._maybeReloadDebounce = true;
+ // Schedule a reload to happen at most 10 seconds after the current run.
+ Services.tm.idleDispatchToMainThread(() => {
+ if (!this._maybeReloadDebounce) {
+ return;
+ }
+ this._maybeReloadDebounce = false;
+ this._maybeReloadEngines().catch(Cu.reportError);
+ }, 10000);
+ logConsole.debug(
+ "Post-poning maybeReloadEngines() as we're currently initializing."
+ );
+ return;
+ }
+
+ // Before entering `_reloadingEngines` get the settings which we'll need.
+ // This also ensures that any pending settings have finished being written,
+ // which could otherwise cause data loss.
+ let settings = await this._settings.get();
+
+ logConsole.debug("Running maybeReloadEngines");
+ this._reloadingEngines = true;
+
+ try {
+ await this._reloadEngines(settings);
+ } catch (ex) {
+ logConsole.error("maybeReloadEngines failed", ex);
+ }
+ this._reloadingEngines = false;
+ logConsole.debug("maybeReloadEngines complete");
+ },
+
+ async _reloadEngines(settings) {
+ // Capture the current engine state, in case we need to notify below.
+ const prevCurrentEngine = this._currentEngine;
+ const prevPrivateEngine = this._currentPrivateEngine;
+
+ // Ensure that we don't set the useSavedOrder flag whilst we're doing this.
+ // This isn't a user action, so we shouldn't be switching it.
+ this._dontSetUseSavedOrder = true;
+
+ // The order of work here is designed to avoid potential issues when updating
+ // the default engines, so that we're not removing active defaults or trying
+ // to set a default to something that hasn't been added yet. The order is:
+ //
+ // 1) Update exising engines that are in both the old and new configuration.
+ // 2) Add any new engines from the new configuration.
+ // 3) Update the default engines.
+ // 4) Remove any old engines.
+
+ let {
+ engines: originalConfigEngines,
+ privateDefault,
+ } = await this._fetchEngineSelectorEngines();
+
+ let enginesToRemove = [];
+ let configEngines = [...originalConfigEngines];
+ let oldEngineList = [...this._engines.values()];
+
+ for (let engine of oldEngineList) {
+ if (!engine.isAppProvided) {
+ continue;
+ }
+
+ let index = configEngines.findIndex(
+ e =>
+ e.webExtension.id == engine._extensionID &&
+ e.webExtension.locale == engine._locale
+ );
+
+ let policy, manifest, locale;
+ if (index == -1) {
+ // No engines directly match on id and locale, however, check to see
+ // if we have a new entry that matches on id and name - we might just
+ // be swapping the in-use locale.
+ let replacementEngines = configEngines.filter(
+ e => e.webExtension.id == engine._extensionID
+ );
+ // If there's no possible, or more than one, we treat these as distinct
+ // engines so we'll remove the existing engine and add new later if
+ // necessary.
+ if (replacementEngines.length != 1) {
+ enginesToRemove.push(engine);
+ continue;
+ }
+
+ policy = await this._getExtensionPolicy(engine._extensionID);
+ manifest = policy.extension.manifest;
+ locale =
+ replacementEngines[0].webExtension.locale || SearchUtils.DEFAULT_TAG;
+ if (locale != SearchUtils.DEFAULT_TAG) {
+ manifest = await policy.extension.getLocalizedManifest(locale);
+ }
+ if (
+ manifest.name !=
+ manifest.chrome_settings_overrides.search_provider.name.trim()
+ ) {
+ // No matching name, so just remove it.
+ enginesToRemove.push(engine);
+ continue;
+ }
+
+ // Update the index so we can handle the updating below.
+ index = configEngines.findIndex(
+ e =>
+ e.webExtension.id == replacementEngines[0].webExtension.id &&
+ e.webExtension.locale == replacementEngines[0].webExtension.locale
+ );
+ } else {
+ // This is an existing engine that we should update (we don't know if
+ // the configuration for this engine has changed or not).
+ policy = await this._getExtensionPolicy(engine._extensionID);
+
+ manifest = policy.extension.manifest;
+ locale = engine._locale || SearchUtils.DEFAULT_TAG;
+ if (locale != SearchUtils.DEFAULT_TAG) {
+ manifest = await policy.extension.getLocalizedManifest(locale);
+ }
+ }
+ engine._updateFromManifest(
+ policy.extension.id,
+ policy.extension.baseURI,
+ manifest,
+ locale,
+ configEngines[index]
+ );
+
+ configEngines.splice(index, 1);
+ }
+
+ // Any remaining configuration engines are ones that we need to add.
+ for (let engine of configEngines) {
+ try {
+ let newEngine = await this.makeEngineFromConfig(engine);
+ this._addEngineToStore(newEngine, true);
+ } catch (ex) {
+ logConsole.warn(
+ `Could not load engine ${
+ "webExtension" in engine ? engine.webExtension.id : "unknown"
+ }: ${ex}`
+ );
+ }
+ }
+ this._loadEnginesMetadataFromSettings(settings.engines);
+
+ // Now set the sort out the default engines and notify as appropriate.
+ this._currentEngine = null;
+ this._currentPrivateEngine = null;
+
+ this._setDefaultAndOrdersFromSelector(
+ originalConfigEngines,
+ privateDefault
+ );
+
+ // If the defaultEngine has changed between the previous load and this one,
+ // dispatch the appropriate notifications.
+ if (prevCurrentEngine && this.defaultEngine !== prevCurrentEngine) {
+ SearchUtils.notifyAction(
+ this._currentEngine,
+ SearchUtils.MODIFIED_TYPE.DEFAULT
+ );
+ // If we've not got a separate private active, notify update of the
+ // private so that the UI updates correctly.
+ if (!this._separatePrivateDefault) {
+ SearchUtils.notifyAction(
+ this._currentEngine,
+ SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
+ );
+ }
+ }
+ if (
+ this._separatePrivateDefault &&
+ prevPrivateEngine &&
+ this.defaultPrivateEngine !== prevPrivateEngine
+ ) {
+ SearchUtils.notifyAction(
+ this._currentPrivateEngine,
+ SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
+ );
+ }
+
+ // Finally, remove any engines that need removing.
+
+ for (let engine of enginesToRemove) {
+ // If we have other engines that use the same extension ID, then
+ // we do not want to remove the add-on - only remove the engine itself.
+ let inUseEngines = [...this._engines.values()].filter(
+ e => e._extensionID == engine._extensionID
+ );
+
+ if (inUseEngines.length <= 1) {
+ if (inUseEngines.length == 1 && inUseEngines[0] == engine) {
+ // No other engines are using this extension ID.
+
+ // The internal remove is done first to avoid a call to removeEngine
+ // which could adjust the sort order when we don't want it to.
+ this._internalRemoveEngine(engine);
+
+ let addon = await AddonManager.getAddonByID(engine._extensionID);
+ if (addon) {
+ // AddonManager won't call removeEngine if an engine with the
+ // WebExtension id doesn't exist in the search service.
+ await addon.uninstall();
+ }
+ }
+ // For the case where `inUseEngines[0] != engine`:
+ // This is a situation where there was an engine added earlier in this
+ // function with the same name.
+ // For example, eBay has the same name for both US and GB, but has
+ // a different domain and uses a different locale of the same
+ // WebExtension.
+ // The result of this is the earlier addition has already replaced
+ // the engine in `this._engines` (which is indexed by name), so all that
+ // needs to be done here is to pretend the old engine was removed
+ // which is notified below.
+ } else {
+ // More than one engine is using this extension ID, so we don't want to
+ // remove the add-on.
+ this._internalRemoveEngine(engine);
+ }
+ SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.REMOVED);
+ }
+
+ this._dontSetUseSavedOrder = false;
+ // Clear out the sorted engines settings, so that we re-sort it if necessary.
+ this.__sortedEngines = null;
+ Services.obs.notifyObservers(
+ null,
+ SearchUtils.TOPIC_SEARCH_SERVICE,
+ "engines-reloaded"
+ );
+ },
+
+ /**
+ * Test only - reset SearchService data. Ideally this should be replaced
+ */
+ reset() {
+ this._initialized = false;
+ this._initObservers = PromiseUtils.defer();
+ this._initStarted = false;
+ this._startupExtensions = new Set();
+ this._engines.clear();
+ this.__sortedEngines = null;
+ this._currentEngine = null;
+ this._currentPrivateEngine = null;
+ this._searchDefault = null;
+ this._searchPrivateDefault = null;
+ this._maybeReloadDebounce = false;
+ },
+
+ _addEngineToStore(engine, skipDuplicateCheck = false) {
+ if (this._engineMatchesIgnoreLists(engine)) {
+ logConsole.debug("_addEngineToStore: Ignoring engine");
+ return;
+ }
+
+ logConsole.debug("_addEngineToStore: Adding engine:", engine.name);
+
+ // See if there is an existing engine with the same name. However, if this
+ // engine is updating another engine, it's allowed to have the same name.
+ var hasSameNameAsUpdate =
+ engine._engineToUpdate && engine.name == engine._engineToUpdate.name;
+ if (
+ !skipDuplicateCheck &&
+ this._engines.has(engine.name) &&
+ !hasSameNameAsUpdate
+ ) {
+ logConsole.debug("_addEngineToStore: Duplicate engine found, aborting!");
+ return;
+ }
+
+ if (engine._engineToUpdate) {
+ // We need to replace engineToUpdate with the engine that just loaded.
+ var oldEngine = engine._engineToUpdate;
+
+ // Remove the old engine from the hash, since it's keyed by name, and our
+ // name might change (the update might have a new name).
+ this._engines.delete(oldEngine.name);
+
+ // Hack: we want to replace the old engine with the new one, but since
+ // people may be holding refs to the nsISearchEngine objects themselves,
+ // we'll just copy over all "private" properties (those without a getter
+ // or setter) from one object to the other.
+ for (var p in engine) {
+ if (!(engine.__lookupGetter__(p) || engine.__lookupSetter__(p))) {
+ oldEngine[p] = engine[p];
+ }
+ }
+ engine = oldEngine;
+ engine._engineToUpdate = null;
+
+ // Add the engine back
+ this._engines.set(engine.name, engine);
+ SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.CHANGED);
+ } else {
+ // Not an update, just add the new engine.
+ this._engines.set(engine.name, engine);
+ // Only add the engine to the list of sorted engines if the initial list
+ // has already been built (i.e. if this.__sortedEngines is non-null). If
+ // it hasn't, we're loading engines from disk and the sorted engine list
+ // will be built once we need it.
+ if (this.__sortedEngines && !this._dontSetUseSavedOrder) {
+ this.__sortedEngines.push(engine);
+ this._saveSortedEngineList();
+ }
+ SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.ADDED);
+ }
+
+ // Let the engine know it can start notifying new updates.
+ engine._engineAddedToStore = true;
+
+ if (engine._hasUpdates) {
+ // Schedule the engine's next update, if it isn't already.
+ if (!engine.getAttr("updateexpir")) {
+ engineUpdateService.scheduleNextUpdate(engine);
+ }
+ }
+ },
+
+ _loadEnginesMetadataFromSettings(engines) {
+ if (!engines) {
+ return;
+ }
+
+ for (let engine of engines) {
+ let name = engine._name;
+ if (this._engines.has(name)) {
+ logConsole.debug(
+ "_loadEnginesMetadataFromSettings, transfering metadata for",
+ name
+ );
+ let eng = this._engines.get(name);
+ // We used to store the alias in metadata.alias, in 1621892 that was
+ // changed to only store the user set alias in metadata.alias, remove
+ // it from metadata if it was previously set to the internal value.
+ if (eng._alias === engine?._metaData?.alias) {
+ delete engine._metaData.alias;
+ }
+ eng._metaData = engine._metaData || {};
+ }
+ }
+ },
+
+ _loadEnginesFromSettings(enginesCache) {
+ if (!enginesCache) {
+ return;
+ }
+
+ logConsole.debug(
+ "_loadEnginesFromSettings: Loading",
+ enginesCache.length,
+ "engines from settings"
+ );
+
+ let skippedEngines = 0;
+ for (let engineJSON of enginesCache) {
+ // We renamed isBuiltin to isAppProvided in 1631898,
+ // keep checking isBuiltin for older settings.
+ if (engineJSON._isAppProvided || engineJSON._isBuiltin) {
+ ++skippedEngines;
+ continue;
+ }
+
+ // Some OpenSearch type engines are now obsolete and no longer supported.
+ // These were application provided engines that used to use the OpenSearch
+ // format before gecko transitioned to WebExtensions.
+ // These will sometimes have been missed in migration due to various
+ // reasons, and due to how the settings saves everything. We therefore
+ // explicitly ignore them here to drop them, and let the rest of the code
+ // fallback to the application/distribution default if necessary.
+ let loadPath = engineJSON._loadPath?.toLowerCase();
+ if (
+ loadPath &&
+ // Replaced by application provided in Firefox 79.
+ (loadPath.startsWith("[distribution]") ||
+ // Langpack engines moved in-app in Firefox 62.
+ // Note: these may be prefixed by jar:,
+ loadPath.includes("[app]/extensions/langpack") ||
+ loadPath.includes("[other]/langpack") ||
+ loadPath.includes("[profile]/extensions/langpack"))
+ ) {
+ continue;
+ }
+
+ try {
+ let engine = new SearchEngine({
+ isAppProvided: false,
+ loadPath: engineJSON._loadPath,
+ });
+ engine._initWithJSON(engineJSON);
+ this._addEngineToStore(engine);
+ } catch (ex) {
+ logConsole.error(
+ "Failed to load",
+ engineJSON._name,
+ "from settings:",
+ ex,
+ engineJSON
+ );
+ }
+ }
+
+ if (skippedEngines) {
+ logConsole.debug(
+ "_loadEnginesFromSettings: skipped",
+ skippedEngines,
+ "built-in engines."
+ );
+ }
+ },
+
+ async _fetchEngineSelectorEngines() {
+ let locale = Services.locale.appLocaleAsBCP47;
+ let region = Region.home || "default";
+
+ let channel = AppConstants.MOZ_APP_VERSION_DISPLAY.endsWith("esr")
+ ? "esr"
+ : AppConstants.MOZ_UPDATE_CHANNEL;
+
+ let {
+ engines,
+ privateDefault,
+ } = await this._engineSelector.fetchEngineConfiguration({
+ locale,
+ region,
+ channel,
+ experiment: gExperiment,
+ distroID: SearchUtils.distroID,
+ });
+
+ for (let e of engines) {
+ if (!e.webExtension) {
+ e.webExtension = {};
+ }
+ e.webExtension.locale = e.webExtension?.locale ?? SearchUtils.DEFAULT_TAG;
+ }
+
+ return { engines, privateDefault };
+ },
+
+ _setDefaultAndOrdersFromSelector(engines, privateDefault) {
+ const defaultEngine = engines[0];
+ this._searchDefault = {
+ id: defaultEngine.webExtension.id,
+ locale: defaultEngine.webExtension.locale,
+ };
+ if (privateDefault) {
+ this._searchPrivateDefault = {
+ id: privateDefault.webExtension.id,
+ locale: privateDefault.webExtension.locale,
+ };
+ }
+ },
+
+ _saveSortedEngineList() {
+ logConsole.debug("_saveSortedEngineList");
+
+ // Set the useSavedOrder attribute to indicate that from now on we should
+ // use the user's order information stored in settings.
+ this._settings.setAttribute("useSavedOrder", true);
+
+ var engines = this._getSortedEngines(true);
+
+ for (var i = 0; i < engines.length; ++i) {
+ engines[i].setAttr("order", i + 1);
+ }
+ },
+
+ _buildSortedEngineList() {
+ // We must initialise __sortedEngines here to avoid infinite recursion
+ // in the case of tests which don't define a default search engine.
+ // If there's no default defined, then we revert to the first item in the
+ // sorted list, but we can't do that if we don't have a list.
+ this.__sortedEngines = [];
+
+ // If the user has specified a custom engine order, read the order
+ // information from the metadata instead of the default prefs.
+ if (this._settings.getAttribute("useSavedOrder")) {
+ logConsole.debug("_buildSortedEngineList: using saved order");
+ let addedEngines = {};
+
+ // Flag to keep track of whether or not we need to call _saveSortedEngineList.
+ let needToSaveEngineList = false;
+
+ for (let engine of this._engines.values()) {
+ var orderNumber = engine.getAttr("order");
+
+ // Since the DB isn't regularly cleared, and engine files may disappear
+ // without us knowing, we may already have an engine in this slot. If
+ // that happens, we just skip it - it will be added later on as an
+ // unsorted engine.
+ if (orderNumber && !this.__sortedEngines[orderNumber - 1]) {
+ this.__sortedEngines[orderNumber - 1] = engine;
+ addedEngines[engine.name] = engine;
+ } else {
+ // We need to call _saveSortedEngineList so this gets sorted out.
+ needToSaveEngineList = true;
+ }
+ }
+
+ // Filter out any nulls for engines that may have been removed
+ var filteredEngines = this.__sortedEngines.filter(function(a) {
+ return !!a;
+ });
+ if (this.__sortedEngines.length != filteredEngines.length) {
+ needToSaveEngineList = true;
+ }
+ this.__sortedEngines = filteredEngines;
+
+ if (needToSaveEngineList) {
+ this._saveSortedEngineList();
+ }
+
+ // Array for the remaining engines, alphabetically sorted.
+ let alphaEngines = [];
+
+ for (let engine of this._engines.values()) {
+ if (!(engine.name in addedEngines)) {
+ alphaEngines.push(engine);
+ }
+ }
+
+ const collator = new Intl.Collator();
+ alphaEngines.sort((a, b) => {
+ return collator.compare(a.name, b.name);
+ });
+ return (this.__sortedEngines = this.__sortedEngines.concat(alphaEngines));
+ }
+ logConsole.debug("_buildSortedEngineList: using default orders");
+
+ return (this.__sortedEngines = this._sortEnginesByDefaults(
+ Array.from(this._engines.values())
+ ));
+ },
+
+ /**
+ * Sorts engines by the default settings (prefs, configuration values).
+ *
+ * @param {Array} engines
+ * An array of engine objects to sort.
+ * @returns {Array}
+ * The sorted array of engine objects.
+ */
+ _sortEnginesByDefaults(engines) {
+ const sortedEngines = [];
+ const addedEngines = new Set();
+
+ function maybeAddEngineToSort(engine) {
+ if (!engine || addedEngines.has(engine.name)) {
+ return;
+ }
+
+ sortedEngines.push(engine);
+ addedEngines.add(engine.name);
+ }
+
+ // The original default engine should always be first in the list (except
+ // for distros, that we should respect).
+ const originalDefault = this.originalDefaultEngine;
+ maybeAddEngineToSort(originalDefault);
+
+ // If there's a private default, and it is different to the normal
+ // default, then it should be second in the list.
+ const originalPrivateDefault = this.originalPrivateDefaultEngine;
+ if (originalPrivateDefault && originalPrivateDefault != originalDefault) {
+ maybeAddEngineToSort(originalPrivateDefault);
+ }
+
+ let remainingEngines;
+ const collator = new Intl.Collator();
+
+ remainingEngines = engines.filter(e => !addedEngines.has(e.name));
+
+ // We sort by highest orderHint first, then alphabetically by name.
+ remainingEngines.sort((a, b) => {
+ if (a._orderHint && b._orderHint) {
+ if (a._orderHint == b._orderHint) {
+ return collator.compare(a.name, b.name);
+ }
+ return b._orderHint - a._orderHint;
+ }
+ if (a._orderHint) {
+ return -1;
+ }
+ if (b._orderHint) {
+ return 1;
+ }
+ return collator.compare(a.name, b.name);
+ });
+
+ return [...sortedEngines, ...remainingEngines];
+ },
+
+ /**
+ * Get a sorted array of engines.
+ *
+ * @param {boolean} withHidden
+ * True if hidden plugins should be included in the result.
+ * @returns {Array<SearchEngine>}
+ * The sorted array.
+ */
+ _getSortedEngines(withHidden) {
+ if (withHidden) {
+ return this._sortedEngines;
+ }
+
+ return this._sortedEngines.filter(function(engine) {
+ return !engine.hidden;
+ });
+ },
+
+ // nsISearchService
+ async init() {
+ logConsole.debug("init");
+ if (this._initStarted) {
+ return this._initObservers.promise;
+ }
+
+ TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
+ this._initStarted = true;
+ try {
+ // Complete initialization by calling asynchronous initializer.
+ await this._init();
+ TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
+ } catch (ex) {
+ Services.telemetry.scalarSet(
+ "browser.searchinit.init_result_status_code",
+ // Scalar is a string due to bug 1651210 when the scalar was created.
+ ex.result?.toString(10)
+ );
+ TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
+ this._initObservers.reject(ex.result);
+ throw ex;
+ }
+ Services.telemetry.scalarSet(
+ "browser.searchinit.init_result_status_code",
+ // Scalar is a string due to bug 1651210 when the scalar was created.
+ this._initRV?.toString(10)
+ );
+
+ if (!Components.isSuccessCode(this._initRV)) {
+ throw Components.Exception(
+ "SearchService initialization failed",
+ this._initRV
+ );
+ } else if (this._startupRemovedExtensions.size) {
+ Services.tm.dispatchToMainThread(async () => {
+ // Now that init() has successfully finished, we remove any engines
+ // that have had their add-ons removed by the add-on manager.
+ // We do this after init() has complete, as that allows us to use
+ // removeEngine to look after any default engine changes as well.
+ // This could cause a slight flicker on startup, but it should be
+ // a rare action.
+ logConsole.debug("Removing delayed extension engines");
+ for (let id of this._startupRemovedExtensions) {
+ for (let engine of this._getEnginesByExtensionID(id)) {
+ // Only do this for non-application provided engines. We shouldn't
+ // ever get application provided engines removed here, but just in case.
+ if (!engine.isAppProvided) {
+ await this.removeEngine(engine);
+ }
+ }
+ }
+ this._startupRemovedExtensions.clear();
+ });
+ }
+ return this._initRV;
+ },
+
+ get isInitialized() {
+ return this._initialized;
+ },
+
+ /**
+ * Checks if Search Engines associated with WebExtensions are valid and
+ * up-to-date, and reports them via telemetry if not.
+ */
+ async checkWebExtensionEngines() {
+ await this.init();
+ logConsole.debug("Running check on WebExtension engines");
+
+ for (let engine of this._engines.values()) {
+ if (
+ engine.isAppProvided ||
+ !engine._extensionID ||
+ engine._extensionID == "set-via-policy" ||
+ engine._extensionID == "set-via-user"
+ ) {
+ continue;
+ }
+
+ let addon = await AddonManager.getAddonByID(engine._extensionID);
+
+ if (!addon) {
+ logConsole.debug(
+ `Add-on ${engine._extensionID} for search engine ${engine.name} is not installed!`
+ );
+ Services.telemetry.keyedScalarSet(
+ "browser.searchinit.engine_invalid_webextension",
+ engine._extensionID,
+ 1
+ );
+ } else if (!addon.isActive) {
+ logConsole.debug(
+ `Add-on ${engine._extensionID} for search engine ${engine.name} is not active!`
+ );
+ Services.telemetry.keyedScalarSet(
+ "browser.searchinit.engine_invalid_webextension",
+ engine._extensionID,
+ 2
+ );
+ } else {
+ let policy = await this._getExtensionPolicy(engine._extensionID);
+ let providerSettings =
+ policy.extension.manifest?.chrome_settings_overrides?.search_provider;
+
+ if (!providerSettings) {
+ logConsole.debug(
+ `Add-on ${engine._extensionID} for search engine ${engine.name} no longer has an engine defined`
+ );
+ Services.telemetry.keyedScalarSet(
+ "browser.searchinit.engine_invalid_webextension",
+ engine._extensionID,
+ 4
+ );
+ } else if (engine.name != providerSettings.name) {
+ logConsole.debug(
+ `Add-on ${engine._extensionID} for search engine ${engine.name} has a different name!`
+ );
+ Services.telemetry.keyedScalarSet(
+ "browser.searchinit.engine_invalid_webextension",
+ engine._extensionID,
+ 5
+ );
+ } else if (!engine.checkSearchUrlMatchesManifest(providerSettings)) {
+ logConsole.debug(
+ `Add-on ${engine._extensionID} for search engine ${engine.name} has out-of-date manifest!`
+ );
+ Services.telemetry.keyedScalarSet(
+ "browser.searchinit.engine_invalid_webextension",
+ engine._extensionID,
+ 6
+ );
+ }
+ }
+ }
+ logConsole.debug("WebExtension engine check complete");
+ },
+
+ async getEngines() {
+ await this.init();
+ logConsole.debug("getEngines: getting all engines");
+ return this._getSortedEngines(true);
+ },
+
+ async getVisibleEngines() {
+ await this.init(true);
+ logConsole.debug("getVisibleEngines: getting all visible engines");
+ return this._getSortedEngines(false);
+ },
+
+ async getAppProvidedEngines() {
+ await this.init();
+
+ return this._sortEnginesByDefaults(
+ this._sortedEngines.filter(e => e.isAppProvided)
+ );
+ },
+
+ async getEnginesByExtensionID(extensionID) {
+ await this.init();
+ return this._getEnginesByExtensionID(extensionID);
+ },
+
+ _getEnginesByExtensionID(extensionID) {
+ logConsole.debug("getEngines: getting all engines for", extensionID);
+ var engines = this._getSortedEngines(true).filter(function(engine) {
+ return engine._extensionID == extensionID;
+ });
+ return engines;
+ },
+
+ /**
+ * Returns the engine associated with the name.
+ *
+ * @param {string} engineName
+ * The name of the engine.
+ * @returns {SearchEngine}
+ * The associated engine if found, null otherwise.
+ */
+ getEngineByName(engineName) {
+ this._ensureInitialized();
+ return this._engines.get(engineName) || null;
+ },
+
+ async getEngineByAlias(alias) {
+ await this.init();
+ for (var engine of this._engines.values()) {
+ if (engine && engine.aliases.includes(alias)) {
+ return engine;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the engine associated with the WebExtension details.
+ *
+ * @param {object} details
+ * @param {string} details.id
+ * The WebExtension ID
+ * @param {string} details.locale
+ * The WebExtension locale
+ * @returns {nsISearchEngine|null}
+ * The found engine, or null if no engine matched.
+ */
+ _getEngineByWebExtensionDetails(details) {
+ for (const engine of this._engines.values()) {
+ if (
+ engine._extensionID == details.id &&
+ engine._locale == details.locale
+ ) {
+ return engine;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Adds a search engine that is specified from enterprise policies.
+ *
+ * @param {object} details
+ * An object that simulates the manifest object from a WebExtension. See
+ * the idl for more details.
+ */
+ async addPolicyEngine(details) {
+ await this._createAndAddEngine({
+ extensionID: "set-via-policy",
+ extensionBaseURI: "",
+ isAppProvided: false,
+ manifest: details,
+ });
+ },
+
+ /**
+ * Adds a search engine that is specified by the user.
+ *
+ * @param {string} name
+ * @param {string} url
+ * @param {string} alias
+ */
+ async addUserEngine(name, url, alias) {
+ await this._createAndAddEngine({
+ extensionID: "set-via-user",
+ extensionBaseURI: "",
+ isAppProvided: false,
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name,
+ search_url: encodeURI(url),
+ keyword: alias,
+ },
+ },
+ },
+ });
+ },
+
+ /**
+ * Adds an engine with specific details, only used for tests and should
+ * be considered obsolete, see bug 1649186.
+ *
+ * @param {string} name
+ * The name of the engine to add.
+ * @param {object} details
+ * The details of the engine to add.
+ */
+ async addEngineWithDetails(name, details) {
+ let manifest = {
+ description: details.description,
+ iconURL: details.iconURL,
+ chrome_settings_overrides: {
+ search_provider: {
+ name,
+ encoding: details.encoding || SearchUtils.DEFAULT_QUERY_CHARSET,
+ search_url: encodeURI(details.template),
+ keyword: details.alias,
+ search_url_get_params: details.searchGetParams,
+ search_url_post_params: details.postData || details.searchPostParams,
+ suggest_url: details.suggestURL,
+ },
+ },
+ };
+ return this._createAndAddEngine({
+ extensionID: details.extensionID ?? `${name}@test.engine`,
+ extensionBaseURI: "",
+ isAppProvided: false,
+ manifest,
+ });
+ },
+
+ /**
+ * Creates and adds a WebExtension based engine.
+ * Note: this is currently used for enterprise policy engines as well.
+ *
+ * @param {object} options
+ * @param {string} options.extensionID
+ * The extension ID being added for the engine.
+ * @param {nsIURI} [options.extensionBaseURI]
+ * The base URI of the extension.
+ * @param {boolean} options.isAppProvided
+ * True if the WebExtension is built-in or installed into the system scope.
+ * @param {object} options.manifest
+ * An object that represents the extension's manifest.
+ * @param {stirng} [options.locale]
+ * The locale to use within the WebExtension. Defaults to the WebExtension's
+ * default locale.
+ * @param {initEngine} [options.initEngine]
+ * Set to true if this engine is being loaded during initialisation.
+ */
+ async _createAndAddEngine({
+ extensionID,
+ extensionBaseURI,
+ isAppProvided,
+ manifest,
+ locale = SearchUtils.DEFAULT_TAG,
+ initEngine = false,
+ }) {
+ if (!extensionID) {
+ throw Components.Exception(
+ "Empty extensionID passed to _createAndAddEngine!",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+ let searchProvider = manifest.chrome_settings_overrides.search_provider;
+ let name = searchProvider.name.trim();
+ logConsole.debug("_createAndAddEngine: Adding", name);
+ let isCurrent = false;
+
+ // We install search extensions during the init phase, both built in
+ // web extensions freshly installed (via addEnginesFromExtension) or
+ // user installed extensions being reenabled calling this directly.
+ if (!this._initialized && !isAppProvided && !initEngine) {
+ await this.init();
+ }
+ let existingEngine = this._engines.get(name);
+ if (existingEngine) {
+ if (
+ extensionID &&
+ existingEngine._loadPath.startsWith(
+ `jar:[profile]/extensions/${extensionID}`
+ )
+ ) {
+ // This is a legacy extension engine that needs to be migrated to WebExtensions.
+ isCurrent = this.defaultEngine == existingEngine;
+ await this.removeEngine(existingEngine);
+ } else {
+ throw Components.Exception(
+ "An engine with that name already exists!",
+ Cr.NS_ERROR_FILE_ALREADY_EXISTS
+ );
+ }
+ }
+
+ let newEngine = new SearchEngine({
+ name,
+ isAppProvided,
+ loadPath: `[other]addEngineWithDetails:${extensionID}`,
+ });
+ newEngine._initFromManifest(
+ extensionID,
+ extensionBaseURI,
+ manifest,
+ locale
+ );
+
+ this._addEngineToStore(newEngine);
+ if (isCurrent) {
+ this.defaultEngine = newEngine;
+ }
+ return newEngine;
+ },
+
+ /**
+ * Called from the AddonManager when it either installs a new
+ * extension containing a search engine definition or an upgrade
+ * to an existing one.
+ *
+ * @param {object} extension
+ * An Extension object containing data about the extension.
+ */
+ async addEnginesFromExtension(extension) {
+ logConsole.debug("addEnginesFromExtension: " + extension.id);
+ // Treat add-on upgrade and downgrades the same - either way, the search
+ // engine gets updated, not added. Generally, we don't expect a downgrade,
+ // but just in case...
+ if (
+ extension.startupReason == "ADDON_UPGRADE" ||
+ extension.startupReason == "ADDON_DOWNGRADE"
+ ) {
+ return this._upgradeExtensionEngine(extension);
+ }
+
+ if (extension.isAppProvided) {
+ // If we are in the middle of initialization or reloading engines,
+ // don't add the engine here. This has been called as the result
+ // of makeEngineFromConfig installing the extension, and that is already
+ // handling the addition of the engine.
+ if (this._initialized && !this._reloadingEngines) {
+ let { engines } = await this._fetchEngineSelectorEngines();
+ let inConfig = engines.filter(el => el.webExtension.id == extension.id);
+ if (inConfig.length) {
+ return this._installExtensionEngine(
+ extension,
+ inConfig.map(el => el.webExtension.locale)
+ );
+ }
+ }
+ logConsole.debug("addEnginesFromExtension: Ignoring builtIn engine.");
+ return [];
+ }
+
+ // If we havent started SearchService yet, store this extension
+ // to install in SearchService.init().
+ if (!this._initialized) {
+ this._startupExtensions.add(extension);
+ return [];
+ }
+
+ return this._installExtensionEngine(extension, [SearchUtils.DEFAULT_TAG]);
+ },
+
+ /**
+ * Called when we see an upgrade to an existing search extension.
+ *
+ * @param {object} extension
+ * An Extension object containing data about the extension.
+ */
+ async _upgradeExtensionEngine(extension) {
+ let { engines } = await this._fetchEngineSelectorEngines();
+ let extensionEngines = await this.getEnginesByExtensionID(extension.id);
+
+ for (let engine of extensionEngines) {
+ let manifest = extension.manifest;
+ let locale = engine._locale || SearchUtils.DEFAULT_TAG;
+ if (locale != SearchUtils.DEFAULT_TAG) {
+ manifest = await extension.getLocalizedManifest(locale);
+ }
+ let configuration =
+ engines.find(
+ e =>
+ e.webExtension.id == extension.id && e.webExtension.locale == locale
+ ) ?? {};
+ engine._updateFromManifest(
+ extension.id,
+ extension.baseURI,
+ manifest,
+ locale,
+ configuration
+ );
+ }
+ return extensionEngines;
+ },
+
+ /**
+ * Create an engine object from the search configuration details.
+ *
+ * @param {object} config
+ * The configuration object that defines the details of the engine
+ * webExtensionId etc.
+ * @returns {nsISearchEngine}
+ * Returns the search engine object.
+ */
+ async makeEngineFromConfig(config) {
+ logConsole.debug("makeEngineFromConfig:", config);
+ let policy = await this._getExtensionPolicy(config.webExtension.id);
+ let locale =
+ "locale" in config.webExtension
+ ? config.webExtension.locale
+ : SearchUtils.DEFAULT_TAG;
+
+ let manifest = policy.extension.manifest;
+ if (locale != SearchUtils.DEFAULT_TAG) {
+ manifest = await policy.extension.getLocalizedManifest(locale);
+ }
+
+ let engine = new SearchEngine({
+ name: manifest.chrome_settings_overrides.search_provider.name.trim(),
+ isAppProvided: policy.extension.isAppProvided,
+ loadPath: `[other]addEngineWithDetails:${policy.extension.id}`,
+ });
+ engine._initFromManifest(
+ policy.extension.id,
+ policy.extension.baseURI,
+ manifest,
+ locale,
+ config
+ );
+ return engine;
+ },
+
+ async _installExtensionEngine(extension, locales, initEngine = false) {
+ logConsole.debug("installExtensionEngine:", extension.id);
+
+ let installLocale = async locale => {
+ let manifest =
+ locale == SearchUtils.DEFAULT_TAG
+ ? extension.manifest
+ : await extension.getLocalizedManifest(locale);
+ return this._addEngineForManifest(
+ extension,
+ manifest,
+ locale,
+ initEngine
+ );
+ };
+
+ let engines = [];
+ for (let locale of locales) {
+ logConsole.debug(
+ "addEnginesFromExtension: installing:",
+ extension.id,
+ ":",
+ locale
+ );
+ engines.push(await installLocale(locale));
+ }
+ return engines;
+ },
+
+ async _addEngineForManifest(
+ extension,
+ manifest,
+ locale = SearchUtils.DEFAULT_TAG,
+ initEngine = false
+ ) {
+ // If we're in the startup cycle, and we've already loaded this engine,
+ // then we use the existing one rather than trying to start from scratch.
+ // This also avoids console errors.
+ if (extension.startupReason == "APP_STARTUP") {
+ let engine = this._getEngineByWebExtensionDetails({
+ id: extension.id,
+ locale,
+ });
+ if (engine) {
+ logConsole.debug(
+ "Engine already loaded via settings, skipping due to APP_STARTUP:",
+ extension.id
+ );
+ return engine;
+ }
+ }
+
+ return this._createAndAddEngine({
+ extensionID: extension.id,
+ extensionBaseURI: extension.baseURI,
+ isAppProvided: extension.isAppProvided,
+ manifest,
+ locale,
+ initEngine,
+ });
+ },
+
+ async addOpenSearchEngine(engineURL, iconURL) {
+ logConsole.debug("addEngine: Adding", engineURL);
+ await this.init();
+ let errCode;
+ try {
+ var engine = new OpenSearchEngine({
+ uri: engineURL,
+ isAppProvided: false,
+ });
+ engine._setIcon(iconURL, false);
+ errCode = await new Promise(resolve => {
+ engine._initFromURIAndLoad(engineURL, errorCode => {
+ resolve(errorCode);
+ });
+ });
+ if (errCode) {
+ throw errCode;
+ }
+ } catch (ex) {
+ throw Components.Exception(
+ "addEngine: Error adding engine:\n" + ex,
+ errCode || Cr.NS_ERROR_FAILURE
+ );
+ }
+ return engine;
+ },
+
+ async removeWebExtensionEngine(id) {
+ if (!this.isInitialized) {
+ logConsole.debug("Delaying removing extension engine on startup:", id);
+ this._startupRemovedExtensions.add(id);
+ return;
+ }
+
+ logConsole.debug("removeWebExtensionEngine:", id);
+ for (let engine of this._getEnginesByExtensionID(id)) {
+ await this.removeEngine(engine);
+ }
+ },
+
+ async removeEngine(engine) {
+ await this.init();
+ if (!engine) {
+ throw Components.Exception(
+ "no engine passed to removeEngine!",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ var engineToRemove = null;
+ for (var e of this._engines.values()) {
+ if (engine.wrappedJSObject == e) {
+ engineToRemove = e;
+ }
+ }
+
+ if (!engineToRemove) {
+ throw Components.Exception(
+ "removeEngine: Can't find engine to remove!",
+ Cr.NS_ERROR_FILE_NOT_FOUND
+ );
+ }
+
+ if (engineToRemove == this.defaultEngine) {
+ this._currentEngine = null;
+ }
+
+ // Bug 1575649 - We can't just check the default private engine here when
+ // we're not using separate, as that re-checks the normal default, and
+ // triggers update of the default search engine, which messes up various
+ // tests. Really, removeEngine should always commit to updating any
+ // changed defaults.
+ if (
+ this._separatePrivateDefault &&
+ engineToRemove == this.defaultPrivateEngine
+ ) {
+ this._currentPrivateEngine = null;
+ }
+
+ if (engineToRemove._isAppProvided) {
+ // Just hide it (the "hidden" setter will notify) and remove its alias to
+ // avoid future conflicts with other engines.
+ engineToRemove.hidden = true;
+ engineToRemove.alias = null;
+ } else {
+ // Remove the engine file from disk if we had a legacy file in the profile.
+ if (engineToRemove._filePath) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = engineToRemove._filePath;
+ if (file.exists()) {
+ file.remove(false);
+ }
+ engineToRemove._filePath = null;
+ }
+ this._internalRemoveEngine(engineToRemove);
+
+ // Since we removed an engine, we may need to update the preferences.
+ if (!this._dontSetUseSavedOrder) {
+ this._saveSortedEngineList();
+ }
+ }
+ SearchUtils.notifyAction(engineToRemove, SearchUtils.MODIFIED_TYPE.REMOVED);
+ },
+
+ _internalRemoveEngine(engine) {
+ // Remove the engine from _sortedEngines
+ if (this.__sortedEngines) {
+ var index = this.__sortedEngines.indexOf(engine);
+ if (index == -1) {
+ throw Components.Exception(
+ "Can't find engine to remove in _sortedEngines!",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+ this.__sortedEngines.splice(index, 1);
+ }
+
+ // Remove the engine from the internal store
+ this._engines.delete(engine.name);
+ },
+
+ async moveEngine(engine, newIndex) {
+ await this.init();
+ if (newIndex > this._sortedEngines.length || newIndex < 0) {
+ throw Components.Exception("moveEngine: Index out of bounds!");
+ }
+ if (
+ !(engine instanceof Ci.nsISearchEngine) &&
+ !(engine instanceof SearchEngine)
+ ) {
+ throw Components.Exception(
+ "moveEngine: Invalid engine passed to moveEngine!",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+ if (engine.hidden) {
+ throw Components.Exception(
+ "moveEngine: Can't move a hidden engine!",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ engine = engine.wrappedJSObject;
+
+ var currentIndex = this._sortedEngines.indexOf(engine);
+ if (currentIndex == -1) {
+ throw Components.Exception(
+ "moveEngine: Can't find engine to move!",
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ // Our callers only take into account non-hidden engines when calculating
+ // newIndex, but we need to move it in the array of all engines, so we
+ // need to adjust newIndex accordingly. To do this, we count the number
+ // of hidden engines in the list before the engine that we're taking the
+ // place of. We do this by first finding newIndexEngine (the engine that
+ // we were supposed to replace) and then iterating through the complete
+ // engine list until we reach it, increasing newIndex for each hidden
+ // engine we find on our way there.
+ //
+ // This could be further simplified by having our caller pass in
+ // newIndexEngine directly instead of newIndex.
+ var newIndexEngine = this._getSortedEngines(false)[newIndex];
+ if (!newIndexEngine) {
+ throw Components.Exception(
+ "moveEngine: Can't find engine to replace!",
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ for (var i = 0; i < this._sortedEngines.length; ++i) {
+ if (newIndexEngine == this._sortedEngines[i]) {
+ break;
+ }
+ if (this._sortedEngines[i].hidden) {
+ newIndex++;
+ }
+ }
+
+ if (currentIndex == newIndex) {
+ return;
+ } // nothing to do!
+
+ // Move the engine
+ var movedEngine = this.__sortedEngines.splice(currentIndex, 1)[0];
+ this.__sortedEngines.splice(newIndex, 0, movedEngine);
+
+ SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.CHANGED);
+
+ // Since we moved an engine, we need to update the preferences.
+ this._saveSortedEngineList();
+ },
+
+ restoreDefaultEngines() {
+ this._ensureInitialized();
+ for (let e of this._engines.values()) {
+ // Unhide all default engines
+ if (e.hidden && e.isAppProvided) {
+ e.hidden = false;
+ }
+ }
+ },
+
+ /**
+ * Helper function to get the current default engine.
+ *
+ * @param {boolean} privateMode
+ * If true, returns the default engine for private browsing mode, otherwise
+ * the default engine for the normal mode. Note, this function does not
+ * check the "separatePrivateDefault" preference - that is up to the caller.
+ * @returns {nsISearchEngine|null}
+ * The appropriate search engine, or null if one could not be determined.
+ */
+ _getEngineDefault(privateMode) {
+ this._ensureInitialized();
+ const currentEngine = privateMode
+ ? "_currentPrivateEngine"
+ : "_currentEngine";
+ if (!this[currentEngine]) {
+ const attributeName = privateMode ? "private" : "current";
+ let name = this._settings.getAttribute(attributeName);
+ let engine = this.getEngineByName(name);
+ if (
+ engine &&
+ (engine.isAppProvided ||
+ this._settings.getVerifiedAttribute(attributeName))
+ ) {
+ // If the current engine is a default one, we can relax the
+ // verification hash check to reduce the annoyance for users who
+ // backup/sync their profile in custom ways.
+ this[currentEngine] = engine;
+ }
+ if (!name) {
+ this[currentEngine] = privateMode
+ ? this.originalPrivateDefaultEngine
+ : this.originalDefaultEngine;
+ }
+ }
+
+ // If the current engine is not set or hidden, we fallback...
+ if (!this[currentEngine] || this[currentEngine].hidden) {
+ // first to the original default engine
+ let originalDefault = privateMode
+ ? this.originalPrivateDefaultEngine
+ : this.originalDefaultEngine;
+ if (!originalDefault || originalDefault.hidden) {
+ // then to the first visible engine
+ let firstVisible = this._getSortedEngines(false)[0];
+ if (firstVisible && !firstVisible.hidden) {
+ if (privateMode) {
+ this.defaultPrivateEngine = firstVisible;
+ } else {
+ this.defaultEngine = firstVisible;
+ }
+ return firstVisible;
+ }
+ // and finally as a last resort we unhide the original default engine.
+ if (originalDefault) {
+ originalDefault.hidden = false;
+ }
+ }
+ if (!originalDefault) {
+ return null;
+ }
+
+ // If the current engine wasn't set or was hidden, we used a fallback
+ // to pick a new current engine. As soon as we return it, this new
+ // current engine will become user-visible, so we should persist it.
+ // by calling the setter.
+ if (privateMode) {
+ this.defaultPrivateEngine = originalDefault;
+ } else {
+ this.defaultEngine = originalDefault;
+ }
+ }
+
+ return this[currentEngine];
+ },
+
+ /**
+ * Helper function to set the current default engine.
+ *
+ * @param {boolean} privateMode
+ * If true, sets the default engine for private browsing mode, otherwise
+ * sets the default engine for the normal mode. Note, this function does not
+ * check the "separatePrivateDefault" preference - that is up to the caller.
+ * @param {nsISearchEngine} newEngine
+ * The search engine to select
+ */
+ _setEngineDefault(privateMode, newEngine) {
+ this._ensureInitialized();
+ // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers),
+ // and sometimes we get raw Engine JS objects (callers in this file), so
+ // handle both.
+ if (
+ !(newEngine instanceof Ci.nsISearchEngine) &&
+ !(newEngine instanceof SearchEngine)
+ ) {
+ throw Components.Exception(
+ "Invalid argument passed to defaultEngine setter",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ const newCurrentEngine = this.getEngineByName(newEngine.name);
+ if (!newCurrentEngine) {
+ throw Components.Exception(
+ "Can't find engine in store!",
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+
+ if (!newCurrentEngine.isAppProvided) {
+ // If a non default engine is being set as the current engine, ensure
+ // its loadPath has a verification hash.
+ if (!newCurrentEngine._loadPath) {
+ newCurrentEngine._loadPath = "[other]unknown";
+ }
+ let loadPathHash = SearchUtils.getVerificationHash(
+ newCurrentEngine._loadPath
+ );
+ let currentHash = newCurrentEngine.getAttr("loadPathHash");
+ if (!currentHash || currentHash != loadPathHash) {
+ newCurrentEngine.setAttr("loadPathHash", loadPathHash);
+ SearchUtils.notifyAction(
+ newCurrentEngine,
+ SearchUtils.MODIFIED_TYPE.CHANGED
+ );
+ }
+ }
+
+ const currentEngine = `_current${privateMode ? "Private" : ""}Engine`;
+
+ if (newCurrentEngine == this[currentEngine]) {
+ return;
+ }
+
+ // Ensure that we reset an engine override if it was previously overridden.
+ this[currentEngine]?.removeExtensionOverride();
+
+ this[currentEngine] = newCurrentEngine;
+
+ // If we change the default engine in the future, that change should impact
+ // users who have switched away from and then back to the build's "default"
+ // engine. So clear the user pref when the currentEngine is set to the
+ // build's default engine, so that the currentEngine getter falls back to
+ // whatever the default is.
+ let newName = this[currentEngine].name;
+ const originalDefault = privateMode
+ ? this.originalPrivateDefaultEngine
+ : this.originalDefaultEngine;
+ if (this[currentEngine] == originalDefault) {
+ newName = "";
+ }
+
+ this._settings.setVerifiedAttribute(
+ privateMode ? "private" : "current",
+ newName
+ );
+
+ SearchUtils.notifyAction(
+ this[currentEngine],
+ SearchUtils.MODIFIED_TYPE[privateMode ? "DEFAULT_PRIVATE" : "DEFAULT"]
+ );
+ // If we've not got a separate private active, notify update of the
+ // private so that the UI updates correctly.
+ if (!privateMode && !this._separatePrivateDefault) {
+ SearchUtils.notifyAction(
+ this[currentEngine],
+ SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
+ );
+ }
+ },
+
+ get defaultEngine() {
+ return this._getEngineDefault(false);
+ },
+
+ set defaultEngine(newEngine) {
+ this._setEngineDefault(false, newEngine);
+ },
+
+ get defaultPrivateEngine() {
+ return this._getEngineDefault(this._separatePrivateDefault);
+ },
+
+ set defaultPrivateEngine(newEngine) {
+ if (!this._separatePrivateDefaultPrefValue) {
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
+ true
+ );
+ }
+ this._setEngineDefault(this._separatePrivateDefault, newEngine);
+ },
+
+ async getDefault() {
+ await this.init();
+ return this.defaultEngine;
+ },
+
+ async setDefault(engine) {
+ await this.init();
+ return (this.defaultEngine = engine);
+ },
+
+ async getDefaultPrivate() {
+ await this.init();
+ return this.defaultPrivateEngine;
+ },
+
+ async setDefaultPrivate(engine) {
+ await this.init();
+ return (this.defaultPrivateEngine = engine);
+ },
+
+ _onSeparateDefaultPrefChanged() {
+ // Clear out the sorted engines settings, so that we re-sort it if necessary.
+ this.__sortedEngines = null;
+ // We should notify if the normal default, and the currently saved private
+ // default are different. Otherwise, save the energy.
+ if (this.defaultEngine != this._getEngineDefault(true)) {
+ SearchUtils.notifyAction(
+ // Always notify with the new private engine, the function checks
+ // the preference value for us.
+ this.defaultPrivateEngine,
+ SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
+ );
+ }
+ },
+
+ async _getEngineInfo(engine) {
+ if (!engine) {
+ // The defaultEngine getter will throw if there's no engine at all,
+ // which shouldn't happen unless an add-on or a test deleted all of them.
+ // Our preferences UI doesn't let users do that.
+ Cu.reportError("getDefaultEngineInfo: No default engine");
+ return ["NONE", { name: "NONE" }];
+ }
+
+ const engineData = {
+ loadPath: engine._loadPath,
+ name: engine.name ? engine.name : "",
+ };
+
+ if (engine.isAppProvided) {
+ engineData.origin = "default";
+ } else {
+ let currentHash = engine.getAttr("loadPathHash");
+ if (!currentHash) {
+ engineData.origin = "unverified";
+ } else {
+ let loadPathHash = SearchUtils.getVerificationHash(engine._loadPath);
+ engineData.origin =
+ currentHash == loadPathHash ? "verified" : "invalid";
+ }
+ }
+
+ // For privacy, we only collect the submission URL for default engines...
+ let sendSubmissionURL = engine.isAppProvided;
+
+ if (!sendSubmissionURL) {
+ // ... or engines that are the same domain as a default engine.
+ let engineHost = engine._getURLOfType(SearchUtils.URL_TYPE.SEARCH)
+ .templateHost;
+ for (let innerEngine of this._engines.values()) {
+ if (!innerEngine.isAppProvided) {
+ continue;
+ }
+
+ let innerEngineURL = innerEngine._getURLOfType(
+ SearchUtils.URL_TYPE.SEARCH
+ );
+ if (innerEngineURL.templateHost == engineHost) {
+ sendSubmissionURL = true;
+ break;
+ }
+ }
+
+ if (!sendSubmissionURL) {
+ // ... or well known search domains.
+ //
+ // Starts with: www.google., search.aol., yandex.
+ // or
+ // Ends with: search.yahoo.com, .ask.com, .bing.com, .startpage.com, baidu.com, duckduckgo.com
+ const urlTest = /^(?:www\.google\.|search\.aol\.|yandex\.)|(?:search\.yahoo|\.ask|\.bing|\.startpage|\.baidu|duckduckgo)\.com$/;
+ sendSubmissionURL = urlTest.test(engineHost);
+ }
+ }
+
+ if (sendSubmissionURL) {
+ let uri = engine
+ ._getURLOfType("text/html")
+ .getSubmission("", engine, "searchbar").uri;
+ uri = uri
+ .mutate()
+ .setUserPass("") // Avoid reporting a username or password.
+ .finalize();
+ engineData.submissionURL = uri.spec;
+ }
+
+ return [engine.telemetryId, engineData];
+ },
+
+ async getDefaultEngineInfo() {
+ let [telemetryId, defaultSearchEngineData] = await this._getEngineInfo(
+ this.defaultEngine
+ );
+ const result = {
+ defaultSearchEngine: telemetryId,
+ defaultSearchEngineData,
+ };
+
+ if (this._separatePrivateDefault) {
+ let [
+ privateTelemetryId,
+ defaultPrivateSearchEngineData,
+ ] = await this._getEngineInfo(this.defaultPrivateEngine);
+ result.defaultPrivateSearchEngine = privateTelemetryId;
+ result.defaultPrivateSearchEngineData = defaultPrivateSearchEngineData;
+ }
+
+ return result;
+ },
+
+ /**
+ * This map is built lazily after the available search engines change. It
+ * allows quick parsing of an URL representing a search submission into the
+ * search engine name and original terms.
+ *
+ * The keys are strings containing the domain name and lowercase path of the
+ * engine submission, for example "www.google.com/search".
+ *
+ * The values are objects with these properties:
+ * {
+ * engine: The associated nsISearchEngine.
+ * termsParameterName: Name of the URL parameter containing the search
+ * terms, for example "q".
+ * }
+ */
+ _parseSubmissionMap: null,
+
+ _buildParseSubmissionMap() {
+ this._parseSubmissionMap = new Map();
+
+ // Used only while building the map, indicates which entries do not refer to
+ // the main domain of the engine but to an alternate domain, for example
+ // "www.google.fr" for the "www.google.com" search engine.
+ let keysOfAlternates = new Set();
+
+ for (let engine of this._sortedEngines) {
+ if (engine.hidden) {
+ continue;
+ }
+
+ let urlParsingInfo = engine.getURLParsingInfo();
+ if (!urlParsingInfo) {
+ continue;
+ }
+
+ // Store the same object on each matching map key, as an optimization.
+ let mapValueForEngine = {
+ engine,
+ termsParameterName: urlParsingInfo.termsParameterName,
+ };
+
+ let processDomain = (domain, isAlternate) => {
+ let key = domain + urlParsingInfo.path;
+
+ // Apply the logic for which main domains take priority over alternate
+ // domains, even if they are found later in the ordered engine list.
+ let existingEntry = this._parseSubmissionMap.get(key);
+ if (!existingEntry) {
+ if (isAlternate) {
+ keysOfAlternates.add(key);
+ }
+ } else if (!isAlternate && keysOfAlternates.has(key)) {
+ keysOfAlternates.delete(key);
+ } else {
+ return;
+ }
+
+ this._parseSubmissionMap.set(key, mapValueForEngine);
+ };
+
+ processDomain(urlParsingInfo.mainDomain, false);
+ SearchStaticData.getAlternateDomains(
+ urlParsingInfo.mainDomain
+ ).forEach(d => processDomain(d, true));
+ }
+ },
+
+ parseSubmissionURL(url) {
+ if (!this._initialized) {
+ // If search is not initialized, do nothing.
+ // This allows us to use this function early in telemetry.
+ // The only other consumer of this (places) uses it much later.
+ return gEmptyParseSubmissionResult;
+ }
+
+ if (!this._parseSubmissionMap) {
+ this._buildParseSubmissionMap();
+ }
+
+ // Extract the elements of the provided URL first.
+ let soughtKey, soughtQuery;
+ try {
+ let soughtUrl = Services.io.newURI(url).QueryInterface(Ci.nsIURL);
+
+ // Exclude any URL that is not HTTP or HTTPS from the beginning.
+ if (soughtUrl.scheme != "http" && soughtUrl.scheme != "https") {
+ return gEmptyParseSubmissionResult;
+ }
+
+ // Reading these URL properties may fail and raise an exception.
+ soughtKey = soughtUrl.host + soughtUrl.filePath.toLowerCase();
+ soughtQuery = soughtUrl.query;
+ } catch (ex) {
+ // Errors while parsing the URL or accessing the properties are not fatal.
+ return gEmptyParseSubmissionResult;
+ }
+
+ // Look up the domain and path in the map to identify the search engine.
+ let mapEntry = this._parseSubmissionMap.get(soughtKey);
+ if (!mapEntry) {
+ return gEmptyParseSubmissionResult;
+ }
+
+ // Extract the search terms from the parameter, for example "caff%C3%A8"
+ // from the URL "https://www.google.com/search?q=caff%C3%A8&client=firefox".
+ let encodedTerms = null;
+ for (let param of soughtQuery.split("&")) {
+ let equalPos = param.indexOf("=");
+ if (
+ equalPos != -1 &&
+ param.substr(0, equalPos) == mapEntry.termsParameterName
+ ) {
+ // This is the parameter we are looking for.
+ encodedTerms = param.substr(equalPos + 1);
+ break;
+ }
+ }
+ if (encodedTerms === null) {
+ return gEmptyParseSubmissionResult;
+ }
+
+ let length = 0;
+ let offset = url.indexOf("?") + 1;
+ let query = url.slice(offset);
+ // Iterate a second time over the original input string to determine the
+ // correct search term offset and length in the original encoding.
+ for (let param of query.split("&")) {
+ let equalPos = param.indexOf("=");
+ if (
+ equalPos != -1 &&
+ param.substr(0, equalPos) == mapEntry.termsParameterName
+ ) {
+ // This is the parameter we are looking for.
+ offset += equalPos + 1;
+ length = param.length - equalPos - 1;
+ break;
+ }
+ offset += param.length + 1;
+ }
+
+ // Decode the terms using the charset defined in the search engine.
+ let terms;
+ try {
+ terms = Services.textToSubURI.UnEscapeAndConvert(
+ mapEntry.engine.queryCharset,
+ encodedTerms.replace(/\+/g, " ")
+ );
+ } catch (ex) {
+ // Decoding errors will cause this match to be ignored.
+ return gEmptyParseSubmissionResult;
+ }
+
+ let submission = new ParseSubmissionResult(
+ mapEntry.engine,
+ terms,
+ mapEntry.termsParameterName,
+ offset,
+ length
+ );
+ return submission;
+ },
+
+ /**
+ * Gets the WebExtensionPolicy for an add-on.
+ *
+ * @param {string} id
+ * The WebExtension id.
+ * @returns {WebExtensionPolicy}
+ */
+ async _getExtensionPolicy(id) {
+ let policy = WebExtensionPolicy.getByID(id);
+ if (!policy) {
+ let idPrefix = id.split("@")[0];
+ let path = `resource://search-extensions/${idPrefix}/`;
+ await 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;
+ },
+
+ // nsIObserver
+ observe(engine, topic, verb) {
+ switch (topic) {
+ case SearchUtils.TOPIC_ENGINE_MODIFIED:
+ switch (verb) {
+ case SearchUtils.MODIFIED_TYPE.LOADED:
+ engine = engine.QueryInterface(Ci.nsISearchEngine);
+ logConsole.debug("observe: Done installation of ", engine.name);
+ this._addEngineToStore(engine.wrappedJSObject);
+ // The addition of the engine to the store always triggers an ADDED
+ // or a CHANGED notification, that will trigger the task below.
+ break;
+ case SearchUtils.MODIFIED_TYPE.ADDED:
+ case SearchUtils.MODIFIED_TYPE.CHANGED:
+ case SearchUtils.MODIFIED_TYPE.REMOVED:
+ // Invalidate the map used to parse URLs to search engines.
+ this._parseSubmissionMap = null;
+ break;
+ }
+ break;
+
+ case "idle": {
+ this.idleService.removeIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
+ this._queuedIdle = false;
+ logConsole.debug(
+ "Reloading engines after idle due to configuration change"
+ );
+ this._maybeReloadEngines().catch(Cu.reportError);
+ break;
+ }
+
+ case QUIT_APPLICATION_TOPIC:
+ this._removeObservers();
+ break;
+
+ case TOPIC_LOCALES_CHANGE:
+ // Locale changed. Re-init. We rely on observers, because we can't
+ // return this promise to anyone.
+
+ // At the time of writing, when the user does a "Apply and Restart" for
+ // a new language the preferences code triggers the locales change and
+ // restart straight after, so we delay the check, which means we should
+ // be able to avoid the reload on shutdown, and we'll sort it out
+ // on next startup.
+ // This also helps to avoid issues with the add-on manager shutting
+ // down at the same time (see _reInit for more info).
+ Services.tm.dispatchToMainThread(() => {
+ if (!Services.startup.shuttingDown) {
+ this._maybeReloadEngines().catch(Cu.reportError);
+ }
+ });
+ break;
+ case Region.REGION_TOPIC:
+ if (verb == Region.REGION_UPDATED) {
+ logConsole.debug("Region updated:", Region.home);
+ this._maybeReloadEngines().catch(Cu.reportError);
+ }
+ break;
+ }
+ },
+
+ // nsITimerCallback
+ notify(timer) {
+ logConsole.debug("_notify: checking for updates");
+
+ if (
+ !Services.prefs.getBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "update",
+ true
+ )
+ ) {
+ return;
+ }
+
+ // Our timer has expired, but unfortunately, we can't get any data from it.
+ // Therefore, we need to walk our engine-list, looking for expired engines
+ var currentTime = Date.now();
+ logConsole.debug("currentTime:" + currentTime);
+ for (let e of this._engines.values()) {
+ let engine = e.wrappedJSObject;
+ if (!engine._hasUpdates) {
+ continue;
+ }
+
+ var expirTime = engine.getAttr("updateexpir");
+ logConsole.debug(
+ engine.name,
+ "expirTime:",
+ expirTime,
+ "updateURL:",
+ engine._updateURL,
+ "iconUpdateURL:",
+ engine._iconUpdateURL
+ );
+
+ var engineExpired = expirTime <= currentTime;
+
+ if (!expirTime || !engineExpired) {
+ logConsole.debug("skipping engine");
+ continue;
+ }
+
+ logConsole.debug(engine.name, "has expired");
+
+ engineUpdateService.update(engine);
+
+ // Schedule the next update
+ engineUpdateService.scheduleNextUpdate(engine);
+ } // end engine iteration
+ },
+
+ _addObservers() {
+ if (this._observersAdded) {
+ // There might be a race between synchronous and asynchronous
+ // initialization for which we try to register the observers twice.
+ return;
+ }
+ this._observersAdded = true;
+
+ Services.obs.addObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED);
+ Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC);
+ Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE);
+
+ this._settings.addObservers();
+
+ // The current stage of shutdown. Used to help analyze crash
+ // signatures in case of shutdown timeout.
+ let shutdownState = {
+ step: "Not started",
+ latestError: {
+ message: undefined,
+ stack: undefined,
+ },
+ };
+ OS.File.profileBeforeChange.addBlocker(
+ "Search service: shutting down",
+ () =>
+ (async () => {
+ // If we are in initialization, then don't attempt to save the settings.
+ // It is likely that shutdown will have caused the add-on manager to
+ // stop, which can cause initialization to fail.
+ // Hence at that stage, we could have broken settings which we don't
+ // want to write.
+ // The good news is, that if we don't write the settings here, we'll
+ // detect the out-of-date settings on next state, and automatically
+ // rebuild it.
+ if (!this._initialized) {
+ logConsole.warn(
+ "not saving settings on shutdown due to initializing."
+ );
+ return;
+ }
+
+ try {
+ await this._settings.shutdown(shutdownState);
+ } catch (ex) {
+ // Ensure that error is reported and that it causes tests
+ // to fail, otherwise ignore it.
+ Promise.reject(ex);
+ }
+ })(),
+
+ () => shutdownState
+ );
+ },
+ _observersAdded: false,
+
+ _removeObservers() {
+ if (this._ignoreListListener) {
+ IgnoreLists.unsubscribe(this._ignoreListListener);
+ delete this._ignoreListListener;
+ }
+ if (this._queuedIdle) {
+ this.idleService.removeIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
+ this._queuedIdle = false;
+ }
+
+ this._settings.removeObservers();
+
+ Services.obs.removeObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED);
+ Services.obs.removeObserver(this, QUIT_APPLICATION_TOPIC);
+ Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGE);
+ Services.obs.removeObserver(this, Region.REGION_TOPIC);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISearchService",
+ "nsIObserver",
+ "nsITimerCallback",
+ ]),
+};
+
+var engineUpdateService = {
+ scheduleNextUpdate(engine) {
+ var interval = engine._updateInterval || SEARCH_DEFAULT_UPDATE_INTERVAL;
+ var milliseconds = interval * 86400000; // |interval| is in days
+ engine.setAttr("updateexpir", Date.now() + milliseconds);
+ },
+
+ update(engine) {
+ engine = engine.wrappedJSObject;
+ logConsole.debug("update called for", engine._name);
+ if (
+ !Services.prefs.getBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "update",
+ true
+ ) ||
+ !engine._hasUpdates
+ ) {
+ return;
+ }
+
+ let testEngine = null;
+ let updateURL = engine._getURLOfType(SearchUtils.URL_TYPE.OPENSEARCH);
+ let updateURI =
+ updateURL && updateURL._hasRelation("self")
+ ? updateURL.getSubmission("", engine).uri
+ : SearchUtils.makeURI(engine._updateURL);
+ if (updateURI) {
+ if (engine.isAppProvided && !updateURI.schemeIs("https")) {
+ logConsole.debug("Invalid scheme for default engine update");
+ return;
+ }
+
+ logConsole.debug("updating", engine.name, updateURI.spec);
+ testEngine = new OpenSearchEngine({
+ uri: updateURI,
+ isAppProvided: false,
+ });
+ testEngine._engineToUpdate = engine;
+ testEngine._initFromURIAndLoad(updateURI);
+ } else {
+ logConsole.debug("invalid updateURI");
+ }
+
+ if (engine._iconUpdateURL) {
+ // If we're updating the engine too, use the new engine object,
+ // otherwise use the existing engine object.
+ (testEngine || engine)._setIcon(engine._iconUpdateURL, true);
+ }
+ },
+};
+
+XPCOMUtils.defineLazyServiceGetter(
+ SearchService.prototype,
+ "idleService",
+ "@mozilla.org/widget/useridleservice;1",
+ "nsIUserIdleService"
+);
+
+/**
+ * Handles getting and checking extensions against the allow list.
+ */
+class SearchDefaultOverrideAllowlistHandler {
+ /**
+ * @param {function} listener
+ * A listener for configuration update changes.
+ */
+ constructor(listener) {
+ this._remoteConfig = RemoteSettings(SearchUtils.SETTINGS_ALLOWLIST_KEY);
+ }
+
+ /**
+ * Determines if a search engine extension can override a default one
+ * according to the allow list.
+ *
+ * @param {object} extension
+ * The extension object (from add-on manager) that will override the
+ * app provided search engine.
+ * @param {string} appProvidedExtensionId
+ * The id of the search engine that will be overriden.
+ * @returns {boolean}
+ * Returns true if the search engine extension may override the app provided
+ * instance.
+ */
+ async canOverride(extension, appProvidedExtensionId) {
+ const overrideTable = await this._getAllowlist();
+
+ let entry = overrideTable.find(e => e.thirdPartyId == extension.id);
+ if (!entry) {
+ return false;
+ }
+
+ if (appProvidedExtensionId != entry.overridesId) {
+ return false;
+ }
+
+ let searchProvider =
+ extension.manifest.chrome_settings_overrides.search_provider;
+
+ return entry.urls.some(
+ e =>
+ searchProvider.search_url == e.search_url &&
+ searchProvider.search_form == e.search_form &&
+ searchProvider.search_url_get_params == e.search_url_get_params &&
+ searchProvider.search_url_post_params == e.search_url_post_params
+ );
+ }
+
+ /**
+ * Obtains the configuration from remote settings. This includes
+ * verifying the signature of the record within the database.
+ *
+ * If the signature in the database is invalid, the database will be wiped
+ * and the stored dump will be used, until the settings next update.
+ *
+ * Note that this may cause a network check of the certificate, but that
+ * should generally be quick.
+ *
+ * @returns {array}
+ * An array of objects in the database, or an empty array if none
+ * could be obtained.
+ */
+ async _getAllowlist() {
+ let result = [];
+ try {
+ result = await this._remoteConfig.get();
+ } catch (ex) {
+ // Don't throw an error just log it, just continue with no data, and hopefully
+ // a sync will fix things later on.
+ Cu.reportError(ex);
+ }
+ logConsole.debug("Allow list is:", result);
+ return result;
+ }
+}
+
+var EXPORTED_SYMBOLS = ["SearchService"];