Bug 1254221 - Support private browsing cookies in WebExtensions r=kmag
authorJesper Kristensen <mail@jesperkristensen.dk>
Thu, 29 Sep 2016 19:18:14 +0200
changeset 316133 f2181015902f1bd9e533bf6103bbbb9e65d2436e
parent 316132 43c03bc692083131117463df747c15ab82eb3ad7
child 316134 b7b705333876329e496b7735036f38072b3fd1a8
push id30762
push userphilringnalda@gmail.com
push dateSat, 01 Oct 2016 21:00:36 +0000
treeherdermozilla-central@d1fd56faaeb9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1254221
milestone52.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 1254221 - Support private browsing cookies in WebExtensions r=kmag MozReview-Commit-ID: 29ci8wbnMra
browser/components/extensions/.eslintrc
browser/components/extensions/ext-utils.js
toolkit/components/extensions/.eslintrc
toolkit/components/extensions/ext-cookies.js
toolkit/components/extensions/test/mochitest/mochitest.ini
toolkit/components/extensions/test/mochitest/test_ext_cookies.html
--- a/browser/components/extensions/.eslintrc
+++ b/browser/components/extensions/.eslintrc
@@ -1,15 +1,16 @@
 {
   "extends": "../../../toolkit/components/extensions/.eslintrc",
 
   "globals": {
     "AllWindowEvents": true,
     "currentWindow": true,
     "EventEmitter": true,
+    "getCookieStoreIdForTab": true,
     "IconDetails": true,
     "makeWidgetId": true,
     "pageActionFor": true,
     "PanelPopup": true,
     "TabContext": true,
     "ViewPopup": true,
     "WindowEventManager": true,
     "WindowListManager": true,
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -701,16 +701,19 @@ ExtensionTabManager.prototype = {
       pinned: tab.pinned,
       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);
+    }
 
     if (this.hasTabPermission(tab)) {
       result.url = browser.currentURI.spec;
       let title = browser.contentTitle || tab.label;
       if (title) {
         result.title = title;
       }
       let icon = window.gBrowser.getIcon(tab);
--- a/toolkit/components/extensions/.eslintrc
+++ b/toolkit/components/extensions/.eslintrc
@@ -18,16 +18,17 @@
     "NetUtil": true,
     "openOptionsPage": true,
     "require": false,
     "runSafe": true,
     "runSafeSync": true,
     "runSafeSyncWithoutClone": true,
     "Services": true,
     "TabManager": true,
+    "WindowListManager": true,
     "XPCOMUtils": true,
   },
 
   "rules": {
     // Rules from the mozilla plugin
     "mozilla/balanced-listeners": 2,
     "mozilla/mark-test-function-used": 1,
     "mozilla/no-aArgs": 1,
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -4,30 +4,34 @@ const {interfaces: Ci, utils: Cu} = Comp
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 
 var {
   EventManager,
 } = ExtensionUtils;
 
-// Cookies from private tabs currently can't be enumerated.
 var DEFAULT_STORE = "firefox-default";
+var PRIVATE_STORE = "firefox-private";
 
-function convert(cookie) {
+global.getCookieStoreIdForTab = function(tab) {
+  return tab.incognito ? PRIVATE_STORE : DEFAULT_STORE;
+};
+
+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: DEFAULT_STORE,
+    storeId: isPrivate ? PRIVATE_STORE : DEFAULT_STORE,
   };
 
   if (!cookie.isSession) {
     result.expirationDate = cookie.expiry;
   }
 
   return result;
 }
@@ -121,45 +125,60 @@ function checkSetCookiePermissions(exten
 
   // We don't do any significant checking of path permissions. RFC2109
   // suggests we only allow sites to add cookies for sub-paths, similar to
   // same origin policy enforcement, but no-one implements this.
 
   return true;
 }
 
-function* query(detailsIn, props, extension) {
+function* query(detailsIn, props, context) {
   // Different callers want to filter on different properties. |props|
   // tells us which ones they're interested in.
   let details = {};
   props.forEach(property => {
     if (detailsIn[property] !== null) {
       details[property] = detailsIn[property];
     }
   });
 
   if ("domain" in details) {
     details.domain = details.domain.toLowerCase().replace(/^\./, "");
   }
 
+  let isPrivate = context.incognito;
+  if (details.storeId == DEFAULT_STORE) {
+    isPrivate = false;
+  } else if (details.storeId == PRIVATE_STORE) {
+    isPrivate = true;
+  } else if ("storeId" in details) {
+    return;
+  }
+
   // We can use getCookiesFromHost for faster searching.
   let enumerator;
   let uri;
   if ("url" in details) {
     try {
       uri = NetUtil.newURI(details.url).QueryInterface(Ci.nsIURL);
-      enumerator = Services.cookies.getCookiesFromHost(uri.host, {});
+      Services.cookies.usePrivateMode(isPrivate, () => {
+        enumerator = Services.cookies.getCookiesFromHost(uri.host, {});
+      });
     } catch (ex) {
       // This often happens for about: URLs
       return;
     }
   } else if ("domain" in details) {
-    enumerator = Services.cookies.getCookiesFromHost(details.domain, {});
+    Services.cookies.usePrivateMode(isPrivate, () => {
+      enumerator = Services.cookies.getCookiesFromHost(details.domain, {});
+    });
   } else {
-    enumerator = Services.cookies.enumerator;
+    Services.cookies.usePrivateMode(isPrivate, () => {
+      enumerator = Services.cookies.enumerator;
+    });
   }
 
   // Based on nsCookieService::GetCookieStringInternal
   function matches(cookie) {
     function domainMatches(host) {
       return cookie.rawHost == host || (cookie.isDomain && host.endsWith(cookie.host));
     }
 
@@ -213,53 +232,49 @@ function* query(detailsIn, props, extens
     if ("secure" in details && details.secure != cookie.isSecure) {
       return false;
     }
 
     if ("session" in details && details.session != cookie.isSession) {
       return false;
     }
 
-    if ("storeId" in details && details.storeId != DEFAULT_STORE) {
-      return false;
-    }
-
     // Check that the extension has permissions for this host.
-    if (!extension.whiteListedHosts.matchesCookie(cookie)) {
+    if (!context.extension.whiteListedHosts.matchesCookie(cookie)) {
       return false;
     }
 
     return true;
   }
 
   while (enumerator.hasMoreElements()) {
     let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
     if (matches(cookie)) {
-      yield cookie;
+      yield {cookie, isPrivate};
     }
   }
 }
 
 extensions.registerSchemaAPI("cookies", "addon_parent", context => {
   let {extension} = context;
   let self = {
     cookies: {
       get: function(details) {
         // FIXME: We don't sort by length of path and creation time.
-        for (let cookie of query(details, ["url", "name", "storeId"], extension)) {
+        for (let cookie of query(details, ["url", "name", "storeId"], context)) {
           return Promise.resolve(convert(cookie));
         }
 
         // Found no match.
         return Promise.resolve(null);
       },
 
       getAll: function(details) {
         let allowed = ["url", "name", "domain", "path", "secure", "session", "storeId"];
-        let result = Array.from(query(details, allowed, extension), convert);
+        let result = Array.from(query(details, allowed, context), convert);
 
         return Promise.resolve(result);
       },
 
       set: function(details) {
         let uri = NetUtil.newURI(details.url).QueryInterface(Ci.nsIURL);
 
         let path;
@@ -274,57 +289,86 @@ extensions.registerSchemaAPI("cookies", 
         }
 
         let name = details.name !== null ? details.name : "";
         let value = details.value !== null ? details.value : "";
         let secure = details.secure !== null ? details.secure : false;
         let httpOnly = details.httpOnly !== null ? details.httpOnly : false;
         let isSession = details.expirationDate === null;
         let expiry = isSession ? Number.MAX_SAFE_INTEGER : details.expirationDate;
-        // Ignore storeID.
+        let isPrivate = context.incognito;
+        if (details.storeId == DEFAULT_STORE) {
+          isPrivate = false;
+        } else if (details.storeId == PRIVATE_STORE) {
+          isPrivate = true;
+        } else if (details.storeId !== null) {
+          return Promise.reject({message: "Unknown storeId"});
+        }
 
         let cookieAttrs = {host: details.domain, path: path, isSecure: secure};
         if (!checkSetCookiePermissions(extension, uri, cookieAttrs)) {
           return Promise.reject({message: `Permission denied to set cookie ${JSON.stringify(details)}`});
         }
 
         // The permission check may have modified the domain, so use
         // the new value instead.
-        Services.cookies.add(cookieAttrs.host, path, name, value,
-                             secure, httpOnly, isSession, expiry, {});
+        Services.cookies.usePrivateMode(isPrivate, () => {
+          Services.cookies.add(cookieAttrs.host, path, name, value,
+                               secure, httpOnly, isSession, expiry, {});
+        });
 
         return self.cookies.get(details);
       },
 
       remove: function(details) {
-        for (let cookie of query(details, ["url", "name", "storeId"], extension)) {
-          Services.cookies.remove(cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+        for (let {cookie, isPrivate} of query(details, ["url", "name", "storeId"], context)) {
+          Services.cookies.usePrivateMode(isPrivate, () => {
+            Services.cookies.remove(cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+          });
           // Todo: could there be multiple per subdomain?
           return Promise.resolve({
             url: details.url,
             name: details.name,
-            storeId: DEFAULT_STORE,
+            storeId: isPrivate ? PRIVATE_STORE : DEFAULT_STORE,
           });
         }
 
         return Promise.resolve(null);
       },
 
       getAllCookieStores: function() {
-        // Todo: list all the tabIds for non-private tabs
-        return Promise.resolve([{id: DEFAULT_STORE, tabIds: []}]);
+        let defaultTabs = [];
+        let privateTabs = [];
+        for (let window of WindowListManager.browserWindows()) {
+          let tabs = TabManager.for(extension).getTabs(window);
+          for (let tab of tabs) {
+            if (tab.incognito) {
+              privateTabs.push(tab.id);
+            } else {
+              defaultTabs.push(tab.id);
+            }
+          }
+        }
+        let result = [];
+        if (defaultTabs.length > 0) {
+          result.push({id: DEFAULT_STORE, tabIds: defaultTabs});
+        }
+        if (privateTabs.length > 0) {
+          result.push({id: PRIVATE_STORE, tabIds: privateTabs});
+        }
+        return Promise.resolve(result);
       },
 
       onChanged: new EventManager(context, "cookies.onChanged", fire => {
         let observer = (subject, topic, data) => {
           let notify = (removed, cookie, cause) => {
             cookie.QueryInterface(Ci.nsICookie2);
 
             if (extension.whiteListedHosts.matchesCookie(cookie)) {
-              fire({removed, cookie: convert(cookie), cause});
+              fire({removed, cookie: convert({cookie, isPrivate: topic == "private-cookie-changed"}), cause});
             }
           };
 
           // We do our best effort here to map the incompatible states.
           switch (data) {
             case "deleted":
               notify(true, subject, "explicit");
               break;
@@ -345,15 +389,19 @@ extensions.registerSchemaAPI("cookies", 
                   notify(true, cookie, "evicted");
                 }
               }
               break;
           }
         };
 
         Services.obs.addObserver(observer, "cookie-changed", false);
-        return () => Services.obs.removeObserver(observer, "cookie-changed");
+        Services.obs.addObserver(observer, "private-cookie-changed", false);
+        return () => {
+          Services.obs.removeObserver(observer, "cookie-changed");
+          Services.obs.removeObserver(observer, "private-cookie-changed");
+        };
       }).api(),
     },
   };
 
   return self;
 });
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -72,16 +72,17 @@ skip-if = os == 'android' # port.sender.
 skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_sendmessage_doublereply.html]
 skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_sendmessage_no_receiver.html]
 [test_ext_storage_content.html]
 [test_ext_storage_tab.html]
 skip-if = os == 'android' # Android does not currently support tabs.
 [test_ext_cookies.html]
+skip-if = (os == 'android' || buildapp == 'b2g') # needs TabManager which is not yet implemented. Bug 1258975 on android.
 [test_ext_background_api_injection.html]
 [test_ext_background_generated_url.html]
 [test_ext_background_teardown.html]
 [test_ext_tab_teardown.html]
 skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
 [test_ext_unload_frame.html]
 [test_ext_i18n.html]
 skip-if = (os == 'android') # Bug 1258975 on android.
--- a/toolkit/components/extensions/test/mochitest/test_ext_cookies.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html
@@ -25,16 +25,17 @@ add_task(function* test_cookies() {
 
     const TEST_URL = "http://example.org/";
     const TEST_SECURE_URL = "https://example.org/";
     const THE_FUTURE = Date.now() + 5 * 60;
     const TEST_PATH = "set_path";
     const TEST_URL_WITH_PATH = TEST_URL + TEST_PATH;
     const TEST_COOKIE_PATH = `/${TEST_PATH}`;
     const STORE_ID = "firefox-default";
+    const PRIVATE_STORE_ID = "firefox-private";
 
     let expected = {
       name: "name1",
       value: "value1",
       domain: "example.org",
       hostOnly: true,
       path: "/",
       secure: false,
@@ -79,17 +80,28 @@ add_task(function* test_cookies() {
       assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, details);
       return browser.cookies.get({url: TEST_URL, name: "name1"});
     }).then(cookie => {
       browser.test.assertEq(null, cookie, "removed cookie not found");
       return browser.cookies.getAllCookieStores();
     }).then(stores => {
       browser.test.assertEq(1, stores.length, "expected number of stores returned");
       browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned");
-      browser.test.assertEq(0, stores[0].tabIds.length, "no tabs returned for store"); // Todo: Implement this.
+      browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for store");
+      return browser.windows.create({incognito: true});
+    }).then(privateWindow => {
+      return browser.cookies.getAllCookieStores().then(stores => {
+        browser.test.assertEq(2, stores.length, "expected number of stores returned");
+        browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned");
+        browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for store");
+        browser.test.assertEq(PRIVATE_STORE_ID, stores[1].id, "expected private store id returned");
+        browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for private store");
+        return browser.windows.remove(privateWindow.id);
+      });
+    }).then(() => {
       return browser.cookies.set({url: TEST_URL, name: "name2", domain: ".example.org", expirationDate: THE_FUTURE});
     }).then(cookie => {
       browser.test.assertEq(false, cookie.hostOnly, "cookie is not a hostOnly cookie");
       return browser.cookies.remove({url: TEST_URL, name: "name2"});
     }).then(details => {
       assertExpected({url: TEST_URL, name: "name2", storeId: STORE_ID}, details);
       // Create a session cookie.
       return browser.cookies.set({url: TEST_URL, name: "name1", value: "value1"});
@@ -158,16 +170,44 @@ add_task(function* test_cookies() {
       return browser.cookies.remove({url: TEST_URL, name: "name1"});
     }).then(details => {
       assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, details);
       return browser.cookies.set({url: TEST_URL});
     }).then(cookie => {
       browser.test.assertEq("", cookie.name, "default name set");
       browser.test.assertEq("", cookie.value, "default value set");
       browser.test.assertEq(true, cookie.session, "no expiry date created session cookie");
+      return browser.windows.create({incognito: true});
+    }).then(privateWindow => {
+      return browser.cookies.set({url: TEST_URL, name: "store", value: "private", expirationDate: THE_FUTURE, storeId: PRIVATE_STORE_ID}).then(cookie => {
+        return browser.cookies.set({url: TEST_URL, name: "store", value: "default", expirationDate: THE_FUTURE, storeId: STORE_ID});
+      }).then(cookie => {
+        return browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+      }).then(cookie => {
+        browser.test.assertEq("private", cookie.value, "get the private cookie");
+        browser.test.assertEq(PRIVATE_STORE_ID, cookie.storeId, "get the private cookie storeId");
+        return browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID});
+      }).then(cookie => {
+        browser.test.assertEq("default", cookie.value, "get the default cookie");
+        browser.test.assertEq(STORE_ID, cookie.storeId, "get the default cookie storeId");
+        return browser.cookies.remove({url: TEST_URL, name: "store", storeId: STORE_ID});
+      }).then(details => {
+        assertExpected({url: TEST_URL, name: "store", storeId: STORE_ID}, details);
+        return browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID});
+      }).then(cookie => {
+        browser.test.assertEq(null, cookie, "deleted the default cookie");
+        return browser.cookies.remove({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+      }).then(details => {
+        assertExpected({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}, details);
+        return browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+      }).then(cookie => {
+        browser.test.assertEq(null, cookie, "deleted the private cookie");
+        return browser.windows.remove(privateWindow.id);
+      });
+    }).then(() => {
       browser.test.notifyPass("cookies");
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background,
     manifest: {
       permissions: ["cookies", "*://example.org/"],