Bug 1320736 - Part 3: Create ExtensionPreferencesManager module, r?aswan
MozReview-Commit-ID: BiY8XikUSUV
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]