Bug 1322856 - Expose ContextualIdentities (aka containers) to WebExtensions - part 1, r=kmaglione
☠☠ backed out by a199014fd2b5 ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Fri, 16 Dec 2016 22:01:24 +0100
changeset 450672 5a145b21bd64eab0a735eb4d713721c497faf9d4
parent 450518 6b11cbf213c40ab3ba38fab885fb38784510fb6b
child 450673 5db9fcefa874c25c01983bc09b733d43f8db278f
push id38944
push userbmo:leftysolara@gmail.com
push dateSun, 18 Dec 2016 16:01:15 +0000
reviewerskmaglione
bugs1322856
milestone53.0a1
Bug 1322856 - Expose ContextualIdentities (aka containers) to WebExtensions - part 1, r=kmaglione
toolkit/components/extensions/.eslintrc.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ext-contextualIdentities.js
toolkit/components/extensions/ext-cookies.js
toolkit/components/extensions/extensions-toolkit.manifest
toolkit/components/extensions/jar.mn
toolkit/components/extensions/schemas/contextual_identities.json
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -11,16 +11,18 @@ module.exports = { // eslint-disable-lin
     "Cu": true,
     "dump": true,
     "TextDecoder": false,
     "TextEncoder": false,
     // Specific to WebExtensions:
     "Extension": true,
     "ExtensionManagement": true,
     "extensions": true,
+    "getContainerForCookieStoreId": true,
+    "getCookieStoreIdForContainer": true,
     "global": true,
     "NetUtil": true,
     "openOptionsPage": true,
     "require": false,
     "runSafe": true,
     "runSafeSync": true,
     "runSafeSyncWithoutClone": true,
     "Services": true,
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -424,20 +424,31 @@ this.ExtensionData = class {
         // Do not override the add-on id that has been already assigned.
         if (!this.id && this.manifest.applications.gecko.id) {
           this.id = this.manifest.applications.gecko.id;
         }
       } catch (e) {
         // Errors are handled by the type checks above.
       }
 
+      let containersEnabled = true;
+      try {
+        containerEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
+      } catch(e) {
+        // If we fail here, we are in some xpcshell test.
+      }
+
       let permissions = this.manifest.permissions || [];
 
       let whitelist = [];
       for (let perm of permissions) {
+        if (perm == "contextualIdentities" && !containersEnabled) {
+          continue;
+        }
+
         this.permissions.add(perm);
 
         let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
         if (!match) {
           whitelist.push(perm);
         } else if (match[1] == "experiments" && match[2]) {
           this.apiNames.add(match[2]);
         }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-contextualIdentities.js
@@ -0,0 +1,109 @@
+"use strict";
+
+const {interfaces: Ci, utils: Cu} = Components;
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
+                                  "resource://gre/modules/ContextualIdentityService.jsm");
+
+function convert(identity) {
+  let result = {
+    name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
+    icon: identity.icon,
+    color: identity.color,
+    cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
+  };
+
+  return result;
+}
+
+extensions.registerSchemaAPI("contextualIdentities", "addon_parent", context => {
+  let self = {
+    contextualIdentities: {
+      get(cookieStoreId) {
+        let containerId = getContainerForCookieStoreId(cookieStoreId);
+        if (!containerId) {
+          return Promise.resolve(null);
+        }
+
+        let identity = ContextualIdentityService.getIdentityFromId(containerId);
+        return Promise.resolve(convert(identity));
+      },
+
+      query(details) {
+        let identities = [];
+        ContextualIdentityService.getIdentities().forEach(identity => {
+          if (details.name &&
+              ContextualIdentityService.getUserContextLabel(identity.userContextId) != details.name) {
+            return;
+          }
+
+          identities.push(convert(identity));
+        });
+
+        return Promise.resolve(identities);
+      },
+
+      create(details) {
+        let identity = ContextualIdentityService.create(details.name,
+                                                        details.icon,
+                                                        details.color);
+        return Promise.resolve(convert(identity));
+      },
+
+      update(cookieStoreId, details) {
+        let containerId = getContainerForCookieStoreId(cookieStoreId);
+        if (!containerId) {
+          return Promise.resolve(null);
+        }
+
+        let identity = ContextualIdentityService.getIdentityFromId(containerId);
+        if (!identity) {
+          return Promise.resolve(null);
+        }
+
+        if (details.name !== null) {
+          identity.name = details.name;
+        }
+
+        if (details.color !== null) {
+          identity.color = details.color;
+        }
+
+        if (details.icon !== null) {
+          identity.icon = details.icon;
+        }
+
+        if (!ContextualIdentityService.update(identity.userContextId,
+                                              identity.name, identity.icon,
+                                              identity.color)) {
+          return Promise.resolve(null);
+        }
+
+        return Promise.resolve(convert(identity));
+      },
+
+      remove(cookieStoreId) {
+        let containerId = getContainerForCookieStoreId(cookieStoreId);
+        if (!containerId) {
+          return Promise.resolve(null);
+        }
+
+        let identity = ContextualIdentityService.getIdentityFromId(containerId);
+        if (!identity) {
+          return Promise.resolve(null);
+        }
+
+        // We have to create the identity object before removing it.
+        let convertedIdentity = convert(identity);
+
+        if (!ContextualIdentityService.remove(identity.userContextId)) {
+          return Promise.resolve(null);
+        }
+
+        return Promise.resolve(convertedIdentity);
+      },
+    },
+  };
+
+  return self;
+});
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -17,17 +17,17 @@ var PRIVATE_STORE = "firefox-private";
 var CONTAINER_STORE = "firefox-container-";
 
 global.getCookieStoreIdForTab = function(data, tab) {
   if (data.incognito) {
     return PRIVATE_STORE;
   }
 
   if (tab.userContextId) {
-    return CONTAINER_STORE + tab.userContextId;
+    return getCookieStoreIdForContainer(tab.userContextId);
   }
 
   return DEFAULT_STORE;
 };
 
 global.isPrivateCookieStoreId = function(storeId) {
   return storeId == PRIVATE_STORE;
 };
@@ -35,16 +35,20 @@ global.isPrivateCookieStoreId = function
 global.isDefaultCookieStoreId = function(storeId) {
   return storeId == DEFAULT_STORE;
 };
 
 global.isContainerCookieStoreId = function(storeId) {
   return storeId !== null && storeId.startsWith(CONTAINER_STORE);
 };
 
+global.getCookieStoreIdForContainer = function(containerId) {
+  return CONTAINER_STORE + containerId;
+};
+
 global.getContainerForCookieStoreId = function(storeId) {
   if (!global.isContainerCookieStoreId(storeId)) {
     return null;
   }
 
   let containerId = storeId.substring(CONTAINER_STORE.length);
   if (ContextualIdentityService.getIdentityFromId(containerId)) {
     return parseInt(containerId, 10);
@@ -71,17 +75,17 @@ function convert({cookie, isPrivate}) {
     session: cookie.isSession,
   };
 
   if (!cookie.isSession) {
     result.expirationDate = cookie.expiry;
   }
 
   if (cookie.originAttributes.userContextId) {
-    result.storeId = CONTAINER_STORE + cookie.originAttributes.userContextId;
+    result.storeId = getCookieStoreIdForContainer(cookie.originAttributes.userContextId);
   } else if (cookie.originAttributes.privateBrowsingId || isPrivate) {
     result.storeId = PRIVATE_STORE;
   } else {
     result.storeId = DEFAULT_STORE;
   }
 
   return result;
 }
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -1,11 +1,12 @@
 # scripts
 category webextension-scripts alarms chrome://extensions/content/ext-alarms.js
 category webextension-scripts backgroundPage chrome://extensions/content/ext-backgroundPage.js
+category webextension-scripts contextualIdentities chrome://extensions/content/ext-contextualIdentities.js
 category webextension-scripts cookies chrome://extensions/content/ext-cookies.js
 category webextension-scripts downloads chrome://extensions/content/ext-downloads.js
 category webextension-scripts management chrome://extensions/content/ext-management.js
 category webextension-scripts notifications chrome://extensions/content/ext-notifications.js
 category webextension-scripts i18n chrome://extensions/content/ext-i18n.js
 category webextension-scripts idle chrome://extensions/content/ext-idle.js
 category webextension-scripts webRequest chrome://extensions/content/ext-webRequest.js
 category webextension-scripts webNavigation chrome://extensions/content/ext-webNavigation.js
@@ -36,16 +37,17 @@ category webextension-scripts-addon i18n
 category webextension-scripts-addon identity chrome://extensions/content/ext-c-identity.js
 #endif
 category webextension-scripts-addon runtime chrome://extensions/content/ext-c-runtime.js
 category webextension-scripts-addon test chrome://extensions/content/ext-c-test.js
 category webextension-scripts-addon storage chrome://extensions/content/ext-c-storage.js
 
 # schemas
 category webextension-schemas alarms chrome://extensions/content/schemas/alarms.json
+category webextension-schemas contextualIdentities chrome://extensions/content/schemas/contextual_identities.json
 category webextension-schemas cookies chrome://extensions/content/schemas/cookies.json
 category webextension-schemas downloads chrome://extensions/content/schemas/downloads.json
 category webextension-schemas events chrome://extensions/content/schemas/events.json
 category webextension-schemas extension chrome://extensions/content/schemas/extension.json
 category webextension-schemas extension_types chrome://extensions/content/schemas/extension_types.json
 category webextension-schemas i18n chrome://extensions/content/schemas/i18n.json
 #ifndef ANDROID
 category webextension-schemas identity chrome://extensions/content/schemas/identity.json
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -2,16 +2,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 toolkit.jar:
 % content extensions %content/extensions/
     content/extensions/ext-alarms.js
     content/extensions/ext-backgroundPage.js
     content/extensions/ext-browser-content.js
+    content/extensions/ext-contextualIdentities.js
     content/extensions/ext-cookies.js
     content/extensions/ext-downloads.js
     content/extensions/ext-management.js
     content/extensions/ext-notifications.js
     content/extensions/ext-i18n.js
     content/extensions/ext-idle.js
     content/extensions/ext-webRequest.js
     content/extensions/ext-webNavigation.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/contextual_identities.json
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "contextualIdentities"
+          ]
+        }]
+      }
+    ]
+  },
+  {
+    "namespace": "contextualIdentities",
+    "description": "Use the <code>browser.contextualIdentities</code> API to query and modify contextual identity, also called as containers.",
+    "permissions": ["contextualIdentities"],
+    "types": [
+      {
+        "id": "ContextualIdentity",
+        "type": "object",
+        "description": "Represents information about a contextual identity.",
+        "properties": {
+          "name": {"type": "string", "description": "The name of the contextual identity."},
+          "icon": {"type": "string", "description": "The icon of the contextual identity."},
+          "color": {"type": "string", "description": "The color of the contextual identity."},
+          "cookieStoreId": {"type": "string", "description": "The cookie store ID of the contextual identity."}
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "get",
+        "type": "function",
+        "description": "Retrieves information about a single contextual identity.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "string",
+            "name": "cookieStoreId",
+            "description": "The ID of the contextual identity cookie store. "
+          }
+        ]
+      },
+      {
+        "name": "query",
+        "type": "function",
+        "description": "Retrieves all contextual identities",
+        "async": true,
+        "parameters": [
+          {
+            "type": "object",
+            "name": "details",
+            "description": "Information to filter the contextual identities being retrieved.",
+            "properties": {
+              "name": {"type": "string", "optional": true, "description": "Filters the contextual identity by name."}
+            }
+          }
+        ]
+      },
+      {
+        "name": "create",
+        "type": "function",
+        "description": "Creates a contextual identity with the given data.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "object",
+            "name": "details",
+            "description": "Details about the contextual identity being created.",
+            "properties": {
+              "name": {"type": "string", "optional": false, "description": "The name of the contextual identity." },
+              "color": {"type": "string", "optional": false, "description": "The color of the contextual identity." },
+              "icon": {"type": "string", "optional": false, "description": "The icon of the contextual identity." }
+            }
+          }
+        ]
+      },
+      {
+        "name": "update",
+        "type": "function",
+        "description": "Updates a contextual identity with the given data.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "string",
+            "name": "cookieStoreId",
+            "description": "The ID of the contextual identity cookie store. "
+          },
+          {
+            "type": "object",
+            "name": "details",
+            "description": "Details about the contextual identity being created.",
+            "properties": {
+              "name": {"type": "string", "optional": true, "description": "The name of the contextual identity." },
+              "color": {"type": "string", "optional": true, "description": "The color of the contextual identity." },
+              "icon": {"type": "string", "optional": true, "description": "The icon of the contextual identity." }
+            }
+          }
+        ]
+      },
+      {
+        "name": "remove",
+        "type": "function",
+        "description": "Deletes a contetual identity by its cookie Store ID.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "string",
+            "name": "cookieStoreId",
+            "description": "The ID of the contextual identity cookie store. "
+          }
+        ]
+      }
+    ]
+  }
+]
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -1,15 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 toolkit.jar:
 % content extensions %content/extensions/
     content/extensions/schemas/alarms.json
