Bug 1344519 - Add web extension events for containers onUpdated, onCreated and onRemoved r?aswan r?baku draft
authorJonathan Kingston <jkt@mozilla.com>
Sun, 14 May 2017 00:39:32 +0100
changeset 641288 eaf2e69e5e46d6d6f71b69332a4f04d3dc3dfff8
parent 641249 fe6609d22dfdd710b11e3ac7773aff89f7a8d12c
child 724761 8c07b1c5ebeb6b74a1e308cc5fd4f7128ec642bf
push id72491
push userjkingston@mozilla.com
push dateSun, 06 Aug 2017 18:14:32 +0000
reviewersaswan, baku
bugs1344519
milestone57.0a1
Bug 1344519 - Add web extension events for containers onUpdated, onCreated and onRemoved r?aswan r?baku MozReview-Commit-ID: 9Zxjc1J2CAt
mobile/android/locales/en-US/chrome/browser.properties
toolkit/components/contextualidentity/ContextualIdentityService.jsm
toolkit/components/contextualidentity/tests/unit/test_basic.js
toolkit/components/contextualidentity/tests/unit/xpcshell.ini
toolkit/components/extensions/ext-contextualIdentities.js
toolkit/components/extensions/schemas/contextual_identities.json
toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -399,16 +399,36 @@ getUserMedia.audioDevice.none = No Audio
 getUserMedia.audioDevice.prompt = Microphone to use
 getUserMedia.sharingCamera.message2 = Camera is on
 getUserMedia.sharingMicrophone.message2 = Microphone is on
 getUserMedia.sharingCameraAndMicrophone.message2 = Camera and microphone are on
 getUserMedia.blockedCameraAccess = Camera has been blocked.
 getUserMedia.blockedMicrophoneAccess = Microphone has been blocked.
 getUserMedia.blockedCameraAndMicrophoneAccess = Camera and microphone have been blocked.
 
+# LOCALIZATION NOTE (userContextPersonal.label,
+#                    userContextWork.label,
+#                    userContextShopping.label,
+#                    userContextBanking.label,
+#                    userContextNone.label):
+# These strings specify the four predefined contexts included in support of the
+# Contextual Identity / Containers project. Each context is meant to represent
+# the context that the user is in when interacting with the site. Different
+# contexts will store cookies and other information from those sites in
+# different, isolated locations. You can enable the feature by typing
+# about:config in the URL bar and changing privacy.userContext.enabled to true.
+# Once enabled, you can open a new tab in a specific context by clicking
+# File > New Container Tab > (1 of 4 contexts). Once opened, you will see these
+# strings on the right-hand side of the URL bar.
+# In android this will be only exposed by web extensions
+userContextPersonal.label = Personal
+userContextWork.label = Work
+userContextBanking.label = Banking
+userContextShopping.label = Shopping
+
 # LOCALIZATION NOTE (readerMode.toolbarTip):
 # Tip shown to users the first time we hide the reader mode toolbar.
 readerMode.toolbarTip=Tap the screen to show reader options
 
 #Open in App
 openInApp.pageAction = Open in App
 openInApp.ok = OK
 openInApp.cancel = Cancel
--- a/toolkit/components/contextualidentity/ContextualIdentityService.jsm
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.jsm
@@ -211,57 +211,73 @@ function _ContextualIdentityService(path
       public: true,
       icon,
       color,
       name
     };
 
     this._identities.push(identity);
     this.saveSoon();
+    Services.obs.notifyObservers(this.getIdentityObserverOutput(identity),
+                                 "contextual-identity-created");
 
     return Cu.cloneInto(identity, {});
   },
 
   update(userContextId, name, icon, color) {
     this.ensureDataReady();
 
     let identity = this._identities.find(identity => identity.userContextId == userContextId &&
                                          identity.public);
     if (identity && name) {
       identity.name = name;
       identity.color = color;
       identity.icon = icon;
       delete identity.l10nID;
       delete identity.accessKey;
 
-      Services.obs.notifyObservers(null, "contextual-identity-updated", userContextId);
       this.saveSoon();
+      Services.obs.notifyObservers(this.getIdentityObserverOutput(identity),
+                                   "contextual-identity-updated");
     }
 
     return !!identity;
   },
 
   remove(userContextId) {
     this.ensureDataReady();
 
     let index = this._identities.findIndex(i => i.userContextId == userContextId && i.public);
     if (index == -1) {
       return false;
     }
 
     Services.obs.notifyObservers(null, "clear-origin-attributes-data",
                                  JSON.stringify({ userContextId }));
 
+    let deletedOutput = this.getIdentityObserverOutput(this.getPublicIdentityFromId(userContextId));
     this._identities.splice(index, 1);
     this._openedIdentities.delete(userContextId);
     this.saveSoon();
+    Services.obs.notifyObservers(deletedOutput, "contextual-identity-deleted");
 
     return true;
   },
 
