Bug 1363860 - Allow WebExtensions to control cookie behaviour, r=mixedpuppy
authorBob Silverberg <bsilverberg@mozilla.com>
Thu, 30 Nov 2017 09:31:21 -0500
changeset 450875 21de927fd6adf57e678e2eac54266af5415cc16b
parent 450865 a3d15c0fe9a8c3e11efbb27d15695c5a623036f9
child 450876 ba6c2fff9fd54ca8f836ce3e2f29aaa8a8d65bd2
child 450917 bec7964d4dd08ba2204c37137c38b2e4f277b093
push id8543
push userryanvm@gmail.com
push dateTue, 16 Jan 2018 14:33:22 +0000
treeherdermozilla-beta@a6525ed16a32 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1363860
milestone59.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 1363860 - Allow WebExtensions to control cookie behaviour, r=mixedpuppy This introduces a new setting to the privacy API, browser.privacy.websites.cookieConfig, which controls both the network.cookie.cookieBehavior pref, and the network.cookie.lifetimePolicy pref. The former controls which types of cookies are accepted, while the latter which controls the expiration date of cookies. This setting accepts an object as its value with properties for "behavior" and "nonPersistentCookies", which control the prefs discussed above. Each of these properties is optional. nonPersistentCookies defaults to false, and an object without a value for the behavior property will result in the network.cookie.cookieBehavior pref being reset to its default value. MozReview-Commit-ID: KKE1dMCzTt6
toolkit/components/extensions/ext-privacy.js
toolkit/components/extensions/schemas/privacy.json
toolkit/components/extensions/test/xpcshell/test_ext_privacy.js
--- a/toolkit/components/extensions/ext-privacy.js
+++ b/toolkit/components/extensions/ext-privacy.js
@@ -1,20 +1,30 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 
 Cu.import("resource://gre/modules/ExtensionPreferencesManager.jsm");
+
 var {
   ExtensionError,
 } = ExtensionUtils;
 
+const cookieSvc = Ci.nsICookieService;
+
+const cookieBehaviorValues = new Map([
+  ["allow_all", cookieSvc.BEHAVIOR_ACCEPT],
+  ["reject_third_party", cookieSvc.BEHAVIOR_REJECT_FOREIGN],
+  ["reject_all", cookieSvc.BEHAVIOR_REJECT],
+  ["allow_visited", cookieSvc.BEHAVIOR_LIMIT_FOREIGN],
+]);
+
 const checkScope = scope => {
   if (scope && scope !== "regular") {
     throw new ExtensionError(
       `Firefox does not support the ${scope} settings scope.`);
   }
 };
 
 const getPrivacyAPI = (extension, name, callback) => {
@@ -110,16 +120,44 @@ ExtensionPreferencesManager.addSetting("
     "signon.rememberSignons",
   ],
 
   setCallback(value) {
     return {[this.prefNames[0]]: value};
   },
 });
 
+ExtensionPreferencesManager.addSetting("websites.cookieConfig", {
+  prefNames: [
+    "network.cookie.cookieBehavior",
+    "network.cookie.lifetimePolicy",
+  ],
+
+  setCallback(value) {
+    return {
+      "network.cookie.cookieBehavior":
+        cookieBehaviorValues.get(value.behavior),
+      "network.cookie.lifetimePolicy":
+        value.nonPersistentCookies ?
+          cookieSvc.ACCEPT_SESSION :
+          cookieSvc.ACCEPT_NORMALLY,
+    };
+  },
+});
+
+ExtensionPreferencesManager.addSetting("websites.firstPartyIsolate", {
+  prefNames: [
+    "privacy.firstparty.isolate",
+  ],
+
+  setCallback(value) {
+    return {[this.prefNames[0]]: value};
+  },
+});
+
 ExtensionPreferencesManager.addSetting("websites.hyperlinkAuditingEnabled", {
   prefNames: [
     "browser.send_pings",
   ],
 
   setCallback(value) {
     return {[this.prefNames[0]]: value};
   },
@@ -143,26 +181,16 @@ ExtensionPreferencesManager.addSetting("
     "privacy.resistFingerprinting",
   ],
 
   setCallback(value) {
     return {[this.prefNames[0]]: value};
   },
 });
 
