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";
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",
29 ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
30 return console.createInstance({
31 prefix: "SearchService",
32 maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
36 XPCOMUtils.defineLazyServiceGetter(
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
56 const RECONFIG_IDLE_TIME_SEC = 5 * 60;
59 * A reason that is used in the change of default search engine event telemetry.
60 * These are mutally exclusive.
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
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
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.
77 Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR_CONTEXT,
78 "user_searchbar_context",
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"],
100 * The ParseSubmissionResult contains getter methods that return attributes
101 * about the parsed submission url.
103 * @implements {nsIParseSubmissionResult}
105 class ParseSubmissionResult {
106 constructor(engine, terms, termsParameterName) {
107 this.#engine = engine;
109 this.#termsParameterName = termsParameterName;
120 get termsParameterName() {
121 return this.#termsParameterName;
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.
129 * @type {nsISearchEngine|null}
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.*
142 * The name of the query parameter used by `engine` for queries. E.g. "q".
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}
161 export class SearchService {
163 this.#initObservers = PromiseUtils.defer();
164 // this._engines is prefixed with _ rather than # because it is called from
166 this._engines = new Map();
167 this._settings = new lazy.SearchSettings(this);
170 classID = Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}");
172 get defaultEngine() {
173 this.#ensureInitialized();
174 return this._getEngineDefault(false);
177 set defaultEngine(newEngine) {
178 this.#ensureInitialized();
179 this.#setEngineDefault(false, newEngine);
182 get defaultPrivateEngine() {
183 this.#ensureInitialized();
184 return this._getEngineDefault(this.#separatePrivateDefault);
187 set defaultPrivateEngine(newEngine) {
188 this.#ensureInitialized();
189 if (!this._separatePrivateDefaultPrefValue) {
190 Services.prefs.setBoolPref(
191 lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
195 this.#setEngineDefault(this.#separatePrivateDefault, newEngine);
200 return this.defaultEngine;
203 async setDefault(engine, changeSource) {
205 this.#setEngineDefault(false, engine, changeSource);
208 async getDefaultPrivate() {
210 return this.defaultPrivateEngine;
213 async setDefaultPrivate(engine, changeSource) {
215 if (!this._separatePrivateDefaultPrefValue) {
216 Services.prefs.setBoolPref(
217 lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
221 this.#setEngineDefault(this.#separatePrivateDefault, engine, changeSource);
225 * @returns {SearchEngine}
226 * The engine that is the default for this locale/region, ignoring any
227 * user changes to the default engine.
229 get appDefaultEngine() {
230 return this.#appDefaultEngine();
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.
240 get appPrivateDefaultEngine() {
241 return this.#appDefaultEngine(this.#separatePrivateDefault);
245 * Determine whether initialization has been completed.
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.
251 * Note that this attribute does not indicate that initialization has
252 * succeeded, use hasSuccessfullyInitialized() for that.
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
258 * |false| if initialization has not been triggered yet or initialization is
261 get isInitialized() {
263 this.#initializationStatus == "success" ||
264 this.#initializationStatus == "failed"
269 * Determine whether initialization has been successfully completed.
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.
276 get hasSuccessfullyInitialized() {
277 return this.#initializationStatus == "success";
280 getDefaultEngineInfo() {
281 let [telemetryId, defaultSearchEngineData] = this.#getEngineInfo(
285 defaultSearchEngine: telemetryId,
286 defaultSearchEngineData,
289 if (this.#separatePrivateDefault) {
290 let [privateTelemetryId, defaultPrivateSearchEngineData] =
291 this.#getEngineInfo(this.defaultPrivateEngine);
292 result.defaultPrivateSearchEngine = privateTelemetryId;
293 result.defaultPrivateSearchEngineData = defaultPrivateSearchEngineData;
300 * If possible, please call getEngineById() rather than getEngineByName()
301 * because engines are stored as { id: object } in this._engine Map.
303 * Returns the engine associated with the name.
305 * @param {string} engineName
306 * The name of the engine.
307 * @returns {SearchEngine}
308 * The associated engine if found, null otherwise.
310 getEngineByName(engineName) {
311 this.#ensureInitialized();
312 return this.#getEngineByName(engineName);
316 * Returns the engine associated with the name without initialization checks.
318 * @param {string} engineName
319 * The name of the engine.
320 * @returns {SearchEngine}
321 * The associated engine if found, null otherwise.
323 #getEngineByName(engineName) {
324 for (let engine of this._engines.values()) {
325 if (engine.name == engineName) {
334 * Returns the engine associated with the id.
336 * @param {string} engineId
337 * The id of the engine.
338 * @returns {SearchEngine}
339 * The associated engine if found, null otherwise.
341 getEngineById(engineId) {
342 this.#ensureInitialized();
343 return this._engines.get(engineId) || null;
346 async getEngineByAlias(alias) {
348 for (var engine of this._engines.values()) {
349 if (engine && engine.aliases.includes(alias)) {
358 lazy.logConsole.debug("getEngines: getting all engines");
359 return this.#sortedEngines;
362 async getVisibleEngines() {
363 await this.init(true);
364 lazy.logConsole.debug("getVisibleEngines: getting all visible engines");
365 return this.#sortedVisibleEngines;
368 async getAppProvidedEngines() {
371 return this._sortEnginesByDefaults(
372 this.#sortedEngines.filter(e => e.isAppProvided)
376 async getEnginesByExtensionID(extensionID) {
378 return this.#getEnginesByExtensionID(extensionID);
383 if (this.#initStarted) {
384 return this.#initObservers.promise;
386 lazy.logConsole.debug("init");
388 TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
389 const timerId = Glean.searchService.startupTime.start();
390 this.#initStarted = true;
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);
398 this.#initializationStatus = "failed";
399 TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
400 Glean.searchService.startupTime.cancel(timerId);
401 this.#initObservers.reject(ex.result);
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
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);
425 this.#startupRemovedExtensions.clear();
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
436 async runBackgroundChecks() {
438 await this.#migrateLegacyEngines();
439 await this.#checkWebExtensionEngines();
440 await this.#addOpenSearchTelemetry();
444 * Test only - reset SearchService data. Ideally this should be replaced
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();
461 // Test-only function to set SearchService initialization status
462 forceInitializationStatusForTests(status) {
463 this.#initializationStatus = status;
467 * Test only variable to indicate an error should occur during
468 * search service initialization.
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)
482 resetToAppDefaultEngine() {
483 let appDefaultEngine = this.appDefaultEngine;
484 appDefaultEngine.hidden = false;
485 this.defaultEngine = appDefaultEngine;
488 async maybeSetAndOverrideDefault(extension) {
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.
498 canChangeToAppProvided: false,
499 canInstallEngine: !engine?.hidden,
503 if (!this.#defaultOverrideAllowlist) {
504 this.#defaultOverrideAllowlist =
505 new SearchDefaultOverrideAllowlistHandler();
509 extension.startupReason === "ADDON_INSTALL" ||
510 extension.startupReason === "ADDON_ENABLE"
512 // Don't allow an extension to set the default if it is already the default.
513 if (this.defaultEngine.name == searchProvider.name) {
515 canChangeToAppProvided: false,
516 canInstallEngine: false,
520 !(await this.#defaultOverrideAllowlist.canOverride(
525 lazy.logConsole.debug(
526 "Allowing default engine to be set to app-provided.",
529 // We don't allow overriding the engine in this case, but we can allow
530 // the extension to change the default engine.
532 canChangeToAppProvided: true,
533 canInstallEngine: false,
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.",
543 canChangeToAppProvided: true,
544 canInstallEngine: false,
549 engine.getAttr("overriddenBy") == extension.id &&
550 (await this.#defaultOverrideAllowlist.canOverride(
555 engine.overrideWithExtension(extension.id, extension.manifest);
556 lazy.logConsole.debug(
557 "Re-enabling overriding of core extension by",
561 canChangeToAppProvided: true,
562 canInstallEngine: false,
567 canChangeToAppProvided: false,
568 canInstallEngine: false,
573 * Adds a search engine that is specified from enterprise policies.
575 * @param {object} details
576 * An object that simulates the manifest object from a WebExtension. See
577 * the idl for more details.
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
588 lazy.logConsole.debug("Adding Policy Engine:", newEngine.name);
589 this.#addEngineToStore(newEngine);
593 * Adds a search engine that is specified by the user.
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
602 async addUserEngine(name, url, alias) {
605 let newEngine = new lazy.UserSearchEngine({
606 details: { name, url, alias },
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
615 lazy.logConsole.debug(`Adding ${newEngine.name}`);
616 this.#addEngineToStore(newEngine);
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.
624 * @param {object} extension
625 * An Extension object containing data about the extension.
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...
633 extension.startupReason == "ADDON_UPGRADE" ||
634 extension.startupReason == "ADDON_DOWNGRADE"
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) {
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(
656 inConfig.map(el => el.webExtension.locale)
660 lazy.logConsole.debug(
661 "addEnginesFromExtension: Ignoring builtIn engine."
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);
673 return this.#installExtensionEngine(extension, [
674 lazy.SearchUtils.DEFAULT_TAG,
678 async addOpenSearchEngine(engineURL, iconURL) {
679 lazy.logConsole.debug("addEngine: Adding", engineURL);
683 var engine = new lazy.OpenSearchEngine();
684 engine._setIcon(iconURL, false);
685 errCode = await new Promise(resolve => {
686 engine.install(engineURL, errorCode => {
694 throw Components.Exception(
695 "addEngine: Error adding engine:\n" + ex,
696 errCode || Cr.NS_ERROR_FAILURE
699 this.#maybeStartOpenSearchUpdateTimer();
703 async removeWebExtensionEngine(id) {
704 if (!this.isInitialized) {
705 lazy.logConsole.debug(
706 "Delaying removing extension engine on startup:",
709 this.#startupRemovedExtensions.add(id);
713 lazy.logConsole.debug("removeWebExtensionEngine:", id);
714 for (let engine of this.#getEnginesByExtensionID(id)) {
715 await this.removeEngine(engine);
719 async removeEngine(engine) {
722 throw Components.Exception(
723 "no engine passed to removeEngine!",
724 Cr.NS_ERROR_INVALID_ARG
728 var engineToRemove = null;
729 for (var e of this._engines.values()) {
730 if (engine.wrappedJSObject == e) {
735 if (!engineToRemove) {
736 throw Components.Exception(
737 "removeEngine: Can't find engine to remove!",
738 Cr.NS_ERROR_FILE_NOT_FOUND
742 engineToRemove.pendingRemoval = true;
744 if (engineToRemove == this.defaultEngine) {
745 this.#findAndSetNewDefaultEngine({
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
756 this.#separatePrivateDefault &&
757 engineToRemove == this.defaultPrivateEngine
759 this.#findAndSetNewDefaultEngine({
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;
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;
778 engineToRemove._filePath = null;
780 this.#internalRemoveEngine(engineToRemove);
782 // Since we removed an engine, we may need to update the preferences.
783 if (!this.#dontSetUseSavedOrder) {
784 this.#saveSortedEngineList();
787 lazy.SearchUtils.notifyAction(
789 lazy.SearchUtils.MODIFIED_TYPE.REMOVED
793 async moveEngine(engine, newIndex) {
795 if (newIndex > this.#sortedEngines.length || newIndex < 0) {
796 throw Components.Exception("moveEngine: Index out of bounds!");
799 !(engine instanceof Ci.nsISearchEngine) &&
800 !(engine instanceof lazy.SearchEngine)
802 throw Components.Exception(
803 "moveEngine: Invalid engine passed to moveEngine!",
804 Cr.NS_ERROR_INVALID_ARG
808 throw Components.Exception(
809 "moveEngine: Can't move a hidden engine!",
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
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.
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
843 for (var i = 0; i < this.#sortedEngines.length; ++i) {
844 if (newIndexEngine == this.#sortedEngines[i]) {
847 if (this.#sortedEngines[i].hidden) {
852 if (currentIndex == newIndex) {
857 var movedEngine = this._cachedSortedEngines.splice(currentIndex, 1)[0];
858 this._cachedSortedEngines.splice(newIndex, 0, movedEngine);
860 lazy.SearchUtils.notifyAction(
862 lazy.SearchUtils.MODIFIED_TYPE.CHANGED
865 // Since we moved an engine, we need to update the preferences.
866 this.#saveSortedEngineList();
869 restoreDefaultEngines() {
870 this.#ensureInitialized();
871 for (let e of this._engines.values()) {
872 // Unhide all default engines
873 if (e.hidden && e.isAppProvided) {
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;
887 if (!this.#parseSubmissionMap) {
888 this.#buildParseSubmissionMap();
891 // Extract the elements of the provided URL first.
892 let soughtKey, soughtQuery;
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;
901 // Reading these URL properties may fail and raise an exception.
902 soughtKey = soughtUrl.host + soughtUrl.filePath.toLowerCase();
903 soughtQuery = soughtUrl.query;
905 // Errors while parsing the URL or accessing the properties are not fatal.
906 return gEmptyParseSubmissionResult;
909 // Look up the domain and path in the map to identify the search engine.
910 let mapEntry = this.#parseSubmissionMap.get(soughtKey);
912 return gEmptyParseSubmissionResult;
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
919 let encodedTerms = null;
920 for (let param of soughtQuery.split("&")) {
921 let equalPos = param.indexOf("=");
924 param.substr(0, equalPos) == mapEntry.termsParameterName
926 // This is the parameter we are looking for.
927 encodedTerms = param.substr(equalPos + 1);
931 if (encodedTerms === null) {
932 return gEmptyParseSubmissionResult;
935 // Decode the terms using the charset defined in the search engine.
938 terms = Services.textToSubURI.UnEscapeAndConvert(
939 mapEntry.engine.queryCharset,
940 encodedTerms.replace(/\+/g, " ")
943 // Decoding errors will cause this match to be ignored.
944 return gEmptyParseSubmissionResult;
947 return new ParseSubmissionResult(
950 mapEntry.termsParameterName
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.
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)) {
970 var expirTime = engine.getAttr("updateexpir");
971 lazy.logConsole.debug(
978 engine._iconUpdateURL
981 var engineExpired = expirTime <= currentTime;
983 if (!expirTime || !engineExpired) {
984 lazy.logConsole.debug("skipping engine");
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
999 #currentPrivateEngine;
1003 * Indicates that the initialization has started or not.
1007 #initStarted = false;
1010 * Indicates if initialization has failed, succeeded or has not finished yet.
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.
1019 #initializationStatus = "not initialized";
1022 * Indicates if we're already waiting for maybeReloadEngines to be called.
1026 #maybeReloadDebounce = false;
1029 * Indicates if we're currently in maybeReloadEngines.
1031 * This is prefixed with _ rather than # because it is
1036 _reloadingEngines = false;
1039 * The engine selector singleton that is managing the engine configuration.
1041 * @type {SearchEngineSelector|null}
1043 #engineSelector = null;
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.
1051 #submissionURLIgnoreList = [];
1054 * Various search engines may be ignored if their load path is contained
1055 * in this list. The list is controlled via remote settings.
1059 #loadPathIgnoreList = [];
1062 * A map of engine display names to `SearchEngine`.
1064 * @type {Map<string, object>|null}
1069 * An array of engine short names sorted into display order.
1073 _cachedSortedEngines = null;
1076 * A flag to prevent setting of useSavedOrder when there's non-user
1077 * activity happening.
1081 #dontSetUseSavedOrder = false;
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.
1090 * This is prefixed with _ rather than # because it is
1093 _searchDefault = null;
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.
1102 #searchPrivateDefault = null;
1105 * A Set of installed search extensions reported by AddonManager
1106 * startup before SearchSevice has started. Will be installed
1109 * @type {Set<object>}
1111 #startupExtensions = new Set();
1114 * A Set of removed search extensions reported by AddonManager
1115 * startup before SearchSevice has started. Will be removed
1118 * @type {Set<object>}
1120 #startupRemovedExtensions = new Set();
1123 * A reference to the handler for the default override allow list.
1125 * @type {SearchDefaultOverrideAllowlistHandler|null}
1127 #defaultOverrideAllowlist = null;
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.
1134 * The keys are strings containing the domain name and lowercase path of the
1135 * engine submission, for example "www.google.com/search".
1137 * The values are objects with these properties:
1139 * engine: The associated nsISearchEngine.
1140 * termsParameterName: Name of the URL parameter containing the search
1141 * terms, for example "q".
1144 #parseSubmissionMap = null;
1147 * Keep track of observers have been added.
1151 #observersAdded = false;
1154 * Keeps track to see if the OpenSearch update timer has been started or not.
1158 #openSearchUpdateTimerStarted = false;
1160 get #sortedEngines() {
1161 if (!this._cachedSortedEngines) {
1162 return this.#buildSortedEngineList();
1164 return this._cachedSortedEngines;
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.
1171 * @returns {boolean}
1173 get #separatePrivateDefault() {
1175 this._separatePrivateDefaultPrefValue &&
1176 this._separatePrivateDefaultEnabledPrefValue
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;
1189 * Returns the engine associated with the WebExtension details.
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.
1200 #getEngineByWebExtensionDetails(details) {
1201 for (const engine of this._engines.values()) {
1203 engine._extensionID == details.id &&
1204 engine._locale == details.locale
1213 * Helper function to get the current default engine.
1215 * This is prefixed with _ rather than # because it is
1216 * called in test_remove_engine_notification_box.js
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.
1225 _getEngineDefault(privateMode) {
1226 let currentEngine = privateMode
1227 ? this.#currentPrivateEngine
1228 : this.#currentEngine;
1230 if (currentEngine && !currentEngine.hidden) {
1231 return currentEngine;
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;
1243 this._settings.getVerifiedMetaDataAttribute(
1245 engine.isAppProvided
1249 this.#currentPrivateEngine = engine;
1251 this.#currentEngine = engine;
1256 this.#currentPrivateEngine = this.appPrivateDefaultEngine;
1258 this.#currentEngine = this.appDefaultEngine;
1262 currentEngine = privateMode
1263 ? this.#currentPrivateEngine
1264 : this.#currentEngine;
1265 if (currentEngine && !currentEngine.hidden) {
1266 return currentEngine;
1268 // No default in settings or it is hidden, so find the new default.
1269 return this.#findAndSetNewDefaultEngine({ privateMode });
1273 * If initialization has not been completed yet, perform synchronous
1275 * Throws in case of initialization error.
1277 #ensureInitialized() {
1278 if (this.#initializationStatus === "success") {
1282 if (this.#initializationStatus === "failed") {
1283 throw new Error("SearchService failed while it was initializing.");
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"
1294 err.message += err.stack;
1299 * Asynchronous implementation of the initializer.
1302 * A Components.results success code on success, otherwise a failure code.
1305 XPCOMUtils.defineLazyPreferenceGetter(
1307 "_separatePrivateDefaultPrefValue",
1308 lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
1310 this.#onSeparateDefaultPrefChanged.bind(this)
1313 XPCOMUtils.defineLazyPreferenceGetter(
1315 "_separatePrivateDefaultEnabledPrefValue",
1316 lazy.SearchUtils.BROWSER_SEARCH_PREF +
1317 "separatePrivateDefault.ui.enabled",
1319 this.#onSeparateDefaultPrefChanged.bind(this)
1322 XPCOMUtils.defineLazyPreferenceGetter(
1324 "separatePrivateDefaultUrlbarResultEnabled",
1325 lazy.SearchUtils.BROWSER_SEARCH_PREF +
1326 "separatePrivateDefault.urlbarResult.enabled",
1330 // We need to catch the region being updated
1331 // during initialisation so we start listening
1333 Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);
1335 let result = Cr.NS_OK;
1338 Services.env.exists("XPCSHELL_TEST_PROFILE_DIR") &&
1339 this.willThrowErrorDuringInitInTest
1341 throw new Error("Fake error during search service initialization.");
1344 // Create the search engine selector.
1345 this.#engineSelector = new lazy.SearchEngineSelector(
1346 this.#handleConfigurationUpdated.bind(this)
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
1363 // We will however, rebuild the settings on next start up if we detect
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;
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);
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);
1385 this.#recordTelemetryData();
1387 Services.obs.notifyObservers(
1389 lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
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);
1402 this.#maybeStartOpenSearchUpdateTimer();
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.
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/`.
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(
1428 lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
1429 "settings-update-complete"
1434 * This handles updating of the ignore list settings, and removing any ignored
1437 * @param {object} eventData
1438 * The event in the format received from RemoteSettings.
1440 async #handleIgnoreListUpdated(eventData) {
1441 lazy.logConsole.debug("#handleIgnoreListUpdated");
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];
1454 // If we have not finished initializing, then we wait for the initialization
1456 if (!this.isInitialized) {
1457 await this.#initObservers;
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;
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);
1477 * Determines if a given engine matches the ignorelists or not.
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.
1484 #engineMatchesIgnoreLists(engine) {
1485 if (this.#loadPathIgnoreList.includes(engine._loadPath)) {
1488 let url = engine.searchURLWithNoTerms.spec.toLowerCase();
1490 this.#submissionURLIgnoreList.some(code =>
1491 url.includes(code.toLowerCase())
1500 * Handles the search configuration being - adds a wait on the user
1501 * being idle, before the search engine update gets handled.
1503 #handleConfigurationUpdated() {
1504 if (this.#queuedIdle) {
1508 this.#queuedIdle = true;
1510 this.idleService.addIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
1514 * Returns the engine that is the default for this locale/region, ignoring any
1515 * user changes to the default engine.
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.
1523 #appDefaultEngine(privateMode = false) {
1524 let defaultEngine = this.#getEngineByWebExtensionDetails(
1525 privateMode && this.#searchPrivateDefault
1526 ? this.#searchPrivateDefault
1527 : this._searchDefault
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);
1536 if (activePolicies.SearchEngines.Remove?.includes(defaultEngine.name)) {
1537 defaultEngine = null;
1542 if (defaultEngine) {
1543 return defaultEngine;
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);
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
1558 return defaultEngine ? defaultEngine : this.#sortedVisibleEngines[0];
1562 * Loads engines asynchronously.
1564 * @param {object} settings
1565 * An object representing the search engine settings.
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);
1587 lazy.logConsole.debug(
1588 "#loadEngines: loading",
1589 this.#startupExtensions.size,
1590 "engines reported by AddonManager startup"
1592 for (let extension of this.#startupExtensions) {
1594 await this.#installExtensionEngine(
1596 [lazy.SearchUtils.DEFAULT_TAG],
1600 lazy.logConsole.error(
1601 `#installExtensionEngine failed for ${extension.id}`,
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
1629 this.#shouldDisplayRemovalOfEngineNotificationBox(
1633 prevCurrentEngineId,
1634 prevAppDefaultEngineId
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,
1644 this._showRemovalOfSearchEngineNotificationBox(
1645 prevCurrentEngineName || prevAppDefaultEngineName,
1646 newCurrentEngineName
1652 * Helper function to determine if the removal of search engine notification
1653 * box should be displayed.
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.
1669 #shouldDisplayRemovalOfEngineNotificationBox(
1673 prevCurrentEngineId,
1674 prevAppDefaultEngineId
1677 !Services.prefs.getBoolPref("browser.search.removeEngineInfobar.enabled")
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) {
1688 // If the previous engine is still available, don't show the notification
1690 if (prevCurrentEngineId && this._engines.has(prevCurrentEngineId)) {
1693 if (!prevCurrentEngineId && this._engines.has(prevAppDefaultEngineId)) {
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
1705 if (engineSettings?._loadPath?.startsWith("[policy]")) {
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.
1715 (prevCurrentEngineId && prevCurrentEngineId !== newCurrentEngineId) ||
1716 (!prevCurrentEngineId &&
1717 prevAppDefaultEngineId &&
1718 prevAppDefaultEngineId !== newCurrentEngineId)
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)) {
1733 * Loads engines as specified by the configuration. We only expect
1734 * configured engines here, user engines should not be listed.
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.
1742 async #loadEnginesFromConfig(engineConfigs) {
1743 lazy.logConsole.debug("#loadEnginesFromConfig");
1745 for (let config of engineConfigs) {
1747 let engine = await this._makeEngineFromConfig(config);
1748 engines.push(engine);
1751 `Could not load engine ${
1752 "webExtension" in config ? config.webExtension.id : "unknown"
1761 * Reloads engines asynchronously, but only when
1762 * the service has already been initialized.
1764 * This is prefixed with _ rather than # because it is
1765 * called in test_reload_engines.js
1767 * @param {integer} changeReason
1768 * The reason reload engines is being called, one of
1769 * Ci.nsISearchService.CHANGE_REASON*
1771 async _maybeReloadEngines(changeReason) {
1772 if (this.#maybeReloadDebounce) {
1773 lazy.logConsole.debug("We're already waiting to reload engines.");
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) {
1784 this.#maybeReloadDebounce = false;
1785 this._maybeReloadEngines(changeReason).catch(console.error);
1787 lazy.logConsole.debug(
1788 "Post-poning maybeReloadEngines() as we're currently initializing."
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;
1802 await this._reloadEngines(settings, changeReason);
1804 lazy.logConsole.error("maybeReloadEngines failed", ex);
1806 this._reloadingEngines = false;
1807 lazy.logConsole.debug("maybeReloadEngines complete");
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:
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
1842 await engine.update();
1847 let index = configEngines.findIndex(
1849 e.webExtension.id == engine._extensionID &&
1850 e.webExtension.locale == engine._locale
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
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
1863 if (replacementEngines.length != 1) {
1864 engine.pendingRemoval = true;
1868 // Update the index so we can handle the updating below.
1869 index = configEngines.findIndex(
1871 e.webExtension.id == replacementEngines[0].webExtension.id &&
1872 e.webExtension.locale == replacementEngines[0].webExtension.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
1881 let hasUpdated = await engine.updateIfNoNameChange({
1882 configuration: configEngines[index],
1886 // No matching name, so just remove it.
1887 engine.pendingRemoval = true;
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,
1899 configEngines.splice(index, 1);
1902 // Any remaining configuration engines are ones that we need to add.
1903 for (let engine of configEngines) {
1905 let newEngine = await this._makeEngineFromConfig(engine);
1906 this.#addEngineToStore(newEngine, true);
1908 lazy.logConsole.warn(
1909 `Could not load engine ${
1910 "webExtension" in engine ? engine.webExtension.id : "unknown"
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
1926 if (prevCurrentEngine?.pendingRemoval) {
1927 this._settings.setMetaDataAttribute("defaultEngineId", "");
1929 if (prevPrivateEngine?.pendingRemoval) {
1930 this._settings.setMetaDataAttribute("privateDefaultEngineId", "");
1933 this.#setDefaultAndOrdersFromSelector(
1934 appDefaultConfigEngines,
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(
1947 lazy.SearchUtils.notifyAction(
1948 this.#currentEngine,
1949 lazy.SearchUtils.MODIFIED_TYPE.DEFAULT
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
1962 settings.metaData &&
1963 !this.#didSettingsMetaDataUpdate(prevMetaData) &&
1964 prevCurrentEngine?.pendingRemoval &&
1965 Services.prefs.getBoolPref("browser.search.removeEngineInfobar.enabled")
1967 this._showRemovalOfSearchEngineNotificationBox(
1968 prevCurrentEngine.name,
1969 this.defaultEngine.name
1975 this.#separatePrivateDefault &&
1976 prevPrivateEngine &&
1977 this.defaultPrivateEngine !== prevPrivateEngine
1979 this.#recordDefaultChangedEvent(
1982 this.defaultPrivateEngine,
1985 lazy.SearchUtils.notifyAction(
1986 this.#currentPrivateEngine,
1987 lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
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) {
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
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);
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();
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
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.
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);
2036 lazy.SearchUtils.notifyAction(
2038 lazy.SearchUtils.MODIFIED_TYPE.REMOVED
2042 // Save app default engine to the user's settings metaData incase it has
2044 this._settings.setMetaDataAttribute(
2045 "appDefaultEngineId",
2046 this.appDefaultEngine?.id
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.
2053 prevMetaData.experiment &&
2054 !this._settings.getMetaDataAttribute("experiment")
2056 if (this.defaultEngine == this.appDefaultEngine) {
2057 this._settings.setVerifiedMetaDataAttribute("defaultEngineId", "");
2060 this.#separatePrivateDefault &&
2061 this.defaultPrivateEngine == this.appPrivateDefaultEngine
2063 this._settings.setVerifiedMetaDataAttribute(
2064 "privateDefaultEngineId",
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(
2075 lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
2080 #addEngineToStore(engine, skipDuplicateCheck = false) {
2081 if (this.#engineMatchesIgnoreLists(engine)) {
2082 lazy.logConsole.debug("#addEngineToStore: Ignoring engine");
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;
2093 !skipDuplicateCheck &&
2094 this.#getEngineByName(engine.name) &&
2095 !hasSameNameAsUpdate
2097 lazy.logConsole.debug(
2098 "#addEngineToStore: Duplicate engine found, aborting!"
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) {
2114 Object.getOwnPropertyDescriptor(engine, p)?.get ||
2115 Object.getOwnPropertyDescriptor(engine, p)?.set
2118 engine._engineToUpdate[p] = engine[p];
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(
2131 lazy.SearchUtils.MODIFIED_TYPE.CHANGED
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();
2144 lazy.SearchUtils.notifyAction(
2146 lazy.SearchUtils.MODIFIED_TYPE.ADDED
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);
2161 #loadEnginesMetadataFromSettings(engineSettings) {
2162 if (!engineSettings) {
2166 for (let engineSetting of engineSettings) {
2167 let eng = this.#getEngineByName(engineSetting._name);
2169 lazy.logConsole.debug(
2170 "#loadEnginesMetadataFromSettings, transfering metadata for",
2171 engineSetting._name,
2172 engineSetting._metaData
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;
2181 eng._metaData = engineSetting._metaData || {};
2186 #loadEnginesFromPolicies() {
2187 if (Services.policies?.status != Ci.nsIEnterprisePolicies.ACTIVE) {
2191 let activePolicies = Services.policies.getActivePolicies();
2192 if (!activePolicies.SearchEngines) {
2195 for (let engineDetails of activePolicies.SearchEngines.Add ?? []) {
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,
2209 this.#addPolicyEngine(details);
2213 #loadEnginesFromSettings(enginesCache) {
2214 if (!enginesCache) {
2218 lazy.logConsole.debug(
2219 "#loadEnginesFromSettings: Loading",
2220 enginesCache.length,
2221 "engines from settings"
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) {
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();
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"))
2258 if (loadPath?.startsWith("[policy]")) {
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,
2269 engine = new lazy.OpenSearchEngine({
2273 this.#addEngineToStore(engine);
2275 lazy.logConsole.error(
2285 if (skippedEngines) {
2286 lazy.logConsole.debug(
2287 "#loadEnginesFromSettings: skipped",
2289 "built-in/policy engines."
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,
2302 lazy.NimbusFeatures.searchConfiguration.getVariable("experiment") ?? "",
2303 distroID: lazy.SearchUtils.distroID ?? "",
2306 for (let [key, value] of Object.entries(searchEngineSelectorProperties)) {
2307 this._settings.setMetaDataAttribute(key, value);
2310 let { engines, privateDefault } =
2311 await this.#engineSelector.fetchEngineConfiguration(
2312 searchEngineSelectorProperties
2315 for (let e of engines) {
2316 if (!e.webExtension) {
2317 e.webExtension = {};
2319 e.webExtension.locale =
2320 e.webExtension?.locale ?? lazy.SearchUtils.DEFAULT_TAG;
2323 return { engines, privateDefault };
2326 #setDefaultAndOrdersFromSelector(engines, privateDefault) {
2327 const defaultEngine = engines[0];
2328 this._searchDefault = {
2329 id: defaultEngine.webExtension.id,
2330 locale: defaultEngine.webExtension.locale,
2332 if (privateDefault) {
2333 this.#searchPrivateDefault = {
2334 id: privateDefault.webExtension.id,
2335 locale: privateDefault.webExtension.locale,
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);
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
2377 if (orderNumber && !this._cachedSortedEngines[orderNumber - 1]) {
2378 this._cachedSortedEngines[orderNumber - 1] = engine;
2379 addedEngines[engine.name] = engine;
2381 // We need to call #saveSortedEngineList so this gets sorted out.
2382 needToSaveEngineList = true;
2386 // Filter out any nulls for engines that may have been removed
2387 var filteredEngines = this._cachedSortedEngines.filter(function (a) {
2390 if (this._cachedSortedEngines.length != filteredEngines.length) {
2391 needToSaveEngineList = true;
2393 this._cachedSortedEngines = filteredEngines;
2395 if (needToSaveEngineList) {
2396 this.#saveSortedEngineList();
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);
2408 const collator = new Intl.Collator();
2409 alphaEngines.sort((a, b) => {
2410 return collator.compare(a.name, b.name);
2412 return (this._cachedSortedEngines =
2413 this._cachedSortedEngines.concat(alphaEngines));
2415 lazy.logConsole.debug("#buildSortedEngineList: using default orders");
2417 return (this._cachedSortedEngines = this._sortEnginesByDefaults(
2418 Array.from(this._engines.values())
2423 * Sorts engines by the default settings (prefs, configuration values).
2425 * @param {Array} engines
2426 * An array of engine objects to sort.
2428 * The sorted array of engine objects.
2430 * This is a private method with _ rather than # because it is
2433 _sortEnginesByDefaults(engines) {
2434 const sortedEngines = [];
2435 const addedEngines = new Set();
2437 function maybeAddEngineToSort(engine) {
2438 if (!engine || addedEngines.has(engine.name)) {
2442 sortedEngines.push(engine);
2443 addedEngines.add(engine.name);
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);
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);
2469 return b._orderHint - a._orderHint;
2477 return collator.compare(a.name, b.name);
2480 return [...sortedEngines, ...remainingEngines];
2484 * Get a sorted array of the visible engines.
2486 * @returns {Array<SearchEngine>}
2489 get #sortedVisibleEngines() {
2490 return this.#sortedEngines.filter(engine => !engine.hidden);
2494 * Migrates legacy add-ons which used the OpenSearch definitions to
2495 * WebExtensions, if an equivalent WebExtension is installed.
2497 * Run during the background checks.
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()) {
2505 !engine.isAppProvided &&
2506 !engine._extensionID &&
2507 engine._loadPath.includes("[profile]/extensions/")
2509 let match = engine._loadPath.match(matchRegExp);
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`
2520 if (this.defaultEngine == engine) {
2521 this.defaultEngine = engines[0];
2523 await this.removeEngine(engine);
2529 lazy.logConsole.debug("Migrate legacy engines complete");
2533 * Checks if Search Engines associated with WebExtensions are valid and
2534 * up-to-date, and reports them via telemetry if not.
2536 * Run during the background checks.
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();
2546 lazy.logConsole.debug("WebExtension engine check complete");
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.
2554 * Run during the background checks.
2556 async #addOpenSearchTelemetry() {
2557 let totalSecure = 0;
2558 let totalInsecure = 0;
2559 let totalWithSecureUpdates = 0;
2560 let totalWithInsecureUpdates = 0;
2565 for (let elem of this._engines) {
2567 if (engine instanceof lazy.OpenSearchEngine) {
2568 searchURI = engine.searchURLWithNoTerms;
2569 updateURI = engine._updateURI;
2571 if (lazy.SearchUtils.isSecureURIForOpenSearch(searchURI)) {
2577 if (updateURI && lazy.SearchUtils.isSecureURIForOpenSearch(updateURI)) {
2578 totalWithSecureUpdates++;
2579 } else if (updateURI) {
2580 totalWithInsecureUpdates++;
2585 Services.telemetry.scalarSet(
2586 "browser.searchinit.secure_opensearch_engine_count",
2589 Services.telemetry.scalarSet(
2590 "browser.searchinit.insecure_opensearch_engine_count",
2593 Services.telemetry.scalarSet(
2594 "browser.searchinit.secure_opensearch_update_count",
2595 totalWithSecureUpdates
2597 Services.telemetry.scalarSet(
2598 "browser.searchinit.insecure_opensearch_update_count",
2599 totalWithInsecureUpdates
2604 * Creates and adds a WebExtension based engine.
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
2613 * @param {initEngine} [options.initEngine]
2614 * Set to true if this engine is being loaded during initialisation.
2616 async _createAndAddEngine({
2618 locale = lazy.SearchUtils.DEFAULT_TAG,
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({
2630 lazy.logConsole.debug(
2631 "Engine already loaded via settings, skipping due to APP_STARTUP:",
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) {
2645 let isCurrent = false;
2647 for (let engine of this._engines.values()) {
2649 !engine.extensionID &&
2650 engine._loadPath.startsWith(`jar:[profile]/extensions/${extension.id}`)
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);
2659 let newEngine = new lazy.AddonSearchEngine({
2660 isAppProvided: extension.isAppProvided,
2662 extensionID: extension.id,
2666 await newEngine.init({
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
2679 this.#addEngineToStore(newEngine);
2681 this.defaultEngine = newEngine;
2687 * Called when we see an upgrade to an existing search extension.
2689 * @param {object} extension
2690 * An Extension object containing data about the extension.
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;
2705 e.webExtension.id == extension.id && e.webExtension.locale == locale
2708 await engine.update({
2714 if (engine.name != originalName) {
2716 this._settings.setVerifiedMetaDataAttribute(
2721 if (isDefaultPrivate) {
2722 this._settings.setVerifiedMetaDataAttribute(
2723 "privateDefaultEngineId",
2727 this._cachedSortedEngines = null;
2730 return extensionEngines;
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 });
2741 for (let locale of locales) {
2742 lazy.logConsole.debug(
2743 "addEnginesFromExtension: installing:",
2748 engines.push(await installLocale(locale));
2753 #internalRemoveEngine(engine) {
2754 // Remove the engine from _sortedEngines
2755 if (this._cachedSortedEngines) {
2756 var index = this._cachedSortedEngines.indexOf(engine);
2758 throw Components.Exception(
2759 "Can't find engine to remove in _sortedEngines!",
2763 this._cachedSortedEngines.splice(index, 1);
2766 // Remove the engine from the internal store
2767 this._engines.delete(engine.id);
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
2775 * This function will not consider engines that have a `pendingRemoval`
2776 * property set to true.
2778 * The new default will be chosen from (in order):
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.
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.
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
2805 // then to the first visible general search engine that isn't excluded...
2806 let firstVisible = generalSearchEngines.find(e => !e.pendingRemoval);
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;
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).
2821 if (!firstVisible) {
2822 sortedEngines = this.#sortedEngines;
2823 firstVisible = sortedEngines.find(e => e.isGeneralPurposeEngine);
2824 if (!firstVisible) {
2825 firstVisible = sortedEngines[0];
2829 firstVisible.hidden = false;
2830 newDefault = firstVisible;
2834 // We tried out best but something went very wrong.
2836 lazy.logConsole.error("Could not find a replacement default engine.");
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;
2850 * Helper function to set the current default engine.
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.
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
2866 !(newEngine instanceof Ci.nsISearchEngine) &&
2867 !(newEngine instanceof lazy.SearchEngine)
2869 throw Components.Exception(
2870 "Invalid argument passed to defaultEngine setter",
2871 Cr.NS_ERROR_INVALID_ARG
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
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";
2889 let loadPathHash = lazy.SearchUtils.getVerificationHash(
2890 newCurrentEngine._loadPath
2892 let currentHash = newCurrentEngine.getAttr("loadPathHash");
2893 if (!currentHash || currentHash != loadPathHash) {
2894 newCurrentEngine.setAttr("loadPathHash", loadPathHash);
2895 lazy.SearchUtils.notifyAction(
2897 lazy.SearchUtils.MODIFIED_TYPE.CHANGED
2902 let currentEngine = privateMode
2903 ? this.#currentPrivateEngine
2904 : this.#currentEngine;
2906 if (newCurrentEngine == currentEngine) {
2910 // Ensure that we reset an engine override if it was previously overridden.
2911 currentEngine?.removeExtensionOverride();
2914 this.#currentPrivateEngine = newCurrentEngine;
2916 this.#currentEngine = newCurrentEngine;
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;
2933 newCurrentEngine == appDefaultEngine &&
2934 !lazy.NimbusFeatures.searchConfiguration.getVariable("experiment")
2939 this._settings.setVerifiedMetaDataAttribute(
2940 privateMode ? "privateDefaultEngineId" : "defaultEngineId",
2944 // Only do this if we're initialized though - this function can get called
2945 // during initalization.
2946 if (this.isInitialized) {
2947 this.#recordDefaultChangedEvent(
2953 this.#recordTelemetryData();
2956 lazy.SearchUtils.notifyAction(
2958 lazy.SearchUtils.MODIFIED_TYPE[
2959 privateMode ? "DEFAULT_PRIVATE" : "DEFAULT"
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(
2967 lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE
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
2985 // Always notify about the change of status of private default if the user
2989 lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault"
2991 if (!previousValue && currentValue) {
2992 this.#recordDefaultChangedEvent(
2995 this._getEngineDefault(true),
2996 Ci.nsISearchService.CHANGE_REASON_USER_PRIVATE_SPLIT
2999 this.#recordDefaultChangedEvent(
3001 this._getEngineDefault(true),
3003 Ci.nsISearchService.CHANGE_REASON_USER_PRIVATE_SPLIT
3007 // Update the telemetry data.
3008 this.#recordTelemetryData();
3011 #getEngineInfo(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" }];
3020 const engineData = {
3021 loadPath: engine._loadPath,
3022 name: engine.name ? engine.name : "",
3025 if (engine.isAppProvided) {
3026 engineData.origin = "default";
3028 let currentHash = engine.getAttr("loadPathHash");
3030 engineData.origin = "unverified";
3032 let loadPathHash = lazy.SearchUtils.getVerificationHash(
3036 currentHash == loadPathHash ? "verified" : "invalid";
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) {
3051 if (innerEngine.searchUrlDomain == engineHost) {
3052 sendSubmissionURL = true;
3057 if (!sendSubmissionURL) {
3058 // ... or well known search domains.
3060 // Starts with: www.google., search.aol., yandex.
3062 // Ends with: search.yahoo.com, .ask.com, .bing.com, .startpage.com, baidu.com, duckduckgo.com
3064 /^(?:www\.google\.|search\.aol\.|yandex\.)|(?:search\.yahoo|\.ask|\.bing|\.startpage|\.baidu|duckduckgo)\.com$/;
3065 sendSubmissionURL = urlTest.test(engineHost);
3069 if (sendSubmissionURL) {
3070 let uri = engine.searchURLWithNoTerms;
3073 .setUserPass("") // Avoid reporting a username or password.
3075 engineData.submissionURL = uri.spec;
3078 return [engine.telemetryId, engineData];
3082 * Records an event for where the default engine is changed. This is
3083 * recorded to both Glean and Telemetry.
3085 * The Glean GIFFT functionality is not used here because we use longer
3086 * names in the extra arguments to the event.
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.
3097 #recordDefaultChangedEvent(
3101 changeSource = Ci.nsISearchService.CHANGE_REASON_UNKNOWN
3103 changeSource = REASON_CHANGE_MAP.get(changeSource) ?? "unknown";
3104 Services.telemetry.setEventRecordingEnabled("search", true);
3107 // If we are toggling the separate private browsing settings, we might not
3108 // have an engine to record.
3110 [telemetryId, engineInfo] = this.#getEngineInfo(newEngine);
3120 let submissionURL = engineInfo.submissionURL ?? "";
3121 Services.telemetry.recordEvent(
3124 isPrivate ? "change_private" : "change_default",
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),
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,
3150 Glean.searchEnginePrivate.changed.record(extraArgs);
3152 Glean.searchEngineDefault.changed.record(extraArgs);
3157 * Records the user's current default engine (normal and private) data to
3160 #recordTelemetryData() {
3161 let info = this.getDefaultEngineInfo();
3163 Glean.searchEngineDefault.engineId.set(info.defaultSearchEngine);
3164 Glean.searchEngineDefault.displayName.set(
3165 info.defaultSearchEngineData.name
3167 Glean.searchEngineDefault.loadPath.set(
3168 info.defaultSearchEngineData.loadPath
3170 Glean.searchEngineDefault.submissionUrl.set(
3171 info.defaultSearchEngineData.submissionURL ?? "blank:"
3173 Glean.searchEngineDefault.verified.set(info.defaultSearchEngineData.origin);
3175 Glean.searchEnginePrivate.engineId.set(
3176 info.defaultPrivateSearchEngine ?? ""
3179 if (info.defaultPrivateSearchEngineData) {
3180 Glean.searchEnginePrivate.displayName.set(
3181 info.defaultPrivateSearchEngineData.name
3183 Glean.searchEnginePrivate.loadPath.set(
3184 info.defaultPrivateSearchEngineData.loadPath
3186 Glean.searchEnginePrivate.submissionUrl.set(
3187 info.defaultPrivateSearchEngineData.submissionURL ?? "blank:"
3189 Glean.searchEnginePrivate.verified.set(
3190 info.defaultPrivateSearchEngineData.origin
3193 Glean.searchEnginePrivate.displayName.set("");
3194 Glean.searchEnginePrivate.loadPath.set("");
3195 Glean.searchEnginePrivate.submissionUrl.set("blank:");
3196 Glean.searchEnginePrivate.verified.set("");
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) {
3213 let urlParsingInfo = engine.getURLParsingInfo();
3214 if (!urlParsingInfo) {
3218 // Store the same object on each matching map key, as an optimization.
3219 let mapValueForEngine = {
3221 termsParameterName: urlParsingInfo.termsParameterName,
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) {
3232 keysOfAlternates.add(key);
3234 } else if (!isAlternate && keysOfAlternates.has(key)) {
3235 keysOfAlternates.delete(key);
3240 this.#parseSubmissionMap.set(key, mapValueForEngine);
3243 processDomain(urlParsingInfo.mainDomain, false);
3244 lazy.SearchStaticData.getAlternateDomains(
3245 urlParsingInfo.mainDomain
3246 ).forEach(d => processDomain(d, true));
3250 #nimbusSearchUpdatedFun = null;
3252 async #nimbusSearchUpdated() {
3253 this.#checkNimbusPrefs();
3254 Services.search.wrappedJSObject._maybeReloadEngines(
3255 Ci.nsISearchService.CHANGE_REASON_EXPERIMENT
3260 * Check the prefs are correctly updated for users enrolled in a Nimbus experiment.
3262 * @param {boolean} isStartup
3263 * Whether this function was called as part of the startup flow.
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.
3271 !lazy.NimbusFeatures.searchConfiguration.getVariable("experiment")
3275 let nimbusPrivateDefaultUIEnabled =
3276 lazy.NimbusFeatures.searchConfiguration.getVariable(
3277 "seperatePrivateDefaultUIEnabled"
3279 let nimbusPrivateDefaultUrlbarResultEnabled =
3280 lazy.NimbusFeatures.searchConfiguration.getVariable(
3281 "seperatePrivateDefaultUrlbarResultEnabled"
3284 let previousPrivateDefault = this.defaultPrivateEngine;
3285 let uiWasEnabled = this._separatePrivateDefaultEnabledPrefValue;
3287 this._separatePrivateDefaultEnabledPrefValue !=
3288 nimbusPrivateDefaultUIEnabled
3290 Services.prefs.setBoolPref(
3291 `${lazy.SearchUtils.BROWSER_SEARCH_PREF}separatePrivateDefault.ui.enabled`,
3292 nimbusPrivateDefaultUIEnabled
3294 let newPrivateDefault = this.defaultPrivateEngine;
3295 if (previousPrivateDefault != newPrivateDefault) {
3296 if (!uiWasEnabled) {
3297 this.#recordDefaultChangedEvent(
3301 Ci.nsISearchService.CHANGE_REASON_EXPERIMENT
3304 this.#recordDefaultChangedEvent(
3306 previousPrivateDefault,
3308 Ci.nsISearchService.CHANGE_REASON_EXPERIMENT
3314 this.separatePrivateDefaultUrlbarResultEnabled !=
3315 nimbusPrivateDefaultUrlbarResultEnabled
3317 Services.prefs.setBoolPref(
3318 `${lazy.SearchUtils.BROWSER_SEARCH_PREF}separatePrivateDefault.urlbarResult.enabled`,
3319 nimbusPrivateDefaultUrlbarResultEnabled
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.
3330 this.#observersAdded = true;
3332 this.#nimbusSearchUpdatedFun = this.#nimbusSearchUpdated.bind(this);
3333 lazy.NimbusFeatures.searchConfiguration.onUpdate(
3334 this.#nimbusSearchUpdatedFun
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",
3352 IOUtils.profileBeforeChange.addBlocker(
3353 "Search service: shutting down",
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
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
3364 if (!this.isInitialized) {
3365 lazy.logConsole.warn(
3366 "not saving settings on shutdown due to initializing."
3372 await this._settings.shutdown(shutdownState);
3374 // Ensure that error is reported and that it causes tests
3375 // to fail, otherwise ignore it.
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;
3391 if (this.#queuedIdle) {
3392 this.idleService.removeIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
3393 this.#queuedIdle = false;
3396 this._settings.removeObservers();
3398 lazy.NimbusFeatures.searchConfiguration.offUpdate(
3399 this.#nimbusSearchUpdatedFun
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);
3408 QueryInterface = ChromeUtils.generateQI([
3415 observe(engine, topic, verb) {
3417 case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED:
3419 case lazy.SearchUtils.MODIFIED_TYPE.LOADED:
3420 engine = engine.QueryInterface(Ci.nsISearchEngine);
3421 lazy.logConsole.debug(
3422 "observe: Done installation of ",
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.
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;
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"
3444 this._maybeReloadEngines(
3445 Ci.nsISearchService.CHANGE_REASON_CONFIG
3446 ).catch(console.error);
3450 case QUIT_APPLICATION_TOPIC:
3451 this._removeObservers();
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
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);
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);
3483 * Create an engine object from the search configuration details.
3485 * This method is prefixed with _ rather than # because it is
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.
3494 async _makeEngineFromConfig(config) {
3495 lazy.logConsole.debug("_makeEngineFromConfig:", config);
3497 "locale" in config.webExtension
3498 ? config.webExtension.locale
3499 : lazy.SearchUtils.DEFAULT_TAG;
3501 let engine = new lazy.AddonSearchEngine({
3502 isAppProvided: true,
3504 extensionID: config.webExtension.id,
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.
3522 #didSettingsMetaDataUpdate(metaData) {
3523 let metaDataProperties = [
3531 return metaDataProperties.some(p => {
3532 return metaData?.[p] !== this._settings.getMetaDataAttribute(p);
3537 * Shows an infobar to notify the user their default search engine has been
3538 * removed and replaced by a new default search engine.
3540 * This method is prefixed with _ rather than # because it is
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.
3549 _showRemovalOfSearchEngineNotificationBox(
3550 prevCurrentEngineName,
3551 newCurrentEngineName
3553 let win = Services.wm.getMostRecentBrowserWindow();
3554 win.BrowserSearch.removalOfSearchEngineNotificationBox(
3555 prevCurrentEngineName,
3556 newCurrentEngineName
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.
3565 #maybeStartOpenSearchUpdateTimer() {
3567 this.#openSearchUpdateTimerStarted ||
3568 !Services.prefs.getBoolPref(
3569 lazy.SearchUtils.BROWSER_SEARCH_PREF + "update",
3576 let engineWithUpdates = [...this._engines.values()].find(
3577 engine => engine instanceof lazy.OpenSearchEngine && engine._hasUpdates
3580 if (engineWithUpdates) {
3581 lazy.logConsole.debug("Engine with updates found, setting update timer");
3582 lazy.timerManager.registerTimer(
3583 OPENSEARCH_UPDATE_TIMER_TOPIC,
3585 OPENSEARCH_UPDATE_TIMER_INTERVAL,
3588 this.#openSearchUpdateTimerStarted = true;
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);
3601 engine = engine.wrappedJSObject;
3602 lazy.logConsole.debug("update called for", engine._name);
3604 !Services.prefs.getBoolPref(
3605 lazy.SearchUtils.BROWSER_SEARCH_PREF + "update",
3613 let testEngine = null;
3614 let updateURI = engine._updateURI;
3616 lazy.logConsole.debug("updating", engine.name, updateURI.spec);
3617 testEngine = new lazy.OpenSearchEngine();
3618 testEngine._engineToUpdate = engine;
3620 testEngine.install(updateURI);
3622 lazy.logConsole.error("Failed to update", engine.name, ex);
3625 lazy.logConsole.debug("invalid updateURI");
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);
3636 XPCOMUtils.defineLazyServiceGetter(
3637 SearchService.prototype,
3639 "@mozilla.org/widget/useridleservice;1",
3640 "nsIUserIdleService"
3644 * Handles getting and checking extensions against the allow list.
3646 class SearchDefaultOverrideAllowlistHandler {
3648 * @param {Function} listener
3649 * A listener for configuration update changes.
3651 constructor(listener) {
3652 this._remoteConfig = lazy.RemoteSettings(
3653 lazy.SearchUtils.SETTINGS_ALLOWLIST_KEY
3658 * Determines if a search engine extension can override a default one
3659 * according to the allow list.
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
3670 async canOverride(extension, appProvidedExtensionId) {
3671 const overrideTable = await this._getAllowlist();
3673 let entry = overrideTable.find(e => e.thirdPartyId == extension.id);
3678 if (appProvidedExtensionId != entry.overridesId) {
3682 let searchProvider =
3683 extension.manifest.chrome_settings_overrides.search_provider;
3685 return entry.urls.some(
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
3695 * Obtains the configuration from remote settings. This includes
3696 * verifying the signature of the record within the database.
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.
3701 * Note that this may cause a network check of the certificate, but that
3702 * should generally be quick.
3705 * An array of objects in the database, or an empty array if none
3706 * could be obtained.
3708 async _getAllowlist() {
3711 result = await this._remoteConfig.get();
3713 // Don't throw an error just log it, just continue with no data, and hopefully
3714 // a sync will fix things later on.
3717 lazy.logConsole.debug("Allow list is:", result);