Bug 1312802 - Implement chrome.privacy API, r?aswan draft
authorBob Silverberg <bsilverberg@mozilla.com>
Wed, 15 Feb 2017 17:32:24 -0500
changeset 487647 447315fde553ec7a99e41fea1aa1f352d9adad3c
parent 487583 c7b015c488cfb2afbcff295a9639acd85df332f8
child 546507 b2ecbf3d74a23ad0be663218eba4d2d7852005d9
push id46280
push userbmo:bob.silverberg@gmail.com
push dateTue, 21 Feb 2017 22:16:05 +0000
reviewersaswan
bugs1312802
milestone54.0a1
Bug 1312802 - Implement chrome.privacy API, r?aswan MozReview-Commit-ID: 5DoGnYb945Z
browser/locales/en-US/chrome/browser/browser.properties
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/ext-privacy.js
toolkit/components/extensions/extensions-toolkit.manifest
toolkit/components/extensions/jar.mn
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/privacy.json
toolkit/components/extensions/schemas/types.json
toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
toolkit/components/extensions/test/xpcshell/test_ext_privacy.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -86,16 +86,17 @@ webextPerms.description.clipboardRead=Ge
 webextPerms.description.clipboardWrite=Input data to the clipboard
 webextPerms.description.downloads=Download files and read and modify the browser’s download history
 webextPerms.description.geolocation=Access your location
 webextPerms.description.history=Access browsing history
 # LOCALIZATION NOTE (webextPerms.description.nativeMessaging)
 # %S will be replaced with the name of the application
 webextPerms.description.nativeMessaging=Exchange messages with programs other than %S
 webextPerms.description.notifications=Display notifications to you
+webextPerms.description.privacy=Read and modify privacy settings
 webextPerms.description.sessions=Access recently closed tabs
 webextPerms.description.tabs=Access browser tabs
 webextPerms.description.topSites=Access browsing history
 webextPerms.description.webNavigation=Access browser activity during navigation
 
 webextPerms.hostDescription.allUrls=Access your data for all websites
 
 # LOCALIZATION NOTE (webextPerms.hostDescription.wildcard)
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -1612,17 +1612,17 @@ class SubModuleProperty extends Entry {
     }
 
     if (DEBUG) {
       if (!type || !(type instanceof SubModuleType)) {
         throw new Error(`Internal error: ${this.namespaceName}.${this.reference} ` +
                         `is not a sub-module`);
       }
     }
