Bug 1356462 Show a notification when non-MPC extensions are disabled draft
authorAndrew Swan <aswan@mozilla.com>
Wed, 19 Apr 2017 11:30:24 -0700
changeset 566528 4f6b385936ebc8b9d02eea1026eb23b641bea49d
parent 566527 14041458355f3f93beeccc7f80f53b3fc8b59c5f
child 569007 9f83f42258cfe7c796fb85d8c75571279646fc41
push id55253
push useraswan@mozilla.com
push dateFri, 21 Apr 2017 19:24:08 +0000
bugs1356462
milestone55.0a1
Bug 1356462 Show a notification when non-MPC extensions are disabled MozReview-Commit-ID: 8KUhRe91AFt
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.properties
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -753,16 +753,37 @@ BrowserGlue.prototype = {
       },
     ];
 
     let nb = win.document.getElementById("high-priority-global-notificationbox");
     nb.appendNotification(message, "unsigned-addons-disabled", "",
                           nb.PRIORITY_WARNING_MEDIUM, buttons);
   },
 
+  _notifyDisabledNonMpc() {
+    let win = RecentWindow.getMostRecentBrowserWindow();
+    if (!win)
+      return;
+
+    let message = win.gNavigatorBundle.getString("nonMpcDisabled.message");
+    let buttons = [
+      {
+        label: win.gNavigatorBundle.getString("nonMpcDisabled.manage.label"),
+        accessKey: win.gNavigatorBundle.getString("nonMpcDisabled.manage.accessKey"),
+        callback() {
+          win.BrowserOpenAddonsMgr("addons://list/extension");
+        }
+      },
+    ];
+
+    let nb = win.document.getElementById("high-priority-global-notificationbox");
+    nb.appendNotification(message, "non-mpc-addons-disabled", "",
+                          nb.PRIORITY_WARNING_MEDIUM, buttons);
+  },
+
   _firstWindowTelemetry(aWindow) {
     let SCALING_PROBE_NAME = "";
     switch (AppConstants.platform) {
       case "win":
         SCALING_PROBE_NAME = "DISPLAY_SCALING_MSWIN";
         break;
       case "macosx":
         SCALING_PROBE_NAME = "DISPLAY_SCALING_OSX";
@@ -959,16 +980,20 @@ BrowserGlue.prototype = {
           if (addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
             this._notifyUnsignedAddonsDisabled();
             break;
           }
         }
       });
     }
 
