Bug 1330494 - Part 2: Use the ExtensionsSettingsStore to handle precedence for extensions using chrome_url_overrides, r?aswan draft
authorBob Silverberg <bsilverberg@mozilla.com>
Wed, 28 Jun 2017 13:11:19 -0700
changeset 608553 746fd8ac359395838281e2d0abcc9d5269131024
parent 608552 c18623deb90ea77f81b05c7adec4fb66ddb4adf7
child 637347 cff2923a95df7649dd7a41acdc3f2b0de366a159
push id68323
push userbmo:bob.silverberg@gmail.com
push dateThu, 13 Jul 2017 20:45:14 +0000
reviewersaswan
bugs1330494
milestone56.0a1
Bug 1330494 - Part 2: Use the ExtensionsSettingsStore to handle precedence for extensions using chrome_url_overrides, r?aswan MozReview-Commit-ID: 5sKtIXjdSmC
browser/components/extensions/ext-browser.js
browser/components/extensions/ext-url-overrides.js
browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js
browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js
browser/components/extensions/test/xpcshell/xpcshell-common.ini
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -218,16 +218,17 @@ extensions.registerModules({
     paths: [
       ["tabs"],
     ],
   },
   urlOverrides: {
     url: "chrome://browser/content/ext-url-overrides.js",
     schema: "chrome://browser/content/schemas/url_overrides.json",
     scopes: ["addon_parent"],
+    events: ["startup"],
     manifest: ["chrome_url_overrides"],
     paths: [
       ["urlOverrides"],
     ],
   },
   windows: {
     url: "chrome://browser/content/ext-windows.js",
     schema: "chrome://browser/content/schemas/windows.json",
--- a/browser/components/extensions/ext-url-overrides.js
+++ b/browser/components/extensions/ext-url-overrides.js
@@ -1,53 +1,70 @@
 /* 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/. */
 
 "use strict";
 
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
+                                  "resource://gre/modules/ExtensionSettingsStore.jsm");
+
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 
-// Bug 1320736 tracks creating a generic precedence manager for handling
-// multiple addons modifying the same properties, and bug 1330494 has been filed
-// to track utilizing this manager for chrome_url_overrides. Until those land,
-// the edge cases surrounding multiple addons using chrome_url_overrides will
-// be ignored and precedence will be first come, first serve.
-let overrides = {
-  // A queue of extensions in line to override the newtab page (sorted oldest to newest).
-  newtab: [],
-};
+const STORE_TYPE = "url_overrides";
+const NEW_TAB_SETTING_NAME = "newTabURL";
 
 this.urlOverrides = class extends ExtensionAPI {
-  onManifestEntry(entryName) {
+  processNewTabSetting(action) {
+    let {extension} = this;
+    let item = ExtensionSettingsStore[action](extension, STORE_TYPE, NEW_TAB_SETTING_NAME);
+    if (item) {
+      aboutNewTabService.newTabURL = item.value || item.initialValue;
+    }
+  }
+
+  async onStartup() {
     let {extension} = this;
     let {manifest} = extension;
 
-    if (manifest.chrome_url_overrides.newtab) {
-      let newtab = manifest.chrome_url_overrides.newtab;
-      let url = extension.baseURI.resolve(newtab);
+    // Record the setting if it exists in the manifest.
+    if (manifest.chrome_url_overrides && manifest.chrome_url_overrides.newtab) {
+      let url = extension.baseURI.resolve(manifest.chrome_url_overrides.newtab);
 
-      // Only set the newtab URL if no other extension is overriding it.
-      if (!overrides.newtab.length) {
-        aboutNewTabService.newTabURL = url;
+      let item = await ExtensionSettingsStore.addSetting(
+        extension, STORE_TYPE, NEW_TAB_SETTING_NAME, url,
+        () => aboutNewTabService.newTabURL);
+
+      // If the extension was just re-enabled, change the setting to enabled.
+      // This is required because addSetting above is used for both add and update.
+      if (["ADDON_ENABLE", "ADDON_UPGRADE", "ADDON_DOWNGRADE"]
+          .includes(extension.startupReason)) {
+        item = ExtensionSettingsStore.enable(extension, STORE_TYPE, NEW_TAB_SETTING_NAME);
       }
 
-      overrides.newtab.push({id: extension.id, url});
+      // Set the newTabURL to the current value of the setting.
+      if (item) {
+        aboutNewTabService.newTabURL = item.value || item.initialValue;
+      }
+    // If the setting exists for the extension, but is missing from the manifest,
+    // remove it.
+    } else if (ExtensionSettingsStore.hasSetting(
+               extension, STORE_TYPE, NEW_TAB_SETTING_NAME)) {
+      this.processNewTabSetting("removeSetting");
     }
   }
 
-  onShutdown(reason) {
-    let {extension} = this;
+  onShutdown(shutdownReason) {
+    switch (shutdownReason) {
+      case "ADDON_DISABLE":
+      case "ADDON_DOWNGRADE":
+      case "ADDON_UPGRADE":
+        this.processNewTabSetting("disable");
+        break;
 
-    let i = overrides.newtab.findIndex(o => o.id === extension.id);
-    if (i !== -1) {
-      overrides.newtab.splice(i, 1);
-
-      if (overrides.newtab.length) {
-        aboutNewTabService.newTabURL = overrides.newtab[0].url;
-      } else {
-        aboutNewTabService.resetNewTabURL();
-      }
+      case "ADDON_UNINSTALL":
+        this.processNewTabSetting("removeSetting");
+        break;
     }
   }
 };
--- a/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
+++ b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
@@ -1,110 +1,47 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 
 "use strict";
 
-XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
-                                   "@mozilla.org/browser/aboutnewtab-service;1",
-                                   "nsIAboutNewTabService");
-
 const NEWTAB_URI_1 = "webext-newtab-1.html";
-const NEWTAB_URI_2 = "webext-newtab-2.html";
-const NEWTAB_URI_3 = "webext-newtab-3.html";
-
-add_task(async function test_multiple_extensions_overriding_newtab_page() {
-  is(aboutNewTabService.newTabURL, "about:newtab",
-     "Default newtab url should be about:newtab");
-
-  let ext1 = ExtensionTestUtils.loadExtension({
-    manifest: {"chrome_url_overrides": {}},
-  });
-
-  let ext2 = ExtensionTestUtils.loadExtension({
-    manifest: {"chrome_url_overrides": {newtab: NEWTAB_URI_1}},
-  });
-
-  let ext3 = ExtensionTestUtils.loadExtension({
-    manifest: {"chrome_url_overrides": {newtab: NEWTAB_URI_2}},
-  });
-
-  let ext4 = ExtensionTestUtils.loadExtension({
-    manifest: {"chrome_url_overrides": {newtab: NEWTAB_URI_3}},
-  });
-
-  await ext1.startup();
-
-  is(aboutNewTabService.newTabURL, "about:newtab",
-       "Default newtab url should still be about:newtab");
-
-  await ext2.startup();
-
-  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_1),
-     "Newtab url should be overriden by the second extension.");
-
-  await ext1.unload();
-
-  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_1),
-     "Newtab url should still be overriden by the second extension.");
-
-  await ext3.startup();
-
-  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_1),
-     "Newtab url should still be overriden by the second extension.");
-
-  await ext2.unload();
-
-  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_2),
-     "Newtab url should be overriden by the third extension.");
-
-  await ext4.startup();
-
-  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_2),
-     "Newtab url should be overriden by the third extension.");
-
-  await ext4.unload();
-
-  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_2),
-     "Newtab url should be overriden by the third extension.");
-
-  await ext3.unload();
-
-  is(aboutNewTabService.newTabURL, "about:newtab",
-     "Newtab url should be reset to about:newtab");
-});
 
 add_task(async function test_sending_message_from_newtab_page() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "chrome_url_overrides": {
-        newtab: NEWTAB_URI_2,
+        newtab: NEWTAB_URI_1,
       },
     },
