Backed out changeset f0a2aa2ffe6d (bug 1832704) for causing xpc failures in toolkit...
[gecko.git] / toolkit / components / search / SearchService.sys.mjs
blob90dec7518dac0101048a886c45efe9e12a0d0b1c
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/. */
5 /* eslint no-shadow: error, mozilla/no-aArgs: error */
7 import { PromiseUtils } from "resource://gre/modules/PromiseUtils.sys.mjs";
8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
10 const lazy = {};
12 ChromeUtils.defineESModuleGetters(lazy, {
13   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
14   AddonSearchEngine: "resource://gre/modules/AddonSearchEngine.sys.mjs",
15   IgnoreLists: "resource://gre/modules/IgnoreLists.sys.mjs",
16   NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
17   OpenSearchEngine: "resource://gre/modules/OpenSearchEngine.sys.mjs",
18   PolicySearchEngine: "resource://gre/modules/PolicySearchEngine.sys.mjs",
19   Region: "resource://gre/modules/Region.sys.mjs",
20   RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
21   SearchEngine: "resource://gre/modules/SearchEngine.sys.mjs",
22   SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
23   SearchSettings: "resource://gre/modules/SearchSettings.sys.mjs",
24   SearchStaticData: "resource://gre/modules/SearchStaticData.sys.mjs",
25   SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
26   UserSearchEngine: "resource://gre/modules/UserSearchEngine.sys.mjs",
27 });
29 ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
30   return console.createInstance({
31     prefix: "SearchService",
32     maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
33   });
34 });
36 XPCOMUtils.defineLazyServiceGetter(
37   lazy,
38   "timerManager",
39   "@mozilla.org/updates/timer-manager;1",
40   "nsIUpdateTimerManager"
43 const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
44 const QUIT_APPLICATION_TOPIC = "quit-application";
46 // The update timer for OpenSearch engines checks in once a day.
47 const OPENSEARCH_UPDATE_TIMER_TOPIC = "search-engine-update-timer";
48 const OPENSEARCH_UPDATE_TIMER_INTERVAL = 60 * 60 * 24;
50 // The default engine update interval, in days. This is only used if an engine
51 // specifies an updateURL, but not an updateInterval.
52 const OPENSEARCH_DEFAULT_UPDATE_INTERVAL = 7;
54 // This is the amount of time we'll be idle for before applying any configuration
55 // changes.
56 const RECONFIG_IDLE_TIME_SEC = 5 * 60;
58 /**
59  * A reason that is used in the change of default search engine event telemetry.
60  * These are mutally exclusive.
61  */
62 const REASON_CHANGE_MAP = new Map([
63   // The cause of the change is unknown.
64   [Ci.nsISearchService.CHANGE_REASON_UNKNOWN, "unknown"],
65   // The user changed the default search engine via the options in the
66   // preferences UI.
67   [Ci.nsISearchService.CHANGE_REASON_USER, "user"],
68   // The change resulted from the user toggling the "Use this search engine in
69   // Private Windows" option in the preferences UI.
70   [Ci.nsISearchService.CHANGE_REASON_USER_PRIVATE_SPLIT, "user_private_split"],
71   // The user changed the default via keys (cmd/ctrl-up/down) in the separate
72   // search bar.
73   [Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR, "user_searchbar"],
74   // The user changed the default via context menu on the one-off buttons in the
75   // separate search bar.
76   [
77     Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR_CONTEXT,
78     "user_searchbar_context",
79   ],
80   // An add-on requested the change of default on install, which was either
81   // accepted automatically or by the user.
82   [Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL, "addon-install"],
83   // An add-on was uninstalled, which caused the engine to be uninstalled.
84   [Ci.nsISearchService.CHANGE_REASON_ADDON_UNINSTALL, "addon-uninstall"],
85   // A configuration update caused a change of default.
86   [Ci.nsISearchService.CHANGE_REASON_CONFIG, "config"],
87   // A locale update caused a change of default.
88   [Ci.nsISearchService.CHANGE_REASON_LOCALE, "locale"],
89   // A region update caused a change of default.
90   [Ci.nsISearchService.CHANGE_REASON_REGION, "region"],
91   // Turning on/off an experiment caused a change of default.
92   [Ci.nsISearchService.CHANGE_REASON_EXPERIMENT, "experiment"],
93   // An enterprise policy caused a change of default.
94   [Ci.nsISearchService.CHANGE_REASON_ENTERPRISE, "enterprise"],
95   // The UI Tour caused a change of default.
96   [Ci.nsISearchService.CHANGE_REASON_UITOUR, "uitour"],
97 ]);
99 /**
100  * The ParseSubmissionResult contains getter methods that return attributes
101  * about the parsed submission url.
103  * @implements {nsIParseSubmissionResult}
104  */
105 class ParseSubmissionResult {
106   constructor(engine, terms, termsParameterName) {
107     this.#engine = engine;
108     this.#terms = terms;
109     this.#termsParameterName = termsParameterName;
110   }
112   get engine() {
113     return this.#engine;
114   }
116   get terms() {
117     return this.#terms;
118   }
120   get termsParameterName() {
121     return this.#termsParameterName;
122   }
124   /**
125    * The search engine associated with the URL passed in to
126    * nsISearchEngine::parseSubmissionURL, or null if the URL does not represent
127    * a search submission.
128    *
129    * @type {nsISearchEngine|null}
130    */
131   #engine;
133   /**
134    * String containing the sought terms. This can be an empty string in case no
135    * terms were specified or the URL does not represent a search submission.*
136    *
137    * @type {string}
138    */
139   #terms;
141   /**
142    * The name of the query parameter used by `engine` for queries. E.g. "q".
143    *
144    * @type {string}
145    */
146   #termsParameterName;
148   QueryInterface = ChromeUtils.generateQI(["nsISearchParseSubmissionResult"]);
151 const gEmptyParseSubmissionResult = Object.freeze(
152   new ParseSubmissionResult(null, "", "")
156  * The search service handles loading and maintaining of search engines. It will
157  * also work out the default lists for each locale/region.
159  * @implements {nsISearchService}
160  */
161 export class SearchService {
162   constructor() {
163     this.#initObservers = PromiseUtils.defer();
164     // this._engines is prefixed with _ rather than # because it is called from
165     // a test.
166     this._engines = new Map();
167     this._settings = new lazy.SearchSettings(this);
168   }
170   classID = Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}");
172   get defaultEngine() {
173     this.#ensureInitialized();
174     return this._getEngineDefault(false);
175   }
177   set defaultEngine(newEngine) {
178     this.#ensureInitialized();
179     this.#setEngineDefault(false, newEngine);
180   }
182   get defaultPrivateEngine() {
183     this.#ensureInitialized();
184     return this._getEngineDefault(this.#separatePrivateDefault);
185   }
187   set defaultPrivateEngine(newEngine) {
188     this.#ensureInitialized();
189     if (!this._separatePrivateDefaultPrefValue) {
190       Services.prefs.setBoolPref(
191         lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
192         true
193       );
194     }
195     this.#setEngineDefault(this.#separatePrivateDefault, newEngine);
196   }
198   async getDefault() {
199     await this.init();
200     return this.defaultEngine;
201   }
203   async setDefault(engine, changeSource) {
204     await this.init();
205     this.#setEngineDefault(false, engine, changeSource);
206   }
208   async getDefaultPrivate() {
209     await this.init();
210     return this.defaultPrivateEngine;
211   }
213   async setDefaultPrivate(engine, changeSource) {
214     await this.init();
215     if (!this._separatePrivateDefaultPrefValue) {
216       Services.prefs.setBoolPref(
217         lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
218         true
219       );
220     }
221     this.#setEngineDefault(this.#separatePrivateDefault, engine, changeSource);
222   }
224   /**
225    * @returns {SearchEngine}
226    *   The engine that is the default for this locale/region, ignoring any
227    *   user changes to the default engine.
228    */
229   get appDefaultEngine() {
230     return this.#appDefaultEngine();
231   }
233   /**
234    * @returns {SearchEngine}
235    *   The engine that is the default for this locale/region in private browsing
236    *   mode, ignoring any user changes to the default engine.
237    *   Note: if there is no default for this locale/region, then the non-private
238    *   browsing engine will be returned.
239    */
240   get appPrivateDefaultEngine() {
241     return this.#appDefaultEngine(this.#separatePrivateDefault);
242   }
244   /**
245    * Determine whether initialization has been completed.
246    *
247    * Clients of the service can use this attribute to quickly determine whether
248    * initialization is complete, and decide to trigger some immediate treatment,
249    * to launch asynchronous initialization or to bailout.
250    *
251    * Note that this attribute does not indicate that initialization has
252    * succeeded, use hasSuccessfullyInitialized() for that.
253    *
254    * @returns {boolean}
255    *  |true | if the search service has finished its attempt to initialize and
256    *          we have an outcome. It could have failed or succeeded during this
257    *          process.
258    *  |false| if initialization has not been triggered yet or initialization is
259    *          still ongoing.
260    */
261   get isInitialized() {
262     return (
263       this.#initializationStatus == "success" ||
264       this.#initializationStatus == "failed"
265     );
266   }
268   /**
269    * Determine whether initialization has been successfully completed.
270    *
271    * @returns {boolean}
272    *  |true | if the search service has succesfully initialized.
273    *  |false| if initialization has not been started yet, initialization is
274    *          still ongoing or initializaiton has failed.
275    */
276   get hasSuccessfullyInitialized() {
277     return this.#initializationStatus == "success";
278   }
280   getDefaultEngineInfo() {
281     let [telemetryId, defaultSearchEngineData] = this.#getEngineInfo(
282       this.defaultEngine
283     );
284     const result = {
285       defaultSearchEngine: telemetryId,
286       defaultSearchEngineData,
287     };
289     if (this.#separatePrivateDefault) {
290       let [privateTelemetryId, defaultPrivateSearchEngineData] =
291         this.#getEngineInfo(this.defaultPrivateEngine);
292       result.defaultPrivateSearchEngine = privateTelemetryId;
293       result.defaultPrivateSearchEngineData = defaultPrivateSearchEngineData;
294     }
296     return result;
297   }
299   /**
300    * If possible, please call getEngineById() rather than getEngineByName()
301    * because engines are stored as { id: object } in this._engine Map.
302    *
303    * Returns the engine associated with the name.
304    *
305    * @param {string} engineName
306    *   The name of the engine.
307    * @returns {SearchEngine}
308    *   The associated engine if found, null otherwise.
309    */
310   getEngineByName(engineName) {
311     this.#ensureInitialized();
312     return this.#getEngineByName(engineName);
313   }
315   /**
316    * Returns the engine associated with the name without initialization checks.
317    *
318    * @param {string} engineName
319    *   The name of the engine.
320    * @returns {SearchEngine}
321    *   The associated engine if found, null otherwise.
322    */
323   #getEngineByName(engineName) {
324     for (let engine of this._engines.values()) {
325       if (engine.name == engineName) {
326         return engine;
327       }
328     }
330     return null;
331   }
333   /**
334    * Returns the engine associated with the id.
335    *
336    * @param {string} engineId
337    *   The id of the engine.
338    * @returns {SearchEngine}
339    *   The associated engine if found, null otherwise.
340    */
341   getEngineById(engineId) {
342     this.#ensureInitialized();
343     return this._engines.get(engineId) || null;
344   }
346   async getEngineByAlias(alias) {
347     await this.init();
348     for (var engine of this._engines.values()) {
349       if (engine && engine.aliases.includes(alias)) {
350         return engine;
351       }
352     }
353     return null;
354   }
356   async getEngines() {
357     await this.init();
358     lazy.logConsole.debug("getEngines: getting all engines");
359     return this.#sortedEngines;
360   }
362   async getVisibleEngines() {
363     await this.init(true);
364     lazy.logConsole.debug("getVisibleEngines: getting all visible engines");
365     return this.#sortedVisibleEngines;
366   }
368   async getAppProvidedEngines() {
369     await this.init();
371     return this._sortEnginesByDefaults(
372       this.#sortedEngines.filter(e => e.isAppProvided)
373     );
374   }
376   async getEnginesByExtensionID(extensionID) {
377     await this.init();
378     return this.#getEnginesByExtensionID(extensionID);
379   }
381   // nsISearchService
382   async init() {
383     if (this.#initStarted) {
384       return this.#initObservers.promise;
385     }
386     lazy.logConsole.debug("init");
388     TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
389     const timerId = Glean.searchService.startupTime.start();
390     this.#initStarted = true;
391     let result;
392     try {
393       // Complete initialization by calling asynchronous initializer.
394       result = await this.#init();
395       TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
396       Glean.searchService.startupTime.stopAndAccumulate(timerId);
397     } catch (ex) {
398       this.#initializationStatus = "failed";
399       TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
400       Glean.searchService.startupTime.cancel(timerId);
401       this.#initObservers.reject(ex.result);
402       throw ex;
403     }
405     if (!Components.isSuccessCode(result)) {
406       throw new Error("SearchService failed while it was initializing.");
407     } else if (this.#startupRemovedExtensions.size) {
408       Services.tm.dispatchToMainThread(async () => {
409         // Now that init() has successfully finished, we remove any engines
410         // that have had their add-ons removed by the add-on manager.
411         // We do this after init() has complete, as that allows us to use
412         // removeEngine to look after any default engine changes as well.
413         // This could cause a slight flicker on startup, but it should be
414         // a rare action.
415         lazy.logConsole.debug("Removing delayed extension engines");
416         for (let id of this.#startupRemovedExtensions) {
417           for (let engine of this.#getEnginesByExtensionID(id)) {
418             // Only do this for non-application provided engines. We shouldn't
419             // ever get application provided engines removed here, but just in case.
420             if (!engine.isAppProvided) {
421               await this.removeEngine(engine);
422             }
423           }
424         }
425         this.#startupRemovedExtensions.clear();
426       });
427     }
428     return Cr.NS_OK;
429   }
431   /**
432    * Runs background checks for the search service. This is called from
433    * BrowserGlue and may be run once per session if the user is idle for
434    * long enough.
435    */
436   async runBackgroundChecks() {
437     await this.init();
438     await this.#migrateLegacyEngines();
439     await this.#checkWebExtensionEngines();
440     await this.#addOpenSearchTelemetry();
441   }
443   /**
444    * Test only - reset SearchService data. Ideally this should be replaced
445    */
446   reset() {
447     this.#initializationStatus = "not initialized";
448     this.#initObservers = PromiseUtils.defer();
449     this.#initStarted = false;
450     this.#startupExtensions = new Set();
451     this._engines.clear();
452     this._cachedSortedEngines = null;
453     this.#currentEngine = null;
454     this.#currentPrivateEngine = null;
455     this._searchDefault = null;
456     this.#searchPrivateDefault = null;
457     this.#maybeReloadDebounce = false;
458     this._settings._batchTask?.disarm();
459   }
461   // Test-only function to set SearchService initialization status
462   forceInitializationStatusForTests(status) {
463     this.#initializationStatus = status;
464   }
466   /**
467    * Test only variable to indicate an error should occur during
468    * search service initialization.
469    *
470    * @type {boolean}
471    */
472   willThrowErrorDuringInitInTest = false;
474   // Test-only function to reset just the engine selector so that it can
475   // load a different configuration.
476   resetEngineSelector() {
477     this.#engineSelector = new lazy.SearchEngineSelector(
478       this.#handleConfigurationUpdated.bind(this)
479     );
480   }
482   resetToAppDefaultEngine() {
483     let appDefaultEngine = this.appDefaultEngine;
484     appDefaultEngine.hidden = false;
485     this.defaultEngine = appDefaultEngine;
486   }
488   async maybeSetAndOverrideDefault(extension) {
489     let searchProvider =
490       extension.manifest.chrome_settings_overrides.search_provider;
491     let engine = this.getEngineByName(searchProvider.name);
492     if (!engine || !engine.isAppProvided || engine.hidden) {
493       // If the engine is not application provided, then we shouldn't simply
494       // set default to it.
495       // If the engine is application provided, but hidden, then we don't
496       // switch to it, nor do we try to install it.
497       return {
498         canChangeToAppProvided: false,
499         canInstallEngine: !engine?.hidden,
500       };
501     }
503     if (!this.#defaultOverrideAllowlist) {
504       this.#defaultOverrideAllowlist =
505         new SearchDefaultOverrideAllowlistHandler();
506     }
508     if (
509       extension.startupReason === "ADDON_INSTALL" ||
510       extension.startupReason === "ADDON_ENABLE"
511     ) {
512       // Don't allow an extension to set the default if it is already the default.
513       if (this.defaultEngine.name == searchProvider.name) {
514         return {
515           canChangeToAppProvided: false,
516           canInstallEngine: false,
517         };
518       }
519       if (
520         !(await this.#defaultOverrideAllowlist.canOverride(
521           extension,
522           engine._extensionID
523         ))
524       ) {
525         lazy.logConsole.debug(
526           "Allowing default engine to be set to app-provided.",
527           extension.id
528         );
529         // We don't allow overriding the engine in this case, but we can allow
530         // the extension to change the default engine.
531         return {
532           canChangeToAppProvided: true,
533           canInstallEngine: false,
534         };
535       }
536       // We're ok to override.
537       engine.overrideWithExtension(extension.id, extension.manifest);
538       lazy.logConsole.debug(
539         "Allowing default engine to be set to app-provided and overridden.",
540         extension.id
541       );
542       return {
543         canChangeToAppProvided: true,
544         canInstallEngine: false,
545       };
546     }
548     if (
549       engine.getAttr("overriddenBy") == extension.id &&
550       (await this.#defaultOverrideAllowlist.canOverride(
551         extension,
552         engine._extensionID
553       ))
554     ) {
555       engine.overrideWithExtension(extension.id, extension.manifest);
556       lazy.logConsole.debug(
557         "Re-enabling overriding of core extension by",
558         extension.id
559       );
560       return {
561         canChangeToAppProvided: true,
562         canInstallEngine: false,
563       };
564     }
566     return {
567       canChangeToAppProvided: false,
568       canInstallEngine: false,
569     };
570   }
572   /**
573    * Adds a search engine that is specified from enterprise policies.
574    *
575    * @param {object} details
576    *   An object that simulates the manifest object from a WebExtension. See
577    *   the idl for more details.
578    */
579   async #addPolicyEngine(details) {
580     let newEngine = new lazy.PolicySearchEngine({ details });
581     let existingEngine = this.#getEngineByName(newEngine.name);
582     if (existingEngine) {
583       throw Components.Exception(
584         "An engine with that name already exists!",
585         Cr.NS_ERROR_FILE_ALREADY_EXISTS
586       );
587     }
588     lazy.logConsole.debug("Adding Policy Engine:", newEngine.name);
589     this.#addEngineToStore(newEngine);
590   }
592   /**
593    * Adds a search engine that is specified by the user.
594    *
595    * @param {string} name
596    *   The name of the search engine
597    * @param {string} url
598    *   The url that the search engine uses for searches
599    * @param {string} alias
600    *   An alias for the search engine
601    */
602   async addUserEngine(name, url, alias) {
603     await this.init();
605     let newEngine = new lazy.UserSearchEngine({
606       details: { name, url, alias },
607     });
608     let existingEngine = this.#getEngineByName(newEngine.name);
609     if (existingEngine) {
610       throw Components.Exception(
611         "An engine with that name already exists!",
612         Cr.NS_ERROR_FILE_ALREADY_EXISTS
613       );
614     }
615     lazy.logConsole.debug(`Adding ${newEngine.name}`);
616     this.#addEngineToStore(newEngine);
617   }
619   /**
620    * Called from the AddonManager when it either installs a new
621    * extension containing a search engine definition or an upgrade
622    * to an existing one.
623    *
624    * @param {object} extension
625    *   An Extension object containing data about the extension.
626    */
627   async addEnginesFromExtension(extension) {
628     lazy.logConsole.debug("addEnginesFromExtension: " + extension.id);
629     // Treat add-on upgrade and downgrades the same - either way, the search
630     // engine gets updated, not added. Generally, we don't expect a downgrade,
631     // but just in case...
632     if (
633       extension.startupReason == "ADDON_UPGRADE" ||
634       extension.startupReason == "ADDON_DOWNGRADE"
635     ) {
636       // Bug 1679861 An a upgrade or downgrade could be adding a search engine
637       // that was not in a prior version, or the addon may have been blocklisted.
638       // In either case, there will not be an existing engine.
639       let existing = await this.#upgradeExtensionEngine(extension);
640       if (existing?.length) {
641         return existing;
642       }
643     }
645     if (extension.isAppProvided) {
646       // If we are in the middle of initialization or reloading engines,
647       // don't add the engine here. This has been called as the result
648       // of _makeEngineFromConfig installing the extension, and that is already
649       // handling the addition of the engine.
650       if (this.isInitialized && !this._reloadingEngines) {
651         let { engines } = await this._fetchEngineSelectorEngines();
652         let inConfig = engines.filter(el => el.webExtension.id == extension.id);
653         if (inConfig.length) {
654           return this.#installExtensionEngine(
655             extension,
656             inConfig.map(el => el.webExtension.locale)
657           );
658         }
659       }
660       lazy.logConsole.debug(
661         "addEnginesFromExtension: Ignoring builtIn engine."
662       );
663       return [];
664     }
666     // If we havent started SearchService yet, store this extension
667     // to install in SearchService.init().
668     if (!this.isInitialized) {
669       this.#startupExtensions.add(extension);
670       return [];
671     }
673     return this.#installExtensionEngine(extension, [
674       lazy.SearchUtils.DEFAULT_TAG,
675     ]);
676   }
678   async addOpenSearchEngine(engineURL, iconURL) {
679     lazy.logConsole.debug("addEngine: Adding", engineURL);
680     await this.init();
681     let errCode;
682     try {
683       var engine = new lazy.OpenSearchEngine();
684       engine._setIcon(iconURL, false);
685       errCode = await new Promise(resolve => {
686         engine.install(engineURL, errorCode => {
687           resolve(errorCode);
688         });
689       });
690       if (errCode) {
691         throw errCode;
692       }
693     } catch (ex) {
694       throw Components.Exception(
695         "addEngine: Error adding engine:\n" + ex,
696         errCode || Cr.NS_ERROR_FAILURE
697       );
698     }
699     this.#maybeStartOpenSearchUpdateTimer();
700     return engine;
701   }
703   async removeWebExtensionEngine(id) {
704     if (!this.isInitialized) {
705       lazy.logConsole.debug(
706         "Delaying removing extension engine on startup:",
707         id
708       );
709       this.#startupRemovedExtensions.add(id);
710       return;
711     }
713     lazy.logConsole.debug("removeWebExtensionEngine:", id);
714     for (let engine of this.#getEnginesByExtensionID(id)) {
715       await this.removeEngine(engine);
716     }
717   }
719   async removeEngine(engine) {
720     await this.init();
721     if (!engine) {
722       throw Components.Exception(
723         "no engine passed to removeEngine!",
724         Cr.NS_ERROR_INVALID_ARG
725       );
726     }
728     var engineToRemove = null;
729     for (var e of this._engines.values()) {
730       if (engine.wrappedJSObject == e) {
731         engineToRemove = e;
732       }
733     }
735     if (!engineToRemove) {
736       throw Components.Exception(
737         "removeEngine: Can't find engine to remove!",
738         Cr.NS_ERROR_FILE_NOT_FOUND
739       );
740     }
742     engineToRemove.pendingRemoval = true;
744     if (engineToRemove == this.defaultEngine) {
745       this.#findAndSetNewDefaultEngine({
746         privateMode: false,
747       });
748     }
750     // Bug 1575649 - We can't just check the default private engine here when
751     // we're not using separate, as that re-checks the normal default, and
752     // triggers update of the default search engine, which messes up various
753     // tests. Really, removeEngine should always commit to updating any
754     // changed defaults.
755     if (
756       this.#separatePrivateDefault &&
757       engineToRemove == this.defaultPrivateEngine
758     ) {
759       this.#findAndSetNewDefaultEngine({
760         privateMode: true,
761       });
762     }
764     if (engineToRemove.inMemory) {
765       // Just hide it (the "hidden" setter will notify) and remove its alias to
766       // avoid future conflicts with other engines.
767       engineToRemove.hidden = true;
768       engineToRemove.alias = null;
769       engineToRemove.pendingRemoval = false;
770     } else {
771       // Remove the engine file from disk if we had a legacy file in the profile.
772       if (engineToRemove._filePath) {
773         let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
774         file.persistentDescriptor = engineToRemove._filePath;
775         if (file.exists()) {
776           file.remove(false);
777         }
778         engineToRemove._filePath = null;
779       }
780       this.#internalRemoveEngine(engineToRemove);
782       // Since we removed an engine, we may need to update the preferences.
783       if (!this.#dontSetUseSavedOrder) {
784         this.#saveSortedEngineList();
785       }
786     }
787     lazy.SearchUtils.notifyAction(
788       engineToRemove,
789       lazy.SearchUtils.MODIFIED_TYPE.REMOVED
790     );
791   }
793   async moveEngine(engine, newIndex) {
794     await this.init();
795     if (newIndex > this.#sortedEngines.length || newIndex < 0) {
796       throw Components.Exception("moveEngine: Index out of bounds!");
797     }
798     if (
799       !(engine instanceof Ci.nsISearchEngine) &&
800       !(engine instanceof lazy.SearchEngine)
801     ) {
802       throw Components.Exception(
803         "moveEngine: Invalid engine passed to moveEngine!",
804         Cr.NS_ERROR_INVALID_ARG
805       );
806     }
807     if (engine.hidden) {
808       throw Components.Exception(
809         "moveEngine: Can't move a hidden engine!",
810         Cr.NS_ERROR_FAILURE
811       );
812     }
814     engine = engine.wrappedJSObject;
816     var currentIndex = this.#sortedEngines.indexOf(engine);
817     if (currentIndex == -1) {
818       throw Components.Exception(
819         "moveEngine: Can't find engine to move!",
820         Cr.NS_ERROR_UNEXPECTED
821       );
822     }
824     // Our callers only take into account non-hidden engines when calculating
825     // newIndex, but we need to move it in the array of all engines, so we
826     // need to adjust newIndex accordingly. To do this, we count the number
827     // of hidden engines in the list before the engine that we're taking the
828     // place of. We do this by first finding newIndexEngine (the engine that
829     // we were supposed to replace) and then iterating through the complete
830     // engine list until we reach it, increasing newIndex for each hidden
831     // engine we find on our way there.
832     //
833     // This could be further simplified by having our caller pass in
834     // newIndexEngine directly instead of newIndex.
835     var newIndexEngine = this.#sortedVisibleEngines[newIndex];
836     if (!newIndexEngine) {
837       throw Components.Exception(
838         "moveEngine: Can't find engine to replace!",
839         Cr.NS_ERROR_UNEXPECTED
840       );
841     }
843     for (var i = 0; i < this.#sortedEngines.length; ++i) {
844       if (newIndexEngine == this.#sortedEngines[i]) {
845         break;
846       }
847       if (this.#sortedEngines[i].hidden) {
848         newIndex++;
849       }
850     }
852     if (currentIndex == newIndex) {
853       return;
854     } // nothing to do!
856     // Move the engine
857     var movedEngine = this._cachedSortedEngines.splice(currentIndex, 1)[0];
858     this._cachedSortedEngines.splice(newIndex, 0, movedEngine);
860     lazy.SearchUtils.notifyAction(
861       engine,
862       lazy.SearchUtils.MODIFIED_TYPE.CHANGED
863     );
865     // Since we moved an engine, we need to update the preferences.
866     this.#saveSortedEngineList();
867   }
869   restoreDefaultEngines() {
870     this.#ensureInitialized();
871     for (let e of this._engines.values()) {
872       // Unhide all default engines
873       if (e.hidden && e.isAppProvided) {
874         e.hidden = false;
875       }
876     }
877   }
879   parseSubmissionURL(url) {
880     if (!this.hasSuccessfullyInitialized) {
881       // If search is not initialized or failed initializing, do nothing.
882       // This allows us to use this function early in telemetry.
883       // The only other consumer of this (places) uses it much later.
884       return gEmptyParseSubmissionResult;
885     }
887     if (!this.#parseSubmissionMap) {
888       this.#buildParseSubmissionMap();
889     }
891     // Extract the elements of the provided URL first.
892     let soughtKey, soughtQuery;
893     try {
894       let soughtUrl = Services.io.newURI(url);
896       // Exclude any URL that is not HTTP or HTTPS from the beginning.
897       if (soughtUrl.schemeIs("http") && soughtUrl.schemeIs("https")) {
898         return gEmptyParseSubmissionResult;
899       }
901       // Reading these URL properties may fail and raise an exception.
902       soughtKey = soughtUrl.host + soughtUrl.filePath.toLowerCase();
903       soughtQuery = soughtUrl.query;
904     } catch (ex) {
905       // Errors while parsing the URL or accessing the properties are not fatal.
906       return gEmptyParseSubmissionResult;
907     }
909     // Look up the domain and path in the map to identify the search engine.
910     let mapEntry = this.#parseSubmissionMap.get(soughtKey);
911     if (!mapEntry) {
912       return gEmptyParseSubmissionResult;
913     }
915     // Extract the search terms from the parameter, for example "caff%C3%A8"
916     // from the URL "https://www.google.com/search?q=caff%C3%A8&client=firefox".
917     // We cannot use `URLSearchParams` here as the terms might not be
918     // encoded in UTF-8.
919     let encodedTerms = null;
920     for (let param of soughtQuery.split("&")) {
921       let equalPos = param.indexOf("=");
922       if (
923         equalPos != -1 &&
924         param.substr(0, equalPos) == mapEntry.termsParameterName
925       ) {
926         // This is the parameter we are looking for.
927         encodedTerms = param.substr(equalPos + 1);
928         break;
929       }
930     }
931     if (encodedTerms === null) {
932       return gEmptyParseSubmissionResult;
933     }
935     // Decode the terms using the charset defined in the search engine.
936     let terms;
937     try {
938       terms = Services.textToSubURI.UnEscapeAndConvert(
939         mapEntry.engine.queryCharset,
940         encodedTerms.replace(/\+/g, " ")
941       );
942     } catch (ex) {
943       // Decoding errors will cause this match to be ignored.
944       return gEmptyParseSubmissionResult;
945     }
947     return new ParseSubmissionResult(
948       mapEntry.engine,
949       terms,
950       mapEntry.termsParameterName
951     );
952   }
954   /**
955    * This is a nsITimerCallback for the timerManager notification that is
956    * registered for handling updates to search engines. Only OpenSearch engines
957    * have these updates and hence, only those are handled here.
958    */
959   notify() {
960     lazy.logConsole.debug("notify: checking for updates");
962     // Walk the engine list, looking for engines whose update time has expired.
963     var currentTime = Date.now();
964     lazy.logConsole.debug("currentTime:" + currentTime);
965     for (let engine of this._engines.values()) {
966       if (!(engine instanceof lazy.OpenSearchEngine && engine._hasUpdates)) {
967         continue;
968       }
970       var expirTime = engine.getAttr("updateexpir");
971       lazy.logConsole.debug(
972         engine.name,
973         "expirTime:",
974         expirTime,
975         "updateURL:",
976         engine._updateURL,
977         "iconUpdateURL:",
978         engine._iconUpdateURL
979       );
981       var engineExpired = expirTime <= currentTime;
983       if (!expirTime || !engineExpired) {
984         lazy.logConsole.debug("skipping engine");
985         continue;
986       }
988       lazy.logConsole.debug(engine.name, "has expired");
990       engineUpdateService.update(engine);
992       // Schedule the next update
993       engineUpdateService.scheduleNextUpdate(engine);
994     } // end engine iteration
995   }
997   #initObservers;
998   #currentEngine;
999   #currentPrivateEngine;
1000   #queuedIdle;
1002   /**
1003    * Indicates that the initialization has started or not.
1004    *
1005    * @type {boolean}
1006    */
1007   #initStarted = false;
1009   /**
1010    * Indicates if initialization has failed, succeeded or has not finished yet.
1011    *
1012    * There are 3 possible statuses:
1013    *   "not initialized" - The SearchService has not finished initialization.
1014    *   "success" - The SearchService successfully completed initialization.
1015    *   "failed" - The SearchService failed during initialization.
1016    *
1017    * @type {string}
1018    */
1019   #initializationStatus = "not initialized";
1021   /**
1022    * Indicates if we're already waiting for maybeReloadEngines to be called.
1023    *
1024    * @type {boolean}
1025    */
1026   #maybeReloadDebounce = false;
1028   /**
1029    * Indicates if we're currently in maybeReloadEngines.
1030    *
1031    * This is prefixed with _ rather than # because it is
1032    * called in a test.
1033    *
1034    * @type {boolean}
1035    */
1036   _reloadingEngines = false;
1038   /**
1039    * The engine selector singleton that is managing the engine configuration.
1040    *
1041    * @type {SearchEngineSelector|null}
1042    */
1043   #engineSelector = null;
1045   /**
1046    * Various search engines may be ignored if their submission urls contain a
1047    * string that is in the list. The list is controlled via remote settings.
1048    *
1049    * @type {Array}
1050    */
1051   #submissionURLIgnoreList = [];
1053   /**
1054    * Various search engines may be ignored if their load path is contained
1055    * in this list. The list is controlled via remote settings.
1056    *
1057    * @type {Array}
1058    */
1059   #loadPathIgnoreList = [];
1061   /**
1062    * A map of engine display names to `SearchEngine`.
1063    *
1064    * @type {Map<string, object>|null}
1065    */
1066   _engines = null;
1068   /**
1069    * An array of engine short names sorted into display order.
1070    *
1071    * @type {Array}
1072    */
1073   _cachedSortedEngines = null;
1075   /**
1076    * A flag to prevent setting of useSavedOrder when there's non-user
1077    * activity happening.
1078    *
1079    * @type {boolean}
1080    */
1081   #dontSetUseSavedOrder = false;
1083   /**
1084    * An object containing the {id, locale} of the WebExtension for the default
1085    * engine, as suggested by the configuration.
1086    * For the legacy configuration, this is the user visible name.
1087    *
1088    * @type {object}
1089    *
1090    * This is prefixed with _ rather than # because it is
1091    * called in a test.
1092    */
1093   _searchDefault = null;
1095   /**
1096    * An object containing the {id, locale} of the WebExtension for the default
1097    * engine for private browsing mode, as suggested by the configuration.
1098    * For the legacy configuration, this is the user visible name.
1099    *
1100    * @type {object}
1101    */
1102   #searchPrivateDefault = null;
1104   /**
1105    * A Set of installed search extensions reported by AddonManager
1106    * startup before SearchSevice has started. Will be installed
1107    * during init().
1108    *
1109    * @type {Set<object>}
1110    */
1111   #startupExtensions = new Set();
1113   /**
1114    * A Set of removed search extensions reported by AddonManager
1115    * startup before SearchSevice has started. Will be removed
1116    * during init().
1117    *
1118    * @type {Set<object>}
1119    */
1120   #startupRemovedExtensions = new Set();
1122   /**
1123    * A reference to the handler for the default override allow list.
1124    *
1125    * @type {SearchDefaultOverrideAllowlistHandler|null}
1126    */
1127   #defaultOverrideAllowlist = null;
1129   /**
1130    * This map is built lazily after the available search engines change.  It
1131    * allows quick parsing of an URL representing a search submission into the
1132    * search engine name and original terms.
1133    *
1134    * The keys are strings containing the domain name and lowercase path of the
1135    * engine submission, for example "www.google.com/search".
1136    *
1137    * The values are objects with these properties:
1138    * {
1139    *   engine: The associated nsISearchEngine.
1140    *   termsParameterName: Name of the URL parameter containing the search
1141    *                       terms, for example "q".
1142    * }
1143    */
1144   #parseSubmissionMap = null;
1146   /**
1147    * Keep track of observers have been added.
1148    *
1149    * @type {boolean}
1150    */
1151   #observersAdded = false;
1153   /**
1154    * Keeps track to see if the OpenSearch update timer has been started or not.
1155    *
1156    * @type {boolean}
1157    */
1158   #openSearchUpdateTimerStarted = false;
1160   get #sortedEngines() {
1161     if (!this._cachedSortedEngines) {
1162       return this.#buildSortedEngineList();
1163     }
1164     return this._cachedSortedEngines;
1165   }
1166   /**
1167    * This reflects the combined values of the prefs for enabling the separate
1168    * private default UI, and for the user choosing a separate private engine.
1169    * If either one is disabled, then we don't enable the separate private default.
1170    *
1171    * @returns {boolean}
1172    */
1173   get #separatePrivateDefault() {
1174     return (
1175       this._separatePrivateDefaultPrefValue &&
1176       this._separatePrivateDefaultEnabledPrefValue
1177     );
1178   }
1180   #getEnginesByExtensionID(extensionID) {
1181     lazy.logConsole.debug("getEngines: getting all engines for", extensionID);
1182     var engines = this.#sortedEngines.filter(function (engine) {
1183       return engine._extensionID == extensionID;
1184     });
1185     return engines;
1186   }
1188   /**
1189    * Returns the engine associated with the WebExtension details.
1190    *
1191    * @param {object} details
1192    *   Details of the WebExtension.
1193    * @param {string} details.id
1194    *   The WebExtension ID
1195    * @param {string} details.locale
1196    *   The WebExtension locale
1197    * @returns {nsISearchEngine|null}
1198    *   The found engine, or null if no engine matched.
1199    */
1200   #getEngineByWebExtensionDetails(details) {
1201     for (const engine of this._engines.values()) {
1202       if (
1203         engine._extensionID == details.id &&
1204         engine._locale == details.locale
1205       ) {
1206         return engine;
1207       }
1208     }
1209     return null;
1210   }
1212   /**
1213    * Helper function to get the current default engine.
1214    *
1215    * This is prefixed with _ rather than # because it is
1216    * called in test_remove_engine_notification_box.js
1217    *
1218    * @param {boolean} privateMode
1219    *   If true, returns the default engine for private browsing mode, otherwise
1220    *   the default engine for the normal mode. Note, this function does not
1221    *   check the "separatePrivateDefault" preference - that is up to the caller.
1222    * @returns {nsISearchEngine|null}
1223    *   The appropriate search engine, or null if one could not be determined.
1224    */
1225   _getEngineDefault(privateMode) {
1226     let currentEngine = privateMode
1227       ? this.#currentPrivateEngine
1228       : this.#currentEngine;
1230     if (currentEngine && !currentEngine.hidden) {
1231       return currentEngine;
1232     }
1234     // No default loaded, so find it from settings.
1235     const attributeName = privateMode
1236       ? "privateDefaultEngineId"
1237       : "defaultEngineId";
1239     let engineId = this._settings.getMetaDataAttribute(attributeName);
1240     let engine = this._engines.get(engineId) || null;
1241     if (
1242       engine &&
1243       this._settings.getVerifiedMetaDataAttribute(
1244         attributeName,
1245         engine.isAppProvided
1246       )
1247     ) {
1248       if (privateMode) {
1249         this.#currentPrivateEngine = engine;
1250       } else {
1251         this.#currentEngine = engine;
1252       }
1253     }
1254     if (!engineId) {
1255       if (privateMode) {
1256         this.#currentPrivateEngine = this.appPrivateDefaultEngine;
1257       } else {
1258         this.#currentEngine = this.appDefaultEngine;
1259       }
1260     }
1262     currentEngine = privateMode
1263       ? this.#currentPrivateEngine
1264       : this.#currentEngine;
1265     if (currentEngine && !currentEngine.hidden) {
1266       return currentEngine;
1267     }
1268     // No default in settings or it is hidden, so find the new default.
1269     return this.#findAndSetNewDefaultEngine({ privateMode });
1270   }
1272   /**
1273    * If initialization has not been completed yet, perform synchronous
1274    * initialization.
1275    * Throws in case of initialization error.
1276    */
1277   #ensureInitialized() {
1278     if (this.#initializationStatus === "success") {
1279       return;
1280     }
1282     if (this.#initializationStatus === "failed") {
1283       throw new Error("SearchService failed while it was initializing.");
1284     }
1286     // This Error is thrown when this.#initializationStatus is
1287     // "not initialized" because it is in the middle of initialization and
1288     // hasn't finished or hasn't started.
1289     let err = new Error(
1290       "Something tried to use the search service before it finished " +
1291         "initializing. Please examine the stack trace to figure out what and " +
1292         "where to fix it:\n"
1293     );
1294     err.message += err.stack;
1295     throw err;
1296   }
1298   /**
1299    * Asynchronous implementation of the initializer.
1300    *
1301    * @returns {number}
1302    *   A Components.results success code on success, otherwise a failure code.
1303    */
1304   async #init() {
1305     XPCOMUtils.defineLazyPreferenceGetter(
1306       this,
1307       "_separatePrivateDefaultPrefValue",
1308       lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
1309       false,
1310       this.#onSeparateDefaultPrefChanged.bind(this)
1311     );
1313     XPCOMUtils.defineLazyPreferenceGetter(
1314       this,
1315       "_separatePrivateDefaultEnabledPrefValue",
1316       lazy.SearchUtils.BROWSER_SEARCH_PREF +
1317         "separatePrivateDefault.ui.enabled",
1318       false,
1319       this.#onSeparateDefaultPrefChanged.bind(this)
1320     );
1322     XPCOMUtils.defineLazyPreferenceGetter(
1323       this,
1324       "separatePrivateDefaultUrlbarResultEnabled",
1325       lazy.SearchUtils.BROWSER_SEARCH_PREF +
1326         "separatePrivateDefault.urlbarResult.enabled",
1327       false
1328     );
1330     // We need to catch the region being updated
1331     // during initialisation so we start listening
1332     // straight away.
1333     Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);
1335     let result = Cr.NS_OK;
1336     try {
1337       if (
1338         Services.env.exists("XPCSHELL_TEST_PROFILE_DIR") &&
1339         this.willThrowErrorDuringInitInTest
1340       ) {
1341         throw new Error("Fake error during search service initialization.");
1342       }
1344       // Create the search engine selector.
1345       this.#engineSelector = new lazy.SearchEngineSelector(
1346         this.#handleConfigurationUpdated.bind(this)
1347       );
1349       // See if we have a settings file so we don't have to parse a bunch of XML.
1350       let settings = await this._settings.get();
1352       this.#setupRemoteSettings().catch(console.error);
1354       await this.#loadEngines(settings);
1356       // If we've got this far, but the application is now shutting down,
1357       // then we need to abandon any further work, especially not writing
1358       // the settings. We do this, because the add-on manager has also
1359       // started shutting down and as a result, we might have an incomplete
1360       // picture of the installed search engines. Writing the settings at
1361       // this stage would potentially mean the user would loose their engine
1362       // data.
1363       // We will however, rebuild the settings on next start up if we detect
1364       // it is necessary.
1365       if (Services.startup.shuttingDown) {
1366         lazy.logConsole.warn("#init: abandoning init due to shutting down");
1367         this.#initializationStatus = "failed";
1368         this.#initObservers.reject(Cr.NS_ERROR_ABORT);
1369         return Cr.NS_ERROR_ABORT;
1370       }
1372       // Make sure the current list of engines is persisted, without the need to wait.
1373       lazy.logConsole.debug("#init: engines loaded, writing settings");
1374       this.#initializationStatus = "success";
1375       this.#addObservers();
1376       this.#initObservers.resolve(result);
1377     } catch (error) {
1378       this.#initializationStatus = "failed";
1379       result = error.result || Cr.NS_ERROR_FAILURE;
1381       lazy.logConsole.error("#init: failure initializing search:", error);
1382       this.#initObservers.reject(result);
1383     }
1385     this.#recordTelemetryData();
1387     Services.obs.notifyObservers(
1388       null,
1389       lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
1390       "init-complete"
1391     );
1393     lazy.logConsole.debug("Completed #init");
1395     // It is possible that Nimbus could have called onUpdate before
1396     // we started listening, so do a check on startup.
1397     Services.tm.dispatchToMainThread(async () => {
1398       await lazy.NimbusFeatures.searchConfiguration.ready();
1399       this.#checkNimbusPrefs(true);
1400     });
1402     this.#maybeStartOpenSearchUpdateTimer();
1404     return result;
1405   }
1407   /**
1408    * Obtains the remote settings for the search service. This should only be
1409    * called from init(). Any subsequent updates to the remote settings are
1410    * handled via a sync listener.
1411    *
1412    * Dumps of remote settings should be available locally to avoid waiting
1413    * for the network on startup. For desktop, the dumps are located in
1414    * `services/settings/dumps/main/`.
1415    */
1416   async #setupRemoteSettings() {
1417     // Now we have the values, listen for future updates.
1418     let listener = this.#handleIgnoreListUpdated.bind(this);
1420     const current = await lazy.IgnoreLists.getAndSubscribe(listener);
1421     // Only save the listener after the subscribe, otherwise for tests it might
1422     // not be fully set up by the time we remove it again.
1423     this.ignoreListListener = listener;
1425     await this.#handleIgnoreListUpdated({ data: { current } });
1426     Services.obs.notifyObservers(
1427       null,
1428       lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
1429       "settings-update-complete"
1430     );
1431   }
1433   /**
1434    * This handles updating of the ignore list settings, and removing any ignored
1435    * engines.
1436    *
1437    * @param {object} eventData
1438    *   The event in the format received from RemoteSettings.
1439    */
1440   async #handleIgnoreListUpdated(eventData) {
1441     lazy.logConsole.debug("#handleIgnoreListUpdated");
1442     const {
1443       data: { current },
1444     } = eventData;
1446     for (const entry of current) {
1447       if (entry.id == "load-paths") {
1448         this.#loadPathIgnoreList = [...entry.matches];
1449       } else if (entry.id == "submission-urls") {
1450         this.#submissionURLIgnoreList = [...entry.matches];
1451       }
1452     }
1454     // If we have not finished initializing, then we wait for the initialization
1455     // to complete.
1456     if (!this.isInitialized) {
1457       await this.#initObservers;
1458     }
1459     // We try to remove engines manually, as this should be more efficient and
1460     // we don't really want to cause a re-init as this upsets unit tests.
1461     let engineRemoved = false;
1462     for (let engine of this._engines.values()) {
1463       if (this.#engineMatchesIgnoreLists(engine)) {
1464         await this.removeEngine(engine);
1465         engineRemoved = true;
1466       }
1467     }
1468     // If we've removed an engine, and we don't have any left, we need to
1469     // reload the engines - it is possible the settings just had one engine in it,
1470     // and that is now empty, so we need to load from our main list.
1471     if (engineRemoved && !this._engines.size) {
1472       this._maybeReloadEngines().catch(console.error);
1473     }
1474   }
1476   /**
1477    * Determines if a given engine matches the ignorelists or not.
1478    *
1479    * @param {Engine} engine
1480    *   The engine to check against the ignorelists.
1481    * @returns {boolean}
1482    *   Returns true if the engine matches a ignorelists entry.
1483    */
1484   #engineMatchesIgnoreLists(engine) {
1485     if (this.#loadPathIgnoreList.includes(engine._loadPath)) {
1486       return true;
1487     }
1488     let url = engine.searchURLWithNoTerms.spec.toLowerCase();
1489     if (
1490       this.#submissionURLIgnoreList.some(code =>
1491         url.includes(code.toLowerCase())
1492       )
1493     ) {
1494       return true;
1495     }
1496     return false;
1497   }
1499   /**
1500    * Handles the search configuration being - adds a wait on the user
1501    * being idle, before the search engine update gets handled.
1502    */
1503   #handleConfigurationUpdated() {
1504     if (this.#queuedIdle) {
1505       return;
1506     }
1508     this.#queuedIdle = true;
1510     this.idleService.addIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
1511   }
1513   /**
1514    * Returns the engine that is the default for this locale/region, ignoring any
1515    * user changes to the default engine.
1516    *
1517    * @param {boolean} privateMode
1518    *   Set to true to return the default engine in private mode,
1519    *   false for normal mode.
1520    * @returns {SearchEngine}
1521    *   The engine that is default.
1522    */
1523   #appDefaultEngine(privateMode = false) {
1524     let defaultEngine = this.#getEngineByWebExtensionDetails(
1525       privateMode && this.#searchPrivateDefault
1526         ? this.#searchPrivateDefault
1527         : this._searchDefault
1528     );
1530     if (Services.policies?.status == Ci.nsIEnterprisePolicies.ACTIVE) {
1531       let activePolicies = Services.policies.getActivePolicies();
1532       if (activePolicies.SearchEngines) {
1533         if (activePolicies.SearchEngines.Default) {
1534           return this.#getEngineByName(activePolicies.SearchEngines.Default);
1535         }
1536         if (activePolicies.SearchEngines.Remove?.includes(defaultEngine.name)) {
1537           defaultEngine = null;
1538         }
1539       }
1540     }
1542     if (defaultEngine) {
1543       return defaultEngine;
1544     }
1546     if (privateMode) {
1547       // If for some reason we can't find the private mode engine, fall back
1548       // to the non-private one.
1549       return this.#appDefaultEngine(false);
1550     }
1552     // Something unexpected has happened. In order to recover the app default
1553     // engine, use the first visible engine that is also a general purpose engine.
1554     // Worst case, we just use the first visible engine.
1555     defaultEngine = this.#sortedVisibleEngines.find(
1556       e => e.isGeneralPurposeEngine
1557     );
1558     return defaultEngine ? defaultEngine : this.#sortedVisibleEngines[0];
1559   }
1561   /**
1562    * Loads engines asynchronously.
1563    *
1564    * @param {object} settings
1565    *   An object representing the search engine settings.
1566    */
1567   async #loadEngines(settings) {
1568     // Get user's current settings and search engine before we load engines from
1569     // config. These values will be compared after engines are loaded.
1570     let prevMetaData = { ...settings?.metaData };
1571     let prevCurrentEngineId = prevMetaData.defaultEngineId;
1572     let prevAppDefaultEngineId = prevMetaData?.appDefaultEngineId;
1574     lazy.logConsole.debug("#loadEngines: start");
1575     let { engines, privateDefault } = await this._fetchEngineSelectorEngines();
1576     this.#setDefaultAndOrdersFromSelector(engines, privateDefault);
1578     // We've done what we can without the add-on manager, now ensure that
1579     // it has finished starting before we continue.
1580     await lazy.AddonManager.readyPromise;
1582     let newEngines = await this.#loadEnginesFromConfig(engines);
1583     for (let engine of newEngines) {
1584       this.#addEngineToStore(engine);
1585     }
1587     lazy.logConsole.debug(
1588       "#loadEngines: loading",
1589       this.#startupExtensions.size,
1590       "engines reported by AddonManager startup"
1591     );
1592     for (let extension of this.#startupExtensions) {
1593       try {
1594         await this.#installExtensionEngine(
1595           extension,
1596           [lazy.SearchUtils.DEFAULT_TAG],
1597           true
1598         );
1599       } catch (ex) {
1600         lazy.logConsole.error(
1601           `#installExtensionEngine failed for ${extension.id}`,
1602           ex
1603         );
1604       }
1605     }
1606     this.#startupExtensions.clear();
1608     this.#loadEnginesFromPolicies();
1610     this.#loadEnginesFromSettings(settings.engines);
1612     // Settings file version 6 and below will need a migration to store the
1613     // engine ids rather than engine names.
1614     this._settings.migrateEngineIds(settings);
1616     this.#loadEnginesMetadataFromSettings(settings.engines);
1618     lazy.logConsole.debug("#loadEngines: done");
1620     let newCurrentEngine = this._getEngineDefault(false);
1621     let newCurrentEngineId = newCurrentEngine?.id;
1623     this._settings.setMetaDataAttribute(
1624       "appDefaultEngineId",
1625       this.appDefaultEngine?.id
1626     );
1628     if (
1629       this.#shouldDisplayRemovalOfEngineNotificationBox(
1630         settings,
1631         prevMetaData,
1632         newCurrentEngineId,
1633         prevCurrentEngineId,
1634         prevAppDefaultEngineId
1635       )
1636     ) {
1637       let newCurrentEngineName = newCurrentEngine?.name;
1639       let [prevCurrentEngineName, prevAppDefaultEngineName] = [
1640         settings.engines.find(e => e.id == prevCurrentEngineId)?._name,
1641         settings.engines.find(e => e.id == prevAppDefaultEngineId)?._name,
1642       ];
1644       this._showRemovalOfSearchEngineNotificationBox(
1645         prevCurrentEngineName || prevAppDefaultEngineName,
1646         newCurrentEngineName
1647       );
1648     }
1649   }
1651   /**
1652    * Helper function to determine if the removal of search engine notification
1653    * box should be displayed.
1654    *
1655    * @param { object } settings
1656    *   The user's search engine settings.
1657    * @param { object } prevMetaData
1658    *   The user's previous search settings metadata.
1659    * @param { object } newCurrentEngineId
1660    *   The user's new current default engine.
1661    * @param { object } prevCurrentEngineId
1662    *   The user's previous default engine.
1663    * @param { object } prevAppDefaultEngineId
1664    *   The user's previous app default engine.
1665    * @returns { boolean }
1666    *   Return true if the previous default engine has been removed and
1667    *   notification box should be displayed.
1668    */
1669   #shouldDisplayRemovalOfEngineNotificationBox(
1670     settings,
1671     prevMetaData,
1672     newCurrentEngineId,
1673     prevCurrentEngineId,
1674     prevAppDefaultEngineId
1675   ) {
1676     if (
1677       !Services.prefs.getBoolPref("browser.search.removeEngineInfobar.enabled")
1678     ) {
1679       return false;
1680     }
1682     // If for some reason we were unable to install any engines and hence no
1683     // default engine, do not display the notification box
1684     if (!newCurrentEngineId) {
1685       return false;
1686     }
1688     // If the previous engine is still available, don't show the notification
1689     // box.
1690     if (prevCurrentEngineId && this._engines.has(prevCurrentEngineId)) {
1691       return false;
1692     }
1693     if (!prevCurrentEngineId && this._engines.has(prevAppDefaultEngineId)) {
1694       return false;
1695     }
1697     // Don't show the notification if the previous engine was an enterprise engine -
1698     // the text doesn't quite make sense.
1699     // let checkPolicyEngineId = prevCurrentEngineId ? prevCurrentEngineId : prevAppDefaultEngineId;
1700     let checkPolicyEngineId = prevCurrentEngineId || prevAppDefaultEngineId;
1701     if (checkPolicyEngineId) {
1702       let engineSettings = settings.engines.find(
1703         e => e.id == checkPolicyEngineId
1704       );
1705       if (engineSettings?._loadPath?.startsWith("[policy]")) {
1706         return false;
1707       }
1708     }
1710     // If the user's previous engine id is different than the new current
1711     // engine id, or if the user was using the app default engine and the
1712     // app default engine id is different than the new current engine id,
1713     // we check if the user's settings metadata has been upddated.
1714     if (
1715       (prevCurrentEngineId && prevCurrentEngineId !== newCurrentEngineId) ||
1716       (!prevCurrentEngineId &&
1717         prevAppDefaultEngineId &&
1718         prevAppDefaultEngineId !== newCurrentEngineId)
1719     ) {
1720       // Check settings metadata to detect an update to locale. Sometimes when
1721       // the user changes their locale it causes a change in engines.
1722       // If there is no update to settings metadata then the engine change was
1723       // caused by an update to config rather than a user changing their locale.
1724       if (!this.#didSettingsMetaDataUpdate(prevMetaData)) {
1725         return true;
1726       }
1727     }
1729     return false;
1730   }
1732   /**
1733    * Loads engines as specified by the configuration. We only expect
1734    * configured engines here, user engines should not be listed.
1735    *
1736    * @param {Array} engineConfigs
1737    *   An array of engines configurations based on the schema.
1738    * @returns {Array.<nsISearchEngine>}
1739    *   Returns an array of the loaded search engines. This may be
1740    *   smaller than the original list if not all engines can be loaded.
1741    */
1742   async #loadEnginesFromConfig(engineConfigs) {
1743     lazy.logConsole.debug("#loadEnginesFromConfig");
1744     let engines = [];
1745     for (let config of engineConfigs) {
1746       try {
1747         let engine = await this._makeEngineFromConfig(config);
1748         engines.push(engine);
1749       } catch (ex) {
1750         console.error(
1751           `Could not load engine ${
1752             "webExtension" in config ? config.webExtension.id : "unknown"
1753           }: ${ex}`
1754         );
1755       }
1756     }
1757     return engines;
1758   }
1760   /**
1761    * Reloads engines asynchronously, but only when
1762    * the service has already been initialized.
1763    *
1764    * This is prefixed with _ rather than # because it is
1765    * called in test_reload_engines.js
1766    *
1767    * @param {integer} changeReason
1768    *   The reason reload engines is being called, one of
1769    *   Ci.nsISearchService.CHANGE_REASON*
1770    */
1771   async _maybeReloadEngines(changeReason) {
1772     if (this.#maybeReloadDebounce) {
1773       lazy.logConsole.debug("We're already waiting to reload engines.");
1774       return;
1775     }
1777     if (!this.isInitialized || this._reloadingEngines) {
1778       this.#maybeReloadDebounce = true;
1779       // Schedule a reload to happen at most 10 seconds after the current run.
1780       Services.tm.idleDispatchToMainThread(() => {
1781         if (!this.#maybeReloadDebounce) {
1782           return;
1783         }
1784         this.#maybeReloadDebounce = false;
1785         this._maybeReloadEngines(changeReason).catch(console.error);
1786       }, 10000);
1787       lazy.logConsole.debug(
1788         "Post-poning maybeReloadEngines() as we're currently initializing."
1789       );
1790       return;
1791     }
1793     // Before entering `_reloadingEngines` get the settings which we'll need.
1794     // This also ensures that any pending settings have finished being written,
1795     // which could otherwise cause data loss.
1796     let settings = await this._settings.get();
1798     lazy.logConsole.debug("Running maybeReloadEngines");
1799     this._reloadingEngines = true;
1801     try {
1802       await this._reloadEngines(settings, changeReason);
1803     } catch (ex) {
1804       lazy.logConsole.error("maybeReloadEngines failed", ex);
1805     }
1806     this._reloadingEngines = false;
1807     lazy.logConsole.debug("maybeReloadEngines complete");
1808   }
1810   // This is prefixed with _ rather than # because it is called in
1811   // test_remove_engine_notification_box.js
1812   async _reloadEngines(settings, changeReason) {
1813     // Capture the current engine state, in case we need to notify below.
1814     let prevCurrentEngine = this.#currentEngine;
1815     let prevPrivateEngine = this.#currentPrivateEngine;
1816     let prevMetaData = { ...settings?.metaData };
1818     // Ensure that we don't set the useSavedOrder flag whilst we're doing this.
1819     // This isn't a user action, so we shouldn't be switching it.
1820     this.#dontSetUseSavedOrder = true;
1822     // The order of work here is designed to avoid potential issues when updating
1823     // the default engines, so that we're not removing active defaults or trying
1824     // to set a default to something that hasn't been added yet. The order is:
1825     //
1826     // 1) Update exising engines that are in both the old and new configuration.
1827     // 2) Add any new engines from the new configuration.
1828     // 3) Update the default engines.
1829     // 4) Remove any old engines.
1831     let { engines: appDefaultConfigEngines, privateDefault } =
1832       await this._fetchEngineSelectorEngines();
1834     let configEngines = [...appDefaultConfigEngines];
1835     let oldEngineList = [...this._engines.values()];
1837     for (let engine of oldEngineList) {
1838       if (!engine.isAppProvided) {
1839         if (engine instanceof lazy.AddonSearchEngine) {
1840           // If this is an add-on search engine, check to see if it needs
1841           // an update.
1842           await engine.update();
1843         }
1844         continue;
1845       }
1847       let index = configEngines.findIndex(
1848         e =>
1849           e.webExtension.id == engine._extensionID &&
1850           e.webExtension.locale == engine._locale
1851       );
1853       if (index == -1) {
1854         // No engines directly match on id and locale, however, check to see
1855         // if we have a new entry that matches on id and name - we might just
1856         // be swapping the in-use locale.
1857         let replacementEngines = configEngines.filter(
1858           e => e.webExtension.id == engine._extensionID
1859         );
1860         // If there's no possible, or more than one, we treat these as distinct
1861         // engines so we'll remove the existing engine and add new later if
1862         // necessary.
1863         if (replacementEngines.length != 1) {
1864           engine.pendingRemoval = true;
1865           continue;
1866         }
1868         // Update the index so we can handle the updating below.
1869         index = configEngines.findIndex(
1870           e =>
1871             e.webExtension.id == replacementEngines[0].webExtension.id &&
1872             e.webExtension.locale == replacementEngines[0].webExtension.locale
1873         );
1874         let locale =
1875           replacementEngines[0].webExtension.locale ||
1876           lazy.SearchUtils.DEFAULT_TAG;
1878         // If the name is different, then we must treat the engine as different,
1879         // and go through the remove and add cycle, rather than modifying the
1880         // existing one.
1881         let hasUpdated = await engine.updateIfNoNameChange({
1882           configuration: configEngines[index],
1883           locale,
1884         });
1885         if (!hasUpdated) {
1886           // No matching name, so just remove it.
1887           engine.pendingRemoval = true;
1888           continue;
1889         }
1890       } else {
1891         // This is an existing engine that we should update (we don't know if
1892         // the configuration for this engine has changed or not).
1893         await engine.update({
1894           configuration: configEngines[index],
1895           locale: engine._locale,
1896         });
1897       }
1899       configEngines.splice(index, 1);
1900     }
1902     // Any remaining configuration engines are ones that we need to add.
1903     for (let engine of configEngines) {
1904       try {
1905         let newEngine = await this._makeEngineFromConfig(engine);
1906         this.#addEngineToStore(newEngine, true);
1907       } catch (ex) {
1908         lazy.logConsole.warn(
1909           `Could not load engine ${
1910             "webExtension" in engine ? engine.webExtension.id : "unknown"
1911           }: ${ex}`
1912         );
1913       }
1914     }
1915     this.#loadEnginesMetadataFromSettings(settings.engines);
1917     // Now set the sort out the default engines and notify as appropriate.
1919     // Clear the current values, so that we'll completely reset.
1920     this.#currentEngine = null;
1921     this.#currentPrivateEngine = null;
1923     // If the user's default is one of the private engines that is being removed,
1924     // reset the stored setting, so that we correctly detect the change in
1925     // in default.
1926     if (prevCurrentEngine?.pendingRemoval) {
1927       this._settings.setMetaDataAttribute("defaultEngineId", "");
1928     }
1929     if (prevPrivateEngine?.pendingRemoval) {
1930       this._settings.setMetaDataAttribute("privateDefaultEngineId", "");
1931     }
1933     this.#setDefaultAndOrdersFromSelector(
1934       appDefaultConfigEngines,
1935       privateDefault
1936     );
1938     // If the defaultEngine has changed between the previous load and this one,
1939     // dispatch the appropriate notifications.
1940     if (prevCurrentEngine && this.defaultEngine !== prevCurrentEngine) {
1941       this.#recordDefaultChangedEvent(
1942         false,
1943         prevCurrentEngine,
1944         this.defaultEngine,
1945         changeReason
1946       );
1947       lazy.SearchUtils.notifyAction(
1948         this.#currentEngine,
1949         lazy.SearchUtils.MODIFIED_TYPE.DEFAULT
1950       );
1951       // If we've not got a separate private active, notify update of the
1952       // private so that the UI updates correctly.
1953       if (!this.#separatePrivateDefault) {
1954         lazy.SearchUtils.notifyAction(
1955           this.#currentEngine,
1956           lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
1957         );
1958       }
1960       if (
1961         prevMetaData &&
1962         settings.metaData &&
1963         !this.#didSettingsMetaDataUpdate(prevMetaData) &&
1964         prevCurrentEngine?.pendingRemoval &&
1965         Services.prefs.getBoolPref("browser.search.removeEngineInfobar.enabled")
1966       ) {
1967         this._showRemovalOfSearchEngineNotificationBox(
1968           prevCurrentEngine.name,
1969           this.defaultEngine.name
1970         );
1971       }
1972     }
1974     if (
1975       this.#separatePrivateDefault &&
1976       prevPrivateEngine &&
1977       this.defaultPrivateEngine !== prevPrivateEngine
1978     ) {
1979       this.#recordDefaultChangedEvent(
1980         true,
1981         prevPrivateEngine,
1982         this.defaultPrivateEngine,
1983         changeReason
1984       );
1985       lazy.SearchUtils.notifyAction(
1986         this.#currentPrivateEngine,
1987         lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
1988       );
1989     }
1991     // Finally, remove any engines that need removing. We do this after sorting
1992     // out the new default, as otherwise this could cause multiple notifications
1993     // and the wrong engine to be selected as default.
1995     for (let engine of this._engines.values()) {
1996       if (!engine.pendingRemoval) {
1997         continue;
1998       }
2000       // If we have other engines that use the same extension ID, then
2001       // we do not want to remove the add-on - only remove the engine itself.
2002       let inUseEngines = [...this._engines.values()].filter(
2003         e => e._extensionID == engine._extensionID
2004       );
2006       if (inUseEngines.length <= 1) {
2007         if (inUseEngines.length == 1 && inUseEngines[0] == engine) {
2008           // No other engines are using this extension ID.
2010           // The internal remove is done first to avoid a call to removeEngine
2011           // which could adjust the sort order when we don't want it to.
2012           this.#internalRemoveEngine(engine);
2014           let addon = await lazy.AddonManager.getAddonByID(engine._extensionID);
2015           if (addon) {
2016             // AddonManager won't call removeEngine if an engine with the
2017             // WebExtension id doesn't exist in the search service.
2018             await addon.uninstall();
2019           }
2020         }
2021         // For the case where `inUseEngines[0] != engine`:
2022         // This is a situation where there was an engine added earlier in this
2023         // function with the same name.
2024         // For example, eBay has the same name for both US and GB, but has
2025         // a different domain and uses a different locale of the same
2026         // WebExtension.
2027         // The result of this is the earlier addition has already replaced
2028         // the engine in `this._engines` (which is indexed by name), so all that
2029         // needs to be done here is to pretend the old engine was removed
2030         // which is notified below.
2031       } else {
2032         // More than one engine is using this extension ID, so we don't want to
2033         // remove the add-on.
2034         this.#internalRemoveEngine(engine);
2035       }
2036       lazy.SearchUtils.notifyAction(
2037         engine,
2038         lazy.SearchUtils.MODIFIED_TYPE.REMOVED
2039       );
2040     }
2042     // Save app default engine to the user's settings metaData incase it has
2043     // been updated
2044     this._settings.setMetaDataAttribute(
2045       "appDefaultEngineId",
2046       this.appDefaultEngine?.id
2047     );
2049     // If we are leaving an experiment, and the default is the same as the
2050     // application default, we reset the user's setting to blank, so that
2051     // future changes of the application default engine may take effect.
2052     if (
2053       prevMetaData.experiment &&
2054       !this._settings.getMetaDataAttribute("experiment")
2055     ) {
2056       if (this.defaultEngine == this.appDefaultEngine) {
2057         this._settings.setVerifiedMetaDataAttribute("defaultEngineId", "");
2058       }
2059       if (
2060         this.#separatePrivateDefault &&
2061         this.defaultPrivateEngine == this.appPrivateDefaultEngine
2062       ) {
2063         this._settings.setVerifiedMetaDataAttribute(
2064           "privateDefaultEngineId",
2065           ""
2066         );
2067       }
2068     }
2070     this.#dontSetUseSavedOrder = false;
2071     // Clear out the sorted engines settings, so that we re-sort it if necessary.
2072     this._cachedSortedEngines = null;
2073     Services.obs.notifyObservers(
2074       null,
2075       lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
2076       "engines-reloaded"
2077     );
2078   }
2080   #addEngineToStore(engine, skipDuplicateCheck = false) {
2081     if (this.#engineMatchesIgnoreLists(engine)) {
2082       lazy.logConsole.debug("#addEngineToStore: Ignoring engine");
2083       return;
2084     }
2086     lazy.logConsole.debug("#addEngineToStore: Adding engine:", engine.name);
2088     // See if there is an existing engine with the same name. However, if this
2089     // engine is updating another engine, it's allowed to have the same name.
2090     var hasSameNameAsUpdate =
2091       engine._engineToUpdate && engine.name == engine._engineToUpdate.name;
2092     if (
2093       !skipDuplicateCheck &&
2094       this.#getEngineByName(engine.name) &&
2095       !hasSameNameAsUpdate
2096     ) {
2097       lazy.logConsole.debug(
2098         "#addEngineToStore: Duplicate engine found, aborting!"
2099       );
2100       return;
2101     }
2103     if (engine._engineToUpdate) {
2104       // Update the old engine by copying over the properties of the new engine
2105       // that is loaded. It is necessary to copy over all the "private"
2106       // properties (those without a getter or setter) from one object to the
2107       // other. Other callers may hold a reference to the old engine, therefore,
2108       // anywhere else that has a reference to the old engine will receive
2109       // the properties that are updated because those other callers
2110       // are referencing the same nsISearchEngine object in memory.
2111       for (let p in engine) {
2112         if (
2113           !(
2114             Object.getOwnPropertyDescriptor(engine, p)?.get ||
2115             Object.getOwnPropertyDescriptor(engine, p)?.set
2116           )
2117         ) {
2118           engine._engineToUpdate[p] = engine[p];
2119         }
2120       }
2122       // The old engine is now updated
2123       engine = engine._engineToUpdate;
2124       engine._engineToUpdate = null;
2126       // Update the engine Map with the updated engine
2127       this._engines.set(engine.id, engine);
2129       lazy.SearchUtils.notifyAction(
2130         engine,
2131         lazy.SearchUtils.MODIFIED_TYPE.CHANGED
2132       );
2133     } else {
2134       // Not an update, just add the new engine.
2135       this._engines.set(engine.id, engine);
2136       // Only add the engine to the list of sorted engines if the initial list
2137       // has already been built (i.e. if this._cachedSortedEngines is non-null). If
2138       // it hasn't, we're loading engines from disk and the sorted engine list
2139       // will be built once we need it.
2140       if (this._cachedSortedEngines && !this.#dontSetUseSavedOrder) {
2141         this._cachedSortedEngines.push(engine);
2142         this.#saveSortedEngineList();
2143       }
2144       lazy.SearchUtils.notifyAction(
2145         engine,
2146         lazy.SearchUtils.MODIFIED_TYPE.ADDED
2147       );
2148     }
2150     // Let the engine know it can start notifying new updates.
2151     engine._engineAddedToStore = true;
2153     if (engine._hasUpdates) {
2154       // Schedule the engine's next update, if it isn't already.
2155       if (!engine.getAttr("updateexpir")) {
2156         engineUpdateService.scheduleNextUpdate(engine);
2157       }
2158     }
2159   }
2161   #loadEnginesMetadataFromSettings(engineSettings) {
2162     if (!engineSettings) {
2163       return;
2164     }
2166     for (let engineSetting of engineSettings) {
2167       let eng = this.#getEngineByName(engineSetting._name);
2168       if (eng) {
2169         lazy.logConsole.debug(
2170           "#loadEnginesMetadataFromSettings, transfering metadata for",
2171           engineSetting._name,
2172           engineSetting._metaData
2173         );
2175         // We used to store the alias in metadata.alias, in 1621892 that was
2176         // changed to only store the user set alias in metadata.alias, remove
2177         // it from metadata if it was previously set to the internal value.
2178         if (eng._alias === engineSetting?._metaData?.alias) {
2179           delete engineSetting._metaData.alias;
2180         }
2181         eng._metaData = engineSetting._metaData || {};
2182       }
2183     }
2184   }
2186   #loadEnginesFromPolicies() {
2187     if (Services.policies?.status != Ci.nsIEnterprisePolicies.ACTIVE) {
2188       return;
2189     }
2191     let activePolicies = Services.policies.getActivePolicies();
2192     if (!activePolicies.SearchEngines) {
2193       return;
2194     }
2195     for (let engineDetails of activePolicies.SearchEngines.Add ?? []) {
2196       let details = {
2197         description: engineDetails.Description,
2198         iconURL: engineDetails.IconURL ? engineDetails.IconURL.href : null,
2199         name: engineDetails.Name,
2200         // If the encoding is not specified or is falsy, we will fall back to
2201         // the default encoding.
2202         encoding: engineDetails.Encoding,
2203         search_url: encodeURI(engineDetails.URLTemplate),
2204         keyword: engineDetails.Alias,
2205         search_url_post_params:
2206           engineDetails.Method == "POST" ? engineDetails.PostData : undefined,
2207         suggest_url: engineDetails.SuggestURLTemplate,
2208       };
2209       this.#addPolicyEngine(details);
2210     }
2211   }
2213   #loadEnginesFromSettings(enginesCache) {
2214     if (!enginesCache) {
2215       return;
2216     }
2218     lazy.logConsole.debug(
2219       "#loadEnginesFromSettings: Loading",
2220       enginesCache.length,
2221       "engines from settings"
2222     );
2224     let skippedEngines = 0;
2225     for (let engineJSON of enginesCache) {
2226       // We renamed isBuiltin to isAppProvided in bug 1631898,
2227       // keep checking isBuiltin for older settings.
2228       if (engineJSON._isAppProvided || engineJSON._isBuiltin) {
2229         ++skippedEngines;
2230         continue;
2231       }
2233       // Some OpenSearch type engines are now obsolete and no longer supported.
2234       // These were application provided engines that used to use the OpenSearch
2235       // format before gecko transitioned to WebExtensions.
2236       // These will sometimes have been missed in migration due to various
2237       // reasons, and due to how the settings saves everything. We therefore
2238       // explicitly ignore them here to drop them, and let the rest of the code
2239       // fallback to the application/distribution default if necessary.
2240       let loadPath = engineJSON._loadPath?.toLowerCase();
2241       if (
2242         loadPath &&
2243         // Replaced by application provided in Firefox 79.
2244         (loadPath.startsWith("[distribution]") ||
2245           // Langpack engines moved in-app in Firefox 62.
2246           // Note: these may be prefixed by jar:,
2247           loadPath.includes("[app]/extensions/langpack") ||
2248           loadPath.includes("[other]/langpack") ||
2249           loadPath.includes("[profile]/extensions/langpack") ||
2250           // Old omni.ja engines also moved to in-app in Firefox 62.
2251           loadPath.startsWith("jar:[app]/omni.ja"))
2252       ) {
2253         continue;
2254       }
2256       try {
2257         let engine;
2258         if (loadPath?.startsWith("[policy]")) {
2259           skippedEngines++;
2260           continue;
2261         } else if (loadPath?.startsWith("[user]")) {
2262           engine = new lazy.UserSearchEngine({ json: engineJSON });
2263         } else if (engineJSON.extensionID ?? engineJSON._extensionID) {
2264           engine = new lazy.AddonSearchEngine({
2265             isAppProvided: false,
2266             json: engineJSON,
2267           });
2268         } else {
2269           engine = new lazy.OpenSearchEngine({
2270             json: engineJSON,
2271           });
2272         }
2273         this.#addEngineToStore(engine);
2274       } catch (ex) {
2275         lazy.logConsole.error(
2276           "Failed to load",
2277           engineJSON._name,
2278           "from settings:",
2279           ex,
2280           engineJSON
2281         );
2282       }
2283     }
2285     if (skippedEngines) {
2286       lazy.logConsole.debug(
2287         "#loadEnginesFromSettings: skipped",
2288         skippedEngines,
2289         "built-in/policy engines."
2290       );
2291     }
2292   }
2294   // This is prefixed with _ rather than # because it is
2295   // called in test_remove_engine_notification_box.js
2296   async _fetchEngineSelectorEngines() {
2297     let searchEngineSelectorProperties = {
2298       locale: Services.locale.appLocaleAsBCP47,
2299       region: lazy.Region.home || "default",
2300       channel: lazy.SearchUtils.MODIFIED_APP_CHANNEL,
2301       experiment:
2302         lazy.NimbusFeatures.searchConfiguration.getVariable("experiment") ?? "",
2303       distroID: lazy.SearchUtils.distroID ?? "",
2304     };
2306     for (let [key, value] of Object.entries(searchEngineSelectorProperties)) {
2307       this._settings.setMetaDataAttribute(key, value);
2308     }
2310     let { engines, privateDefault } =
2311       await this.#engineSelector.fetchEngineConfiguration(
2312         searchEngineSelectorProperties
2313       );
2315     for (let e of engines) {
2316       if (!e.webExtension) {
2317         e.webExtension = {};
2318       }
2319       e.webExtension.locale =
2320         e.webExtension?.locale ?? lazy.SearchUtils.DEFAULT_TAG;
2321     }
2323     return { engines, privateDefault };
2324   }
2326   #setDefaultAndOrdersFromSelector(engines, privateDefault) {
2327     const defaultEngine = engines[0];
2328     this._searchDefault = {
2329       id: defaultEngine.webExtension.id,
2330       locale: defaultEngine.webExtension.locale,
2331     };
2332     if (privateDefault) {
2333       this.#searchPrivateDefault = {
2334         id: privateDefault.webExtension.id,
2335         locale: privateDefault.webExtension.locale,
2336       };
2337     }
2338   }
2340   #saveSortedEngineList() {
2341     lazy.logConsole.debug("#saveSortedEngineList");
2343     // Set the useSavedOrder attribute to indicate that from now on we should
2344     // use the user's order information stored in settings.
2345     this._settings.setMetaDataAttribute("useSavedOrder", true);
2347     var engines = this.#sortedEngines;
2349     for (var i = 0; i < engines.length; ++i) {
2350       engines[i].setAttr("order", i + 1);
2351     }
2352   }
2354   #buildSortedEngineList() {
2355     // We must initialise _cachedSortedEngines here to avoid infinite recursion
2356     // in the case of tests which don't define a default search engine.
2357     // If there's no default defined, then we revert to the first item in the
2358     // sorted list, but we can't do that if we don't have a list.
2359     this._cachedSortedEngines = [];
2361     // If the user has specified a custom engine order, read the order
2362     // information from the metadata instead of the default prefs.
2363     if (this._settings.getMetaDataAttribute("useSavedOrder")) {
2364       lazy.logConsole.debug("#buildSortedEngineList: using saved order");
2365       let addedEngines = {};
2367       // Flag to keep track of whether or not we need to call #saveSortedEngineList.
2368       let needToSaveEngineList = false;
2370       for (let engine of this._engines.values()) {
2371         var orderNumber = engine.getAttr("order");
2373         // Since the DB isn't regularly cleared, and engine files may disappear
2374         // without us knowing, we may already have an engine in this slot. If
2375         // that happens, we just skip it - it will be added later on as an
2376         // unsorted engine.
2377         if (orderNumber && !this._cachedSortedEngines[orderNumber - 1]) {
2378           this._cachedSortedEngines[orderNumber - 1] = engine;
2379           addedEngines[engine.name] = engine;
2380         } else {
2381           // We need to call #saveSortedEngineList so this gets sorted out.
2382           needToSaveEngineList = true;
2383         }
2384       }
2386       // Filter out any nulls for engines that may have been removed
2387       var filteredEngines = this._cachedSortedEngines.filter(function (a) {
2388         return !!a;
2389       });
2390       if (this._cachedSortedEngines.length != filteredEngines.length) {
2391         needToSaveEngineList = true;
2392       }
2393       this._cachedSortedEngines = filteredEngines;
2395       if (needToSaveEngineList) {
2396         this.#saveSortedEngineList();
2397       }
2399       // Array for the remaining engines, alphabetically sorted.
2400       let alphaEngines = [];
2402       for (let engine of this._engines.values()) {
2403         if (!(engine.name in addedEngines)) {
2404           alphaEngines.push(engine);
2405         }
2406       }
2408       const collator = new Intl.Collator();
2409       alphaEngines.sort((a, b) => {
2410         return collator.compare(a.name, b.name);
2411       });
2412       return (this._cachedSortedEngines =
2413         this._cachedSortedEngines.concat(alphaEngines));
2414     }
2415     lazy.logConsole.debug("#buildSortedEngineList: using default orders");
2417     return (this._cachedSortedEngines = this._sortEnginesByDefaults(
2418       Array.from(this._engines.values())
2419     ));
2420   }
2422   /**
2423    * Sorts engines by the default settings (prefs, configuration values).
2424    *
2425    * @param {Array} engines
2426    *   An array of engine objects to sort.
2427    * @returns {Array}
2428    *   The sorted array of engine objects.
2429    *
2430    * This is a private method with _ rather than # because it is
2431    * called in a test.
2432    */
2433   _sortEnginesByDefaults(engines) {
2434     const sortedEngines = [];
2435     const addedEngines = new Set();
2437     function maybeAddEngineToSort(engine) {
2438       if (!engine || addedEngines.has(engine.name)) {
2439         return;
2440       }
2442       sortedEngines.push(engine);
2443       addedEngines.add(engine.name);
2444     }
2446     // The app default engine should always be first in the list (except
2447     // for distros, that we should respect).
2448     const appDefault = this.appDefaultEngine;
2449     maybeAddEngineToSort(appDefault);
2451     // If there's a private default, and it is different to the normal
2452     // default, then it should be second in the list.
2453     const appPrivateDefault = this.appPrivateDefaultEngine;
2454     if (appPrivateDefault && appPrivateDefault != appDefault) {
2455       maybeAddEngineToSort(appPrivateDefault);
2456     }
2458     let remainingEngines;
2459     const collator = new Intl.Collator();
2461     remainingEngines = engines.filter(e => !addedEngines.has(e.name));
2463     // We sort by highest orderHint first, then alphabetically by name.
2464     remainingEngines.sort((a, b) => {
2465       if (a._orderHint && b._orderHint) {
2466         if (a._orderHint == b._orderHint) {
2467           return collator.compare(a.name, b.name);
2468         }
2469         return b._orderHint - a._orderHint;
2470       }
2471       if (a._orderHint) {
2472         return -1;
2473       }
2474       if (b._orderHint) {
2475         return 1;
2476       }
2477       return collator.compare(a.name, b.name);
2478     });
2480     return [...sortedEngines, ...remainingEngines];
2481   }
2483   /**
2484    * Get a sorted array of the visible engines.
2485    *
2486    * @returns {Array<SearchEngine>}
2487    */
2489   get #sortedVisibleEngines() {
2490     return this.#sortedEngines.filter(engine => !engine.hidden);
2491   }
2493   /**
2494    * Migrates legacy add-ons which used the OpenSearch definitions to
2495    * WebExtensions, if an equivalent WebExtension is installed.
2496    *
2497    * Run during the background checks.
2498    */
2499   async #migrateLegacyEngines() {
2500     lazy.logConsole.debug("Running migrate legacy engines");
2502     const matchRegExp = /extensions\/(.*?)\.xpi!/i;
2503     for (let engine of this._engines.values()) {
2504       if (
2505         !engine.isAppProvided &&
2506         !engine._extensionID &&
2507         engine._loadPath.includes("[profile]/extensions/")
2508       ) {
2509         let match = engine._loadPath.match(matchRegExp);
2510         if (match?.[1]) {
2511           // There's a chance here that the WebExtension might not be
2512           // installed any longer, even though the engine is. We'll deal
2513           // with that in `checkWebExtensionEngines`.
2514           let engines = await this.getEnginesByExtensionID(match[1]);
2515           if (engines.length) {
2516             lazy.logConsole.debug(
2517               `Migrating ${engine.name} to WebExtension install`
2518             );
2520             if (this.defaultEngine == engine) {
2521               this.defaultEngine = engines[0];
2522             }
2523             await this.removeEngine(engine);
2524           }
2525         }
2526       }
2527     }
2529     lazy.logConsole.debug("Migrate legacy engines complete");
2530   }
2532   /**
2533    * Checks if Search Engines associated with WebExtensions are valid and
2534    * up-to-date, and reports them via telemetry if not.
2535    *
2536    * Run during the background checks.
2537    */
2538   async #checkWebExtensionEngines() {
2539     lazy.logConsole.debug("Running check on WebExtension engines");
2541     for (let engine of this._engines.values()) {
2542       if (engine instanceof lazy.AddonSearchEngine && !engine.isAppProvided) {
2543         await engine.checkAndReportIfSettingsValid();
2544       }
2545     }
2546     lazy.logConsole.debug("WebExtension engine check complete");
2547   }
2549   /**
2550    * Counts the number of secure, insecure, securely updated and insecurely
2551    * updated OpenSearch engines the user has installed and reports those
2552    * counts via telemetry.
2553    *
2554    * Run during the background checks.
2555    */
2556   async #addOpenSearchTelemetry() {
2557     let totalSecure = 0;
2558     let totalInsecure = 0;
2559     let totalWithSecureUpdates = 0;
2560     let totalWithInsecureUpdates = 0;
2562     let engine;
2563     let searchURI;
2564     let updateURI;
2565     for (let elem of this._engines) {
2566       engine = elem[1];
2567       if (engine instanceof lazy.OpenSearchEngine) {
2568         searchURI = engine.searchURLWithNoTerms;
2569         updateURI = engine._updateURI;
2571         if (lazy.SearchUtils.isSecureURIForOpenSearch(searchURI)) {
2572           totalSecure++;
2573         } else {
2574           totalInsecure++;
2575         }
2577         if (updateURI && lazy.SearchUtils.isSecureURIForOpenSearch(updateURI)) {
2578           totalWithSecureUpdates++;
2579         } else if (updateURI) {
2580           totalWithInsecureUpdates++;
2581         }
2582       }
2583     }
2585     Services.telemetry.scalarSet(
2586       "browser.searchinit.secure_opensearch_engine_count",
2587       totalSecure
2588     );
2589     Services.telemetry.scalarSet(
2590       "browser.searchinit.insecure_opensearch_engine_count",
2591       totalInsecure
2592     );
2593     Services.telemetry.scalarSet(
2594       "browser.searchinit.secure_opensearch_update_count",
2595       totalWithSecureUpdates
2596     );
2597     Services.telemetry.scalarSet(
2598       "browser.searchinit.insecure_opensearch_update_count",
2599       totalWithInsecureUpdates
2600     );
2601   }
2603   /**
2604    * Creates and adds a WebExtension based engine.
2605    *
2606    * @param {object} options
2607    *   Options for the engine.
2608    * @param {Extension} options.extension
2609    *   An Extension object containing data about the extension.
2610    * @param {string} [options.locale]
2611    *   The locale to use within the WebExtension. Defaults to the WebExtension's
2612    *   default locale.
2613    * @param {initEngine} [options.initEngine]
2614    *   Set to true if this engine is being loaded during initialisation.
2615    */
2616   async _createAndAddEngine({
2617     extension,
2618     locale = lazy.SearchUtils.DEFAULT_TAG,
2619     initEngine = false,
2620   }) {
2621     // If we're in the startup cycle, and we've already loaded this engine,
2622     // then we use the existing one rather than trying to start from scratch.
2623     // This also avoids console errors.
2624     if (extension.startupReason == "APP_STARTUP") {
2625       let engine = this.#getEngineByWebExtensionDetails({
2626         id: extension.id,
2627         locale,
2628       });
2629       if (engine) {
2630         lazy.logConsole.debug(
2631           "Engine already loaded via settings, skipping due to APP_STARTUP:",
2632           extension.id
2633         );
2634         return engine;
2635       }
2636     }
2638     // We install search extensions during the init phase, both built in
2639     // web extensions freshly installed (via addEnginesFromExtension) or
2640     // user installed extensions being reenabled calling this directly.
2641     if (!this.isInitialized && !extension.isAppProvided && !initEngine) {
2642       await this.init();
2643     }
2645     let isCurrent = false;
2647     for (let engine of this._engines.values()) {
2648       if (
2649         !engine.extensionID &&
2650         engine._loadPath.startsWith(`jar:[profile]/extensions/${extension.id}`)
2651       ) {
2652         // This is a legacy extension engine that needs to be migrated to WebExtensions.
2653         lazy.logConsole.debug("Migrating existing engine");
2654         isCurrent = isCurrent || this.defaultEngine == engine;
2655         await this.removeEngine(engine);
2656       }
2657     }
2659     let newEngine = new lazy.AddonSearchEngine({
2660       isAppProvided: extension.isAppProvided,
2661       details: {
2662         extensionID: extension.id,
2663         locale,
2664       },
2665     });
2666     await newEngine.init({
2667       extension,
2668       locale,
2669     });
2671     let existingEngine = this.#getEngineByName(newEngine.name);
2672     if (existingEngine) {
2673       throw Components.Exception(
2674         `An engine called ${newEngine.name} already exists!`,
2675         Cr.NS_ERROR_FILE_ALREADY_EXISTS
2676       );
2677     }
2679     this.#addEngineToStore(newEngine);
2680     if (isCurrent) {
2681       this.defaultEngine = newEngine;
2682     }
2683     return newEngine;
2684   }
2686   /**
2687    * Called when we see an upgrade to an existing search extension.
2688    *
2689    * @param {object} extension
2690    *   An Extension object containing data about the extension.
2691    */
2692   async #upgradeExtensionEngine(extension) {
2693     let { engines } = await this._fetchEngineSelectorEngines();
2694     let extensionEngines = await this.getEnginesByExtensionID(extension.id);
2696     for (let engine of extensionEngines) {
2697       let isDefault = engine == this.defaultEngine;
2698       let isDefaultPrivate = engine == this.defaultPrivateEngine;
2700       let originalName = engine.name;
2701       let locale = engine._locale || lazy.SearchUtils.DEFAULT_TAG;
2702       let configuration =
2703         engines.find(
2704           e =>
2705             e.webExtension.id == extension.id && e.webExtension.locale == locale
2706         ) ?? {};
2708       await engine.update({
2709         configuration,
2710         extension,
2711         locale,
2712       });
2714       if (engine.name != originalName) {
2715         if (isDefault) {
2716           this._settings.setVerifiedMetaDataAttribute(
2717             "defaultEngineId",
2718             engine.id
2719           );
2720         }
2721         if (isDefaultPrivate) {
2722           this._settings.setVerifiedMetaDataAttribute(
2723             "privateDefaultEngineId",
2724             engine.id
2725           );
2726         }
2727         this._cachedSortedEngines = null;
2728       }
2729     }
2730     return extensionEngines;
2731   }
2733   async #installExtensionEngine(extension, locales, initEngine = false) {
2734     lazy.logConsole.debug("installExtensionEngine:", extension.id);
2736     let installLocale = async locale => {
2737       return this._createAndAddEngine({ extension, locale, initEngine });
2738     };
2740     let engines = [];
2741     for (let locale of locales) {
2742       lazy.logConsole.debug(
2743         "addEnginesFromExtension: installing:",
2744         extension.id,
2745         ":",
2746         locale
2747       );
2748       engines.push(await installLocale(locale));
2749     }
2750     return engines;
2751   }
2753   #internalRemoveEngine(engine) {
2754     // Remove the engine from _sortedEngines
2755     if (this._cachedSortedEngines) {
2756       var index = this._cachedSortedEngines.indexOf(engine);
2757       if (index == -1) {
2758         throw Components.Exception(
2759           "Can't find engine to remove in _sortedEngines!",
2760           Cr.NS_ERROR_FAILURE
2761         );
2762       }
2763       this._cachedSortedEngines.splice(index, 1);
2764     }
2766     // Remove the engine from the internal store
2767     this._engines.delete(engine.id);
2768   }
2770   /**
2771    * Helper function to find a new default engine and set it. This could
2772    * be used if there is not default set yet, or if the current default is
2773    * being removed.
2774    *
2775    * This function will not consider engines that have a `pendingRemoval`
2776    * property set to true.
2777    *
2778    * The new default will be chosen from (in order):
2779    *
2780    * - Existing default from configuration, if it is not hidden.
2781    * - The first non-hidden engine that is a general search engine.
2782    * - If all other engines are hidden, unhide the default from the configuration.
2783    * - If the default from the configuration is the one being removed, unhide
2784    *   the first general search engine, or first visible engine.
2785    *
2786    * @param {boolean} privateMode
2787    *   If true, returns the default engine for private browsing mode, otherwise
2788    *   the default engine for the normal mode. Note, this function does not
2789    *   check the "separatePrivateDefault" preference - that is up to the caller.
2790    * @returns {nsISearchEngine|null}
2791    *   The appropriate search engine, or null if one could not be determined.
2792    */
2793   #findAndSetNewDefaultEngine({ privateMode }) {
2794     // First to the app default engine...
2795     let newDefault = privateMode
2796       ? this.appPrivateDefaultEngine
2797       : this.appDefaultEngine;
2799     if (!newDefault || newDefault.hidden || newDefault.pendingRemoval) {
2800       let sortedEngines = this.#sortedVisibleEngines;
2801       let generalSearchEngines = sortedEngines.filter(
2802         e => e.isGeneralPurposeEngine
2803       );
2805       // then to the first visible general search engine that isn't excluded...
2806       let firstVisible = generalSearchEngines.find(e => !e.pendingRemoval);
2807       if (firstVisible) {
2808         newDefault = firstVisible;
2809       } else if (newDefault) {
2810         // then to the app default if it is not the one that is excluded...
2811         if (!newDefault.pendingRemoval) {
2812           newDefault.hidden = false;
2813         } else {
2814           newDefault = null;
2815         }
2816       }
2818       // and finally as a last resort we unhide the first engine
2819       // even if the name is the same as the excluded one (should never happen).
2820       if (!newDefault) {
2821         if (!firstVisible) {
2822           sortedEngines = this.#sortedEngines;
2823           firstVisible = sortedEngines.find(e => e.isGeneralPurposeEngine);
2824           if (!firstVisible) {
2825             firstVisible = sortedEngines[0];
2826           }
2827         }
2828         if (firstVisible) {
2829           firstVisible.hidden = false;
2830           newDefault = firstVisible;
2831         }
2832       }
2833     }
2834     // We tried out best but something went very wrong.
2835     if (!newDefault) {
2836       lazy.logConsole.error("Could not find a replacement default engine.");
2837       return null;
2838     }
2840     // If the current engine wasn't set or was hidden, we used a fallback
2841     // to pick a new current engine. As soon as we return it, this new
2842     // current engine will become user-visible, so we should persist it.
2843     // by calling the setter.
2844     this.#setEngineDefault(privateMode, newDefault);
2846     return privateMode ? this.#currentPrivateEngine : this.#currentEngine;
2847   }
2849   /**
2850    * Helper function to set the current default engine.
2851    *
2852    * @param {boolean} privateMode
2853    *   If true, sets the default engine for private browsing mode, otherwise
2854    *   sets the default engine for the normal mode. Note, this function does not
2855    *   check the "separatePrivateDefault" preference - that is up to the caller.
2856    * @param {nsISearchEngine} newEngine
2857    *   The search engine to select
2858    * @param {SearchUtils.REASON_CHANGE_MAP} changeSource
2859    *   The source of the change of engine.
2860    */
2861   #setEngineDefault(privateMode, newEngine, changeSource) {
2862     // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers),
2863     // and sometimes we get raw Engine JS objects (callers in this file), so
2864     // handle both.
2865     if (
2866       !(newEngine instanceof Ci.nsISearchEngine) &&
2867       !(newEngine instanceof lazy.SearchEngine)
2868     ) {
2869       throw Components.Exception(
2870         "Invalid argument passed to defaultEngine setter",
2871         Cr.NS_ERROR_INVALID_ARG
2872       );
2873     }
2875     const newCurrentEngine = this._engines.get(newEngine.id);
2876     if (!newCurrentEngine) {
2877       throw Components.Exception(
2878         "Can't find engine in store!",
2879         Cr.NS_ERROR_UNEXPECTED
2880       );
2881     }
2883     if (!newCurrentEngine.isAppProvided) {
2884       // If a non default engine is being set as the current engine, ensure
2885       // its loadPath has a verification hash.
2886       if (!newCurrentEngine._loadPath) {
2887         newCurrentEngine._loadPath = "[other]unknown";
2888       }
2889       let loadPathHash = lazy.SearchUtils.getVerificationHash(
2890         newCurrentEngine._loadPath
2891       );
2892       let currentHash = newCurrentEngine.getAttr("loadPathHash");
2893       if (!currentHash || currentHash != loadPathHash) {
2894         newCurrentEngine.setAttr("loadPathHash", loadPathHash);
2895         lazy.SearchUtils.notifyAction(
2896           newCurrentEngine,
2897           lazy.SearchUtils.MODIFIED_TYPE.CHANGED
2898         );
2899       }
2900     }
2902     let currentEngine = privateMode
2903       ? this.#currentPrivateEngine
2904       : this.#currentEngine;
2906     if (newCurrentEngine == currentEngine) {
2907       return;
2908     }
2910     // Ensure that we reset an engine override if it was previously overridden.
2911     currentEngine?.removeExtensionOverride();
2913     if (privateMode) {
2914       this.#currentPrivateEngine = newCurrentEngine;
2915     } else {
2916       this.#currentEngine = newCurrentEngine;
2917     }
2919     // If we change the default engine in the future, that change should impact
2920     // users who have switched away from and then back to the build's
2921     // "app default" engine. So clear the user pref when the currentEngine is
2922     // set to the build's app default engine, so that the currentEngine getter
2923     // falls back to whatever the default is.
2924     // However, we do not do this whilst we are running an experiment - an
2925     // experiment must preseve the user's choice of default engine during it's
2926     // runtime and when it ends. Once the experiment ends, we will reset the
2927     // attribute elsewhere.
2928     let newId = newCurrentEngine.id;
2929     const appDefaultEngine = privateMode
2930       ? this.appPrivateDefaultEngine
2931       : this.appDefaultEngine;
2932     if (
2933       newCurrentEngine == appDefaultEngine &&
2934       !lazy.NimbusFeatures.searchConfiguration.getVariable("experiment")
2935     ) {
2936       newId = "";
2937     }
2939     this._settings.setVerifiedMetaDataAttribute(
2940       privateMode ? "privateDefaultEngineId" : "defaultEngineId",
2941       newId
2942     );
2944     // Only do this if we're initialized though - this function can get called
2945     // during initalization.
2946     if (this.isInitialized) {
2947       this.#recordDefaultChangedEvent(
2948         privateMode,
2949         currentEngine,
2950         newCurrentEngine,
2951         changeSource
2952       );
2953       this.#recordTelemetryData();
2954     }
2956     lazy.SearchUtils.notifyAction(
2957       newCurrentEngine,
2958       lazy.SearchUtils.MODIFIED_TYPE[
2959         privateMode ? "DEFAULT_PRIVATE" : "DEFAULT"
2960       ]
2961     );
2962     // If we've not got a separate private active, notify update of the
2963     // private so that the UI updates correctly.
2964     if (!privateMode && !this.#separatePrivateDefault) {
2965       lazy.SearchUtils.notifyAction(
2966         newCurrentEngine,
2967         lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
2968       );
2969     }
2970   }
2972   #onSeparateDefaultPrefChanged(prefName, previousValue, currentValue) {
2973     // Clear out the sorted engines settings, so that we re-sort it if necessary.
2974     this._cachedSortedEngines = null;
2975     // We should notify if the normal default, and the currently saved private
2976     // default are different. Otherwise, save the energy.
2977     if (this.defaultEngine != this._getEngineDefault(true)) {
2978       lazy.SearchUtils.notifyAction(
2979         // Always notify with the new private engine, the function checks
2980         // the preference value for us.
2981         this.defaultPrivateEngine,
2982         lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
2983       );
2984     }
2985     // Always notify about the change of status of private default if the user
2986     // toggled the UI.
2987     if (
2988       prefName ==
2989       lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault"
2990     ) {
2991       if (!previousValue && currentValue) {
2992         this.#recordDefaultChangedEvent(
2993           true,
2994           null,
2995           this._getEngineDefault(true),
2996           Ci.nsISearchService.CHANGE_REASON_USER_PRIVATE_SPLIT
2997         );
2998       } else {
2999         this.#recordDefaultChangedEvent(
3000           true,
3001           this._getEngineDefault(true),
3002           null,
3003           Ci.nsISearchService.CHANGE_REASON_USER_PRIVATE_SPLIT
3004         );
3005       }
3006     }
3007     // Update the telemetry data.
3008     this.#recordTelemetryData();
3009   }
3011   #getEngineInfo(engine) {
3012     if (!engine) {
3013       // The defaultEngine getter will throw if there's no engine at all,
3014       // which shouldn't happen unless an add-on or a test deleted all of them.
3015       // Our preferences UI doesn't let users do that.
3016       console.error("getDefaultEngineInfo: No default engine");
3017       return ["NONE", { name: "NONE" }];
3018     }
3020     const engineData = {
3021       loadPath: engine._loadPath,
3022       name: engine.name ? engine.name : "",
3023     };
3025     if (engine.isAppProvided) {
3026       engineData.origin = "default";
3027     } else {
3028       let currentHash = engine.getAttr("loadPathHash");
3029       if (!currentHash) {
3030         engineData.origin = "unverified";
3031       } else {
3032         let loadPathHash = lazy.SearchUtils.getVerificationHash(
3033           engine._loadPath
3034         );
3035         engineData.origin =
3036           currentHash == loadPathHash ? "verified" : "invalid";
3037       }
3038     }
3040     // For privacy, we only collect the submission URL for default engines...
3041     let sendSubmissionURL = engine.isAppProvided;
3043     if (!sendSubmissionURL) {
3044       // ... or engines that are the same domain as a default engine.
3045       let engineHost = engine.searchUrlDomain;
3046       for (let innerEngine of this._engines.values()) {
3047         if (!innerEngine.isAppProvided) {
3048           continue;
3049         }
3051         if (innerEngine.searchUrlDomain == engineHost) {
3052           sendSubmissionURL = true;
3053           break;
3054         }
3055       }
3057       if (!sendSubmissionURL) {
3058         // ... or well known search domains.
3059         //
3060         // Starts with: www.google., search.aol., yandex.
3061         // or
3062         // Ends with: search.yahoo.com, .ask.com, .bing.com, .startpage.com, baidu.com, duckduckgo.com
3063         const urlTest =
3064           /^(?:www\.google\.|search\.aol\.|yandex\.)|(?:search\.yahoo|\.ask|\.bing|\.startpage|\.baidu|duckduckgo)\.com$/;
3065         sendSubmissionURL = urlTest.test(engineHost);
3066       }
3067     }
3069     if (sendSubmissionURL) {
3070       let uri = engine.searchURLWithNoTerms;
3071       uri = uri
3072         .mutate()
3073         .setUserPass("") // Avoid reporting a username or password.
3074         .finalize();
3075       engineData.submissionURL = uri.spec;
3076     }
3078     return [engine.telemetryId, engineData];
3079   }
3081   /**
3082    * Records an event for where the default engine is changed. This is
3083    * recorded to both Glean and Telemetry.
3084    *
3085    * The Glean GIFFT functionality is not used here because we use longer
3086    * names in the extra arguments to the event.
3087    *
3088    * @param {boolean} isPrivate
3089    *   True if this is a event about a private engine.
3090    * @param {SearchEngine} [previousEngine]
3091    *   The previously default search engine.
3092    * @param {SearchEngine} [newEngine]
3093    *   The new default search engine.
3094    * @param {string} changeSource
3095    *   The source of the change of default.
3096    */
3097   #recordDefaultChangedEvent(
3098     isPrivate,
3099     previousEngine,
3100     newEngine,
3101     changeSource = Ci.nsISearchService.CHANGE_REASON_UNKNOWN
3102   ) {
3103     changeSource = REASON_CHANGE_MAP.get(changeSource) ?? "unknown";
3104     Services.telemetry.setEventRecordingEnabled("search", true);
3105     let telemetryId;
3106     let engineInfo;
3107     // If we are toggling the separate private browsing settings, we might not
3108     // have an engine to record.
3109     if (newEngine) {
3110       [telemetryId, engineInfo] = this.#getEngineInfo(newEngine);
3111     } else {
3112       telemetryId = "";
3113       engineInfo = {
3114         name: "",
3115         loadPath: "",
3116         submissionURL: "",
3117       };
3118     }
3120     let submissionURL = engineInfo.submissionURL ?? "";
3121     Services.telemetry.recordEvent(
3122       "search",
3123       "engine",
3124       isPrivate ? "change_private" : "change_default",
3125       changeSource,
3126       {
3127         // In docshell tests, the previous engine does not exist, so we allow
3128         // for the previousEngine to be undefined.
3129         prev_id: previousEngine?.telemetryId ?? "",
3130         new_id: telemetryId,
3131         new_name: engineInfo.name,
3132         new_load_path: engineInfo.loadPath,
3133         // Telemetry has a limit of 80 characters.
3134         new_sub_url: submissionURL.slice(0, 80),
3135       }
3136     );
3138     let extraArgs = {
3139       // In docshell tests, the previous engine does not exist, so we allow
3140       // for the previousEngine to be undefined.
3141       previous_engine_id: previousEngine?.telemetryId ?? "",
3142       new_engine_id: telemetryId,
3143       new_display_name: engineInfo.name,
3144       new_load_path: engineInfo.loadPath,
3145       // Glean has a limit of 100 characters.
3146       new_submission_url: submissionURL.slice(0, 100),
3147       change_source: changeSource,
3148     };
3149     if (isPrivate) {
3150       Glean.searchEnginePrivate.changed.record(extraArgs);
3151     } else {
3152       Glean.searchEngineDefault.changed.record(extraArgs);
3153     }
3154   }
3156   /**
3157    * Records the user's current default engine (normal and private) data to
3158    * telemetry.
3159    */
3160   #recordTelemetryData() {
3161     let info = this.getDefaultEngineInfo();
3163     Glean.searchEngineDefault.engineId.set(info.defaultSearchEngine);
3164     Glean.searchEngineDefault.displayName.set(
3165       info.defaultSearchEngineData.name
3166     );
3167     Glean.searchEngineDefault.loadPath.set(
3168       info.defaultSearchEngineData.loadPath
3169     );
3170     Glean.searchEngineDefault.submissionUrl.set(
3171       info.defaultSearchEngineData.submissionURL ?? "blank:"
3172     );
3173     Glean.searchEngineDefault.verified.set(info.defaultSearchEngineData.origin);
3175     Glean.searchEnginePrivate.engineId.set(
3176       info.defaultPrivateSearchEngine ?? ""
3177     );
3179     if (info.defaultPrivateSearchEngineData) {
3180       Glean.searchEnginePrivate.displayName.set(
3181         info.defaultPrivateSearchEngineData.name
3182       );
3183       Glean.searchEnginePrivate.loadPath.set(
3184         info.defaultPrivateSearchEngineData.loadPath
3185       );
3186       Glean.searchEnginePrivate.submissionUrl.set(
3187         info.defaultPrivateSearchEngineData.submissionURL ?? "blank:"
3188       );
3189       Glean.searchEnginePrivate.verified.set(
3190         info.defaultPrivateSearchEngineData.origin
3191       );
3192     } else {
3193       Glean.searchEnginePrivate.displayName.set("");
3194       Glean.searchEnginePrivate.loadPath.set("");
3195       Glean.searchEnginePrivate.submissionUrl.set("blank:");
3196       Glean.searchEnginePrivate.verified.set("");
3197     }
3198   }
3200   #buildParseSubmissionMap() {
3201     this.#parseSubmissionMap = new Map();
3203     // Used only while building the map, indicates which entries do not refer to
3204     // the main domain of the engine but to an alternate domain, for example
3205     // "www.google.fr" for the "www.google.com" search engine.
3206     let keysOfAlternates = new Set();
3208     for (let engine of this.#sortedEngines) {
3209       if (engine.hidden) {
3210         continue;
3211       }
3213       let urlParsingInfo = engine.getURLParsingInfo();
3214       if (!urlParsingInfo) {
3215         continue;
3216       }
3218       // Store the same object on each matching map key, as an optimization.
3219       let mapValueForEngine = {
3220         engine,
3221         termsParameterName: urlParsingInfo.termsParameterName,
3222       };
3224       let processDomain = (domain, isAlternate) => {
3225         let key = domain + urlParsingInfo.path;
3227         // Apply the logic for which main domains take priority over alternate
3228         // domains, even if they are found later in the ordered engine list.
3229         let existingEntry = this.#parseSubmissionMap.get(key);
3230         if (!existingEntry) {
3231           if (isAlternate) {
3232             keysOfAlternates.add(key);
3233           }
3234         } else if (!isAlternate && keysOfAlternates.has(key)) {
3235           keysOfAlternates.delete(key);
3236         } else {
3237           return;
3238         }
3240         this.#parseSubmissionMap.set(key, mapValueForEngine);
3241       };
3243       processDomain(urlParsingInfo.mainDomain, false);
3244       lazy.SearchStaticData.getAlternateDomains(
3245         urlParsingInfo.mainDomain
3246       ).forEach(d => processDomain(d, true));
3247     }
3248   }
3250   #nimbusSearchUpdatedFun = null;
3252   async #nimbusSearchUpdated() {
3253     this.#checkNimbusPrefs();
3254     Services.search.wrappedJSObject._maybeReloadEngines(
3255       Ci.nsISearchService.CHANGE_REASON_EXPERIMENT
3256     );
3257   }
3259   /**
3260    * Check the prefs are correctly updated for users enrolled in a Nimbus experiment.
3261    *
3262    * @param {boolean} isStartup
3263    *   Whether this function was called as part of the startup flow.
3264    */
3265   #checkNimbusPrefs(isStartup = false) {
3266     // If we are in an experiment we may need to check the status on startup, otherwise
3267     // ignore the call to check on startup so we do not reset users prefs when they are
3268     // not an experiment.
3269     if (
3270       isStartup &&
3271       !lazy.NimbusFeatures.searchConfiguration.getVariable("experiment")
3272     ) {
3273       return;
3274     }
3275     let nimbusPrivateDefaultUIEnabled =
3276       lazy.NimbusFeatures.searchConfiguration.getVariable(
3277         "seperatePrivateDefaultUIEnabled"
3278       );
3279     let nimbusPrivateDefaultUrlbarResultEnabled =
3280       lazy.NimbusFeatures.searchConfiguration.getVariable(
3281         "seperatePrivateDefaultUrlbarResultEnabled"
3282       );
3284     let previousPrivateDefault = this.defaultPrivateEngine;
3285     let uiWasEnabled = this._separatePrivateDefaultEnabledPrefValue;
3286     if (
3287       this._separatePrivateDefaultEnabledPrefValue !=
3288       nimbusPrivateDefaultUIEnabled
3289     ) {
3290       Services.prefs.setBoolPref(
3291         `${lazy.SearchUtils.BROWSER_SEARCH_PREF}separatePrivateDefault.ui.enabled`,
3292         nimbusPrivateDefaultUIEnabled
3293       );
3294       let newPrivateDefault = this.defaultPrivateEngine;
3295       if (previousPrivateDefault != newPrivateDefault) {
3296         if (!uiWasEnabled) {
3297           this.#recordDefaultChangedEvent(
3298             true,
3299             null,
3300             newPrivateDefault,
3301             Ci.nsISearchService.CHANGE_REASON_EXPERIMENT
3302           );
3303         } else {
3304           this.#recordDefaultChangedEvent(
3305             true,
3306             previousPrivateDefault,
3307             null,
3308             Ci.nsISearchService.CHANGE_REASON_EXPERIMENT
3309           );
3310         }
3311       }
3312     }
3313     if (
3314       this.separatePrivateDefaultUrlbarResultEnabled !=
3315       nimbusPrivateDefaultUrlbarResultEnabled
3316     ) {
3317       Services.prefs.setBoolPref(
3318         `${lazy.SearchUtils.BROWSER_SEARCH_PREF}separatePrivateDefault.urlbarResult.enabled`,
3319         nimbusPrivateDefaultUrlbarResultEnabled
3320       );
3321     }
3322   }
3324   #addObservers() {
3325     if (this.#observersAdded) {
3326       // There might be a race between synchronous and asynchronous
3327       // initialization for which we try to register the observers twice.
3328       return;
3329     }
3330     this.#observersAdded = true;
3332     this.#nimbusSearchUpdatedFun = this.#nimbusSearchUpdated.bind(this);
3333     lazy.NimbusFeatures.searchConfiguration.onUpdate(
3334       this.#nimbusSearchUpdatedFun
3335     );
3337     Services.obs.addObserver(this, lazy.SearchUtils.TOPIC_ENGINE_MODIFIED);
3338     Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC);
3339     Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE);
3341     this._settings.addObservers();
3343     // The current stage of shutdown. Used to help analyze crash
3344     // signatures in case of shutdown timeout.
3345     let shutdownState = {
3346       step: "Not started",
3347       latestError: {
3348         message: undefined,
3349         stack: undefined,
3350       },
3351     };
3352     IOUtils.profileBeforeChange.addBlocker(
3353       "Search service: shutting down",
3354       () =>
3355         (async () => {
3356           // If we are in initialization, then don't attempt to save the settings.
3357           // It is likely that shutdown will have caused the add-on manager to
3358           // stop, which can cause initialization to fail.
3359           // Hence at that stage, we could have broken settings which we don't
3360           // want to write.
3361           // The good news is, that if we don't write the settings here, we'll
3362           // detect the out-of-date settings on next state, and automatically
3363           // rebuild it.
3364           if (!this.isInitialized) {
3365             lazy.logConsole.warn(
3366               "not saving settings on shutdown due to initializing."
3367             );
3368             return;
3369           }
3371           try {
3372             await this._settings.shutdown(shutdownState);
3373           } catch (ex) {
3374             // Ensure that error is reported and that it causes tests
3375             // to fail, otherwise ignore it.
3376             Promise.reject(ex);
3377           }
3378         })(),
3380       () => shutdownState
3381     );
3382   }
3384   // This is prefixed with _ rather than # because it is
3385   // called in a test.
3386   _removeObservers() {
3387     if (this.ignoreListListener) {
3388       lazy.IgnoreLists.unsubscribe(this.ignoreListListener);
3389       delete this.ignoreListListener;
3390     }
3391     if (this.#queuedIdle) {
3392       this.idleService.removeIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
3393       this.#queuedIdle = false;
3394     }
3396     this._settings.removeObservers();
3398     lazy.NimbusFeatures.searchConfiguration.offUpdate(
3399       this.#nimbusSearchUpdatedFun
3400     );
3402     Services.obs.removeObserver(this, lazy.SearchUtils.TOPIC_ENGINE_MODIFIED);
3403     Services.obs.removeObserver(this, QUIT_APPLICATION_TOPIC);
3404     Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGE);
3405     Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
3406   }
3408   QueryInterface = ChromeUtils.generateQI([
3409     "nsISearchService",
3410     "nsIObserver",
3411     "nsITimerCallback",
3412   ]);
3414   // nsIObserver
3415   observe(engine, topic, verb) {
3416     switch (topic) {
3417       case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED:
3418         switch (verb) {
3419           case lazy.SearchUtils.MODIFIED_TYPE.LOADED:
3420             engine = engine.QueryInterface(Ci.nsISearchEngine);
3421             lazy.logConsole.debug(
3422               "observe: Done installation of ",
3423               engine.name
3424             );
3425             this.#addEngineToStore(engine.wrappedJSObject);
3426             // The addition of the engine to the store always triggers an ADDED
3427             // or a CHANGED notification, that will trigger the task below.
3428             break;
3429           case lazy.SearchUtils.MODIFIED_TYPE.ADDED:
3430           case lazy.SearchUtils.MODIFIED_TYPE.CHANGED:
3431           case lazy.SearchUtils.MODIFIED_TYPE.REMOVED:
3432             // Invalidate the map used to parse URLs to search engines.
3433             this.#parseSubmissionMap = null;
3434             break;
3435         }
3436         break;
3438       case "idle": {
3439         this.idleService.removeIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
3440         this.#queuedIdle = false;
3441         lazy.logConsole.debug(
3442           "Reloading engines after idle due to configuration change"
3443         );
3444         this._maybeReloadEngines(
3445           Ci.nsISearchService.CHANGE_REASON_CONFIG
3446         ).catch(console.error);
3447         break;
3448       }
3450       case QUIT_APPLICATION_TOPIC:
3451         this._removeObservers();
3452         break;
3454       case TOPIC_LOCALES_CHANGE:
3455         // Locale changed. Re-init. We rely on observers, because we can't
3456         // return this promise to anyone.
3458         // At the time of writing, when the user does a "Apply and Restart" for
3459         // a new language the preferences code triggers the locales change and
3460         // restart straight after, so we delay the check, which means we should
3461         // be able to avoid the reload on shutdown, and we'll sort it out
3462         // on next startup.
3463         // This also helps to avoid issues with the add-on manager shutting
3464         // down at the same time (see _reInit for more info).
3465         Services.tm.dispatchToMainThread(() => {
3466           if (!Services.startup.shuttingDown) {
3467             this._maybeReloadEngines(
3468               Ci.nsISearchService.CHANGE_REASON_LOCALE
3469             ).catch(console.error);
3470           }
3471         });
3472         break;
3473       case lazy.Region.REGION_TOPIC:
3474         lazy.logConsole.debug("Region updated:", lazy.Region.home);
3475         this._maybeReloadEngines(
3476           Ci.nsISearchService.CHANGE_REASON_REGION
3477         ).catch(console.error);
3478         break;
3479     }
3480   }
3482   /**
3483    * Create an engine object from the search configuration details.
3484    *
3485    * This method is prefixed with _ rather than # because it is
3486    * called in a test.
3487    *
3488    * @param {object} config
3489    *   The configuration object that defines the details of the engine
3490    *   webExtensionId etc.
3491    * @returns {nsISearchEngine}
3492    *   Returns the search engine object.
3493    */
3494   async _makeEngineFromConfig(config) {
3495     lazy.logConsole.debug("_makeEngineFromConfig:", config);
3496     let locale =
3497       "locale" in config.webExtension
3498         ? config.webExtension.locale
3499         : lazy.SearchUtils.DEFAULT_TAG;
3501     let engine = new lazy.AddonSearchEngine({
3502       isAppProvided: true,
3503       details: {
3504         extensionID: config.webExtension.id,
3505         locale,
3506       },
3507     });
3508     await engine.init({
3509       locale,
3510       config,
3511     });
3512     return engine;
3513   }
3515   /**
3516    * @param {object} metaData
3517    *    The metadata object that defines the details of the engine.
3518    * @returns {boolean}
3519    *    Returns true if metaData has different property values than
3520    *    the cached _metaData.
3521    */
3522   #didSettingsMetaDataUpdate(metaData) {
3523     let metaDataProperties = [
3524       "locale",
3525       "region",
3526       "channel",
3527       "experiment",
3528       "distroID",
3529     ];
3531     return metaDataProperties.some(p => {
3532       return metaData?.[p] !== this._settings.getMetaDataAttribute(p);
3533     });
3534   }
3536   /**
3537    * Shows an infobar to notify the user their default search engine has been
3538    * removed and replaced by a new default search engine.
3539    *
3540    * This method is prefixed with _ rather than # because it is
3541    * called in a test.
3542    *
3543    * @param {string} prevCurrentEngineName
3544    *   The name of the previous default engine that will be replaced.
3545    * @param {string} newCurrentEngineName
3546    *   The name of the engine that will be the new default engine.
3547    *
3548    */
3549   _showRemovalOfSearchEngineNotificationBox(
3550     prevCurrentEngineName,
3551     newCurrentEngineName
3552   ) {
3553     let win = Services.wm.getMostRecentBrowserWindow();
3554     win.BrowserSearch.removalOfSearchEngineNotificationBox(
3555       prevCurrentEngineName,
3556       newCurrentEngineName
3557     );
3558   }
3560   /**
3561    * Maybe starts the timer for OpenSearch engine updates. This will be set
3562    * only if updates are enabled and there are OpenSearch engines installed
3563    * which have updates.
3564    */
3565   #maybeStartOpenSearchUpdateTimer() {
3566     if (
3567       this.#openSearchUpdateTimerStarted ||
3568       !Services.prefs.getBoolPref(
3569         lazy.SearchUtils.BROWSER_SEARCH_PREF + "update",
3570         true
3571       )
3572     ) {
3573       return;
3574     }
3576     let engineWithUpdates = [...this._engines.values()].find(
3577       engine => engine instanceof lazy.OpenSearchEngine && engine._hasUpdates
3578     );
3580     if (engineWithUpdates) {
3581       lazy.logConsole.debug("Engine with updates found, setting update timer");
3582       lazy.timerManager.registerTimer(
3583         OPENSEARCH_UPDATE_TIMER_TOPIC,
3584         this,
3585         OPENSEARCH_UPDATE_TIMER_INTERVAL,
3586         true
3587       );
3588       this.#openSearchUpdateTimerStarted = true;
3589     }
3590   }
3591 } // end SearchService class
3593 var engineUpdateService = {
3594   scheduleNextUpdate(engine) {
3595     var interval = engine._updateInterval || OPENSEARCH_DEFAULT_UPDATE_INTERVAL;
3596     var milliseconds = interval * 86400000; // |interval| is in days
3597     engine.setAttr("updateexpir", Date.now() + milliseconds);
3598   },
3600   update(engine) {
3601     engine = engine.wrappedJSObject;
3602     lazy.logConsole.debug("update called for", engine._name);
3603     if (
3604       !Services.prefs.getBoolPref(
3605         lazy.SearchUtils.BROWSER_SEARCH_PREF + "update",
3606         true
3607       ) ||
3608       !engine._hasUpdates
3609     ) {
3610       return;
3611     }
3613     let testEngine = null;
3614     let updateURI = engine._updateURI;
3615     if (updateURI) {
3616       lazy.logConsole.debug("updating", engine.name, updateURI.spec);
3617       testEngine = new lazy.OpenSearchEngine();
3618       testEngine._engineToUpdate = engine;
3619       try {
3620         testEngine.install(updateURI);
3621       } catch (ex) {
3622         lazy.logConsole.error("Failed to update", engine.name, ex);
3623       }
3624     } else {
3625       lazy.logConsole.debug("invalid updateURI");
3626     }
3628     if (engine._iconUpdateURL) {
3629       // If we're updating the engine too, use the new engine object,
3630       // otherwise use the existing engine object.
3631       (testEngine || engine)._setIcon(engine._iconUpdateURL, true);
3632     }
3633   },
3636 XPCOMUtils.defineLazyServiceGetter(
3637   SearchService.prototype,
3638   "idleService",
3639   "@mozilla.org/widget/useridleservice;1",
3640   "nsIUserIdleService"
3644  * Handles getting and checking extensions against the allow list.
3645  */
3646 class SearchDefaultOverrideAllowlistHandler {
3647   /**
3648    * @param {Function} listener
3649    *   A listener for configuration update changes.
3650    */
3651   constructor(listener) {
3652     this._remoteConfig = lazy.RemoteSettings(
3653       lazy.SearchUtils.SETTINGS_ALLOWLIST_KEY
3654     );
3655   }
3657   /**
3658    * Determines if a search engine extension can override a default one
3659    * according to the allow list.
3660    *
3661    * @param {object} extension
3662    *   The extension object (from add-on manager) that will override the
3663    *   app provided search engine.
3664    * @param {string} appProvidedExtensionId
3665    *   The id of the search engine that will be overriden.
3666    * @returns {boolean}
3667    *   Returns true if the search engine extension may override the app provided
3668    *   instance.
3669    */
3670   async canOverride(extension, appProvidedExtensionId) {
3671     const overrideTable = await this._getAllowlist();
3673     let entry = overrideTable.find(e => e.thirdPartyId == extension.id);
3674     if (!entry) {
3675       return false;
3676     }
3678     if (appProvidedExtensionId != entry.overridesId) {
3679       return false;
3680     }
3682     let searchProvider =
3683       extension.manifest.chrome_settings_overrides.search_provider;
3685     return entry.urls.some(
3686       e =>
3687         searchProvider.search_url == e.search_url &&
3688         searchProvider.search_form == e.search_form &&
3689         searchProvider.search_url_get_params == e.search_url_get_params &&
3690         searchProvider.search_url_post_params == e.search_url_post_params
3691     );
3692   }
3694   /**
3695    * Obtains the configuration from remote settings. This includes
3696    * verifying the signature of the record within the database.
3697    *
3698    * If the signature in the database is invalid, the database will be wiped
3699    * and the stored dump will be used, until the settings next update.
3700    *
3701    * Note that this may cause a network check of the certificate, but that
3702    * should generally be quick.
3703    *
3704    * @returns {Array}
3705    *   An array of objects in the database, or an empty array if none
3706    *   could be obtained.
3707    */
3708   async _getAllowlist() {
3709     let result = [];
3710     try {
3711       result = await this._remoteConfig.get();
3712     } catch (ex) {
3713       // Don't throw an error just log it, just continue with no data, and hopefully
3714       // a sync will fix things later on.
3715       console.error(ex);
3716     }
3717     lazy.logConsole.debug("Allow list is:", result);
3718     return result;
3719   }