Bug 1320736 - Part 2: Create ExtensionPreferencesManager module, r?aswan draft
authorBob Silverberg <bsilverberg@mozilla.com>
Mon, 16 Jan 2017 17:30:47 -0500
changeset 463329 6c54783dbe5ac6d7843933cf033d8a28fe9040b9
parent 463093 96f1de78fe3d44a1ad34345f53122a7fd786a15e
child 542641 28a02f469ad2d425a80f166d04b08e744aa2ed97
push id42026
push userbmo:bob.silverberg@gmail.com
push dateWed, 18 Jan 2017 21:21:27 +0000
reviewersaswan
bugs1320736
milestone53.0a1
Bug 1320736 - Part 2: Create ExtensionPreferencesManager module, r?aswan MozReview-Commit-ID: BiY8XikUSUV
toolkit/components/extensions/ExtensionPreferencesManager.jsm
toolkit/components/extensions/moz.build
toolkit/components/extensions/test/xpcshell/test_ext_extensionPreferencesManager.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionPreferencesManager.jsm
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * @fileOverview
+ * This module is used for managing preferences from WebExtension APIs.
+ * It takes care of the precedence chain and decides whether a preference
+ * needs to be updated when a change is requested by an API.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ExtensionPreferencesManager"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
+                                  "resource://gre/modules/ExtensionSettingsStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+
+function defualtInitialValueCallback(prefName) {
+  return Promise.resolve(Preferences.get(prefName));
+}
+
+function canSetValueIntoPref(value) {
+  if (typeof value == "undefined" || value == null) {
+    return false;
+  }
+
+  let prefType = value.constructor.name;
+  if (!["String", "Number", "Boolean"].includes(prefType)) {
+    return false;
+  }
+
+  return true;
+}
+
+this.ExtensionPreferencesManager = {
+  /**
+   * Indicates that a preference should be set.
+   *
+   * @param {string} extensionId An id that uniquely identifies an extension.
+   * @param {string} prefName The name of the preference to set.
+   * @param {string} value The value to be stored in the preference.
+   * @param {function} initialValueCallback An async function to be called to
+   * determine the initial value for the preference. This must return a promise
+   * which resolves to the initial value of the preference, and will be passed
+   * the prefName argument. It defaults to defualtInitialValueCallback, which
+   * simply calls Preferences.get() using the prefName.
+   *
+   * @returns {Promise} Rejects if the value is not valid for a preference.
+   * Resolves to true if the preference was changed and to false if the
+   * preference was not changed.
+   */
+  async setPref(extensionId, prefName, value, initialValueCallback = defualtInitialValueCallback) {
+    if (!canSetValueIntoPref(value)) {
+      return Promise.reject(`Cannot set value with type ${typeof value} into a preference.`);
+    }
+    let item = await ExtensionSettingsStore.addSetting(
+      extensionId, "pref", prefName, value, initialValueCallback);
+    if (item) {
+      Preferences.set(prefName, item.value);
+      return Promise.resolve(true);
+    }
+    return Promise.resolve(false);
+  },
+
+  /**
+   * Indicates that a preference is being unset.
+   *
+   * @param {string} extensionId An id that uniquely identifies an extension.
+   * @param {string} prefName The name of the preference to unset.
+   */
+  async unsetPref(extensionId, prefName) {
+    let item = await ExtensionSettingsStore.removeSetting(
+      extensionId, "pref", prefName);
+    if (item) {
+      if (canSetValueIntoPref(item.value)) {
+        Preferences.set(prefName, item.value);
+      } else {
+        Preferences.reset(prefName);
+      }
+    }
+  },
+
+  /**
+   * Unsets all previously set prefs for an extension. This can be called when
+   * an extension is being uninstalled or disabled, for example.
+   *
+   * @param {string} extensionId An id that uniquely identifies an extension.
+   */
+  async unsetAll(extensionId) {
+    let prefs = await ExtensionSettingsStore.getAllForExtension(extensionId, "pref");
+    for (let pref of prefs) {
+      await this.unsetPref(extensionId, pref);
+    }
+  },
+};
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -7,16 +7,17 @@
 EXTRA_JS_MODULES += [
     'Extension.jsm',
     'ExtensionAPI.jsm',
     'ExtensionChild.jsm',
     'ExtensionCommon.jsm',
     'ExtensionContent.jsm',
     'ExtensionManagement.jsm',
     'ExtensionParent.jsm',
+    'ExtensionPreferencesManager.jsm',
     'ExtensionSettingsStore.jsm',
     'ExtensionStorage.jsm',
     'ExtensionStorageSync.jsm',
     'ExtensionUtils.jsm',
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
     'NativeMessaging.jsm',
     'Schemas.jsm',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionPreferencesManager.js
@@ -0,0 +1,94 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPreferencesManager",
+                                  "resource://gre/modules/ExtensionPreferencesManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
+                                  "resource://gre/modules/ExtensionSettingsStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+
+const TEST_PREF = "extensions.webextensions.base-content-security-policy";
+
+function initialValue(key) {
+  return `key:${key}`;
+}
+
+function initialValueCallback(key) {
+  let initial = initialValue(key);
+  return Promise.resolve(initial);
+}
+
+add_task(async function test_existing_pref() {
+  let initValue = Preferences.get(TEST_PREF);
+  let newValue1 = `${initValue}-1`;
+  let prefSet = await ExtensionPreferencesManager.setPref(0, TEST_PREF, newValue1);
+  ok(prefSet, "setPref returns true when the pref has been set.");
+  equal(Preferences.get(TEST_PREF), newValue1,
+        "setPref sets the pref for the first extension.");
+  let newValue2 = `${initValue}-2`;
+  prefSet = await ExtensionPreferencesManager.setPref(1, TEST_PREF, newValue2);
+  ok(!prefSet, "setPref returns false when the pref has not been set.");
+  equal(Preferences.get(TEST_PREF), newValue1,
+        "setPref does not set the pref for the second extension.");
+  await ExtensionPreferencesManager.unsetPref(1, TEST_PREF);
+  equal(Preferences.get(TEST_PREF), newValue1,
+        "unsetPref does not set the pref for the non-top extension.");
+  await ExtensionPreferencesManager.setPref(1, TEST_PREF, newValue2);
+  equal(Preferences.get(TEST_PREF), newValue1,
+        "setPref does not set the pref for the second extension.");
+  await ExtensionPreferencesManager.unsetPref(0, TEST_PREF);
+  equal(Preferences.get(TEST_PREF), newValue2,
+        "unsetPref sets the pref to the next value when removing the top extension.");
+  await ExtensionPreferencesManager.unsetPref(1, TEST_PREF);
+  equal(Preferences.get(TEST_PREF), initValue,
+        "unsetPref sets the pref to the initial value when removing the last extension.");
+});
+
+add_task(async function test_initial_value_callback() {
+  let prefName = "myNewPref.1";
+  let newValue = "value1";
+  await ExtensionPreferencesManager.setPref(0, prefName, newValue, initialValueCallback);
+  equal(Preferences.get(prefName), newValue,
+        "setPref sets the pref for the first extension.");
+  await ExtensionPreferencesManager.unsetPref(0, prefName);
+  equal(Preferences.get(prefName), initialValue(prefName),
+        "unsetPref sets the pref to the initial value when removing the last extension.");
+
+  function callback() {
+    return Promise.resolve([]);
+  }
+  prefName = "myNewPref.2";
+  newValue = "value2";
+  await ExtensionPreferencesManager.setPref(0, prefName, newValue, callback);
+  equal(Preferences.get(prefName), newValue,
+        "setPref sets the pref for the first extension.");
+  await ExtensionPreferencesManager.unsetPref(0, prefName);
+  equal(Preferences.get(prefName), undefined,
+        "unsetPref resets the pref if the initial value is an invalid type.");
+});
+
+add_task(async function test_unset_all() {
+  const PREFS_TO_SET = ["pref1", "pref2", "pref3"];
+  for (let pref of PREFS_TO_SET) {
+    await ExtensionPreferencesManager.setPref(0, pref, 1);
+    equal(Preferences.get(pref), 1,
+          "setPref set the pref.");
+  }
+  let setPrefs = await ExtensionSettingsStore.getAllForExtension(0, "pref");
+  deepEqual(setPrefs, PREFS_TO_SET, "Expected prefs were set for extension.");
+  await ExtensionPreferencesManager.unsetAll(0);
+  for (let pref of PREFS_TO_SET) {
+    equal(Preferences.get(pref), undefined, "unsetAll unset the pref.");
+  }
+  setPrefs = await ExtensionSettingsStore.getAllForExtension(0, "pref");
+  deepEqual(setPrefs, [], "unsetAll removed all settings.");
+});
+
+add_task(async function test_exceptions() {
+  await Assert.rejects(
+    ExtensionPreferencesManager.setPref(0, "myPref", []),
+    /Cannot set value with type object into a preference/,
+    "setPref rejects with a, invalid value type.");
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -32,16 +32,17 @@ skip-if = os == "android" # Containers a
 skip-if = os == "android"
 [test_ext_downloads_misc.js]
 skip-if = os == "android" || (os=='linux' && bits==32) # linux32: bug 1324870
 [test_ext_downloads_search.js]
 skip-if = os == "android"
 [test_ext_experiments.js]
 skip-if = release_or_beta
 [test_ext_extension.js]
+[test_ext_extensionPreferencesManager.js]
 [test_ext_extensionSettingsStore.js]
 [test_ext_idle.js]
 [test_ext_json_parser.js]
 [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]