Bug 1302697 - Containers and WebExtensions - part 1 - Tab API, r=kmag
authorAndrea Marchesini <amarchesini@mozilla.com>
Fri, 28 Oct 2016 10:15:42 +0200
changeset 319972 ca0b48d8543465cd633e6893882956fe7515d3ec
parent 319971 a49efd91c07751acec140323f05c8d54b107919c
child 319973 691162eba71737f765cb0dda5470a3a516e0d47d
push id20749
push userryanvm@gmail.com
push dateSat, 29 Oct 2016 13:21:21 +0000
treeherderfx-team@1b170b39ed6b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1302697
milestone52.0a1
Bug 1302697 - Containers and WebExtensions - part 1 - Tab API, r=kmag
browser/base/content/tabbrowser.xml
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-utils.js
browser/components/extensions/schemas/tabs.json
browser/components/extensions/test/browser/browser.ini
browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js
toolkit/components/extensions/ext-cookies.js
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -6602,16 +6602,25 @@
       Describes how the tab ended up in this mute state. May be any of:
 
        - undefined: The tabs mute state has never changed.
        - null: The mute state was last changed through the UI.
        - Any string: The ID was changed through an extension API. The string
                      must be the ID of the extension which changed it.
       -->
       <field name="muteReason">undefined</field>
+
+      <property name="userContextId" readonly="true">
+        <getter>
+          return this.hasAttribute("usercontextid")
+                   ? parseInt(this.getAttribute("usercontextid"))
+                   : 0;
+        </getter>
+      </property>
+
       <property name="soundPlaying" readonly="true">
         <getter>
           return this.getAttribute("soundplaying") == "true";
         </getter>
       </property>
 
       <property name="lastAccessed">
         <getter>
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -3,16 +3,18 @@
 "use strict";
 
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 
 XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                   "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
@@ -518,18 +520,47 @@ extensions.registerSchemaAPI("tabs", "ad
           if (createProperties.url !== null) {
             url = context.uri.resolve(createProperties.url);
 
             if (!context.checkLoadURL(url, {dontReportErrors: true})) {
               return Promise.reject({message: `Illegal URL: ${url}`});
             }
           }
 
+          if (createProperties.cookieStoreId && !extension.hasPermission("cookies")) {
+            return Promise.reject({message: `No permission for cookieStoreId: ${createProperties.cookieStoreId}`});
+          }
+
+          let options = {};
+          if (createProperties.cookieStoreId) {
+            if (!global.isValidCookieStoreId(createProperties.cookieStoreId)) {
+              return Promise.reject({message: `Illegal cookieStoreId: ${createProperties.cookieStoreId}`});
+            }
+
+            let privateWindow = PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser);
+            if (privateWindow && !global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
+              return Promise.reject({message: `Illegal to set non-private cookieStorageId in a private window`});
+            }
+
+            if (!privateWindow && global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
+              return Promise.reject({message: `Illegal to set private cookieStorageId in a non-private window`});
+            }
+
+            if (global.isContainerCookieStoreId(createProperties.cookieStoreId)) {
+              let containerId = global.getContainerForCookieStoreId(createProperties.cookieStoreId);
+              if (!containerId) {
+                return Promise.reject({message: `No cookie store exists with ID ${createProperties.cookieStoreId}`});
+              }
+
+              options.userContextId = containerId;
+            }
+          }
+
           tabListener.initTabReady();
-          let tab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL);
+          let tab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
 
           let active = true;
           if (createProperties.active !== null) {
             active = createProperties.active;
           }
           if (active) {
             window.gBrowser.selectedTab = tab;
           }
