Bug 1659530 skip 3rd party panel when installing recommended addons r=rpl
authorShane Caraveo <scaraveo@mozilla.com>
Wed, 16 Sep 2020 16:53:56 +0000
changeset 549067 d05f2016d467f256b488bf1510ed4b0c6de85fa3
parent 549066 ca57b2951ec03d604a2552f2b1c62c189878a0a1
child 549068 65963fef39983a218697bdfb6dce05287662a5fa
push id126560
push userscaraveo@mozilla.com
push dateThu, 17 Sep 2020 02:37:04 +0000
treeherderautoland@d05f2016d467 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrpl
bugs1659530
milestone82.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1659530 skip 3rd party panel when installing recommended addons r=rpl Differential Revision: https://phabricator.services.mozilla.com/D87326
browser/app/profile/firefox.js
browser/base/content/browser-addons.js
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/internal/XPIDatabase.jsm
toolkit/mozapps/extensions/test/xpinstall/browser.ini
toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js
toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js
toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js
toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js
toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js
toolkit/mozapps/extensions/test/xpinstall/head.js
toolkit/mozapps/extensions/test/xpinstall/recommended.xpi
tools/lint/rejected-words.yml
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -28,16 +28,18 @@ pref("extensions.logging.enabled", false
 pref("extensions.strictCompatibility", false);
 
 // Temporary preference to forcibly make themes more safe with Australis even if
 // extensions.checkCompatibility=false has been set.
 pref("extensions.checkCompatibility.temporaryThemeOverride_minAppVersion", "29.0a1");
 
 pref("extensions.webextPermissionPrompts", true);
 pref("extensions.webextOptionalPermissionPrompts", true);
+// If enabled, install origin permission verification happens after addons are downloaded.
+pref("extensions.postDownloadThirdPartyPrompt", true);
 
 // Preferences for AMO integration
 pref("extensions.getAddons.cache.enabled", true);
 pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/api/v3/addons/search/?guid=%IDS%&lang=%LOCALE%");
 pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/firefox/search?q=%TERMS%&platform=%OS%&appver=%VERSION%");
 pref("extensions.getAddons.link.url", "https://addons.mozilla.org/%LOCALE%/firefox/");
 pref("extensions.getAddons.langpacks.url", "https://services.addons.mozilla.org/api/v3/addons/language-tools/?app=firefox&type=language&appversion=%VERSION%");
 pref("extensions.getAddons.discovery.api_url", "https://services.addons.mozilla.org/api/v4/discovery/?lang=%LOCALE%&edition=%DISTRIBUTION%");
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -583,16 +583,27 @@ var gXPInstallObserver = {
           null,
           null,
           options
         );
         removeNotificationOnEnd(popup, installInfo.installs);
         break;
       }
       case "addon-install-blocked": {
+        // 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 = PopupNotifications.getNotification(
+          "addon-progress",
+          browser
+        );
+        if (progressNotification) {
+          progressNotification.remove();
+        }
+
         let hasHost = !!options.displayURI;
         if (hasHost) {
           messageString = gNavigatorBundle.getFormattedString(
             "xpinstallPromptMessage.header",
             ["<>"]
           );
           options.name = options.displayURI.displayHost;
         } else {
@@ -658,16 +669,19 @@ var gXPInstallObserver = {
             "xpinstallPromptMessage.dontAllow.accesskey"
           ),
           callback: () => {
             for (let install of installInfo.installs) {
               if (install.state != AddonManager.STATE_CANCELLED) {
                 install.cancel();
               }
             }
+            if (installInfo.cancel) {
+              installInfo.cancel();
+            }
           },
         };
         let neverAllowAction = {
           label: gNavigatorBundle.getString(
             "xpinstallPromptMessage.neverAllow"
           ),
           accessKey: gNavigatorBundle.getString(
             "xpinstallPromptMessage.neverAllow.accesskey"
@@ -678,16 +692,19 @@ var gXPInstallObserver = {
               "install",
               SitePermissions.BLOCK
             );
             for (let install of installInfo.installs) {
               if (install.state != AddonManager.STATE_CANCELLED) {
                 install.cancel();
               }
             }
+            if (installInfo.cancel) {
+              installInfo.cancel();
+            }
           },
         };
 
         secHistogram.add(
           Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
         );
         let popup = PopupNotifications.show(
           browser,
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -37,16 +37,18 @@ const PREF_EM_AUTOUPDATE_DEFAULT = "exte
 const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
 const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
 const PREF_SYS_ADDON_UPDATE_ENABLED = "extensions.systemAddon.update.enabled";
 
 const PREF_MIN_WEBEXT_PLATFORM_VERSION =
   "extensions.webExtensionsMinPlatformVersion";
 const PREF_WEBAPI_TESTING = "extensions.webapi.testing";
 const PREF_WEBEXT_PERM_PROMPTS = "extensions.webextPermissionPrompts";
+const PREF_EM_POSTDOWNLOAD_THIRD_PARTY =
+  "extensions.postDownloadThirdPartyPrompt";
 
 const UPDATE_REQUEST_VERSION = 2;
 
 const BRANCH_REGEXP = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
 const PREF_EM_CHECK_COMPATIBILITY_BASE = "extensions.checkCompatibility";
 var PREF_EM_CHECK_COMPATIBILITY = MOZ_COMPATIBILITY_NIGHTLY
   ? PREF_EM_CHECK_COMPATIBILITY_BASE + ".nightly"
   : undefined;
@@ -89,16 +91,23 @@ XPCOMUtils.defineLazyModuleGetters(this,
 
 XPCOMUtils.defineLazyPreferenceGetter(
   this,
   "WEBEXT_PERMISSION_PROMPTS",
   PREF_WEBEXT_PERM_PROMPTS,
   false
 );
 
+XPCOMUtils.defineLazyPreferenceGetter(
+  this,
+  "WEBEXT_POSTDOWNLOAD_THIRD_PARTY",
+  PREF_EM_POSTDOWNLOAD_THIRD_PARTY,
+  false
+);
+
 // Initialize the WebExtension process script service as early as possible,
 // since it needs to be able to track things like new frameLoader globals that
 // are created before other framework code has been initialized.
 Services.ppmm.loadProcessScript(
   "resource://gre/modules/extensionProcessScriptLoader.js",
   true
 );
 
@@ -2052,23 +2061,31 @@ var AddonManagerInternal = {
           return true;
         }
         return false;
       }
     }
     return !explicit;
   },
 
