Bug 1544013 - Support search extensions and temporarily ship the default ones from mozilla-central. rs=bustage-fix,jorgk
authorGeoff Lankow <geoff@darktrojan.net>
Mon, 15 Apr 2019 15:51:19 +0200
changeset 26344 30a7914d1c644a567f1956c6667295183accbb42
parent 26343 4f2702e359aa413a1032a2b5201582446a86f76b
child 26345 ad2d84d80a1ee61bd4ba33a33715a9859236a388
push id15794
push usermozilla@jorgk.com
push dateMon, 15 Apr 2019 13:56:46 +0000
treeherdercomm-central@ad2d84d80a1e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbustage-fix, jorgk
bugs1544013
Bug 1544013 - Support search extensions and temporarily ship the default ones from mozilla-central. rs=bustage-fix,jorgk
mail/components/extensions/ext-mail.json
mail/components/extensions/jar.mn
mail/components/extensions/parent/ext-chrome-settings-overrides.js
mail/components/extensions/parent/ext-mail.js
mail/components/extensions/schemas/chrome_settings_overrides.json
mail/components/search/jar.mn
mail/installer/allowed-dupes.mn
mail/test/mozmill/newmailaccount/test-newmailaccount.js
--- a/mail/components/extensions/ext-mail.json
+++ b/mail/components/extensions/ext-mail.json
@@ -19,16 +19,23 @@
     "url": "chrome://messenger/content/parent/ext-browserAction.js",
     "schema": "chrome://messenger/content/schemas/browserAction.json",
     "scopes": ["addon_parent"],
     "manifest": ["browser_action"],
     "paths": [
       ["browserAction"]
     ]
   },
