Bug 1406181 - Add identity reserved for storage.local extension API to ContextualIdentityService. r=baku
☠☠ backed out by 3d3c72079cfd ☠ ☠
authorLuca Greco <lgreco@mozilla.com>
Wed, 21 Feb 2018 15:23:45 +0100
changeset 475306 3241c2dfb2962e0c7f32b8d75a471e21a096b301
parent 475305 912a2eaf4d26414f9ae2acd38afe8be383391dbc
child 475307 d34810cab822c9f8717405c27f5ad9367eb430d9
push id9374
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:43:20 +0000
treeherdermozilla-beta@160e085dfb0b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku
bugs1406181
milestone62.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1406181 - Add identity reserved for storage.local extension API to ContextualIdentityService. r=baku 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,29 @@ function _ContextualIdentityService(path
       accessKey: "userContextShopping.accesskey",
       telemetryId: 4,
     },
     { userContextId: 5,
       public: false,
       icon: "",
       color: "",
       name: "userContextIdInternal.thumbnail",
-      accessKey: "" },
+      accessKey: "",
+    },
+    // This userContextId is used by ExtensionStorageIDB.jsm to create an IndexedDB database
+    // opened with the extension principal but not directly accessible to the extension code
+    // (do not change the userContextId assigned here, otherwise the installed extensions will
+    // not be able to access the data previously stored with the browser.storage.local API).
+    { userContextId: -10,
+      public: false,
+      icon: "",
+      color: "",
+      name: "userContextIdInternal.webextStorageLocal",
+      accessKey: "",
+    },
   ],
 
   _identities: null,
   _openedIdentities: new Set(),
   _lastUserContextId: 0,
 
   _path: null,
   _dataReady: false,
@@ -143,20 +155,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 +306,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 +360,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 (e.g. it is 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 +430,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 +487,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"