Bug 1621213 - Request user's permission to do anything with a WebExtension experiment. r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Fri, 20 Mar 2020 11:18:26 +1300
changeset 38634 2373d70119596f0d5305021d1d89dbef857d08b1
parent 38633 7ba40e153105efa78c68c2e922056ed117b01154
child 38635 6b562736cc9ad74d5de34e6325902df74cdf4b39
push id400
push userclokep@gmail.com
push dateMon, 04 May 2020 18:56:09 +0000
reviewersmkmelin
bugs1621213
Bug 1621213 - Request user's permission to do anything with a WebExtension experiment. r=mkmelin
mail/base/content/messenger.xhtml
mail/base/modules/ExtensionsUI.jsm
mail/base/test/webextensions/browser.ini
mail/base/test/webextensions/browser_extension_install_experiment.js
mail/base/test/webextensions/browser_extension_update_background.js
mail/base/test/webextensions/browser_legacy_webext.xpi
mail/base/test/webextensions/browser_webext_experiment.xpi
mail/base/test/webextensions/browser_webext_experiment_permissions.xpi
mail/base/test/webextensions/browser_webext_update.json
mail/base/test/webextensions/head.js
mail/locales/en-US/chrome/messenger/addons.properties
mail/themes/shared/mail/messenger.css
--- a/mail/base/content/messenger.xhtml
+++ b/mail/base/content/messenger.xhtml
@@ -475,16 +475,17 @@
     <popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/>
   </popupnotification>
 
   <popupnotification id="addon-webext-permissions-notification" hidden="true">
     <popupnotificationcontent class="addon-webext-perm-notification-content" orient="vertical">
       <description id="addon-webext-perm-text" class="addon-webext-perm-text"/>
       <label id="addon-webext-perm-intro" class="addon-webext-perm-text"/>
       <html:ul id="addon-webext-perm-list" class="addon-webext-perm-list"/>
+      <description id="addon-webext-experiment-warning" class="addon-webext-experiment-warning"/>
       <hbox>
         <label id="addon-webext-perm-info" is="text-link" class="popup-notification-learnmore-link"/>
       </hbox>
     </popupnotificationcontent>
   </popupnotification>
 
   <popupnotification id="addon-installed-notification" hidden="true">
     <popupnotificationcontent class="addon-installed-notification-content" orient="vertical">
--- a/mail/base/modules/ExtensionsUI.jsm
+++ b/mail/base/modules/ExtensionsUI.jsm
@@ -570,23 +570,16 @@ var gXPInstallObserver = {
               extensionSettings &&
               "blocked_install_message" in extensionSettings
             ) {
               message = " " + extensionSettings.blocked_install_message;
             }
             args = [install.name, install.addon.id, message];
           }
 
-          // Add Learn More link when refusing to install an unsigned add-on
-          if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
-            options.learnMoreURL =
-              Services.urlFormatter.formatURLPref("app.support.baseURL") +
-              "unsigned-addons";
-          }
-
           messageString = addonsBundle.getFormattedString(error, args);
 
           showNotification(
             browser,
             notificationID,
             messageString,
             anchorID,
             action,
@@ -804,42 +797,47 @@ var ExtensionsUI = {
       }
       // 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._updateNotifications();
     });
   },
 