+  getIdentityObserverOutput(identity) {
+    let wrappedJSObject = {
+      name: this.getUserContextLabel(identity.userContextId),
+      icon: identity.icon,
+      color: identity.color,
+      userContextId: identity.userContextId,
+    };
+
+    return {wrappedJSObject};
+  },
+
   ensureDataReady() {
     if (this._dataReady) {
       return;
     }
 
     try {
       // This reads the file and automatically detects the UTF-8 encoding.
       let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
--- a/toolkit/components/contextualidentity/tests/unit/test_basic.js
+++ b/toolkit/components/contextualidentity/tests/unit/test_basic.js
@@ -1,25 +1,26 @@
 "use strict";
 
-do_get_profile();
+const profileDir = do_get_profile();
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/ContextualIdentityService.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
 
-const TEST_STORE_FILE_NAME = "test-containers.json";
+const TEST_STORE_FILE_PATH = OS.Path.join(profileDir.path, "test-containers.json");
 
 let cis;
 
 // Basic tests
 add_task(function() {
   ok(!!ContextualIdentityService, "ContextualIdentityService exists");
 
-  cis = ContextualIdentityService.createNewInstanceForTesting(TEST_STORE_FILE_NAME);
+  cis = ContextualIdentityService.createNewInstanceForTesting(TEST_STORE_FILE_PATH);
   ok(!!cis, "We have our instance of ContextualIdentityService");
 
   equal(cis.getPublicIdentities().length, 4, "By default, 4 containers.");
   equal(cis.getPublicIdentityFromId(0), null, "No identity with id 0");
 
   ok(!!cis.getPublicIdentityFromId(1), "Identity 1 exists");
   ok(!!cis.getPublicIdentityFromId(2), "Identity 2 exists");
   ok(!!cis.getPublicIdentityFromId(3), "Identity 3 exists");
--- a/toolkit/components/contextualidentity/tests/unit/xpcshell.ini
+++ b/toolkit/components/contextualidentity/tests/unit/xpcshell.ini
@@ -1,3 +1,4 @@
 [DEFAULT]
+firefox-appdir = browser
 
 [test_basic.js]
--- a/toolkit/components/extensions/ext-contextualIdentities.js
+++ b/toolkit/components/extensions/ext-contextualIdentities.js
@@ -14,16 +14,28 @@ const convertIdentity = identity => {
     icon: identity.icon,
     color: identity.color,
     cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
   };
 
   return result;
 };
 
+const convertIdentityFromObserver = wrappedIdentity => {
+  let identity = wrappedIdentity.wrappedJSObject;
+  let result = {
+    name: identity.name,
+    icon: identity.icon,
+    color: identity.color,
+    cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
+  };
+
+  return result;
+};
+
 this.contextualIdentities = class extends ExtensionAPI {
   getAPI(context) {
     let self = {
       contextualIdentities: {
         get(cookieStoreId) {
           if (!containersEnabled) {
             return Promise.resolve(false);
           }
@@ -121,14 +133,48 @@ this.contextualIdentities = class extend
           let convertedIdentity = convertIdentity(identity);
 
           if (!ContextualIdentityService.remove(identity.userContextId)) {
             return Promise.resolve(null);
           }
 
           return Promise.resolve(convertedIdentity);
         },
+
+        onCreated: new EventManager(context, "contextualIdentities.onCreated", fire => {
+          let observer = (subject, topic) => {
+            fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+          };
+
+          Services.obs.addObserver(observer, "contextual-identity-created");
+          return () => {
+            Services.obs.removeObserver(observer, "contextual-identity-created");
+          };
+        }).api(),
+
+        onUpdated: new EventManager(context, "contextualIdentities.onUpdated", fire => {
+          let observer = (subject, topic) => {
+            fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+          };
+
+          Services.obs.addObserver(observer, "contextual-identity-updated");
+          return () => {
+            Services.obs.removeObserver(observer, "contextual-identity-updated");
+          };
+        }).api(),
+
+        onRemoved: new EventManager(context, "contextualIdentities.onRemoved", fire => {
+          let observer = (subject, topic) => {
+            fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+          };
+
+          Services.obs.addObserver(observer, "contextual-identity-deleted");
+          return () => {
+            Services.obs.removeObserver(observer, "contextual-identity-deleted");
+          };
+        }).api(),
+
       },
     };
 
     return self;
   }
 };
--- a/toolkit/components/extensions/schemas/contextual_identities.json
+++ b/toolkit/components/extensions/schemas/contextual_identities.json
@@ -113,11 +113,55 @@
         "parameters": [
           {
             "type": "string",
             "name": "cookieStoreId",
             "description": "The ID of the contextual identity cookie store. "
           }
         ]
       }
+    ],
+    "events": [
+      {
+        "name": "onUpdated",
+        "type": "function",
+        "description": "Fired when a container is updated.",
+        "parameters": [
+          {
+            "type": "object",
+            "name": "changeInfo",
+            "properties": {
+              "contextualIdentity": {"$ref": "ContextualIdentity", "description": "Contextual identity that has been updated"}
+            }
+          }
+        ]
+      },
+      {
+        "name": "onCreated",
+        "type": "function",
+        "description": "Fired when a new container is created.",
+        "parameters": [
+          {
+            "type": "object",
+            "name": "changeInfo",
+            "properties": {
+              "contextualIdentity": {"$ref": "ContextualIdentity", "description": "Contextual identity that has been created"}
+            }
+          }
+        ]
+      },
+      {
+        "name": "onRemoved",
+        "type": "function",
+        "description": "Fired when a container is removed.",
+        "parameters": [
+          {
+            "type": "object",
+            "name": "changeInfo",
+            "properties": {
+              "contextualIdentity": {"$ref": "ContextualIdentity", "description": "Contextual identity that has been removed"}
+            }
+          }
+        ]
+      }
     ]
   }
 ]
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
@@ -56,16 +56,83 @@ add_task(async function test_contextualI
 
   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() {
+    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);
+          };
+          browser.contextualIdentities[type].addListener(listener);
+        } catch (e) {
+          reject(e);
+        }
+      });
+    }
+
+    function assertExpected(expected, container) {
+      for (let key of Object.keys(container)) {
+        browser.test.assertTrue(key in expected, `found property ${key}`);
+        browser.test.assertEq(expected[key], container[key], `property value for ${key} is correct`);
+      }
+      browser.test.assertEq(Object.keys(expected).length, Object.keys(container).length, "all expected properties found");
+    }
+
+    let onCreatePromise = createOneTimeListener("onCreated");
+
+    let containerObj = {name: "foobar", color: "red", icon: "icon"};
+    let ci = await browser.contextualIdentities.create(containerObj);
+    browser.test.assertTrue(!!ci, "We have an identity");
+    const onCreateListenerResponse = await onCreatePromise;
+    const cookieStoreId = ci.cookieStoreId;
+    assertExpected(onCreateListenerResponse.contextualIdentity, Object.assign(containerObj, {cookieStoreId}));
+
+    let onUpdatedPromise = createOneTimeListener("onUpdated");
+    let updateContainerObj = {name: "testing", color: "blue", icon: "thing"};
+    ci = await browser.contextualIdentities.update(cookieStoreId, updateContainerObj);
+    browser.test.assertTrue(!!ci, "We have an update identity");
+    const onUpdatedListenerResponse = await onUpdatedPromise;
+    assertExpected(onUpdatedListenerResponse.contextualIdentity, Object.assign(updateContainerObj, {cookieStoreId}));
+
+    let onRemovePromise = createOneTimeListener("onRemoved");
+    ci = await browser.contextualIdentities.remove(updateContainerObj.cookieStoreId);
+    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})()`,
+    manifest: {
+      permissions: ["contextualIdentities"],
+    },
+  });
+
+  Services.prefs.setBoolPref("privacy.userContext.enabled", true);
+
+  await extension.startup();
+  await extension.awaitFinish("contextualIdentities_events");
+  await extension.unload();
+
+  Services.prefs.clearUserPref("privacy.userContext.enabled");
+});
+
 add_task(async 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");