-    let subpath = [path, this.name];
+    let subpath = [...path, this.name];
     let namespace = subpath.join(".");
 
     let functions = type.functions;
     for (let fun of functions) {
       let allowedContexts = fun.allowedContexts.length ? fun.allowedContexts : ns.defaultContexts;
       if (context.shouldInject(namespace, fun.name, allowedContexts)) {
         exportLazyProperty(obj, fun.name,
                            () => fun.getDescriptor(subpath, context));
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-privacy.js
@@ -0,0 +1,164 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+
+Cu.import("resource://gre/modules/ExtensionPreferencesManager.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+const {
+  ExtensionError,
+} = ExtensionUtils;
+
+function checkScope(scope) {
+  if (scope && scope !== "regular") {
+    throw new ExtensionError(
+      `Firefox does not support the ${scope} settings scope.`);
+  }
+}
+
+function getAPI(extension, context, name, callback) {
+  let anythingSet = false;
+  return {
+    async get(details) {
+      return {
+        levelOfControl: details.incognito ?
+          "not_controllable" :
+          await ExtensionPreferencesManager.getLevelOfControl(
+            extension, name),
+        value: await callback(),
+      };
+    },
+    async set(details) {
+      checkScope(details.scope);
+      if (!anythingSet) {
+        anythingSet = true;
+        context.callOnClose({
+          close: async () => {
+            if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(extension.shutdownReason)) {
+              await ExtensionPreferencesManager.unsetAll(extension);
+              anythingSet = false;
+            }
+          },
+        });
+      }
+      return await ExtensionPreferencesManager.setSetting(
+        extension, name, details.value);
+    },
+    async clear(details) {
+      checkScope(details.scope);
+      return await ExtensionPreferencesManager.unsetSetting(
+        extension, name);
+    },
+  };
+}
+
+// Add settings objects for supported APIs to the preferences manager.
+ExtensionPreferencesManager.addSetting("network.networkPredictionEnabled", {
+  prefNames: [
+    "network.predictor.enabled",
+    "network.prefetch-next",
+    "network.http.speculative-parallel-limit",
+    "network.dns.disablePrefetch",
+  ],
+
+  setCallback(value) {
+    return {
+      "network.http.speculative-parallel-limit": value ? undefined : 0,
+      "network.dns.disablePrefetch": !value,
+      "network.predictor.enabled": value,
+      "network.prefetch-next": value,
+    };
+  },
+});
+
+ExtensionPreferencesManager.addSetting("network.webRTCIPHandlingPolicy", {
+  prefNames: [
+    "media.peerconnection.ice.default_address_only",
+    "media.peerconnection.ice.no_host",
+    "media.peerconnection.ice.proxy_only",
+  ],
+
+  setCallback(value) {
+    let prefs = {};
+    // Start with all prefs being reset.
+    for (let pref of this.prefNames) {
+      prefs[pref] = undefined;
+    }
+    switch (value) {
+      case "default":
+        // All prefs are already set to be reset.
+        break;
+
+      case "default_public_and_private_interfaces":
+        prefs["media.peerconnection.ice.default_address_only"] = true;
+        break;
+
+      case "default_public_interface_only":
+        prefs["media.peerconnection.ice.default_address_only"] = true;
+        prefs["media.peerconnection.ice.no_host"] = true;
+        break;
+
+      case "disable_non_proxied_udp":
+        prefs["media.peerconnection.ice.proxy_only"] = true;
+        break;
+    }
+    return prefs;
+  },
+});
+
+ExtensionPreferencesManager.addSetting("websites.hyperlinkAuditingEnabled", {
+  prefNames: [
+    "browser.send_pings",
+  ],
+
+  setCallback(value) {
+    return {[this.prefNames[0]]: value};
+  },
+});
+
+extensions.registerSchemaAPI("privacy.network", "addon_parent", context => {
+  let {extension} = context;
+  return {
+    privacy: {
+      network: {
+        networkPredictionEnabled: getAPI(extension, context,
+          "network.networkPredictionEnabled",
+          () => {
+            return Preferences.get("network.predictor.enabled") &&
+              Preferences.get("network.prefetch-next") &&
+              Preferences.get("network.http.speculative-parallel-limit") > 0 &&
+              !Preferences.get("network.dns.disablePrefetch");
+          }),
+        webRTCIPHandlingPolicy: getAPI(extension, context,
+          "network.webRTCIPHandlingPolicy",
+          () => {
+            if (Preferences.get("media.peerconnection.ice.proxy_only")) {
+              return "disable_non_proxied_udp";
+            }
+
+            let default_address_only =
+              Preferences.get("media.peerconnection.ice.default_address_only");
+            if (default_address_only) {
+              if (Preferences.get("media.peerconnection.ice.no_host")) {
+                return "default_public_interface_only";
+              }
+              return "default_public_and_private_interfaces";
+            }
+
+            return "default";
+          }),
+      },
+      websites: {
+        hyperlinkAuditingEnabled: getAPI(extension, context,
+          "websites.hyperlinkAuditingEnabled",
+          () => {
+            return Preferences.get("browser.send_pings");
+          }),
+      },
+    },
+  };
+});
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -10,16 +10,17 @@ category webextension-scripts notificati
 category webextension-scripts i18n chrome://extensions/content/ext-i18n.js
 category webextension-scripts idle chrome://extensions/content/ext-idle.js
 category webextension-scripts webRequest chrome://extensions/content/ext-webRequest.js
 category webextension-scripts webNavigation chrome://extensions/content/ext-webNavigation.js
 category webextension-scripts runtime chrome://extensions/content/ext-runtime.js
 category webextension-scripts extension chrome://extensions/content/ext-extension.js
 category webextension-scripts storage chrome://extensions/content/ext-storage.js
 category webextension-scripts topSites chrome://extensions/content/ext-topSites.js
+category webextension-scripts privacy chrome://extensions/content/ext-privacy.js
 
 # scripts specific for content process.
 category webextension-scripts-content extension chrome://extensions/content/ext-c-extension.js
 category webextension-scripts-content i18n chrome://extensions/content/ext-i18n.js
 category webextension-scripts-content runtime chrome://extensions/content/ext-c-runtime.js
 category webextension-scripts-content test chrome://extensions/content/ext-c-test.js
 category webextension-scripts-content storage chrome://extensions/content/ext-c-storage.js
 
@@ -52,14 +53,16 @@ category webextension-schemas extension_
 category webextension-schemas i18n chrome://extensions/content/schemas/i18n.json
 #ifndef ANDROID
 category webextension-schemas identity chrome://extensions/content/schemas/identity.json
 #endif
 category webextension-schemas idle chrome://extensions/content/schemas/idle.json
 category webextension-schemas management chrome://extensions/content/schemas/management.json
 category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
 category webextension-schemas notifications chrome://extensions/content/schemas/notifications.json
+category webextension-schemas privacy chrome://extensions/content/schemas/privacy.json
 category webextension-schemas runtime chrome://extensions/content/schemas/runtime.json
 category webextension-schemas storage chrome://extensions/content/schemas/storage.json
 category webextension-schemas test chrome://extensions/content/schemas/test.json
 category webextension-schemas top_sites chrome://extensions/content/schemas/top_sites.json
+category webextension-schemas types chrome://extensions/content/schemas/types.json
 category webextension-schemas web_navigation chrome://extensions/content/schemas/web_navigation.json
 category webextension-schemas web_request chrome://extensions/content/schemas/web_request.json
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -16,16 +16,17 @@ toolkit.jar:
     content/extensions/ext-i18n.js
     content/extensions/ext-idle.js
     content/extensions/ext-webRequest.js
     content/extensions/ext-webNavigation.js
     content/extensions/ext-runtime.js
     content/extensions/ext-extension.js
     content/extensions/ext-storage.js
     content/extensions/ext-topSites.js
+    content/extensions/ext-privacy.js
     content/extensions/ext-c-backgroundPage.js
     content/extensions/ext-c-extension.js
 #ifndef ANDROID
     content/extensions/ext-c-identity.js
 #endif
     content/extensions/ext-c-runtime.js
     content/extensions/ext-c-storage.js
     content/extensions/ext-c-test.js
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -16,14 +16,16 @@ toolkit.jar:
 #ifndef ANDROID
     content/extensions/schemas/identity.json
 #endif
     content/extensions/schemas/idle.json
     content/extensions/schemas/management.json
     content/extensions/schemas/manifest.json
     content/extensions/schemas/native_host_manifest.json
     content/extensions/schemas/notifications.json
+    content/extensions/schemas/privacy.json
     content/extensions/schemas/runtime.json
     content/extensions/schemas/storage.json
     content/extensions/schemas/test.json
     content/extensions/schemas/top_sites.json
+    content/extensions/schemas/types.json
     content/extensions/schemas/web_navigation.json
     content/extensions/schemas/web_request.json
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/privacy.json
@@ -0,0 +1,73 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "privacy"
+          ]
+        }]
+      }
+    ]
+  },
+  {
+    "namespace": "privacy",
+    "permissions": ["privacy"]
+  },
+  {
+    "namespace": "privacy.network",
+    "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": "IPHandlingPolicy",
+        "type": "string",
+        "enum": ["default", "default_public_and_private_interfaces", "default_public_interface_only", "disable_non_proxied_udp"],
+        "description": "The IP handling policy of WebRTC."
+      }
+    ],
+    "properties": {
+      "networkPredictionEnabled": {
+        "$ref": "types.Setting",
+        "description": "If enabled, the browser attempts to speed up your web browsing experience by pre-resolving DNS entries, prerendering sites (<code>&lt;link rel='prefetch' ...&gt;</code>), and preemptively opening TCP and SSL connections to servers.  This preference's value is a boolean, defaulting to <code>true</code>."
+      },
+      "webRTCIPHandlingPolicy": {
+        "$ref": "types.Setting",
+        "description": "Allow users to specify the media performance/privacy tradeoffs which impacts how WebRTC traffic will be routed and how much local address information is exposed. This preference's value is of type IPHandlingPolicy, defaulting to <code>default</code>."
+      }
+    }
+  },
+  {
+    "namespace": "privacy.websites",
+    "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"],
+    "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
+      },
+      "hyperlinkAuditingEnabled": {
+        "$ref": "types.Setting",
+        "description": "If enabled, the browser sends auditing pings when requested by a website (<code>&lt;a ping&gt;</code>). The value of this preference is of type boolean, and the default value is <code>true</code>."
+      },
+      "referrersEnabled": {
+        "$ref": "types.Setting",
+        "description": "If enabled, the browser sends <code>referer</code> headers with your requests. Yes, the name of this preference doesn't match the misspelled header. No, we're not going to change it. The value of this preference is of type boolean, and the default value is <code>true</code>.",
+        "unsupported": true
+      },
+      "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
+      }
+    }
+  }
+]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/types.json
@@ -0,0 +1,163 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+  {
+    "namespace": "types",
+    "description": "Contains types used by other schemas.",
+    "types": [
+      {
+        "id": "SettingScope",
+        "type": "string",
+        "enum": ["regular", "regular_only", "incognito_persistent", "incognito_session_only"],
+        "description": "The scope of the Setting. One of<ul><li><var>regular</var>: setting for the regular profile (which is inherited by the incognito profile if not overridden elsewhere),</li><li><var>regular_only</var>: setting for the regular profile only (not inherited by the incognito profile),</li><li><var>incognito_persistent</var>: setting for the incognito profile that survives browser restarts (overrides regular preferences),</li><li><var>incognito_session_only</var>: setting for the incognito profile that can only be set during an incognito session and is deleted when the incognito session ends (overrides regular and incognito_persistent preferences).</li></ul> Only <var>regular</var> is supported by Firefox at this time."
+      },
+      {
+        "id": "LevelOfControl",
+        "type": "string",
+        "enum": ["not_controllable", "controlled_by_other_extensions", "controllable_by_this_extension", "controlled_by_this_extension"],
+        "description": "One of<ul><li><var>not_controllable</var>: cannot be controlled by any extension</li><li><var>controlled_by_other_extensions</var>: controlled by extensions with higher precedence</li><li><var>controllable_by_this_extension</var>: can be controlled by this extension</li><li><var>controlled_by_this_extension</var>: controlled by this extension</li></ul>"
+      },
+      {
+        "id": "Setting",
+        "type": "object",
+        "functions": [
+          {
+            "name": "get",
+            "type": "function",
+            "description": "Gets the value of a setting.",
+            "async": "callback",
+            "parameters": [
+              {
+                "name": "details",
+                "type": "object",
+                "description": "Which setting to consider.",
+                "properties": {
+                  "incognito": {
+                    "type": "boolean",
+                    "optional": true,
+                    "description": "Whether to return the value that applies to the incognito session (default false)."
+                  }
+                }
+              },
+              {
+                "name": "callback",
+                "type": "function",
+                "parameters": [
+                  {
+                    "name": "details",
+                    "type": "object",
+                    "description": "Details of the currently effective value.",
+                    "properties": {
+                      "value": {
+                        "description": "The value of the setting.",
+                        "type": "any"
+                      },
+                      "levelOfControl": {
+                        "$ref": "LevelOfControl",
+                        "description": "The level of control of the setting."
+                      },
+                      "incognitoSpecific": {
+                        "description": "Whether the effective value is specific to the incognito session.<br/>This property will <em>only</em> be present if the <var>incognito</var> property in the <var>details</var> parameter of <code>get()</code> was true.",
+                        "type": "boolean",
+                        "optional": true
+                      }
+                    }
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "name": "set",
+            "type": "function",
+            "description": "Sets the value of a setting.",
+            "async": "callback",
+            "parameters": [
+              {
+                "name": "details",
+                "type": "object",
+                "description": "Which setting to change.",
+                "properties": {
+                  "value": {
+                    "description": "The value of the setting. <br/>Note that every setting has a specific value type, which is described together with the setting. An extension should <em>not</em> set a value of a different type.",
+                    "type": "any"
+                  },
+                  "scope": {
+                    "$ref": "SettingScope",
+                    "optional": true,
+                    "description": "Where to set the setting (default: regular)."
+                  }
+                }
+              },
+              {
+                "name": "callback",
+                "type": "function",
+                "description": "Called at the completion of the set operation.",
+                "optional": true,
+                "parameters": []
+              }
+            ]
+          },
+          {
+            "name": "clear",
+            "type": "function",
+            "description": "Clears the setting, restoring any default value.",
+            "async": "callback",
+            "parameters": [
+              {
+                "name": "details",
+                "type": "object",
+                "description": "Which setting to clear.",
+                "properties": {
+                  "scope": {
+                    "$ref": "SettingScope",
+                    "optional": true,
+                    "description": "Where to clear the setting (default: regular)."
+                  }
+                }
+              },
+              {
+                "name": "callback",
+                "type": "function",
+                "description": "Called at the completion of the clear operation.",
+                "optional": true,
+                "parameters": []
+              }
+            ]
+          }
+        ],
+        "events": [
+          {
+            "name": "onChange",
+            "type": "function",
+            "description": "Fired after the setting changes.",
+            "unsupported": true,
+            "parameters": [
+              {
+                "type": "object",
+                "name": "details",
+                "properties": {
+                  "value": {
+                    "description": "The value of the setting after the change.",
+                    "type": "any"
+                  },
+                  "levelOfControl": {
+                    "$ref": "LevelOfControl",
+                    "description": "The level of control of the setting."
+                  },
+                  "incognitoSpecific": {
+                    "description": "Whether the value that has changed is specific to the incognito session.<br/>This property will <em>only</em> be present if the user has enabled the extension in incognito mode.",
+                    "type": "boolean",
+                    "optional": true
+                  }
+                }
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+]
--- a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
+++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
@@ -77,16 +77,18 @@ let expectedBackgroundApis = [
   "runtime.getBrowserInfo",
   "runtime.getPlatformInfo",
   "runtime.onInstalled",
   "runtime.onStartup",
   "runtime.onUpdateAvailable",
   "runtime.openOptionsPage",
   "runtime.reload",
   "runtime.setUninstallURL",
+  "types.LevelOfControl",
+  "types.SettingScope",
 ];
 
 function sendAllApis() {
   function isEvent(key, val) {
     if (!/^on[A-Z]/.test(key)) {
       return false;
     }
     let eventKeys = [];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_privacy.js
@@ -0,0 +1,326 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPreferencesManager",
+                                  "resource://gre/modules/ExtensionPreferencesManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+
+const {
+  createAppInfo,
+  promiseShutdownManager,
+  promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+add_task(async function test_privacy() {
+  // Create a object to hold the values to which we will initialize the prefs.
+  const SETTINGS = {
+    "network.networkPredictionEnabled": {
+      "network.predictor.enabled": true,
+      "network.prefetch-next": true,
+      "network.http.speculative-parallel-limit": 10,
+      "network.dns.disablePrefetch": false,
+    },
+    "websites.hyperlinkAuditingEnabled": {
+      "browser.send_pings": true,
+    },
+  };
+
+  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.networkPredictionEnabled".
+      let apiObj = args[1].split(".").reduce((o, i) => o[i], browser.privacy);
+      let settingData;
+      switch (msg) {
+        case "get":
+          settingData = await apiObj.get(data);
+          browser.test.sendMessage("gotData", settingData);
+          break;
+
+        case "set":
+          await apiObj.set(data);
+          settingData = await apiObj.get({});
+          browser.test.sendMessage("afterSet", settingData);
+          break;
+
+        case "clear":
+          await apiObj.clear(data);
+          settingData = await apiObj.get({});
+          browser.test.sendMessage("afterClear", settingData);
+          break;
+      }
+    });
+  }
+
+  // Set prefs to our initial values.
+  for (let setting in SETTINGS) {
+    for (let pref in SETTINGS[setting]) {
+      Preferences.set(pref, SETTINGS[setting][pref]);
+    }
+  }
+
+  do_register_cleanup(() => {
+    // Reset the prefs.
+    for (let setting in SETTINGS) {
+      for (let pref in SETTINGS[setting]) {
+        Preferences.reset(pref);
+      }
+    }
+  });
+
+  // Create an array of extensions to install.
+  let testExtensions = [
+    ExtensionTestUtils.loadExtension({
+      background,
+      manifest: {
+        permissions: ["privacy"],
+      },
+      useAddonManager: "temporary",
+    }),
+
+    ExtensionTestUtils.loadExtension({
+      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.");
+    equal(data.levelOfControl, "controllable_by_this_extension",
+      "get returns expected levelOfControl.");
+
+    testExtensions[0].sendMessage("get", {incognito: true}, setting);
+    data = await testExtensions[0].awaitMessage("gotData");
+    ok(data.value, "get returns expected value with incognito.");
+    equal(data.levelOfControl, "not_controllable",
+      "get returns expected levelOfControl with incognito.");
+
+    // Change the value to false.
+    testExtensions[0].sendMessage("set", {value: false}, setting);
+    data = await testExtensions[0].awaitMessage("afterSet");
+    ok(!data.value, "get returns expected value after setting.");
+    equal(data.levelOfControl, "controlled_by_this_extension",
+      "get returns expected levelOfControl after setting.");
+
+    // Verify the prefs have been set to match the "false" setting.
+    for (let pref in SETTINGS[setting]) {
+      let msg = `${pref} set correctly for ${setting}`;
+      if (pref === "network.http.speculative-parallel-limit") {
+        equal(Preferences.get(pref), 0, msg);
+      } else {
+        equal(Preferences.get(pref), !SETTINGS[setting][pref], msg);
+      }
+    }
+
+    // Change the value with a newer extension.
+    testExtensions[1].sendMessage("set", {value: true}, setting);
+    data = await testExtensions[1].awaitMessage("afterSet");
+    ok(data.value, "get returns expected value after setting via newer extension.");
+    equal(data.levelOfControl, "controlled_by_this_extension",
+      "get returns expected levelOfControl after setting.");
+
+    // Verify the prefs have been set to match the "true" setting.
+    for (let pref in SETTINGS[setting]) {
+      let msg = `${pref} set correctly for ${setting}`;
+      if (pref === "network.http.speculative-parallel-limit") {
+        equal(Preferences.get(pref), ExtensionPreferencesManager.getDefaultValue(pref), msg);
+      } else {
+        equal(Preferences.get(pref), SETTINGS[setting][pref], msg);
+      }
+    }
+
+    // Change the value with an older extension.
+    testExtensions[0].sendMessage("set", {value: false}, setting);
+    data = await testExtensions[0].awaitMessage("afterSet");
+    ok(data.value, "Newer extension remains in control.");
+    equal(data.levelOfControl, "controlled_by_other_extensions",
+      "get returns expected levelOfControl when controlled by other.");
+
+    // Clear the value of the newer extension.
+    testExtensions[1].sendMessage("clear", {}, setting);
+    data = await testExtensions[1].awaitMessage("afterClear");
+    ok(!data.value, "Older extension gains control.");
+    equal(data.levelOfControl, "controllable_by_this_extension",
+      "Expected levelOfControl returned after clearing.");
+
+    testExtensions[0].sendMessage("get", {}, setting);
+    data = await testExtensions[0].awaitMessage("gotData");
+    ok(!data.value, "Current, older extension has control.");
+    equal(data.levelOfControl, "controlled_by_this_extension",
+      "Expected levelOfControl returned after clearing.");
+
+    // Set the value again with the newer extension.
+    testExtensions[1].sendMessage("set", {value: true}, setting);
+    data = await testExtensions[1].awaitMessage("afterSet");
+    ok(data.value, "get returns expected value after setting via newer extension.");
+    equal(data.levelOfControl, "controlled_by_this_extension",
+      "get returns expected levelOfControl after setting.");
+
+    // Unload the newer extension. Expect the older extension to regain control.
+    await testExtensions[1].unload();
+    testExtensions[0].sendMessage("get", {}, setting);
+    data = await testExtensions[0].awaitMessage("gotData");
+    ok(!data.value, "Older extension regained control.");
+    equal(data.levelOfControl, "controlled_by_this_extension",
+      "Expected levelOfControl returned after unloading.");
+
+    // Reload the extension for the next iteration of the loop.
+    testExtensions[1] = ExtensionTestUtils.loadExtension({
+      background,
+      manifest: {
+        permissions: ["privacy"],
+      },
+      useAddonManager: "temporary",
+    });
+    await testExtensions[1].startup();
+
+    // Clear the value of the older extension.
+    testExtensions[0].sendMessage("clear", {}, setting);
+    data = await testExtensions[0].awaitMessage("afterClear");
+    ok(data.value, "Setting returns to original value when all are cleared.");
+    equal(data.levelOfControl, "controllable_by_this_extension",
+      "Expected levelOfControl returned after clearing.");
+
+    // Verify that our initial values were restored.
+    for (let pref in SETTINGS[setting]) {
+      equal(Preferences.get(pref), SETTINGS[setting][pref], `${pref} was reset to its initial value.`);
+    }
+  }
+
+  for (let extension of testExtensions) {
+    await extension.unload();
+  }
+
+  await promiseShutdownManager();
+});
+
+add_task(async function test_privacy_webRTCIPHandlingPolicy() {
+  // Create a object to hold the default values of all the prefs.
+  const PREF_DEFAULTS = {
+    "media.peerconnection.ice.default_address_only": null,
+    "media.peerconnection.ice.no_host": null,
+    "media.peerconnection.ice.proxy_only": null,
+  };
+
+  // Store the default values of each pref.
+  for (let pref in PREF_DEFAULTS) {
+    PREF_DEFAULTS[pref] = ExtensionPreferencesManager.getDefaultValue(pref);
+  }
+
+  do_register_cleanup(() => {
+    // Reset the prefs.
+    for (let pref in PREF_DEFAULTS) {
+      Preferences.reset(pref);
+    }
+  });
+
+  async function background() {
+    browser.test.onMessage.addListener(async (msg, value) => {
+      let rtcData;
+      switch (msg) {
+        case "set":
+          await browser.privacy.network.webRTCIPHandlingPolicy.set({value});
+          rtcData = await browser.privacy.network.webRTCIPHandlingPolicy.get({});
+          browser.test.sendMessage("rtcData", rtcData);
+          break;
+
+        case "clear":
+          await browser.privacy.network.webRTCIPHandlingPolicy.clear({});
+          rtcData = await browser.privacy.network.webRTCIPHandlingPolicy.get({});
+          browser.test.sendMessage("rtcData", rtcData);
+          break;
+
+      }
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["privacy"],
+    },
+    useAddonManager: "temporary",
+  });
+
+  await promiseStartupManager();
+  await extension.startup();
+
+  async function testSetting(value, truePrefs) {
+    extension.sendMessage("set", value);
+    let data = await extension.awaitMessage("rtcData");
+    equal(data.value, value);
+    for (let pref in PREF_DEFAULTS) {
+      let prefValue = Preferences.get(pref);
+      if (truePrefs.includes(pref)) {
+        ok(prefValue, `${pref} set correctly for ${value}`);
+      } else {
+        equal(prefValue, PREF_DEFAULTS[pref], `${pref} contains default value for ${value}`);
+      }
+    }
+  }
+
+  await testSetting(
+    "default_public_and_private_interfaces",
+    ["media.peerconnection.ice.default_address_only"]);
+
+  await testSetting(
+    "default_public_interface_only",
+    ["media.peerconnection.ice.default_address_only", "media.peerconnection.ice.no_host"]);
+
+  await testSetting(
+    "disable_non_proxied_udp",
+    ["media.peerconnection.ice.proxy_only"]);
+
+  await testSetting("default", []);
+
+  await extension.unload();
+
+  await promiseShutdownManager();
+});
+
+add_task(async function test_exceptions() {
+  async function background() {
+    await browser.test.assertRejects(
+      browser.privacy.network.networkPredictionEnabled.set({value: true, scope: "regular_only"}),
+      "Firefox does not support the regular_only settings scope.",
+      "Expected rejection calling set with invalid scope.");
+
+    await browser.test.assertRejects(
+      browser.privacy.network.networkPredictionEnabled.clear({scope: "incognito_persistent"}),
+      "Firefox does not support the incognito_persistent settings scope.",
+      "Expected rejection calling clear with invalid scope.");
+
+    browser.test.notifyPass("exceptionTests");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["privacy"],
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("exceptionTests");
+  await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -43,16 +43,17 @@ skip-if = release_or_beta
 [test_ext_localStorage.js]
 [test_ext_management.js]
 [test_ext_management_uninstall_self.js]
 [test_ext_manifest_content_security_policy.js]
 [test_ext_manifest_incognito.js]
 [test_ext_manifest_minimum_chrome_version.js]
 [test_ext_onmessage_removelistener.js]
 skip-if = true # This test no longer tests what it is meant to test.
+[test_ext_privacy.js]
 [test_ext_runtime_connect_no_receiver.js]
 [test_ext_runtime_getBrowserInfo.js]
 [test_ext_runtime_getPlatformInfo.js]
 [test_ext_runtime_onInstalled_and_onStartup.js]
 [test_ext_runtime_sendMessage.js]
 [test_ext_runtime_sendMessage_errors.js]
 [test_ext_runtime_sendMessage_no_receiver.js]
 [test_ext_runtime_sendMessage_self.js]