+    if (AddonManager.nonMpcDisabled) {
+      this._notifyDisabledNonMpc();
+    }
+
     // Perform default browser checking.
     if (ShellService) {
       let shouldCheck = AppConstants.DEBUG ? false :
                                              ShellService.shouldCheckDefaultBrowser;
 
       const skipDefaultBrowserCheck =
         Services.prefs.getBoolPref("browser.shell.skipDefaultBrowserCheckOnFirstRun") &&
         Services.prefs.getBoolPref("browser.shell.skipDefaultBrowserCheck");
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -209,16 +209,20 @@ addonInstallErrorIncompatible=%3$S could
 
 # LOCALIZATION NOTE (addonInstallErrorBlocklisted): %S is add-on name
 addonInstallErrorBlocklisted=%S could not be installed because it has a high risk of causing stability or security problems.
 
 unsignedAddonsDisabled.message=One or more installed add-ons cannot be verified and have been disabled.
 unsignedAddonsDisabled.learnMore.label=Learn More
 unsignedAddonsDisabled.learnMore.accesskey=L
 
+nonMpcDisabled.message=Due to performance testing, we have disabled some of your add-ons. They can be re-enabled in the Add-ons Manager.
+nonMpcDisabled.manage.label=Manage Add-Ons
+nonMpcDisabled.manage.accessKey=M
+
 # LOCALIZATION NOTE (compactLightTheme.name): This is displayed in about:addons -> Appearance
 compactLightTheme.name=Compact Light
 compactLightTheme.description=A compact theme with a light color scheme.
 
 # LOCALIZATION NOTE (compactDarkTheme.name): This is displayed in about:addons -> Appearance
 compactDarkTheme.name=Compact Dark
 compactDarkTheme.description=A compact theme with a dark color scheme.
 
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -620,16 +620,17 @@ var gUpdateEnabled = true;
 var gAutoUpdateDefault = true;
 var gHotfixID = "";
 var gWebExtensionsMinPlatformVersion = "";
 var gShutdownBarrier = null;
 var gRepoShutdownState = "";
 var gShutdownInProgress = false;
 var gPluginPageListener = null;
 var gBrowserUpdated = null;
+var gNonMpcDisabled = false;
 
 /**
  * This is the real manager, kept here rather than in AddonManager to keep its
  * contents hidden from API users.
  */
 var AddonManagerInternal = {
   managerListeners: [],
   installListeners: [],
@@ -3243,16 +3244,20 @@ this.AddonManagerPrivate = {
 
      if (!extensionId || typeof extensionId != "string")
        throw Components.Exception("extensionId must be a string",
                                   Cr.NS_ERROR_INVALID_ARG);
 
     return AddonManagerInternal._getProviderByName("XPIProvider")
                                .isTemporaryInstallID(extensionId);
   },
+
+  set nonMpcDisabled(val) {
+    gNonMpcDisabled = val;
+  },
 };
 
 /**
  * This is the public API that UI and developers should be calling. All methods
  * just forward to AddonManagerInternal.
  */
 this.AddonManager = {
   // Constants for the AddonInstall.state property
@@ -3741,16 +3746,20 @@ this.AddonManager = {
   getPreferredIconURL(aAddon, aSize, aWindow = undefined) {
     return AddonManagerInternal.getPreferredIconURL(aAddon, aSize, aWindow);
   },
 
   get webAPI() {
     return AddonManagerInternal.webAPI;
   },
 
+  get nonMpcDisabled() {
+    return gNonMpcDisabled;
+  },
+
   get shutdown() {
     return gShutdownBarrier.client;
   },
 };
 
 this.AddonManager.init();
 
 // load the timestamps module into AddonManagerInternal
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -30,16 +30,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "Blocklist",
                                    "@mozilla.org/extensions/blocklist;1",
                                    Ci.nsIBlocklistService);
 
+XPCOMUtils.defineLazyPreferenceGetter(this, "ALLOW_NON_MPC",
+                                      "extensions.allow-non-mpc-extensions");
+
 Cu.import("resource://gre/modules/Log.jsm");
 const LOGGER_ID = "addons.xpi-utils";
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile");
 
 // Create a new logger for use by the Addons XPI Provider Utils
 // (Requires AddonManager.jsm)
 var logger = Log.repository.getLogger(LOGGER_ID);
@@ -1928,16 +1931,18 @@ this.XPIDatabaseReconcile = {
                   oldtime: oldAddon.updateDate
                 });
               } else {
                 XPIProvider.setTelemetry(oldAddon.id, "modifiedFile",
                                          XPIProvider._mostRecentlyModifiedFile[id]);
               }
             }
 
+            let wasDisabled = oldAddon.appDisabled;
+
             // The add-on has changed if the modification time has changed, if
             // we have an updated manifest for it, or if the schema version has
             // changed.
             //
             // Also reload the metadata for add-ons in the application directory
             // when the application version has changed.
             let newAddon = loadedManifest(installLocation, id);
             if (newAddon || oldAddon.updateDate != xpiState.mtime ||
@@ -1953,16 +1958,28 @@ this.XPIDatabaseReconcile = {
               newAddon = this.updateCompatibility(installLocation, oldAddon, xpiState,
                                                   aOldAppVersion, aOldPlatformVersion,
                                                   aSchemaChange);
             } else {
               // No change
               newAddon = oldAddon;
             }
 
+            // If an extension has just become appDisabled and it appears to
+            // be due to the ALLOW_NON_MPC pref, show a notification.  If the
+            // extension is also disabled for some other reason(s), don't
+            // bother with the notification since flipping the pref will leave
+            // the extension disabled.
+            if (!wasDisabled && newAddon.appDisabled &&
+                !ALLOW_NON_MPC && !newAddon.multiprocessCompatible &&
+                (newAddon.blocklistState != Ci.nsIBlocklistService.STATE_BLOCKED) &&
+                newAddon.isPlatformCompatible && newAddon.isCompatible) {
+              AddonManagerPrivate.nonMpcDisabled = true;
+            }
+
             if (newAddon)
               locationAddonMap.set(newAddon.id, newAddon);
           } else {
             // The add-on is in the DB, but not in xpiState (and thus not on disk).
             this.removeMetadata(oldAddon);
           }
         }
       }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js
