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