@@ -688,16 +719,21 @@ extensions.registerSchemaAPI("tabs", "ad
 
           if (queryInfo.currentWindow !== null) {
             let eq = window == currentWindow(context);
             if (queryInfo.currentWindow != eq) {
               return false;
             }
           }
 
+          if (queryInfo.cookieStoreId !== null &&
+              tab.cookieStoreId != queryInfo.cookieStoreId) {
+            return false;
+          }
+
           if (pattern && !pattern.matches(Services.io.newURI(tab.url, null, null))) {
             return false;
           }
 
           return true;
         }
 
         let result = [];
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -639,17 +639,17 @@ ExtensionTabManager.prototype = {
       status: TabManager.getStatus(tab),
       incognito: PrivateBrowsingUtils.isBrowserPrivate(browser),
       width: browser.frameLoader.lazyWidth || browser.clientWidth,
       height: browser.frameLoader.lazyHeight || browser.clientHeight,
       audible: tab.soundPlaying,
       mutedInfo,
     };
     if (this.extension.hasPermission("cookies")) {
-      result.cookieStoreId = getCookieStoreIdForTab(result);
+      result.cookieStoreId = getCookieStoreIdForTab(result, tab);
     }
 
     if (this.hasTabPermission(tab)) {
       result.url = browser.currentURI.spec;
       let title = browser.contentTitle || tab.label;
       if (title) {
         result.title = title;
       }
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -68,17 +68,18 @@
           "mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
           "url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
           "status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
           "incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
           "width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
           "height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."},
