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
--- 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(