Bug 1336085 Apply updates with only non-promptable permissions automatically draft
authorAndrew Swan <aswan@mozilla.com>
Wed, 08 Feb 2017 12:09:49 -0800
changeset 480686 32e216a6a3b3a49dffa8726c83b46243f393a057
parent 479958 e677ba018b22558fef1d07b74d416fd3a28a5dc3
child 480697 d000b3d66aedf711381f7e96b1bfb882962e635b
push id44630
push useraswan@mozilla.com
push dateWed, 08 Feb 2017 20:25:09 +0000
bugs1336085
milestone54.0a1
Bug 1336085 Apply updates with only non-promptable permissions automatically MozReview-Commit-ID: 8Sc4WhstrCg
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_extension_sideloading.js
browser/base/content/test/general/browser_extension_update.js
browser/base/content/test/general/browser_webext_update.json
browser/base/content/test/general/browser_webext_update_perms1.xpi
browser/base/content/test/general/browser_webext_update_perms2.xpi
browser/modules/ExtensionsUI.jsm
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -76,16 +76,18 @@ support-files =
   zoom_test.html
   file_install_extensions.html
   browser_webext_permissions.xpi
   browser_webext_nopermissions.xpi
   browser_webext_update1.xpi
   browser_webext_update2.xpi
   browser_webext_update_icon1.xpi
   browser_webext_update_icon2.xpi
+  browser_webext_update_perms1.xpi
+  browser_webext_update_perms2.xpi
   browser_webext_update.json
   !/image/test/mochitest/blue.png
   !/toolkit/content/tests/browser/common/mockTransfer.js
   !/toolkit/modules/tests/browser/metadata_*.html
   !/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
--- a/browser/base/content/test/general/browser_extension_sideloading.js
+++ b/browser/base/content/test/general/browser_extension_sideloading.js
@@ -66,35 +66,16 @@ class MockProvider {
     let addons = [];
     if (!types || types.includes("extension")) {
       addons = [...this.addons];
     }
     callback(addons);
   }
 }
 
-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 => {
-    function listener() {
-      if (win.gViewController.currentViewId != viewid) {
-        return;
-      }
-      win.document.removeEventListener("ViewChanged", listener);
-      resolve();
-    }
-    win.document.addEventListener("ViewChanged", listener);
-  });
-}
-
 function promisePopupNotificationShown(name) {
   return new Promise(resolve => {
     function popupshown() {
       let notification = PopupNotifications.getNotification(name);
       if (!notification) {
         return;
       }
 
@@ -164,16 +145,27 @@ add_task(function* () {
     AddonManagerPrivate.unregisterProvider(provider);
 
     // clear out ExtensionsUI state about sideloaded extensions so
     // subsequent tests don't get confused.
     ExtensionsUI.sideloaded.clear();
     ExtensionsUI.emit("change");
   });
 
+  // Navigate away from the starting page to force about:addons to load
+  // in a new tab during the tests below.
+  gBrowser.selectedBrowser.loadURI("about:robots");
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+  registerCleanupFunction(function*() {
+    // Return to about:blank when we're done
+    gBrowser.selectedBrowser.loadURI("about:blank");
+    yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  });
+
   let changePromise = new Promise(resolve => {
     ExtensionsUI.on("change", function listener() {
       ExtensionsUI.off("change", listener);
       resolve();
     });
   });
   ExtensionsUI._checkForSideloaded();
   yield changePromise;
@@ -184,77 +176,73 @@ add_task(function* () {
 
   // 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");
 
   // Click the first sideloaded extension
-  let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
   let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
   addons.children[0].click();
 
-  // about:addons should load and go to the list of extensions
-  let tab = yield tabPromise;
-  is(tab.linkedBrowser.currentURI.spec, "about:addons", "Newly opened tab is at about:addons");
+  // When we get the permissions prompt, we should be at the extensions
+  // list in about:addons
+  let panel = yield popupPromise;
+  is(gBrowser.currentURI.spec, "about:addons", "Foreground tab is at about:addons");
 
   const VIEW = "addons://list/extension";
-  yield promiseViewLoaded(tab, VIEW);
-  let win = tab.linkedBrowser.contentWindow;
+  let 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");
 
-  // Wait for the permission prompt and cancel it
-  let panel = yield popupPromise;
+  // Check the contents of the notification, then choose "Cancel"
   let icon = panel.getAttribute("icon");
   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]);
   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");
 
-  yield BrowserTestUtils.removeTab(tab);
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
   // 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");
 
-  // Click the second sideloaded extension
-  tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
+  // Click the second sideloaded extension and wait for the notification
   popupPromise = promisePopupNotificationShown("addon-webext-permissions");
   addons.children[0].click();
-
-  tab = yield tabPromise;
-  is(tab.linkedBrowser.currentURI.spec, "about:addons", "Newly opened tab is at about:addons");
+  panel = yield popupPromise;
 
   isnot(menuButton.getAttribute("badge-status"), "addon-alert", "Should no longer have addon alert badge");
 
-  yield promiseViewLoaded(tab, VIEW);
-  win = tab.linkedBrowser.contentWindow;
+  // 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");
 
-  // Wait for the permission prompt and accept it this time
-  panel = yield popupPromise;
+  // 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]);
   is(addon1.userDisabled, true, "Addon 1 should still be disabled");
   is(addon2.userDisabled, false, "Addon 2 should now be enabled");
 
-  yield BrowserTestUtils.removeTab(tab);
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
--- a/browser/base/content/test/general/browser_extension_update.js
+++ b/browser/base/content/test/general/browser_extension_update.js
@@ -69,25 +69,36 @@ function promiseInstallEvent(addon, even
         resolve(...args);
       }
     };
     AddonManager.addInstallListener(listener);
   });
 }
 
 // Set some prefs that apply to all the tests in this file
