Bug 1354602 - Enabling containers for container addons on startup. r=aswan,mconley,zombie
authorJonathan Kingston <jkt@mozilla.com>
Sun, 13 Aug 2017 16:17:41 +0100
changeset 647052 33a59196dc7001ce459f94de87733f213064d5a7
parent 647051 cfef5aec8f82a572acaafc56e50d7125de7076e1
child 647053 36037d88c1b6e34d8547a5f17bc0b47d7b56fb90
push id74288
push userhikezoe@mozilla.com
push dateWed, 16 Aug 2017 00:19:57 +0000
reviewersaswan, mconley, zombie
bugs1354602
milestone57.0a1
Bug 1354602 - Enabling containers for container addons on startup. r=aswan,mconley,zombie MozReview-Commit-ID: BXLyQz8CGDl
browser/components/preferences/in-content-new/main.js
browser/components/preferences/in-content/privacy.js
toolkit/components/contextualidentity/ContextualIdentityService.jsm
toolkit/components/extensions/ext-contextualIdentities.js
toolkit/components/extensions/ext-toolkit.json
toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
--- a/browser/components/preferences/in-content-new/main.js
+++ b/browser/components/preferences/in-content-new/main.js
@@ -943,17 +943,16 @@ var gMainPane = {
     let cancelButton = bundlePreferences.getString("disableContainersButton2");
 
     let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
                       (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1);
 
     let rv = Services.prompt.confirmEx(window, title, message, buttonFlags,
                                        okButton, cancelButton, null, null, {});
     if (rv == 0) {
-      ContextualIdentityService.closeContainerTabs();
       Services.prefs.setBoolPref("privacy.userContext.enabled", false);
       return;
     }
 
     checkbox.checked = true;
   },
 
   /**
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -82,17 +82,16 @@ var gPrivacyPane = {
     let checkbox = document.getElementById("browserContainersCheckbox");
     if (checkbox.checked) {
       Services.prefs.setBoolPref("privacy.userContext.enabled", true);
       return;
     }
 
     let count = ContextualIdentityService.countContainerTabs();
     if (count == 0) {
-      ContextualIdentityService.notifyAllContainersCleared();
       Services.prefs.setBoolPref("privacy.userContext.enabled", false);
       return;
     }
 
     let bundlePreferences = document.getElementById("bundlePreferences");
 
     let title = bundlePreferences.getString("disableContainersAlertTitle");
     let message = PluralForm.get(count, bundlePreferences.getString("disableContainersMsg"))
@@ -103,19 +102,16 @@ var gPrivacyPane = {
 
     let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
                       (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1);
 
     let rv = Services.prompt.confirmEx(window, title, message, buttonFlags,
                                        okButton, cancelButton, null, null, {});
     if (rv == 0) {
       Services.prefs.setBoolPref("privacy.userContext.enabled", false);
-      ContextualIdentityService.closeContainerTabs().then(() => {
-        ContextualIdentityService.notifyAllContainersCleared();
-      });
       return;
     }
 
     checkbox.checked = true;
   },
 
   /**
    * Sets up the UI for the number of days of history to keep, and updates the
--- a/toolkit/components/contextualidentity/ContextualIdentityService.jsm
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.jsm
@@ -6,16 +6,17 @@ this.EXPORTED_SYMBOLS = ["ContextualIden
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 const DEFAULT_TAB_COLOR = "#909090";
 const SAVE_DELAY_MS = 1500;
+const CONTEXTUAL_IDENTITY_ENABLED_PREF = "privacy.userContext.enabled";
 
 XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
   return Services.strings.createBundle("chrome://browser/locale/browser.properties");
 });
 
 XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function() {
   return new TextDecoder();
 });
@@ -109,16 +110,28 @@ function _ContextualIdentityService(path
 
   _path: null,
   _dataReady: false,
 
   _saver: null,
 
   init(path) {
     this._path = path;
+
+    Services.prefs.addObserver(CONTEXTUAL_IDENTITY_ENABLED_PREF, this);
+  },
+
+  // observe() is only used to listen to container enabling pref
+  async observe() {
+    const contextualIdentitiesEnabled = Services.prefs.getBoolPref(CONTEXTUAL_IDENTITY_ENABLED_PREF);
+    if (!contextualIdentitiesEnabled) {
+      await this.closeContainerTabs();
+      this.notifyAllContainersCleared();
+      this.resetDefault();
+    }
   },
 
   load() {
     OS.File.read(this._path).then(bytes => {
       // If synchronous loading happened in the meantime, exit now.
       if (this._dataReady) {
         return;
       }
@@ -142,18 +155,23 @@ function _ContextualIdentityService(path
         this.loadError(error);
       }
     }, (error) => {
       this.loadError(error);
     });
   },
 
   resetDefault() {
-    this._identities = this._defaultIdentities;
+    this._identities = [];
+    // Clone the array
     this._lastUserContextId = this._defaultIdentities.length;
+    for (let i = 0; i < this._lastUserContextId; i++) {
+      this._identities.push(Object.assign({}, this._defaultIdentities[i]));
+    }
+    this._openedIdentities = new Set();
 
     this._dataReady = true;
 
     this.saveSoon();
   },
 
   loadError(error) {
     if (error != null &&
--- a/toolkit/components/extensions/ext-contextualIdentities.js
+++ b/toolkit/components/extensions/ext-contextualIdentities.js
@@ -3,16 +3,27 @@
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-toolkit.js */
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
                                   "resource://gre/modules/ContextualIdentityService.jsm");
 XPCOMUtils.defineLazyPreferenceGetter(this, "containersEnabled",
                                       "privacy.userContext.enabled");
 