+    useAddonManager: "temporary",
     files: {
-      [NEWTAB_URI_2]: `
+      [NEWTAB_URI_1]: `
         <!DOCTYPE html>
         <head>
           <meta charset="utf-8"/></head>
         <html>
           <body>
             <script src="newtab.js"></script>
           </body>
         </html>
       `,
 
       "newtab.js": function() {
         window.onload = () => {
-          browser.test.sendMessage("from-newtab-page");
+          browser.test.sendMessage("from-newtab-page", window.location.href);
         };
       },
     },
   });
 
   await extension.startup();
 
   // Simulate opening the newtab open as a user would.
   BrowserOpenTab();
 
-  await extension.awaitMessage("from-newtab-page");
+  let url = await extension.awaitMessage("from-newtab-page");
+  ok(url.endsWith(NEWTAB_URI_1),
+     "Newtab url is overriden by the extension.");
+
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
   await extension.unload();
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js
@@ -0,0 +1,123 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "Management", () => {
+  const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
+  return Management;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+                                   "@mozilla.org/browser/aboutnewtab-service;1",
+                                   "nsIAboutNewTabService");
+
+Cu.import("resource://testing-common/AddonTestUtils.jsm");
+
+const {
+  createAppInfo,
+  promiseShutdownManager,
+  promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+function awaitEvent(eventName) {
+  return new Promise(resolve => {
+    Management.once(eventName, (e, ...args) => resolve(...args));
+  });
+}
+
+add_task(async function test_multiple_extensions_overriding_newtab_page() {
+  const NEWTAB_URI_1 = "webext-newtab-1.html";
+  const NEWTAB_URI_2 = "webext-newtab-2.html";
+  const EXT_2_ID = "ext2@tests.mozilla.org";
+
+  equal(aboutNewTabService.newTabURL, "about:newtab",
+     "Default newtab url is about:newtab");
+
+  await promiseStartupManager();
+
+  let ext1 = ExtensionTestUtils.loadExtension({
+    manifest: {"chrome_url_overrides": {}},
+    useAddonManager: "temporary",
+  });
+
+  let ext2 = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "chrome_url_overrides": {newtab: NEWTAB_URI_1},
+      applications: {
+        gecko: {
+          id: EXT_2_ID,
+        },
+      },
+    },
+    useAddonManager: "temporary",
+  });
+
+  let ext3 = ExtensionTestUtils.loadExtension({
+    manifest: {"chrome_url_overrides": {newtab: NEWTAB_URI_2}},
+    useAddonManager: "temporary",
+  });
+
+  await ext1.startup();
+  equal(aboutNewTabService.newTabURL, "about:newtab",
+       "Default newtab url is still about:newtab");
+
+  await ext2.startup();
+  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_1),
+     "Newtab url is overriden by the second extension.");
+
+  // Disable the second extension.
+  let addon = await AddonManager.getAddonByID(EXT_2_ID);
+  let disabledPromise = awaitEvent("shutdown");
+  addon.userDisabled = true;
+  await disabledPromise;
+  equal(aboutNewTabService.newTabURL, "about:newtab",
+        "Newtab url is about:newtab after second extension is disabled.");
+
+  // Re-enable the second extension.
+  let enabledPromise = awaitEvent("ready");
+  addon.userDisabled = false;
+  await enabledPromise;
+  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_1),
+     "Newtab url is overriden by the second extension.");
+
+  await ext1.unload();
+  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_1),
+     "Newtab url is still overriden by the second extension.");
+
+  await ext3.startup();
+  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_2),
+   "Newtab url is overriden by the third extension.");
+
+  // Disable the second extension.
+  disabledPromise = awaitEvent("shutdown");
+  addon.userDisabled = true;
+  await disabledPromise;
+  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_2),
+   "Newtab url is still overriden by the third extension.");
+
+  // Re-enable the second extension.
+  enabledPromise = awaitEvent("ready");
+  addon.userDisabled = false;
+  await enabledPromise;
+  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_2),
+   "Newtab url is still overriden by the third extension.");
+
+  await ext3.unload();
+  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_1),
+     "Newtab url reverts to being overriden by the second extension.");
+
+  await ext2.unload();
+  equal(aboutNewTabService.newTabURL, "about:newtab",
+     "Newtab url is reset to about:newtab");
+
+  await promiseShutdownManager();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js
@@ -0,0 +1,116 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "Management", () => {
+  const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
+  return Management;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+                                   "@mozilla.org/browser/aboutnewtab-service;1",
+                                   "nsIAboutNewTabService");
+
+Cu.import("resource://testing-common/AddonTestUtils.jsm");
+
+const {
+  createAppInfo,
+  createTempWebExtensionFile,
+  promiseCompleteAllInstalls,
+  promiseFindAddonUpdates,
+  promiseShutdownManager,
+  promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+// Allow for unsigned addons.
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+add_task(async function test_url_overrides_newtab_update() {
+  const EXTENSION_ID = "test_url_overrides_update@tests.mozilla.org";
+  const NEWTAB_URI = "webext-newtab-1.html";
+  const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+
+  const testServer = createHttpServer();
+  const port = testServer.identity.primaryPort;
+
+  // The test extension uses an insecure update url.
+  Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+  testServer.registerPathHandler("/test_update.json", (request, response) => {
+    response.write(`{
+      "addons": {
+        "${EXTENSION_ID}": {
+          "updates": [
+            {
+              "version": "2.0",
+              "update_link": "http://localhost:${port}/addons/test_url_overrides-2.0.xpi"
+            }
+          ]
+        }
+      }
+    }`);
+  });
+
+  let webExtensionFile = createTempWebExtensionFile({
+    manifest: {
+      version: "2.0",
+      applications: {
+        gecko: {
+          id: EXTENSION_ID,
+        },
+      },
+    },
+  });
+
+  testServer.registerFile("/addons/test_url_overrides-2.0.xpi", webExtensionFile);
+
+  await promiseStartupManager();
+
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "permanent",
+    manifest: {
+      "version": "1.0",
+      "applications": {
+        "gecko": {
+          "id": EXTENSION_ID,
+          "update_url": `http://localhost:${port}/test_update.json`,
+        },
+      },
+      chrome_url_overrides: {newtab: NEWTAB_URI},
+    },
+  });
+
+  let defaultNewTabURL = aboutNewTabService.newTabURL;
+  equal(aboutNewTabService.newTabURL, defaultNewTabURL,
+        `Default newtab url is ${defaultNewTabURL}.`);
+
+  await extension.startup();
+
+  equal(extension.version, "1.0", "The installed addon has the expected version.");
+  ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI),
+     "Newtab url is overriden by the extension.");
+
+  let update = await promiseFindAddonUpdates(extension.addon);
+  let install = update.updateAvailable;
+
+  await promiseCompleteAllInstalls([install]);
+
+  await extension.awaitStartup();
+
+  equal(extension.version, "2.0", "The updated addon has the expected version.");
+  equal(aboutNewTabService.newTabURL, defaultNewTabURL,
+        "Newtab url reverted to the default after update.");
+
+  await extension.unload();
+
+  await promiseShutdownManager();
+});
--- a/browser/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/browser/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -1,8 +1,11 @@
 [test_ext_bookmarks.js]
 [test_ext_browsingData.js]
 [test_ext_browsingData_cookies_cache.js]
 [test_ext_browsingData_downloads.js]
 [test_ext_browsingData_passwords.js]
 [test_ext_browsingData_settings.js]
+[test_ext_geckoProfiler_control.js]
 [test_ext_history.js]
-[test_ext_geckoProfiler_control.js]
+[test_ext_url_overrides_newtab.js]
+[test_ext_url_overrides_newtab_update.js]
+