-add_task(function setup() {
-  return SpecialPowers.pushPrefEnv({set: [
+add_task(function* setup() {
+  yield 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],
   ]});
+
+  // Navigate away from the initial page so that about:addons always
+  // opens in a new tab during tests
+  gBrowser.selectedBrowser.loadURI("about:robots");
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+  registerCleanupFunction(function*() {
+    // Return to about:blank when we're done
+    gBrowser.selectedBrowser.loadURI("about:blank");
+    yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  });
 });
 
 // Helper function to test background updates.
 function* backgroundUpdateTest(url, id, checkIconFn) {
   yield SpecialPowers.pushPrefEnv({set: [
     // Turn on background updates
     ["extensions.update.enabled", true],
 
@@ -118,17 +129,17 @@ function* backgroundUpdateTest(url, id, 
 
   // Click the menu item
   let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
   let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
   addons.children[0].click();
 
   // about:addons should load and go to the list of extensions
   let tab = yield tabPromise;
-  is(tab.linkedBrowser.currentURI.spec, "about:addons");
+  is(tab.linkedBrowser.currentURI.spec, "about:addons", "Browser is at about:addons");
 
   const VIEW = "addons://list/extension";
   yield promiseViewLoaded(tab, VIEW);
   let 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, check the contents, then cancel the update
@@ -206,16 +217,59 @@ function checkNonDefaultIcon(icon) {
   // inside the jar.
   ok(icon.startsWith("jar:file://"), "Icon is a jar url");
   ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
 }
 
 add_task(() => backgroundUpdateTest(`${URL_BASE}/browser_webext_update_icon1.xpi`,
                                     ID_ICON, checkNonDefaultIcon));
 
+// Test that an update that adds new non-promptable permissions is just
+// applied without showing a notification dialog.
+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`],
+  ]});
+
+  // Install version 1.0 of the test extension
+  let addon = yield promiseInstallAddon(`${URL_BASE}/browser_webext_update_perms1.xpi`);
+
+  ok(addon, "Addon was installed");
+
+  let sawPopup = false;
+  PopupNotifications.panel.addEventListener("popupshown",
+                                            () => sawPopup = true,
+                                            {once: true});
+
+  // Trigger an update check and wait for the update to be applied.
+  let updatePromise = promiseInstallEvent(addon, "onInstallEnded");
+  AddonManagerPrivate.backgroundUpdateCheck();
+  yield updatePromise;
+
+  // There should be no notifications about the update
+  is(getBadgeStatus(), "", "Should not have addon alert badge");
+
+  yield PanelUI.show();
+  let addons = document.getElementById("PanelUI-footer-addons");
+  is(addons.children.length, 0, "Have 0 updates in the PanelUI menu");
+  yield PanelUI.hide();
+
+  ok(!sawPopup, "Should not have seen permissions notification");
+
+  addon = yield AddonManager.getAddonByID("update_perms@tests.mozilla.org");
+  is(addon.version, "2.0", "Update should have applied");
+
+  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],
 
--- a/browser/base/content/test/general/browser_webext_update.json
+++ b/browser/base/content/test/general/browser_webext_update.json
@@ -22,11 +22,25 @@
           "applications": {
             "gecko": {
               "strict_min_version": "1",
               "advisory_max_version": "55.0"
             }
           }
         }
       ]
+    },
+    "update_perms@tests.mozilla.org": {
+      "updates": [
+        {
+          "version": "2.0",
+          "update_link": "https://example.com/browser/browser/base/content/test/general/browser_webext_update_perms2.xpi",
+          "applications": {
+            "gecko": {
+              "strict_min_version": "1",
+              "advisory_max_version": "55.0"
+            }
+          }
+        }
+      ]
     }
   }
 }
new file mode 100644
index 0000000000000000000000000000000000000000..5887987844f5324fae82908b7ff66b524e01b747
GIT binary patch
literal 309
zc$^FHW@Zs#U|`^2a1D3&dZntcr4`6K3&gw(G7Pzid6{Xc#U*-K#rb)mA)E}%2PI}j
zfN*IAHv=QfS4IW~uy)^`(_Bo3Jg(pWc3s*kQu^}Le*sA+>Gp(pldG-H|L>N#UGaS|
zueexz>DJB(FWT-bQx97<=ZvR(xwA@##zB*bX{vHJL$z|V|90MAZFytw2F8U`9QfR3
zAJsY3a_+^upVF)o<n#L<EqoZV!OgBeahu_O4VDc%&!y>>@2tHoXPflTCw|?k)#d@-
rj7)OOxO^kQ00KY<Gc0KYvC#a+3h^77zXH5j*&x~&8G?cI8L%M$3!h}H
new file mode 100644
index 0000000000000000000000000000000000000000..71f613ca77c64128cb9fe2a29f5152ee4cf1f9eb
GIT binary patch
literal 317
zc$^FHW@Zs#U|`^2hzfW2>N@^jwHL^{3&gw(G7Pzid6{Xc#U*-K#rb)mA)E}%uO((h
zTms_K3T_5QmamKq3}EfPJ*T-2Iq<lC|JyaGRC0-T_`h}!&Rm^?m4=eB5x?I1Mg^KT
z&MAH$J8N5O!^HP0r4@mKeuC$6@6YUz@Q`1jm8dR!GQ#4z@>lh`&=+?-XF8s_`MG17
zm}$4sQsdtKFSVi{Qnseg+i+>ZN|v2BHtd*i?~U!QkhB|zSKs@3W1CEO&9e0ka$7m(
zo~>+(-fSiv;LXS+$BfHY5)2>!bU4G3Mi2|lkE{?sqWLeto0ScsjgcW3NM8XP0syBC
BYkvR$
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -19,16 +19,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
                                       "extensions.webextPermissionPrompts", false);
 
 const DEFAULT_EXTENSION_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 
+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(),
 
   init() {
     Services.obs.addObserver(this, "webextension-permission-prompt", false);
@@ -60,65 +63,54 @@ this.ExtensionsUI = {
         let win = RecentWindow.getMostRecentBrowserWindow();
         for (let addon of sideloaded) {
           win.openUILinkIn(`about:newaddon?id=${addon.id}`, "tab");
         }
       }
     });
   },
 
-  showAddonsManager(browser, info) {
-    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 = browser.addTab("about:addons");
-    browser.selectedTab = tab;
-
-    return loadPromise.then(win => {
-      win.loadView("addons://list/extension");
-      return this.showPermissionsPrompt(browser.selectedBrowser, info);
+  showAddonsManager(browser, strings, icon) {
+    let global = browser.selectedBrowser.ownerGlobal;
+    return global.BrowserOpenAddonsMgr("addons://list/extension").then(aomWin => {
+      let aomBrowser = aomWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIDocShell)
+                             .chromeEventHandler;
+      return this.showPermissionsPrompt(aomBrowser, strings, icon);
     });
   },
 
   showSideloaded(browser, addon) {
     addon.markAsSeen();
     this.sideloaded.delete(addon);
     this.emit("change");
 
-    let info = {
+    let strings = this._buildStrings({
       addon,
       permissions: addon.userPermissions,
-      icon: addon.iconURL,
       type: "sideload",
-    };
-    this.showAddonsManager(browser, info).then(answer => {
+    });
+    this.showAddonsManager(browser, strings, addon.iconURL).then(answer => {
       addon.userDisabled = !answer;
     });
   },
 
   showUpdate(browser, info) {
-    info.icon = info.addon.iconURL;
-    info.type = "update";
-    this.showAddonsManager(browser, info).then(answer => {
-      if (answer) {
-        info.resolve();
-      } else {
-        info.reject();
-      }
-      // At the moment, this prompt will re-appear next time we do an update
-      // check.  See bug 1332360 for proposal to avoid this.
-      this.updates.delete(info);
-      this.emit("change");
-    });
+    this.showAddonsManager(browser, info.strings, info.addon.iconURL)
+        .then(answer => {
+          if (answer) {
+            info.resolve();
+          } else {
+            info.reject();
+          }
+          // At the moment, this prompt will re-appear next time we do an update
+          // check.  See bug 1332360 for proposal to avoid this.
+          this.updates.delete(info);
+          this.emit("change");
+        });
   },
 
   observe(subject, topic, data) {
     if (topic == "webextension-permission-prompt") {
       let {target, info} = subject.wrappedJSObject;
 
       // Dismiss the progress notification.  Note that this is bad if
       // there are multiple simultaneous installs happening, see
@@ -136,20 +128,37 @@ this.ExtensionsUI = {
         }
       };
 
       let perms = info.addon.userPermissions;
       if (!perms) {
         reply(true);
       } else {
         info.permissions = perms;
-        this.showPermissionsPrompt(target, info).then(reply);
+        let strings = this._buildStrings(info);
+        this.showPermissionsPrompt(target, strings, info.icon).then(reply);
       }
     } else if (topic == "webextension-update-permissions") {
-      this.updates.add(subject.wrappedJSObject);
+      let info = subject.wrappedJSObject;
+      let strings = this._buildStrings(info);
+
+      // If we don't prompt for any new permissions, just apply it
+      if (strings.msgs.length == 0) {
+        info.resolve();
+        return;
+      }
+
+      let update = {
+        strings,
+        addon: info.addon,
+        resolve: info.resolve,
+        reject: info.reject,
+      };
+
+      this.updates.add(update);
       this.emit("change");
     } else if (topic == "webextension-install-notify") {
       let {target, addon, callback} = subject.wrappedJSObject;
       this.showInstallNotification(target, addon).then(() => {
         if (callback) {
           callback();
         }
       });
@@ -159,66 +168,64 @@ this.ExtensionsUI = {
   // Escape &, <, and > characters in a string so that it may be
   // injected as part of raw markup.
   _sanitizeName(name) {
     return name.replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;");
   },
 
-  showPermissionsPrompt(target, info) {
-    let perms = info.permissions;
-    if (!perms) {
-      return Promise.resolve();
-    }
+  // Create a set of formatted strings for a permission prompt
+  _buildStrings(info) {
+    let result = {};
 
-    let win = target.ownerGlobal;
+    let bundle = Services.strings.createBundle(BROWSER_PROPERTIES);
 
     let name = info.addon.name;
     if (name.length > 50) {
       name = name.slice(0, 49) + "…";
     }
     name = this._sanitizeName(name);
-
     let addonName = `<span class="addon-webext-name">${name}</span>`;
-    let bundle = win.gNavigatorBundle;
 
-    let header = bundle.getFormattedString("webextPerms.header", [addonName]);
-    let text = "";
-    let listIntro = bundle.getString("webextPerms.listIntro");
+    result.header = bundle.formatStringFromName("webextPerms.header", [addonName], 1);
+    result.text = "";
+    result.listIntro = bundle.GetStringFromName("webextPerms.listIntro");
 
-    let acceptText = bundle.getString("webextPerms.add.label");
-    let acceptKey = bundle.getString("webextPerms.add.accessKey");
-    let cancelText = bundle.getString("webextPerms.cancel.label");
-    let cancelKey = bundle.getString("webextPerms.cancel.accessKey");
+    result.acceptText = bundle.GetStringFromName("webextPerms.add.label");
+    result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey");
+    result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label");
+    result.cancelKey = bundle.GetStringFromName("webextPerms.cancel.accessKey");
 
     if (info.type == "sideload") {
-      header = bundle.getFormattedString("webextPerms.sideloadHeader", [addonName]);
-      text = bundle.getString("webextPerms.sideloadText");
-      acceptText = bundle.getString("webextPerms.sideloadEnable.label");
-      acceptKey = bundle.getString("webextPerms.sideloadEnable.accessKey");
-      cancelText = bundle.getString("webextPerms.sideloadDisable.label");
-      cancelKey = bundle.getString("webextPerms.sideloadDisable.accessKey");
+      result.header = bundle.formatStringFromName("webextPerms.sideloadHeader", [addonName], 1);
+      result.text = bundle.GetStringFromName("webextPerms.sideloadText");
+      result.acceptText = bundle.GetStringFromName("webextPerms.sideloadEnable.label");
+      result.acceptKey = bundle.GetStringFromName("webextPerms.sideloadEnable.accessKey");
+      result.cancelText = bundle.GetStringFromName("webextPerms.sideloadDisable.label");
+      result.cancelKey = bundle.GetStringFromName("webextPerms.sideloadDisable.accessKey");
     } else if (info.type == "update") {
-      header = "";
-      text = bundle.getFormattedString("webextPerms.updateText", [addonName]);
-      acceptText = bundle.getString("webextPerms.updateAccept.label");
-      acceptKey = bundle.getString("webextPerms.updateAccept.accessKey");
+      result.header = "";
+      result.text = bundle.formatStringFromName("webextPerms.updateText", [addonName], 1);
+      result.acceptText = bundle.GetStringFromName("webextPerms.updateAccept.label");
+      result.acceptKey = bundle.GetStringFromName("webextPerms.updateAccept.accessKey");
     }
 
-    let msgs = [];
+    let perms = info.permissions || {hosts: [], permissions: []};
+
+    result.msgs = [];
     for (let permission of perms.permissions) {
       let key = `webextPerms.description.${permission}`;
       if (permission == "nativeMessaging") {
-        let brandBundle = win.document.getElementById("bundle_brand");
-        let appName = brandBundle.getString("brandShortName");
-        msgs.push(bundle.getFormattedString(key, [appName]));
+        let brandBundle = Services.strings.createBundle(BRAND_PROPERTIES);
+        let appName = brandBundle.GetStringFromName("brandShortName");
+        result.msgs.push(bundle.formatStringFromName(key, [appName], 1));
       } else {
         try {
-          msgs.push(bundle.getString(key));
+          result.msgs.push(bundle.GetStringFromName(key));
         } catch (err) {
           // We deliberately do not include all permissions in the prompt.
           // So if we don't find one then just skip it.
         }
       }
     }
 
     let allUrls = false, wildcards = [], sites = [];
@@ -236,92 +243,100 @@ this.ExtensionsUI = {
       } else if (match[1].startsWith("*.")) {
         wildcards.push(match[1].slice(2));
       } else {
         sites.push(match[1]);
       }
     }
 
     if (allUrls) {
-      msgs.push(bundle.getString("webextPerms.hostDescription.allUrls"));
+      result.msgs.push(bundle.GetStringFromName("webextPerms.hostDescription.allUrls"));
     } else {
       // Formats a list of host permissions.  If we have 4 or fewer, display
       // them all, otherwise display the first 3 followed by an item that
       // says "...plus N others"
       function format(list, itemKey, moreKey) {
         function formatItems(items) {
-          msgs.push(...items.map(item => bundle.getFormattedString(itemKey, [item])));
+          result.msgs.push(...items.map(item => bundle.formatStringFromName(itemKey, [item], 1)));
         }
         if (list.length < 5) {
           formatItems(list);
         } else {
           formatItems(list.slice(0, 3));
 
           let remaining = list.length - 3;
-          msgs.push(PluralForm.get(remaining, bundle.getString(moreKey))
-                              .replace("#1", remaining));
+          result.msgs.push(PluralForm.get(remaining, bundle.GetStringFromName(moreKey))
+                                     .replace("#1", remaining));
         }
       }
 
       format(wildcards, "webextPerms.hostDescription.wildcard",
              "webextPerms.hostDescription.tooManyWildcards");
       format(sites, "webextPerms.hostDescription.oneSite",
              "webextPerms.hostDescription.tooManySites");
     }
 
+    return result;
+  },
+
+  showPermissionsPrompt(browser, strings, icon) {
+    function eventCallback(topic) {
+      if (topic == "showing") {
+        let doc = this.browser.ownerDocument;
+        doc.getElementById("addon-webext-perm-header").innerHTML = strings.header;
+
+        let textEl = doc.getElementById("addon-webext-perm-text");
+        textEl.innerHTML = strings.text;
+        textEl.hidden = !strings.text;
+
+        let listIntroEl = doc.getElementById("addon-webext-perm-intro");
+        listIntroEl.value = strings.listIntro;
+        listIntroEl.hidden = (strings.msgs.length == 0);
+
+        let list = doc.getElementById("addon-webext-perm-list");
+        while (list.firstChild) {
+          list.firstChild.remove();
+        }
+
+        for (let msg of strings.msgs) {
+          let item = doc.createElementNS(HTML_NS, "li");
+          item.textContent = msg;
+          list.appendChild(item);
+        }
+      } else if (topic == "swapping") {
+        return true;
+      }
+      return false;
+    }
+
     let popupOptions = {
       hideClose: true,
-      popupIconURL: info.icon || DEFAULT_EXTENSION_ICON,
+      popupIconURL: icon || DEFAULT_EXTENSION_ICON,
       persistent: true,
-
-      eventCallback(topic) {
-        if (topic == "showing") {
-          let doc = this.browser.ownerDocument;
-          doc.getElementById("addon-webext-perm-header").innerHTML = header;
-
-          let textEl = doc.getElementById("addon-webext-perm-text");
-          textEl.innerHTML = text;
-          textEl.hidden = !text;
-
-          let listIntroEl = doc.getElementById("addon-webext-perm-intro");
-          listIntroEl.value = listIntro;
-          listIntroEl.hidden = (msgs.length == 0);
-
-          let list = doc.getElementById("addon-webext-perm-list");
-          while (list.firstChild) {
-            list.firstChild.remove();
-          }
-
-          for (let msg of msgs) {
-            let item = doc.createElementNS(HTML_NS, "li");
-            item.textContent = msg;
-            list.appendChild(item);
-          }
-        } else if (topic == "swapping") {
-          return true;
-        }
-        return false;
-      },
+      eventCallback,
     };
 
+    let win = browser.ownerGlobal;
     return new Promise(resolve => {
-      win.PopupNotifications.show(target, "addon-webext-permissions", "",
+      let action = {
+        label: strings.acceptText,
+        accessKey: strings.acceptKey,
+        callback: () => resolve(true),
+      };
+      let secondaryActions = [
+        {
+          label: strings.cancelText,
+          accessKey: strings.cancelKey,
+          callback: () => resolve(false),
+        },
+      ];
+
+      win.PopupNotifications.show(browser, "addon-webext-permissions", "",
                                   "addons-notification-icon",
-                                  {
-                                    label: acceptText,
-                                    accessKey: acceptKey,
-                                    callback: () => resolve(true),
-                                  },
-                                  [
-                                    {
-                                      label: cancelText,
-                                      accessKey: cancelKey,
-                                      callback: () => resolve(false),
-                                    },
-                                  ], popupOptions);
+                                  action, secondaryActions, popupOptions);
     });
   },
 
   showInstallNotification(target, addon) {
     let win = target.ownerGlobal;
     let popups = win.PopupNotifications;
 
     let name = this._sanitizeName(addon.name);