Backed out changeset d9b0de6a3abc (bug 1410412) for failing at browser_extension_controlled.js on a CLOSED TREE.
authorGurzau Raul <rgurzau@mozilla.com>
Tue, 19 Nov 2019 01:56:52 +0200
changeset 502513 fdd07df83c87f12725f4b97c80e644fd11673977
parent 502512 610f541e1fd6e34b31df7fecb0bc14a6ece963b9
child 502514 11f48aaae9552c1f8da22578aedc41d5fa07ebbc
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1410412
milestone72.0a1
backs outd9b0de6a3abc950bccbe20bb4eff14baffd5f830
first release with
nightly linux32
fdd07df83c87 / 72.0a1 / 20191119043902 / files
nightly linux64
fdd07df83c87 / 72.0a1 / 20191119043902 / files
nightly mac
fdd07df83c87 / 72.0a1 / 20191119043902 / files
nightly win32
fdd07df83c87 / 72.0a1 / 20191119043902 / files
nightly win64
fdd07df83c87 / 72.0a1 / 20191119043902 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out changeset d9b0de6a3abc (bug 1410412) for failing at browser_extension_controlled.js on a CLOSED TREE.
browser/components/extensions/parent/ext-urlbar.js
toolkit/components/extensions/ExtensionPreferencesManager.jsm
toolkit/components/extensions/parent/ext-browserSettings.js
toolkit/components/extensions/parent/ext-privacy.js
toolkit/components/extensions/parent/ext-proxy.js
toolkit/components/extensions/schemas/types.json
toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js
--- a/browser/components/extensions/parent/ext-urlbar.js
+++ b/browser/components/extensions/parent/ext-urlbar.js
@@ -220,27 +220,27 @@ this.urlbar = class extends ExtensionAPI
               return fire.async(resultPayload).catch(error => {
                 throw context.normalizeError(error);
               });
             });
             return () => provider.setEventListener("resultPicked", null);
           },
         }).api(),
 
-        openViewOnFocus: getSettingsAPI({
-          context,
-          name: "openViewOnFocus",
-          callback: () => UrlbarPrefs.get("openViewOnFocus"),
-        }),
+        openViewOnFocus: getSettingsAPI(
+          context.extension.id,
+          "openViewOnFocus",
+          () => UrlbarPrefs.get("openViewOnFocus")
+        ),
 