-          "sessionId": {"unsupported": true, "type": "string", "optional": true, "description": "The session ID used to uniquely identify a Tab obtained from the $(ref:sessions) API."}
+          "sessionId": {"unsupported": true, "type": "string", "optional": true, "description": "The session ID used to uniquely identify a Tab obtained from the $(ref:sessions) API."},
+          "cookieStoreId": {"type": "string", "description": "The CookieStoreId used for the tab."}
         }
       },
       {
         "id": "ZoomSettingsMode",
         "type": "string",
         "description": "Defines how zoom changes are handled, i.e. which entity is responsible for the actual scaling of the page; defaults to <code>automatic</code>.",
         "enum": [
           {
@@ -386,16 +387,21 @@
                 "description": "Whether the tab should be pinned. Defaults to <var>false</var>"
               },
               "openerTabId": {
                 "unsupported": true,
                 "type": "integer",
                 "minimum": 0,
                 "optional": true,
                 "description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as the newly created tab."
+              },
+              "cookieStoreId": {
+                "type": "string",
+                "optional": true,
+                "description": "The CookieStoreId for the tab that opened this tab."
               }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "optional": true,
             "parameters": [
@@ -509,16 +515,21 @@
                 "optional": true,
                 "description": "The type of window the tabs are in."
               },
               "index": {
                 "type": "integer",
                 "optional": true,
                 "minimum": 0,
                 "description": "The position of the tabs within their windows."
+              },
+              "cookieStoreId": {
+                "type": "string",
+                "optional": true,
+                "description": "The CookieStoreId used for the tab."
               }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "parameters": [
               {
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -78,16 +78,17 @@ tags = webextensions
 [browser_ext_tabs_move_window_multiple.js]
 [browser_ext_tabs_move_window_pinned.js]
 [browser_ext_tabs_onHighlighted.js]
 [browser_ext_tabs_onUpdated.js]
 [browser_ext_tabs_query.js]
 [browser_ext_tabs_reload.js]
 [browser_ext_tabs_reload_bypass_cache.js]
 [browser_ext_tabs_sendMessage.js]
+[browser_ext_tabs_cookieStoreId.js]
 [browser_ext_tabs_update.js]
 [browser_ext_tabs_zoom.js]
 [browser_ext_tabs_update_url.js]
 [browser_ext_topwindowid.js]
 [browser_ext_webNavigation_frameId0.js]
 [browser_ext_webNavigation_getFrames.js]
 [browser_ext_webNavigation_urlbar_transitions.js]
 [browser_ext_windows.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js
@@ -0,0 +1,153 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* setup() {
+  // make sure userContext is enabled.
+  return SpecialPowers.pushPrefEnv({"set": [
+    ["privacy.userContext.enabled", true],
+  ]});
+});
+
+add_task(function* () {
+  info("Start testing tabs.create with cookieStoreId");
+
+  let testCases = [
+    // No private window
+    {privateTab: false, cookieStoreId: null, success: true, expectedCookieStoreId: "firefox-default"},
+    {privateTab: false, cookieStoreId: "firefox-default", success: true, expectedCookieStoreId: "firefox-default"},
+    {privateTab: false, cookieStoreId: "firefox-container-1", success: true, expectedCookieStoreId: "firefox-container-1"},
+    {privateTab: false, cookieStoreId: "firefox-container-2", success: true, expectedCookieStoreId: "firefox-container-2"},
+    {privateTab: false, cookieStoreId: "firefox-container-42", failure: "exist"},
+    {privateTab: false, cookieStoreId: "firefox-private", failure: "defaultToPrivate"},
+    {privateTab: false, cookieStoreId: "wow", failure: "illegal"},
+
+    // Private window
+    {privateTab: true, cookieStoreId: null, success: true, expectedCookieStoreId: "firefox-private"},
+    {privateTab: true, cookieStoreId: "firefox-private", success: true, expectedCookieStoreId: "firefox-private"},
+    {privateTab: true, cookieStoreId: "firefox-default", failure: "privateToDefault"},
+    {privateTab: true, cookieStoreId: "firefox-container-1", failure: "privateToDefault"},
+    {privateTab: true, cookieStoreId: "wow", failure: "illegal"},
+  ];
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["tabs", "cookies"],
+    },
+
+    background: function() {
+      function testTab(data, tab) {
+        browser.test.assertTrue(data.success, "we want a success");
+        browser.test.assertTrue(!!tab, "we have a tab");
+        browser.test.assertEq(data.expectedCookieStoreId, tab.cookieStoreId, "tab should have the correct cookieStoreId");
+      }
+
+      function runTest(data) {
+        // Tab Creation
+        browser.tabs.create({windowId: data.privateTab ? this.privateWindowId : this.defaultWindowId,
+                             cookieStoreId: data.cookieStoreId})
+
+        // Tests for tab creation
+        .then((tab) => {
+          testTab(data, tab);
+          return tab;
+        }, (error) => {
+          browser.test.assertTrue(!!data.failure, "we want a failure");
+          if (data.failure == "illegal") {
+            browser.test.assertTrue(/Illegal cookieStoreId/.test(error.message),
+                                    "runtime.lastError should report the expected error message");
+          } else if (data.failure == "defaultToPrivate") {
+            browser.test.assertTrue("Illegal to set private cookieStorageId in a non private window",
+                                    error.message,
+                                    "runtime.lastError should report the expected error message");
+          } else if (data.failure == "privateToDefault") {
+            browser.test.assertTrue("Illegal to set non private cookieStorageId in a private window",
+                                    error.message,
+                                    "runtime.lastError should report the expected error message");
+          } else if (data.failure == "exist") {
+            browser.test.assertTrue(/No cookie store exists/.test(error.message),
+                                    "runtime.lastError should report the expected error message");
+          } else {
+            browser.test.fail("The test is broken");
+          }
+
+          return null;
+        })
+
+        // Tests for tab querying
+        .then((tab) => {
+          if (tab) {
+            return browser.tabs.query({windowId: data.privateTab ? this.privateWindowId : this.defaultWindowId,
+                                       cookieStoreId: data.cookieStoreId})
+                   .then((tabs) => {
+                     browser.test.assertTrue(tabs.length >= 1, "Tab found!");
+                     testTab(data, tabs[0]);
+                     return browser.tabs.remove(tab.id);
+                   });
+          }
+        })
+
+        .then(() => {
+          browser.test.sendMessage("test-done");
+        }, () => {
+          browser.test.fail("An exception has ben thrown");
+        });
+      }
+
+      function initialize() {
+        browser.windows.create({incognito: true})
+        .then((win) => {
+          this.privateWindowId = win.id;
+          return browser.windows.create({incognito: false});
+        })
+        .then((win) => {
+          this.defaultWindowId = win.id;
+        })
+        .then(() => {
+          browser.test.sendMessage("ready");
+        });
+      }
+
+      function shutdown() {
+        browser.windows.remove(this.privateWindowId)
+        .then(() => {
+          browser.windows.remove(this.defaultWindowId);
+        })
+        .then(() => {
+          browser.test.sendMessage("gone");
+        });
+      }
+
+      // Waiting for messages
+      browser.test.onMessage.addListener((msg, data) => {
+        if (msg == "be-ready") {
+          initialize();
+        } else if (msg == "test") {
+          runTest(data);
+        } else {
+          browser.test.assertTrue("finish", msg, "Shutting down");
+          shutdown();
+        }
+      });
+    },
+  });
+
+  yield extension.startup();
+
+  info("Tests must be ready...");
+  extension.sendMessage("be-ready");
+  yield extension.awaitMessage("ready");
+  info("Tests are ready to run!");
+
+  for (let test of testCases) {
+    info(`test tab.create with cookieStoreId: "${test.cookieStoreId}"`);
+    extension.sendMessage("test", test);
+    yield extension.awaitMessage("test-done");
+  }
+
+  info("Waiting for shutting down...");
+  extension.sendMessage("finish");
+  yield extension.awaitMessage("gone");
+
+  yield extension.unload();
+});
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -1,43 +1,93 @@
 "use strict";
 
 const {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
+                                  "resource://gre/modules/ContextualIdentityService.jsm");
+
 var {
   EventManager,
 } = ExtensionUtils;
 
 var DEFAULT_STORE = "firefox-default";
 var PRIVATE_STORE = "firefox-private";
+var CONTAINER_STORE = "firefox-container-";
 
-global.getCookieStoreIdForTab = function(tab) {
-  return tab.incognito ? PRIVATE_STORE : DEFAULT_STORE;
+global.getCookieStoreIdForTab = function(data, tab) {
+  if (data.incognito) {
+    return PRIVATE_STORE;
+  }
+
+  if (tab.userContextId) {
+    return CONTAINER_STORE + tab.userContextId;
+  }
+
+  return DEFAULT_STORE;
+};
+
+global.isPrivateCookieStoreId = function(storeId) {
+  return storeId == PRIVATE_STORE;
+};
+
+global.isDefaultCookieStoreId = function(storeId) {
+  return storeId == DEFAULT_STORE;
+};
+
+global.isContainerCookieStoreId = function(storeId) {
+  return storeId !== null && storeId.startsWith(CONTAINER_STORE);
+};
+
+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);
+  }
+
+  return null;
+};
+
+global.isValidCookieStoreId = function(storeId) {
+  return global.isDefaultCookieStoreId(storeId) ||
+         global.isPrivateCookieStoreId(storeId) ||
+         global.isContainerCookieStoreId(storeId);
 };
 
 function convert({cookie, isPrivate}) {
   let result = {
     name: cookie.name,
     value: cookie.value,
     domain: cookie.host,
     hostOnly: !cookie.isDomain,
     path: cookie.path,
     secure: cookie.isSecure,
     httpOnly: cookie.isHttpOnly,
     session: cookie.isSession,
-    storeId: isPrivate ? PRIVATE_STORE : DEFAULT_STORE,
   };
 
   if (!cookie.isSession) {
     result.expirationDate = cookie.expiry;
   }
 
+  if (cookie.originAttributes.userContextId) {
+    result.storeId = CONTAINER_STORE + cookie.originAttributes.userContextId;
+  } else if (cookie.originAttributes.privateBrowsingId || isPrivate) {
+    result.storeId = PRIVATE_STORE;
+  } else {
+    result.storeId = DEFAULT_STORE;
+  }
+
   return result;
 }
 
 function isSubdomain(otherDomain, baseDomain) {
   return otherDomain == baseDomain || otherDomain.endsWith("." + baseDomain);
 }
 
 // Checks that the given extension has permission to set the given cookie for