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,184 @@
+/* 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.
+ *
+ * It deals with preferences via settings objects, which are objects with
+ * the following properties:
+ *
+ * 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.
+ */
+
+"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");
+
+XPCOMUtils.defineLazyGetter(this, "defaultPreferences", function() {
+ return new Preferences({defaultBranch: true});
+});
+
+const STORE_TYPE = "prefs";
+
+// Definitions of settings, each of which correspond to a different API.
+let settingsMap = new Map();
+
+/**
+ * This function is passed into the ExtensionSettingsStore to determine the
+ * initial value of the setting. It reads an array of preference names from
+ * the this scope, which gets bound to a settings object.
+ *
+ * @returns {Object}
+ * An object with one property per preference, which holds the current
+ * value of that preference.
+ */
+function initialValueCallback() {
+ let initialValue = {};
+ for (let pref of this.prefNames) {
+ initialValue[pref] = Preferences.get(pref);
+ }
+ return initialValue;
+}
+
+/**
+ * Takes an object of preferenceName:value pairs and either sets or resets the
+ * preference to the value.
+ *
+ * @param {Object} prefsObject
+ * An object with one property per preference, which holds the value to
+ * store in that preference. If the value is undefined then the
+ * preference is reset.
+ */
+function setPrefs(prefsObject) {
+ for (let pref in prefsObject) {
+ if (prefsObject[pref] === undefined) {
+ Preferences.reset(pref);
+ } else {
+ Preferences.set(pref, prefsObject[pref]);
+ }
+ }
+}
+
+this.ExtensionPreferencesManager = {
+ /**
+ * Adds a setting to the settingsMap. This is how an API tells the
+ * preferences manager what its setting object is. The preferences
+ * manager needs to know this when settings need to be removed
+ * automatically.
+ *
+ * @param {string} name The unique id of the setting.
+ * @param {Object} setting
+ * A setting object that should have properties for
+ * prefNames, getCallback and setCallback.
+ */
+ addSetting(name, setting) {
+ settingsMap.set(name, setting);
+ },
+
+ /**
+ * Gets the default value for a preference.
+ *
+ * @param {string} prefName The name of the preference.
+ *
+ * @returns {string|number|boolean} The default value of the preference.
+ */
+ getDefaultValue(prefName) {
+ return defaultPreferences.get(prefName);
+ },
+
+ /**
+ * Indicates that an extension would like to change the value of a previously
+ * defined setting.
+ *
+ * @param {Extension} extension
+ * The extension for which a setting is being set.
+ * @param {string} name
+ * The unique id of the setting.
+ * @param {any} value
+ * The value to be stored in the settings store for this
+ * group of preferences.
+ *
+ * @returns {Promise}
+ * Resolves to true if the preferences were changed and to false if
+ * the preferences were not changed.
+ */
+ async setSetting(extension, name, value) {
+ let setting = settingsMap.get(name);
+ let item = await ExtensionSettingsStore.addSetting(
+ extension, STORE_TYPE, name, value, initialValueCallback.bind(setting));
+ if (item) {
+ let prefs = await setting.setCallback(item.value);
+ setPrefs(prefs);
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Indicates that this extension no longer wants to set the given preference.
+ *
+ * @param {Extension} extension
+ * The extension for which a preference setting is being removed.
+ * @param {string} name
+ * The unique id of the setting.
+ */
+ async unsetSetting(extension, name) {
+ let item = await ExtensionSettingsStore.removeSetting(
+ extension, STORE_TYPE, name);
+ if (item) {
+ let prefs = item.initialValue || await settingsMap.get(name).setCallback(item.value);
+ setPrefs(prefs);
+ }
+ },
+
+ /**
+ * Unsets all previously set settings 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 settings are being unset.
+ */
+ async unsetAll(extension) {
+ let settings = await ExtensionSettingsStore.getAllForExtension(extension, STORE_TYPE);
+ for (let name of settings) {
+ await this.unsetSetting(extension, name);
+ }
+ },
+
+ /**
+ * Return the levelOfControl for a setting / extension combo.
+ * This queries the levelOfControl from the ExtensionSettingsStore and also
+ * takes into account whether any of the setting's preferences are locked.
+ *
+ * @param {Extension} extension
+ * The extension for which levelOfControl is being requested.
+ * @param {string} name
+ * The unique id of the setting.
+ *
+ * @returns {Promise}
+ * Resolves to the level of control of the extension over the setting.
+ */
+ async getLevelOfControl(extension, name) {
+ for (let prefName of settingsMap.get(name).prefNames) {
+ if (Preferences.locked(prefName)) {
+ return "not_controllable";
+ }
+ }
+ return await ExtensionSettingsStore.getLevelOfControl(extension, STORE_TYPE, name);
+ },
+};
--- 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,201 @@
+/* -*- 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 {
+ createAppInfo,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+const STORE_TYPE = "prefs";
+
+// Test settings to use with the preferences manager.
+const SETTINGS = {
+ multiple_prefs: {
+ prefNames: ["my.pref.1", "my.pref.2", "my.pref.3"],
+
+ initalValues: ["value1", "value2", "value3"],
+
+ valueFn(pref, value) {
+ return `${pref}-${value}`;
+ },
+
+ setCallback(value) {
+ let prefs = {};
+ for (let pref of this.prefNames) {
+ prefs[pref] = this.valueFn(pref, value);
+ }
+ return prefs;
+ },
+ },
+
+ singlePref: {
+ prefNames: [
+ "my.single.pref",
+ ],
+
+ initalValues: ["value1"],
+
+ valueFn(pref, value) {
+ return value;
+ },
+
+ setCallback(value) {
+ return {[this.prefNames[0]]: this.valueFn(null, value)};
+ },
+ },
+};
+
+ExtensionPreferencesManager.addSetting("multiple_prefs", SETTINGS.multiple_prefs);
+ExtensionPreferencesManager.addSetting("singlePref", SETTINGS.singlePref);
+
+// Set initial values for prefs.
+for (let setting in SETTINGS) {
+ setting = SETTINGS[setting];
+ for (let i = 0; i < setting.prefNames.length; i++) {
+ Preferences.set(setting.prefNames[i], setting.initalValues[i]);
+ }
+}
+
+add_task(async function test_preference_manager() {
+ // 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);
+
+ for (let setting in SETTINGS) {
+ let settingObj = SETTINGS[setting];
+ let newValue1 = "newValue1";
+ let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(extensions[1], setting);
+ equal(levelOfControl, "controllable_by_this_extension",
+ "getLevelOfControl returns correct levelOfControl with no settings set.");
+ let settingSet = await ExtensionPreferencesManager.setSetting(extensions[1], setting, newValue1);
+ ok(settingSet, "setSetting returns true when the pref(s) have been set.");
+ for (let pref of settingObj.prefNames) {
+ equal(Preferences.get(pref), settingObj.valueFn(pref, newValue1),
+ "setSetting sets the prefs for the first extension.");
+ }
+ levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(extensions[1], setting);
+ equal(
+ levelOfControl,
+ "controlled_by_this_extension",
+ "getLevelOfControl returns correct levelOfControl when a pref has been set.");
+
+ let newValue2 = "newValue2";
+ settingSet = await ExtensionPreferencesManager.setSetting(extensions[0], setting, newValue2);
+ ok(!settingSet, "setSetting returns false when the pref(s) have not been set.");
+ for (let pref of settingObj.prefNames) {
+ equal(Preferences.get(pref), settingObj.valueFn(pref, newValue1),
+ "setSetting does not set the pref(s) for an earlier extension.");
+ }
+
+ await ExtensionPreferencesManager.unsetSetting(extensions[0], setting);
+ for (let pref of settingObj.prefNames) {
+ equal(Preferences.get(pref), settingObj.valueFn(pref, newValue1),
+ "unsetSetting does not change the pref(s) for the non-top extension.");
+ }
+
+ await ExtensionPreferencesManager.setSetting(extensions[0], setting, newValue2);
+ for (let pref of settingObj.prefNames) {
+ equal(Preferences.get(pref), settingObj.valueFn(pref, newValue1),
+ "setSetting does not set the pref(s) for an earlier extension.");
+ }
+
+ await ExtensionPreferencesManager.unsetSetting(extensions[1], setting);
+ for (let pref of settingObj.prefNames) {
+ equal(Preferences.get(pref), settingObj.valueFn(pref, newValue2),
+ "unsetSetting sets the pref(s) to the next value when removing the top extension.");
+ }
+
+ await ExtensionPreferencesManager.unsetSetting(extensions[0], setting);
+ for (let i = 0; i < settingObj.prefNames.length; i++) {
+ equal(Preferences.get(settingObj.prefNames[i]), settingObj.initalValues[i],
+ "unsetSetting sets the pref(s) to the initial value(s) when removing the last extension.");
+ }
+ }
+
+ // Tests for unsetAll.
+ let newValue3 = "newValue3";
+ for (let setting in SETTINGS) {
+ let settingObj = SETTINGS[setting];
+ await ExtensionPreferencesManager.setSetting(extensions[0], setting, newValue3);
+ for (let pref of settingObj.prefNames) {
+ equal(Preferences.get(pref), settingObj.valueFn(pref, newValue3), `setSetting set the pref for ${pref}.`);
+ }
+ }
+
+ let setSettings = await ExtensionSettingsStore.getAllForExtension(extensions[0], STORE_TYPE);
+ deepEqual(setSettings, Object.keys(SETTINGS), "Expected settings were set for extension.");
+ await ExtensionPreferencesManager.unsetAll(extensions[0]);
+
+ for (let setting in SETTINGS) {
+ let settingObj = SETTINGS[setting];
+ for (let i = 0; i < settingObj.prefNames.length; i++) {
+ equal(Preferences.get(settingObj.prefNames[i]), settingObj.initalValues[i],
+ "unsetAll unset the pref.");
+ }
+ }
+
+ setSettings = await ExtensionSettingsStore.getAllForExtension(extensions[0], STORE_TYPE);
+ deepEqual(setSettings, [], "unsetAll removed all settings.");
+
+ // Test with an uninitialized pref.
+ let setting = "singlePref";
+ let settingObj = SETTINGS[setting];
+ let pref = settingObj.prefNames[0];
+ let newValue = "newValue";
+ Preferences.reset(pref);
+ await ExtensionPreferencesManager.setSetting(extensions[1], setting, newValue);
+ equal(Preferences.get(pref), settingObj.valueFn(pref, newValue),
+ "Uninitialized pref is set.");
+ await ExtensionPreferencesManager.unsetSetting(extensions[1], setting);
+ ok(!Preferences.has(pref), "unsetSetting removed the pref.");
+
+ // Test levelOfControl with a locked pref.
+ setting = "multiple_prefs";
+ let prefToLock = SETTINGS[setting].prefNames[0];
+ Preferences.lock(prefToLock, 1);
+ ok(Preferences.locked(prefToLock), `Preference ${prefToLock} is locked.`);
+ let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(extensions[1], setting);
+ equal(
+ levelOfControl,
+ "not_controllable",
+ "getLevelOfControl returns correct levelOfControl when a pref is locked.");
+
+ for (let extension of testExtensions) {
+ await extension.unload();
+ }
+
+ await promiseShutdownManager();
+});
--- 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]