+    content/extensions/schemas/contextual_identities.json
     content/extensions/schemas/cookies.json
     content/extensions/schemas/downloads.json
     content/extensions/schemas/events.json
     content/extensions/schemas/experiments.json
     content/extensions/schemas/extension.json
     content/extensions/schemas/extension_types.json
     content/extensions/schemas/i18n.json
 #ifndef ANDROID
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
@@ -0,0 +1,122 @@
+"use strict";
+
+add_task(function* test_contextualIdentities_without_permissions() {
+  function backgroundScript() {
+    browser.test.assertTrue(!browser.contextualIdentities,
+                            "contextualIdentities API is not available when the contextualIdentities permission is not required");
+    browser.test.notifyPass("contextualIdentities_permission");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: [],
+    },
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("contextualIdentities_permission");
+  yield extension.unload();
+});
+
+
+add_task(function* test_contextualIdentity_no_containers() {
+  function backgroundScript() {
+    browser.test.assertTrue(!browser.contextualIdentities,
+                            "contextualIdentities API is not available when the containers are disabled");
+    browser.test.notifyPass("contextualIdentities_pref");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["contextualIdentities"],
+    },
+  });
+
+  Services.prefs.setBoolPref("privacy.userContext.enabled", false);
+
+  yield extension.startup();
+  yield extension.awaitFinish("contextualIdentities_pref");
+  yield extension.unload();
+
+  Services.prefs.clearUserPref("privacy.userContext.enabled");
+});
+
+add_task(function* test_contextualIdentity_with_permissions() {
+  async function backgroundScript() {
+    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");
+    browser.test.assertEq("Personal", ci.name, "identity.name is correct");
+    browser.test.assertEq("firefox-container-1", ci.cookieStoreId, "identity.cookieStoreId is correct");
+
+    let cis = await browser.contextualIdentities.query({});
+    browser.test.assertEq(4, cis.length, "by default we should have 4 containers");
+
+    cis = await browser.contextualIdentities.query({name: "Personal"});
+    browser.test.assertEq(1, cis.length, "by default we should have 1 container called Personal");
+
+    cis = await browser.contextualIdentities.query({name: "foobar"});
+    browser.test.assertEq(0, cis.length, "by default we should have 0 container called foobar");
+
+    ci = await browser.contextualIdentities.create({name: "foobar", color: "red", icon: "icon"});
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("foobar", ci.name, "identity.name is correct");
+    browser.test.assertEq("red", ci.color, "identity.color is correct");
+    browser.test.assertEq("icon", ci.icon, "identity.icon is correct");
+    browser.test.assertTrue(!!ci.cookieStoreId, "identity.cookieStoreId is correct");
+
+    ci = await browser.contextualIdentities.get(ci.cookieStoreId);
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("foobar", ci.name, "identity.name is correct");
+    browser.test.assertEq("red", ci.color, "identity.color is correct");
+    browser.test.assertEq("icon", ci.icon, "identity.icon is correct");
+
+    cis = await browser.contextualIdentities.query({});
+    browser.test.assertEq(5, cis.length, "now we have 5 identities");
+
+    ci = await browser.contextualIdentities.update(ci.cookieStoreId, {name: "barfoo", color: "blue", icon: "icon icon"});
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
+    browser.test.assertEq("blue", ci.color, "identity.color is correct");
+    browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+
+    ci = await browser.contextualIdentities.get(ci.cookieStoreId);
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
+    browser.test.assertEq("blue", ci.color, "identity.color is correct");
+    browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+
+    ci = await browser.contextualIdentities.remove(ci.cookieStoreId);
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
+    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");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["contextualIdentities"],
+    },
+  });
+
+  Services.prefs.setBoolPref("privacy.userContext.enabled", true);
+
+  yield extension.startup();
+  yield extension.awaitFinish("contextualIdentities");
+  yield extension.unload();
+
+  Services.prefs.clearUserPref("privacy.userContext.enabled");
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -20,16 +20,17 @@ tags = webextensions
 [test_ext_background_global_history.js]
 skip-if = os == "android" # Android does not use Places for history.
 [test_ext_background_private_browsing.js]
 [test_ext_background_runtime_connect_params.js]
 [test_ext_background_sub_windows.js]
 [test_ext_background_window_properties.js]
 skip-if = os == "android"
 [test_ext_contexts.js]
+[test_ext_contextual_identities.js]
 [test_ext_downloads.js]
 [test_ext_downloads_download.js]
 skip-if = os == "android"
 [test_ext_downloads_misc.js]
 skip-if = os == "android"
 [test_ext_downloads_search.js]
 skip-if = os == "android"
 [test_ext_experiments.js]