Bug 1363860 - Allow WebExtensions to control cookie behaviour, r?mixedpuppy draft
authorBob Silverberg <bsilverberg@mozilla.com>
Thu, 30 Nov 2017 09:31:21 -0500
changeset 718842 51e5864910bab7690fd58e71afedaf4949bb6b3c
parent 718598 2438a090b77a56b38547f7250bb499cb2e1ffcce
child 745606 187cd792137becea6b2aa3fe5279cddbcc89f8aa
push id95055
push userbmo:bob.silverberg@gmail.com
push dateWed, 10 Jan 2018 22:30:05 +0000
reviewersmixedpuppy
bugs1363860
milestone59.0a1
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(