Commit 3e85c8f6 authored by Dale Harvey's avatar Dale Harvey
Browse files

Bug 1552559 - Ensure built in engines are available on startup. r=mikedeboer

Differential Revision: https://phabricator.services.mozilla.com/D32320

--HG--
extra : moz-landing-system : lando
parent b73a3454
Loading
Loading
Loading
Loading
+6 −2
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@
 */

const ENGINE_NAME = "mozSearch";
const ENGINE_ID = "mozsearch-engine@search.mozilla.org";

add_task(async function() {
  // We want select events to be fired.
@@ -12,6 +13,8 @@ add_task(async function() {

  await Services.search.init();

  // replace the path we load search engines from with
  // the path to our test data.
  let searchExtensions = getChromeDir(getResolvedURI(gTestPath));
  searchExtensions.append("mozsearch");
  let resProt = Services.io.getProtocolHandler("resource")
@@ -20,9 +23,10 @@ add_task(async function() {
  resProt.setSubstitution("search-extensions",
                          Services.io.newURI("file://" + searchExtensions.path));

  let addonPath = "resource://search-extensions/mozsearch-engine/";
  await AddonManager.installBuiltinAddon(addonPath);
  await Services.search.ensureBuiltinExtension(ENGINE_ID);

  let engine = await Services.search.getEngineByName(ENGINE_NAME);
  ok(engine, "Got a search engine");
  let defaultEngine = await Services.search.getDefault();
  await Services.search.setDefault(engine);

+114 −131
Original line number Diff line number Diff line
@@ -739,9 +739,10 @@ SearchService.prototype = {
  _visibleDefaultEngines: [],
  _searchDefault: null,
  _searchOrder: [],
  // Stores a map of the built in engines installed and their params so
  // they can be reconstructed in restarts.
  _extensions: new Map(),
  // A Set of installed search extensions reported by AddonManager
  // startup before SearchSevice has started. Will be installed
  // during init().
  _startupExtensions: new Set(),

  get _sortedEngines() {
    if (!this.__sortedEngines)
@@ -879,26 +880,6 @@ SearchService.prototype = {
      return !cache.visibleDefaultEngines.includes(engineName);
    }

    // Parse the engine name into the extension name + locale pair, some engines
    // will be exempt (ie yahoo-jp-auctions), can turn this from a whitelist to a
    // blacklist when more engines are multilocale than not.
    function parseEngineName(engineName) {
      let [name, locale] = engineName.split(/-(.+)/);

      if (!MULTI_LOCALE_ENGINES.includes(name)) {
        return [engineName, DEFAULT_TAG];
      }

      if (!locale) {
        locale = DEFAULT_TAG;
      }
      return [name, locale];
    }

    function extensionId(name) {
      return name + "@" + EXT_SIGNING_ADDRESS;
    }

    let buildID = Services.appinfo.platformBuildID;
    let rebuildCache = gEnvironment.get("RELOAD_ENGINES") ||
                       !cache.engines ||
@@ -908,38 +889,6 @@ SearchService.prototype = {
                       cache.visibleDefaultEngines.length != this._visibleDefaultEngines.length ||
                       this._visibleDefaultEngines.some(notInCacheVisibleEngines);

    await WebExtensionPolicy.readyPromise;

    // If we are reiniting, delete previously installed built in
    // extensions that arent in the current engine list.
    for (let id of this._extensions.keys()) {
      let policy = WebExtensionPolicy.getByID(id);
      if (!policy) {
        // If we are reiniting due to a remote settings update, we may have
        // removed all the engines and be rebuilding without the cache. In this
        // case, we won't have the cached engines loaded, so just skip this check.
        continue;
      }
      let extension = policy.extension;
      if (extension.addonData.builtIn && !engines.some(name => extensionId(name) === id)) {
        this._extensions.delete(id);
      }
    }

    // Turn our engine list into a list of extension names + the locales
    // to be installed.
    for (let engine of engines) {
      let [extensionName, locale] = parseEngineName(engine);
      let id = extensionId(extensionName);
      let localeMap = this._extensions.get(id) || new Map();
      let params = localeMap.get(locale);

      if (!params) {
        localeMap.set(locale, !rebuildCache);
        this._extensions.set(id, localeMap);
      }
    }

    if (!rebuildCache) {
      SearchUtils.log("_loadEngines: loading from cache directories");
      this._loadEnginesFromCache(cache);
@@ -959,42 +908,90 @@ SearchService.prototype = {
      let enginesFromURLs = await this._loadFromChromeURLs(engines, isReload);
      enginesFromURLs.forEach(this._addEngineToStore, this);
    } else {
      for (let [id, localeMap] of this._extensions) {
        let policy = WebExtensionPolicy.getByID(id);
        if (policy) {
          SearchUtils.log("_loadEngines: Found previously installed extension");
          await this.addEnginesFromExtension(policy.extension);
        } else {
          SearchUtils.log("_loadEngines: Installing " + id);
          // We may have marked these as loading from the cache previously
          // but if there wasnt an valid cache, mark as installing.
          for (let [locale, installed] of localeMap) {
            if (installed === true) {
              localeMap.set(locale, null);
      let engineList = this._enginesToLocales(engines);
      for (let [id, locales] of engineList) {
        await this.ensureBuiltinExtension(id, locales);
      }

      SearchUtils.log("_loadEngines: loading " +
        this._startupExtensions.size + " engines reported by AddonManager startup");
      for (let extension of this._startupExtensions) {
        await this._installExtensionEngine(extension, [DEFAULT_TAG]);
      }
          this._extensions.set(id, localeMap);
          let path = EXT_SEARCH_PREFIX + id.split("@")[0] + "/";
    }

    SearchUtils.log("_loadEngines: loading user-installed engines from the obsolete cache");
    this._loadEnginesFromCache(cache, true);

    this._loadEnginesMetadataFromCache(cache);

    SearchUtils.log("_loadEngines: done using rebuilt cache");
  },

  /**
   * Ensures a built in search WebExtension is installed, installing
   * it if necessary.
   *
   * @returns {Promise} A promise, resolved successfully once the
   * extension is installed and registered by the SearchService.
   */
  async ensureBuiltinExtension(id, locales = [DEFAULT_TAG]) {
    SearchUtils.log("ensureBuiltinExtension: " + id);
    try {
      let policy = WebExtensionPolicy.getByID(id);
      if (!policy) {
        SearchUtils.log("ensureBuiltinExtension: Installing " + id);
        let path = EXT_SEARCH_PREFIX + id.split("@")[0] + "/";
        await AddonManager.installBuiltinAddon(path);
            // The AddonManager will install the engine asynchronously
            // We can manually wait on that happening here.
            await ExtensionParent.apiManager.global.pendingSearchSetupTasks.get(id);
            SearchUtils.log("_loadEngines: " + id + " installed");
        policy = WebExtensionPolicy.getByID(id);
      }
      // On startup the extension may have not finished parsing the
      // manifest, wait for that here.
      await policy.readyPromise;
      await this._installExtensionEngine(policy.extension, locales);
      SearchUtils.log("ensureBuiltinExtension: " + id + " installed.");
    } catch (err) {
            this._extensions.delete(id);
      Cu.reportError("Failed to install engine: " + err.message + "\n" + err.stack);
    }
  },

  /**
   * Converts array of engines into a Map of extensions + the locales
   * of those extensions to install.
   *
   * @return {Map} A Map of extension names + locales.
   */
  _enginesToLocales(engines) {
    let engineLocales = new Map();
    for (let engine of engines) {
      let [extensionName, locale] = this._parseEngineName(engine);
      let id = extensionName + "@" + EXT_SIGNING_ADDRESS;
      let locales = engineLocales.get(id) || new Set();
      locales.add(locale);
      engineLocales.set(id, locales);
    }
      }
    }
    return engineLocales;
  },

    SearchUtils.log("_loadEngines: loading user-installed engines from the obsolete cache");
    this._loadEnginesFromCache(cache, true);
  /**
   * Parse the engine name into the extension name + locale pair
   * some engines will be exempt (ie yahoo-jp-auctions), can turn
   * this from a whitelist to a blacklist when more engines
   * are multilocale than not.
   *
   * @return {Array} The extension name and the locale to use.
   */
  _parseEngineName(engineName) {
    let [name, locale] = engineName.split(/-(.+)/);

    this._loadEnginesMetadataFromCache(cache);
    if (!MULTI_LOCALE_ENGINES.includes(name)) {
      return [engineName, DEFAULT_TAG];
    }

    SearchUtils.log("_loadEngines: done using rebuilt cache");
    if (!locale) {
      locale = DEFAULT_TAG;
    }
    return [name, locale];
  },

  /**
@@ -1112,8 +1109,13 @@ SearchService.prototype = {
   */
  reset() {
    gInitialized = false;
    this._extensions = new Map();
    this._startupExtensions = new Map();
    this._initStarted = this.__sortedEngines =
      this._currentEngine = this._searchDefault = null;
    this._startupExtensions = new Set();
    this._engines = {};
    this._visibleDefaultEngines = [];
    this._searchOrder = [];
    this._metaData = {};
  },

  /**
@@ -1823,7 +1825,7 @@ SearchService.prototype = {
  },

  async addEngineWithDetails(name, iconURL, alias, description, method, template, extensionID) {
    SearchUtils.log("addEngineWithDetails: Adding \"" + template + "\".");
    SearchUtils.log("addEngineWithDetails: Adding \"" + name + "\".");
    let isCurrent = false;
    var params;

@@ -1844,8 +1846,7 @@ SearchService.prototype = {
    // We install search extensions during the init phase, both built in
    // web extensions freshly installed (via addEnginesFromExtension) or
    // user installed extensions being reenabled calling this directly.
    if (!gInitialized && !this._extensions.has(params.extensionID)) {
      SearchUtils.log("addEngineWithDetails: Not expecting " + params.extensionID);
    if (!gInitialized && !isBuiltin) {
      await this.init(true);
    }
    if (!name)
@@ -1884,19 +1885,21 @@ SearchService.prototype = {

  async addEnginesFromExtension(extension) {
    SearchUtils.log("addEnginesFromExtension: " + extension.id);
    try {
      // When Firefox starts, the AddonManager will report all the currently
      // installed search addons to us, keep the user installed engines
      // but the Builtin ones we will install during init().
      if (!gInitialized && extension.addonData.builtIn &&
          !this._extensions.has(extension.id)) {
    if (extension.addonData.builtIn) {
      SearchUtils.log("addEnginesFromExtension: Ignoring builtIn engine.");
      return [];
    }

      if (!this._extensions.has(extension.id)) {
        SearchUtils.log("addEnginesFromExtension: User installed extension " + extension.id);
        this._extensions.set(extension.id, new Map([[DEFAULT_TAG, null]]));
    // If we havent started SearchService yet, store this extension
    // to install in SearchService.init().
    if (!gInitialized) {
      this._startupExtensions.add(extension);
      return [];
    }
    return this._installExtensionEngine(extension, [DEFAULT_TAG]);
  },

  async _installExtensionEngine(extension, locales) {
    SearchUtils.log("installExtensionEngine: " + extension.id);

    let installLocale = async (locale) => {
      let manifest = (locale === DEFAULT_TAG) ? extension.manifest :
@@ -1905,21 +1908,12 @@ SearchService.prototype = {
    };

    let engines = [];
      for (let [locale, installed] of this._extensions.get(extension.id)) {
        // If we have installed the engine from cache previously then
        // no need to reinstall.
        if (installed !== true) {
    for (let locale of locales) {
      SearchUtils.log("addEnginesFromExtension: installing locale: " +
        extension.id + ":" + locale);
      engines.push(await installLocale(locale));
    }
      }
    return engines;
    } catch (err) {
      SearchUtils.log("addEnginesFromExtension: Failed to install " + extension.id + ": \"" +
          err.message + "\"\n" + err.stack);
      return [];
    }
  },

  async _addEngineForManifest(extension, manifest, locale = DEFAULT_TAG) {
@@ -1974,10 +1968,6 @@ SearchService.prototype = {
      mozParams: searchProvider.params,
    };

    let localeMap = this._extensions.get(extension.id);
    localeMap.set(locale, params);
    this._extensions.set(extension.id, localeMap);

    return this.addEngineWithDetails(params.name, params);
  },

@@ -2017,16 +2007,9 @@ SearchService.prototype = {

  async removeWebExtensionEngine(id) {
    SearchUtils.log("removeWebExtensionEngine: " + id);
    let localeMap = this._extensions.get(id);
    if (!localeMap) {
      Cu.reportError("Cannot find extension (" + id + ") to remove");
      return;
    }
    for (let params of localeMap.values()) {
      let engine = await this.getEngineByName(params.name);
    for (let engine of await this.getEnginesByExtensionID(id)) {
      await this.removeEngine(engine);
    }
    this._extensions.delete(id);
  },

  async removeEngine(engine) {
+5 −10
Original line number Diff line number Diff line
@@ -217,18 +217,12 @@ interface nsISearchService : nsISupports
  Promise init();

  /**
   * Redo asynchronous initialization
   *
   * Exposed for testing initializations
   * Exposed for testing.
   */
  void reInit([optional] in boolean skipRegionCheck);

  /**
   * Clear locally stored data
   *
   * Exposed for testing initializations
   */
  void reset();
  Promise ensureBuiltinExtension(in AString id,
                                [optional] in jsval locales);

  /**
   * Determine whether initialization has been completed.
@@ -361,7 +355,8 @@ interface nsISearchService : nsISupports
  /**
   * Adds search providers to the search service.  If the search
   * service is configured to load multiple locales for the extension,
   * it may load more than one search engine.
   * it may load more than one search engine. If called directly
   * ensure the extension has been initialised.
   *
   * @param extension
   *        The extension to load from.
+10 −5
Original line number Diff line number Diff line
@@ -18,7 +18,8 @@ add_task(async function test_selectedEngine() {
  // Test the selectedEngine pref.
  Services.prefs.setCharPref(kSelectedEnginePref, kTestEngineName);

  await asyncReInit();
  Services.search.reset();
  await Services.search.init(true);
  Assert.equal(Services.search.defaultEngine.name, defaultEngineName);

  Services.prefs.clearUserPref(kSelectedEnginePref);
@@ -26,7 +27,8 @@ add_task(async function test_selectedEngine() {
  // Test the defaultenginename pref.
  Services.prefs.setCharPref(kDefaultenginenamePref, kTestEngineName);

  await asyncReInit();
  Services.search.reset();
  await Services.search.init(true);
  Assert.equal(Services.search.defaultEngine.name, defaultEngineName);

  Services.prefs.clearUserPref(kDefaultenginenamePref);
@@ -45,7 +47,8 @@ add_task(async function test_persistAcrossRestarts() {
  Assert.equal(metadata.hash.length, 44);

  // Re-init and check the engine is still the same.
  await asyncReInit();
  Services.search.reset();
  await Services.search.init(true);
  Assert.equal(Services.search.defaultEngine.name, kTestEngineName);

  // Cleanup (set the engine back to default).
@@ -66,7 +69,8 @@ add_task(async function test_ignoreInvalidHash() {
  await promiseSaveGlobalMetadata(metadata);

  // Re-init the search service, and check that the json file is ignored.
  await asyncReInit();
  Services.search.reset();
  await Services.search.init(true);
  Assert.equal(Services.search.defaultEngine.name, getDefaultEngineName());
});

@@ -136,7 +140,8 @@ add_task(async function test_fallback_kept_after_restart() {
  await promiseAfterCache();

  // After a restart, the defaultEngine value should still be unchanged.
  await asyncReInit();
  Services.search.reset();
  await Services.search.init(true);
  Assert.equal(Services.search.defaultEngine.name, defaultName);
});

+2 −3
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ AddonTestUtils.overrideCertDB();
async function restart() {
  Services.search.reset();
  await promiseRestartManager();
  await asyncReInit({waitForRegionFetch: true, skipReset: true});
  await Services.search.init(false);
}

async function getEngineNames() {
@@ -92,9 +92,8 @@ add_task(async function basic_install_test() {

  // User uninstalls their engine
  await extension.awaitStartup();
  let commitPromise = promiseAfterCache();
  await extension.unload();
  await commitPromise;
  await promiseAfterCache();
  Assert.deepEqual((await getEngineNames()), ["Plain"]);
});