-ExtensionPreferencesManager.addSetting("websites.firstPartyIsolate", {
-  prefNames: [
-    "privacy.firstparty.isolate",
-  ],
-
-  setCallback(value) {
-    return {[this.prefNames[0]]: value};
-  },
-});
-
 ExtensionPreferencesManager.addSetting("websites.trackingProtectionMode", {
   prefNames: [
     "privacy.trackingprotection.enabled",
     "privacy.trackingprotection.pbmode.enabled",
   ],
 
   setCallback(value) {
     // Default to private browsing.
@@ -231,36 +259,48 @@ this.privacy = class extends ExtensionAP
           passwordSavingEnabled: getPrivacyAPI(
             extension, "services.passwordSavingEnabled",
             () => {
               return Preferences.get("signon.rememberSignons");
             }),
         },
 
         websites: {
+          cookieConfig: getPrivacyAPI(
+            extension, "websites.cookieConfig",
+            () => {
+              let prefValue = Preferences.get("network.cookie.cookieBehavior");
+              return {
+                behavior:
+                  Array.from(
+                    cookieBehaviorValues.entries()).find(entry => entry[1] === prefValue)[0],
+                nonPersistentCookies:
+                  Preferences.get("network.cookie.lifetimePolicy") === cookieSvc.ACCEPT_SESSION,
+              };
+            }),
+          firstPartyIsolate: getPrivacyAPI(
+            extension, "websites.firstPartyIsolate",
+            () => {
+              return Preferences.get("privacy.firstparty.isolate");
+            }),
           hyperlinkAuditingEnabled: getPrivacyAPI(
             extension, "websites.hyperlinkAuditingEnabled",
             () => {
               return Preferences.get("browser.send_pings");
             }),
           referrersEnabled: getPrivacyAPI(
             extension, "websites.referrersEnabled",
             () => {
               return Preferences.get("network.http.sendRefererHeader") !== 0;
             }),
           resistFingerprinting: getPrivacyAPI(
             extension, "websites.resistFingerprinting",
             () => {
               return Preferences.get("privacy.resistFingerprinting");
             }),
-          firstPartyIsolate: getPrivacyAPI(
-            extension, "websites.firstPartyIsolate",
-            () => {
-              return Preferences.get("privacy.firstparty.isolate");
-            }),
           trackingProtectionMode: getPrivacyAPI(
             extension, "websites.trackingProtectionMode",
             () => {
               if (Preferences.get("privacy.trackingprotection.enabled")) {
                 return "always";
               } else if (Preferences.get("privacy.trackingprotection.pbmode.enabled")) {
                 return "private_browsing";
               }
--- a/toolkit/components/extensions/schemas/privacy.json
+++ b/toolkit/components/extensions/schemas/privacy.json
@@ -64,16 +64,40 @@
     "description": "Use the <code>browser.privacy</code> API to control usage of the features in the browser that can affect a user's privacy.",
     "permissions": ["privacy"],
     "types": [
       {
         "id": "TrackingProtectionModeOption",
         "type": "string",
         "enum": ["always", "never", "private_browsing"],
         "description": "The mode for tracking protection."
+      },
+      {
+        "id": "CookieConfig",
+        "type": "object",
+        "description": "The settings for cookies.",
+        "properties": {
+          "behavior": {
+            "type": "string",
+            "optional": true,
+            "enum": [
+              "allow_all",
+              "reject_all",
+              "reject_third_party",
+              "allow_visited"
+            ],
+            "description": "The type of cookies to allow."
+          },
+          "nonPersistentCookies": {
+            "type": "boolean",
+            "optional": true,
+            "default": false,
+            "description": "Whether to create all cookies as nonPersistent (i.e., session) cookies."
+          }
+        }
       }
     ],
     "properties": {
       "thirdPartyCookiesAllowed": {
         "$ref": "types.Setting",
         "description": "If disabled, the browser blocks third-party sites from setting cookies. The value of this preference is of type boolean, and the default value is <code>true</code>.",
         "unsupported": true
       },
@@ -96,12 +120,16 @@
       "protectedContentEnabled": {
         "$ref": "types.Setting",
         "description": "<strong>Available on Windows and ChromeOS only</strong>: If enabled, the browser provides a unique ID to plugins in order to run protected content. The value of this preference is of type boolean, and the default value is <code>true</code>.",
         "unsupported": true
       },
       "trackingProtectionMode": {
         "$ref": "types.Setting",
         "description": "Allow users to specify the mode for tracking protection. This setting's value is of type TrackingProtectionModeOption, defaulting to <code>private_browsing_only</code>."
+      },
+      "cookieConfig": {
+        "$ref": "types.Setting",
+        "description": "Allow users to specify the default settings for allowing cookies, as well as whether all cookies should be created as non-persistent cookies. This setting's value is of type CookieConfig."
       }
     }
   }
 ]
--- a/toolkit/components/extensions/test/xpcshell/test_ext_privacy.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_privacy.js
@@ -76,16 +76,18 @@ add_task(async function test_privacy() {
     // Reset the prefs.
     for (let setting in SETTINGS) {
       for (let pref in SETTINGS[setting]) {
         Preferences.reset(pref);
       }
     }
   });
 
+  await promiseStartupManager();
+
   // Create an array of extensions to install.
   let testExtensions = [
     ExtensionTestUtils.loadExtension({
       background,
       manifest: {
         permissions: ["privacy"],
       },
       useAddonManager: "temporary",
@@ -95,18 +97,16 @@ add_task(async function test_privacy() {
       background,
       manifest: {
         permissions: ["privacy"],
       },
       useAddonManager: "temporary",
     }),
   ];
 
-  await promiseStartupManager();
-
   for (let extension of testExtensions) {
     await extension.startup();
   }
 
   for (let setting in SETTINGS) {
     testExtensions[0].sendMessage("get", {}, setting);
     let data = await testExtensions[0].awaitMessage("gotData");
     ok(data.value, "get returns expected value.");
@@ -214,16 +214,18 @@ add_task(async function test_privacy() {
   for (let extension of testExtensions) {
     await extension.unload();
   }
 
   await promiseShutdownManager();
 });
 
 add_task(async function test_privacy_other_prefs() {
+  const cookieSvc = Ci.nsICookieService;
+
   // Create an object to hold the values to which we will initialize the prefs.
   const SETTINGS = {
     "network.webRTCIPHandlingPolicy": {
       "media.peerconnection.ice.default_address_only": false,
       "media.peerconnection.ice.no_host": false,
       "media.peerconnection.ice.proxy_only": false,
     },
     "network.peerConnectionEnabled": {
@@ -236,16 +238,20 @@ add_task(async function test_privacy_oth
       "network.http.sendRefererHeader": 2,
     },
     "websites.resistFingerprinting": {
       "privacy.resistFingerprinting": true,
     },
     "websites.firstPartyIsolate": {
       "privacy.firstparty.isolate": true,
     },
+    "websites.cookieConfig": {
+      "network.cookie.cookieBehavior": cookieSvc.BEHAVIOR_ACCEPT,
+      "network.cookie.lifetimePolicy": cookieSvc.ACCEPT_NORMALLY,
+    },
   };
 
   async function background() {
     browser.test.onMessage.addListener(async (msg, ...args) => {
       let data = args[0];
       // The second argument is the end of the api name,
       // e.g., "network.webRTCIPHandlingPolicy".
       let apiObj = args[1].split(".").reduce((o, i) => o[i], browser.privacy);
@@ -271,31 +277,32 @@ add_task(async function test_privacy_oth
     // Reset the prefs.
     for (let setting in SETTINGS) {
       for (let pref in SETTINGS[setting]) {
         Preferences.reset(pref);
       }
     }
   });
 
+  await promiseStartupManager();
+
   let extension = ExtensionTestUtils.loadExtension({
     background,
     manifest: {
       permissions: ["privacy"],
     },
     useAddonManager: "temporary",
   });
 
-  await promiseStartupManager();
   await extension.startup();
 
-  async function testSetting(setting, value, expected) {
+  async function testSetting(setting, value, expected, expectedValue = value) {
     extension.sendMessage("set", {value: value}, setting);
     let data = await extension.awaitMessage("settingData");
-    equal(data.value, value);
+    deepEqual(data.value, expectedValue);
     for (let pref in expected) {
       equal(Preferences.get(pref), expected[pref], `${pref} set correctly for ${value}`);
     }
   }
 
   await testSetting(
     "network.webRTCIPHandlingPolicy",
     "default_public_and_private_interfaces",
@@ -394,16 +401,90 @@ add_task(async function test_privacy_oth
       "signon.rememberSignons": false,
     });
   await testSetting(
     "services.passwordSavingEnabled", true,
     {
       "signon.rememberSignons": true,
     });
 
+  await testSetting(
+    "websites.cookieConfig",
+    {behavior: "reject_third_party", nonPersistentCookies: true},
+    {
+      "network.cookie.cookieBehavior": cookieSvc.BEHAVIOR_REJECT_FOREIGN,
+      "network.cookie.lifetimePolicy": cookieSvc.ACCEPT_SESSION,
+    },
+  );
+  // A missing nonPersistentCookies property should default to false.
+  await testSetting(
+    "websites.cookieConfig",
+    {behavior: "reject_third_party"},
+    {
+      "network.cookie.cookieBehavior": cookieSvc.BEHAVIOR_REJECT_FOREIGN,
+      "network.cookie.lifetimePolicy": cookieSvc.ACCEPT_NORMALLY,
+    },
+    {behavior: "reject_third_party", nonPersistentCookies: false},
+  );
+  // A missing behavior property should reset the pref.
+  await testSetting(
+    "websites.cookieConfig",
+    {nonPersistentCookies: true},
+    {
+      "network.cookie.cookieBehavior": cookieSvc.BEHAVIOR_ACCEPT,
+      "network.cookie.lifetimePolicy": cookieSvc.ACCEPT_SESSION,
+    },
+    {behavior: "allow_all", nonPersistentCookies: true},
+  );
+  await testSetting(
+    "websites.cookieConfig",
+    {behavior: "reject_all"},
+    {
+      "network.cookie.cookieBehavior": cookieSvc.BEHAVIOR_REJECT,
+      "network.cookie.lifetimePolicy": cookieSvc.ACCEPT_NORMALLY,
+    },
+    {behavior: "reject_all", nonPersistentCookies: false},
+  );
+  await testSetting(
+    "websites.cookieConfig",
+    {behavior: "allow_visited"},
+    {
+      "network.cookie.cookieBehavior": cookieSvc.BEHAVIOR_LIMIT_FOREIGN,
+      "network.cookie.lifetimePolicy": cookieSvc.ACCEPT_NORMALLY,
+    },
+    {behavior: "allow_visited", nonPersistentCookies: false},
+  );
+  await testSetting(
+    "websites.cookieConfig",
+    {behavior: "allow_all"},
+    {
+      "network.cookie.cookieBehavior": cookieSvc.BEHAVIOR_ACCEPT,
+      "network.cookie.lifetimePolicy": cookieSvc.ACCEPT_NORMALLY,
+    },
+    {behavior: "allow_all", nonPersistentCookies: false},
+  );
+  await testSetting(
+    "websites.cookieConfig",
+    {nonPersistentCookies: true},
+    {
+      "network.cookie.cookieBehavior": cookieSvc.BEHAVIOR_ACCEPT,
+      "network.cookie.lifetimePolicy": cookieSvc.ACCEPT_SESSION,
+    },
+    {behavior: "allow_all", nonPersistentCookies: true},
+  );
+  await testSetting(
+    "websites.cookieConfig",
+    {nonPersistentCookies: false},
+    {
+      "network.cookie.cookieBehavior": cookieSvc.BEHAVIOR_ACCEPT,
+      "network.cookie.lifetimePolicy": cookieSvc.ACCEPT_NORMALLY,
+    },
+    {behavior: "allow_all", nonPersistentCookies: false},
+  );
+
   await extension.unload();
 
   await promiseShutdownManager();
 });
 
 add_task(async function test_exceptions() {
   async function background() {
     await browser.test.assertRejects(