Bug 1330823 Part 2 permission prompts for interactive extension updates draft
authorAndrew Swan <aswan@mozilla.com>
Wed, 01 Feb 2017 19:51:10 -0800
changeset 469961 04c7958f9676dfd2ee5e7908a9c0c5984d51e737
parent 469960 6d2003fa6b8efd8ab9bebab769c0b5e2d8dd2cd1
child 544348 f393dc4065e05c6e90b9bfc9d6b85d19b2dfafb1
push id43892
push useraswan@mozilla.com
push dateFri, 03 Feb 2017 01:00:51 +0000
bugs1330823
milestone54.0a1
Bug 1330823 Part 2 permission prompts for interactive extension updates MozReview-Commit-ID: HI41MbwkqW6
browser/base/content/test/general/browser_extension_update.js
toolkit/mozapps/extensions/content/extensions.js
--- a/browser/base/content/test/general/browser_extension_update.js
+++ b/browser/base/content/test/general/browser_extension_update.js
@@ -1,13 +1,28 @@
 const {AddonManagerPrivate} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
 
 const URL_BASE = "https://example.com/browser/browser/base/content/test/general";
 const ID = "update@tests.mozilla.org";
 
+function promiseInstallAddon(url) {
+  return AddonManager.getInstallForURL(url, null, "application/x-xpinstall")
+                     .then(install => {
+                       ok(install, "Created install");
+                       return new Promise(resolve => {
+                         install.addListener({
+                           onInstallEnded(_install, addon) {
+                             resolve(addon);
+                           },
+                         });
+                         install.install();
+                       });
+                     });
+}
+
 function promiseViewLoaded(tab, viewid) {
   let win = tab.linkedBrowser.contentWindow;
   if (win.gViewController && !win.gViewController.isLoading &&
       win.gViewController.currentViewId == viewid) {
      return Promise.resolve();
   }
 
   return new Promise(resolve => {
@@ -39,80 +54,60 @@ function promisePopupNotificationShown(n
   });
 }
 
 function getBadgeStatus() {
   let menuButton = document.getElementById("PanelUI-menu-button");
   return menuButton.getAttribute("badge-status");
 }
 
-function promiseUpdateDownloaded(addon) {
+function promiseInstallEvent(addon, event) {
   return new Promise(resolve => {
-    let listener = {
-      onDownloadEnded(install) {
-        if (install.addon.id == addon.id) {
-          AddonManager.removeInstallListener(listener);
-          resolve();
-        }
-      },
+    let listener = {};
+    listener[event] = (install, ...args) => {
+      if (install.addon.id == addon.id) {
+        AddonManager.removeInstallListener(listener);
+        resolve(...args);
+      }
     };
     AddonManager.addInstallListener(listener);
   });
 }
 
-function promiseUpgrade(addon) {
-  return new Promise(resolve => {
-    let listener = {
-      onInstallEnded(install, newAddon) {
-        if (newAddon.id == addon.id) {
-          AddonManager.removeInstallListener(listener);
-          resolve(newAddon);
-        }
-      },
-    };
-    AddonManager.addInstallListener(listener);
-  });
-}
-
-add_task(function* () {
-  yield SpecialPowers.pushPrefEnv({set: [
-    // Turn on background updates
-    ["extensions.update.enabled", true],
-
-    // Point updates to the local mochitest server
-    ["extensions.update.background.url", `${URL_BASE}/browser_webext_update.json`],
-
+// Set some prefs that apply to all the tests in this file
+add_task(function setup() {
+  return SpecialPowers.pushPrefEnv({set: [
     // We don't have pre-pinned certificates for the local mochitest server
     ["extensions.install.requireBuiltInCerts", false],
     ["extensions.update.requireBuiltInCerts", false],
 
     // XXX remove this when prompts are enabled by default
     ["extensions.webextPermissionPrompts", true],
   ]});
+});
+
+add_task(function* test_background_update() {
+  yield SpecialPowers.pushPrefEnv({set: [
+    // Turn on background updates
+    ["extensions.update.enabled", true],
+
+    // Point updates to the local mochitest server
+    ["extensions.update.background.url", `${URL_BASE}/browser_webext_update.json`],
+  ]});
 
   // Install version 1.0 of the test extension
-  let url1 = `${URL_BASE}/browser_webext_update1.xpi`;
-  let install = yield AddonManager.getInstallForURL(url1, null, "application/x-xpinstall");
-  ok(install, "Created install");
-
-  let addon = yield new Promise(resolve => {
-    install.addListener({
-      onInstallEnded(_install, _addon) {
-        resolve(_addon);
-      },
-    });
-    install.install();
-  });
+  let addon = yield promiseInstallAddon(`${URL_BASE}/browser_webext_update1.xpi`);
 
   ok(addon, "Addon was installed");
   is(getBadgeStatus(), "", "Should not start out with an addon alert badge");
 
   // Trigger an update check and wait for the update for this addon
   // to be downloaded.
-  let updatePromise = promiseUpdateDownloaded(addon);
+  let updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
+
   AddonManagerPrivate.backgroundUpdateCheck();
   yield updatePromise;
 
   is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
 
   // Find the menu entry for the update
   yield PanelUI.show();
 
@@ -147,17 +142,17 @@ add_task(function* () {
   is(getBadgeStatus(), "", "Addon alert badge should be gone");
 
   yield PanelUI.show();
   addons = document.getElementById("PanelUI-footer-addons");
   is(addons.children.length, 0, "Update menu entries should be gone");
   yield PanelUI.hide();
 
   // Re-check for an update
-  updatePromise = promiseUpdateDownloaded(addon);
+  updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
   yield AddonManagerPrivate.backgroundUpdateCheck();
   yield updatePromise;
 
   is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
 
   // Find the menu entry for the update
   yield PanelUI.show();
 
@@ -174,19 +169,134 @@ add_task(function* () {
   is(tab.linkedBrowser.currentURI.spec, "about:addons");
 
   yield promiseViewLoaded(tab, VIEW);
   win = tab.linkedBrowser.contentWindow;
   ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
   is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
 
   // Wait for the permission prompt and accept it this time
-  updatePromise = promiseUpgrade(addon);
+  updatePromise = promiseInstallEvent(addon, "onInstallEnded");
   panel = yield popupPromise;
   panel.button.click();
 
   addon = yield updatePromise;
   is(addon.version, "2.0", "Should have upgraded to the new version");
 
   yield BrowserTestUtils.removeTab(tab);
 
   is(getBadgeStatus(), "", "Addon alert badge should be gone");
+
+  addon.uninstall();
+  yield SpecialPowers.popPrefEnv();
 });
+
+// Helper function to test a specific scenario for interactive updates.
+// `checkFn` is a callable that triggers a check for updates.
+// `autoUpdate` specifies whether the test should be run with
+// updates applied automatically or not.
+function* interactiveUpdateTest(autoUpdate, checkFn) {
+  yield SpecialPowers.pushPrefEnv({set: [
+    ["extensions.update.autoUpdateDefault", autoUpdate],
+
+    // Point updates to the local mochitest server
+    ["extensions.update.url", `${URL_BASE}/browser_webext_update.json`],
+  ]});
+
+  // Trigger an update check, manually applying the update if we're testing
+  // without auto-update.
+  function* triggerUpdate(win, addon) {
+    let manualUpdatePromise;
+    if (!autoUpdate) {
+      manualUpdatePromise = new Promise(resolve => {
+        let listener = {
+          onNewInstall() {
+            AddonManager.removeInstallListener(listener);
+            resolve();
+          },
+        };
+        AddonManager.addInstallListener(listener);
+      });
+    }
+
+    checkFn(win, addon);
+
+    if (manualUpdatePromise) {
+      yield manualUpdatePromise;
+
+      let item = win.document.getElementById("addon-list")
+                    .children.find(_item => _item.value == ID);
+      EventUtils.synthesizeMouseAtCenter(item._updateBtn, {}, win);
+    }
+  }
+
+  // Install version 1.0 of the test extension
+  let addon = yield promiseInstallAddon(`${URL_BASE}/browser_webext_update1.xpi`);
+  ok(addon, "Addon was installed");
+  is(addon.version, "1.0", "Version 1 of the addon is installed");
+
+  // Open add-ons manager and navigate to extensions list
+  let loadPromise = new Promise(resolve => {
+    let listener = (subject, topic) => {
+      if (subject.location.href == "about:addons") {
+        Services.obs.removeObserver(listener, topic);
+        resolve(subject);
+      }
+    };
+    Services.obs.addObserver(listener, "EM-loaded", false);
+  });
+  let tab = gBrowser.addTab("about:addons");
+  gBrowser.selectedTab = tab;
+  let win = yield loadPromise;
+
+  const VIEW = "addons://list/extension";
+  let viewPromise = promiseViewLoaded(tab, VIEW);
+  win.loadView(VIEW);
+  yield viewPromise;
+
+  // Trigger an update check
+  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  yield triggerUpdate(win, addon);
+  let panel = yield popupPromise;
+
+  // Click the cancel button, wait to see the cancel event
+  let cancelPromise = promiseInstallEvent(addon, "onInstallCancelled");
+  panel.secondaryButton.click();
+  yield cancelPromise;
+
+  addon = yield AddonManager.getAddonByID(ID);
+  is(addon.version, "1.0", "Should still be running the old version");
+
+  // Trigger a new update check
+  popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  yield triggerUpdate(win, addon);
+
+  // This time, accept the upgrade
+  let updatePromise = promiseInstallEvent(addon, "onInstallEnded");
+  panel = yield popupPromise;
+  panel.button.click();
+
+  addon = yield updatePromise;
+  is(addon.version, "2.0", "Should have upgraded");
+
+  yield BrowserTestUtils.removeTab(tab);
+  addon.uninstall();
+  yield SpecialPowers.popPrefEnv();
+}
+
+// Invoke the "Check for Updates" menu item
+function checkAll(win) {
+  win.gViewController.doCommand("cmd_findAllUpdates");
+}
+
+// Test "Check for Updates" with both auto-update settings
+add_task(() => interactiveUpdateTest(true, checkAll));
+add_task(() => interactiveUpdateTest(false, checkAll));
+
+
+// Invoke an invidual extension's "Find Updates" menu item
+function checkOne(win, addon) {
+  win.gViewController.doCommand("cmd_findItemUpdates", addon);
+}
+
+// Test "Find Updates" with both auto-update settings
+add_task(() => interactiveUpdateTest(true, checkOne));
+add_task(() => interactiveUpdateTest(false, checkOne));
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -16,16 +16,18 @@ var Cr = Components.results;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/DownloadUtils.jsm");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", "resource:///modules/E10SUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+                                  "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
                                   "resource://gre/modules/ExtensionParent.jsm");
 
 const CONSTANTS = {};
 Cu.import("resource://gre/modules/addons/AddonConstants.jsm", CONSTANTS);
 const SIGNING_REQUIRED = CONSTANTS.REQUIRE_SIGNING ?
                          true :
                          Services.prefs.getBoolPref("xpinstall.signatures.required");
@@ -33,16 +35,19 @@ const SIGNING_REQUIRED = CONSTANTS.REQUI
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Experiments",
   "resource:///modules/experiments/Experiments.jsm");
 
+XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
+                                      "extensions.webextPermissionPrompts", false);
+
 const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
 const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
 const PREF_XPI_ENABLED = "xpinstall.enabled";
 const PREF_MAXRESULTS = "extensions.getAddons.maxResults";
 const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
 const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled";
 const PREF_UI_TYPE_HIDDEN = "extensions.ui.%TYPE%.hidden";
 const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
@@ -689,16 +694,50 @@ var gEventManager = {
     this.refreshGlobalWarning();
   },
 
   onUpdateModeChanged() {
     this.refreshAutoUpdateDefault();
   }
 };
 
+function attachUpdateHandler(install) {
+  if (!WEBEXT_PERMISSION_PROMPTS) {
+    return;
+  }
+
+  install.promptHandler = (info) => {
+    let oldPerms = info.existingAddon.userPermissions || {hosts: [], permissions: []};
+    let newPerms = info.addon.userPermissions;
+
+    let difference = Extension.comparePermissions(oldPerms, newPerms);
+
+    // If there are no new permissions, just proceed
+    if (difference.hosts.length == 0 && difference.permissions.length == 0) {
+      return Promise.resolve();
+    }
+
+    return new Promise((resolve, reject) => {
+      let subject = {
+        wrappedJSObject: {
+          target: getBrowserElement(),
+          info: {
+            type: "update",
+            addon: info.addon,
+            icon: info.addon.icon,
+            permissions: difference,
+            resolve,
+            reject,
+          },
+        },
+      };
+      Services.obs.notifyObservers(subject, "webextension-permission-prompt", null);
+    });
+  };
+}
 
 var gViewController = {
   viewPort: null,
   currentViewId: "",
   currentViewObj: null,
   currentViewRequest: 0,
   viewObjects: {},
   viewChangeCallback: null,
@@ -1076,16 +1115,20 @@ var gViewController = {
           }
         }
 
         var updateInstallListener = {
           onDownloadFailed() {
             pendingChecks--;
             updateStatus();
           },
+          onInstallCancelled() {
+            pendingChecks--;
+            updateStatus();
+          },
           onInstallFailed() {
             pendingChecks--;
             updateStatus();
           },
           onInstallEnded(aInstall, aAddon) {
             pendingChecks--;
             numUpdated++;
             if (isPending(aInstall.existingAddon, "upgrade"))
@@ -1093,16 +1136,17 @@ var gViewController = {
             updateStatus();
           }
         };
 
         var updateCheckListener = {
           onUpdateAvailable(aAddon, aInstall) {
             gEventManager.delegateAddonEvent("onUpdateAvailable",
                                              [aAddon, aInstall]);
+            attachUpdateHandler(aInstall);
             if (AddonManager.shouldAutoUpdate(aAddon)) {
               aInstall.addListener(updateInstallListener);
               aInstall.install();
             } else {
               pendingChecks--;
               numManualUpdates++;
               updateStatus();
             }
@@ -1138,16 +1182,17 @@ var gViewController = {
           return false;
         return hasPermission(aAddon, "upgrade");
       },
       doCommand(aAddon) {
         var listener = {
           onUpdateAvailable(aAddon, aInstall) {
             gEventManager.delegateAddonEvent("onUpdateAvailable",
                                              [aAddon, aInstall]);
+            attachUpdateHandler(aInstall);
             if (AddonManager.shouldAutoUpdate(aAddon))
               aInstall.install();
           },
           onNoUpdateAvailable(aAddon) {
             gEventManager.delegateAddonEvent("onNoUpdateAvailable",
                                              [aAddon]);
           }
         };