-  installNotifyObservers(aTopic, aBrowser, aUri, aInstall, aInstallFn) {
+  installNotifyObservers(
+    aTopic,
+    aBrowser,
+    aUri,
+    aInstall,
+    aInstallFn,
+    aCancelFn
+  ) {
     let info = {
       wrappedJSObject: {
         browser: aBrowser,
         originatingURI: aUri,
         installs: [aInstall],
         install: aInstallFn,
+        cancel: aCancelFn,
       },
     };
     Services.obs.notifyObservers(info, aTopic);
   },
 
   startInstall(browser, url, install) {
     this.installNotifyObservers("addon-install-started", browser, url, install);
 
@@ -2329,25 +2346,29 @@ var AddonManagerInternal = {
         // Block without prompt
         aInstall.cancel();
         this.installNotifyObservers(
           "addon-install-blocked-silent",
           topBrowser,
           aInstallingPrincipal.URI,
           aInstall
         );
-      } else {
+      } else if (!WEBEXT_POSTDOWNLOAD_THIRD_PARTY) {
         // Block with prompt
         this.installNotifyObservers(
           "addon-install-blocked",
           topBrowser,
           aInstallingPrincipal.URI,
           aInstall,
           () => startInstall("other")
         );
+      } else {
+        // We download the addon and validate recommended states prior to
+        // showing the third party install panel.
+        startInstall("other");
       }
     } catch (e) {
       // In the event that the weblistener throws during instantiation or when
       // calling onWebInstallBlocked or onWebInstallRequested the
       // install should get cancelled.
       logger.warn("Failure calling web installer", e);
       aInstall.cancel();
     }
@@ -3088,99 +3109,137 @@ var AddonManagerInternal = {
   set updateEnabled(aValue) {
     aValue = !!aValue;
     if (aValue != gUpdateEnabled) {
       Services.prefs.setBoolPref(PREF_EM_UPDATE_ENABLED, aValue);
     }
     return aValue;
   },
 
+  _verifyThirdPartyInstall(browser, url, install, info, source) {
+    // If this is an install from a recognized source, or it is a recommended addon, we
+    // skip the third party panel.  The source param was generated based on the installing
+    // principal and checking against site permissions and enterprise policy, so we
+    // can rely on that rather than re-validating against that principal.
+    if (
+      !WEBEXT_POSTDOWNLOAD_THIRD_PARTY ||
+      ["AMO", "local"].includes(source) ||
+      info.addon.canBypassThirdParyInstallPrompt
+    ) {
+      return Promise.resolve();
+    }
+
+    return new Promise((resolve, reject) => {
+      this.installNotifyObservers(
+        "addon-install-blocked",
+        browser,
+        url,
+        install,
+        resolve,
+        reject
+      );
+    });
+  },
+
   setupPromptHandler(browser, url, install, requireConfirm, source) {
     install.promptHandler = info =>
       new Promise((resolve, _reject) => {
         let reject = () => {
           this.installNotifyObservers(
             "addon-install-cancelled",
             browser,
             url,
             install
           );
           _reject();
         };
 
-        // All installs end up in this callback when the add-on is available
-        // for installation.  There are numerous different things that can
-        // happen from here though.  For webextensions, if the application
-        // implements webextension permission prompts, those always take
-        // precedence.
-        // If this add-on is not a webextension or if the application does not
-        // implement permission prompts, no confirmation is displayed for
-        // installs created from about:addons (in which case requireConfirm
-        // is false).
-        // In the remaining cases, a confirmation prompt is displayed but the
-        // application may override it either by implementing the
-        // "@mozilla.org/addons/web-install-prompt;1" contract or by setting
-        // the customConfirmationUI preference and responding to the
-        // "addon-install-confirmation" notification.  If the application
-        // does not implement its own prompt, use the built-in xul dialog.
-        if (info.addon.userPermissions && WEBEXT_PERMISSION_PROMPTS) {
-          let subject = {
-            wrappedJSObject: {
-              target: browser,
-              info: Object.assign({ resolve, reject, source }, info),
-            },
-          };
-          subject.wrappedJSObject.info.permissions = info.addon.userPermissions;
-          Services.obs.notifyObservers(
-            subject,
-            "webextension-permission-prompt"
-          );
-        } else if (requireConfirm) {
-          // The methods below all want to call the install() or cancel()
-          // method on the provided AddonInstall object to either accept
-          // or reject the confirmation.  Fit that into our promise-based
-          // control flow by wrapping the install object.  However,
-          // xpInstallConfirm.xul matches the install object it is passed
-          // with the argument passed to an InstallListener, so give it
-          // access to the underlying object through the .wrapped property.
-          let proxy = new Proxy(install, {
-            get(target, property) {
-              if (property == "install") {
-                return resolve;
-              } else if (property == "cancel") {
-                return reject;
-              } else if (property == "wrapped") {
-                return target;
+        this._verifyThirdPartyInstall(browser, url, install, info, source)
+          .then(() => {
+            // All installs end up in this callback when the add-on is available
+            // for installation.  There are numerous different things that can
+            // happen from here though.  For webextensions, if the application
+            // implements webextension permission prompts, those always take
+            // precedence.
+            // If this add-on is not a webextension or if the application does not
+            // implement permission prompts, no confirmation is displayed for
+            // installs created from about:addons (in which case requireConfirm
+            // is false).
+            // In the remaining cases, a confirmation prompt is displayed but the
+            // application may override it either by implementing the
+            // "@mozilla.org/addons/web-install-prompt;1" contract or by setting
+            // the customConfirmationUI preference and responding to the
+            // "addon-install-confirmation" notification.  If the application
+            // does not implement its own prompt, use the built-in xul dialog.
+            if (info.addon.userPermissions && WEBEXT_PERMISSION_PROMPTS) {
+              let subject = {
+                wrappedJSObject: {
+                  target: browser,
+                  info: Object.assign({ resolve, reject, source }, info),
+                },
+              };
+              subject.wrappedJSObject.info.permissions =
+                info.addon.userPermissions;
+              Services.obs.notifyObservers(
+                subject,
+                "webextension-permission-prompt"
+              );
+            } else if (requireConfirm) {
+              // The methods below all want to call the install() or cancel()
+              // method on the provided AddonInstall object to either accept
+              // or reject the confirmation.  Fit that into our promise-based
+              // control flow by wrapping the install object.  However,
+              // xpInstallConfirm.xul matches the install object it is passed
+              // with the argument passed to an InstallListener, so give it
+              // access to the underlying object through the .wrapped property.
+              let proxy = new Proxy(install, {
+                get(target, property) {
+                  if (property == "install") {
+                    return resolve;
+                  } else if (property == "cancel") {
+                    return reject;
+                  } else if (property == "wrapped") {
+                    return target;
+                  }
+                  let result = target[property];
+                  return typeof result == "function"
+                    ? result.bind(target)
+                    : result;
+                },
+              });
+
+              // Check for a custom installation prompt that may be provided by the
+              // applicaton
+              if ("@mozilla.org/addons/web-install-prompt;1" in Cc) {
+                try {
+                  let prompt = Cc[
+                    "@mozilla.org/addons/web-install-prompt;1"
+                  ].getService(Ci.amIWebInstallPrompt);
+                  prompt.confirm(browser, url, [proxy]);
+                  return;
+                } catch (e) {}
               }
-              let result = target[property];
-              return typeof result == "function" ? result.bind(target) : result;
-            },
+
+              this.installNotifyObservers(
+                "addon-install-confirmation",
+                browser,
+                url,
+                proxy
+              );
+            } else {
+              resolve();
+            }
+          })
+          .catch(e => {
+            // Error is undefined if the promise was rejected.
+            if (e) {
+              Cu.reportError(`Install prompt handler error: ${e}`);
+            }
+            reject();
           });
-
-          // Check for a custom installation prompt that may be provided by the
-          // applicaton
-          if ("@mozilla.org/addons/web-install-prompt;1" in Cc) {
-            try {
-              let prompt = Cc[
-                "@mozilla.org/addons/web-install-prompt;1"
-              ].getService(Ci.amIWebInstallPrompt);
-              prompt.confirm(browser, url, [proxy]);
-              return;
-            } catch (e) {}
-          }
-
-          this.installNotifyObservers(
-            "addon-install-confirmation",
-            browser,
-            url,
-            proxy
-          );
-        } else {
-          resolve();
-        }
       });
   },
 
   webAPI: {
     // installs maps integer ids to AddonInstall instances.
     installs: new Map(),
     nextInstall: 0,
 
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -950,16 +950,25 @@ AddonWrapper = class {
     }
     return [];
   }
 
   get isRecommended() {
     return this.recommendationStates.includes("recommended");
   }
 
+  get canBypassThirdParyInstallPrompt() {
+    // We only bypass if the extension is signed (to support distributions
+    // that turn off the signing requirement) and has recommendation states.
+    return (
+      this.signedState >= AddonManager.SIGNEDSTATE_SIGNED &&
+      this.recommendationStates.length
+    );
+  }
+
   get applyBackgroundUpdates() {
     return addonFor(this).applyBackgroundUpdates;
   }
   set applyBackgroundUpdates(val) {
     let addon = addonFor(this);
     if (
       val != AddonManager.AUTOUPDATE_DEFAULT &&
       val != AddonManager.AUTOUPDATE_DISABLE &&
--- a/toolkit/mozapps/extensions/test/xpinstall/browser.ini
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser.ini
@@ -11,16 +11,17 @@ support-files =
   enabled.html
   hashRedirect.sjs
   head.js
   incompatible.xpi
   installchrome.html
   installtrigger.html
   installtrigger_frame.html
   navigate.html
+  recommended.xpi
   redirect.sjs
   slowinstall.sjs
   startsoftwareupdate.html
   triggerredirect.html
   unsigned.xpi
 
 [browser_amosigned_trigger.js]
 [browser_amosigned_trigger_iframe.js]
@@ -34,16 +35,17 @@ support-files =
 [browser_badhash.js]
 [browser_badhashtype.js]
 [browser_block_fullscreen_prompt.js]
 skip-if = (os == 'mac' && debug) #Bug 1590136
 [browser_bug540558.js]
 [browser_bug611242.js]
 [browser_bug638292.js]
 [browser_bug645699.js]
+[browser_bug645699_postDownload.js]
 [browser_bug672485.js]
 skip-if = true # disabled due to a leak. See bug 682410.
 [browser_containers.js]
 [browser_cookies.js]
 [browser_cookies2.js]
 [browser_cookies3.js]
 [browser_cookies4.js]
 skip-if = true # Bug 1084646
@@ -64,16 +66,17 @@ skip-if = (os == 'win' && os_version == 
 [browser_httphash5.js]
 [browser_httphash6.js]
 skip-if = true # Bug 1449788
 [browser_installchrome.js]
 [browser_localfile.js]
 [browser_localfile2.js]
 [browser_localfile3.js]
 [browser_localfile4.js]
+[browser_localfile4_postDownload.js]
 [browser_newwindow.js]
 skip-if = !debug  # This is a test for leaks, see comment in the test.
 [browser_offline.js]
 [browser_privatebrowsing.js]
 skip-if = debug # Bug 1541577 - leaks on debug
 [browser_relative.js]
 [browser_softwareupdate.js]
 [browser_trigger_redirect.js]
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js
@@ -1,13 +1,18 @@
 // ----------------------------------------------------------------------------
 // Tests installing an unsigned add-on through an InstallTrigger call in web
 // content. This should be blocked by the whitelist check.
 // This verifies bug 645699
 function test() {
+  // prompt prior to download
+  SpecialPowers.pushPrefEnv({
+    set: [["extensions.postDownloadThirdPartyPrompt", false]],
+  });
+
   Harness.installConfirmCallback = confirm_install;
   Harness.installBlockedCallback = allow_blocked;
   Harness.installsCompletedCallback = finish_test;
   Harness.setup();
 
   PermissionTestUtils.add(
     "http://example.org/",
     "install",
@@ -34,14 +39,14 @@ function allow_blocked(installInfo) {
 
 function confirm_install(panel) {
   ok(false, "Should not see the install dialog");
   return false;
 }
 
 function finish_test(count) {
   is(count, 0, "0 Add-ons should have been successfully installed");
-  PermissionTestUtils.remove("http://addons.mozilla.org", "install");
+  PermissionTestUtils.remove("http://example.org/", "install");
 
   gBrowser.removeCurrentTab();
   Harness.finish();
 }
 // ----------------------------------------------------------------------------
copy from toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js
copy to toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js
@@ -1,16 +1,21 @@
 // ----------------------------------------------------------------------------
 // Tests installing an unsigned add-on through an InstallTrigger call in web
-// content. This should be blocked by the whitelist check.
+// content. This should be blocked by the origin allow check.
 // This verifies bug 645699
 function test() {
   Harness.installConfirmCallback = confirm_install;
   Harness.installBlockedCallback = allow_blocked;
   Harness.installsCompletedCallback = finish_test;
+  // Prevent the Harness from ending the test on download cancel.
+  Harness.downloadCancelledCallback = () => {
+    return false;
+  };
+
   Harness.setup();
 
   PermissionTestUtils.add(
     "http://example.org/",
     "install",
     Services.perms.ALLOW_ACTION
   );
 
@@ -34,14 +39,14 @@ function allow_blocked(installInfo) {
 
 function confirm_install(panel) {
   ok(false, "Should not see the install dialog");
   return false;
 }
 
 function finish_test(count) {
   is(count, 0, "0 Add-ons should have been successfully installed");
-  PermissionTestUtils.remove("http://addons.mozilla.org", "install");
+  PermissionTestUtils.remove("http://example.org/", "install");
 
   gBrowser.removeCurrentTab();
   Harness.finish();
 }
 // ----------------------------------------------------------------------------
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js
@@ -368,16 +368,20 @@ var TESTS = [
 
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
     let installs = await AddonManager.getAllInstalls();
     is(installs.length, 0, "Shouldn't be any pending installs");
     await SpecialPowers.popPrefEnv();
   },
 
   async function test_blockedInstall() {
+    SpecialPowers.pushPrefEnv({
+      set: [["extensions.postDownloadThirdPartyPrompt", false]],
+    });
+
     let notificationPromise = waitForNotification("addon-install-blocked");
     let triggers = encodeURIComponent(
       JSON.stringify({
         XPI: "amosigned.xpi",
       })
     );
     BrowserTestUtils.openNewForegroundTab(
       gBrowser,
@@ -398,16 +402,17 @@ var TESTS = [
       message.textContent,
       "You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.",
       "Should have seen the right message"
     );
 
     let dialogPromise = waitForInstallDialog();
     // Click on Allow
     EventUtils.synthesizeMouse(notification.button, 20, 10, {});
+
     // Notification should have changed to progress notification
     ok(PopupNotifications.isPanelOpen, "Notification should still be open");
     notification = panel.childNodes[0];
     is(
       notification.id,
       "addon-progress-notification",
       "Should have seen the progress notification"
     );
@@ -428,16 +433,115 @@ var TESTS = [
     let addon = await AddonManager.getAddonByID(
       "amosigned-xpi@tests.mozilla.org"
     );
     await addon.uninstall();
 
     await BrowserTestUtils.removeTab(gBrowser.selectedTab);
   },
 
+  async function test_blockedPostDownload() {
+    SpecialPowers.pushPrefEnv({
+      set: [["extensions.postDownloadThirdPartyPrompt", true]],
+    });
+
+    let notificationPromise = waitForNotification("addon-install-blocked");
+    let triggers = encodeURIComponent(
+      JSON.stringify({
+        XPI: "amosigned.xpi",
+      })
+    );
+    BrowserTestUtils.openNewForegroundTab(
+      gBrowser,
+      TESTROOT + "installtrigger.html?" + triggers
+    );
+    let panel = await notificationPromise;
+
+    let notification = panel.childNodes[0];
+    is(
+      notification.button.label,
+      "Continue to Installation",
+      "Should have seen the right button"
+    );
+    let message = panel.ownerDocument.getElementById(
+      "addon-install-blocked-message"
+    );
+    is(
+      message.textContent,
+      "You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.",
+      "Should have seen the right message"
+    );
+
+    let dialogPromise = waitForInstallDialog();
+    // Click on Allow
+    EventUtils.synthesizeMouse(notification.button, 20, 10, {});
+
+    let installDialog = await dialogPromise;
+
+    notificationPromise = acceptAppMenuNotificationWhenShown(
+      "addon-installed",
+      "amosigned-xpi@tests.mozilla.org"
+    );
+
+    installDialog.button.click();
+    await notificationPromise;
+
+    let installs = await AddonManager.getAllInstalls();
+    is(installs.length, 0, "Should be no pending installs");
+
+    let addon = await AddonManager.getAddonByID(
+      "amosigned-xpi@tests.mozilla.org"
+    );
+    await addon.uninstall();
+
+    await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    await SpecialPowers.popPrefEnv();
+  },
+
+  async function test_recommendedPostDownload() {
+    // recommended.xpi is dev root signed
+    SpecialPowers.pushPrefEnv({
+      set: [
+        ["extensions.postDownloadThirdPartyPrompt", true],
+        ["xpinstall.signatures.dev-root", true],
+      ],
+    });
+
+    let triggers = encodeURIComponent(
+      JSON.stringify({
+        XPI: "recommended.xpi",
+      })
+    );
+    BrowserTestUtils.openNewForegroundTab(
+      gBrowser,
+      TESTROOT + "installtrigger.html?" + triggers
+    );
+
+    let installDialog = await waitForInstallDialog();
+
+    let notificationPromise = acceptAppMenuNotificationWhenShown(
+      "addon-installed",
+      "recommended-xpi@tests.mozilla.orgs"
+    );
+
+    installDialog.button.click();
+    await notificationPromise;
+
+    let installs = await AddonManager.getAllInstalls();
+    is(installs.length, 0, "Should be no pending installs");
+
+    let addon = await AddonManager.getAddonByID(
+      "recommended-xpi@tests.mozilla.orgs"
+    );
+    await addon.uninstall();
+
+    await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    await SpecialPowers.popPrefEnv();
+  },
+
   async function test_permaBlockInstall() {
     let notificationPromise = waitForNotification("addon-install-blocked");
     let triggers = encodeURIComponent(
       JSON.stringify({
         XPI: "amosigned.xpi",
       })
     );
     let target = TESTROOT + "installtrigger.html?" + triggers;
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js
@@ -1,17 +1,22 @@
 // ----------------------------------------------------------------------------
-// Tests installing an add-on from a local file with whitelisting disabled.
-// This should be blocked by the whitelist check.
+// Tests installing an add-on from a local file with file origins disabled.
+// This should be blocked by the origin allowed check.
 function test() {
+  // prompt prior to download
+  SpecialPowers.pushPrefEnv({
+    set: [["extensions.postDownloadThirdPartyPrompt", false]],
+  });
+
   Harness.installBlockedCallback = allow_blocked;
   Harness.installsCompletedCallback = finish_test;
   Harness.setup();
 
-  // Disable file request whitelisting, installing by file referrer should be blocked.
+  // Disable local file install, installing by file referrer should be blocked.
   Services.prefs.setBoolPref("xpinstall.whitelist.fileRequest", false);
 
   var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
     Ci.nsIChromeRegistry
   );
 
   var chromeroot = extractChromeRoot(gTestPath);
   var xpipath = chromeroot;
copy from toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js
copy to toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js
@@ -1,17 +1,21 @@
 // ----------------------------------------------------------------------------
-// Tests installing an add-on from a local file with whitelisting disabled.
-// This should be blocked by the whitelist check.
+// Tests installing an add-on from a local file with file origins disabled.
+// This should be blocked by the origin allowed check.
 function test() {
   Harness.installBlockedCallback = allow_blocked;
   Harness.installsCompletedCallback = finish_test;
+  // Prevent the Harness from ending the test on download cancel.
+  Harness.downloadCancelledCallback = () => {
+    return false;
+  };
   Harness.setup();
 
-  // Disable file request whitelisting, installing by file referrer should be blocked.
+  // Disable local file install, installing by file referrer should be blocked.
   Services.prefs.setBoolPref("xpinstall.whitelist.fileRequest", false);
 
   var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
     Ci.nsIChromeRegistry
   );
 
   var chromeroot = extractChromeRoot(gTestPath);
   var xpipath = chromeroot;
--- a/toolkit/mozapps/extensions/test/xpinstall/head.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/head.js
@@ -407,18 +407,21 @@ var Harness = {
   onDownloadCancelled(install) {
     isnot(
       this.runningInstalls.indexOf(install),
       -1,
       "Should only see cancelations for started installs"
     );
     this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1);
 
-    if (this.downloadCancelledCallback) {
-      this.downloadCancelledCallback(install);
+    if (
+      this.downloadCancelledCallback &&
+      this.downloadCancelledCallback(install) === false
+    ) {
+      return;
     }
     this.checkTestEnded();
   },
 
   onDownloadFailed(install) {
     if (this.downloadFailedCallback) {
       this.downloadFailedCallback(install);
     }
new file mode 100644
index 0000000000000000000000000000000000000000..6369b81b0a94a116b23f13c48b2f98455bafe6c3
GIT binary patch
literal 7958
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
P`aMQ}C(ldvpVj{ZGt`C?
--- a/tools/lint/rejected-words.yml
+++ b/tools/lint/rejected-words.yml
@@ -438,16 +438,17 @@ avoid-blacklist-and-whitelist:
         - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js
         - toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js
         - toolkit/mozapps/extensions/test/xpcshell/test_permissions.js
         - toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js
         - toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js
         - toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js
         - toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js
         - toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js
+        - toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js
         - toolkit/mozapps/extensions/test/xpinstall/browser_whitelist.js
         - toolkit/mozapps/extensions/test/xpinstall/head.js
         - toolkit/xre/nsAppRunner.cpp
         - toolkit/xre/nsEmbedFunctions.cpp
         - toolkit/xre/nsXREDirProvider.cpp
         - tools/crashreporter/system-symbols/win/symsrv-fetch.py
         - tools/fuzzing/faulty/Faulty.cpp
         - tools/fuzzing/faulty/Faulty.h