-        engagementTelemetry: getSettingsAPI({
-          context,
-          name: "engagementTelemetry",
-          callback: () => UrlbarPrefs.get("eventTelemetry.enabled"),
-        }),
+        engagementTelemetry: getSettingsAPI(
+          context.extension.id,
+          "engagementTelemetry",
+          () => UrlbarPrefs.get("eventTelemetry.enabled")
+        ),
 
         contextualTip: {
           /**
            * Sets the contextual tip's icon,
            * title, button title, and link title.
            *
            * @param {object} details
            *   If null, then the contextual tip will be hidden.
--- a/toolkit/components/extensions/ExtensionPreferencesManager.jsm
+++ b/toolkit/components/extensions/ExtensionPreferencesManager.jsm
@@ -17,18 +17,16 @@
  * prefNames:   An array of strings, each of which is a preference on
  *              which the setting depends.
  * setCallback: A function that returns an object containing properties and
  *              values that correspond to the prefs to be set.
  */
 
 var EXPORTED_SYMBOLS = ["ExtensionPreferencesManager"];
 
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
 const { Management } = ChromeUtils.import(
   "resource://gre/modules/Extension.jsm",
   null
 );
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
@@ -38,44 +36,33 @@ ChromeUtils.defineModuleGetter(
   "ExtensionSettingsStore",
   "resource://gre/modules/ExtensionSettingsStore.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "Preferences",
   "resource://gre/modules/Preferences.jsm"
 );
-ChromeUtils.defineModuleGetter(
-  this,
-  "ExtensionCommon",
-  "resource://gre/modules/ExtensionCommon.jsm"
-);
-
-const { ExtensionUtils } = ChromeUtils.import(
-  "resource://gre/modules/ExtensionUtils.jsm"
-);
-
-const { ExtensionError } = ExtensionUtils;
 
 XPCOMUtils.defineLazyGetter(this, "defaultPreferences", function() {
   return new Preferences({ defaultBranch: true });
 });
 
 /* eslint-disable mozilla/balanced-listeners */
 Management.on("uninstall", (type, { id }) => {
   ExtensionPreferencesManager.removeAll(id);
 });
 
 Management.on("disable", (type, id) => {
-  ExtensionPreferencesManager.disableAll(id);
+  this.ExtensionPreferencesManager.disableAll(id);
 });
 
 Management.on("startup", async (type, extension) => {
   if (extension.startupReason == "ADDON_ENABLE") {
-    ExtensionPreferencesManager.enableAll(extension.id);
+    this.ExtensionPreferencesManager.enableAll(extension.id);
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
 const STORE_TYPE = "prefs";
 
 // Definitions of settings, each of which correspond to a different API.
 let settingsMap = new Map();
@@ -123,44 +110,41 @@ function settingsUpdate(initialValue) {
     }
   }
   return initialValue;
 }
 
 /**
  * Loops through a set of prefs, either setting or resetting them.
  *
- * @param {string} name
- *        The api name of the setting.
  * @param {Object} setting
  *        An object that represents a setting, which will have a setCallback
  *        property. If a onPrefsChanged function is provided it will be called
  *        with item when the preferences change.
  * @param {Object} item
  *        An object that represents an item handed back from the setting store
  *        from which the new pref values can be calculated.
  */
-function setPrefs(name, setting, item) {
+function setPrefs(setting, item) {
   let prefs = item.initialValue || setting.setCallback(item.value);
   let changed = false;
   for (let pref of setting.prefNames) {
     if (prefs[pref] === undefined) {
       if (Preferences.isSet(pref)) {
         changed = true;
         Preferences.reset(pref);
       }
     } else if (Preferences.get(pref) != prefs[pref]) {
       Preferences.set(pref, prefs[pref]);
       changed = true;
     }
   }
   if (changed && typeof setting.onPrefsChanged == "function") {
     setting.onPrefsChanged(item);
   }
-  Management.emit(`extension-setting-changed:${name}`);
 }
 
 /**
  * Commits a change to a setting and conditionally sets preferences.
  *
  * If the change to the setting causes a different extension to gain
  * control of the pref (or removes all extensions with control over the pref)
  * then the prefs should be updated, otherwise they should not be.
@@ -192,17 +176,17 @@ async function processSetting(id, name, 
     if (
       Object.keys(expectedPrefs).some(
         pref =>
           expectedPrefs[pref] && Preferences.get(pref) != expectedPrefs[pref]
       )
     ) {
       return false;
     }
-    setPrefs(name, setting, item);
+    setPrefs(setting, item);
     return true;
   }
   return false;
 }
 
 this.ExtensionPreferencesManager = {
   /**
    * Adds a setting to the settingsMap. This is how an API tells the
@@ -254,17 +238,17 @@ this.ExtensionPreferencesManager = {
       STORE_TYPE,
       name,
       value,
       initialValueCallback.bind(setting),
       name,
       settingsUpdate.bind(setting)
     );
     if (item) {
-      setPrefs(name, setting, item);
+      setPrefs(setting, item);
       return true;
     }
     return false;
   },
 
   /**
    * Indicates that this extension wants to temporarily cede control over the
    * given setting.
@@ -410,102 +394,40 @@ this.ExtensionPreferencesManager = {
     }
     await ExtensionSettingsStore.initialize();
     return ExtensionSettingsStore.getLevelOfControl(id, storeType, name);
   },
 
   /**
    * Returns an API object with get/set/clear used for a setting.
    *
-   * @param {string|object} extensionId or params object
+   * @param {string} extensionId
    * @param {string} name
-   *          The unique id of the setting.
+   *        The unique id of the setting.
    * @param {Function} callback
-   *          The function that retreives the current setting from prefs.
+   *        The function that retreives the current setting from prefs.
    * @param {string} storeType
-   *          The name of the store in ExtensionSettingsStore.
-   *          Defaults to STORE_TYPE.
+   *        The name of the store in ExtensionSettingsStore.
+   *        Defaults to STORE_TYPE.
    * @param {boolean} readOnly
    * @param {Function} validate
-   *          Utility function for any specific validation, such as checking
-   *          for supported platform.  Function should throw an error if necessary.
+   *        Utility function for any specific validation, such as checking
+   *        for supported platform.  Function should throw an error if necessary.
    *
    * @returns {object} API object with get/set/clear methods
    */
   getSettingsAPI(
     extensionId,
     name,
     callback,
     storeType,
     readOnly = false,
     validate = () => {}
   ) {
-    if (arguments.length > 1) {
-      Services.console.logStringMessage(
-        `ExtensionPreferencesManager.getSettingsAPI for ${name} should be updated to use a single paramater object.`
-      );
-    }
-    return ExtensionPreferencesManager._getSettingsAPI(
-      arguments.length === 1
-        ? extensionId
-        : {
-            extensionId,
-            name,
-            storeType,
-            readOnly,
-            validate,
-          }
-    );
-  },
-
-  /**
-   * Returns an API object with get/set/clear used for a setting.
-   *
-   * @param {object} params The params object contains the following:
-   *        {BaseContext} context
-   *        {string} extensionId, optional to support old API
-   *        {string} name
-   *          The unique id of the setting.
-   *        {Function} callback
-   *          The function that retreives the current setting from prefs.
-   *        {string} storeType
-   *          The name of the store in ExtensionSettingsStore.
-   *          Defaults to STORE_TYPE.
-   *        {boolean} readOnly
-   *        {Function} validate
-   *          Utility function for any specific validation, such as checking
-   *          for supported platform.  Function should throw an error if necessary.
-   *
-   * @returns {object} API object with get/set/clear methods
-   */
-  _getSettingsAPI(params) {
-    let {
-      extensionId,
-      context,
-      name,
-      callback,
-      storeType,
-      readOnly = false,
-      onChange,
-      validate = () => {},
-    } = params;
-    if (!extensionId) {
-      extensionId = context.extension.id;
-    }
-
-    const checkScope = details => {
-      let { scope } = details;
-      if (scope && scope !== "regular") {
-        throw new ExtensionError(
-          `Firefox does not support the ${scope} settings scope.`
-        );
-      }
-    };
-
-    let settingsAPI = {
+    return {
       async get(details) {
         validate();
         let levelOfControl = details.incognito
           ? "not_controllable"
           : await ExtensionPreferencesManager.getLevelOfControl(
               extensionId,
               name,
               storeType
@@ -516,61 +438,27 @@ this.ExtensionPreferencesManager = {
             : levelOfControl;
         return {
           levelOfControl,
           value: await callback(),
         };
       },
       set(details) {
         validate();
-        checkScope(details);
         if (!readOnly) {
           return ExtensionPreferencesManager.setSetting(
             extensionId,
             name,
             details.value
           );
         }
         return false;
       },
       clear(details) {
         validate();
-        checkScope(details);
         if (!readOnly) {
           return ExtensionPreferencesManager.removeSetting(extensionId, name);
         }
         return false;
       },
-      onChange,
     };
-    // Any caller using the old call signature will not have passed
-    // context to us.  This should only be experimental addons in the
-    // wild.
-    if (onChange === undefined && context) {
-      // Some settings that are read-only may not have called addSetting, in
-      // which case we have no way to listen on the pref changes.
-      let setting = settingsMap.get(name);
-      if (!setting) {
-        Services.console.logStringMessage(
-          `ExtensionPreferencesManager API ${name} created but addSetting was not called.`
-        );
-        return settingsAPI;
-      }
-
-      settingsAPI.onChange = new ExtensionCommon.EventManager({
-        context,
-        name: `${name}.onChange`,
-        register: fire => {
-          let listener = async () => {
-            fire.async({
-              details: await settingsAPI.get({}),
-            });
-          };
-          Management.on(`extension-setting-changed:${name}`, listener);
-          return () => {
-            Management.off(`extension-setting-changed:${name}`, listener);
-          };
-        },
-      }).api();
-    }
-    return settingsAPI;
   },
 };
--- a/toolkit/components/extensions/parent/ext-browserSettings.js
+++ b/toolkit/components/extensions/parent/ext-browserSettings.js
@@ -73,24 +73,16 @@ ExtensionPreferencesManager.addSetting("
 ExtensionPreferencesManager.addSetting("contextMenuShowEvent", {
   prefNames: ["ui.context_menus.after_mouseup"],
 
   setCallback(value) {
     return { [this.prefNames[0]]: value === "mouseup" };
   },
 });
 
-ExtensionPreferencesManager.addSetting(HOMEPAGE_OVERRIDE_SETTING, {
-  prefNames: [HOMEPAGE_URL_PREF],
-
-  setCallback() {
-    throw new Error("Unable to set read-only setting");
-  },
-});
-
 ExtensionPreferencesManager.addSetting("ftpProtocolEnabled", {
   prefNames: ["network.ftp.enabled"],
 
   setCallback(value) {
     return { [this.prefNames[0]]: value };
   },
 });
 
@@ -164,63 +156,57 @@ ExtensionPreferencesManager.addSetting("
   },
 });
 
 this.browserSettings = class extends ExtensionAPI {
   getAPI(context) {
     let { extension } = context;
     return {
       browserSettings: {
-        allowPopupsForUserEvents: getSettingsAPI({
-          context,
-          name: "allowPopupsForUserEvents",
-          callback() {
+        allowPopupsForUserEvents: getSettingsAPI(
+          extension.id,
+          "allowPopupsForUserEvents",
+          () => {
             return Services.prefs.getCharPref("dom.popup_allowed_events") != "";
-          },
+          }
+        ),
+        cacheEnabled: getSettingsAPI(extension.id, "cacheEnabled", () => {
+          return (
+            Services.prefs.getBoolPref("browser.cache.disk.enable") &&
+            Services.prefs.getBoolPref("browser.cache.memory.enable")
+          );
         }),
-        cacheEnabled: getSettingsAPI({
-          context,
-          name: "cacheEnabled",
-          callback() {
-            return (
-              Services.prefs.getBoolPref("browser.cache.disk.enable") &&
-              Services.prefs.getBoolPref("browser.cache.memory.enable")
-            );
-          },
-        }),
-        closeTabsByDoubleClick: getSettingsAPI({
-          context,
-          name: "closeTabsByDoubleClick",
-          callback() {
+        closeTabsByDoubleClick: getSettingsAPI(
+          extension.id,
+          "closeTabsByDoubleClick",
+          () => {
             return Services.prefs.getBoolPref(
               "browser.tabs.closeTabByDblclick"
             );
           },
-          validate() {
+          undefined,
+          false,
+          () => {
             if (AppConstants.platform == "android") {
               throw new ExtensionError(
                 `android is not a supported platform for the closeTabsByDoubleClick setting.`
               );
             }
-          },
-        }),
+          }
+        ),
         contextMenuShowEvent: Object.assign(
-          getSettingsAPI({
-            context,
-            name: "contextMenuShowEvent",
-            callback() {
-              if (AppConstants.platform === "win") {
-                return "mouseup";
-              }
-              let prefValue = Services.prefs.getBoolPref(
-                "ui.context_menus.after_mouseup",
-                null
-              );
-              return prefValue ? "mouseup" : "mousedown";
-            },
+          getSettingsAPI(extension.id, "contextMenuShowEvent", () => {
+            if (AppConstants.platform === "win") {
+              return "mouseup";
+            }
+            let prefValue = Services.prefs.getBoolPref(
+              "ui.context_menus.after_mouseup",
+              null
+            );
+            return prefValue ? "mouseup" : "mousedown";
           }),
           {
             set: details => {
               if (!["mouseup", "mousedown"].includes(details.value)) {
                 throw new ExtensionError(
                   `${
                     details.value
                   } is not a valid value for contextMenuShowEvent.`
@@ -236,131 +222,104 @@ this.browserSettings = class extends Ext
               return ExtensionPreferencesManager.setSetting(
                 extension.id,
                 "contextMenuShowEvent",
                 details.value
               );
             },
           }
         ),
-        ftpProtocolEnabled: getSettingsAPI({
-          context,
-          name: "ftpProtocolEnabled",
-          callback() {
+        ftpProtocolEnabled: getSettingsAPI(
+          extension.id,
+          "ftpProtocolEnabled",
+          () => {
             return Services.prefs.getBoolPref("network.ftp.enabled");
-          },
-        }),
-        homepageOverride: getSettingsAPI({
-          context,
-          name: HOMEPAGE_OVERRIDE_SETTING,
-          callback() {
+          }
+        ),
+        homepageOverride: getSettingsAPI(
+          extension.id,
+          HOMEPAGE_OVERRIDE_SETTING,
+          () => {
             return Services.prefs.getStringPref(HOMEPAGE_URL_PREF);
           },
-          readOnly: true,
-        }),
-        imageAnimationBehavior: getSettingsAPI({
-          context,
-          name: "imageAnimationBehavior",
-          callback() {
+          undefined,
+          true
+        ),
+        imageAnimationBehavior: getSettingsAPI(
+          extension.id,
+          "imageAnimationBehavior",
+          () => {
             return Services.prefs.getCharPref("image.animation_mode");
-          },
+          }
+        ),
+        newTabPosition: getSettingsAPI(extension.id, "newTabPosition", () => {
+          if (Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")) {
+            return "afterCurrent";
+          }
+          if (
+            Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")
+          ) {
+            return "relatedAfterCurrent";
+          }
+          return "atEnd";
         }),
-        newTabPosition: getSettingsAPI({
-          context,
-          name: "newTabPosition",
-          callback() {
-            if (Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")) {
-              return "afterCurrent";
-            }
-            if (
-              Services.prefs.getBoolPref(
-                "browser.tabs.insertRelatedAfterCurrent"
-              )
-            ) {
-              return "relatedAfterCurrent";
-            }
-            return "atEnd";
-          },
-        }),
-        newTabPageOverride: getSettingsAPI({
-          context,
-          name: NEW_TAB_OVERRIDE_SETTING,
-          callback() {
+        newTabPageOverride: getSettingsAPI(
+          extension.id,
+          NEW_TAB_OVERRIDE_SETTING,
+          () => {
             return aboutNewTabService.newTabURL;
           },
-          storeType: URL_STORE_TYPE,
-          readOnly: true,
-          onChange: new ExtensionCommon.EventManager({
-            context,
-            name: `${NEW_TAB_OVERRIDE_SETTING}.onChange`,
-            register: fire => {
-              let listener = (text, id) => {
-                fire.async({
-                  details: {
-                    levelOfControl: "not_controllable",
-                    value: aboutNewTabService.newTabURL,
-                  },
-                });
-              };
-              Services.obs.addObserver(listener, "newtab-url-changed");
-              return () => {
-                Services.obs.removeObserver(listener, "newtab-url-changed");
-              };
-            },
-          }).api(),
-        }),
-        openBookmarksInNewTabs: getSettingsAPI({
-          context,
-          name: "openBookmarksInNewTabs",
-          callback() {
+          URL_STORE_TYPE,
+          true
+        ),
+        openBookmarksInNewTabs: getSettingsAPI(
+          extension.id,
+          "openBookmarksInNewTabs",
+          () => {
             return Services.prefs.getBoolPref(
               "browser.tabs.loadBookmarksInTabs"
             );
-          },
-        }),
-        openSearchResultsInNewTabs: getSettingsAPI({
-          context,
-          name: "openSearchResultsInNewTabs",
-          callback() {
+          }
+        ),
+        openSearchResultsInNewTabs: getSettingsAPI(
+          extension.id,
+          "openSearchResultsInNewTabs",
+          () => {
             return Services.prefs.getBoolPref("browser.search.openintab");
-          },
-        }),
-        openUrlbarResultsInNewTabs: getSettingsAPI({
-          context,
-          name: "openUrlbarResultsInNewTabs",
-          callback() {
+          }
+        ),
+        openUrlbarResultsInNewTabs: getSettingsAPI(
+          extension.id,
+          "openUrlbarResultsInNewTabs",
+          () => {
             return Services.prefs.getBoolPref("browser.urlbar.openintab");
-          },
-        }),
-        webNotificationsDisabled: getSettingsAPI({
-          context,
-          name: "webNotificationsDisabled",
-          callback() {
+          }
+        ),
+        webNotificationsDisabled: getSettingsAPI(
+          extension.id,
+          "webNotificationsDisabled",
+          () => {
             let prefValue = Services.prefs.getIntPref(
               "permissions.default.desktop-notification",
               null
             );
             return prefValue === PERM_DENY_ACTION;
-          },
-        }),
+          }
+        ),
         overrideDocumentColors: Object.assign(
-          getSettingsAPI({
-            context,
-            name: "overrideDocumentColors",
-            callback() {
-              let prefValue = Services.prefs.getIntPref(
-                "browser.display.document_color_use"
-              );
-              if (prefValue === 1) {
-                return "never";
-              } else if (prefValue === 2) {
-                return "always";
-              }
-              return "high-contrast-only";
-            },
+          getSettingsAPI(extension.id, "overrideDocumentColors", () => {
+            let prefValue = Services.prefs.getIntPref(
+              "browser.display.document_color_use"
+            );
+            if (prefValue === 1) {
+              return "never";
+            } else if (prefValue === 2) {
+              return "always";
+            }
+            return "high-contrast-only";
           }),
           {
             set: details => {
               if (
                 !["never", "always", "high-contrast-only"].includes(
                   details.value
                 )
               ) {
@@ -380,26 +339,22 @@ this.browserSettings = class extends Ext
                 extension.id,
                 "overrideDocumentColors",
                 prefValue
               );
             },
           }
         ),
         useDocumentFonts: Object.assign(
-          getSettingsAPI({
-            context,
-            name: "useDocumentFonts",
-            callback() {
-              return (
-                Services.prefs.getIntPref(
-                  "browser.display.use_document_fonts"
-                ) !== 0
-              );
-            },
+          getSettingsAPI(extension.id, "useDocumentFonts", () => {
+            return (
+              Services.prefs.getIntPref(
+                "browser.display.use_document_fonts"
+              ) !== 0
+            );
           }),
           {
             set: details => {
               if (typeof details.value !== "boolean") {
                 throw new ExtensionError(
                   `${details.value} is not a valid value for useDocumentFonts.`
                 );
               }
--- a/toolkit/components/extensions/parent/ext-privacy.js
+++ b/toolkit/components/extensions/parent/ext-privacy.js
@@ -10,28 +10,65 @@ ChromeUtils.defineModuleGetter(
   this,
   "Preferences",
   "resource://gre/modules/Preferences.jsm"
 );
 
 var { ExtensionPreferencesManager } = ChromeUtils.import(
   "resource://gre/modules/ExtensionPreferencesManager.jsm"
 );
-const { getSettingsAPI } = ExtensionPreferencesManager;
+
+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],
   ["reject_trackers", cookieSvc.BEHAVIOR_REJECT_TRACKER],
 ]);
 
+const checkScope = scope => {
+  if (scope && scope !== "regular") {
+    throw new ExtensionError(
+      `Firefox does not support the ${scope} settings scope.`
+    );
+  }
+};
+
+const getPrivacyAPI = (extension, name, callback) => {
+  return {
+    async get(details) {
+      return {
+        levelOfControl: details.incognito
+          ? "not_controllable"
+          : await ExtensionPreferencesManager.getLevelOfControl(
+              extension.id,
+              name
+            ),
+        value: await callback(),
+      };
+    },
+    set(details) {
+      checkScope(details.scope);
+      return ExtensionPreferencesManager.setSetting(
+        extension.id,
+        name,
+        details.value
+      );
+    },
+    clear(details) {
+      checkScope(details.scope);
+      return ExtensionPreferencesManager.removeSetting(extension.id, 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",
   ],
@@ -175,43 +212,44 @@ ExtensionPreferencesManager.addSetting("
     }
 
     return prefs;
   },
 });
 
 this.privacy = class extends ExtensionAPI {
   getAPI(context) {
+    let { extension } = context;
     return {
       privacy: {
         network: {
-          networkPredictionEnabled: getSettingsAPI({
-            context,
-            name: "network.networkPredictionEnabled",
-            callback() {
+          networkPredictionEnabled: getPrivacyAPI(
+            extension,
+            "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")
               );
-            },
-          }),
-          peerConnectionEnabled: getSettingsAPI({
-            context,
-            name: "network.peerConnectionEnabled",
-            callback() {
+            }
+          ),
+          peerConnectionEnabled: getPrivacyAPI(
+            extension,
+            "network.peerConnectionEnabled",
+            () => {
               return Preferences.get("media.peerconnection.enabled");
-            },
-          }),
-          webRTCIPHandlingPolicy: getSettingsAPI({
-            context,
-            name: "network.webRTCIPHandlingPolicy",
-            callback() {
+            }
+          ),
+          webRTCIPHandlingPolicy: getPrivacyAPI(
+            extension,
+            "network.webRTCIPHandlingPolicy",
+            () => {
               if (Preferences.get("media.peerconnection.ice.proxy_only")) {
                 return "proxy_only";
               }
 
               let default_address_only = Preferences.get(
                 "media.peerconnection.ice.default_address_only"
               );
               if (default_address_only) {
@@ -227,85 +265,85 @@ this.privacy = class extends ExtensionAP
                     return "disable_non_proxied_udp";
                   }
                   return "default_public_interface_only";
                 }
                 return "default_public_and_private_interfaces";
               }
 
               return "default";
-            },
-          }),
+            }
+          ),
         },
 
         services: {
-          passwordSavingEnabled: getSettingsAPI({
-            context,
-            name: "services.passwordSavingEnabled",
-            callback() {
+          passwordSavingEnabled: getPrivacyAPI(
+            extension,
+            "services.passwordSavingEnabled",
+            () => {
               return Preferences.get("signon.rememberSignons");
-            },
-          }),
+            }
+          ),
         },
 
         websites: {
-          cookieConfig: getSettingsAPI({
-            context,
-            name: "websites.cookieConfig",
-            callback() {
+          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: getSettingsAPI({
-            context,
-            name: "websites.firstPartyIsolate",
-            callback() {
+            }
+          ),
+          firstPartyIsolate: getPrivacyAPI(
+            extension,
+            "websites.firstPartyIsolate",
+            () => {
               return Preferences.get("privacy.firstparty.isolate");
-            },
-          }),
-          hyperlinkAuditingEnabled: getSettingsAPI({
-            context,
-            name: "websites.hyperlinkAuditingEnabled",
-            callback() {
+            }
+          ),
+          hyperlinkAuditingEnabled: getPrivacyAPI(
+            extension,
+            "websites.hyperlinkAuditingEnabled",
+            () => {
               return Preferences.get("browser.send_pings");
-            },
-          }),
-          referrersEnabled: getSettingsAPI({
-            context,
-            name: "websites.referrersEnabled",
-            callback() {
+            }
+          ),
+          referrersEnabled: getPrivacyAPI(
+            extension,
+            "websites.referrersEnabled",
+            () => {
               return Preferences.get("network.http.sendRefererHeader") !== 0;
-            },
-          }),
-          resistFingerprinting: getSettingsAPI({
-            context,
-            name: "websites.resistFingerprinting",
-            callback() {
+            }
+          ),
+          resistFingerprinting: getPrivacyAPI(
+            extension,
+            "websites.resistFingerprinting",
+            () => {
               return Preferences.get("privacy.resistFingerprinting");
-            },
-          }),
-          trackingProtectionMode: getSettingsAPI({
-            context,
-            name: "websites.trackingProtectionMode",
-            callback() {
+            }
+          ),
+          trackingProtectionMode: getPrivacyAPI(
+            extension,
+            "websites.trackingProtectionMode",
+            () => {
               if (Preferences.get("privacy.trackingprotection.enabled")) {
                 return "always";
               } else if (
                 Preferences.get("privacy.trackingprotection.pbmode.enabled")
               ) {
                 return "private_browsing";
               }
               return "never";
-            },
-          }),
+            }
+          ),
         },
       },
     };
   }
 };
--- a/toolkit/components/extensions/parent/ext-proxy.js
+++ b/toolkit/components/extensions/parent/ext-proxy.js
@@ -168,20 +168,20 @@ this.proxy = class extends ExtensionAPI 
             extension.on("proxy-error", listener);
             return () => {
               extension.off("proxy-error", listener);
             };
           },
         }).api(),
 
         settings: Object.assign(
-          getSettingsAPI({
-            context,
-            name: "proxy.settings",
-            callback() {
+          getSettingsAPI(
+            extension.id,
+            "proxy.settings",
+            () => {
               let prefValue = Services.prefs.getIntPref("network.proxy.type");
               let proxyConfig = {
                 proxyType: Array.from(PROXY_TYPES_MAP.entries()).find(
                   entry => entry[1] === prefValue
                 )[0],
                 autoConfigUrl: Services.prefs.getCharPref(
                   "network.proxy.autoconfig_url"
                 ),
@@ -209,24 +209,26 @@ this.proxy = class extends ExtensionAPI 
                   `network.proxy.${prop}_port`
                 );
                 proxyConfig[prop] = port ? `${host}:${port}` : host;
               }
 
               return proxyConfig;
             },
             // proxy.settings is unsupported on android.
-            validate() {
+            undefined,
+            false,
+            () => {
               if (AppConstants.platform == "android") {
                 throw new ExtensionError(
                   `proxy.settings is not supported on android.`
                 );
               }
-            },
-          }),
+            }
+          ),
           {
             set: details => {
               if (AppConstants.platform === "android") {
                 throw new ExtensionError(
                   "proxy.settings is not supported on android."
                 );
               }
 
--- a/toolkit/components/extensions/schemas/types.json
+++ b/toolkit/components/extensions/schemas/types.json
@@ -128,16 +128,17 @@
             ]
           }
         ],
         "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"
--- a/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js
@@ -35,30 +35,18 @@ add_task(async function test_browser_set
     "browser.search.openintab": false,
     "browser.tabs.insertRelatedAfterCurrent": true,
     "browser.tabs.insertAfterCurrent": false,
     "browser.display.document_color_use": 1,
     "browser.display.use_document_fonts": 1,
   };
 
   async function background() {
-    let listeners = new Set([]);
     browser.test.onMessage.addListener(async (msg, apiName, value) => {
       let apiObj = browser.browserSettings[apiName];
-      // Don't add more than one listner per apiName.  We leave the
-      // listener to ensure we do not get more calls than we expect.
-      if (!listeners.has(apiName)) {
-        apiObj.onChange.addListener(details => {
-          browser.test.sendMessage("onChange", {
-            details: details.details,
-            setting: apiName,
-          });
-        });
-        listeners.add(apiName);
-      }
       let result = await apiObj.set({ value });
       if (msg === "set") {
         browser.test.assertTrue(result, "set returns true.");
         browser.test.sendMessage("settingData", await apiObj.get({}));
       } else {
         browser.test.assertFalse(result, "set returns false for a no-op.");
         browser.test.sendMessage("no-op set");
       }
@@ -86,23 +74,16 @@ add_task(async function test_browser_set
   });
 
   await promiseStartupManager();
   await extension.startup();
 
   async function testSetting(setting, value, expected, expectedValue = value) {
     extension.sendMessage("set", setting, value);
     let data = await extension.awaitMessage("settingData");
-    let dataChange = await extension.awaitMessage("onChange");
-    equal(setting, dataChange.setting, "onChange fired");
-    equal(
-      data.value,
-      dataChange.details.value,
-      "onChange fired with correct value"
-    );
     deepEqual(
       data.value,
       expectedValue,
       `The ${setting} setting has the expected value.`
     );
     equal(
       data.levelOfControl,
       "controlled_by_this_extension",
@@ -188,22 +169,22 @@ add_task(async function test_browser_set
     await testSetting("closeTabsByDoubleClick", true, {
       "browser.tabs.closeTabByDblclick": true,
     });
     await testSetting("closeTabsByDoubleClick", false, {
       "browser.tabs.closeTabByDblclick": false,
     });
   }
 
+  await testSetting("ftpProtocolEnabled", true, {
+    "network.ftp.enabled": true,
+  });
   await testSetting("ftpProtocolEnabled", false, {
     "network.ftp.enabled": false,
   });
-  await testSetting("ftpProtocolEnabled", true, {
-    "network.ftp.enabled": true,
-  });
 
   await testSetting("newTabPosition", "afterCurrent", {
     "browser.tabs.insertRelatedAfterCurrent": false,
     "browser.tabs.insertAfterCurrent": true,
   });
   await testSetting("newTabPosition", "atEnd", {
     "browser.tabs.insertRelatedAfterCurrent": false,
     "browser.tabs.insertAfterCurrent": false,