/* 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; const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {}); 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}); }); /* eslint-disable mozilla/balanced-listeners */ Management.on("shutdown", (type, extension) => { switch (extension.shutdownReason) { case "ADDON_DISABLE": case "ADDON_DOWNGRADE": case "ADDON_UPGRADE": this.ExtensionPreferencesManager.disableAll(extension); break; case "ADDON_UNINSTALL": this.ExtensionPreferencesManager.removeAll(extension); break; } }); Management.on("startup", (type, extension) => { if (["ADDON_ENABLE", "ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(extension.startupReason)) { this.ExtensionPreferencesManager.enableAll(extension); } }); /* eslint-enable mozilla/balanced-listeners */ 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 item returned by the ExtensionSettingsStore and conditionally sets * preferences based on the item's contents. * * @param {string} name * The name of the setting being processed. * @param {Object|null} item * Either null, or an object with a value property which indicates the * value stored for the setting in the settings store. * @returns {Promise} * Resolves to true if the preferences were changed and to false if * the preferences were not changed. */ async function processItem(name, item) { if (item) { let prefs = item.initialValue || await settingsMap.get(name).setCallback(item.value); for (let pref in prefs) { if (prefs[pref] === undefined) { Preferences.reset(pref); } else { Preferences.set(pref, prefs[pref]); } } return true; } return false; } 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)); return await processItem(name, item); }, /** * Indicates that this extension wants to temporarily cede control over the * given setting. * * @param {Extension} extension * The extension for which a preference setting is being removed. * @param {string} name * The unique id of the setting. * * @returns {Promise} * Resolves to true if the preferences were changed and to false if * the preferences were not changed. */ async disableSetting(extension, name) { let item = await ExtensionSettingsStore.disable( extension, STORE_TYPE, name); return await processItem(name, item); }, /** * Enable a setting that has been disabled. * * @param {Extension} extension * The extension for which a setting is being enabled. * @param {string} name * The unique id of the setting. * * @returns {Promise} * Resolves to true if the preferences were changed and to false if * the preferences were not changed. */ async enableSetting(extension, name) { let item = await ExtensionSettingsStore.enable(extension, STORE_TYPE, name); return await processItem(name, item); }, /** * Indicates that this extension no longer wants to set the given setting. * * @param {Extension} extension * The extension for which a preference setting is being removed. * @param {string} name * The unique id of the setting. * * @returns {Promise} * Resolves to true if the preferences were changed and to false if * the preferences were not changed. */ async removeSetting(extension, name) { let item = await ExtensionSettingsStore.removeSetting( extension, STORE_TYPE, name); return await processItem(name, item); }, /** * Disables all previously set settings for an extension. This can be called when * an extension is being disabled, for example. * * @param {Extension} extension * The extension for which all settings are being unset. */ async disableAll(extension) { let settings = await ExtensionSettingsStore.getAllForExtension(extension, STORE_TYPE); let disablePromises = []; for (let name of settings) { disablePromises.push(this.disableSetting(extension, name)); } await Promise.all(disablePromises); }, /** * Enables all disabled settings for an extension. This can be called when * an extension has finsihed updating or is being re-enabled, for example. * * @param {Extension} extension * The extension for which all settings are being enabled. */ async enableAll(extension) { let settings = await ExtensionSettingsStore.getAllForExtension(extension, STORE_TYPE); let enablePromises = []; for (let name of settings) { enablePromises.push(this.enableSetting(extension, name)); } await Promise.all(enablePromises); }, /** * Removes all previously set settings for an extension. This can be called when * an extension is being uninstalled, for example. * * @param {Extension} extension * The extension for which all settings are being unset. */ async removeAll(extension) { let settings = await ExtensionSettingsStore.getAllForExtension(extension, STORE_TYPE); let removePromises = []; for (let name of settings) { removePromises.push(this.removeSetting(extension, name)); } await Promise.all(removePromises); }, /** * 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); }, };