Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  SearchService.sys.mjs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint no-shadow: error, mozilla/no-aArgs: error */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  AppProvidedSearchEngine:
    "resource://gre/modules/AppProvidedSearchEngine.sys.mjs",
  AddonSearchEngine: "resource://gre/modules/AddonSearchEngine.sys.mjs",
  IgnoreLists: "resource://gre/modules/IgnoreLists.sys.mjs",
  loadAndParseOpenSearchEngine:
    "resource://gre/modules/OpenSearchLoader.sys.mjs",
  OpenSearchEngine: "resource://gre/modules/OpenSearchEngine.sys.mjs",
  PolicySearchEngine: "resource://gre/modules/PolicySearchEngine.sys.mjs",
  Region: "resource://gre/modules/Region.sys.mjs",
  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
  SearchEngine: "resource://gre/modules/SearchEngine.sys.mjs",
  SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs",
  SearchSettings: "resource://gre/modules/SearchSettings.sys.mjs",
  SearchStaticData: "resource://gre/modules/SearchStaticData.sys.mjs",
  SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
  UserInstalledAppEngine:
    "resource://gre/modules/AppProvidedSearchEngine.sys.mjs",
  UserSearchEngine: "resource://gre/modules/UserSearchEngine.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
  return console.createInstance({
    prefix: "SearchService",
    maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
  });
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "timerManager",
  "@mozilla.org/updates/timer-manager;1",
  "nsIUpdateTimerManager"
);

/**
 * @typedef {import("SearchEngine.sys.mjs").SearchEngine} SearchEngine
 * @typedef {import("SearchEngineSelector.sys.mjs").RefinedConfig} RefinedConfig
 * @typedef {import("SearchEngineSelector.sys.mjs").SearchEngineSelector} SearchEngineSelector
 */

/**
 * A reference to the handler for the default override allowlist.
 *
 * @type {SearchDefaultOverrideAllowlistHandler}
 */
ChromeUtils.defineLazyGetter(lazy, "defaultOverrideAllowlist", () => {
  return new SearchDefaultOverrideAllowlistHandler();
});

const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
const QUIT_APPLICATION_TOPIC = "quit-application";

// The update timer for OpenSearch engines checks in once a day.
const OPENSEARCH_UPDATE_TIMER_TOPIC = "search-engine-update-timer";
const OPENSEARCH_UPDATE_TIMER_INTERVAL = 60 * 60 * 24;

// This is the amount of time we'll be idle for before applying any configuration
// changes.
const RECONFIG_IDLE_TIME_SEC = 5 * 60;

// The key for the metadata we store about whether to prompt users to
// install engines they are using.
const ENGINES_SEEN_KEY = "contextual-engines-seen";

// Value we store to indicate prompt should not be shown.
const DONT_SHOW_PROMPT = -1;

// Amount of times the engine has to be used before prompting.
const ENGINES_SEEN_FOR_PROMPT = 1;
/**
 * A reason that is used in the change of default search engine event telemetry.
 * These are mutally exclusive.
 */
const REASON_CHANGE_MAP = new Map([
  // The cause of the change is unknown.
  [Ci.nsISearchService.CHANGE_REASON_UNKNOWN, "unknown"],
  // The user changed the default search engine via the options in the
  // preferences UI.
  [Ci.nsISearchService.CHANGE_REASON_USER, "user"],
  // The change resulted from the user toggling the "Use this search engine in
  // Private Windows" option in the preferences UI.
  [Ci.nsISearchService.CHANGE_REASON_USER_PRIVATE_SPLIT, "user_private_split"],
  // The user changed the default via keys (cmd/ctrl-up/down) in the separate
  // search bar.
  [Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR, "user_searchbar"],
  // The user changed the default via context menu on the one-off buttons in the
  // separate search bar.
  [
    Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR_CONTEXT,
    "user_searchbar_context",
  ],
  // An add-on requested the change of default on install, which was either
  // accepted automatically or by the user.
  [Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL, "addon-install"],
  // An add-on was uninstalled, which caused the engine to be uninstalled.
  [Ci.nsISearchService.CHANGE_REASON_ADDON_UNINSTALL, "addon-uninstall"],
  // A configuration update caused a change of default.
  [Ci.nsISearchService.CHANGE_REASON_CONFIG, "config"],
  // A locale update caused a change of default.
  [Ci.nsISearchService.CHANGE_REASON_LOCALE, "locale"],
  // A region update caused a change of default.
  [Ci.nsISearchService.CHANGE_REASON_REGION, "region"],
  // Turning on/off an experiment caused a change of default.
  [Ci.nsISearchService.CHANGE_REASON_EXPERIMENT, "experiment"],
  // An enterprise policy caused a change of default.
  [Ci.nsISearchService.CHANGE_REASON_ENTERPRISE, "enterprise"],
  // The UI Tour caused a change of default.
  [Ci.nsISearchService.CHANGE_REASON_UITOUR, "uitour"],
  // The engine updated.
  [Ci.nsISearchService.CHANGE_REASON_ENGINE_UPDATE, "engine-update"],
  // When the private default UI is enabled (e.g. via toggling the preference
  // when an experiment is run).
  [
    Ci.nsISearchService.CHANGE_REASON_USER_PRIVATE_PREF_ENABLED,
    "user_private_pref_enabled",
  ],
]);

/**
 * The ParseSubmissionResult contains getter methods that return attributes
 * about the parsed submission url.
 *
 * @implements {nsISearchParseSubmissionResult}
 */
class ParseSubmissionResult {
  constructor(engine, terms, termsParameterName) {
    this.#engine = engine;
    this.#terms = terms;
    this.#termsParameterName = termsParameterName;
  }

  get engine() {
    return this.#engine;
  }

  get terms() {
    return this.#terms;
  }

  get termsParameterName() {
    return this.#termsParameterName;
  }

  /**
   * The search engine associated with the URL passed in to
   * nsISearchEngine::parseSubmissionURL, or null if the URL does not represent
   * a search submission.
   *
   * @type {nsISearchEngine|null}
   */
  #engine;

  /**
   * String containing the sought terms. This can be an empty string in case no
   * terms were specified or the URL does not represent a search submission.
   *
   * @type {string}
   */
  #terms;

  /**
   * The name of the query parameter used by `engine` for queries. E.g. "q".
   *
   * @type {string}
   */
  #termsParameterName;

  QueryInterface = ChromeUtils.generateQI(["nsISearchParseSubmissionResult"]);
}

const gEmptyParseSubmissionResult = Object.freeze(
  new ParseSubmissionResult(null, "", "")
);

/**
 * The search service handles loading and maintaining of search engines. It will
 * also work out the default lists for each locale/region.
 *
 * @implements {nsISearchService}
 */
export class SearchService {
  constructor() {
    // this._engines is prefixed with _ rather than # because it is called from
    // a test.
    this._engines = new Map();
    this._settings = new lazy.SearchSettings(this);

    this.#defineLazyPreferenceGetters();
  }

