Backed out changeset 4d11ccc12529 (bug 1410412) on mixedpuppy's request
authorBogdan Tara <btara@mozilla.com>
Tue, 19 Nov 2019 06:01:46 +0200
changeset 502532 fbf7d8f9ba38924f9a71fd5ffed8441ab97bcbf0
parent 502531 9a50611200cfb50a2cbca9f063a364fb7d20fd4c
child 502533 195a6104b5fdd7b3f9d04d1ce8e0e86bdbb29869
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 out4d11ccc1252922ab6ec9090468e844ab5fe96ee7
Backed out changeset 4d11ccc12529 (bug 1410412) on mixedpuppy's request
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"
 );
-var { 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,