@@ -1,14 +1,18 @@
 Components.utils.import("resource://testing-common/httpd.js");
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
 var gServer;
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
+const NON_MPC_PREF = "extensions.allow-non-mpc-extensions";
+
 Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
 
 function build_test(multiprocessCompatible, bootstrap, updateMultiprocessCompatible) {
   return function* () {
     dump("Running test" +
       " multiprocessCompatible: " + multiprocessCompatible +
       " bootstrap: " + bootstrap +
       " updateMultiprocessCompatible: " + updateMultiprocessCompatible +
@@ -98,17 +102,16 @@ for (let bootstrap of [false, true]) {
   for (let multiprocessCompatible of [undefined, false, true]) {
     for (let updateMultiprocessCompatible of [undefined, false, true]) {
       add_task(build_test(multiprocessCompatible, bootstrap, updateMultiprocessCompatible));
     }
   }
 }
 
 add_task(async function test_disable() {
-  const PREF = "extensions.allow-non-mpc-extensions";
   const ID_MPC = "mpc@tests.mozilla.org";
   const ID_NON_MPC = "non-mpc@tests.mozilla.org";
 
   let addonData = {
     name: "Test Add-on",
     version: "1.0",
     bootstrap: true,
     targetApplications: [{
@@ -124,17 +127,17 @@ add_task(async function test_disable() {
   }, addonData));
   let xpi2 = createTempXPIFile(Object.assign({
       id: ID_NON_MPC,
       multiprocessCompatible: false,
   }, addonData));
 
   async function testOnce(initialAllow) {
     if (initialAllow !== undefined) {
-      Services.prefs.setBoolPref(PREF, initialAllow);
+      Services.prefs.setBoolPref(NON_MPC_PREF, initialAllow);
     }
 
     let install1 = await AddonManager.getInstallForFile(xpi1);
     let install2 = await AddonManager.getInstallForFile(xpi2);
     await promiseCompleteAllInstalls([install1, install2]);
 
     let [addon1, addon2] = await AddonManager.getAddonsByIDs([ID_MPC, ID_NON_MPC]);
     do_check_neq(addon1, null);
@@ -142,39 +145,176 @@ add_task(async function test_disable() {
     do_check_eq(addon1.appDisabled, false);
 
     do_check_neq(addon2, null);
     do_check_eq(addon2.multiprocessCompatible, false);
     do_check_eq(addon2.appDisabled, initialAllow === false);
 
     // Flip the allow-non-mpc preference
     let newValue = (initialAllow === true) ? false : true;
-    Services.prefs.setBoolPref(PREF, newValue);
+    Services.prefs.setBoolPref(NON_MPC_PREF, newValue);
 
     // the mpc extension should never become appDisabled
     do_check_eq(addon1.appDisabled, false);
 
     // The non-mpc extension should become disabled if we don't allow non-mpc
     do_check_eq(addon2.appDisabled, !newValue);
 
     // Flip the pref back and check appDisabled
-    Services.prefs.setBoolPref(PREF, !newValue);
+    Services.prefs.setBoolPref(NON_MPC_PREF, !newValue);
 
     do_check_eq(addon1.appDisabled, false);
     do_check_eq(addon2.appDisabled, newValue);
 
     addon1.uninstall();
     addon2.uninstall();
   }
 
   await testOnce(undefined);
   await testOnce(true);
   await testOnce(false);
 
-  Services.prefs.clearUserPref(PREF);
+  Services.prefs.clearUserPref(NON_MPC_PREF);
+});
+
+// Test that the nonMpcDisabled flag gets set properly at startup
+// when the allow-non-mpc-extensions pref is flipped.
+add_task(async function test_restart() {
+  const ID = "non-mpc@tests.mozilla.org";
+
+  let xpifile = createTempXPIFile({
+    id: ID,
+    name: "Test Add-on",
+    version: "1.0",
+    bootstrap: true,
+    multiprocessCompatible: false,
+    targetApplications: [{
+      id: "xpcshell@tests.mozilla.org",
+      minVersion: "1",
+      maxVersion: "2"
+    }]
+  });
+
+  Services.prefs.setBoolPref(NON_MPC_PREF, true);
+  let install = await AddonManager.getInstallForFile(xpifile);
+  await promiseCompleteAllInstalls([install]);
+
+  let addon = await AddonManager.getAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_eq(addon.multiprocessCompatible, false);
+  do_check_eq(addon.appDisabled, false);
+
+  // Simulate a new app version in which the allow-non-mpc-extensions
+  // pref is flipped.
+  await promiseShutdownManager();
+  Services.prefs.setBoolPref(NON_MPC_PREF, false);
+  gAppInfo.version = "1.5";
+  await promiseStartupManager();
+
+  addon = await AddonManager.getAddonByID(ID);
+  do_check_neq(addon, null);
+  do_check_eq(addon.appDisabled, true);
+
+  // The flag we use for startup notification should be true
+  do_check_eq(AddonManager.nonMpcDisabled, true);
+
+  addon.uninstall();
+
+  Services.prefs.clearUserPref(NON_MPC_PREF);
+  AddonManagerPrivate.nonMpcDisabled = false;
+});
+
+// Test that the nonMpcDisabled flag is not set if there are non-mpc
+// extensions that are also disabled for some other reason.
+add_task(async function test_restart2() {
+  const ID1 = "blocked@tests.mozilla.org";
+  let xpi1 = createTempXPIFile({
+    id: ID1,
+    name: "Blocked Add-on",
+    version: "1.0",
+    bootstrap: true,
+    multiprocessCompatible: false,
+    targetApplications: [{
+      id: "xpcshell@tests.mozilla.org",
+      minVersion: "1",
+      maxVersion: "2"
+    }]
+  });
+
+  const ID2 = "incompatible@tests.mozilla.org";
+  let xpi2 = createTempXPIFile({
+    id: ID2,
+    name: "Incompatible Add-on",
+    version: "1.0",
+    bootstrap: true,
+    multiprocessCompatible: false,
+    targetApplications: [{
+      id: "xpcshell@tests.mozilla.org",
+      minVersion: "1",
+      maxVersion: "1.5"
+    }]
+  });
+
+  const BLOCKLIST = `<?xml version="1.0"?>
+  <blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1396046918000">
+  <emItems>
+  <emItem  blockID="i454" id="${ID1}">
+  <versionRange  minVersion="0" maxVersion="*" severity="3"/>
+  </emItem>
+  </emItems>
+  </blocklist>`;
+
+
+  Services.prefs.setBoolPref(NON_MPC_PREF, true);
+  let install1 = await AddonManager.getInstallForFile(xpi1);
+  let install2 = await AddonManager.getInstallForFile(xpi2);
+  await promiseCompleteAllInstalls([install1, install2]);
+
+  let [addon1, addon2] = await AddonManager.getAddonsByIDs([ID1, ID2]);
+  do_check_neq(addon1, null);
+  do_check_eq(addon1.multiprocessCompatible, false);
+  do_check_eq(addon1.appDisabled, false);
+  do_check_neq(addon2, null);
+  do_check_eq(addon2.multiprocessCompatible, false);
+  do_check_eq(addon2.appDisabled, false);
+
+  await promiseShutdownManager();
+
+  Services.prefs.setBoolPref(NON_MPC_PREF, false);
+  gAppInfo.version = "2";
+
+  // Simulate including a new blocklist with the new version by
+  // flipping the pref below which causes the blocklist to be re-read.
+  let blocklistPath = OS.Path.join(OS.Constants.Path.profileDir, "blocklist.xml");
+  await OS.File.writeAtomic(blocklistPath, BLOCKLIST);
+  let BLOCKLIST_PREF = "extensions.blocklist.enabled";
+  Services.prefs.setBoolPref(BLOCKLIST_PREF, false);
+  Services.prefs.setBoolPref(BLOCKLIST_PREF, true);
+
+  await promiseStartupManager();
+
+  // When we restart, one of the test addons should be blocklisted, and
+  // one is incompatible.  Both are MPC=false but that should not trigger
+  // the startup notification since flipping allow-non-mpc-extensions
+  // won't re-enable either extension.
+  const {STATE_BLOCKED} = Components.interfaces.nsIBlocklistService;
+  [addon1, addon2] = await AddonManager.getAddonsByIDs([ID1, ID2]);
+  do_check_neq(addon1, null);
+  do_check_eq(addon1.appDisabled, true);
+  do_check_eq(addon1.blocklistState, STATE_BLOCKED);
+  do_check_neq(addon2, null);
+  do_check_eq(addon2.appDisabled, true);
+  do_check_eq(addon2.isCompatible, false);
+
+  do_check_eq(AddonManager.nonMpcDisabled, false);
+
+  addon1.uninstall();
+  addon2.uninstall();
+
+  Services.prefs.clearUserPref(NON_MPC_PREF);
 });
 
 function run_test() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
   startupManager();
 
   // Create and configure the HTTP server.
   gServer = new HttpServer();