Bug 1406181 - Add identity reserved for storage.local extension API to ContextualIdentityService. draft
authorLuca Greco <lgreco@mozilla.com>
Wed, 21 Feb 2018 15:23:45 +0100
changeset 802303 5f107e6633a37aa51925d0e251ca5a7056801978
parent 800562 1622f4b526f4e4bf2376b2dc19f4b4d6cf00826f
child 802304 4605e69d072f83bfcc5d5dc6cbee5cd5b89c1b32
child 802765 d1722d857c67fc0484c05e32fd06bb4ba4f73918
child 802773 2b73072317411c47cbccfc0dd486267efb9191d7
child 802827 bbfd765b3994ba3b0a9098f21d3ec4cff64c5c10
push id111857
push userluca.greco@alcacoop.it
push dateThu, 31 May 2018 17:21:49 +0000
bugs1406181
milestone62.0a1
Bug 1406181 - Add identity reserved for storage.local extension API to ContextualIdentityService. MozReview-Commit-ID: APi7yJnuDe2
toolkit/components/contextualidentity/ContextualIdentityService.jsm
toolkit/components/contextualidentity/tests/unit/test_corruptedFile.js
toolkit/components/contextualidentity/tests/unit/test_migratedFile.js
toolkit/components/contextualidentity/tests/unit/xpcshell.ini
--- a/toolkit/components/contextualidentity/ContextualIdentityService.jsm
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.jsm
@@ -95,17 +95,26 @@ function _ContextualIdentityService(path
       accessKey: "userContextShopping.accesskey",
       telemetryId: 4,
     },
     { userContextId: 5,
       public: false,
       icon: "",
       color: "",
       name: "userContextIdInternal.thumbnail",
-      accessKey: "" },
+      accessKey: "",
+    },
+    // This userContextId is also hardcoded in ExtensionStorageIDB.jsm.
+    { userContextId: -10,
+      public: false,
+      icon: "",
+      color: "",
+      name: "userContextIdInternal.webextStorageLocal",
+      accessKey: "",
+    },
   ],
 
   _identities: null,
   _openedIdentities: new Set(),
   _lastUserContextId: 0,
 
   _path: null,
   _dataReady: false,