+Cu.import("resource://gre/modules/ExtensionPreferencesManager.jsm");
+
+const CONTAINER_PREF_INSTALL_DEFAULTS = {
+  "privacy.userContext.enabled": true,
+  "privacy.userContext.longPressBehavior": 2,
+  "privacy.userContext.ui.enabled": true,
+  "privacy.usercontext.about_newtab_segregation.enabled": true,
+};
+
+const CONTAINERS_ENABLED_SETTING_NAME = "privacy.containers";
+
 const convertIdentity = identity => {
   let result = {
     name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
     icon: identity.icon,
     color: identity.color,
     cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
   };
 
@@ -26,68 +37,76 @@ const convertIdentityFromObserver = wrap
     icon: identity.icon,
     color: identity.color,
     cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
   };
 
   return result;
 };
 
+ExtensionPreferencesManager.addSetting(CONTAINERS_ENABLED_SETTING_NAME, {
+  prefNames: Object.keys(CONTAINER_PREF_INSTALL_DEFAULTS),
+
+  setCallback(value) {
+    if (value === true) {
+      return CONTAINER_PREF_INSTALL_DEFAULTS;
+    }
+
+    let prefs = {};
+    for (let pref of this.prefNames) {
+      prefs[pref] = undefined;
+    }
+    return prefs;
+  },
+});
+
 this.contextualIdentities = class extends ExtensionAPI {
+  onStartup() {
+    let {extension} = this;
+
+    if (extension.hasPermission("contextualIdentities")) {
+      ExtensionPreferencesManager.setSetting(extension, CONTAINERS_ENABLED_SETTING_NAME, true);
+    }
+  }
+
   getAPI(context) {
     let self = {
       contextualIdentities: {
         get(cookieStoreId) {
-          if (!containersEnabled) {
-            return Promise.resolve(false);
-          }
-
           let containerId = getContainerForCookieStoreId(cookieStoreId);
           if (!containerId) {
             return Promise.resolve(null);
           }
 
           let identity = ContextualIdentityService.getPublicIdentityFromId(containerId);
           return Promise.resolve(convertIdentity(identity));
         },
 
         query(details) {
-          if (!containersEnabled) {
-            return Promise.resolve(false);
-          }
-
           let identities = [];
           ContextualIdentityService.getPublicIdentities().forEach(identity => {
             if (details.name &&
                 ContextualIdentityService.getUserContextLabel(identity.userContextId) != details.name) {
               return;
             }
 
             identities.push(convertIdentity(identity));
           });
 
           return Promise.resolve(identities);
         },
 
         create(details) {
-          if (!containersEnabled) {
-            return Promise.resolve(false);
-          }
-
           let identity = ContextualIdentityService.create(details.name,
                                                           details.icon,
                                                           details.color);
           return Promise.resolve(convertIdentity(identity));
         },
 
         update(cookieStoreId, details) {
-          if (!containersEnabled) {
-            return Promise.resolve(false);
-          }
-
           let containerId = getContainerForCookieStoreId(cookieStoreId);
           if (!containerId) {
             return Promise.resolve(null);
           }
 
           let identity = ContextualIdentityService.getPublicIdentityFromId(containerId);
           if (!identity) {
             return Promise.resolve(null);
@@ -110,20 +129,16 @@ this.contextualIdentities = class extend
                                                 identity.color)) {
             return Promise.resolve(null);
           }
 
           return Promise.resolve(convertIdentity(identity));
         },
 
         remove(cookieStoreId) {
-          if (!containersEnabled) {
-            return Promise.resolve(false);
-          }
-
           let containerId = getContainerForCookieStoreId(cookieStoreId);
           if (!containerId) {
             return Promise.resolve(null);
           }
 
           let identity = ContextualIdentityService.getPublicIdentityFromId(containerId);
           if (!identity) {
             return Promise.resolve(null);
--- a/toolkit/components/extensions/ext-toolkit.json
+++ b/toolkit/components/extensions/ext-toolkit.json
@@ -23,16 +23,17 @@
     "paths": [
       ["browserSettings"]
     ]
   },
   "contextualIdentities": {
     "url": "chrome://extensions/content/ext-contextualIdentities.js",
     "schema": "chrome://extensions/content/schemas/contextual_identities.json",
     "scopes": ["addon_parent"],
+    "events": ["startup"],
     "paths": [
       ["contextualIdentities"]
     ]
   },
   "cookies": {
     "url": "chrome://extensions/content/ext-cookies.js",
     "schema": "chrome://extensions/content/schemas/cookies.json",
     "scopes": ["addon_parent"],
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
@@ -1,73 +1,44 @@
 "use strict";
 
 do_get_profile();
 
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://modules/AddonManager.jsm");
+add_task(async function startup() {
+  await ExtensionTestUtils.startAddonManager();
+});
+
 add_task(async function test_contextualIdentities_without_permissions() {
-  function backgroundScript() {
+  function background() {
     browser.test.assertTrue(!browser.contextualIdentities,
                             "contextualIdentities API is not available when the contextualIdentities permission is not required");
-    browser.test.notifyPass("contextualIdentities_permission");
+    browser.test.notifyPass("contextualIdentities_without_permission");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: `(${backgroundScript})()`,
+    useAddonManager: "temporary",
+    background,
     manifest: {
+      applications: {
+        gecko: {id: "testing@thing.com"},
+      },
       permissions: [],
     },
   });
 
   await extension.startup();
-  await extension.awaitFinish("contextualIdentities_permission");
+  await extension.awaitFinish("contextualIdentities_without_permission");
   await extension.unload();
 });
 
-
-add_task(async function test_contextualIdentity_no_containers() {
-  async function backgroundScript() {
-    let ci = await browser.contextualIdentities.get("foobar");
-    browser.test.assertEq(false, ci, "No identity should be returned here");
-
-    ci = await browser.contextualIdentities.get("firefox-container-1");
-    browser.test.assertEq(false, ci, "We don't have any identity");
-
-    let cis = await browser.contextualIdentities.query({});
-    browser.test.assertEq(false, cis, "no containers, 0 containers");
-
-    ci = await browser.contextualIdentities.create({name: "foobar", color: "red", icon: "icon"});
-    browser.test.assertEq(false, ci, "We don't have any identity");
-
-    ci = await browser.contextualIdentities.update("firefox-container-1", {name: "barfoo", color: "blue", icon: "icon icon"});
-    browser.test.assertEq(false, ci, "We don't have any identity");
-
-    ci = await browser.contextualIdentities.remove("firefox-container-1");
-    browser.test.assertEq(false, ci, "We have an identity");
-
-    browser.test.notifyPass("contextualIdentities");
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${backgroundScript})()`,
-    manifest: {
-      permissions: ["contextualIdentities"],
-    },
-  });
-
-  Services.prefs.setBoolPref("privacy.userContext.enabled", false);
-
-  await extension.startup();
-  await extension.awaitFinish("contextualIdentities");
-  await extension.unload();
-
-  Services.prefs.clearUserPref("privacy.userContext.enabled");
-});
-
 add_task(async function test_contextualIdentity_events() {
-  async function backgroundScript() {
+  const CONTAINERS_PREF = "privacy.userContext.enabled";
+  async function background() {
     function createOneTimeListener(type) {
       return new Promise((resolve, reject) => {
         try {
           browser.test.assertTrue(type in browser.contextualIdentities, `Found API object browser.contextualIdentities.${type}`);
           const listener = (change) => {
             browser.test.assertTrue("contextualIdentity" in change, `Found identity in change`);
             browser.contextualIdentities[type].removeListener(listener);
             resolve(change);
@@ -108,33 +79,39 @@ add_task(async function test_contextualI
     browser.test.assertTrue(!!ci, "We have an remove identity");
     const onRemoveListenerResponse = await onRemovePromise;
     assertExpected(onRemoveListenerResponse.contextualIdentity, Object.assign(updateContainerObj, {cookieStoreId}));
 
     browser.test.notifyPass("contextualIdentities_events");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: `(${backgroundScript})()`,
+    background,
+    useAddonManager: "temporary",
     manifest: {
+      applications: {
+        gecko: {id: "testing@thing.com"},
+      },
       permissions: ["contextualIdentities"],
     },
   });
 
-  Services.prefs.setBoolPref("privacy.userContext.enabled", true);
+  Services.prefs.setBoolPref(CONTAINERS_PREF, true);
 
   await extension.startup();
   await extension.awaitFinish("contextualIdentities_events");
   await extension.unload();
 
-  Services.prefs.clearUserPref("privacy.userContext.enabled");
+  Services.prefs.clearUserPref(CONTAINERS_PREF);
 });
 
 add_task(async function test_contextualIdentity_with_permissions() {
-  async function backgroundScript() {
+  const CONTAINERS_PREF = "privacy.userContext.enabled";
+  const initial = Services.prefs.getBoolPref(CONTAINERS_PREF);
+  async function background(ver) {
     let ci = await browser.contextualIdentities.get("foobar");
     browser.test.assertEq(null, ci, "No identity should be returned here");
 
     ci = await browser.contextualIdentities.get("firefox-container-1");
     browser.test.assertTrue(!!ci, "We have an identity");
     browser.test.assertTrue("name" in ci, "We have an identity.name");
     browser.test.assertTrue("color" in ci, "We have an identity.color");
     browser.test.assertTrue("icon" in ci, "We have an identity.icon");
@@ -184,24 +161,79 @@ add_task(async function test_contextualI
     browser.test.assertEq("blue", ci.color, "identity.color is correct");
     browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
 
     cis = await browser.contextualIdentities.query({});
     browser.test.assertEq(4, cis.length, "we are back to 4 identities");
 
     browser.test.notifyPass("contextualIdentities");
   }
+  function makeExtension(id) {
+    return ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      background,
+      manifest: {
+        applications: {
+          gecko: {id},
+        },
+        permissions: ["contextualIdentities"],
+      },
+    });
+  }
 
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${backgroundScript})()`,
-    manifest: {
-      permissions: ["contextualIdentities"],
-    },
-  });
-
-  Services.prefs.setBoolPref("privacy.userContext.enabled", true);
-
+  let extension = makeExtension("containers-test@mozilla.org");
   await extension.startup();
   await extension.awaitFinish("contextualIdentities");
+  equal(Services.prefs.getBoolPref(CONTAINERS_PREF), true, "Pref should now be enabled, whatever it's initial state");
   await extension.unload();
+  equal(Services.prefs.getBoolPref(CONTAINERS_PREF), initial, "Pref should now be initial state");
+
+  Services.prefs.clearUserPref(CONTAINERS_PREF);
+});
+
+add_task(async function test_contextualIdentity_extensions_enable_containers() {
+  const CONTAINERS_PREF = "privacy.userContext.enabled";
+  const initial = Services.prefs.getBoolPref(CONTAINERS_PREF);
+  async function background(ver) {
+    let ci = await browser.contextualIdentities.get("firefox-container-1");
+    browser.test.assertTrue(!!ci, "We have an identity");
 
-  Services.prefs.clearUserPref("privacy.userContext.enabled");
+    browser.test.notifyPass("contextualIdentities");
+  }
+  function makeExtension(id) {
+    return ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      background,
+      manifest: {
+        applications: {
+          gecko: {id},
+        },
+        permissions: ["contextualIdentities"],
+      },
+    });
+  }
+
+  let extension = makeExtension("containers-test@mozilla.org");
+  await extension.startup();
+  await extension.awaitFinish("contextualIdentities");
+  equal(Services.prefs.getBoolPref(CONTAINERS_PREF), true, "Pref should now be enabled, whatever it's initial state");
+  await extension.unload();
+  equal(Services.prefs.getBoolPref(CONTAINERS_PREF), initial, "Pref should now be initial state");
+
+  // Lets set containers explicitly to be on and test we keep it that way after removal
+  Services.prefs.setBoolPref(CONTAINERS_PREF, true);
+
+  let extension2 = makeExtension("containers-test-2@mozilla.org");
+  let extension3 = makeExtension("containers-test-3@mozilla.org");
+  await extension2.startup();
+  await extension2.awaitFinish("contextualIdentities");
+  await extension3.startup();
+  await extension3.awaitFinish("contextualIdentities");
+
+  // Flip the ordering to check it's still enabled
+  equal(Services.prefs.getBoolPref(CONTAINERS_PREF), true, "Pref should now be enabled 1");
+  await extension3.unload();
+  equal(Services.prefs.getBoolPref(CONTAINERS_PREF), true, "Pref should now be enabled 2");
+  await extension2.unload();
+  equal(Services.prefs.getBoolPref(CONTAINERS_PREF), true, "Pref should now be enabled 3");
+
+  Services.prefs.clearUserPref(CONTAINERS_PREF);
 });