Bug 1320736 - Part 3: Create ExtensionPreferencesManager module, r?aswan draft
authorBob Silverberg <bsilverberg@mozilla.com>
Mon, 16 Jan 2017 17:30:47 -0500
changeset 479422 d60d1b0804e5887f4476b4fb0a6d4efbcf83106e
parent 479421 cf10e49d9ecec034500df99f37bc880fce560b9d
child 544678 07647f969d41d2c5b8c25971f39d7b81e84e42dd
push id44248
push userbmo:bob.silverberg@gmail.com
push dateMon, 06 Feb 2017 16:53:21 +0000
reviewersaswan
bugs1320736
milestone54.0a1
Bug 1320736 - Part 3: 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 {Extension} extension The extension for which a preference is being added.
+   * @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(extension, prefName, value, initialValueCallback = defualtInitialValueCallback) {
+    if (!canSetValueIntoPref(value)) {
+      throw new Error(`Cannot set value with type ${typeof value} into a preference.`);
+    }
+    let item = await ExtensionSettingsStore.addSetting(
+      extension, "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 {Extension} extension The extension for which a preference is being unset.
+   * @param {string} prefName The name of the preference to unset.
+   */
+  async unsetPref(extension, prefName) {
+    let item = await ExtensionSettingsStore.removeSetting(
+      extension, "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 {Extension} extension The extension for which all preferences are being unset.
+   */
+  async unsetAll(extension) {
+    let prefs = await ExtensionSettingsStore.getAllForExtension(extension, "pref");
+    for (let pref of prefs) {
+      await this.unsetPref(extension, 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',
     'ExtensionTabs.jsm',
     'ExtensionUtils.jsm',
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
     'NativeMessaging.jsm',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionPreferencesManager.js
@@ -0,0 +1,139 @@
+/* -*- 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, "ExtensionSettingsStore",
+                                  "resource://gre/modules/ExtensionSettingsStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+
+const TEST_PREF = "extensions.webextensions.base-content-security-policy";
+const {
+  createAppInfo,
+  promiseShutdownManager,
+  promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+// Allow for unsigned addons.
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+function initialValue(key) {
+  return `key:${key}`;
+}
+
+function initialValueCallback(key) {
+  let initial = initialValue(key);
+  return Promise.resolve(initial);
+}
+
+add_task(async function test_existing_pref() {
+  // Create an array of test framework extension wrappers to install.
+  let testExtensions = [
+    ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      manifest: {},
+    }),
+    ExtensionTestUtils.loadExtension({
+      useAddonManager: "temporary",
+      manifest: {},
+    }),
+  ];
+
+  await promiseStartupManager();
+
+  for (let extension of testExtensions) {
+    await extension.startup();
+  }
+
+  // Create an array actual Extension objects which correspond to the
+  // test framework extension wrappers.
+  let extensions = testExtensions.map(extension => extension.extension._extension);
+
+  // Tests for setting and unsetting individual prefs.
+  let initValue = Preferences.get(TEST_PREF);
+  let newValue1 = `${initValue}-1`;
+  let prefSet = await ExtensionPreferencesManager.setPref(extensions[1], 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(extensions[0], 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(extensions[0], TEST_PREF);
+  equal(Preferences.get(TEST_PREF), newValue1,
+        "unsetPref does not set the pref for the non-top extension.");
+  await ExtensionPreferencesManager.setPref(extensions[0], TEST_PREF, newValue2);
+  equal(Preferences.get(TEST_PREF), newValue1,
+        "setPref does not set the pref for the second extension.");
+  await ExtensionPreferencesManager.unsetPref(extensions[1], TEST_PREF);
+  equal(Preferences.get(TEST_PREF), newValue2,
+        "unsetPref sets the pref to the next value when removing the top extension.");
+  await ExtensionPreferencesManager.unsetPref(extensions[0], TEST_PREF);
+  equal(Preferences.get(TEST_PREF), initValue,
+        "unsetPref sets the pref to the initial value when removing the last extension.");
+
+  // Tests for initialValueCallback.
+  let prefName = "myNewPref.1";
+  let newValue = "value1";
+  await ExtensionPreferencesManager.setPref(extensions[0], prefName, newValue, initialValueCallback);
+  equal(Preferences.get(prefName), newValue,
+        "setPref sets the pref for the first extension.");
+  await ExtensionPreferencesManager.unsetPref(extensions[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(extensions[0], prefName, newValue, callback);
+  equal(Preferences.get(prefName), newValue,
+        "setPref sets the pref for the first extension.");
+  await ExtensionPreferencesManager.unsetPref(extensions[0], prefName);
+  equal(Preferences.get(prefName), undefined,
+        "unsetPref resets the pref if the initial value is an invalid type.");
+
+  // Tests for unsetAll.
+  const PREFS_TO_SET = ["pref1", "pref2", "pref3"];
+  for (let pref of PREFS_TO_SET) {
+    await ExtensionPreferencesManager.setPref(extensions[0], pref, 1);
+    equal(Preferences.get(pref), 1,
+          "setPref set the pref.");
+  }
+  let setPrefs = await ExtensionSettingsStore.getAllForExtension(extensions[0], "pref");
+  deepEqual(setPrefs, PREFS_TO_SET, "Expected prefs were set for extension.");
+  await ExtensionPreferencesManager.unsetAll(extensions[0]);
+  for (let pref of PREFS_TO_SET) {
+    equal(Preferences.get(pref), undefined, "unsetAll unset the pref.");
+  }
+  setPrefs = await ExtensionSettingsStore.getAllForExtension(extensions[0], "pref");
+  deepEqual(setPrefs, [], "unsetAll removed all settings.");
+
+  for (let extension of testExtensions) {
+    await extension.unload();
+  }
+
+  await promiseShutdownManager();
+});
+
+add_task(async function test_exceptions() {
+  await Assert.rejects(
+    ExtensionPreferencesManager.setPref(null, "myPref", []),
+    /Cannot set value with type object into a preference/,
+    "setPref rejects with an invalid value type.");
+  await Assert.rejects(
+    ExtensionPreferencesManager.setPref(null, "myPref", undefined),
+    /Cannot set value with type undefined into a preference/,
+    "setPref rejects with a undefined as a value.");
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -31,16 +31,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]