Bug 1334096 Show permissions prompts when a sideloaded extension is enabled draft
authorAndrew Swan <aswan@mozilla.com>
Wed, 15 Feb 2017 15:40:56 -0800
changeset 488796 446cfeddd91b875a7ecc9f45ea1c173d9f17b046
parent 485446 bf6b9caab2c7eb3ebc642afd82bc19598829c6f3
child 546837 3d446329ddb99e56425345770533d081b756932c
push id46644
push useraswan@mozilla.com
push dateThu, 23 Feb 2017 19:36:03 +0000
bugs1334096
milestone54.0a1
Bug 1334096 Show permissions prompts when a sideloaded extension is enabled MozReview-Commit-ID: 1yXgkBg6W1p
browser/base/content/test/general/browser_extension_sideloading.js
browser/modules/ExtensionsUI.jsm
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/extensions.xml
--- a/browser/base/content/test/general/browser_extension_sideloading.js
+++ b/browser/base/content/test/general/browser_extension_sideloading.js
@@ -26,16 +26,17 @@ class MockAddon {
   }
 
   get userDisabled() {
     return this._userDisabled;
   }
 
   set userDisabled(val) {
     this._userDisabled = val;
+    AddonManagerPrivate.callAddonListeners(val ? "onDisabled" : "onEnabled", this);
     let fn = setCallbacks.get(this);
     if (fn) {
       setCallbacks.delete(this);
       fn(val);
     }
     return val;
   }
 
@@ -128,17 +129,43 @@ add_task(function* () {
     userDisabled: true,
     seen: false,
     userPermissions: {
       permissions: [],
       hosts: [],
     },
   });
 