-  observe(subject, topic, data) {
+  async observe(subject, topic, data) {
     if (topic == "webextension-permission-prompt") {
       let { target, info } = subject.wrappedJSObject;
 
       let { browser } = getTabBrowser(target);
 
       // Dismiss the progress notification.  Note that this is bad if
       // there are multiple simultaneous installs happening, see
       // bug 1329884 for a longer explanation.
       let progressNotification = getNotification("addon-progress", browser);
       if (progressNotification) {
         progressNotification.remove();
       }
 
-      info.unsigned =
-        info.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING;
-      if (
-        info.unsigned &&
-        Cu.isInAutomation &&
-        Services.prefs.getBoolPref("extensions.ui.ignoreUnsigned", false)
-      ) {
-        info.unsigned = false;
+      let strings = this._buildStrings(info);
+      let data = new ExtensionData(info.addon.getResourceURI());
+      await data.loadManifest();
+      if (data.manifest.experiment_apis) {
+        strings.msgs = [
+          addonsBundle.getFormattedString(
+            "webextPerms.description.experiment",
+            [brandShortName]
+          ),
+        ];
+        if (info.source != "AMO") {
+          strings.experimentWarning = addonsBundle.getString(
+            "webextPerms.experimentWarning"
+          );
+        }
       }
 
-      let strings = this._buildStrings(info);
-
       // If this is an update with no promptable permissions, just apply it
       if (info.type == "update" && !strings.msgs.length) {
         info.resolve();
         return;
       }
 
       let histkey;
       if (info.type == "sideload") {
@@ -961,24 +959,16 @@ var ExtensionsUI = {
   // Create a set of formatted strings for a permission prompt
   _buildStrings(info) {
     let bundle = Services.strings.createBundle(ADDONS_PROPERTIES);
     let info2 = Object.assign({ appName: brandShortName }, info);
 
     let strings = ExtensionData.formatPermissionStrings(info2, bundle, {
       collapseOrigins: true,
     });
-    // Silence the unsigned add-on warning. We can't stop
-    // formatPermissionStrings returning this string without changing it in
-    // addons.properties, and it might be wanted in future.
-    if (
-      strings.text == bundle.GetStringFromName("webextPerms.unsignedWarning")
-    ) {
-      strings.text = "";
-    }
     strings.addonName = info.addon.name;
     strings.learnMore = addonsBundle.getString("webextPerms.learnMore");
     return strings;
   },
 
   async showPermissionsPrompt(target, strings, icon, histkey) {
     let { browser, window } = getTabBrowser(target);
 
@@ -1013,16 +1003,22 @@ var ExtensionsUI = {
             list.firstChild.remove();
           }
 
           for (let msg of strings.msgs) {
             let item = doc.createElementNS(HTML_NS, "li");
             item.textContent = msg;
             list.appendChild(item);
           }
+
+          let experimentsEl = doc.getElementById(
+            "addon-webext-experiment-warning"
+          );
+          experimentsEl.textContent = strings.experimentWarning;
+          experimentsEl.hidden = !strings.experimentWarning;
         } else if (topic == "swapping") {
           return true;
         }
         if (topic == "removed") {
           Services.tm.dispatchToMainThread(() => {
             resolve(false);
           });
         }
--- a/mail/base/test/webextensions/browser.ini
+++ b/mail/base/test/webextensions/browser.ini
@@ -6,30 +6,32 @@ prefs =
   mail.winsearch.firstRunDone=true
   mailnews.start_page.override_url=about:blank
   mailnews.start_page.url=about:blank
   toolkit.telemetry.testing.overrideProductsCheck=true
 subsuite = thunderbird
 support-files =
   head.js
   file_install_extensions.html
-  browser_legacy_webext.xpi
+  browser_webext_experiment.xpi
+  browser_webext_experiment_permissions.xpi
   browser_webext_permissions.xpi
   browser_webext_nopermissions.xpi
   browser_webext_unsigned.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_origins1.xpi
   browser_webext_update_origins2.xpi
   browser_webext_update.json
 
+[browser_extension_install_experiment.js]
 [browser_extension_sideloading.js]
 [browser_extension_update_background.js]
 [browser_extension_update_background_noprompt.js]
 [browser_permissions_installTrigger.js]
 [browser_permissions_local_file.js]
 [browser_permissions_mozAddonManager.js]
 [browser_permissions_optional.js]
 [browser_permissions_pointerevent.js]
new file mode 100644
--- /dev/null
+++ b/mail/base/test/webextensions/browser_extension_install_experiment.js
@@ -0,0 +1,83 @@
+"use strict";
+
+async function installFile(filename) {
+  const ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+    Ci.nsIChromeRegistry
+  );
+  let chromeUrl = Services.io.newURI(gTestPath);
+  let fileUrl = ChromeRegistry.convertChromeURL(chromeUrl);
+  let file = fileUrl.QueryInterface(Ci.nsIFileURL).file;
+  file.leafName = filename;
+
+  let MockFilePicker = SpecialPowers.MockFilePicker;
+  MockFilePicker.init(window);
+  MockFilePicker.setFiles([file]);
+  MockFilePicker.afterOpenCallback = MockFilePicker.cleanup;
+
+  let managerWin = await openAddonsMgr("addons://list/extension");
+
+  // Do the install...
+  if (managerWin.gViewController.isLoading) {
+    await BrowserTestUtils.waitForEvent(managerWin.document, "ViewChanged");
+  }
+  let installButton = managerWin
+    .getHtmlBrowser()
+    .contentDocument.querySelector('[action="install-from-file"]');
+  installButton.click();
+}
+
+async function testExperimentPrompt(filename) {
+  let installPromise = new Promise(resolve => {
+    let listener = {
+      onDownloadCancelled() {
+        AddonManager.removeInstallListener(listener);
+        resolve(false);
+      },
+
+      onDownloadFailed() {
+        AddonManager.removeInstallListener(listener);
+        resolve(false);
+      },
+
+      onInstallCancelled() {
+        AddonManager.removeInstallListener(listener);
+        resolve(false);
+      },
+
+      onInstallEnded() {
+        AddonManager.removeInstallListener(listener);
+        resolve(true);
+      },
+
+      onInstallFailed() {
+        AddonManager.removeInstallListener(listener);
+        resolve(false);
+      },
+    };
+    AddonManager.addInstallListener(listener);
+  });
+
+  await installFile(filename);
+
+  let panel = await promisePopupNotificationShown("addon-webext-permissions");
+  checkNotification(
+    panel,
+    isDefaultIcon,
+    [["webextPerms.description.experiment"]],
+    true
+  );
+  panel.secondaryButton.click();
+
+  let result = await installPromise;
+  ok(!result, "Installation was cancelled");
+  let addon = await AddonManager.getAddonByID("thisisatest@test.invalid");
+  is(addon, null, "Extension is not installed");
+
+  let tabmail = document.getElementById("tabmail");
+  tabmail.closeTab(tabmail.currentTabInfo);
+}
+
+add_task(async () => {
+  await testExperimentPrompt("browser_webext_experiment.xpi");
+  await testExperimentPrompt("browser_webext_experiment_permissions.xpi");
+});
--- a/mail/base/test/webextensions/browser_extension_update_background.js
+++ b/mail/base/test/webextensions/browser_extension_update_background.js
@@ -6,17 +6,16 @@ const { AddonTestUtils } = ChromeUtils.i
   "resource://testing-common/AddonTestUtils.jsm"
 );
 
 AddonTestUtils.initMochitest(this);
 
 const ID = "update2@tests.mozilla.org";
 const ID_ICON = "update_icon2@tests.mozilla.org";
 const ID_PERMS = "update_perms@tests.mozilla.org";
-const ID_LEGACY = "legacy_update@tests.mozilla.org";
 const FAKE_INSTALL_TELEMETRY_SOURCE = "fake-install-source";
 
 requestLongerTimeout(2);
 
 function promiseViewLoaded(tab, viewid) {
   let win = tab.linkedBrowser.contentWindow;
   if (
     win.gViewController &&
@@ -38,16 +37,33 @@ function promiseViewLoaded(tab, viewid) 
   });
 }
 
 function getBadgeStatus() {
   let menuButton = document.getElementById("button-appmenu");
   return menuButton.getAttribute("badge-status");
 }
 
+function promiseBadgeChange() {
+  return new Promise(resolve => {
+    let menuButton = document.getElementById("button-appmenu");
+    new MutationObserver((mutationsList, observer) => {
+      for (let mutation of mutationsList) {
+        if (mutation.attributeName == "badge-status") {
+          observer.disconnect();
+          resolve();
+          return;
+        }
+      }
+    }).observe(menuButton, {
+      attributes: true,
+    });
+  });
+}
+
 // Set some prefs that apply to all the tests in this file
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({
     set: [
       // We don't have pre-pinned certificates for the local mochitest server
       ["extensions.install.requireBuiltInCerts", false],
       ["extensions.update.requireBuiltInCerts", false],
     ],
@@ -79,19 +95,20 @@ async function backgroundUpdateTest(url,
   let addonId = addon.id;
 
   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 = promiseInstallEvent(addon, "onDownloadEnded");
+  let badgePromise = promiseBadgeChange();
 
   AddonManagerPrivate.backgroundUpdateCheck();
-  await updatePromise;
+  await Promise.all([updatePromise, badgePromise]);
 
   is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
 
   // Find the menu entry for the update
   await gCUITestUtils.openMainMenu();
 
   let addons = PanelUI.addonNotificationContainer;
   is(addons.children.length, 1, "Have a menu entry for the update");
@@ -124,18 +141,19 @@ async function backgroundUpdateTest(url,
 
   await gCUITestUtils.openMainMenu();
   addons = PanelUI.addonNotificationContainer;
   is(addons.children.length, 0, "Update menu entries should be gone");
   await gCUITestUtils.hideMainMenu();
 
   // Re-check for an update
   updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
+  badgePromise = promiseBadgeChange();
   await AddonManagerPrivate.backgroundUpdateCheck();
-  await updatePromise;
+  await Promise.all([updatePromise, badgePromise]);
 
   is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
 
   // Find the menu entry for the update
   await gCUITestUtils.openMainMenu();
 
   addons = PanelUI.addonNotificationContainer;
   is(addons.children.length, 1, "Have a menu entry for the update");
deleted file mode 100644
index a3bdf6f8325851964f6d61b6f3bc716b645ab87e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..92ee46d52af6e7dcc2654a9505961efeb725c175
GIT binary patch
literal 2466
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
Q?y&Fff9!(p;rizN0Wg&^qW}N^
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..db1aca7f2a6a8203594a44ffb76c9cd860d58509
GIT binary patch
literal 2482
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
--- a/mail/base/test/webextensions/browser_webext_update.json
+++ b/mail/base/test/webextensions/browser_webext_update.json
@@ -34,30 +34,16 @@
           "applications": {
             "gecko": {
               "strict_min_version": "1"
             }
           }
         }
       ]
     },
-    "legacy_update@tests.mozilla.org": {
-      "updates": [
-        {
-          "version": "2.0",
-          "update_link": "https://example.com/browser/comm/mail/base/test/webextensions/browser_legacy_webext.xpi",
-          "applications": {
-            "gecko": {
-              "strict_min_version": "1",
-              "advisory_max_version": "*"
-            }
-          }
-        }
-      ]
-    },
     "update_origins@tests.mozilla.org": {
       "updates": [
         {
           "version": "2.0",
           "update_link": "https://example.com/browser/comm/mail/base/test/webextensions/browser_webext_update_origins2.xpi",
           "applications": {
             "gecko": {
               "strict_min_version": "1"
--- a/mail/base/test/webextensions/head.js
+++ b/mail/base/test/webextensions/head.js
@@ -216,20 +216,23 @@ function checkPermissionString(string, k
  *        it is a function, it is called with the icon url and returns
  *        true if the url is correct.
  * @param {array} permissions
  *        The expected entries in the permissions list.  Each element
  *        in this array is itself a 2-element array with the string key
  *        for the item (e.g., "webextPerms.description.foo") and an
  *        optional formatting parameter.
  */
-function checkNotification(panel, checkIcon, permissions) {
+function checkNotification(panel, checkIcon, permissions, warning = false) {
   let icon = panel.getAttribute("icon");
   let ul = document.getElementById("addon-webext-perm-list");
   let header = document.getElementById("addon-webext-perm-intro");
+  let experimentWarning = document.getElementById(
+    "addon-webext-experiment-warning"
+  );
   let learnMoreLink = document.getElementById("addon-webext-perm-info");
 
   if (checkIcon instanceof RegExp) {
     ok(
       checkIcon.test(icon),
       `Notification icon is correct ${JSON.stringify(icon)} ~= ${checkIcon}`
     );
   } else if (typeof checkIcon == "function") {
@@ -263,16 +266,30 @@ function checkNotification(panel, checkI
     let [key, param] = permissions[i];
     checkPermissionString(
       ul.children[i].textContent,
       key,
       param,
       `Permission number ${i + 1} is correct`
     );
   }
+
+  if (warning) {
+    is(
+      experimentWarning.getAttribute("hidden"),
+      "",
+      "Experiments warning is visible"
+    );
+  } else {
+    is(
+      experimentWarning.getAttribute("hidden"),
+      "true",
+      "Experiments warning is hidden"
+    );
+  }
 }
 
 /**
  * Test that install-time permission prompts work for a given
  * installation method.
  *
  * @param {Function} installFn
  *        Callable that takes the name of an xpi file to install and
--- a/mail/locales/en-US/chrome/messenger/addons.properties
+++ b/mail/locales/en-US/chrome/messenger/addons.properties
@@ -106,16 +106,18 @@ addonInstallErrorBlocklisted=%S could no
 # LOCALIZATION NOTE (webextPerms.header)
 # This string is used as a header in the webextension permissions dialog,
 # %S is replaced with the localized name of the extension being installed.
 # See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612
 # for an example of the full dialog.
 # Note, this string will be used as raw markup. Avoid characters like <, >, &
 webextPerms.header=Add %S?
 
+# %S is brandShortName
+webextPerms.experimentWarning=Malicious add-ons can steal your private information or compromise your computer. Only install this add-on if you trust the source.
 webextPerms.unsignedWarning=Caution: This add-on is unverified. Malicious add-ons can steal your private information or compromise your computer. Only install this add-on if you trust the source.
 
 # LOCALIZATION NOTE (webextPerms.listIntro)
 # This string will be followed by a list of permissions requested
 # by the webextension.
 webextPerms.listIntro=It requires your permission to:
 webextPerms.learnMore=Learn more about permissions
 webextPerms.add.label=Add
@@ -174,16 +176,19 @@ webextPerms.description.browserSettings=
 webextPerms.description.browsingData=Clear recent browsing history, cookies, and related data
 webextPerms.description.clipboardRead=Get data from the clipboard
 webextPerms.description.clipboardWrite=Input data to the clipboard
 webextPerms.description.compose=Read and modify your email messages as you compose and send them
 webextPerms.description.devtools=Extend developer tools to access your data in open tabs
 webextPerms.description.dns=Access IP address and hostname information
 webextPerms.description.downloads=Download files and read and modify the browser’s download history
 webextPerms.description.downloads.open=Open files downloaded to your computer
+# LOCALIZATION NOTE (webextPerms.description.experiment)
+# %S will be replaced with the name of the application
+webextPerms.description.experiment=Have full, unrestricted access to %S, and your computer
 webextPerms.description.find=Read the text of all open tabs
 webextPerms.description.geolocation=Access your location
 webextPerms.description.history=Access browsing history
 webextPerms.description.management=Monitor extension usage and manage themes
 webextPerms.description.messagesMove=Move, copy, or delete your email messages
 webextPerms.description.messagesRead=Read your email messages and mark or tag them
 # LOCALIZATION NOTE (webextPerms.description.nativeMessaging)
 # %S will be replaced with the name of the application
--- a/mail/themes/shared/mail/messenger.css
+++ b/mail/themes/shared/mail/messenger.css
@@ -187,16 +187,20 @@ description.error {
 .popup-notification-icon[popupid="app-update"].updates-unsupported {
   background-color: #FFE900;
 }
 
 html|ul.addon-installed-list {
   margin-top: 0;
 }
 
+html|ul.addon-webext-perm-list:empty {
+  display: none;
+}
+
 #tabbar-toolbar {
   -moz-appearance: none;
   padding: 0;
 }
 
 #tabbar-toolbar[customizing="true"] {
   min-width: 16px;
   min-height: 10px;