+  "chrome_settings_overrides": {
+    "url": "chrome://messenger/content/parent/ext-chrome-settings-overrides.js",
+    "scopes": [],
+    "events": ["update", "uninstall"],
+    "schema": "chrome://messenger/content/schemas/chrome_settings_overrides.json",
+    "manifest": ["chrome_settings_overrides"]
+  },
   "cloudFile": {
     "url": "chrome://messenger/content/parent/ext-cloudFile.js",
     "schema": "chrome://messenger/content/schemas/cloudFile.json",
     "scopes": ["addon_parent", "content_parent"],
     "manifest": ["cloud_file"],
     "paths": [
       ["cloudFile"]
     ]
--- a/mail/components/extensions/jar.mn
+++ b/mail/components/extensions/jar.mn
@@ -9,32 +9,34 @@ messenger.jar:
     content/messenger/child/ext-mail.js            (child/ext-mail.js)
     content/messenger/child/ext-menus-child.js     (child/ext-menus-child.js)
     content/messenger/child/ext-menus.js           (child/ext-menus.js)
     content/messenger/child/ext-tabs.js            (child/ext-tabs.js)
 
     content/messenger/parent/ext-accounts.js       (parent/ext-accounts.js)
     content/messenger/parent/ext-addressBook.js    (parent/ext-addressBook.js)
     content/messenger/parent/ext-browserAction.js  (parent/ext-browserAction.js)
+    content/messenger/parent/ext-chrome-settings-overrides.js      (parent/ext-chrome-settings-overrides.js)
     content/messenger/parent/ext-cloudFile.js      (parent/ext-cloudFile.js)
     content/messenger/parent/ext-commands.js       (../../../../browser/components/extensions/parent/ext-commands.js)
     content/messenger/parent/ext-compose.js        (parent/ext-compose.js)
     content/messenger/parent/ext-composeAction.js  (parent/ext-composeAction.js)
     content/messenger/parent/ext-legacy.js         (parent/ext-legacy.js)
     content/messenger/parent/ext-mail.js           (parent/ext-mail.js)
     content/messenger/parent/ext-mailTabs.js       (parent/ext-mailTabs.js)
     content/messenger/parent/ext-menus.js          (parent/ext-menus.js)
     content/messenger/parent/ext-messages.js       (parent/ext-messages.js)
     content/messenger/parent/ext-pkcs11.js         (../../../../browser/components/extensions/parent/ext-pkcs11.js)
     content/messenger/parent/ext-tabs.js           (parent/ext-tabs.js)
     content/messenger/parent/ext-windows.js        (parent/ext-windows.js)
 
     content/messenger/schemas/accounts.json        (schemas/accounts.json)
     content/messenger/schemas/addressBook.json     (schemas/addressBook.json)
     content/messenger/schemas/browserAction.json   (schemas/browserAction.json)
+    content/messenger/schemas/chrome_settings_overrides.json       (schemas/chrome_settings_overrides.json)
     content/messenger/schemas/cloudFile.json       (schemas/cloudFile.json)
     content/messenger/schemas/commands.json        (../../../../browser/components/extensions/schemas/commands.json)
     content/messenger/schemas/compose.json         (schemas/compose.json)
     content/messenger/schemas/composeAction.json   (schemas/composeAction.json)
     content/messenger/schemas/legacy.json          (schemas/legacy.json)
     content/messenger/schemas/mailTabs.json        (schemas/mailTabs.json)
     content/messenger/schemas/menus.json           (schemas/menus.json)
     content/messenger/schemas/menus_child.json     (schemas/menus_child.json)
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/parent/ext-chrome-settings-overrides.js
@@ -0,0 +1,246 @@
+/* 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/. */
+
+/* import-globals-from ext-mail.js */
+
+"use strict";
+
+var {ExtensionPreferencesManager} = ChromeUtils.import("resource://gre/modules/ExtensionPreferencesManager.jsm");
+var {ExtensionParent} = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
+
+ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
+                               "resource://gre/modules/ExtensionSettingsStore.jsm");
+
+const DEFAULT_SEARCH_STORE_TYPE = "default_search";
+const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch";
+const ENGINE_ADDED_SETTING_NAME = "engineAdded";
+
+// When an extension starts up, a search engine may asynchronously be
+// registered, without blocking the startup. When an extension is
+// uninstalled, we need to wait for this registration to finish
+// before running the uninstallation handler.
+// Map[extension id -> Promise]
+var pendingSearchSetupTasks = new Map();
+
+this.chrome_settings_overrides = class extends ExtensionAPI {
+  static async processDefaultSearchSetting(action, id) {
+    await ExtensionSettingsStore.initialize();
+    let item = ExtensionSettingsStore.getSetting(DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
+    if (!item) {
+      return;
+    }
+    if (Services.search.defaultEngine.name != item.value &&
+        Services.search.defaultEngine.name != item.initialValue) {
+      // The current engine is not the same as the value that the ExtensionSettingsStore has.
+      // This means that the user changed the engine, so we shouldn't control it anymore.
+      // Do nothing and remove our entry from the ExtensionSettingsStore.
+      ExtensionSettingsStore.removeSetting(id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
+      return;
+    }
+    item = ExtensionSettingsStore[action](id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
+    if (item) {
+      try {
+        let engine = Services.search.getEngineByName(item.value || item.initialValue);
+        if (engine) {
+          Services.search.defaultEngine = engine;
+        }
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    }
+  }
+
+  static async removeEngine(id) {
+    await ExtensionSettingsStore.initialize();
+    let item = await ExtensionSettingsStore.getSetting(
+      DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME, id);
+    if (item) {
+      ExtensionSettingsStore.removeSetting(
+        id, DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME);
+    }
+    // We can call removeEngine in nsSearchService startup, if so we dont
+    // need to reforward the call, just disable the web extension.
+    if (!Services.search.isInitialized) {
+      return;
+    }
+
+    try {
+      await Services.search.removeWebExtensionEngine(id);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  }
+
+  static removeSearchSettings(id) {
+    return Promise.all([
+      this.processDefaultSearchSetting("removeSetting", id),
+      this.removeEngine(id),
+    ]);
+  }
+
+  static async onUninstall(id) {
+    let searchStartupPromise = pendingSearchSetupTasks.get(id);
+    if (searchStartupPromise) {
+      await searchStartupPromise;
+    }
+    return Promise.all([
+      this.removeSearchSettings(id),
+    ]);
+  }
+
+  static onUpdate(id, manifest) {
+    let haveSearchProvider = manifest && manifest.chrome_settings_overrides &&
+                             manifest.chrome_settings_overrides.search_provider;
+    if (!haveSearchProvider) {
+      this.removeSearchSettings(id);
+    }
+  }
+
+  async onManifestEntry(entryName) {
+    let {extension} = this;
+    let {manifest} = extension;
+
+    await ExtensionSettingsStore.initialize();
+
+    if (manifest.chrome_settings_overrides.search_provider) {
+      // Registering a search engine can potentially take a long while,
+      // or not complete at all (when searchInitialized is never resolved),
+      // so we are deliberately not awaiting the returned promise here.
+      let searchStartupPromise =
+        this.processSearchProviderManifestEntry().finally(() => {
+          if (pendingSearchSetupTasks.get(extension.id) === searchStartupPromise) {
+            pendingSearchSetupTasks.delete(extension.id);
+          }
+        });
+
+      // Save the promise so we can await at onUninstall.
+      pendingSearchSetupTasks.set(extension.id, searchStartupPromise);
+    }
+  }
+
+  async processSearchProviderManifestEntry() {
+    let {extension} = this;
+    let {manifest} = extension;
+    let searchProvider = manifest.chrome_settings_overrides.search_provider;
+    if (searchProvider.is_default) {
+      await searchInitialized;
+      if (!this.extension) {
+        Cu.reportError(`Extension shut down before search provider was registered`);
+        return;
+      }
+    }
+    extension.callOnClose({
+      close: () => {
+        if (extension.shutdownReason == "ADDON_DISABLE") {
+          chrome_settings_overrides.processDefaultSearchSetting("disable", extension.id);
+          chrome_settings_overrides.removeEngine(extension.id);
+        }
+      },
+    });
+
+    let engineName = searchProvider.name.trim();
+    if (searchProvider.is_default) {
+      let engine = Services.search.getEngineByName(engineName);
+      let defaultEngines = await Services.search.getDefaultEngines();
+      if (engine && defaultEngines.some(defaultEngine => defaultEngine.name == engineName)) {
+        // Needs to be called every time to handle reenabling, but
+        // only sets default for install or enable.
+        await this.setDefault(engineName);
+        // For built in search engines, we don't do anything further
+        return;
+      }
+    }
+    await this.addSearchEngine();
+    if (searchProvider.is_default) {
+      if (extension.startupReason === "ADDON_INSTALL") {
+        // Don't ask if it already the current engine
+        let engine = Services.search.getEngineByName(engineName);
+        let defaultEngine = await Services.search.getDefault();
+        if (defaultEngine.name != engine.name) {
+          let subject = {
+            wrappedJSObject: {
+              // This is a hack because we don't have the browser of
+              // the actual install. This means the popup might show
+              // in a different window. Will be addressed in a followup bug.
+              browser: windowTracker.topWindow.gBrowser.selectedBrowser,
+              name: this.extension.name,
+              icon: this.extension.iconURL,
+              currentEngine: defaultEngine.name,
+              newEngine: engineName,
+              respond(allow) {
+                if (allow) {
+                  ExtensionSettingsStore.addSetting(
+                    extension.id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME, engineName, () => defaultEngine.name);
+                  Services.search.defaultEngine = Services.search.getEngineByName(engineName);
+                }
+              },
+            },
+          };
+          Services.obs.notifyObservers(subject, "webextension-defaultsearch-prompt");
+        }
+      } else {
+        // Needs to be called every time to handle reenabling, but
+        // only sets default for install or enable.
+        this.setDefault(engineName);
+      }
+    } else if (ExtensionSettingsStore.hasSetting(extension.id,
+                                                 DEFAULT_SEARCH_STORE_TYPE,
+                                                 DEFAULT_SEARCH_SETTING_NAME)) {
+      // is_default has been removed, but we still have a setting. Remove it.
+      chrome_settings_overrides.processDefaultSearchSetting("removeSetting", extension.id);
+    }
+  }
+
+  async setDefault(engineName) {
+    let {extension} = this;
+    if (extension.startupReason === "ADDON_INSTALL") {
+      let defaultEngine = await Services.search.getDefault();
+      let item = await ExtensionSettingsStore.addSetting(
+        extension.id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME, engineName, () => defaultEngine.name);
+      await Services.search.setDefault(Services.search.getEngineByName(item.value));
+    } else if (extension.startupReason === "ADDON_ENABLE") {
+      chrome_settings_overrides.processDefaultSearchSetting("enable", extension.id);
+    }
+  }
+
+  async addSearchEngine() {
+    let {extension} = this;
+    let isCurrent = false;
+    let index = -1;
+    if (extension.startupReason === "ADDON_UPGRADE") {
+      let engines = await Services.search.getEnginesByExtensionID(extension.id);
+      if (engines.length > 0) {
+        let firstEngine = engines[0];
+        let firstEngineName = firstEngine.name;
+        // There can be only one engine right now
+        isCurrent = (await Services.search.getDefault()).name == firstEngineName;
+        // Get position of engine and store it
+        index = (await Services.search.getEngines()).map(engine => engine.name).indexOf(firstEngineName);
+        await Services.search.removeEngine(firstEngine);
+      }
+    }
+    try {
+      let engines = await Services.search.addEnginesFromExtension(extension);
+      if (engines.length > 0) {
+        await ExtensionSettingsStore.addSetting(
+          extension.id, DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME,
+          engines[0].name);
+      }
+      if (extension.startupReason === "ADDON_UPGRADE") {
+        let engines = await Services.search.getEnginesByExtensionID(extension.id);
+        let engine = Services.search.getEngineByName(engines[0].name);
+        if (isCurrent) {
+          await Services.search.setDefault(engine);
+        }
+        if (index != -1) {
+          await Services.search.moveEngine(engine, index);
+        }
+      }
+    } catch (e) {
+      Cu.reportError(e);
+      return false;
+    }
+    return true;
+  }
+};
--- a/mail/components/extensions/parent/ext-mail.js
+++ b/mail/components/extensions/parent/ext-mail.js
@@ -70,16 +70,28 @@ function getTabBrowser(nativeTabInfo) {
   if (nativeTabInfo.mode.tabType.getBrowser) {
     return nativeTabInfo.mode.tabType.getBrowser(nativeTabInfo);
   }
 
   return null;
 }
 global.getTabBrowser = getTabBrowser;
 
+/* global searchInitialized */
+// This promise is used to wait for the search service to be initialized.
+// None of the code in the WebExtension modules requests that initialization.
+// It is assumed that it is started at some point. That might never happen,
+// e.g. if the application shuts down before the search service initializes.
+XPCOMUtils.defineLazyGetter(global, "searchInitialized", () => {
+  if (Services.search.isInitialized) {
+    return Promise.resolve();
+  }
+  return ExtensionUtils.promiseObserved("browser-search-service", (_, data) => data == "init-complete");
+});
+
 /**
  * The window tracker tracks opening and closing Thunderbird windows. Each window has an id, which
  * is mapped to native window objects.
  */
 class WindowTracker extends WindowTrackerBase {
   /**
    * Adds a tab progress listener to the given mail window.
    *
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/schemas/chrome_settings_overrides.json
@@ -0,0 +1,172 @@
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "chrome_settings_overrides": {
+            "type": "object",
+            "optional": true,
+            "additionalProperties": { "$ref": "UnrecognizedProperty" },
+            "properties": {
+             "search_provider": {
+                "type": "object",
+                "optional": true,
+                "additionalProperties": { "$ref": "UnrecognizedProperty" },
+                "properties": {
+                  "name": {
+                    "type": "string",
+                    "preprocess": "localize"
+                  },
+                  "keyword": {
+                    "type": "string",
+                    "optional": true,
+                    "preprocess": "localize"
+                  },
+                  "search_url": {
+                    "type": "string",
+                    "format": "url",
+                    "pattern": "^https://.*$",
+                    "preprocess": "localize"
+                  },
+                  "favicon_url": {
+                    "type": "string",
+                    "optional": true,
+                    "format": "url",
+                    "preprocess": "localize"
+                  },
+                  "suggest_url": {
+                    "type": "string",
+                    "optional": true,
+                    "pattern": "^https://.*$|^$",
+                    "preprocess": "localize"
+                  },
+                  "instant_url": {
+                    "type": "string",
+                    "optional": true,
+                    "format": "url",
+                    "preprocess": "localize",
+                    "deprecated": "Unsupported on Firefox at this time."
+                  },
+                  "image_url": {
+                    "type": "string",
+                    "optional": true,
+                    "format": "url",
+                    "preprocess": "localize",
+                    "deprecated": "Unsupported on Firefox at this time."
+                  },
+                  "search_url_get_params": {
+                    "type": "string",
+                    "optional": true,
+                    "preprocess": "localize",
+                    "description": "GET parameters to the search_url as a query string."
+                  },
+                  "search_url_post_params": {
+                    "type": "string",
+                    "optional": true,
+                    "preprocess": "localize",
+                    "description": "POST parameters to the search_url as a query string."
+                  },
+                  "suggest_url_get_params": {
+                    "type": "string",
+                    "optional": true,
+                    "preprocess": "localize",
+                    "description": "GET parameters to the suggest_url as a query string."
+                  },
+                  "suggest_url_post_params": {
+                    "type": "string",
+                    "optional": true,
+                    "preprocess": "localize",
+                    "description": "POST parameters to the suggest_url as a query string."
+                  },
+                  "instant_url_post_params": {
+                    "type": "string",
+                    "optional": true,
+                    "preprocess": "localize",
+                    "deprecated": "Unsupported on Firefox at this time."
+                  },
+                  "image_url_post_params": {
+                    "type": "string",
+                    "optional": true,
+                    "preprocess": "localize",
+                    "deprecated": "Unsupported on Firefox at this time."
+                  },
+                  "search_form": {
+                    "type": "string",
+                    "optional": true,
+                    "format": "url",
+                    "pattern": "^https://.*$",
+                    "preprocess": "localize"
+                  },
+                  "alternate_urls": {
+                    "type": "array",
+                    "items": {
+                      "type": "string",
+                      "format": "url",
+                      "preprocess": "localize"
+                    },
+                    "optional": true,
+                    "deprecated": "Unsupported on Firefox at this time."
+                  },
+                  "prepopulated_id": {
+                    "type": "integer",
+                    "optional": true,
+                    "deprecated": "Unsupported on Firefox."
+                  },
+                  "encoding": {
+                    "type": "string",
+                    "optional": true,
+                    "description": "Encoding of the search term."
+                  },
+                  "is_default": {
+                    "type": "boolean",
+                    "optional": true,
+                    "description": "Sets the default engine to a built-in engine only."
+                  },
+                  "params": {
+                    "optional": true,
+                    "type": "array",
+                    "items": {
+                      "type": "object",
+                      "properties": {
+                        "name": {
+                          "type": "string",
+                          "description": "A url parameter name"
+                        },
+                        "condition": {
+                          "type": "string",
+                          "optional": true,
+                          "enum": ["purpose", "pref"],
+                          "description": "The type of param can be either \"purpose\" or \"pref\"."
+                        },
+                        "pref": {
+                          "type": "string",
+                          "optional": true,
+                          "description": "The preference to retreive the value from."
+                        },
+                        "purpose": {
+                          "type": "string",
+                          "optional": true,
+                          "enum": ["contextmenu", "searchbar", "homepage", "keyword", "newtab"],
+                          "description": "The context that initiates a search, required if condition is \"purpose\"."
+                        },
+                        "value": {
+                          "type": "string",
+                          "optional": true,
+                          "description": "A url parameter value.",
+                          "preprocess": "localize"
+                        }
+                      }
+                    },
+                    "description": "A list of optional search url parameters. This allows the additon of search url parameters based on how the search is performed in Firefox."
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    ]
+  }
+]
--- a/mail/components/search/jar.mn
+++ b/mail/components/search/jar.mn
@@ -1,14 +1,14 @@
 # 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/.
 
 messenger.jar:
-    searchplugins/                            (searchplugins/**)
+    search-extensions/                        (../../../../browser/components/search/extensions/**)
 #ifdef XP_MACOSX
     content/messenger/SpotlightIntegration.js (content/SpotlightIntegration.js)
 #endif
 #ifdef XP_WIN
     content/messenger/WinSearchIntegration.js (content/WinSearchIntegration.js)
 #endif
 
-% resource search-plugins %searchplugins/
+% resource search-extensions %search-extensions/ contentaccessible=yes
--- a/mail/installer/allowed-dupes.mn
+++ b/mail/installer/allowed-dupes.mn
@@ -166,8 +166,15 @@ chrome/devtools/skin/images/tool-styleed
 chrome/devtools/skin/promisedebugger.css
 chrome/devtools/skin/variables.css
 modules/devtools/client/framework/gDevTools.jsm
 modules/devtools/gDevTools.jsm
 
 # Wetransfer locales - Bug 1518076 (en and en_GB are the same)
 features/wetransfer@extensions.thunderbird.net/_locales/en/messages.json
 features/wetransfer@extensions.thunderbird.net/_locales/en_GB/messages.json
+
+# Bug 1496075 - Switch searchplugins to Web Extensions
+chrome/messenger/search-extensions/amazon/favicon.ico
+chrome/messenger/search-extensions/amazondotcn/favicon.ico
+chrome/messenger/search-extensions/amazondotcom/favicon.ico
+chrome/messenger/search-extensions/mercadolibre/favicon.ico
+chrome/messenger/search-extensions/mercadolivre/favicon.ico
--- a/mail/test/mozmill/newmailaccount/test-newmailaccount.js
+++ b/mail/test/mozmill/newmailaccount/test-newmailaccount.js
@@ -90,17 +90,16 @@ function nAccounts() {
  * this test is split over 3 functions, and uses a global gNumAccounts.  The
  * three functions are "test_get_an_account", "subtest_get_an_account",
  * and "subtest_get_an_account_part_2".
  *
  * @param aCloseAndRestore a boolean for whether or not we should close and
  *                         restore the Account Provisioner tab before filling
  *                         in the form. Defaults to false.
  */
-test_get_an_account.__force_skip__ = true;
 function test_get_an_account(aCloseAndRestore) {
   let originalEngine = Services.search.defaultEngine;
   // Open the provisioner - once opened, let subtest_get_an_account run.
   plan_for_modal_dialog("AccountCreation", subtest_get_an_account);
   open_provisioner_window();
   wait_for_modal_dialog("AccountCreation");
 
   // Once we're here, subtest_get_an_account has completed, and we're waiting
@@ -201,17 +200,16 @@ function subtest_get_an_account_part_2(w
   // Then click "Finish"
   mc.click(w.eid("closeWindow"));
 }
 
 /**
  * Runs test_get_an_account again, but this time, closes and restores the
  * order form tab before submitting it.
  */
-test_restored_ap_tab_works.__force_skip__ = true;
 function test_restored_ap_tab_works() {
   test_get_an_account(true);
 }
 
 /**
  * Test that clicking on the "I think I'll configure my account later"
  * button dismisses the Account Provisioner window.
  */