@@ -143,20 +152,23 @@ function _ContextualIdentityService(path
       }
     }, (error) => {
       this.loadError(error);
     });
   },
 
   resetDefault() {
     this._identities = [];
+
+    this._lastUserContextId = this._defaultIdentities.map(
+      identity => identity.userContextId).sort().pop();
+
     // Clone the array
-    this._lastUserContextId = this._defaultIdentities.length;
-    for (let i = 0; i < this._lastUserContextId; i++) {
-      this._identities.push(Object.assign({}, this._defaultIdentities[i]));
+    for (let identity of this._defaultIdentities) {
+      this._identities.push(Object.assign({}, identity));
     }
     this._openedIdentities = new Set();
 
     this._dataReady = true;
 
     // Let's delete all the data of any userContextId. 1 is the first valid
     // userContextId value.
     this.deleteContainerData();
@@ -291,17 +303,22 @@ function _ContextualIdentityService(path
 
     let saveNeeded = false;
 
     if (data.version == 2) {
       data = this.migrate2to3(data);
       saveNeeded = true;
     }
 
-    if (data.version != 3) {
+    if (data.version == 3) {
+      data = this.migrate3to4(data);
+      saveNeeded = true;
+    }
+
+    if (data.version != 4) {
       dump("ERROR - ContextualIdentityService - Unknown version found in " + this._path + "\n");
       this.loadError(null);
       return;
     }
 
     this._identities = data.identities;
     this._lastUserContextId = data.lastUserContextId;
 
@@ -340,16 +357,23 @@ function _ContextualIdentityService(path
     return Cu.cloneInto(this._identities.filter(info => info.public), {});
   },
 
   getPrivateIdentity(name) {
     this.ensureDataReady();
     return Cu.cloneInto(this._identities.find(info => !info.public && info.name == name), {});
   },
 
+  // getDefaultPrivateIdentity is similar to getPrivateIdentity but it only looks in the
+  // default identities (used in the data migration methods to retrieve a new default
+  // private identity and add it to the containers data stored on file).
+  getDefaultPrivateIdentity(name) {
+    return Cu.cloneInto(this._defaultIdentities.find(info => !info.public && info.name == name), {});
+  },
+
   getPublicIdentityFromId(userContextId) {
     this.ensureDataReady();
     return Cu.cloneInto(this._identities.find(info => info.userContextId == userContextId &&
                                               info.public), {});
   },
 
   getUserContextLabel(userContextId) {
     let identity = this.getPublicIdentityFromId(userContextId);
@@ -403,16 +427,21 @@ function _ContextualIdentityService(path
       }
 
       new _TabRemovalObserver(resolve, tabParentIds);
     });
   },
 
   notifyAllContainersCleared() {
     for (let identity of this._identities) {
+      // Don't clear the data related to private identities (e.g. the one used internally
+      // for the thumbnails and the one used for the storage.local IndexedDB backend).
+      if (!identity.public) {
+        continue;
+      }
       Services.obs.notifyObservers(null, "clear-origin-attributes-data",
                                    JSON.stringify({ userContextId: identity.userContextId }));
     }
   },
 
   _forEachContainerTab(callback, userContextId = 0) {
     let windowList = Services.wm.getEnumerator("navigator:browser");
     while (windowList.hasMoreElements()) {
@@ -455,35 +484,64 @@ function _ContextualIdentityService(path
     }
   },
 
   createNewInstanceForTesting(path) {
     return new _ContextualIdentityService(path);
   },
 
   deleteContainerData() {
+    // Compute the range of userContextId to clear (and exclude 0 which is reserved
+    // to the default firefox identity).
     let minUserContextId = 1;
     let maxUserContextId = minUserContextId;
+
     const enumerator = Services.cookies.enumerator;
     while (enumerator.hasMoreElements()) {
       const cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
       if (cookie.originAttributes.userContextId > maxUserContextId) {
         maxUserContextId = cookie.originAttributes.userContextId;
       }
     }
 
+    // Collect the userContextId related to the identities that should not be cleared
+    // (the ones marked as `public = false`).
+    const keepDataContextIds = this._identities
+      .filter(identity => !identity.public)
+      .map(identity => identity.userContextId);
+
     for (let i = minUserContextId; i <= maxUserContextId; ++i) {
-      Services.obs.notifyObservers(null, "clear-origin-attributes-data",
-                                   JSON.stringify({ userContextId: i }));
+      // Skip any userContextIds that should not be cleared.
+      if (!keepDataContextIds.includes(i)) {
+        Services.obs.notifyObservers(null, "clear-origin-attributes-data",
+                                     JSON.stringify({ userContextId: i }));
+      }
     }
   },
 
   migrate2to3(data) {
     // migrating from 2 to 3 is basically just increasing the version id.
     // This migration was needed for bug 1419591. See bug 1419591 to know more.
     data.version = 3;
 
     return data;
   },
+
+  migrate3to4(data) {
+    // Migrating from 3 to 4 is:
+    // - adding the reserver userContextId used by the webextension storage.local API
+    // - add the keepData property to all the existent identities
+    // - increasing the version id.
+    //
+    // This migration was needed for Bug 1406181. See bug 1406181 for rationale.
+    const webextStorageLocalIdentity = this.getDefaultPrivateIdentity(
+      "userContextIdInternal.webextStorageLocal");
+
+    data.identities.push(webextStorageLocalIdentity);
+
+    data.version = 4;
+
+    return data;
+  },
 };
 
 let path = OS.Path.join(OS.Constants.Path.profileDir, "containers.json");
 var ContextualIdentityService = new _ContextualIdentityService(path);
--- a/toolkit/components/contextualidentity/tests/unit/test_corruptedFile.js
+++ b/toolkit/components/contextualidentity/tests/unit/test_corruptedFile.js
@@ -14,53 +14,70 @@ const COOKIE = {
   host: BASE_URL,
   path: "/",
   name: "test",
   value: "yes",
   isSecure: false,
   isHttpOnly: false,
   isSession: true,
   expiry: 2145934800,
-  originAttributes: { userContextId: 1 },
 };
 
-function createCookie() {
+function createCookie(userContextId) {
   Services.cookies.add(COOKIE.host,
                        COOKIE.path,
                        COOKIE.name,
                        COOKIE.value,
                        COOKIE.isSecure,
                        COOKIE.isHttpOnly,
                        COOKIE.isSession,
                        COOKIE.expiry,
-                       COOKIE.originAttributes);
+                       {userContextId});
 }
 
-function hasCookie() {
+function hasCookie(userContextId) {
   let found = false;
-  let enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE.originAttributes);
+  let enumerator = Services.cookies.getCookiesFromHost(BASE_URL, {userContextId});
   while (enumerator.hasMoreElements()) {
     let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
-    if (cookie.originAttributes.userContextId == COOKIE.originAttributes.userContextId) {
+    if (cookie.originAttributes.userContextId == userContextId) {
       found = true;
       break;
     }
   }
   return found;
 }
 
 // Correpted file should delete all.
 add_task(async function corruptedFile() {
-  createCookie();
-  ok(hasCookie(), "We have the new cookie!");
+  const thumbnailPrivateId = ContextualIdentityService._defaultIdentities.filter(
+    identity => identity.name === "userContextIdInternal.thumbnail").pop().userContextId;
+
+  // Create a cookie in the userContextId 1.
+  createCookie(1);
+
+  // Create a cookie in the thumbnail private userContextId.
+  createCookie(thumbnailPrivateId);
+
+  ok(hasCookie(1), "We have the new cookie in a public identity!");
+  ok(hasCookie(thumbnailPrivateId), "We have the new cookie in the thumbnail private identity!");
 
   // Let's create a corrupted file.
   await OS.File.writeAtomic(TEST_STORE_FILE_PATH, "{ vers",
                             { tmpPath: TEST_STORE_FILE_PATH + ".tmp" });
 
   let cis = ContextualIdentityService.createNewInstanceForTesting(TEST_STORE_FILE_PATH);
   ok(!!cis, "We have our instance of ContextualIdentityService");
 
-  equal(cis.getPublicIdentities().length, 4, "We should have the default identities");
+  equal(cis.getPublicIdentities().length, 4, "We should have the default public identities");
+
+  const privThumbnailIdentity = cis.getPrivateIdentity("userContextIdInternal.thumbnail");
+  equal(privThumbnailIdentity && privThumbnailIdentity.userContextId, thumbnailPrivateId,
+        "We should have the default thumbnail private identity");
 
   // Cookie is gone!
-  ok(!hasCookie(), "We should not have the new cookie!");
+  ok(!hasCookie(1), "We should not have the new cookie in the userContextId 1!");
+
+  // The data stored in the non-public userContextId (e.g. thumbnails private identity)
+  // should have not be cleared.
+  ok(hasCookie(thumbnailPrivateId),
+     "We should have the new cookie in the thumbnail private userContextId!");
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contextualidentity/tests/unit/test_migratedFile.js
@@ -0,0 +1,93 @@
+"use strict";
+
+const profileDir = do_get_profile();
+
+ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+const TEST_STORE_FILE_PATH = OS.Path.join(profileDir.path, "test-containers.json");
+
+// Test the containers JSON file migrations.
+add_task(async function migratedFile() {
+  // Let's create a file that has to be migrated.
+  const oldFileData = {
+    version: 2,
+    lastUserContextId: 6,
+    identities: [
+      { userContextId: 1,
+        public: true,
+        icon: "fingerprint",
+        color: "blue",
+        l10nID: "userContextPersonal.label",
+        accessKey: "userContextPersonal.accesskey",
+        telemetryId: 1,
+      },
+      { userContextId: 2,
+        public: true,
+        icon: "briefcase",
+        color: "orange",
+        l10nID: "userContextWork.label",
+        accessKey: "userContextWork.accesskey",
+        telemetryId: 2,
+      },
+      { userContextId: 3,
+        public: true,
+        icon: "dollar",
+        color: "green",
+        l10nID: "userContextBanking.label",
+        accessKey: "userContextBanking.accesskey",
+        telemetryId: 3,
+      },
+      { userContextId: 4,
+        public: true,
+        icon: "cart",
+        color: "pink",
+        l10nID: "userContextShopping.label",
+        accessKey: "userContextShopping.accesskey",
+        telemetryId: 4,
+      },
+      { userContextId: 5,
+        public: false,
+        icon: "",
+        color: "",
+        name: "userContextIdInternal.thumbnail",
+        accessKey: "",
+      },
+      { userContextId: 6,
+        public: true,
+        icon: "cart",
+        color: "ping",
+        name: "Custom user-created identity",
+      },
+    ]
+  };
+
+  await OS.File.writeAtomic(TEST_STORE_FILE_PATH, JSON.stringify(oldFileData),
+                            { tmpPath: TEST_STORE_FILE_PATH + ".tmp" });
+
+  let cis = ContextualIdentityService.createNewInstanceForTesting(TEST_STORE_FILE_PATH);
+  ok(!!cis, "We have our instance of ContextualIdentityService");
+
+  // Check that the custom user-created identity exists.
+
+  const expectedPublicLength = oldFileData.identities.filter(
+    identity => identity.public).length;
+  const publicIdentities = cis.getPublicIdentities();
+  const oldLastIdentity = oldFileData.identities[oldFileData.identities.length - 1];
+  const customUserCreatedIdentity = publicIdentities.filter(
+    identity => identity.name === oldLastIdentity.name).pop();
+
+  equal(publicIdentities.length, expectedPublicLength,
+        "We should have the expected number of public identities");
+  ok(!!customUserCreatedIdentity, "Got the custom user-created identity");
+
+  // Check that the reserved userContextIdInternal.webextStorageLocal identity exists.
+
+  const webextStorageLocalPrivateId = ContextualIdentityService._defaultIdentities.filter(
+    identity => identity.name === "userContextIdInternal.webextStorageLocal").pop().userContextId;
+
+  const privWebExtStorageLocal = cis.getPrivateIdentity("userContextIdInternal.webextStorageLocal");
+  equal(privWebExtStorageLocal && privWebExtStorageLocal.userContextId, webextStorageLocalPrivateId,
+        "We should have the default userContextIdInternal.webextStorageLocal private identity");
+});
--- a/toolkit/components/contextualidentity/tests/unit/xpcshell.ini
+++ b/toolkit/components/contextualidentity/tests/unit/xpcshell.ini
@@ -1,7 +1,9 @@
 [DEFAULT]
 firefox-appdir = browser
 
 [test_basic.js]
 skip-if = appname == "thunderbird"
 [test_corruptedFile.js]
 skip-if = appname == "thunderbird"
+[test_migratedFile.js]
+skip-if = appname == "thunderbird"