  classID = Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}");

  get defaultEngine() {
    this.#ensureInitialized();
    return this._getEngineDefault(false);
  }

  set defaultEngine(newEngine) {
    this.#ensureInitialized();
    this.#setEngineDefault(false, newEngine);
  }

  get defaultPrivateEngine() {
    this.#ensureInitialized();
    return this._getEngineDefault(this.#separatePrivateDefault);
  }

  set defaultPrivateEngine(newEngine) {
    this.#ensureInitialized();
    if (!this._separatePrivateDefaultPrefValue) {
      Services.prefs.setBoolPref(
        lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
        true
      );
    }
    this.#setEngineDefault(this.#separatePrivateDefault, newEngine);
  }

  async getDefault() {
    await this.init();
    return this.defaultEngine;
  }

  async setDefault(engine, changeSource) {
    await this.init();
    this.#setEngineDefault(false, engine, changeSource);
  }

  async getDefaultPrivate() {
    await this.init();
    return this.defaultPrivateEngine;
  }

  async setDefaultPrivate(engine, changeSource) {
    await this.init();
    if (!this._separatePrivateDefaultPrefValue) {
      Services.prefs.setBoolPref(
        lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
        true
      );
    }
    this.#setEngineDefault(this.#separatePrivateDefault, engine, changeSource);
  }

  /**
   * @returns {SearchEngine}
   *   The engine that is the default for this locale/region, ignoring any
   *   user changes to the default engine.
   */
  get appDefaultEngine() {
    return this.#appDefaultEngine();
  }

  /**
   * @returns {SearchEngine}
   *   The engine that is the default for this locale/region in private browsing
   *   mode, ignoring any user changes to the default engine.
   *   Note: if there is no default for this locale/region, then the non-private
   *   browsing engine will be returned.
   */
  get appPrivateDefaultEngine() {
    return this.#appDefaultEngine(this.#separatePrivateDefault);
  }

  /**
   * Determine whether initialization has been completed.
   *
   * Clients of the service can use this attribute to quickly determine whether
   * initialization is complete, and decide to trigger some immediate treatment,
   * to launch asynchronous initialization or to bailout.
   *
   * Note that this attribute does not indicate that initialization has
   * succeeded, use hasSuccessfullyInitialized() for that.
   *
   * @returns {boolean}
   *  |true | if the search service has finished its attempt to initialize and
   *          we have an outcome. It could have failed or succeeded during this
   *          process.
   *  |false| if initialization has not been triggered yet or initialization is
   *          still ongoing.
   */
  get isInitialized() {
    return (
      this.#initializationStatus == "success" ||
      this.#initializationStatus == "failed"
    );
  }

  /**
   * Determine whether initialization has been successfully completed.
   *
   * @returns {boolean}
   *  |true | if the search service has succesfully initialized.
   *  |false| if initialization has not been started yet, initialization is
   *          still ongoing or initializaiton has failed.
   */
  get hasSuccessfullyInitialized() {
    return this.#initializationStatus == "success";
  }

  /**
   * A promise that is resolved when initialization has finished. This does not
   * trigger initialization to begin.
   *
   * @returns {Promise}
   *   Resolved when initalization has successfully finished, and rejected if it
   *   has failed.
   */
  get promiseInitialized() {
    return this.#initDeferredPromise.promise;
  }

  getDefaultEngineInfo() {
    let [telemetryId, defaultSearchEngineData] = this.#getEngineInfo(
      this.defaultEngine
    );
    const result = {
      defaultSearchEngine: telemetryId,
      defaultSearchEngineData,
    };

    if (this.#separatePrivateDefault) {
      let [privateTelemetryId, defaultPrivateSearchEngineData] =
        this.#getEngineInfo(this.defaultPrivateEngine);
      result.defaultPrivateSearchEngine = privateTelemetryId;
      result.defaultPrivateSearchEngineData = defaultPrivateSearchEngineData;
    }

    return result;
  }

  /**
   * If possible, please call getEngineById() rather than getEngineByName()
   * because engines are stored as { id: object } in this._engine Map.
   *
   * Returns the engine associated with the name.
   *
   * @param {string} engineName
   *   The name of the engine.
   * @returns {SearchEngine}
   *   The associated engine if found, null otherwise.
   */
  getEngineByName(engineName) {
    this.#ensureInitialized();
    return this.#getEngineByName(engineName);
  }

  /**
   * Returns the engine associated with the name without initialization checks.
   *
   * @param {string} engineName
   *   The name of the engine.
   * @returns {SearchEngine}
   *   The associated engine if found, null otherwise.
   */
  #getEngineByName(engineName) {
    for (let engine of this._engines.values()) {
      if (engine.name == engineName) {
        return engine;
      }
    }

    return null;
  }

  /**
   * Returns the engine associated with the id.
   *
   * @param {string} engineId
   *   The id of the engine.
   * @returns {SearchEngine}
   *   The associated engine if found, null otherwise.
   */
  getEngineById(engineId) {
    this.#ensureInitialized();
    return this._engines.get(engineId) || null;
  }

  async getEngineByAlias(alias) {
    await this.init();
    for (var engine of this._engines.values()) {
      if (engine && engine.aliases.includes(alias)) {
        return engine;
      }
    }
    return null;
  }

  async getEngines() {
    await this.init();
    lazy.logConsole.debug("getEngines: getting all engines");
    return this.#sortedEngines;
  }

  async getVisibleEngines() {
    await this.init();
    lazy.logConsole.debug("getVisibleEngines: getting all visible engines");
    return this.#sortedVisibleEngines;
  }

  async getAppProvidedEngines() {
    await this.init();

    return lazy.SearchUtils.sortEnginesByDefaults({
      engines: this.#sortedEngines.filter(e => e.isAppProvided),
      appDefaultEngine: this.appDefaultEngine,
      appPrivateDefaultEngine: this.appPrivateDefaultEngine,
    });
  }

  async getEnginesByExtensionID(extensionID) {
    await this.init();
    return this.#getEnginesByExtensionID(extensionID);
  }

  async findContextualSearchEngineByHost(host) {
    await this.init();
    let settings = await this._settings.get();
    let config =
      await this.#engineSelector.findContextualSearchEngineByHost(host);
    if (config) {
      return new lazy.UserInstalledAppEngine({ config, settings });
    }
    return null;
  }

  async shouldShowInstallPrompt(engine) {
    let identifer = engine._loadPath;
    let seenEngines =
      this._settings.getMetaDataAttribute(ENGINES_SEEN_KEY) ?? {};

    if (!(identifer in seenEngines)) {
      seenEngines[identifer] = 1;
      this._settings.setMetaDataAttribute(ENGINES_SEEN_KEY, seenEngines);
      return false;
    }

    let value = seenEngines[identifer];
    if (value == DONT_SHOW_PROMPT) {
      return false;
    }

    if (value == ENGINES_SEEN_FOR_PROMPT) {
      seenEngines[identifer] = DONT_SHOW_PROMPT;
      this._settings.setMetaDataAttribute(ENGINES_SEEN_KEY, seenEngines);
      return true;
    }

    console.error(`Unexpected value ${value} in seenEngines`);
    return false;
  }

  /**
   * This function calls #init to start initialization when it has not been
   * started yet. Otherwise, it returns the pending promise.
   *
   * @returns {Promise}
   *   Returns the pending Promise when #init has started but not yet finished.
   *   | Resolved | when initialization has successfully finished.
   *   | Rejected | when initialization has failed.
   */
  async init() {
    if (["started", "success", "failed"].includes(this.#initializationStatus)) {
      return this.promiseInitialized;
    }
    this.#initializationStatus = "started";
    return this.#init();
  }

  /**
   * Runs background checks for the search service. This is called from
   * BrowserGlue and may be run once per session if the user is idle for
   * long enough.
   */
  async runBackgroundChecks() {
    await this.init();
    await this.#migrateLegacyEngines();
    await this.#checkWebExtensionEngines();
    await this.#addOpenSearchTelemetry();
    await this.#removeAppProvidedExtensions();
  }

  /**
   * Test only - reset SearchService data. Ideally this should be replaced
   */
  reset() {
    this.#initializationStatus = "not initialized";
    this.#initDeferredPromise = Promise.withResolvers();
    this.#startupExtensions = new Set();
    this._engines.clear();
    this._cachedSortedEngines = null;
    this.#currentEngine = null;
    this.#currentPrivateEngine = null;
    this._searchDefault = null;
    this.#searchPrivateDefault = null;
    this.#maybeReloadDebounce = false;
    this._settings._batchTask?.disarm();
    if (this.#engineSelector) {
      this.#engineSelector.reset();
      this.#engineSelector = null;
    }
  }

  // Test-only function to set SearchService initialization status
  forceInitializationStatusForTests(status) {
    this.#initializationStatus = status;
  }

  /**
   * Test only variable to indicate an error should occur during
   * search service initialization.
   *
   * @type {{type : string, message: string}}
   */
  errorToThrowInTest = { type: null, message: null };

  // Test-only function to reset just the engine selector so that it can
  // load a different configuration.
  resetEngineSelector() {
    this.#engineSelector = new lazy.SearchEngineSelector(
      this.#handleConfigurationUpdated.bind(this)
    );
  }

  resetToAppDefaultEngine() {
    let appDefaultEngine = this.appDefaultEngine;
    appDefaultEngine.hidden = false;
    this.defaultEngine = appDefaultEngine;

    let appPrivateDefaultEngine = this.appPrivateDefaultEngine;
    appPrivateDefaultEngine.hidden = false;
    this.defaultPrivateEngine = appPrivateDefaultEngine;
  }

  async maybeSetAndOverrideDefault(extension) {
    let searchProvider =
      extension.manifest.chrome_settings_overrides.search_provider;
    let engine = this.getEngineByName(searchProvider.name);
    if (!engine || !engine.isAppProvided || engine.hidden) {
      // If the engine is not application provided, then we shouldn't simply
      // set default to it.
      // If the engine is application provided, but hidden, then we don't
      // switch to it, nor do we try to install it.
      return {
        canChangeToAppProvided: false,
        canInstallEngine: !engine?.hidden,
      };
    }

    if (
      extension.startupReason === "ADDON_INSTALL" ||
      extension.startupReason === "ADDON_ENABLE"
    ) {
      // Don't allow an extension to set the default if it is already the default.
      if (this.defaultEngine.name == searchProvider.name) {
        return {
          canChangeToAppProvided: false,
          canInstallEngine: false,
        };
      }
      if (
        !(await lazy.defaultOverrideAllowlist.canOverride(extension, engine.id))
      ) {
        lazy.logConsole.debug(
          "Allowing default engine to be set to app-provided.",
          extension.id
        );
        // We don't allow overriding the engine in this case, but we can allow
        // the extension to change the default engine.
        return {
          canChangeToAppProvided: true,
          canInstallEngine: false,
        };
      }
      // We're ok to override.
      engine.overrideWithEngine({ extension });
      lazy.logConsole.debug(
        "Allowing default engine to be set to app-provided and overridden.",
        extension.id
      );
      return {
        canChangeToAppProvided: true,
        canInstallEngine: false,
      };
    }

    if (
      engine.getAttr("overriddenBy") == extension.id &&
      (await lazy.defaultOverrideAllowlist.canOverride(extension, engine.id))
    ) {
      engine.overrideWithEngine({ extension });
      lazy.logConsole.debug(
        "Re-enabling overriding of core extension by",
        extension.id
      );
      return {
        canChangeToAppProvided: true,
        canInstallEngine: false,
      };
    }

    return {
      canChangeToAppProvided: false,
      canInstallEngine: false,
    };
  }

  /**
   * Adds a search engine that is specified from enterprise policies.
   *
   * @param {object} details
   *   An object that matches the `SearchEngines` policy schema.
   * @param {object} [settings]
   *   The saved settings for the user.
   * @see browser/components/enterprisepolicies/schemas/policies-schema.json
   */
  async #addPolicyEngine(details, settings) {
    let newEngine = new lazy.PolicySearchEngine({ details, settings });
    lazy.logConsole.debug("Adding Policy Engine:", newEngine.name);
    this.#addEngineToStore(newEngine);
  }

  /**
   * Adds a search engine that is specified by the user.
   *
   * @param {string} name
   *   The name of the search engine
   * @param {string} url
   *   The url that the search engine uses for searches
   * @param {string} alias
   *   An alias for the search engine
   */
  async addUserEngine(name, url, alias) {
    await this.init();

    let newEngine = new lazy.UserSearchEngine({
      details: { name, url, alias },
    });
    lazy.logConsole.debug(`Adding ${newEngine.name}`);
    this.#addEngineToStore(newEngine);
  }

  async addSearchEngine(engine) {
    await this.init();
    this.#addEngineToStore(engine);
  }

  /**
   * Called from the AddonManager when it either installs a new
   * extension containing a search engine definition or an upgrade
   * to an existing one.
   *
   * @param {object} extension
   *   An Extension object containing data about the extension.
   */
  async addEnginesFromExtension(extension) {
    // Treat add-on upgrade and downgrades the same - either way, the search
    // engine gets updated, not added. Generally, we don't expect a downgrade,
    // but just in case...
    if (
      extension.startupReason == "ADDON_UPGRADE" ||
      extension.startupReason == "ADDON_DOWNGRADE"
    ) {
      // Bug 1679861 An a upgrade or downgrade could be adding a search engine
      // that was not in a prior version, or the addon may have been blocklisted.
      // In either case, there will not be an existing engine.
      let existing = await this.#upgradeExtensionEngine(extension);
      if (existing?.length) {
        return;
      }
    }

    if (extension.isAppProvided) {
      this.#extensionsToRemove.add(extension.id);
      lazy.logConsole.debug(
        "addEnginesFromExtension: Queuing old app provided WebExtension for uninstall",
        extension.id
      );
      return;
    }
    lazy.logConsole.debug("addEnginesFromExtension:", extension.id);

    // If we haven't started the SearchService yet, store this extension
    // to install in SearchService.init().
    if (!this.isInitialized) {
      this.#startupExtensions.add(extension);
      return;
    }

    await this.#createAndAddAddonEngine({
      extension,
    });
  }

  async addOpenSearchEngine(engineURL, iconURL) {
    lazy.logConsole.debug("addOpenSearchEngine: Adding", engineURL);
    await this.init();
    let engine;
    try {
      let engineData = await lazy.loadAndParseOpenSearchEngine(
        Services.io.newURI(engineURL)
      );
      engine = new lazy.OpenSearchEngine({ engineData, faviconURL: iconURL });
    } catch (ex) {
      throw Components.Exception(
        "addEngine: Error adding engine:\n" + ex,
        ex.result || Cr.NS_ERROR_FAILURE
      );
    }
    this.#addEngineToStore(engine);
    this.#maybeStartOpenSearchUpdateTimer();
    return engine;
  }

  async removeWebExtensionEngine(id) {
    if (!this.isInitialized) {
      lazy.logConsole.debug(
        "Delaying removing extension engine on startup:",
        id
      );
      this.#startupRemovedExtensions.add(id);
      return;
    }

    lazy.logConsole.debug("removeWebExtensionEngine:", id);
    for (let engine of this.#getEnginesByExtensionID(id)) {
      await this.removeEngine(engine);
    }
  }

  async removeEngine(engine) {
    await this.init();
    if (!engine) {
      throw Components.Exception(
        "no engine passed to removeEngine!",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    var engineToRemove = null;
    for (var e of this._engines.values()) {
      if (engine.wrappedJSObject == e) {
        engineToRemove = e;
      }
    }

    if (!engineToRemove) {
      throw Components.Exception(
        "removeEngine: Can't find engine to remove!",
        Cr.NS_ERROR_FILE_NOT_FOUND
      );
    }

    engineToRemove.pendingRemoval = true;

    if (engineToRemove == this.defaultEngine) {
      this.#findAndSetNewDefaultEngine({
        privateMode: false,
      });
    }

    // Bug 1575649 - We can't just check the default private engine here when
    // we're not using separate, as that re-checks the normal default, and
    // triggers update of the default search engine, which messes up various
    // tests. Really, removeEngine should always commit to updating any
    // changed defaults.
    if (
      this.#separatePrivateDefault &&
      engineToRemove == this.defaultPrivateEngine
    ) {
      this.#findAndSetNewDefaultEngine({
        privateMode: true,
      });
    }

    if (engineToRemove.inMemory) {
      // Just hide it (the "hidden" setter will notify) and remove its alias to
      // avoid future conflicts with other engines.
      engineToRemove.hidden = true;
      engineToRemove.alias = null;
      engineToRemove.pendingRemoval = false;
    } else {
      // Remove the engine file from disk if we had a legacy file in the profile.
      if (engineToRemove._filePath) {
        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
        file.persistentDescriptor = engineToRemove._filePath;
        if (file.exists()) {
          file.remove(false);
        }
        engineToRemove._filePath = null;
      }
      this.#internalRemoveEngine(engineToRemove);

      // Since we removed an engine, we may need to update the preferences.
      if (!this.#dontSetUseSavedOrder) {
        this.#saveSortedEngineList();
      }
    }
    lazy.SearchUtils.notifyAction(
      engineToRemove,
      lazy.SearchUtils.MODIFIED_TYPE.REMOVED
    );
  }

  async moveEngine(engine, newIndex) {
    await this.init();
    if (newIndex > this.#sortedEngines.length || newIndex < 0) {
      throw Components.Exception(
        "moveEngine: Index out of bounds!",
        Cr.NS_ERROR_INVALID_ARG
      );
    }
    if (
      !(engine instanceof Ci.nsISearchEngine) &&
      !(engine instanceof lazy.SearchEngine)
    ) {
      throw Components.Exception(
        "moveEngine: Invalid engine passed to moveEngine!",
        Cr.NS_ERROR_INVALID_ARG
      );
    }
    if (engine.hidden) {
      throw Components.Exception(
        "moveEngine: Can't move a hidden engine!",
        Cr.NS_ERROR_FAILURE
      );
    }

    engine = engine.wrappedJSObject;

    var currentIndex = this.#sortedEngines.indexOf(engine);
    if (currentIndex == -1) {
      throw Components.Exception(
        "moveEngine: Can't find engine to move!",
        Cr.NS_ERROR_UNEXPECTED
      );
    }

    // Our callers only take into account non-hidden engines when calculating
    // newIndex, but we need to move it in the array of all engines, so we
    // need to adjust newIndex accordingly. To do this, we count the number
    // of hidden engines in the list before the engine that we're taking the
    // place of. We do this by first finding newIndexEngine (the engine that
    // we were supposed to replace) and then iterating through the complete
    // engine list until we reach it, increasing newIndex for each hidden
    // engine we find on our way there.
    //
    // This could be further simplified by having our caller pass in
    // newIndexEngine directly instead of newIndex.
    var newIndexEngine = this.#sortedVisibleEngines[newIndex];
    if (!newIndexEngine) {
      throw Components.Exception(
        "moveEngine: Can't find engine to replace!",
        Cr.NS_ERROR_UNEXPECTED
      );
    }

    for (var i = 0; i < this.#sortedEngines.length; ++i) {
      if (newIndexEngine == this.#sortedEngines[i]) {
        break;
      }
      if (this.#sortedEngines[i].hidden) {
        newIndex++;
      }
    }

    if (currentIndex == newIndex) {
      return;
    } // nothing to do!

    // Move the engine
    var movedEngine = this._cachedSortedEngines.splice(currentIndex, 1)[0];
    this._cachedSortedEngines.splice(newIndex, 0, movedEngine);

    lazy.SearchUtils.notifyAction(
      engine,
      lazy.SearchUtils.MODIFIED_TYPE.CHANGED
    );

    // Since we moved an engine, we need to update the preferences.
    this.#saveSortedEngineList();
  }

  restoreDefaultEngines() {
    this.#ensureInitialized();
    for (let e of this._engines.values()) {
      // Unhide all default engines
      if (e.hidden && e.isAppProvided) {
        e.hidden = false;
      }
    }
  }

  parseSubmissionURL(url) {
    if (!this.hasSuccessfullyInitialized) {
      // If search is not initialized or failed initializing, do nothing.
      // This allows us to use this function early in telemetry.
      // The only other consumer of this (places) uses it much later.
      return gEmptyParseSubmissionResult;
    }

    if (!this.#parseSubmissionMap) {
      this.#buildParseSubmissionMap();
    }

    // Extract the elements of the provided URL first.
    let soughtKey, soughtQuery;
    try {
      let soughtUrl = Services.io.newURI(url);

      // Exclude any URL that is not HTTP or HTTPS from the beginning.
      if (!soughtUrl.schemeIs("http") && !soughtUrl.schemeIs("https")) {
        return gEmptyParseSubmissionResult;
      }

      // Reading these URL properties may fail and raise an exception.
      soughtKey = soughtUrl.host + soughtUrl.filePath.toLowerCase();
      soughtQuery = soughtUrl.query;
    } catch (ex) {
      // Errors while parsing the URL or accessing the properties are not fatal.
      return gEmptyParseSubmissionResult;
    }

    // Look up the domain and path in the map to identify the search engine.
    let mapEntry = this.#parseSubmissionMap.get(soughtKey);
    if (!mapEntry) {
      return gEmptyParseSubmissionResult;
    }

    // Extract the search terms from the parameter, for example "caff%C3%A8"
    // from the URL "https://www.google.com/search?q=caff%C3%A8&client=firefox".
    // We cannot use `URLSearchParams` here as the terms might not be
    // encoded in UTF-8.
    let encodedTerms = null;
    for (let param of soughtQuery.split("&")) {
      let equalPos = param.indexOf("=");
      if (
        equalPos != -1 &&
        param.substr(0, equalPos) == mapEntry.termsParameterName
      ) {
        // This is the parameter we are looking for.
        encodedTerms = param.substr(equalPos + 1);
        break;
      }
    }
    if (encodedTerms === null) {
      return gEmptyParseSubmissionResult;
    }

    // Decode the terms using the charset defined in the search engine.
    let terms;
    try {
      terms = Services.textToSubURI.UnEscapeAndConvert(
        mapEntry.engine.queryCharset,
        encodedTerms.replace(/\+/g, " ")
      );
    } catch (ex) {
      // Decoding errors will cause this match to be ignored.
      return gEmptyParseSubmissionResult;
    }

    return new ParseSubmissionResult(
      mapEntry.engine,
      terms,
      mapEntry.termsParameterName
    );
  }

  getAlternateDomains(domain) {
    return lazy.SearchStaticData.getAlternateDomains(domain);
  }

  /**
   * This is a nsITimerCallback for the timerManager notification that is
   * registered for handling updates to search engines. Only OpenSearch engines
   * have these updates and hence, only those are handled here.
   */
  async notify() {
    lazy.logConsole.debug("notify: checking for updates");

    // Walk the engine list, looking for engines whose update time has expired.
    for (let engine of this._engines.values()) {
      if (!(engine instanceof lazy.OpenSearchEngine)) {
        continue;
      }
      await engine.maybeUpdate();
    }
  }

  #currentEngine;
  #currentPrivateEngine;
  #queuedIdle;

  /**
   * A deferred promise that is resolved when initialization has finished.
   *
   * Resolved when initalization has successfully finished, and rejected if it
   * has failed.
   *
   * @type {Promise}
   */
  #initDeferredPromise = Promise.withResolvers();

  /**
   * Indicates if initialization has started, failed, succeeded or has not
   * started yet.
   *
   * These are the statuses:
   *   "not initialized" - The SearchService has not started initialization.
   *   "started" - The SearchService has started initializaiton.
   *   "success" - The SearchService successfully completed initialization.
   *   "failed" - The SearchService failed during initialization.
   *
   * @type {string}
   */
  #initializationStatus = "not initialized";

  /**
   * Indicates if we're already waiting for maybeReloadEngines to be called.
   *
   * @type {boolean}
   */
  #maybeReloadDebounce = false;

  /**
   * Indicates if we're currently in maybeReloadEngines.
   *
   * This is prefixed with _ rather than # because it is
   * called in a test.
   *
   * @type {boolean}
   */
  _reloadingEngines = false;

  /**
   * The engine selector singleton that is managing the engine configuration.
   *
   * @type {SearchEngineSelector|null}
   */
  #engineSelector = null;

  /**
   * Various search engines may be ignored if their submission urls contain a
   * string that is in the list. The list is controlled via remote settings.
   *
   * @type {Array}
   */
  #submissionURLIgnoreList = [];

  /**
   * Various search engines may be ignored if their load path is contained
   * in this list. The list is controlled via remote settings.
   *
   * @type {Array}
   */
  #loadPathIgnoreList = [];

  /**
   * A map of engine identifiers to `SearchEngine`.
   *
   * @type {Map<string, object>|null}
   */
  _engines = null;

  /**
   * An array of engine short names sorted into display order.
   *
   * @type {Array}
   */
  _cachedSortedEngines = null;

  /**
   * A flag to prevent setting of useSavedOrder when there's non-user
   * activity happening.
   *
   * @type {boolean}
   */
  #dontSetUseSavedOrder = false;

  /**
   * An object containing the id of the AppProvidedSearchEngine for the default
   * engine, as suggested by the configuration.
   *
   * This is prefixed with _ rather than # because it is
   * called in a test.
   *
   * @type {object}
   */
  _searchDefault = null;

  /**
   * An object containing the id of the AppProvidedSearchEngine for the default
   * engine for private browsing mode, as suggested by the configuration.
   *
   * @type {object}
   */
  #searchPrivateDefault = null;

  /**
   * A Set of installed search extensions reported by AddonManager
   * startup before SearchSevice has started. Will be installed
   * during init(). Does not contain application provided engines.
   *
   * @type {Set<object>}
   */
  #startupExtensions = new Set();

  /**
   * A Set of installed app provided search Web Extensions to be uninstalled by
   * the AddonManager on idle. We no longer have app provided engines as
   * web extensions after search-config-v2 enabled in Firefox version 128.
   *
   * @type {Set<object>}
   */
  #extensionsToRemove = new Set();

  /**
   * A Set of removed search extensions reported by AddonManager
   * startup before SearchSevice has started. Will be removed
   * during init().
   *
   * @type {Set<object>}
   */
  #startupRemovedExtensions = new Set();

  /**
   * Used in #parseSubmissionMap
   *
   * @typedef {object} submissionMapEntry
   * @property {nsISearchEngine} engine
   *   The search engine.
   * @property {string} termsParameterName
   *   The search term parameter name.
   */

  /**
   * This map is built lazily after the available search engines change.  It
   * allows quick parsing of an URL representing a search submission into the
   * search engine name and original terms.
   *
   * The keys are strings containing the domain name and lowercase path of the
   * engine submission, for example "www.google.com/search".
   *
   * @type {Map<string, submissionMapEntry>|null}
   */
  #parseSubmissionMap = null;

  /**
   * Keep track of observers have been added.
   *
   * @type {boolean}
   */
  #observersAdded = false;

  /**
   * Keeps track to see if the OpenSearch update timer has been started or not.
   *
   * @type {boolean}
   */
  #openSearchUpdateTimerStarted = false;

  get #sortedEngines() {
    if (!this._cachedSortedEngines) {
      return this.#buildSortedEngineList();
    }
    return this._cachedSortedEngines;
  }
  /**
   * This reflects the combined values of the prefs for enabling the separate
   * private default UI, and for the user choosing a separate private engine.
   * If either one is disabled, then we don't enable the separate private default.
   *
   * @returns {boolean}
   */
  get #separatePrivateDefault() {
    return (
      this._separatePrivateDefaultPrefValue &&
      this._separatePrivateDefaultEnabledPrefValue
    );
  }

  #getEnginesByExtensionID(extensionID) {
    lazy.logConsole.debug(
      "getEnginesByExtensionID: getting all engines for",
      extensionID
    );
    var engines = this.#sortedEngines.filter(function (engine) {
      return engine._extensionID == extensionID;
    });
    return engines;
  }

  /**
   * Returns the engine associated with the WebExtension details.
   *
   * @param {object} details
   *   Details of the WebExtension.
   * @param {string} details.id
   *   The WebExtension ID
   * @param {string} details.locale
   *   The WebExtension locale
   * @returns {nsISearchEngine|null}
   *   The found engine, or null if no engine matched.
   */
  #getEngineByWebExtensionDetails(details) {
    for (const engine of this._engines.values()) {
      if (engine._extensionID == details.id) {
        return engine;
      }
    }
    return null;
  }

  /**
   * Helper function to get the current default engine.
   *
   * This is prefixed with _ rather than # because it is
   * called in test_remove_engine_notification_box.js
   *
   * @param {boolean} privateMode
   *   If true, returns the default engine for private browsing mode, otherwise
   *   the default engine for the normal mode. Note, this function does not
   *   check the "separatePrivateDefault" preference - that is up to the caller.
   * @returns {nsISearchEngine|null}
   *   The appropriate search engine, or null if one could not be determined.
   */
  _getEngineDefault(privateMode) {
    let currentEngine = privateMode
      ? this.#currentPrivateEngine
      : this.#currentEngine;

    if (currentEngine && !currentEngine.hidden) {
      return currentEngine;
    }

    // No default loaded, so find it from settings.
    const attributeName = privateMode
      ? "privateDefaultEngineId"
      : "defaultEngineId";

    let engineId = this._settings.getMetaDataAttribute(attributeName);
    let engine = this._engines.get(engineId) || null;
    if (
      engine &&
      this._settings.getVerifiedMetaDataAttribute(
        attributeName,
        engine.isAppProvided
      )
    ) {
      if (privateMode) {
        this.#currentPrivateEngine = engine;
      } else {
        this.#currentEngine = engine;
      }
    }
    if (!engineId) {
      if (privateMode) {
        this.#currentPrivateEngine = this.appPrivateDefaultEngine;
      } else {
        this.#currentEngine = this.appDefaultEngine;
      }
    }

    currentEngine = privateMode
      ? this.#currentPrivateEngine
      : this.#currentEngine;
    if (currentEngine && !currentEngine.hidden) {
      return currentEngine;
    }
    // No default in settings or it is hidden, so find the new default.
    return this.#findAndSetNewDefaultEngine({ privateMode });
  }

  /**
   * If initialization has not been completed yet, perform synchronous
   * initialization.
   * Throws in case of initialization error.
   */
  #ensureInitialized() {
    if (this.#initializationStatus === "success") {
      return;
    }

    if (this.#initializationStatus === "failed") {
      throw new Error("SearchService failed while it was initializing.");
    }

    let err = new Error(
      "Something tried to use the search service before it finished " +
        "initializing. Please examine the stack trace to figure out what and " +
        "where to fix it:\n"
    );
    err.message += err.stack;
    throw err;
  }

  /**
   * Define lazy preference getters for separate private default engine in
   * private browsing mode.
   */
  #defineLazyPreferenceGetters() {
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "_separatePrivateDefaultPrefValue",
      lazy.SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault",
      false,
      this.#onSeparateDefaultPrefChanged.bind(this)
    );

    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "_separatePrivateDefaultEnabledPrefValue",
      lazy.SearchUtils.BROWSER_SEARCH_PREF +
        "separatePrivateDefault.ui.enabled",
      false,
      this.#onSeparateDefaultPrefChanged.bind(this)
    );

    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "separatePrivateDefaultUrlbarResultEnabled",
      lazy.SearchUtils.BROWSER_SEARCH_PREF +
        "separatePrivateDefault.urlbarResult.enabled",
      false
      // No need to reload engines, as this only affects the Urlbar result list.
    );

    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "_experimentPrefValue",
      lazy.SearchUtils.BROWSER_SEARCH_PREF + "experiment",
      "",
      () => {
        Services.search.wrappedJSObject._maybeReloadEngines(
          Ci.nsISearchService.CHANGE_REASON_EXPERIMENT
        );
      }
    );
  }

  /**
   * This function adds observers, retrieves the search engine ignore list, and
   * initializes the Search Engine Selector prior to doing the core tasks of
   * search service initialization.
   *
   */
  #doPreInitWork() {
    // We need to catch the region being updated during initialization so we
    // start listening straight away.
    Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);

    this.#getIgnoreListAndSubscribe().catch(ex =>
      console.error(ex, "Search Service could not get the ignore list.")
    );

    this.#engineSelector = new lazy.SearchEngineSelector(
      this.#handleConfigurationUpdated.bind(this)
    );
  }

  /**
   * This function fetches information to load search engines and ensures the
   * search service is in the correct state for external callers to interact
   * with it.
   *
   * This function sets #initDeferredPromise to resolve or reject.
   *   | Resolved | when initalization has successfully finished.
   *   | Rejected | when initialization has failed.
   */
  async #init() {
    lazy.logConsole.debug("init");

    const timerId = Glean.searchService.startupTime.start();

    this.#doPreInitWork();

    let initSection;
    try {
      initSection = "Settings";
      this.#maybeThrowErrorInTest(initSection);
      const settings = await this._settings.get();

      initSection = "FetchEngines";
      this.#maybeThrowErrorInTest(initSection);
      const refinedConfig = await this._fetchEngineSelectorEngines();

      initSection = "LoadEngines";
      this.#maybeThrowErrorInTest(initSection);
      this.#maybeThrowErrorInTest("LoadSettingsAddonManager");
      await this.#loadEngines(settings, refinedConfig);
    } catch (ex) {
      if (ex.message.startsWith("Addon manager")) {
        if (
          !Services.startup.shuttingDown &&
          ex.message != "Addon manager shutting down"
        ) {
          Glean.searchService.initializationStatus.failedLoadSettingsAddonManager.add();
        }
      } else {
        Glean.searchService.initializationStatus[`failed${initSection}`].add();
      }
      Glean.searchService.startupTime.cancel(timerId);

      lazy.logConsole.error("#init: failure initializing search:", ex);
      this.#initializationStatus = "failed";
      this.#initDeferredPromise.reject(ex);

      throw ex;
    }

    // If we've got this far, but the application is now shutting down,
    // then we need to abandon any further work, especially not writing
    // the settings. We do this, because the add-on manager has also
    // started shutting down and as a result, we might have an incomplete
    // picture of the installed search engines. Writing the settings at
    // this stage would potentially mean the user would loose their engine
    // data.
    // We will however, rebuild the settings on next start up if we detect
    // it is necessary.
    if (Services.startup.shuttingDown) {
      Glean.searchService.startupTime.cancel(timerId);

      let ex = Components.Exception(
        "#init: abandoning init due to shutting down",
        Cr.NS_ERROR_ABORT
      );

      this.#initializationStatus = "failed";
      this.#initDeferredPromise.reject(ex);
      throw ex;
    }

    this.#initializationStatus = "success";
    Glean.searchService.initializationStatus.success.add();
    this.#initDeferredPromise.resolve();
    this.#addObservers();

    Glean.searchService.startupTime.stopAndAccumulate(timerId);

    this.#recordTelemetryData();

    Services.obs.notifyObservers(
      null,
      lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
      "init-complete"
    );

    lazy.logConsole.debug("Completed #init");
    this.#doPostInitWork();
  }

  /**
   * This function records telemetry, checks experiment updates, sets up a timer
   * for opensearch, removes any necessary Add-on engines immediately after the
   * search service has successfully initialized.
   *
   */
  #doPostInitWork() {
    this.#maybeStartOpenSearchUpdateTimer();

    if (this.#startupRemovedExtensions.size) {
      Services.tm.dispatchToMainThread(async () => {
        // Now that init() has successfully finished, we remove any engines
        // that have had their add-ons removed by the add-on manager.
        // We do this after init() has complete, as that allows us to use
        // removeEngine to look after any default engine changes as well.
        // This could cause a slight flicker on startup, but it should be
        // a rare action.
        lazy.logConsole.debug("Removing delayed extension engines");
        for (let id of this.#startupRemovedExtensions) {
          for (let engine of this.#getEnginesByExtensionID(id)) {
            // Only do this for non-application provided engines. We shouldn't
            // ever get application provided engines removed here, but just in case.
            if (!engine.isAppProvided) {
              await this.removeEngine(engine);
            }
          }
        }
        this.#startupRemovedExtensions.clear();
      });
    }
  }

  /**
   * Obtains the ignore list from remote settings. This should only be
   * called from init(). Any subsequent updates to the remote settings are
   * handled via a sync listener.
   *
   */
  async #getIgnoreListAndSubscribe() {
    let listener = this.#handleIgnoreListUpdated.bind(this);
    const current = await lazy.IgnoreLists.getAndSubscribe(listener);

    // Only save the listener after the subscribe, otherwise for tests it might
    // not be fully set up by the time we remove it again.
    this.ignoreListListener = listener;

    await this.#handleIgnoreListUpdated({ data: { current } });
    Services.obs.notifyObservers(
      null,
      lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
      "settings-update-complete"
    );
  }

  /**
   * This handles updating of the ignore list settings, and removing any ignored
   * engines.
   *
   * @param {object} eventData
   *   The event in the format received from RemoteSettings.
   */
  async #handleIgnoreListUpdated(eventData) {
    lazy.logConsole.debug("#handleIgnoreListUpdated");
    const {
      data: { current },
    } = eventData;

    for (const entry of current) {
      if (entry.id == "load-paths") {
        this.#loadPathIgnoreList = [...entry.matches];
      } else if (entry.id == "submission-urls") {
        this.#submissionURLIgnoreList = [...entry.matches];
      }
    }

    try {
      await this.promiseInitialized;
    } catch (ex) {
      // If there's a problem with initialization return early to allow
      // search service to continue in a limited mode without engines.
      return;
    }

    // We try to remove engines manually, as this should be more efficient and
    // we don't really want to cause a re-init as this upsets unit tests.
    let engineRemoved = false;
    for (let engine of this._engines.values()) {
      if (this.#engineMatchesIgnoreLists(engine)) {
        await this.removeEngine(engine);
        engineRemoved = true;
      }
    }
    // If we've removed an engine, and we don't have any left, we need to
    // reload the engines - it is possible the settings just had one engine in it,
    // and that is now empty, so we need to load from our main list.
    if (engineRemoved && !this._engines.size) {
      this._maybeReloadEngines().catch(console.error);
    }
  }

  /**
   * Determines if a given engine matches the ignorelists or not.
   *
   * @param {SearchEngine} engine
   *   The engine to check against the ignorelists.
   * @returns {boolean}
   *   Returns true if the engine matches a ignorelists entry.
   */
  #engineMatchesIgnoreLists(engine) {
    if (this.#loadPathIgnoreList.includes(engine._loadPath)) {
      return true;
    }
    let url = engine.searchURLWithNoTerms.spec.toLowerCase();
    if (
      this.#submissionURLIgnoreList.some(code =>
        url.includes(code.toLowerCase())
      )
    ) {
      return true;
    }
    return false;
  }

  /**
   * Handles the search configuration being - adds a wait on the user
   * being idle, before the search engine update gets handled.
   */
  #handleConfigurationUpdated() {
    if (this.#queuedIdle) {
      return;
    }

    this.#queuedIdle = true;

    this.idleService.addIdleObserver(this, RECONFIG_IDLE_TIME_SEC);
  }

  /**
   * Returns the engine that is the default for this locale/region, ignoring any
   * user changes to the default engine.
   *
   * @param {boolean} privateMode
   *   Set to true to return the default engine in private mode,
   *   false for normal mode.
   * @returns {SearchEngine}
   *   The engine that is default.
   */
  #appDefaultEngine(privateMode = false) {
    let defaultEngine = this._engines.get(
      privateMode && this.#searchPrivateDefault
        ? this.#searchPrivateDefault
        : this._searchDefault
    );

    if (Services.policies?.status == Ci.nsIEnterprisePolicies.ACTIVE) {
      let activePolicies = Services.policies.getActivePolicies();
      if (activePolicies.SearchEngines) {
        let policyDefault =
          privateMode &&
          this.#separatePrivateDefault &&
          activePolicies.SearchEngines.DefaultPrivate
            ? activePolicies.SearchEngines.DefaultPrivate
            : activePolicies.SearchEngines.Default;
        if (policyDefault) {
          let policyEngine = this.#getEngineByName(policyDefault);
          if (policyEngine) {
            return policyEngine;
          }
        }
        if (activePolicies.SearchEngines.Remove?.includes(defaultEngine.name)) {
          defaultEngine = null;
        }
      }
    }

    if (defaultEngine) {
      return defaultEngine;
    }

    if (privateMode) {
      // If for some reason we can't find the private mode engine, fall back
      // to the non-private one.
      return this.#appDefaultEngine(false);
    }

    // Something unexpected has happened. In order to recover the app default
    // engine, use the first visible engine that is also a general purpose engine.
    // Worst case, we just use the first visible engine.
    defaultEngine = this.#sortedVisibleEngines.find(
      e => e.isGeneralPurposeEngine
    );
    return defaultEngine ? defaultEngine : this.#sortedVisibleEngines[0];
  }

  /**
   * Loads engines asynchronously.
   *
   * @param {object} settings
   *   An object representing the search engine settings.
   * @param {RefinedConfig} refinedConfig
   *   The refined search configuration for this user.
   */
  async #loadEngines(settings, refinedConfig) {
    // Get user's current settings and search engine before we load engines from
    // config. These values will be compared after engines are loaded.
    let prevMetaData = { ...settings?.metaData };
    let prevCurrentEngineId = prevMetaData.defaultEngineId;
    let prevAppDefaultEngineId = prevMetaData?.appDefaultEngineId;

    lazy.logConsole.debug("#loadEngines: start");
    this.#setDefaultFromSelector(refinedConfig);

    this.#loadEnginesFromConfig(refinedConfig.engines, settings);

    await this.#loadStartupEngines(settings);

    this.#loadEnginesFromPolicies(settings);

    // `loadEnginesFromSettings` loads the engines and their settings together.
    // If loading the settings caused the default engine to change because of an
    // override, then we don't want to show the notification box.
    let skipDefaultChangedNotification =
      await this.#loadEnginesFromSettings(settings);

    // If #loadEnginesFromSettings changed the default engine, then we don't
    // need to call #checkOpenSearchOverrides as we know that the overrides have
    // only just been applied.
    skipDefaultChangedNotification ||=
      await this.#checkOpenSearchOverrides(settings);

    // Settings file version 6 and below will need a migration to store the
    // engine ids rather than engine names.
    this._settings.migrateEngineIds(settings);

    lazy.logConsole.debug("#loadEngines: done");

    let newCurrentEngine = this._getEngineDefault(false);
    let newCurrentEngineId = newCurrentEngine?.id;

    this._settings.setMetaDataAttribute(
      "appDefaultEngineId",
      this.appDefaultEngine?.id
    );

    if (
      !skipDefaultChangedNotification &&
      this.#shouldDisplayRemovalOfEngineNotificationBox(
        settings,
        prevMetaData,
        newCurrentEngineId,
        prevCurrentEngineId,
        prevAppDefaultEngineId
      )
    ) {
      let newCurrentEngineName = newCurrentEngine?.name;

      let [prevCurrentEngineName, prevAppDefaultEngineName] = [
        settings.engines.find(e => e.id == prevCurrentEngineId)?._name,
        settings.engines.find(e => e.id == prevAppDefaultEngineId)?._name,
      ];

      this._showRemovalOfSearchEngineNotificationBox(
        prevCurrentEngineName || prevAppDefaultEngineName,
        newCurrentEngineName
      );
    }
  }

  /**
   * Helper function to determine if the removal of search engine notification
   * box should be displayed.
   *
   * @param { object } settings
   *   The user's search engine settings.
   * @param { object } prevMetaData
   *   The user's previous search settings metadata.
   * @param { object } newCurrentEngineId
   *   The user's new current default engine.
   * @param { object } prevCurrentEngineId
   *   The user's previous default engine.
   * @param { object } prevAppDefaultEngineId
   *   The user's previous app default engine.
   * @returns { boolean }
   *   Return true if the previous default engine has been removed and
   *   notification box should be displayed.
   */
  #shouldDisplayRemovalOfEngineNotificationBox(
    settings,
    prevMetaData,
    newCurrentEngineId,
    prevCurrentEngineId,
    prevAppDefaultEngineId
  ) {
    if (
      !Services.prefs.getBoolPref("browser.search.removeEngineInfobar.enabled")
    ) {
      return false;
    }

    // If for some reason we were unable to install any engines and hence no
    // default engine, do not display the notification box
    if (!newCurrentEngineId) {
      return false;
    }

    // If the previous engine is still available, don't show the notification
    // box.
    if (prevCurrentEngineId && this._engines.has(prevCurrentEngineId)) {
      return false;
    }
    if (!prevCurrentEngineId && this._engines.has(prevAppDefaultEngineId)) {
      return false;
    }

    // Don't show the notification if the previous engine was an enterprise engine -
    // the text doesn't quite make sense.
    // let checkPolicyEngineId = prevCurrentEngineId ? prevCurrentEngineId : prevAppDefaultEngineId;
    let checkPolicyEngineId = prevCurrentEngineId || prevAppDefaultEngineId;
    if (checkPolicyEngineId) {
      let engineSettings = settings.engines.find(
        e => e.id == checkPolicyEngineId
      );
      if (engineSettings?._loadPath?.startsWith("[policy]")) {
        return false;
      }
    }

    // If the user's previous engine id is different than the new current
    // engine id, or if the user was using the app default engine and the
    // app default engine id is different than the new current engine id,
    // we check if the user's settings metadata has been upddated.
    if (
      (prevCurrentEngineId && prevCurrentEngineId !== newCurrentEngineId) ||
      (!prevCurrentEngineId &&
        prevAppDefaultEngineId &&
        prevAppDefaultEngineId !== newCurrentEngineId)
    ) {
      // Check settings metadata to detect an update to locale. Sometimes when
      // the user changes their locale it causes a change in engines.
      // If there is no update to settings metadata then the engine change was
      // caused by an update to config rather than a user changing their locale.
      if (!this.#didSettingsMetaDataUpdate(prevMetaData)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Loads engines as specified by the configuration. We only expect
   * configured engines here, user engines should not be listed.
   *
   * @param {Array} engineConfigs
   *   An array of engines configurations based on the schema.
   * @param {object} [settings]
   *   The saved settings for the user.
   */
  #loadEnginesFromConfig(engineConfigs, settings) {
    lazy.logConsole.debug("#loadEnginesFromConfig");
    for (let config of engineConfigs) {
      try {
        let engine = new lazy.AppProvidedSearchEngine({ config, settings });
        this.#addEngineToStore(engine);
      } catch (ex) {
        console.error(
          "Could not load app provided search engine id:",
          config.identifier,
          ex
        );
      }
    }
  }

  /**
   * Loads any engines that have been received from the AddonManager during
   * startup and before we have finished initialising.
   *
   * @param {object} [settings]
   *   The saved settings for the user.
   */
  async #loadStartupEngines(settings) {
    if (this.#startupExtensions.size) {
      await lazy.AddonManager.readyPromise;
    }

    lazy.logConsole.debug(
      "#loadStartupEngines: loading",
      this.#startupExtensions.size,
      "engines reported by AddonManager startup"
    );
    for (let extension of this.#startupExtensions) {
      try {
        await this.#createAndAddAddonEngine({
          extension,
          settings,
        });
      } catch (ex) {
        lazy.logConsole.error(
          "#loadStartupEngines failed for",
          extension.id,
          ex
        );
      }
    }
    this.#startupExtensions.clear();
  }

  /**
   * When starting up, check if any of the saved application provided engines
   * are no longer required, previously were default and were overridden by
   * an OpenSearch engine.
   *
   * Also check if any OpenSearch overrides need to be re-applied.
   *
   * Add-on search engines are handled separately.
   *
   * @param {object} settings
   *   The loaded settings for the user.
   * @returns {Promise<boolean>}
   *   Returns true if the default engine was changed.
   */
  async #checkOpenSearchOverrides(settings) {
    let defaultEngineChanged = false;
    let savedDefaultEngineId =
      settings.metaData.defaultEngineId || settings.metaData.appDefaultEngineId;
    if (!savedDefaultEngineId) {
      return false;
    }
    // First handle the case where the application provided engine was removed,
    // and we need to restore the OpenSearch engine.
    for (let engineSettings of settings.engines) {
      if (
        !this._engines.get(engineSettings.id) &&
        engineSettings._isAppProvided &&
        engineSettings.id == savedDefaultEngineId &&
        engineSettings._metaData.overriddenByOpenSearch
      ) {
        let restoringEngine = new lazy.OpenSearchEngine({
          json: engineSettings._metaData.overriddenByOpenSearch,
        });
        restoringEngine.copyUserSettingsFrom(engineSettings);
        this.#addEngineToStore(restoringEngine, true);

        // We assume that the app provided engine was removed due to a
        // configuration change, and therefore we have re-added the OpenSearch
        // search engine. It is possible that it was actually due to a
        // locale/region change, but that is harder to detect here.
        this.#setEngineDefault(
          false,
          restoringEngine,
          Ci.nsISearchService.CHANGE_REASON_CONFIG
        );
        delete engineSettings._metaData.overriddenByOpenSearch;
      }
    }
    // Now handle the case where the an application provided engine has been
    // overridden by an OpenSearch engine, and we need to re-apply the override.
    for (let engine of this._engines.values()) {
      if (
        engine.isAppProvided &&
        engine.getAttr("overriddenByOpenSearch") &&
        engine.id == savedDefaultEngineId
      ) {
        let restoringEngine = new lazy.OpenSearchEngine({
          json: engine.getAttr("overriddenByOpenSearch"),
        });
        if (
          await lazy.defaultOverrideAllowlist.canEngineOverride(
            restoringEngine,
            engine.id
          )
        ) {
          engine.overrideWithEngine({ engine: restoringEngine });
        }
      }
    }

    return defaultEngineChanged;
  }

  /**
   * Reloads engines asynchronously, but only when
   * the service has already been initialized.
   *
   * This is prefixed with _ rather than # because it is
   * called in test_reload_engines.js
   *
   * @param {number} changeReason
   *   The reason reload engines is being called, one of
   *   Ci.nsISearchService.CHANGE_REASON*
   */
  async _maybeReloadEngines(changeReason) {
    if (this.#maybeReloadDebounce) {
      lazy.logConsole.debug("We're already waiting to reload engines.");
      return;
    }

    if (!this.isInitialized || this._reloadingEngines) {
      this.#maybeReloadDebounce = true;
      // Schedule a reload to happen at most 10 seconds after the current run.
      Services.tm.idleDispatchToMainThread(() => {
        if (!this.#maybeReloadDebounce) {
          return;
        }
        this.#maybeReloadDebounce = false;
        this._maybeReloadEngines(changeReason).catch(console.error);
      }, 10000);
      lazy.logConsole.debug(
        "Post-poning maybeReloadEngines() as we're currently initializing."
      );
      return;
    }

    // Before entering `_reloadingEngines` get the settings which we'll need.
    // This also ensures that any pending settings have finished being written,
    // which could otherwise cause data loss.
    let settings = await this._settings.get();

    lazy.logConsole.debug("Running maybeReloadEngines");
    this._reloadingEngines = true;

    try {
      await this._reloadEngines(settings, changeReason);
    } catch (ex) {
      lazy.logConsole.error("maybeReloadEngines failed", ex);
    }
    this._reloadingEngines = false;
    lazy.logConsole.debug("maybeReloadEngines complete");
  }

  /**
   * Manages reloading of the search engines when something in the user's
   * environment or the configuration has changed.
   *
   * The order of work here is designed to avoid potential issues when updating
   * the default engines, so that we're not removing active defaults or trying
   * to set a default to something that hasn't been added yet. The order is:
   *
   * 1) Update exising engines that are in both the old and new configuration.
   * 2) Add any new engines from the new configuration.
   * 3) Check for changes needed to the default engines due to environment changes
   *    and potentially overriding engines as per the override allowlist.
   * 4) Update the default engines.
   * 5) Remove any old engines.
   *
   * This is prefixed with _ rather than # because it is called in
   * test_remove_engine_notification_box.js
   *
   * @param {object} settings
   *   The user's current saved settings.
   * @param {number} changeReason
   *   The reason reload engines is being called, one of
   *   Ci.nsISearchService.CHANGE_REASON*
   */
  async _reloadEngines(settings, changeReason) {
    // Capture the current engine state, in case we need to notify below.
    let prevCurrentEngine = this.#currentEngine;
    let prevPrivateEngine = this.#currentPrivateEngine;
    let prevMetaData = { ...settings?.metaData };

    // Ensure that we don't set the useSavedOrder flag whilst we're doing this.
    // This isn't a user action, so we shouldn't be switching it.
    this.#dontSetUseSavedOrder = true;

    let refinedConfig = await this._fetchEngineSelectorEngines();

    let configEngines = [...refinedConfig.engines];
    let oldEngineList = [...this._engines.values()];

    for (let engine of oldEngineList) {
      if (!engine.isAppProvided) {
        if (engine instanceof lazy.AddonSearchEngine) {
          // If this is an add-on search engine, check to see if it needs
          // an update.
          await engine.update();
        }
        continue;
      }

      let index = configEngines.findIndex(e => e.identifier == engine.id);
      let configuration = configEngines?.[index];

      if (!configuration && engine._metaData["user-installed"]) {
        configuration =
          await this.#engineSelector.findContextualSearchEngineById(engine.id);
      }

      if (!configuration) {
        engine.pendingRemoval = true;
        continue;
      } else {
        // This is an existing engine that we should update. (However
        // notification will happen only if the configuration for this engine
        // has changed).
--> --------------------

--> maximum size reached

--> --------------------

[ zur Elbe Produktseite wechseln0.53Quellennavigators  Analyse erneut starten  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge