1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
9 * This module is used for storing changes to settings that are
10 * requested by extensions, and for finding out what the current value
11 * of a setting should be, based on the precedence chain.
13 * When multiple extensions request to make a change to a particular
14 * setting, the most recently installed extension will be given
17 * This precedence chain of settings is stored in JSON format,
18 * without indentation, using UTF-8 encoding.
19 * With indentation applied, the file would look like this:
22 * type: { // The type of settings being stored in this object, i.e., prefs.
23 * key: { // The unique key for the setting.
24 * initialValue, // The initial value of the setting.
27 * id, // The id of the extension requesting the setting.
28 * installDate, // The install date of the extension, stored as a number.
29 * value, // The value of the setting requested by the extension.
30 * enabled // Whether the setting is currently enabled.
42 import { ExtensionParent } from "resource://gre/modules/ExtensionParent.sys.mjs";
46 ChromeUtils.defineESModuleGetters(lazy, {
47 AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
48 JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
51 // Defined for readability of precedence and selection code. keyInfo.selected will be
52 // one of these defines, or the id of an extension if an extension has been explicitly
54 const SETTING_USER_SET = null;
55 const SETTING_PRECEDENCE_ORDER = undefined;
57 const JSON_FILE_NAME = "extension-settings.json";
58 const JSON_FILE_VERSION = 3;
59 const STORE_PATH = PathUtils.join(
60 Services.dirsvc.get("ProfD", Ci.nsIFile).path,
64 let _initializePromise;
67 // Processes the JSON data when read from disk to convert string dates into numbers.
68 function dataPostProcessor(json) {
69 if (json.version !== JSON_FILE_VERSION) {
70 for (let storeType in json) {
71 for (let setting in json[storeType]) {
72 for (let extData of json[storeType][setting].precedenceList) {
73 if (setting == "overrideContentColorScheme" && extData.value > 2) {
76 if (typeof extData.installDate != "number") {
77 extData.installDate = new Date(extData.installDate).valueOf();
82 json.version = JSON_FILE_VERSION;
87 // Loads the data from the JSON file into memory.
88 function initialize() {
89 if (!_initializePromise) {
90 _store = new lazy.JSONFile({
94 _initializePromise = _store.load();
96 return _initializePromise;
99 // Test-only method to force reloading of the JSON file.
100 async function reloadFile(saveChanges) {
102 // Disarm the saver so that the current changes are dropped.
103 _store._saver.disarm();
105 await _store.finalize();
106 _initializePromise = null;
110 // Checks that the store is ready and that the requested type exists.
111 function ensureType(type) {
112 if (!_store.dataReady) {
114 "The ExtensionSettingsStore was accessed before the initialize promise resolved."
118 // Ensure a property exists for the given type.
119 if (!_store.data[type]) {
120 _store.data[type] = {};
125 * Return an object with properties for key, value|initialValue, id|null, or
126 * null if no setting has been stored for that key.
128 * If no id is passed then return the highest priority item for the key.
130 * @param {string} type
131 * The type of setting to be retrieved.
132 * @param {string} key
133 * A string that uniquely identifies the setting.
134 * @param {string} [id]
135 * The id of the extension for which the item is being retrieved.
136 * If no id is passed, then the highest priority item for the key
139 * @returns {object | null}
140 * Either an object with properties for key and value, or
141 * null if no key is found.
143 function getItem(type, key, id) {
146 let keyInfo = _store.data[type][key];
151 // If no id was provided, the selected entry will have precedence.
152 if (!id && keyInfo.selected) {
153 id = keyInfo.selected;
156 // Return the item that corresponds to the extension with id of id.
157 let item = keyInfo.precedenceList.find(item => item.id === id);
158 return item ? { key, value: item.value, id } : null;
161 // Find the highest precedence, enabled setting, if it has not been
163 if (keyInfo.selected === SETTING_PRECEDENCE_ORDER) {
164 for (let item of keyInfo.precedenceList) {
166 return { key, value: item.value, id: item.id };
171 // Nothing found in the precedenceList or the setting is user-set,
172 // return the initialValue.
173 return { key, initialValue: keyInfo.initialValue };
177 * Return an array of objects with properties for key, value, id, and enabled
178 * or an empty array if no settings have been stored for that key.
180 * @param {string} type
181 * The type of setting to be retrieved.
182 * @param {string} key
183 * A string that uniquely identifies the setting.
185 * @returns {Array} an array of objects with properties for key, value, id, and enabled
187 function getAllItems(type, key) {
190 let keyInfo = _store.data[type][key];
195 let items = keyInfo.precedenceList;
197 ? items.map(item => ({
201 enabled: item.enabled,
206 // Comparator used when sorting the precedence list.
207 function precedenceComparator(a, b) {
208 if (a.enabled && !b.enabled) {
211 if (b.enabled && !a.enabled) {
214 return b.installDate - a.installDate;
218 * Helper method that alters a setting, either by changing its enabled status
221 * @param {string|null} id
222 * The id of the extension for which a setting is being altered, may also
223 * be SETTING_USER_SET (null).
224 * @param {string} type
225 * The type of setting to be altered.
226 * @param {string} key
227 * A string that uniquely identifies the setting.
228 * @param {string} action
229 * The action to perform on the setting.
230 * Will be one of remove|enable|disable.
232 * @returns {object | null}
233 * Either an object with properties for key and value, which
234 * corresponds to the current top precedent setting, or null if
235 * the current top precedent setting has not changed.
237 function alterSetting(id, type, key, action) {
238 let returnItem = null;
241 let keyInfo = _store.data[type][key];
243 if (action === "remove") {
247 `Cannot alter the setting for ${type}:${key} as it does not exist.`
251 let foundIndex = keyInfo.precedenceList.findIndex(item => item.id == id);
253 if (foundIndex === -1 && (action !== "select" || id !== SETTING_USER_SET)) {
254 if (action === "remove") {
258 `Cannot alter the setting for ${type}:${key} as ${id} does not exist.`
262 let selected = keyInfo.selected;
265 if (foundIndex >= 0 && !keyInfo.precedenceList[foundIndex].enabled) {
267 `Cannot select the setting for ${type}:${key} as ${id} is disabled.`
270 keyInfo.selected = id;
271 keyInfo.selectedDate = Date.now();
275 // Removing a user-set setting reverts to precedence order.
276 if (id === keyInfo.selected) {
277 keyInfo.selected = SETTING_PRECEDENCE_ORDER;
278 delete keyInfo.selectedDate;
280 keyInfo.precedenceList.splice(foundIndex, 1);
284 keyInfo.precedenceList[foundIndex].enabled = true;
285 keyInfo.precedenceList.sort(precedenceComparator);
286 // Enabling a setting does not change a user-set setting, so we
287 // save and bail early.
288 if (keyInfo.selected !== SETTING_PRECEDENCE_ORDER) {
292 foundIndex = keyInfo.precedenceList.findIndex(item => item.id == id);
296 // Disabling a user-set setting reverts to precedence order.
297 if (keyInfo.selected === id) {
298 keyInfo.selected = SETTING_PRECEDENCE_ORDER;
299 delete keyInfo.selectedDate;
301 keyInfo.precedenceList[foundIndex].enabled = false;
302 keyInfo.precedenceList.sort(precedenceComparator);
306 throw new Error(`${action} is not a valid action for alterSetting.`);
309 if (selected !== keyInfo.selected || foundIndex === 0) {
310 returnItem = getItem(type, key);
313 if (action === "remove" && keyInfo.precedenceList.length === 0) {
314 delete _store.data[type][key];
318 ExtensionParent.apiManager.emit("extension-setting-changed", {
328 export var ExtensionSettingsStore = {
332 * Loads the JSON file for the SettingsStore into memory.
333 * The promise this returns must be resolved before asking the SettingsStore
334 * to perform any other operations.
337 * A promise that resolves when the Store is ready to be accessed.
344 * Adds a setting to the store, returning the new setting if it changes.
347 * The id of the extension for which a setting is being added.
348 * @param {string} type
349 * The type of setting to be stored.
350 * @param {string} key
351 * A string that uniquely identifies the setting.
352 * @param {string} value
353 * The value to be stored in the setting.
354 * @param {Function} initialValueCallback
355 * A function to be called to determine the initial value for the
356 * setting. This will be passed the value in the callbackArgument
357 * argument. If omitted the initial value will be undefined.
358 * @param {any} callbackArgument
359 * The value to be passed into the initialValueCallback. It defaults to
360 * the value of the key argument.
361 * @param {Function} settingDataUpdate
362 * A function to be called to modify the initial value if necessary.
364 * @returns {Promise<object?>} Either an object with properties for key and
365 * value, which corresponds to the item that was
366 * just added, or null if the item that was just
367 * added does not need to be set because it is not
368 * selected or at the top of the precedence list.
375 initialValueCallback = () => undefined,
376 callbackArgument = key,
377 settingDataUpdate = val => val
379 if (typeof initialValueCallback != "function") {
380 throw new Error("initialValueCallback must be a function.");
385 if (!_store.data[type][key]) {
386 // The setting for this key does not exist. Set the initial value.
387 let initialValue = await initialValueCallback(callbackArgument);
388 _store.data[type][key] = {
393 let keyInfo = _store.data[type][key];
395 // Allow settings to upgrade the initial value if necessary.
396 keyInfo.initialValue = settingDataUpdate(keyInfo.initialValue);
398 // Check for this item in the precedenceList.
399 let foundIndex = keyInfo.precedenceList.findIndex(item => item.id == id);
400 let newInstall = false;
401 if (foundIndex === -1) {
402 // No item for this extension, so add a new one.
403 let addon = await lazy.AddonManager.getAddonByID(id);
404 keyInfo.precedenceList.push({
406 installDate: addon.installDate.valueOf(),
410 newInstall = addon.installDate.valueOf() > keyInfo.selectedDate;
412 // Item already exists or this extension, so update it.
413 let item = keyInfo.precedenceList[foundIndex];
415 // Ensure the item is enabled.
420 keyInfo.precedenceList.sort(precedenceComparator);
421 foundIndex = keyInfo.precedenceList.findIndex(item => item.id == id);
423 // If our new setting is top of precedence, then reset the selected entry.
424 if (foundIndex === 0 && newInstall) {
425 keyInfo.selected = SETTING_PRECEDENCE_ORDER;
426 delete keyInfo.selectedDate;
431 // Check whether this is currently selected item if one is
432 // selected, otherwise the top item has precedence.
434 keyInfo.selected !== SETTING_USER_SET &&
435 (keyInfo.selected === id || foundIndex === 0)
437 return { id, key, value };
443 * Removes a setting from the store, returning the new setting if it changes.
446 * The id of the extension for which a setting is being removed.
447 * @param {string} type
448 * The type of setting to be removed.
449 * @param {string} key
450 * A string that uniquely identifies the setting.
452 * @returns {object | null}
453 * Either an object with properties for key and value if the setting changes, or null.
455 removeSetting(id, type, key) {
456 return alterSetting(id, type, key, "remove");
460 * Enables a setting in the store, returning the new setting if it changes.
463 * The id of the extension for which a setting is being enabled.
464 * @param {string} type
465 * The type of setting to be enabled.
466 * @param {string} key
467 * A string that uniquely identifies the setting.
469 * @returns {object | null}
470 * Either an object with properties for key and value if the setting changes, or null.
472 enable(id, type, key) {
473 return alterSetting(id, type, key, "enable");
477 * Disables a setting in the store, returning the new setting if it changes.
480 * The id of the extension for which a setting is being disabled.
481 * @param {string} type
482 * The type of setting to be disabled.
483 * @param {string} key
484 * A string that uniquely identifies the setting.
486 * @returns {object | null}
487 * Either an object with properties for key and value if the setting changes, or null.
489 disable(id, type, key) {
490 return alterSetting(id, type, key, "disable");
494 * Specifically select an extension, or no extension, that will be in control of
497 * To select a specific extension that controls this setting, pass the extension id.
499 * To select as user-set pass SETTING_USER_SET as the id. In this case, no extension
500 * will have control of the setting.
502 * Once a specific selection is made, precedence order will not be used again unless the selected
503 * extension is disabled, removed, or a new extension takes control of the setting.
505 * @param {string | null} id
506 * The id of the extension being selected or SETTING_USER_SET (null).
507 * @param {string} type
508 * The type of setting to be selected.
509 * @param {string} key
510 * A string that uniquely identifies the setting.
512 * @returns {object | null}
513 * Either an object with properties for key and value if the setting changes, or null.
515 select(id, type, key) {
516 return alterSetting(id, type, key, "select");
520 * Retrieves all settings from the store for a given extension.
523 * The id of the extension for which a settings are being retrieved.
524 * @param {string} type
525 * The type of setting to be returned.
528 * A list of settings which have been stored for the extension.
530 getAllForExtension(id, type) {
533 let keysObj = _store.data[type];
535 for (let key in keysObj) {
536 if (keysObj[key].precedenceList.find(item => item.id == id)) {
544 * Retrieves a setting from the store, either for a specific extension,
545 * or current top precedent setting for the key.
547 * @param {string} type The type of setting to be returned.
548 * @param {string} key A string that uniquely identifies the setting.
550 * The id of the extension for which the setting is being retrieved.
551 * Defaults to undefined, in which case the top setting is returned.
553 * @returns {object} An object with properties for key, value and id.
555 getSetting(type, key, id) {
556 return getItem(type, key, id);
560 * Retrieves an array of objects representing extensions attempting to control the specified setting
561 * or an empty array if no settings have been stored for that key.
563 * @param {string} type
564 * The type of setting to be retrieved.
565 * @param {string} key
566 * A string that uniquely identifies the setting.
568 * @returns {Array} an array of objects with properties for key, value, id, and enabled
570 getAllSettings(type, key) {
571 return getAllItems(type, key);
575 * Returns whether an extension currently has a stored setting for a given
578 * @param {string} id The id of the extension which is being checked.
579 * @param {string} type The type of setting to be checked.
580 * @param {string} key A string that uniquely identifies the setting.
582 * @returns {boolean} Whether the extension currently has a stored setting.
584 hasSetting(id, type, key) {
585 return this.getAllForExtension(id, type).includes(key);
589 * Return the levelOfControl for a key / extension combo.
590 * levelOfControl is required by Google's ChromeSetting prototype which
591 * in turn is used by the privacy API among others.
593 * It informs a caller of the state of a setting with respect to the current
594 * extension, and can be one of the following values:
596 * controlled_by_other_extensions: controlled by extensions with higher precedence
597 * controllable_by_this_extension: can be controlled by this extension
598 * controlled_by_this_extension: controlled by this extension
601 * The id of the extension for which levelOfControl is being requested.
602 * @param {string} type
603 * The type of setting to be returned. For example `pref`.
604 * @param {string} key
605 * A string that uniquely identifies the setting, for example, a
608 * @returns {Promise<string>}
609 * The level of control of the extension over the key.
611 async getLevelOfControl(id, type, key) {
614 let keyInfo = _store.data[type][key];
615 if (!keyInfo || !keyInfo.precedenceList.length) {
616 return "controllable_by_this_extension";
619 if (keyInfo.selected !== SETTING_PRECEDENCE_ORDER) {
620 if (id === keyInfo.selected) {
621 return "controlled_by_this_extension";
623 // When user set, the setting is never "controllable" unless the installDate
624 // is later than the user date.
625 let addon = await lazy.AddonManager.getAddonByID(id);
626 return !addon || keyInfo.selectedDate > addon.installDate.valueOf()
628 : "controllable_by_this_extension";
631 let enabledItems = keyInfo.precedenceList.filter(item => item.enabled);
632 if (!enabledItems.length) {
633 return "controllable_by_this_extension";
636 let topItem = enabledItems[0];
637 if (topItem.id == id) {
638 return "controlled_by_this_extension";
641 let addon = await lazy.AddonManager.getAddonByID(id);
642 return !addon || topItem.installDate > addon.installDate.valueOf()
643 ? "controlled_by_other_extensions"
644 : "controllable_by_this_extension";
648 * Test-only method to force reloading of the JSON file.
650 * Note that this method simply clears the local variable that stores the
651 * file, so the next time the file is accessed it will be reloaded.
653 * @param {boolean} saveChanges
654 * When false, discard any changes that have been made since the last
655 * time the store was saved.
657 * A promise that resolves once the settings store has been cleared.
659 _reloadFile(saveChanges = true) {
660 return reloadFile(saveChanges);
664 // eslint-disable-next-line mozilla/balanced-listeners
665 ExtensionParent.apiManager.on("uninstall-complete", async (type, { id }) => {
666 // Catch any settings that were not properly removed during "uninstall".
667 await ExtensionSettingsStore.initialize();
668 for (let type in _store.data) {
669 // prefs settings must be handled by ExtensionPreferencesManager.
670 if (type === "prefs") {
673 let items = ExtensionSettingsStore.getAllForExtension(id, type);
674 for (let key of items) {
675 ExtensionSettingsStore.removeSetting(id, type, key);
676 Services.console.logStringMessage(
677 `Post-Uninstall removal of addon settings for ${id}, type: ${type} key: ${key}`