1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/* eslint no-shadow: error, mozilla/no-aArgs: error */
6
7const { XPCOMUtils } = ChromeUtils.import(
8  "resource://gre/modules/XPCOMUtils.jsm"
9);
10const { PromiseUtils } = ChromeUtils.import(
11  "resource://gre/modules/PromiseUtils.jsm"
12);
13
14XPCOMUtils.defineLazyModuleGetters(this, {
15  AppConstants: "resource://gre/modules/AppConstants.jsm",
16  AddonManager: "resource://gre/modules/AddonManager.jsm",
17  IgnoreLists: "resource://gre/modules/IgnoreLists.jsm",
18  OpenSearchEngine: "resource://gre/modules/OpenSearchEngine.jsm",
19  Region: "resource://gre/modules/Region.jsm",
20  RemoteSettings: "resource://services-settings/remote-settings.js",
21  SearchEngine: "resource://gre/modules/SearchEngine.jsm",
22  SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.jsm",
23  SearchSettings: "resource://gre/modules/SearchSettings.jsm",
24  SearchStaticData: "resource://gre/modules/SearchStaticData.jsm",
25  SearchUtils: "resource://gre/modules/SearchUtils.jsm",
26  Services: "resource://gre/modules/Services.jsm",
27});
28
29XPCOMUtils.defineLazyPreferenceGetter(
30  this,
31  "gExperiment",
32  SearchUtils.BROWSER_SEARCH_PREF + "experiment",
33  false,
34  () => {
35    Services.search.wrappedJSObject._maybeReloadEngines();
36  }
37);
38
39XPCOMUtils.defineLazyGetter(this, "logConsole", () => {
40  return console.createInstance({
41    prefix: "SearchService",
42    maxLogLevel: SearchUtils.loggingEnabled ? "Debug" : "Warn",
43  });
44});
45
46const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
47const QUIT_APPLICATION_TOPIC = "quit-application";
48
49// The default engine update interval, in days. This is only used if an engine
50// specifies an updateURL, but not an updateInterval.
51const SEARCH_DEFAULT_UPDATE_INTERVAL = 7;
52
53// This is the amount of time we'll be idle for before applying any configuration
54// changes.
55const RECONFIG_IDLE_TIME_SEC = 5 * 60;
56
57// nsISearchParseSubmissionResult
58function ParseSubmissionResult(
59  engine,
60  terms,
61  termsParameterName,
62  termsOffset,
63  termsLength
64) {
65  this._engine = engine;
66  this._terms = terms;
67  this._termsParameterName = termsParameterName;
68  this._termsOffset = termsOffset;
69  this._termsLength = termsLength;
70}
71ParseSubmissionResult.prototype = {
72  get engine() {
73    return this._engine;
74  },
75  get terms() {
76    return this._terms;
77  },
78  get termsParameterName() {
79    return this._termsParameterName;
80  },
81  get termsOffset() {
82    return this._termsOffset;
83  },
84  get termsLength() {
85    return this._termsLength;
86  },
87  QueryInterface: ChromeUtils.generateQI(["nsISearchParseSubmissionResult"]),
88};
89
90const gEmptyParseSubmissionResult = Object.freeze(
91  new ParseSubmissionResult(null, "", "", -1, 0)
92);
93
94/**
95 * The search service handles loading and maintaining of search engines. It will
96 * also work out the default lists for each locale/region.
97 *
98 * @implements {nsISearchService}
99 */
100function SearchService() {
101  this._initObservers = PromiseUtils.defer();
102  this._engines = new Map();
103  this._settings = new SearchSettings(this);
104}
105
106SearchService.prototype = {
107  classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"),
108
109  // The current status of initialization. Note that it does not determine if
110  // initialization is complete, only if an error has been encountered so far.
111  _initRV: Cr.NS_OK,
112
113  // The boolean indicates that the initialization has started or not.
114  _initStarted: false,
115
116  // The boolean that indicates if initialization has been completed (successful
117  // or not).
118  _initialized: false,
119
120  // Indicates if we're already waiting for maybeReloadEngines to be called.
121  _maybeReloadDebounce: false,
122
123  // Indicates if we're currently in maybeReloadEngines.
124  _reloadingEngines: false,
125
126  // The engine selector singleton that is managing the engine configuration.
127  _engineSelector: null,
128
129  /**
130   * Various search engines may be ignored if their submission urls contain a
131   * string that is in the list. The list is controlled via remote settings.
132   */
133  _submissionURLIgnoreList: [],
134
135  /**
136   * Various search engines may be ignored if their load path is contained
137   * in this list. The list is controlled via remote settings.
138   */
139  _loadPathIgnoreList: [],
140
141  /**
142   * A map of engine display names to `SearchEngine`.
143   */
144  _engines: null,
145
146  /**
147   * An array of engine short names sorted into display order.
148   */
149  __sortedEngines: null,
150
151  /**
152   * A flag to prevent setting of useSavedOrder when there's non-user
153   * activity happening.
154   */
155  _dontSetUseSavedOrder: false,
156
157  /**
158   * An object containing the {id, locale} of the WebExtension for the default
159   * engine, as suggested by the configuration.
160   * For the legacy configuration, this is the user visible name.
161   */
162  _searchDefault: null,
163
164  /**
165   * An object containing the {id, locale} of the WebExtension for the default
166   * engine for private browsing mode, as suggested by the configuration.
167   * For the legacy configuration, this is the user visible name.
168   */
169  _searchPrivateDefault: null,
170
171  /**
172   * A Set of installed search extensions reported by AddonManager
173   * startup before SearchSevice has started. Will be installed
174   * during init().
175   */
176  _startupExtensions: new Set(),
177
178  /**
179   * A Set of removed search extensions reported by AddonManager
180   * startup before SearchSevice has started. Will be removed
181   * during init().
182   */
183  _startupRemovedExtensions: new Set(),
184
185  // A reference to the handler for the default override allow list.
186  _defaultOverrideAllowlist: null,
187
188  // This reflects the combined values of the prefs for enabling the separate
189  // private default UI, and for the user choosing a separate private engine.
190  // If either one is disabled, then we don't enable the separate private default.
191  get _separatePrivateDefault() {
192    return (
193      this._separatePrivateDefaultPrefValue &&
194      this._separatePrivateDefaultEnabledPrefValue
195    );
196  },
197
198  // If initialization has not been completed yet, perform synchronous
199  // initialization.
200  // Throws in case of initialization error.
201  _ensureInitialized() {
202    if (this._initialized) {
203      if (!Components.isSuccessCode(this._initRV)) {
204        logConsole.debug("_ensureInitialized: failure");
205        throw Components.Exception(
206          "SearchService previously failed to initialize",
207          this._initRV
208        );
209      }
210      return;
211    }
212
213    let err = new Error(
214      "Something tried to use the search service before it's been " +
215        "properly intialized. Please examine the stack trace to figure out what and " +
216        "where to fix it:\n"
217    );
218    err.message += err.stack;
219    throw err;
220  },
221
222  /**
223   * Asynchronous implementation of the initializer.
224   *
225   * @returns {number}
226   *   A Components.results success code on success, otherwise a failure code.
227   */
228  async _init() {
229    XPCOMUtils.defineLazyPreferenceGetter(
230      this,
231      "_separatePrivateDefaultPrefValue",
232      SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
233      false,
234      this._onSeparateDefaultPrefChanged.bind(this)
235    );
236
237    XPCOMUtils.defineLazyPreferenceGetter(
238      this,
239      "_separatePrivateDefaultEnabledPrefValue",
240      SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled",
241      false,
242      this._onSeparateDefaultPrefChanged.bind(this)
243    );
244
245    // We need to catch the region being updated
246    // during initialisation so we start listening
247    // straight away.
248    Services.obs.addObserver(this, Region.REGION_TOPIC);
249
250    try {
251      // Create the search engine selector.
252      this._engineSelector = new SearchEngineSelector(
253        this._handleConfigurationUpdated.bind(this)
254      );
255
256      // See if we have a settings file so we don't have to parse a bunch of XML.
257      let settings = await this._settings.get();
258
259      this._setupRemoteSettings().catch(Cu.reportError);
260
261      await this._loadEngines(settings);
262
263      // If we've got this far, but the application is now shutting down,
264      // then we need to abandon any further work, especially not writing
265      // the settings. We do this, because the add-on manager has also
266      // started shutting down and as a result, we might have an incomplete
267      // picture of the installed search engines. Writing the settings at
268      // this stage would potentially mean the user would loose their engine
269      // data.
270      // We will however, rebuild the settings on next start up if we detect
271      // it is necessary.
272      if (Services.startup.shuttingDown) {
273        logConsole.warn("_init: abandoning init due to shutting down");
274        this._initRV = Cr.NS_ERROR_ABORT;
275        this._initObservers.reject(this._initRV);
276        return this._initRV;
277      }
278
279      // Make sure the current list of engines is persisted, without the need to wait.
280      logConsole.debug("_init: engines loaded, writing settings");
281      this._addObservers();
282    } catch (ex) {
283      this._initRV = ex.result !== undefined ? ex.result : Cr.NS_ERROR_FAILURE;
284      logConsole.error("_init: failure initializing search:", ex.result);
285    }
286
287    this._initialized = true;
288    if (Components.isSuccessCode(this._initRV)) {
289      this._initObservers.resolve(this._initRV);
290    } else {
291      this._initObservers.reject(this._initRV);
292    }
293    Services.obs.notifyObservers(
294      null,
295      SearchUtils.TOPIC_SEARCH_SERVICE,
296      "init-complete"
297    );
298
299    logConsole.debug("Completed _init");
300    return this._initRV;
301  },
302
303  /**
304   * Obtains the remote settings for the search service. This should only be
305   * called from init(). Any subsequent updates to the remote settings are
306   * handled via a sync listener.
307   *
308   * For desktop, the initial remote settings are obtained from dumps in
309   * `services/settings/dumps/main/`.
310   *
311   * When enabling for Android, be aware the dumps are not shipped there, and
312   * hence the `get` may take a while to return.
313   */
314  async _setupRemoteSettings() {
315    // Now we have the values, listen for future updates.
316    let listener = this._handleIgnoreListUpdated.bind(this);
317
318    const current = await IgnoreLists.getAndSubscribe(listener);
319    // Only save the listener after the subscribe, otherwise for tests it might
320    // not be fully set up by the time we remove it again.
321    this._ignoreListListener = listener;
322
323    await this._handleIgnoreListUpdated({ data: { current } });
324    Services.obs.notifyObservers(
325      null,
326      SearchUtils.TOPIC_SEARCH_SERVICE,
327      "settings-update-complete"
328    );
329  },
330
331  /**
332   * This handles updating of the ignore list settings, and removing any ignored
333   * engines.
334   *
335   * @param {object} eventData
336   *   The event in the format received from RemoteSettings.
337   */
338  async _handleIgnoreListUpdated(eventData) {
339    logConsole.debug("_handleIgnoreListUpdated");
340    const {
341      data: { current },
342    } = eventData;
343
344    for (const entry of current) {
345      if (entry.id == "load-paths") {
346        this._loadPathIgnoreList = [...entry.matches];
347      } else if (entry.id == "submission-urls") {
348        this._submissionURLIgnoreList = [...entry.matches];
349      }
350    }
351
352    // If we have not finished initializing, then we wait for the initialization
353    // to complete.
354    if (!this.isInitialized) {
355      await this._initObservers;
356    }
357    // We try to remove engines manually, as this should be more efficient and
358    // we don't really want to cause a re-init as this upsets unit tests.
359    let engineRemoved = false;
360    for (let engine of this._engines.values()) {
361      if (this._engineMatchesIgnoreLists(engine)) {
362        await this.removeEngine(engine);
363        engineRemoved = true;
364      }
365    }
366    // If we've removed an engine, and we don't have any left, we need to
367    // reload the engines - it is possible the settings just had one engine in it,
368    // and that is now empty, so we need to load from our main list.
369    if (engineRemoved && !this._engines.size) {
370      this._maybeReloadEngines().catch(Cu.reportError);
371    }
372  },
373
374  /**
375   * Determines if a given engine matches the ignorelists or not.
376   *
377   * @param {Engine} engine
378   *   The engine to check against the ignorelists.
379   * @returns {boolean}
380   *   Returns true if the engine matches a ignorelists entry.
381   */
382  _engineMatchesIgnoreLists(engine) {
383    if (this._loadPathIgnoreList.includes(engine._loadPath)) {
384      return true;
385    }
386    let url = engine
387      ._getURLOfType("text/html")
388      .getSubmission("dummy", engine)
389      .uri.spec.toLowerCase();
390    if (
391      this._submissionURLIgnoreList.some(code =>
392        url.includes(code.toLowerCase())
393      )
394    ) {
395      return true;
396    }
397    return false;
398  },
399
400  async maybeSetAndOverrideDefault(extension) {
401    let searchProvider =
402      extension.manifest.chrome_settings_overrides.search_provider;
403    let engine = this._engines.get(searchProvider.name);
404    if (!engine || !engine.isAppProvided || engine.hidden) {
405      // If the engine is not application provided, then we shouldn't simply
406      // set default to it.
407      // If the engine is application provided, but hidden, then we don't
408      // switch to it, nor do we try to install it.
409      return {
410        canChangeToAppProvided: false,
411        canInstallEngine: !engine?.hidden,
412      };
413    }
414
415    if (!this._defaultOverrideAllowlist) {
416      this._defaultOverrideAllowlist = new SearchDefaultOverrideAllowlistHandler();
417    }
418
419    if (
420      extension.startupReason === "ADDON_INSTALL" ||
421      extension.startupReason === "ADDON_ENABLE"
422    ) {
423      // Don't allow an extension to set the default if it is already the default.
424      if (this.defaultEngine.name == searchProvider.name) {
425        return {
426          canChangeToAppProvided: false,
427          canInstallEngine: false,
428        };
429      }
430      if (
431        !(await this._defaultOverrideAllowlist.canOverride(
432          extension,
433          engine._extensionID
434        ))
435      ) {
436        logConsole.debug(
437          "Allowing default engine to be set to app-provided.",
438          extension.id
439        );
440        // We don't allow overriding the engine in this case, but we can allow
441        // the extension to change the default engine.
442        return {
443          canChangeToAppProvided: true,
444          canInstallEngine: false,
445        };
446      }
447      // We're ok to override.
448      engine.overrideWithExtension(extension.id, extension.manifest);
449      logConsole.debug(
450        "Allowing default engine to be set to app-provided and overridden.",
451        extension.id
452      );
453      return {
454        canChangeToAppProvided: true,
455        canInstallEngine: false,
456      };
457    }
458
459    if (
460      engine.getAttr("overriddenBy") == extension.id &&
461      (await this._defaultOverrideAllowlist.canOverride(
462        extension,
463        engine._extensionID
464      ))
465    ) {
466      engine.overrideWithExtension(extension.id, extension.manifest);
467      logConsole.debug(
468        "Re-enabling overriding of core extension by",
469        extension.id
470      );
471      return {
472        canChangeToAppProvided: true,
473        canInstallEngine: false,
474      };
475    }
476
477    return {
478      canChangeToAppProvided: false,
479      canInstallEngine: false,
480    };
481  },
482
483  /**
484   * Handles the search configuration being - adds a wait on the user
485   * being idle, before the search engine update gets handled.
486   */
487  _handleConfigurationUpdated() {
488    if (this._queuedIdle) {
489      return;
490    }
491
492    this._queuedIdle = true;
493
494    this.idleService.addIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
495  },
496
497  get _sortedEngines() {
498    if (!this.__sortedEngines) {
499      return this._buildSortedEngineList();
500    }
501    return this.__sortedEngines;
502  },
503
504  /**
505   * Returns the engine that is the default for this locale/region, ignoring any
506   * user changes to the default engine.
507   *
508   * @param {boolean} privateMode
509   *   Set to true to return the default engine in private mode,
510   *   false for normal mode.
511   * @returns {SearchEngine}
512   *   The engine that is default.
513   */
514  _originalDefaultEngine(privateMode = false) {
515    let defaultEngine = this._getEngineByWebExtensionDetails(
516      privateMode && this._searchPrivateDefault
517        ? this._searchPrivateDefault
518        : this._searchDefault
519    );
520
521    if (defaultEngine) {
522      return defaultEngine;
523    }
524
525    if (privateMode) {
526      // If for some reason we can't find the private mode engine, fall back
527      // to the non-private one.
528      return this._originalDefaultEngine(false);
529    }
530
531    // Something unexpected as happened. In order to recover the original
532    // default engine, use the first visible engine which is the best we can do.
533    return this._getSortedEngines(false)[0];
534  },
535
536  /**
537   * @returns {SearchEngine}
538   *   The engine that is the default for this locale/region, ignoring any
539   *   user changes to the default engine.
540   */
541  get originalDefaultEngine() {
542    return this._originalDefaultEngine();
543  },
544
545  /**
546   * @returns {SearchEngine}
547   *   The engine that is the default for this locale/region in private browsing
548   *   mode, ignoring any user changes to the default engine.
549   *   Note: if there is no default for this locale/region, then the non-private
550   *   browsing engine will be returned.
551   */
552  get originalPrivateDefaultEngine() {
553    return this._originalDefaultEngine(this._separatePrivateDefault);
554  },
555
556  resetToOriginalDefaultEngine() {
557    let originalDefaultEngine = this.originalDefaultEngine;
558    originalDefaultEngine.hidden = false;
559    this.defaultEngine = originalDefaultEngine;
560  },
561
562  /**
563   * Loads engines asynchronously.
564   *
565   * @param {object} settings
566   *   An object representing the search engine settings.
567   */
568  async _loadEngines(settings) {
569    logConsole.debug("_loadEngines: start");
570    let { engines, privateDefault } = await this._fetchEngineSelectorEngines();
571    this._setDefaultAndOrdersFromSelector(engines, privateDefault);
572
573    let newEngines = await this._loadEnginesFromConfig(engines);
574    for (let engine of newEngines) {
575      this._addEngineToStore(engine);
576    }
577
578    logConsole.debug(
579      "_loadEngines: loading",
580      this._startupExtensions.size,
581      "engines reported by AddonManager startup"
582    );
583    for (let extension of this._startupExtensions) {
584      await this._installExtensionEngine(
585        extension,
586        [SearchUtils.DEFAULT_TAG],
587        true
588      );
589    }
590    this._startupExtensions.clear();
591
592    this._loadEnginesFromSettings(settings.engines);
593
594    this._loadEnginesMetadataFromSettings(settings.engines);
595
596    logConsole.debug("_loadEngines: done");
597  },
598
599  /**
600   * Loads engines as specified by the configuration. We only expect
601   * configured engines here, user engines should not be listed.
602   *
603   * @param {array} engineConfigs
604   *   An array of engines configurations based on the schema.
605   * @returns {array.<nsISearchEngine>}
606   *   Returns an array of the loaded search engines. This may be
607   *   smaller than the original list if not all engines can be loaded.
608   */
609  async _loadEnginesFromConfig(engineConfigs) {
610    logConsole.debug("_loadEnginesFromConfig");
611    let engines = [];
612    for (let config of engineConfigs) {
613      try {
614        let engine = await this.makeEngineFromConfig(config);
615        engines.push(engine);
616      } catch (ex) {
617        console.error(
618          `Could not load engine ${
619            "webExtension" in config ? config.webExtension.id : "unknown"
620          }: ${ex}`
621        );
622      }
623    }
624    return engines;
625  },
626
627  /**
628   * Reloads engines asynchronously, but only when
629   * the service has already been initialized.
630   */
631  async _maybeReloadEngines() {
632    if (this._maybeReloadDebounce) {
633      logConsole.debug("We're already waiting to reload engines.");
634      return;
635    }
636
637    if (!this._initialized || this._reloadingEngines) {
638      this._maybeReloadDebounce = true;
639      // Schedule a reload to happen at most 10 seconds after the current run.
640      Services.tm.idleDispatchToMainThread(() => {
641        if (!this._maybeReloadDebounce) {
642          return;
643        }
644        this._maybeReloadDebounce = false;
645        this._maybeReloadEngines().catch(Cu.reportError);
646      }, 10000);
647      logConsole.debug(
648        "Post-poning maybeReloadEngines() as we're currently initializing."
649      );
650      return;
651    }
652
653    // Before entering `_reloadingEngines` get the settings which we'll need.
654    // This also ensures that any pending settings have finished being written,
655    // which could otherwise cause data loss.
656    let settings = await this._settings.get();
657
658    logConsole.debug("Running maybeReloadEngines");
659    this._reloadingEngines = true;
660
661    try {
662      await this._reloadEngines(settings);
663    } catch (ex) {
664      logConsole.error("maybeReloadEngines failed", ex);
665    }
666    this._reloadingEngines = false;
667    logConsole.debug("maybeReloadEngines complete");
668  },
669
670  async _reloadEngines(settings) {
671    // Capture the current engine state, in case we need to notify below.
672    const prevCurrentEngine = this._currentEngine;
673    const prevPrivateEngine = this._currentPrivateEngine;
674
675    // Ensure that we don't set the useSavedOrder flag whilst we're doing this.
676    // This isn't a user action, so we shouldn't be switching it.
677    this._dontSetUseSavedOrder = true;
678
679    // The order of work here is designed to avoid potential issues when updating
680    // the default engines, so that we're not removing active defaults or trying
681    // to set a default to something that hasn't been added yet. The order is:
682    //
683    // 1) Update exising engines that are in both the old and new configuration.
684    // 2) Add any new engines from the new configuration.
685    // 3) Update the default engines.
686    // 4) Remove any old engines.
687
688    let {
689      engines: originalConfigEngines,
690      privateDefault,
691    } = await this._fetchEngineSelectorEngines();
692
693    let enginesToRemove = [];
694    let configEngines = [...originalConfigEngines];
695    let oldEngineList = [...this._engines.values()];
696
697    for (let engine of oldEngineList) {
698      if (!engine.isAppProvided) {
699        continue;
700      }
701
702      let index = configEngines.findIndex(
703        e =>
704          e.webExtension.id == engine._extensionID &&
705          e.webExtension.locale == engine._locale
706      );
707
708      let policy, manifest, locale;
709      if (index == -1) {
710        // No engines directly match on id and locale, however, check to see
711        // if we have a new entry that matches on id and name - we might just
712        // be swapping the in-use locale.
713        let replacementEngines = configEngines.filter(
714          e => e.webExtension.id == engine._extensionID
715        );
716        // If there's no possible, or more than one, we treat these as distinct
717        // engines so we'll remove the existing engine and add new later if
718        // necessary.
719        if (replacementEngines.length != 1) {
720          enginesToRemove.push(engine);
721          continue;
722        }
723
724        policy = await this._getExtensionPolicy(engine._extensionID);
725        manifest = policy.extension.manifest;
726        locale =
727          replacementEngines[0].webExtension.locale || SearchUtils.DEFAULT_TAG;
728        if (locale != SearchUtils.DEFAULT_TAG) {
729          manifest = await policy.extension.getLocalizedManifest(locale);
730        }
731        if (
732          manifest.name !=
733          manifest.chrome_settings_overrides.search_provider.name.trim()
734        ) {
735          // No matching name, so just remove it.
736          enginesToRemove.push(engine);
737          continue;
738        }
739
740        // Update the index so we can handle the updating below.
741        index = configEngines.findIndex(
742          e =>
743            e.webExtension.id == replacementEngines[0].webExtension.id &&
744            e.webExtension.locale == replacementEngines[0].webExtension.locale
745        );
746      } else {
747        // This is an existing engine that we should update (we don't know if
748        // the configuration for this engine has changed or not).
749        policy = await this._getExtensionPolicy(engine._extensionID);
750
751        manifest = policy.extension.manifest;
752        locale = engine._locale || SearchUtils.DEFAULT_TAG;
753        if (locale != SearchUtils.DEFAULT_TAG) {
754          manifest = await policy.extension.getLocalizedManifest(locale);
755        }
756      }
757      engine._updateFromManifest(
758        policy.extension.id,
759        policy.extension.baseURI,
760        manifest,
761        locale,
762        configEngines[index]
763      );
764
765      configEngines.splice(index, 1);
766    }
767
768    // Any remaining configuration engines are ones that we need to add.
769    for (let engine of configEngines) {
770      try {
771        let newEngine = await this.makeEngineFromConfig(engine);
772        this._addEngineToStore(newEngine, true);
773      } catch (ex) {
774        logConsole.warn(
775          `Could not load engine ${
776            "webExtension" in engine ? engine.webExtension.id : "unknown"
777          }: ${ex}`
778        );
779      }
780    }
781    this._loadEnginesMetadataFromSettings(settings.engines);
782
783    // Now set the sort out the default engines and notify as appropriate.
784    this._currentEngine = null;
785    this._currentPrivateEngine = null;
786
787    this._setDefaultAndOrdersFromSelector(
788      originalConfigEngines,
789      privateDefault
790    );
791
792    // If the defaultEngine has changed between the previous load and this one,
793    // dispatch the appropriate notifications.
794    if (prevCurrentEngine && this.defaultEngine !== prevCurrentEngine) {
795      SearchUtils.notifyAction(
796        this._currentEngine,
797        SearchUtils.MODIFIED_TYPE.DEFAULT
798      );
799      // If we've not got a separate private active, notify update of the
800      // private so that the UI updates correctly.
801      if (!this._separatePrivateDefault) {
802        SearchUtils.notifyAction(
803          this._currentEngine,
804          SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
805        );
806      }
807    }
808    if (
809      this._separatePrivateDefault &&
810      prevPrivateEngine &&
811      this.defaultPrivateEngine !== prevPrivateEngine
812    ) {
813      SearchUtils.notifyAction(
814        this._currentPrivateEngine,
815        SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
816      );
817    }
818
819    // Finally, remove any engines that need removing.
820
821    for (let engine of enginesToRemove) {
822      // If we have other engines that use the same extension ID, then
823      // we do not want to remove the add-on - only remove the engine itself.
824      let inUseEngines = [...this._engines.values()].filter(
825        e => e._extensionID == engine._extensionID
826      );
827
828      if (inUseEngines.length <= 1) {
829        if (inUseEngines.length == 1 && inUseEngines[0] == engine) {
830          // No other engines are using this extension ID.
831
832          // The internal remove is done first to avoid a call to removeEngine
833          // which could adjust the sort order when we don't want it to.
834          this._internalRemoveEngine(engine);
835
836          let addon = await AddonManager.getAddonByID(engine._extensionID);
837          if (addon) {
838            // AddonManager won't call removeEngine if an engine with the
839            // WebExtension id doesn't exist in the search service.
840            await addon.uninstall();
841          }
842        }
843        // For the case where `inUseEngines[0] != engine`:
844        // This is a situation where there was an engine added earlier in this
845        // function with the same name.
846        // For example, eBay has the same name for both US and GB, but has
847        // a different domain and uses a different locale of the same
848        // WebExtension.
849        // The result of this is the earlier addition has already replaced
850        // the engine in `this._engines` (which is indexed by name), so all that
851        // needs to be done here is to pretend the old engine was removed
852        // which is notified below.
853      } else {
854        // More than one engine is using this extension ID, so we don't want to
855        // remove the add-on.
856        this._internalRemoveEngine(engine);
857      }
858      SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.REMOVED);
859    }
860
861    this._dontSetUseSavedOrder = false;
862    // Clear out the sorted engines settings, so that we re-sort it if necessary.
863    this.__sortedEngines = null;
864    Services.obs.notifyObservers(
865      null,
866      SearchUtils.TOPIC_SEARCH_SERVICE,
867      "engines-reloaded"
868    );
869  },
870
871  /**
872   * Test only - reset SearchService data. Ideally this should be replaced
873   */
874  reset() {
875    this._initialized = false;
876    this._initObservers = PromiseUtils.defer();
877    this._initStarted = false;
878    this._startupExtensions = new Set();
879    this._engines.clear();
880    this.__sortedEngines = null;
881    this._currentEngine = null;
882    this._currentPrivateEngine = null;
883    this._searchDefault = null;
884    this._searchPrivateDefault = null;
885    this._maybeReloadDebounce = false;
886  },
887
888  _addEngineToStore(engine, skipDuplicateCheck = false) {
889    if (this._engineMatchesIgnoreLists(engine)) {
890      logConsole.debug("_addEngineToStore: Ignoring engine");
891      return;
892    }
893
894    logConsole.debug("_addEngineToStore: Adding engine:", engine.name);
895
896    // See if there is an existing engine with the same name. However, if this
897    // engine is updating another engine, it's allowed to have the same name.
898    var hasSameNameAsUpdate =
899      engine._engineToUpdate && engine.name == engine._engineToUpdate.name;
900    if (
901      !skipDuplicateCheck &&
902      this._engines.has(engine.name) &&
903      !hasSameNameAsUpdate
904    ) {
905      logConsole.debug("_addEngineToStore: Duplicate engine found, aborting!");
906      return;
907    }
908
909    if (engine._engineToUpdate) {
910      // We need to replace engineToUpdate with the engine that just loaded.
911      var oldEngine = engine._engineToUpdate;
912
913      // Remove the old engine from the hash, since it's keyed by name, and our
914      // name might change (the update might have a new name).
915      this._engines.delete(oldEngine.name);
916
917      // Hack: we want to replace the old engine with the new one, but since
918      // people may be holding refs to the nsISearchEngine objects themselves,
919      // we'll just copy over all "private" properties (those without a getter
920      // or setter) from one object to the other.
921      for (var p in engine) {
922        if (!(engine.__lookupGetter__(p) || engine.__lookupSetter__(p))) {
923          oldEngine[p] = engine[p];
924        }
925      }
926      engine = oldEngine;
927      engine._engineToUpdate = null;
928
929      // Add the engine back
930      this._engines.set(engine.name, engine);
931      SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.CHANGED);
932    } else {
933      // Not an update, just add the new engine.
934      this._engines.set(engine.name, engine);
935      // Only add the engine to the list of sorted engines if the initial list
936      // has already been built (i.e. if this.__sortedEngines is non-null). If
937      // it hasn't, we're loading engines from disk and the sorted engine list
938      // will be built once we need it.
939      if (this.__sortedEngines && !this._dontSetUseSavedOrder) {
940        this.__sortedEngines.push(engine);
941        this._saveSortedEngineList();
942      }
943      SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.ADDED);
944    }
945
946    // Let the engine know it can start notifying new updates.
947    engine._engineAddedToStore = true;
948
949    if (engine._hasUpdates) {
950      // Schedule the engine's next update, if it isn't already.
951      if (!engine.getAttr("updateexpir")) {
952        engineUpdateService.scheduleNextUpdate(engine);
953      }
954    }
955  },
956
957  _loadEnginesMetadataFromSettings(engines) {
958    if (!engines) {
959      return;
960    }
961
962    for (let engine of engines) {
963      let name = engine._name;
964      if (this._engines.has(name)) {
965        logConsole.debug(
966          "_loadEnginesMetadataFromSettings, transfering metadata for",
967          name,
968          engine._metaData
969        );
970        let eng = this._engines.get(name);
971        // We used to store the alias in metadata.alias, in 1621892 that was
972        // changed to only store the user set alias in metadata.alias, remove
973        // it from metadata if it was previously set to the internal value.
974        if (eng._alias === engine?._metaData?.alias) {
975          delete engine._metaData.alias;
976        }
977        eng._metaData = engine._metaData || {};
978      }
979    }
980  },
981
982  _loadEnginesFromSettings(enginesCache) {
983    if (!enginesCache) {
984      return;
985    }
986
987    logConsole.debug(
988      "_loadEnginesFromSettings: Loading",
989      enginesCache.length,
990      "engines from settings"
991    );
992
993    let skippedEngines = 0;
994    for (let engineJSON of enginesCache) {
995      // We renamed isBuiltin to isAppProvided in 1631898,
996      // keep checking isBuiltin for older settings.
997      if (engineJSON._isAppProvided || engineJSON._isBuiltin) {
998        ++skippedEngines;
999        continue;
1000      }
1001
1002      // Some OpenSearch type engines are now obsolete and no longer supported.
1003      // These were application provided engines that used to use the OpenSearch
1004      // format before gecko transitioned to WebExtensions.
1005      // These will sometimes have been missed in migration due to various
1006      // reasons, and due to how the settings saves everything. We therefore
1007      // explicitly ignore them here to drop them, and let the rest of the code
1008      // fallback to the application/distribution default if necessary.
1009      let loadPath = engineJSON._loadPath?.toLowerCase();
1010      if (
1011        loadPath &&
1012        // Replaced by application provided in Firefox 79.
1013        (loadPath.startsWith("[distribution]") ||
1014          // Langpack engines moved in-app in Firefox 62.
1015          // Note: these may be prefixed by jar:,
1016          loadPath.includes("[app]/extensions/langpack") ||
1017          loadPath.includes("[other]/langpack") ||
1018          loadPath.includes("[profile]/extensions/langpack") ||
1019          // Old omni.ja engines also moved to in-app in Firefox 62.
1020          loadPath.startsWith("jar:[app]/omni.ja"))
1021      ) {
1022        continue;
1023      }
1024
1025      try {
1026        let engine = new SearchEngine({
1027          isAppProvided: false,
1028          loadPath: engineJSON._loadPath,
1029        });
1030        engine._initWithJSON(engineJSON);
1031        this._addEngineToStore(engine);
1032      } catch (ex) {
1033        logConsole.error(
1034          "Failed to load",
1035          engineJSON._name,
1036          "from settings:",
1037          ex,
1038          engineJSON
1039        );
1040      }
1041    }
1042
1043    if (skippedEngines) {
1044      logConsole.debug(
1045        "_loadEnginesFromSettings: skipped",
1046        skippedEngines,
1047        "built-in engines."
1048      );
1049    }
1050  },
1051
1052  async _fetchEngineSelectorEngines() {
1053    let locale = Services.locale.appLocaleAsBCP47;
1054    let region = Region.home || "default";
1055
1056    let channel = AppConstants.MOZ_APP_VERSION_DISPLAY.endsWith("esr")
1057      ? "esr"
1058      : AppConstants.MOZ_UPDATE_CHANNEL;
1059
1060    let {
1061      engines,
1062      privateDefault,
1063    } = await this._engineSelector.fetchEngineConfiguration({
1064      locale,
1065      region,
1066      channel,
1067      experiment: gExperiment,
1068      distroID: SearchUtils.distroID,
1069    });
1070
1071    for (let e of engines) {
1072      if (!e.webExtension) {
1073        e.webExtension = {};
1074      }
1075      e.webExtension.locale = e.webExtension?.locale ?? SearchUtils.DEFAULT_TAG;
1076    }
1077
1078    return { engines, privateDefault };
1079  },
1080
1081  _setDefaultAndOrdersFromSelector(engines, privateDefault) {
1082    const defaultEngine = engines[0];
1083    this._searchDefault = {
1084      id: defaultEngine.webExtension.id,
1085      locale: defaultEngine.webExtension.locale,
1086    };
1087    if (privateDefault) {
1088      this._searchPrivateDefault = {
1089        id: privateDefault.webExtension.id,
1090        locale: privateDefault.webExtension.locale,
1091      };
1092    }
1093  },
1094
1095  _saveSortedEngineList() {
1096    logConsole.debug("_saveSortedEngineList");
1097
1098    // Set the useSavedOrder attribute to indicate that from now on we should
1099    // use the user's order information stored in settings.
1100    this._settings.setAttribute("useSavedOrder", true);
1101
1102    var engines = this._getSortedEngines(true);
1103
1104    for (var i = 0; i < engines.length; ++i) {
1105      engines[i].setAttr("order", i + 1);
1106    }
1107  },
1108
1109  _buildSortedEngineList() {
1110    // We must initialise __sortedEngines here to avoid infinite recursion
1111    // in the case of tests which don't define a default search engine.
1112    // If there's no default defined, then we revert to the first item in the
1113    // sorted list, but we can't do that if we don't have a list.
1114    this.__sortedEngines = [];
1115
1116    // If the user has specified a custom engine order, read the order
1117    // information from the metadata instead of the default prefs.
1118    if (this._settings.getAttribute("useSavedOrder")) {
1119      logConsole.debug("_buildSortedEngineList: using saved order");
1120      let addedEngines = {};
1121
1122      // Flag to keep track of whether or not we need to call _saveSortedEngineList.
1123      let needToSaveEngineList = false;
1124
1125      for (let engine of this._engines.values()) {
1126        var orderNumber = engine.getAttr("order");
1127
1128        // Since the DB isn't regularly cleared, and engine files may disappear
1129        // without us knowing, we may already have an engine in this slot. If
1130        // that happens, we just skip it - it will be added later on as an
1131        // unsorted engine.
1132        if (orderNumber && !this.__sortedEngines[orderNumber - 1]) {
1133          this.__sortedEngines[orderNumber - 1] = engine;
1134          addedEngines[engine.name] = engine;
1135        } else {
1136          // We need to call _saveSortedEngineList so this gets sorted out.
1137          needToSaveEngineList = true;
1138        }
1139      }
1140
1141      // Filter out any nulls for engines that may have been removed
1142      var filteredEngines = this.__sortedEngines.filter(function(a) {
1143        return !!a;
1144      });
1145      if (this.__sortedEngines.length != filteredEngines.length) {
1146        needToSaveEngineList = true;
1147      }
1148      this.__sortedEngines = filteredEngines;
1149
1150      if (needToSaveEngineList) {
1151        this._saveSortedEngineList();
1152      }
1153
1154      // Array for the remaining engines, alphabetically sorted.
1155      let alphaEngines = [];
1156
1157      for (let engine of this._engines.values()) {
1158        if (!(engine.name in addedEngines)) {
1159          alphaEngines.push(engine);
1160        }
1161      }
1162
1163      const collator = new Intl.Collator();
1164      alphaEngines.sort((a, b) => {
1165        return collator.compare(a.name, b.name);
1166      });
1167      return (this.__sortedEngines = this.__sortedEngines.concat(alphaEngines));
1168    }
1169    logConsole.debug("_buildSortedEngineList: using default orders");
1170
1171    return (this.__sortedEngines = this._sortEnginesByDefaults(
1172      Array.from(this._engines.values())
1173    ));
1174  },
1175
1176  /**
1177   * Sorts engines by the default settings (prefs, configuration values).
1178   *
1179   * @param {Array} engines
1180   *   An array of engine objects to sort.
1181   * @returns {Array}
1182   *   The sorted array of engine objects.
1183   */
1184  _sortEnginesByDefaults(engines) {
1185    const sortedEngines = [];
1186    const addedEngines = new Set();
1187
1188    function maybeAddEngineToSort(engine) {
1189      if (!engine || addedEngines.has(engine.name)) {
1190        return;
1191      }
1192
1193      sortedEngines.push(engine);
1194      addedEngines.add(engine.name);
1195    }
1196
1197    // The original default engine should always be first in the list (except
1198    // for distros, that we should respect).
1199    const originalDefault = this.originalDefaultEngine;
1200    maybeAddEngineToSort(originalDefault);
1201
1202    // If there's a private default, and it is different to the normal
1203    // default, then it should be second in the list.
1204    const originalPrivateDefault = this.originalPrivateDefaultEngine;
1205    if (originalPrivateDefault && originalPrivateDefault != originalDefault) {
1206      maybeAddEngineToSort(originalPrivateDefault);
1207    }
1208
1209    let remainingEngines;
1210    const collator = new Intl.Collator();
1211
1212    remainingEngines = engines.filter(e => !addedEngines.has(e.name));
1213
1214    // We sort by highest orderHint first, then alphabetically by name.
1215    remainingEngines.sort((a, b) => {
1216      if (a._orderHint && b._orderHint) {
1217        if (a._orderHint == b._orderHint) {
1218          return collator.compare(a.name, b.name);
1219        }
1220        return b._orderHint - a._orderHint;
1221      }
1222      if (a._orderHint) {
1223        return -1;
1224      }
1225      if (b._orderHint) {
1226        return 1;
1227      }
1228      return collator.compare(a.name, b.name);
1229    });
1230
1231    return [...sortedEngines, ...remainingEngines];
1232  },
1233
1234  /**
1235   * Get a sorted array of engines.
1236   *
1237   * @param {boolean} withHidden
1238   *   True if hidden plugins should be included in the result.
1239   * @returns {Array<SearchEngine>}
1240   *   The sorted array.
1241   */
1242  _getSortedEngines(withHidden) {
1243    if (withHidden) {
1244      return this._sortedEngines;
1245    }
1246
1247    return this._sortedEngines.filter(function(engine) {
1248      return !engine.hidden;
1249    });
1250  },
1251
1252  // nsISearchService
1253  async init() {
1254    logConsole.debug("init");
1255    if (this._initStarted) {
1256      return this._initObservers.promise;
1257    }
1258
1259    TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
1260    this._initStarted = true;
1261    try {
1262      // Complete initialization by calling asynchronous initializer.
1263      await this._init();
1264      TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
1265    } catch (ex) {
1266      TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
1267      this._initObservers.reject(ex.result);
1268      throw ex;
1269    }
1270
1271    if (!Components.isSuccessCode(this._initRV)) {
1272      throw Components.Exception(
1273        "SearchService initialization failed",
1274        this._initRV
1275      );
1276    } else if (this._startupRemovedExtensions.size) {
1277      Services.tm.dispatchToMainThread(async () => {
1278        // Now that init() has successfully finished, we remove any engines
1279        // that have had their add-ons removed by the add-on manager.
1280        // We do this after init() has complete, as that allows us to use
1281        // removeEngine to look after any default engine changes as well.
1282        // This could cause a slight flicker on startup, but it should be
1283        // a rare action.
1284        logConsole.debug("Removing delayed extension engines");
1285        for (let id of this._startupRemovedExtensions) {
1286          for (let engine of this._getEnginesByExtensionID(id)) {
1287            // Only do this for non-application provided engines. We shouldn't
1288            // ever get application provided engines removed here, but just in case.
1289            if (!engine.isAppProvided) {
1290              await this.removeEngine(engine);
1291            }
1292          }
1293        }
1294        this._startupRemovedExtensions.clear();
1295      });
1296    }
1297    return this._initRV;
1298  },
1299
1300  get isInitialized() {
1301    return this._initialized;
1302  },
1303
1304  /**
1305   * Runs background checks for the search service. This is called from
1306   * BrowserGlue and may be run once per session if the user is idle for
1307   * long enough.
1308   */
1309  async runBackgroundChecks() {
1310    await this.init();
1311    await this._migrateLegacyEngines();
1312    await this._checkWebExtensionEngines();
1313  },
1314
1315  /**
1316   * Migrates legacy add-ons which used the OpenSearch definitions to
1317   * WebExtensions, if an equivalent WebExtension is installed.
1318   *
1319   * Run during the background checks.
1320   */
1321  async _migrateLegacyEngines() {
1322    logConsole.debug("Running migrate legacy engines");
1323
1324    const matchRegExp = /extensions\/(.*?)\.xpi!/i;
1325    for (let engine of this._engines.values()) {
1326      if (
1327        !engine.isAppProvided &&
1328        !engine._extensionID &&
1329        engine._loadPath.includes("[profile]/extensions/")
1330      ) {
1331        let match = engine._loadPath.match(matchRegExp);
1332        if (match?.[1]) {
1333          // There's a chance here that the WebExtension might not be
1334          // installed any longer, even though the engine is. We'll deal
1335          // with that in `checkWebExtensionEngines`.
1336          let engines = await this.getEnginesByExtensionID(match[1]);
1337          if (engines.length) {
1338            logConsole.debug(
1339              `Migrating ${engine.name} to WebExtension install`
1340            );
1341
1342            if (this.defaultEngine == engine) {
1343              this.defaultEngine = engines[0];
1344            }
1345            await this.removeEngine(engine);
1346          }
1347        }
1348      }
1349    }
1350
1351    logConsole.debug("Migrate legacy engines complete");
1352  },
1353
1354  /**
1355   * Checks if Search Engines associated with WebExtensions are valid and
1356   * up-to-date, and reports them via telemetry if not.
1357   *
1358   * Run during the background checks.
1359   */
1360  async _checkWebExtensionEngines() {
1361    logConsole.debug("Running check on WebExtension engines");
1362
1363    for (let engine of this._engines.values()) {
1364      if (
1365        engine.isAppProvided ||
1366        !engine._extensionID ||
1367        engine._extensionID == "set-via-policy" ||
1368        engine._extensionID == "set-via-user"
1369      ) {
1370        continue;
1371      }
1372
1373      let addon = await AddonManager.getAddonByID(engine._extensionID);
1374
1375      if (!addon) {
1376        logConsole.debug(
1377          `Add-on ${engine._extensionID} for search engine ${engine.name} is not installed!`
1378        );
1379        Services.telemetry.keyedScalarSet(
1380          "browser.searchinit.engine_invalid_webextension",
1381          engine._extensionID,
1382          1
1383        );
1384      } else if (!addon.isActive) {
1385        logConsole.debug(
1386          `Add-on ${engine._extensionID} for search engine ${engine.name} is not active!`
1387        );
1388        Services.telemetry.keyedScalarSet(
1389          "browser.searchinit.engine_invalid_webextension",
1390          engine._extensionID,
1391          2
1392        );
1393      } else {
1394        let policy = await this._getExtensionPolicy(engine._extensionID);
1395        let providerSettings =
1396          policy.extension.manifest?.chrome_settings_overrides?.search_provider;
1397
1398        if (!providerSettings) {
1399          logConsole.debug(
1400            `Add-on ${engine._extensionID} for search engine ${engine.name} no longer has an engine defined`
1401          );
1402          Services.telemetry.keyedScalarSet(
1403            "browser.searchinit.engine_invalid_webextension",
1404            engine._extensionID,
1405            4
1406          );
1407        } else if (engine.name != providerSettings.name) {
1408          logConsole.debug(
1409            `Add-on ${engine._extensionID} for search engine ${engine.name} has a different name!`
1410          );
1411          Services.telemetry.keyedScalarSet(
1412            "browser.searchinit.engine_invalid_webextension",
1413            engine._extensionID,
1414            5
1415          );
1416        } else if (!engine.checkSearchUrlMatchesManifest(providerSettings)) {
1417          logConsole.debug(
1418            `Add-on ${engine._extensionID} for search engine ${engine.name} has out-of-date manifest!`
1419          );
1420          Services.telemetry.keyedScalarSet(
1421            "browser.searchinit.engine_invalid_webextension",
1422            engine._extensionID,
1423            6
1424          );
1425        }
1426      }
1427    }
1428    logConsole.debug("WebExtension engine check complete");
1429  },
1430
1431  async getEngines() {
1432    await this.init();
1433    logConsole.debug("getEngines: getting all engines");
1434    return this._getSortedEngines(true);
1435  },
1436
1437  async getVisibleEngines() {
1438    await this.init(true);
1439    logConsole.debug("getVisibleEngines: getting all visible engines");
1440    return this._getSortedEngines(false);
1441  },
1442
1443  async getAppProvidedEngines() {
1444    await this.init();
1445
1446    return this._sortEnginesByDefaults(
1447      this._sortedEngines.filter(e => e.isAppProvided)
1448    );
1449  },
1450
1451  async getEnginesByExtensionID(extensionID) {
1452    await this.init();
1453    return this._getEnginesByExtensionID(extensionID);
1454  },
1455
1456  _getEnginesByExtensionID(extensionID) {
1457    logConsole.debug("getEngines: getting all engines for", extensionID);
1458    var engines = this._getSortedEngines(true).filter(function(engine) {
1459      return engine._extensionID == extensionID;
1460    });
1461    return engines;
1462  },
1463
1464  /**
1465   * Returns the engine associated with the name.
1466   *
1467   * @param {string} engineName
1468   *   The name of the engine.
1469   * @returns {SearchEngine}
1470   *   The associated engine if found, null otherwise.
1471   */
1472  getEngineByName(engineName) {
1473    this._ensureInitialized();
1474    return this._engines.get(engineName) || null;
1475  },
1476
1477  async getEngineByAlias(alias) {
1478    await this.init();
1479    for (var engine of this._engines.values()) {
1480      if (engine && engine.aliases.includes(alias)) {
1481        return engine;
1482      }
1483    }
1484    return null;
1485  },
1486
1487  /**
1488   * Returns the engine associated with the WebExtension details.
1489   *
1490   * @param {object} details
1491   * @param {string} details.id
1492   *   The WebExtension ID
1493   * @param {string} details.locale
1494   *   The WebExtension locale
1495   * @returns {nsISearchEngine|null}
1496   *   The found engine, or null if no engine matched.
1497   */
1498  _getEngineByWebExtensionDetails(details) {
1499    for (const engine of this._engines.values()) {
1500      if (
1501        engine._extensionID == details.id &&
1502        engine._locale == details.locale
1503      ) {
1504        return engine;
1505      }
1506    }
1507    return null;
1508  },
1509
1510  /**
1511   * Adds a search engine that is specified from enterprise policies.
1512   *
1513   * @param {object} details
1514   *   An object that simulates the manifest object from a WebExtension. See
1515   *   the idl for more details.
1516   */
1517  async addPolicyEngine(details) {
1518    await this._createAndAddEngine({
1519      extensionID: "set-via-policy",
1520      extensionBaseURI: "",
1521      isAppProvided: false,
1522      manifest: details,
1523    });
1524  },
1525
1526  /**
1527   * Updates a search engine that is specified from enterprise policies.
1528   *
1529   * @param {object} details
1530   *   An object that simulates the manifest object from a WebExtension. See
1531   *   the idl for more details.
1532   */
1533  async updatePolicyEngine(details) {
1534    let engine = this.getEngineByName(
1535      details.chrome_settings_overrides.search_provider.name
1536    );
1537    if (engine && !engine.isAppProvided) {
1538      engine._updateFromManifest(
1539        "set-via-policy",
1540        "",
1541        details,
1542        engine._locale || SearchUtils.DEFAULT_TAG
1543      );
1544    }
1545  },
1546
1547  /**
1548   * Adds a search engine that is specified by the user.
1549   *
1550   * @param {string} name
1551   * @param {string} url
1552   * @param {string} alias
1553   */
1554  async addUserEngine(name, url, alias) {
1555    await this._createAndAddEngine({
1556      extensionID: "set-via-user",
1557      extensionBaseURI: "",
1558      isAppProvided: false,
1559      manifest: {
1560        chrome_settings_overrides: {
1561          search_provider: {
1562            name,
1563            search_url: encodeURI(url),
1564            keyword: alias,
1565          },
1566        },
1567      },
1568    });
1569  },
1570
1571  /**
1572   * Creates and adds a WebExtension based engine.
1573   * Note: this is currently used for enterprise policy engines as well.
1574   *
1575   * @param {object} options
1576   * @param {string} options.extensionID
1577   *   The extension ID being added for the engine.
1578   * @param {nsIURI} [options.extensionBaseURI]
1579   *   The base URI of the extension.
1580   * @param {boolean} options.isAppProvided
1581   *   True if the WebExtension is built-in or installed into the system scope.
1582   * @param {object} options.manifest
1583   *   An object that represents the extension's manifest.
1584   * @param {stirng} [options.locale]
1585   *   The locale to use within the WebExtension. Defaults to the WebExtension's
1586   *   default locale.
1587   * @param {initEngine} [options.initEngine]
1588   *   Set to true if this engine is being loaded during initialisation.
1589   */
1590  async _createAndAddEngine({
1591    extensionID,
1592    extensionBaseURI,
1593    isAppProvided,
1594    manifest,
1595    locale = SearchUtils.DEFAULT_TAG,
1596    initEngine = false,
1597  }) {
1598    if (!extensionID) {
1599      throw Components.Exception(
1600        "Empty extensionID passed to _createAndAddEngine!",
1601        Cr.NS_ERROR_INVALID_ARG
1602      );
1603    }
1604    let searchProvider = manifest.chrome_settings_overrides.search_provider;
1605    let name = searchProvider.name.trim();
1606    logConsole.debug("_createAndAddEngine: Adding", name);
1607    let isCurrent = false;
1608
1609    // We install search extensions during the init phase, both built in
1610    // web extensions freshly installed (via addEnginesFromExtension) or
1611    // user installed extensions being reenabled calling this directly.
1612    if (!this._initialized && !isAppProvided && !initEngine) {
1613      await this.init();
1614    }
1615    // Special search engines (policy and user) are skipped for migration as
1616    // there would never have been an OpenSearch engine associated with those.
1617    if (extensionID && !extensionID.startsWith("set-via")) {
1618      for (let engine of this._engines.values()) {
1619        if (
1620          !engine.extensionID &&
1621          engine._loadPath.startsWith(`jar:[profile]/extensions/${extensionID}`)
1622        ) {
1623          // This is a legacy extension engine that needs to be migrated to WebExtensions.
1624          logConsole.debug("Migrating existing engine");
1625          isCurrent = isCurrent || this.defaultEngine == engine;
1626          await this.removeEngine(engine);
1627        }
1628      }
1629    }
1630
1631    let existingEngine = this._engines.get(name);
1632    if (existingEngine) {
1633      throw Components.Exception(
1634        "An engine with that name already exists!",
1635        Cr.NS_ERROR_FILE_ALREADY_EXISTS
1636      );
1637    }
1638
1639    let newEngine = new SearchEngine({
1640      name,
1641      isAppProvided,
1642      loadPath: `[other]addEngineWithDetails:${extensionID}`,
1643    });
1644    newEngine._initFromManifest(
1645      extensionID,
1646      extensionBaseURI,
1647      manifest,
1648      locale
1649    );
1650
1651    this._addEngineToStore(newEngine);
1652    if (isCurrent) {
1653      this.defaultEngine = newEngine;
1654    }
1655    return newEngine;
1656  },
1657
1658  /**
1659   * Called from the AddonManager when it either installs a new
1660   * extension containing a search engine definition or an upgrade
1661   * to an existing one.
1662   *
1663   * @param {object} extension
1664   *   An Extension object containing data about the extension.
1665   */
1666  async addEnginesFromExtension(extension) {
1667    logConsole.debug("addEnginesFromExtension: " + extension.id);
1668    // Treat add-on upgrade and downgrades the same - either way, the search
1669    // engine gets updated, not added. Generally, we don't expect a downgrade,
1670    // but just in case...
1671    if (
1672      extension.startupReason == "ADDON_UPGRADE" ||
1673      extension.startupReason == "ADDON_DOWNGRADE"
1674    ) {
1675      // Bug 1679861 An a upgrade or downgrade could be adding a search engine
1676      // that was not in a prior version, or the addon may have been blocklisted.
1677      // In either case, there will not be an existing engine.
1678      let existing = await this._upgradeExtensionEngine(extension);
1679      if (existing?.length) {
1680        return existing;
1681      }
1682    }
1683
1684    if (extension.isAppProvided) {
1685      // If we are in the middle of initialization or reloading engines,
1686      // don't add the engine here. This has been called as the result
1687      // of makeEngineFromConfig installing the extension, and that is already
1688      // handling the addition of the engine.
1689      if (this._initialized && !this._reloadingEngines) {
1690        let { engines } = await this._fetchEngineSelectorEngines();
1691        let inConfig = engines.filter(el => el.webExtension.id == extension.id);
1692        if (inConfig.length) {
1693          return this._installExtensionEngine(
1694            extension,
1695            inConfig.map(el => el.webExtension.locale)
1696          );
1697        }
1698      }
1699      logConsole.debug("addEnginesFromExtension: Ignoring builtIn engine.");
1700      return [];
1701    }
1702
1703    // If we havent started SearchService yet, store this extension
1704    // to install in SearchService.init().
1705    if (!this._initialized) {
1706      this._startupExtensions.add(extension);
1707      return [];
1708    }
1709
1710    return this._installExtensionEngine(extension, [SearchUtils.DEFAULT_TAG]);
1711  },
1712
1713  /**
1714   * Called when we see an upgrade to an existing search extension.
1715   *
1716   * @param {object} extension
1717   *   An Extension object containing data about the extension.
1718   */
1719  async _upgradeExtensionEngine(extension) {
1720    let { engines } = await this._fetchEngineSelectorEngines();
1721    let extensionEngines = await this.getEnginesByExtensionID(extension.id);
1722
1723    for (let engine of extensionEngines) {
1724      let manifest = extension.manifest;
1725      let locale = engine._locale || SearchUtils.DEFAULT_TAG;
1726      if (locale != SearchUtils.DEFAULT_TAG) {
1727        manifest = await extension.getLocalizedManifest(locale);
1728      }
1729      let configuration =
1730        engines.find(
1731          e =>
1732            e.webExtension.id == extension.id && e.webExtension.locale == locale
1733        ) ?? {};
1734
1735      let originalName = engine.name;
1736      let name = manifest.chrome_settings_overrides.search_provider.name.trim();
1737      if (originalName != name && this._engines.has(name)) {
1738        throw new Error("Can't upgrade to the same name as an existing engine");
1739      }
1740
1741      let isDefault = engine == this.defaultEngine;
1742      let isDefaultPrivate = engine == this.defaultPrivateEngine;
1743
1744      engine._updateFromManifest(
1745        extension.id,
1746        extension.baseURI,
1747        manifest,
1748        locale,
1749        configuration
1750      );
1751
1752      if (originalName != engine.name) {
1753        this._engines.delete(originalName);
1754        this._engines.set(engine.name, engine);
1755        if (isDefault) {
1756          this._settings.setVerifiedAttribute("current", engine.name);
1757        }
1758        if (isDefaultPrivate) {
1759          this._settings.setVerifiedAttribute("private", engine.name);
1760        }
1761        this.__sortedEngines = null;
1762      }
1763    }
1764    return extensionEngines;
1765  },
1766
1767  /**
1768   * Create an engine object from the search configuration details.
1769   *
1770   * @param {object} config
1771   *   The configuration object that defines the details of the engine
1772   *   webExtensionId etc.
1773   * @returns {nsISearchEngine}
1774   *   Returns the search engine object.
1775   */
1776  async makeEngineFromConfig(config) {
1777    logConsole.debug("makeEngineFromConfig:", config);
1778    let policy = await this._getExtensionPolicy(config.webExtension.id);
1779    let locale =
1780      "locale" in config.webExtension
1781        ? config.webExtension.locale
1782        : SearchUtils.DEFAULT_TAG;
1783
1784    let manifest = policy.extension.manifest;
1785    if (locale != SearchUtils.DEFAULT_TAG) {
1786      manifest = await policy.extension.getLocalizedManifest(locale);
1787    }
1788
1789    let engine = new SearchEngine({
1790      name: manifest.chrome_settings_overrides.search_provider.name.trim(),
1791      isAppProvided: policy.extension.isAppProvided,
1792      loadPath: `[other]addEngineWithDetails:${policy.extension.id}`,
1793    });
1794    engine._initFromManifest(
1795      policy.extension.id,
1796      policy.extension.baseURI,
1797      manifest,
1798      locale,
1799      config
1800    );
1801    return engine;
1802  },
1803
1804  async _installExtensionEngine(extension, locales, initEngine = false) {
1805    logConsole.debug("installExtensionEngine:", extension.id);
1806
1807    let installLocale = async locale => {
1808      let manifest =
1809        locale == SearchUtils.DEFAULT_TAG
1810          ? extension.manifest
1811          : await extension.getLocalizedManifest(locale);
1812      return this._addEngineForManifest(
1813        extension,
1814        manifest,
1815        locale,
1816        initEngine
1817      );
1818    };
1819
1820    let engines = [];
1821    for (let locale of locales) {
1822      logConsole.debug(
1823        "addEnginesFromExtension: installing:",
1824        extension.id,
1825        ":",
1826        locale
1827      );
1828      engines.push(await installLocale(locale));
1829    }
1830    return engines;
1831  },
1832
1833  async _addEngineForManifest(
1834    extension,
1835    manifest,
1836    locale = SearchUtils.DEFAULT_TAG,
1837    initEngine = false
1838  ) {
1839    // If we're in the startup cycle, and we've already loaded this engine,
1840    // then we use the existing one rather than trying to start from scratch.
1841    // This also avoids console errors.
1842    if (extension.startupReason == "APP_STARTUP") {
1843      let engine = this._getEngineByWebExtensionDetails({
1844        id: extension.id,
1845        locale,
1846      });
1847      if (engine) {
1848        logConsole.debug(
1849          "Engine already loaded via settings, skipping due to APP_STARTUP:",
1850          extension.id
1851        );
1852        return engine;
1853      }
1854    }
1855
1856    return this._createAndAddEngine({
1857      extensionID: extension.id,
1858      extensionBaseURI: extension.baseURI,
1859      isAppProvided: extension.isAppProvided,
1860      manifest,
1861      locale,
1862      initEngine,
1863    });
1864  },
1865
1866  async addOpenSearchEngine(engineURL, iconURL) {
1867    logConsole.debug("addEngine: Adding", engineURL);
1868    await this.init();
1869    let errCode;
1870    try {
1871      var engine = new OpenSearchEngine();
1872      engine._setIcon(iconURL, false);
1873      errCode = await new Promise(resolve => {
1874        engine._install(engineURL, errorCode => {
1875          resolve(errorCode);
1876        });
1877      });
1878      if (errCode) {
1879        throw errCode;
1880      }
1881    } catch (ex) {
1882      throw Components.Exception(
1883        "addEngine: Error adding engine:\n" + ex,
1884        errCode || Cr.NS_ERROR_FAILURE
1885      );
1886    }
1887    return engine;
1888  },
1889
1890  async removeWebExtensionEngine(id) {
1891    if (!this.isInitialized) {
1892      logConsole.debug("Delaying removing extension engine on startup:", id);
1893      this._startupRemovedExtensions.add(id);
1894      return;
1895    }
1896
1897    logConsole.debug("removeWebExtensionEngine:", id);
1898    for (let engine of this._getEnginesByExtensionID(id)) {
1899      await this.removeEngine(engine);
1900    }
1901  },
1902
1903  async removeEngine(engine) {
1904    await this.init();
1905    if (!engine) {
1906      throw Components.Exception(
1907        "no engine passed to removeEngine!",
1908        Cr.NS_ERROR_INVALID_ARG
1909      );
1910    }
1911
1912    var engineToRemove = null;
1913    for (var e of this._engines.values()) {
1914      if (engine.wrappedJSObject == e) {
1915        engineToRemove = e;
1916      }
1917    }
1918
1919    if (!engineToRemove) {
1920      throw Components.Exception(
1921        "removeEngine: Can't find engine to remove!",
1922        Cr.NS_ERROR_FILE_NOT_FOUND
1923      );
1924    }
1925
1926    if (engineToRemove == this.defaultEngine) {
1927      this._findAndSetNewDefaultEngine({
1928        privateMode: false,
1929        excludeEngineName: engineToRemove.name,
1930      });
1931    }
1932
1933    // Bug 1575649 - We can't just check the default private engine here when
1934    // we're not using separate, as that re-checks the normal default, and
1935    // triggers update of the default search engine, which messes up various
1936    // tests. Really, removeEngine should always commit to updating any
1937    // changed defaults.
1938    if (
1939      this._separatePrivateDefault &&
1940      engineToRemove == this.defaultPrivateEngine
1941    ) {
1942      this._findAndSetNewDefaultEngine({
1943        privateMode: true,
1944        excludeEngineName: engineToRemove.name,
1945      });
1946    }
1947
1948    if (engineToRemove._isAppProvided) {
1949      // Just hide it (the "hidden" setter will notify) and remove its alias to
1950      // avoid future conflicts with other engines.
1951      engineToRemove.hidden = true;
1952      engineToRemove.alias = null;
1953    } else {
1954      // Remove the engine file from disk if we had a legacy file in the profile.
1955      if (engineToRemove._filePath) {
1956        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
1957        file.persistentDescriptor = engineToRemove._filePath;
1958        if (file.exists()) {
1959          file.remove(false);
1960        }
1961        engineToRemove._filePath = null;
1962      }
1963      this._internalRemoveEngine(engineToRemove);
1964
1965      // Since we removed an engine, we may need to update the preferences.
1966      if (!this._dontSetUseSavedOrder) {
1967        this._saveSortedEngineList();
1968      }
1969    }
1970    SearchUtils.notifyAction(engineToRemove, SearchUtils.MODIFIED_TYPE.REMOVED);
1971  },
1972
1973  _internalRemoveEngine(engine) {
1974    // Remove the engine from _sortedEngines
1975    if (this.__sortedEngines) {
1976      var index = this.__sortedEngines.indexOf(engine);
1977      if (index == -1) {
1978        throw Components.Exception(
1979          "Can't find engine to remove in _sortedEngines!",
1980          Cr.NS_ERROR_FAILURE
1981        );
1982      }
1983      this.__sortedEngines.splice(index, 1);
1984    }
1985
1986    // Remove the engine from the internal store
1987    this._engines.delete(engine.name);
1988  },
1989
1990  async moveEngine(engine, newIndex) {
1991    await this.init();
1992    if (newIndex > this._sortedEngines.length || newIndex < 0) {
1993      throw Components.Exception("moveEngine: Index out of bounds!");
1994    }
1995    if (
1996      !(engine instanceof Ci.nsISearchEngine) &&
1997      !(engine instanceof SearchEngine)
1998    ) {
1999      throw Components.Exception(
2000        "moveEngine: Invalid engine passed to moveEngine!",
2001        Cr.NS_ERROR_INVALID_ARG
2002      );
2003    }
2004    if (engine.hidden) {
2005      throw Components.Exception(
2006        "moveEngine: Can't move a hidden engine!",
2007        Cr.NS_ERROR_FAILURE
2008      );
2009    }
2010
2011    engine = engine.wrappedJSObject;
2012
2013    var currentIndex = this._sortedEngines.indexOf(engine);
2014    if (currentIndex == -1) {
2015      throw Components.Exception(
2016        "moveEngine: Can't find engine to move!",
2017        Cr.NS_ERROR_UNEXPECTED
2018      );
2019    }
2020
2021    // Our callers only take into account non-hidden engines when calculating
2022    // newIndex, but we need to move it in the array of all engines, so we
2023    // need to adjust newIndex accordingly. To do this, we count the number
2024    // of hidden engines in the list before the engine that we're taking the
2025    // place of. We do this by first finding newIndexEngine (the engine that
2026    // we were supposed to replace) and then iterating through the complete
2027    // engine list until we reach it, increasing newIndex for each hidden
2028    // engine we find on our way there.
2029    //
2030    // This could be further simplified by having our caller pass in
2031    // newIndexEngine directly instead of newIndex.
2032    var newIndexEngine = this._getSortedEngines(false)[newIndex];
2033    if (!newIndexEngine) {
2034      throw Components.Exception(
2035        "moveEngine: Can't find engine to replace!",
2036        Cr.NS_ERROR_UNEXPECTED
2037      );
2038    }
2039
2040    for (var i = 0; i < this._sortedEngines.length; ++i) {
2041      if (newIndexEngine == this._sortedEngines[i]) {
2042        break;
2043      }
2044      if (this._sortedEngines[i].hidden) {
2045        newIndex++;
2046      }
2047    }
2048
2049    if (currentIndex == newIndex) {
2050      return;
2051    } // nothing to do!
2052
2053    // Move the engine
2054    var movedEngine = this.__sortedEngines.splice(currentIndex, 1)[0];
2055    this.__sortedEngines.splice(newIndex, 0, movedEngine);
2056
2057    SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.CHANGED);
2058
2059    // Since we moved an engine, we need to update the preferences.
2060    this._saveSortedEngineList();
2061  },
2062
2063  restoreDefaultEngines() {
2064    this._ensureInitialized();
2065    for (let e of this._engines.values()) {
2066      // Unhide all default engines
2067      if (e.hidden && e.isAppProvided) {
2068        e.hidden = false;
2069      }
2070    }
2071  },
2072
2073  /**
2074   * Helper function to find a new default engine and set it. This could
2075   * be used if there is not default set yet, or if the current default is
2076   * being removed.
2077   *
2078   * The new default will be chosen from (in order):
2079   *
2080   * - Existing default from configuration, if it is not hidden.
2081   * - The first non-hidden engine that is a general search engine.
2082   * - If all other engines are hidden, unhide the default from the configuration.
2083   * - If the default from the configuration is the one being removed, unhide
2084   *   the first general search engine, or first visible engine.
2085   *
2086   * @param {boolean} privateMode
2087   *   If true, returns the default engine for private browsing mode, otherwise
2088   *   the default engine for the normal mode. Note, this function does not
2089   *   check the "separatePrivateDefault" preference - that is up to the caller.
2090   * @param {string} [excludeEngineName]
2091   *   Exclude the given engine name from the search for a new engine. This is
2092   *   typically used when removing engines to ensure we do not try to reselect
2093   *   the same engine again.
2094   * @returns {nsISearchEngine|null}
2095   *   The appropriate search engine, or null if one could not be determined.
2096   */
2097  _findAndSetNewDefaultEngine({ privateMode, excludeEngineName = "" }) {
2098    const currentEngineProp = privateMode
2099      ? "_currentPrivateEngine"
2100      : "_currentEngine";
2101
2102    // First to the original default engine...
2103    let newDefault = privateMode
2104      ? this.originalPrivateDefaultEngine
2105      : this.originalDefaultEngine;
2106
2107    if (
2108      !newDefault ||
2109      newDefault.hidden ||
2110      newDefault.name == excludeEngineName
2111    ) {
2112      let sortedEngines = this._getSortedEngines(false);
2113      let generalSearchEngines = sortedEngines.filter(
2114        e => e.isGeneralPurposeEngine
2115      );
2116
2117      // then to the first visible general search engine that isn't excluded...
2118      let firstVisible = generalSearchEngines.find(
2119        e => e.name != excludeEngineName
2120      );
2121      if (firstVisible) {
2122        newDefault = firstVisible;
2123      } else if (newDefault) {
2124        // then to the original if it is not the one that is excluded...
2125        if (newDefault.name != excludeEngineName) {
2126          newDefault.hidden = false;
2127        } else {
2128          newDefault = null;
2129        }
2130      }
2131
2132      // and finally as a last resort we unhide the first engine
2133      // even if the name is the same as the excluded one (should never happen).
2134      if (!newDefault) {
2135        if (!firstVisible) {
2136          sortedEngines = this._getSortedEngines(true);
2137          firstVisible = sortedEngines.find(e => e.isGeneralPurposeEngine);
2138          if (!firstVisible) {
2139            firstVisible = sortedEngines[0];
2140          }
2141        }
2142        if (firstVisible) {
2143          firstVisible.hidden = false;
2144          newDefault = firstVisible;
2145        }
2146      }
2147    }
2148    // We tried out best but something went very wrong.
2149    if (!newDefault) {
2150      logConsole.error("Could not find a replacement default engine.");
2151      return null;
2152    }
2153
2154    // If the current engine wasn't set or was hidden, we used a fallback
2155    // to pick a new current engine. As soon as we return it, this new
2156    // current engine will become user-visible, so we should persist it.
2157    // by calling the setter.
2158    if (privateMode) {
2159      this.defaultPrivateEngine = newDefault;
2160    } else {
2161      this.defaultEngine = newDefault;
2162    }
2163
2164    return this[currentEngineProp];
2165  },
2166
2167  /**
2168   * Helper function to get the current default engine.
2169   *
2170   * @param {boolean} privateMode
2171   *   If true, returns the default engine for private browsing mode, otherwise
2172   *   the default engine for the normal mode. Note, this function does not
2173   *   check the "separatePrivateDefault" preference - that is up to the caller.
2174   * @returns {nsISearchEngine|null}
2175   *   The appropriate search engine, or null if one could not be determined.
2176   */
2177  _getEngineDefault(privateMode) {
2178    this._ensureInitialized();
2179    const currentEngineProp = privateMode
2180      ? "_currentPrivateEngine"
2181      : "_currentEngine";
2182
2183    if (this[currentEngineProp] && !this[currentEngineProp].hidden) {
2184      return this[currentEngineProp];
2185    }
2186
2187    // No default loaded, so find it from settings.
2188    const attributeName = privateMode ? "private" : "current";
2189    let name = this._settings.getAttribute(attributeName);
2190    let engine = this.getEngineByName(name);
2191    if (
2192      engine &&
2193      (engine.isAppProvided ||
2194        this._settings.getVerifiedAttribute(attributeName))
2195    ) {
2196      // If the current engine is a default one, we can relax the
2197      // verification hash check to reduce the annoyance for users who
2198      // backup/sync their profile in custom ways.
2199      this[currentEngineProp] = engine;
2200    }
2201    if (!name) {
2202      this[currentEngineProp] = privateMode
2203        ? this.originalPrivateDefaultEngine
2204        : this.originalDefaultEngine;
2205    }
2206
2207    if (this[currentEngineProp] && !this[currentEngineProp].hidden) {
2208      return this[currentEngineProp];
2209    }
2210    // No default in settings or it is hidden, so find the new default.
2211    return this._findAndSetNewDefaultEngine({ privateMode });
2212  },
2213
2214  /**
2215   * Helper function to set the current default engine.
2216   *
2217   * @param {boolean} privateMode
2218   *   If true, sets the default engine for private browsing mode, otherwise
2219   *   sets the default engine for the normal mode. Note, this function does not
2220   *   check the "separatePrivateDefault" preference - that is up to the caller.
2221   * @param {nsISearchEngine} newEngine
2222   *   The search engine to select
2223   */
2224  _setEngineDefault(privateMode, newEngine) {
2225    this._ensureInitialized();
2226    // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers),
2227    // and sometimes we get raw Engine JS objects (callers in this file), so
2228    // handle both.
2229    if (
2230      !(newEngine instanceof Ci.nsISearchEngine) &&
2231      !(newEngine instanceof SearchEngine)
2232    ) {
2233      throw Components.Exception(
2234        "Invalid argument passed to defaultEngine setter",
2235        Cr.NS_ERROR_INVALID_ARG
2236      );
2237    }
2238
2239    const newCurrentEngine = this.getEngineByName(newEngine.name);
2240    if (!newCurrentEngine) {
2241      throw Components.Exception(
2242        "Can't find engine in store!",
2243        Cr.NS_ERROR_UNEXPECTED
2244      );
2245    }
2246
2247    if (!newCurrentEngine.isAppProvided) {
2248      // If a non default engine is being set as the current engine, ensure
2249      // its loadPath has a verification hash.
2250      if (!newCurrentEngine._loadPath) {
2251        newCurrentEngine._loadPath = "[other]unknown";
2252      }
2253      let loadPathHash = SearchUtils.getVerificationHash(
2254        newCurrentEngine._loadPath
2255      );
2256      let currentHash = newCurrentEngine.getAttr("loadPathHash");
2257      if (!currentHash || currentHash != loadPathHash) {
2258        newCurrentEngine.setAttr("loadPathHash", loadPathHash);
2259        SearchUtils.notifyAction(
2260          newCurrentEngine,
2261          SearchUtils.MODIFIED_TYPE.CHANGED
2262        );
2263      }
2264    }
2265
2266    const currentEngine = `_current${privateMode ? "Private" : ""}Engine`;
2267
2268    if (newCurrentEngine == this[currentEngine]) {
2269      return;
2270    }
2271
2272    // Ensure that we reset an engine override if it was previously overridden.
2273    this[currentEngine]?.removeExtensionOverride();
2274
2275    this[currentEngine] = newCurrentEngine;
2276
2277    // If we change the default engine in the future, that change should impact
2278    // users who have switched away from and then back to the build's "default"
2279    // engine. So clear the user pref when the currentEngine is set to the
2280    // build's default engine, so that the currentEngine getter falls back to
2281    // whatever the default is.
2282    let newName = this[currentEngine].name;
2283    const originalDefault = privateMode
2284      ? this.originalPrivateDefaultEngine
2285      : this.originalDefaultEngine;
2286    if (this[currentEngine] == originalDefault) {
2287      newName = "";
2288    }
2289
2290    this._settings.setVerifiedAttribute(
2291      privateMode ? "private" : "current",
2292      newName
2293    );
2294
2295    SearchUtils.notifyAction(
2296      this[currentEngine],
2297      SearchUtils.MODIFIED_TYPE[privateMode ? "DEFAULT_PRIVATE" : "DEFAULT"]
2298    );
2299    // If we've not got a separate private active, notify update of the
2300    // private so that the UI updates correctly.
2301    if (!privateMode && !this._separatePrivateDefault) {
2302      SearchUtils.notifyAction(
2303        this[currentEngine],
2304        SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
2305      );
2306    }
2307  },
2308
2309  get defaultEngine() {
2310    return this._getEngineDefault(false);
2311  },
2312
2313  set defaultEngine(newEngine) {
2314    this._setEngineDefault(false, newEngine);
2315  },
2316
2317  get defaultPrivateEngine() {
2318    return this._getEngineDefault(this._separatePrivateDefault);
2319  },
2320
2321  set defaultPrivateEngine(newEngine) {
2322    if (!this._separatePrivateDefaultPrefValue) {
2323      Services.prefs.setBoolPref(
2324        SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
2325        true
2326      );
2327    }
2328    this._setEngineDefault(this._separatePrivateDefault, newEngine);
2329  },
2330
2331  async getDefault() {
2332    await this.init();
2333    return this.defaultEngine;
2334  },
2335
2336  async setDefault(engine) {
2337    await this.init();
2338    return (this.defaultEngine = engine);
2339  },
2340
2341  async getDefaultPrivate() {
2342    await this.init();
2343    return this.defaultPrivateEngine;
2344  },
2345
2346  async setDefaultPrivate(engine) {
2347    await this.init();
2348    return (this.defaultPrivateEngine = engine);
2349  },
2350
2351  _onSeparateDefaultPrefChanged() {
2352    // Clear out the sorted engines settings, so that we re-sort it if necessary.
2353    this.__sortedEngines = null;
2354    // We should notify if the normal default, and the currently saved private
2355    // default are different. Otherwise, save the energy.
2356    if (this.defaultEngine != this._getEngineDefault(true)) {
2357      SearchUtils.notifyAction(
2358        // Always notify with the new private engine, the function checks
2359        // the preference value for us.
2360        this.defaultPrivateEngine,
2361        SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
2362      );
2363    }
2364  },
2365
2366  async _getEngineInfo(engine) {
2367    if (!engine) {
2368      // The defaultEngine getter will throw if there's no engine at all,
2369      // which shouldn't happen unless an add-on or a test deleted all of them.
2370      // Our preferences UI doesn't let users do that.
2371      Cu.reportError("getDefaultEngineInfo: No default engine");
2372      return ["NONE", { name: "NONE" }];
2373    }
2374
2375    const engineData = {
2376      loadPath: engine._loadPath,
2377      name: engine.name ? engine.name : "",
2378    };
2379
2380    if (engine.isAppProvided) {
2381      engineData.origin = "default";
2382    } else {
2383      let currentHash = engine.getAttr("loadPathHash");
2384      if (!currentHash) {
2385        engineData.origin = "unverified";
2386      } else {
2387        let loadPathHash = SearchUtils.getVerificationHash(engine._loadPath);
2388        engineData.origin =
2389          currentHash == loadPathHash ? "verified" : "invalid";
2390      }
2391    }
2392
2393    // For privacy, we only collect the submission URL for default engines...
2394    let sendSubmissionURL = engine.isAppProvided;
2395
2396    if (!sendSubmissionURL) {
2397      // ... or engines that are the same domain as a default engine.
2398      let engineHost = engine._getURLOfType(SearchUtils.URL_TYPE.SEARCH)
2399        .templateHost;
2400      for (let innerEngine of this._engines.values()) {
2401        if (!innerEngine.isAppProvided) {
2402          continue;
2403        }
2404
2405        let innerEngineURL = innerEngine._getURLOfType(
2406          SearchUtils.URL_TYPE.SEARCH
2407        );
2408        if (innerEngineURL.templateHost == engineHost) {
2409          sendSubmissionURL = true;
2410          break;
2411        }
2412      }
2413
2414      if (!sendSubmissionURL) {
2415        // ... or well known search domains.
2416        //
2417        // Starts with: www.google., search.aol., yandex.
2418        // or
2419        // Ends with: search.yahoo.com, .ask.com, .bing.com, .startpage.com, baidu.com, duckduckgo.com
2420        const urlTest = /^(?:www\.google\.|search\.aol\.|yandex\.)|(?:search\.yahoo|\.ask|\.bing|\.startpage|\.baidu|duckduckgo)\.com$/;
2421        sendSubmissionURL = urlTest.test(engineHost);
2422      }
2423    }
2424
2425    if (sendSubmissionURL) {
2426      let uri = engine
2427        ._getURLOfType("text/html")
2428        .getSubmission("", engine, "searchbar").uri;
2429      uri = uri
2430        .mutate()
2431        .setUserPass("") // Avoid reporting a username or password.
2432        .finalize();
2433      engineData.submissionURL = uri.spec;
2434    }
2435
2436    return [engine.telemetryId, engineData];
2437  },
2438
2439  async getDefaultEngineInfo() {
2440    let [telemetryId, defaultSearchEngineData] = await this._getEngineInfo(
2441      this.defaultEngine
2442    );
2443    const result = {
2444      defaultSearchEngine: telemetryId,
2445      defaultSearchEngineData,
2446    };
2447
2448    if (this._separatePrivateDefault) {
2449      let [
2450        privateTelemetryId,
2451        defaultPrivateSearchEngineData,
2452      ] = await this._getEngineInfo(this.defaultPrivateEngine);
2453      result.defaultPrivateSearchEngine = privateTelemetryId;
2454      result.defaultPrivateSearchEngineData = defaultPrivateSearchEngineData;
2455    }
2456
2457    return result;
2458  },
2459
2460  /**
2461   * This map is built lazily after the available search engines change.  It
2462   * allows quick parsing of an URL representing a search submission into the
2463   * search engine name and original terms.
2464   *
2465   * The keys are strings containing the domain name and lowercase path of the
2466   * engine submission, for example "www.google.com/search".
2467   *
2468   * The values are objects with these properties:
2469   * {
2470   *   engine: The associated nsISearchEngine.
2471   *   termsParameterName: Name of the URL parameter containing the search
2472   *                       terms, for example "q".
2473   * }
2474   */
2475  _parseSubmissionMap: null,
2476
2477  _buildParseSubmissionMap() {
2478    this._parseSubmissionMap = new Map();
2479
2480    // Used only while building the map, indicates which entries do not refer to
2481    // the main domain of the engine but to an alternate domain, for example
2482    // "www.google.fr" for the "www.google.com" search engine.
2483    let keysOfAlternates = new Set();
2484
2485    for (let engine of this._sortedEngines) {
2486      if (engine.hidden) {
2487        continue;
2488      }
2489
2490      let urlParsingInfo = engine.getURLParsingInfo();
2491      if (!urlParsingInfo) {
2492        continue;
2493      }
2494
2495      // Store the same object on each matching map key, as an optimization.
2496      let mapValueForEngine = {
2497        engine,
2498        termsParameterName: urlParsingInfo.termsParameterName,
2499      };
2500
2501      let processDomain = (domain, isAlternate) => {
2502        let key = domain + urlParsingInfo.path;
2503
2504        // Apply the logic for which main domains take priority over alternate
2505        // domains, even if they are found later in the ordered engine list.
2506        let existingEntry = this._parseSubmissionMap.get(key);
2507        if (!existingEntry) {
2508          if (isAlternate) {
2509            keysOfAlternates.add(key);
2510          }
2511        } else if (!isAlternate && keysOfAlternates.has(key)) {
2512          keysOfAlternates.delete(key);
2513        } else {
2514          return;
2515        }
2516
2517        this._parseSubmissionMap.set(key, mapValueForEngine);
2518      };
2519
2520      processDomain(urlParsingInfo.mainDomain, false);
2521      SearchStaticData.getAlternateDomains(
2522        urlParsingInfo.mainDomain
2523      ).forEach(d => processDomain(d, true));
2524    }
2525  },
2526
2527  parseSubmissionURL(url) {
2528    if (!this._initialized) {
2529      // If search is not initialized, do nothing.
2530      // This allows us to use this function early in telemetry.
2531      // The only other consumer of this (places) uses it much later.
2532      return gEmptyParseSubmissionResult;
2533    }
2534
2535    if (!this._parseSubmissionMap) {
2536      this._buildParseSubmissionMap();
2537    }
2538
2539    // Extract the elements of the provided URL first.
2540    let soughtKey, soughtQuery;
2541    try {
2542      let soughtUrl = Services.io.newURI(url).QueryInterface(Ci.nsIURL);
2543
2544      // Exclude any URL that is not HTTP or HTTPS from the beginning.
2545      if (soughtUrl.scheme != "http" && soughtUrl.scheme != "https") {
2546        return gEmptyParseSubmissionResult;
2547      }
2548
2549      // Reading these URL properties may fail and raise an exception.
2550      soughtKey = soughtUrl.host + soughtUrl.filePath.toLowerCase();
2551      soughtQuery = soughtUrl.query;
2552    } catch (ex) {
2553      // Errors while parsing the URL or accessing the properties are not fatal.
2554      return gEmptyParseSubmissionResult;
2555    }
2556
2557    // Look up the domain and path in the map to identify the search engine.
2558    let mapEntry = this._parseSubmissionMap.get(soughtKey);
2559    if (!mapEntry) {
2560      return gEmptyParseSubmissionResult;
2561    }
2562
2563    // Extract the search terms from the parameter, for example "caff%C3%A8"
2564    // from the URL "https://www.google.com/search?q=caff%C3%A8&client=firefox".
2565    let encodedTerms = null;
2566    for (let param of soughtQuery.split("&")) {
2567      let equalPos = param.indexOf("=");
2568      if (
2569        equalPos != -1 &&
2570        param.substr(0, equalPos) == mapEntry.termsParameterName
2571      ) {
2572        // This is the parameter we are looking for.
2573        encodedTerms = param.substr(equalPos + 1);
2574        break;
2575      }
2576    }
2577    if (encodedTerms === null) {
2578      return gEmptyParseSubmissionResult;
2579    }
2580
2581    let length = 0;
2582    let offset = url.indexOf("?") + 1;
2583    let query = url.slice(offset);
2584    // Iterate a second time over the original input string to determine the
2585    // correct search term offset and length in the original encoding.
2586    for (let param of query.split("&")) {
2587      let equalPos = param.indexOf("=");
2588      if (
2589        equalPos != -1 &&
2590        param.substr(0, equalPos) == mapEntry.termsParameterName
2591      ) {
2592        // This is the parameter we are looking for.
2593        offset += equalPos + 1;
2594        length = param.length - equalPos - 1;
2595        break;
2596      }
2597      offset += param.length + 1;
2598    }
2599
2600    // Decode the terms using the charset defined in the search engine.
2601    let terms;
2602    try {
2603      terms = Services.textToSubURI.UnEscapeAndConvert(
2604        mapEntry.engine.queryCharset,
2605        encodedTerms.replace(/\+/g, " ")
2606      );
2607    } catch (ex) {
2608      // Decoding errors will cause this match to be ignored.
2609      return gEmptyParseSubmissionResult;
2610    }
2611
2612    let submission = new ParseSubmissionResult(
2613      mapEntry.engine,
2614      terms,
2615      mapEntry.termsParameterName,
2616      offset,
2617      length
2618    );
2619    return submission;
2620  },
2621
2622  /**
2623   * Gets the WebExtensionPolicy for an add-on.
2624   *
2625   * @param {string} id
2626   *   The WebExtension id.
2627   * @returns {WebExtensionPolicy}
2628   */
2629  async _getExtensionPolicy(id) {
2630    let policy = WebExtensionPolicy.getByID(id);
2631    if (!policy) {
2632      let idPrefix = id.split("@")[0];
2633      let path = `resource://search-extensions/${idPrefix}/`;
2634      await AddonManager.installBuiltinAddon(path);
2635      policy = WebExtensionPolicy.getByID(id);
2636    }
2637    // On startup the extension may have not finished parsing the
2638    // manifest, wait for that here.
2639    await policy.readyPromise;
2640    return policy;
2641  },
2642
2643  // nsIObserver
2644  observe(engine, topic, verb) {
2645    switch (topic) {
2646      case SearchUtils.TOPIC_ENGINE_MODIFIED:
2647        switch (verb) {
2648          case SearchUtils.MODIFIED_TYPE.LOADED:
2649            engine = engine.QueryInterface(Ci.nsISearchEngine);
2650            logConsole.debug("observe: Done installation of ", engine.name);
2651            this._addEngineToStore(engine.wrappedJSObject);
2652            // The addition of the engine to the store always triggers an ADDED
2653            // or a CHANGED notification, that will trigger the task below.
2654            break;
2655          case SearchUtils.MODIFIED_TYPE.ADDED:
2656          case SearchUtils.MODIFIED_TYPE.CHANGED:
2657          case SearchUtils.MODIFIED_TYPE.REMOVED:
2658            // Invalidate the map used to parse URLs to search engines.
2659            this._parseSubmissionMap = null;
2660            break;
2661        }
2662        break;
2663
2664      case "idle": {
2665        this.idleService.removeIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
2666        this._queuedIdle = false;
2667        logConsole.debug(
2668          "Reloading engines after idle due to configuration change"
2669        );
2670        this._maybeReloadEngines().catch(Cu.reportError);
2671        break;
2672      }
2673
2674      case QUIT_APPLICATION_TOPIC:
2675        this._removeObservers();
2676        break;
2677
2678      case TOPIC_LOCALES_CHANGE:
2679        // Locale changed. Re-init. We rely on observers, because we can't
2680        // return this promise to anyone.
2681
2682        // At the time of writing, when the user does a "Apply and Restart" for
2683        // a new language the preferences code triggers the locales change and
2684        // restart straight after, so we delay the check, which means we should
2685        // be able to avoid the reload on shutdown, and we'll sort it out
2686        // on next startup.
2687        // This also helps to avoid issues with the add-on manager shutting
2688        // down at the same time (see _reInit for more info).
2689        Services.tm.dispatchToMainThread(() => {
2690          if (!Services.startup.shuttingDown) {
2691            this._maybeReloadEngines().catch(Cu.reportError);
2692          }
2693        });
2694        break;
2695      case Region.REGION_TOPIC:
2696        logConsole.debug("Region updated:", Region.home);
2697        this._maybeReloadEngines().catch(Cu.reportError);
2698        break;
2699    }
2700  },
2701
2702  // nsITimerCallback
2703  notify(timer) {
2704    logConsole.debug("_notify: checking for updates");
2705
2706    if (
2707      !Services.prefs.getBoolPref(
2708        SearchUtils.BROWSER_SEARCH_PREF + "update",
2709        true
2710      )
2711    ) {
2712      return;
2713    }
2714
2715    // Our timer has expired, but unfortunately, we can't get any data from it.
2716    // Therefore, we need to walk our engine-list, looking for expired engines
2717    var currentTime = Date.now();
2718    logConsole.debug("currentTime:" + currentTime);
2719    for (let e of this._engines.values()) {
2720      let engine = e.wrappedJSObject;
2721      if (!engine._hasUpdates) {
2722        continue;
2723      }
2724
2725      var expirTime = engine.getAttr("updateexpir");
2726      logConsole.debug(
2727        engine.name,
2728        "expirTime:",
2729        expirTime,
2730        "updateURL:",
2731        engine._updateURL,
2732        "iconUpdateURL:",
2733        engine._iconUpdateURL
2734      );
2735
2736      var engineExpired = expirTime <= currentTime;
2737
2738      if (!expirTime || !engineExpired) {
2739        logConsole.debug("skipping engine");
2740        continue;
2741      }
2742
2743      logConsole.debug(engine.name, "has expired");
2744
2745      engineUpdateService.update(engine);
2746
2747      // Schedule the next update
2748      engineUpdateService.scheduleNextUpdate(engine);
2749    } // end engine iteration
2750  },
2751
2752  _addObservers() {
2753    if (this._observersAdded) {
2754      // There might be a race between synchronous and asynchronous
2755      // initialization for which we try to register the observers twice.
2756      return;
2757    }
2758    this._observersAdded = true;
2759
2760    Services.obs.addObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED);
2761    Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC);
2762    Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE);
2763
2764    this._settings.addObservers();
2765
2766    // The current stage of shutdown. Used to help analyze crash
2767    // signatures in case of shutdown timeout.
2768    let shutdownState = {
2769      step: "Not started",
2770      latestError: {
2771        message: undefined,
2772        stack: undefined,
2773      },
2774    };
2775    IOUtils.profileBeforeChange.addBlocker(
2776      "Search service: shutting down",
2777      () =>
2778        (async () => {
2779          // If we are in initialization, then don't attempt to save the settings.
2780          // It is likely that shutdown will have caused the add-on manager to
2781          // stop, which can cause initialization to fail.
2782          // Hence at that stage, we could have broken settings which we don't
2783          // want to write.
2784          // The good news is, that if we don't write the settings here, we'll
2785          // detect the out-of-date settings on next state, and automatically
2786          // rebuild it.
2787          if (!this._initialized) {
2788            logConsole.warn(
2789              "not saving settings on shutdown due to initializing."
2790            );
2791            return;
2792          }
2793
2794          try {
2795            await this._settings.shutdown(shutdownState);
2796          } catch (ex) {
2797            // Ensure that error is reported and that it causes tests
2798            // to fail, otherwise ignore it.
2799            Promise.reject(ex);
2800          }
2801        })(),
2802
2803      () => shutdownState
2804    );
2805  },
2806  _observersAdded: false,
2807
2808  _removeObservers() {
2809    if (this._ignoreListListener) {
2810      IgnoreLists.unsubscribe(this._ignoreListListener);
2811      delete this._ignoreListListener;
2812    }
2813    if (this._queuedIdle) {
2814      this.idleService.removeIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
2815      this._queuedIdle = false;
2816    }
2817
2818    this._settings.removeObservers();
2819
2820    Services.obs.removeObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED);
2821    Services.obs.removeObserver(this, QUIT_APPLICATION_TOPIC);
2822    Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGE);
2823    Services.obs.removeObserver(this, Region.REGION_TOPIC);
2824  },
2825
2826  QueryInterface: ChromeUtils.generateQI([
2827    "nsISearchService",
2828    "nsIObserver",
2829    "nsITimerCallback",
2830  ]),
2831};
2832
2833var engineUpdateService = {
2834  scheduleNextUpdate(engine) {
2835    var interval = engine._updateInterval || SEARCH_DEFAULT_UPDATE_INTERVAL;
2836    var milliseconds = interval * 86400000; // |interval| is in days
2837    engine.setAttr("updateexpir", Date.now() + milliseconds);
2838  },
2839
2840  update(engine) {
2841    engine = engine.wrappedJSObject;
2842    logConsole.debug("update called for", engine._name);
2843    if (
2844      !Services.prefs.getBoolPref(
2845        SearchUtils.BROWSER_SEARCH_PREF + "update",
2846        true
2847      ) ||
2848      !engine._hasUpdates
2849    ) {
2850      return;
2851    }
2852
2853    let testEngine = null;
2854    let updateURL = engine._getURLOfType(SearchUtils.URL_TYPE.OPENSEARCH);
2855    let updateURI =
2856      updateURL && updateURL._hasRelation("self")
2857        ? updateURL.getSubmission("", engine).uri
2858        : SearchUtils.makeURI(engine._updateURL);
2859    if (updateURI) {
2860      if (engine.isAppProvided && !updateURI.schemeIs("https")) {
2861        logConsole.debug("Invalid scheme for default engine update");
2862        return;
2863      }
2864
2865      logConsole.debug("updating", engine.name, updateURI.spec);
2866      testEngine = new OpenSearchEngine();
2867      testEngine._engineToUpdate = engine;
2868      try {
2869        testEngine._install(updateURI);
2870      } catch (ex) {
2871        logConsole.error("Failed to update", engine.name, ex);
2872      }
2873    } else {
2874      logConsole.debug("invalid updateURI");
2875    }
2876
2877    if (engine._iconUpdateURL) {
2878      // If we're updating the engine too, use the new engine object,
2879      // otherwise use the existing engine object.
2880      (testEngine || engine)._setIcon(engine._iconUpdateURL, true);
2881    }
2882  },
2883};
2884
2885XPCOMUtils.defineLazyServiceGetter(
2886  SearchService.prototype,
2887  "idleService",
2888  "@mozilla.org/widget/useridleservice;1",
2889  "nsIUserIdleService"
2890);
2891
2892/**
2893 * Handles getting and checking extensions against the allow list.
2894 */
2895class SearchDefaultOverrideAllowlistHandler {
2896  /**
2897   * @param {function} listener
2898   *   A listener for configuration update changes.
2899   */
2900  constructor(listener) {
2901    this._remoteConfig = RemoteSettings(SearchUtils.SETTINGS_ALLOWLIST_KEY);
2902  }
2903
2904  /**
2905   * Determines if a search engine extension can override a default one
2906   * according to the allow list.
2907   *
2908   * @param {object} extension
2909   *   The extension object (from add-on manager) that will override the
2910   *   app provided search engine.
2911   * @param {string} appProvidedExtensionId
2912   *   The id of the search engine that will be overriden.
2913   * @returns {boolean}
2914   *   Returns true if the search engine extension may override the app provided
2915   *   instance.
2916   */
2917  async canOverride(extension, appProvidedExtensionId) {
2918    const overrideTable = await this._getAllowlist();
2919
2920    let entry = overrideTable.find(e => e.thirdPartyId == extension.id);
2921    if (!entry) {
2922      return false;
2923    }
2924
2925    if (appProvidedExtensionId != entry.overridesId) {
2926      return false;
2927    }
2928
2929    let searchProvider =
2930      extension.manifest.chrome_settings_overrides.search_provider;
2931
2932    return entry.urls.some(
2933      e =>
2934        searchProvider.search_url == e.search_url &&
2935        searchProvider.search_form == e.search_form &&
2936        searchProvider.search_url_get_params == e.search_url_get_params &&
2937        searchProvider.search_url_post_params == e.search_url_post_params
2938    );
2939  }
2940
2941  /**
2942   * Obtains the configuration from remote settings. This includes
2943   * verifying the signature of the record within the database.
2944   *
2945   * If the signature in the database is invalid, the database will be wiped
2946   * and the stored dump will be used, until the settings next update.
2947   *
2948   * Note that this may cause a network check of the certificate, but that
2949   * should generally be quick.
2950   *
2951   * @returns {array}
2952   *   An array of objects in the database, or an empty array if none
2953   *   could be obtained.
2954   */
2955  async _getAllowlist() {
2956    let result = [];
2957    try {
2958      result = await this._remoteConfig.get();
2959    } catch (ex) {
2960      // Don't throw an error just log it, just continue with no data, and hopefully
2961      // a sync will fix things later on.
2962      Cu.reportError(ex);
2963    }
2964    logConsole.debug("Allow list is:", result);
2965    return result;
2966  }
2967}
2968
2969var EXPORTED_SYMBOLS = ["SearchService"];
2970