-  let provider = new MockProvider(mock1, mock2);
+  const ID3 = "addon3@tests.mozilla.org";
+  let mock3 = new MockAddon({
+    id: ID3,
+    name: "Test 3",
+    isWebExtension: true,
+    userDisabled: true,
+    seen: false,
+    userPermissions: {
+      permissions: [],
+      hosts: ["<all_urls>"],
+    }
+  });
+
+  const ID4 = "addon4@tests.mozilla.org";
+  let mock4 = new MockAddon({
+    id: ID4,
+    name: "Test 4",
+    isWebExtension: true,
+    userDisabled: true,
+    seen: false,
+    userPermissions: {
+      permissions: [],
+      hosts: ["<all_urls>"],
+    }
+  });
+
+  let provider = new MockProvider(mock1, mock2, mock3, mock4);
   AddonManagerPrivate.registerProvider(provider, [{
     id: "extension",
     name: "Extensions",
     uiPriority: 4000,
     flags: AddonManager.TYPE_UI_VIEW_LIST |
            AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL,
   }]);
   registerCleanupFunction(function*() {
@@ -173,17 +200,17 @@ add_task(function* () {
   // Check for the addons badge on the hamburger menu
   let menuButton = document.getElementById("PanelUI-menu-button");
   is(menuButton.getAttribute("badge-status"), "addon-alert", "Should have addon alert badge");
 
   // Find the menu entries for sideloaded extensions
   yield PanelUI.show();
 
   let addons = document.getElementById("PanelUI-footer-addons");
-  is(addons.children.length, 2, "Have 2 menu entries for sideloaded extensions");
+  is(addons.children.length, 4, "Have 4 menu entries for sideloaded extensions");
 
   // Click the first sideloaded extension
   let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
   addons.children[0].click();
 
   // When we get the permissions prompt, we should be at the extensions
   // list in about:addons
   let panel = yield popupPromise;
@@ -199,50 +226,119 @@ add_task(function* () {
   is(icon, ICON_URL, "Permissions notification has the addon icon");
 
   let disablePromise = promiseSetDisabled(mock1);
   panel.secondaryButton.click();
 
   let value = yield disablePromise;
   is(value, true, "Addon should remain disabled");
 
-  let [addon1, addon2] = yield AddonManager.getAddonsByIDs([ID1, ID2]);
+  let [addon1, addon2, addon3, addon4] = yield AddonManager.getAddonsByIDs([ID1, ID2, ID3, ID4]);
   ok(addon1.seen, "Addon should be marked as seen");
   is(addon1.userDisabled, true, "Addon 1 should still be disabled");
   is(addon2.userDisabled, true, "Addon 2 should still be disabled");
+  is(addon3.userDisabled, true, "Addon 3 should still be disabled");
+  is(addon4.userDisabled, true, "Addon 4 should still be disabled");
 
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
-  // Should still have 1 entry in the hamburger menu
+  // Should still have 3 entries in the hamburger menu
   yield PanelUI.show();
 
   addons = document.getElementById("PanelUI-footer-addons");
-  is(addons.children.length, 1, "Have 1 menu entry for sideloaded extensions");
+  is(addons.children.length, 3, "Have 3 menu entries for sideloaded extensions");
 
   // Click the second sideloaded extension and wait for the notification
   popupPromise = promisePopupNotificationShown("addon-webext-permissions");
   addons.children[0].click();
   panel = yield popupPromise;
 
-  isnot(menuButton.getAttribute("badge-status"), "addon-alert", "Should no longer have addon alert badge");
-
   // Again we should be at the extentions list in about:addons
   is(gBrowser.currentURI.spec, "about:addons", "Foreground tab is at about:addons");
 
   win = gBrowser.selectedBrowser.contentWindow;
   ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
   is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
 
   // Check the notification contents, this time accept the install
   icon = panel.getAttribute("icon");
   is(icon, DEFAULT_ICON_URL, "Permissions notification has the default icon");
   disablePromise = promiseSetDisabled(mock2);
   panel.button.click();
 
   value = yield disablePromise;
   is(value, false, "Addon should be set to enabled");
 
-  [addon1, addon2] = yield AddonManager.getAddonsByIDs([ID1, ID2]);
+  [addon1, addon2, addon3, addon4] = yield AddonManager.getAddonsByIDs([ID1, ID2, ID3, ID4]);
   is(addon1.userDisabled, true, "Addon 1 should still be disabled");
   is(addon2.userDisabled, false, "Addon 2 should now be enabled");
+  is(addon3.userDisabled, true, "Addon 3 should still be disabled");
+  is(addon4.userDisabled, true, "Addon 4 should still be disabled");
+
+  // Should still have 2 entries in the hamburger menu
+  yield PanelUI.show();
+
+  addons = document.getElementById("PanelUI-footer-addons");
+  is(addons.children.length, 2, "Have 2 menu entries for sideloaded extensions");
+
+  // Close the hamburger menu and go directly to the addons manager
+  yield PanelUI.hide();
+
+  win = yield BrowserOpenAddonsMgr(VIEW);
+
+  let list = win.document.getElementById("addon-list");
+
+  // Make sure XBL bindings are applied
+  list.clientHeight;
+
+  let item = list.children.find(_item => _item.value == ID3);
+  ok(item, "Found entry for sideloaded extension in about:addons");
+  item.scrollIntoView({behavior: "instant"});
+
+  ok(is_visible(item._enableBtn), "Enable button is visible for sideloaded extension");
+  ok(is_hidden(item._disableBtn), "Disable button is not visible for sideloaded extension");
+
+  // When clicking enable we should see the permissions notification
+  popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  BrowserTestUtils.synthesizeMouseAtCenter(item._enableBtn, {},
+                                           gBrowser.selectedBrowser);
+  panel = yield popupPromise;
+
+  // Accept the permissions
+  disablePromise = promiseSetDisabled(mock3);
+  panel.button.click();
+  value = yield disablePromise;
+  is(value, false, "userDisabled should be set on addon 3");
+
+  addon3 = yield AddonManager.getAddonByID(ID3);
+  is(addon3.userDisabled, false, "Addon 3 should be enabled");
+
+  // Should still have 1 entry in the hamburger menu
+  yield PanelUI.show();
+
+  addons = document.getElementById("PanelUI-footer-addons");
+  is(addons.children.length, 1, "Have 1 menu entry for sideloaded extensions");
+
+  // Close the hamburger menu and go to the detail page for this addon
+  yield PanelUI.hide();
+
+  win = yield BrowserOpenAddonsMgr(`addons://detail/${encodeURIComponent(ID4)}`);
+  let button = win.document.getElementById("detail-enable-btn");
+
+  // When clicking enable we should see the permissions notification
+  popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  BrowserTestUtils.synthesizeMouseAtCenter(button, {},
+                                           gBrowser.selectedBrowser);
+  panel = yield popupPromise;
+
+  // Accept the permissions
+  disablePromise = promiseSetDisabled(mock4);
+  panel.button.click();
+  value = yield disablePromise;
+  is(value, false, "userDisabled should be set on addon 4");
+
+  addon4 = yield AddonManager.getAddonByID(ID4);
+  is(addon4.userDisabled, false, "Addon 4 should be enabled");
+
+  isnot(menuButton.getAttribute("badge-status"), "addon-alert", "Should no longer have addon alert badge");
 
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -27,16 +27,17 @@ const DEFAULT_EXTENSION_ICON = "chrome:/
 const BROWSER_PROPERTIES = "chrome://browser/locale/browser.properties";
 const BRAND_PROPERTIES = "chrome://browser/locale/brand.properties";
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 this.ExtensionsUI = {
   sideloaded: new Set(),
   updates: new Set(),
+  sideloadListener: null,
 
   init() {
     Services.obs.addObserver(this, "webextension-permission-prompt", false);
     Services.obs.addObserver(this, "webextension-update-permissions", false);
     Services.obs.addObserver(this, "webextension-install-notify", false);
 
     this._checkForSideloaded();
   },
@@ -48,16 +49,35 @@ this.ExtensionsUI = {
       let sideloaded = addons.filter(
         addon => addon.seen === false && (addon.permissions & AddonManager.PERM_CAN_ENABLE));
 
       if (!sideloaded.length) {
         return;
       }
 
       if (WEBEXT_PERMISSION_PROMPTS) {
+        if (!this.sideloadListener) {
+          this.sideloadListener = {
+            onEnabled: addon => {
+              if (!this.sideloaded.has(addon)) {
+                return;
+              }
+
+              this.sideloaded.delete(addon);
+              this.emit("change");
+
+              if (this.sideloaded.size == 0) {
+                AddonManager.removeAddonListener(this.sideloadListener);
+                this.sideloadListener = null;
+              }
+            },
+          };
+          AddonManager.addAddonListener(this.sideloadListener);
+        }
+
         for (let addon of sideloaded) {
           this.sideloaded.add(addon);
         }
         this.emit("change");
       } else {
         // This and all the accompanying about:newaddon code can eventually
         // be removed.  See bug 1331521.
         let win = RecentWindow.getMostRecentBrowserWindow();
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -1268,16 +1268,36 @@ var gViewController = {
       isEnabled(aAddon) {
         if (!aAddon)
           return false;
         let addonType = AddonManager.addonTypes[aAddon.type];
         return (!(addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) &&
                 hasPermission(aAddon, "enable"));
       },
       doCommand(aAddon) {
+        if (aAddon.isWebExtension && !aAddon.seen && WEBEXT_PERMISSION_PROMPTS) {
+          let perms = aAddon.userPermissions;
+          if (perms.hosts.length > 0 || perms.permissions.length > 0) {
+            let subject = {
+              wrappedJSObject: {
+                target: getBrowserElement(),
+                info: {
+                  type: "sideload",
+                  addon: aAddon,
+                  icon: aAddon.iconURL,
+                  permissions: perms,
+                  resolve() { aAddon.userDisabled = false },
+                  reject() {},
+                },
+              },
+            };
+            Services.obs.notifyObservers(subject, "webextension-permission-prompt", null);
+            return;
+          }
+        }
         aAddon.userDisabled = false;
       },
       getTooltip(aAddon) {
         if (!aAddon)
           return "";
         if (aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE)
           return gStrings.ext.GetStringFromName("enableAddonRestartRequiredTooltip");
         return gStrings.ext.GetStringFromName("enableAddonTooltip");
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -1081,17 +1081,23 @@
                                                 "relnotes");
       </field>
 
       <property name="userDisabled">
         <getter><![CDATA[
           return this.mAddon.userDisabled;
         ]]></getter>
         <setter><![CDATA[
-          this.mAddon.userDisabled = val;
+          if (val === true) {
+            gViewController.commands["cmd_disableItem"].doCommand(this.mAddon);
+          } else if (val === false) {
+            gViewController.commands["cmd_enableItem"].doCommand(this.mAddon);
+          } else {
+            this.mAddon.userDisabled = val;
+          }
         ]]></setter>
       </property>
 
       <property name="includeUpdate">
         <getter><![CDATA[
           return this._includeUpdate.checked && !!this.mManualUpdate;
         ]]></getter>
         <setter><![CDATA[