gecko-dev/toolkit/components/extensions/ExtensionSettingsStore.jsm
Bob Silverberg 767ea1e9b1 Bug 1341277 - Part 1: Update ExtensionSettingsStore to support disabled settings. r=aswan
MozReview-Commit-ID: 4N67JXfO81D

--HG--
extra : rebase_source : 12d4ad1ede515f57d7256899e197e83e129877a0
2017-02-22 11:35:10 -05:00

389 lines
12 KiB
JavaScript

/* 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 storing changes to settings that are
* requested by extensions, and for finding out what the current value
* of a setting should be, based on the precedence chain.
*
* When multiple extensions request to make a change to a particular
* setting, the most recently installed extension will be given
* precedence.
*
* This precedence chain of settings is stored in JSON format,
* without indentation, using UTF-8 encoding.
* With indentation applied, the file would look like this:
*
* {
* type: { // The type of settings being stored in this object, i.e., prefs.
* key: { // The unique key for the setting.
* initialValue, // The initial value of the setting.
* precedenceList: [
* {
* id, // The id of the extension requesting the setting.
* installDate, // The install date of the extension.
* value, // The value of the setting requested by the extension.
* enabled // Whether the setting is currently enabled.
* }
* ],
* },
* key: {
* // ...
* }
* }
* }
*
*/
"use strict";
this.EXPORTED_SYMBOLS = ["ExtensionSettingsStore"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
"resource://gre/modules/JSONFile.jsm");
const JSON_FILE_NAME = "extension-settings.json";
const STORE_PATH = OS.Path.join(Services.dirsvc.get("ProfD", Ci.nsIFile).path, JSON_FILE_NAME);
let _store;
// Get the internal settings store, which is persisted in a JSON file.
function getStore(type) {
if (!_store) {
let initStore = new JSONFile({
path: STORE_PATH,
});
initStore.ensureDataReady();
_store = initStore;
}
// Ensure a property exists for the given type.
if (!_store.data[type]) {
_store.data[type] = {};
}
return _store;
}
// Return an object with properties for key and value|initialValue, or null
// if no setting has been stored for that key.
async function getTopItem(type, key) {
let store = getStore(type);
let keyInfo = store.data[type][key];
if (!keyInfo) {
return null;
}
// Find the highest precedence, enabled setting.
for (let item of keyInfo.precedenceList) {
if (item.enabled) {
return {key, value: item.value};
}
}
// Nothing found in the precedenceList, return the initialValue.
return {key, initialValue: keyInfo.initialValue};
}
// Comparator used when sorting the precedence list.
function precedenceComparator(a, b) {
if (a.enabled && !b.enabled) {
return -1;
}
if (b.enabled && !a.enabled) {
return 1;
}
return b.installDate - a.installDate;
}
/**
* Helper method that alters a setting, either by changing its enabled status
* or by removing it.
*
* @param {Extension} extension
* The extension for which a setting is being removed/disabled.
* @param {string} type
* The type of setting to be altered.
* @param {string} key
* A string that uniquely identifies the setting.
* @param {string} action
* The action to perform on the setting.
* Will be one of remove|enable|disable.
*
* @returns {object | null}
* Either an object with properties for key and value, which
* corresponds to the current top precedent setting, or null if
* the current top precedent setting has not changed.
*/
async function alterSetting(extension, type, key, action) {
let returnItem;
let store = getStore(type);
let keyInfo = store.data[type][key];
if (!keyInfo) {
throw new Error(
`Cannot alter the setting for ${type}:${key} as it does not exist.`);
}
let id = extension.id;
let foundIndex = keyInfo.precedenceList.findIndex(item => item.id == id);
if (foundIndex === -1) {
throw new Error(
`Cannot alter the setting for ${type}:${key} as it does not exist.`);
}
switch (action) {
case "remove":
keyInfo.precedenceList.splice(foundIndex, 1);
break;
case "enable":
keyInfo.precedenceList[foundIndex].enabled = true;
keyInfo.precedenceList.sort(precedenceComparator);
foundIndex = keyInfo.precedenceList.findIndex(item => item.id == id);
break;
case "disable":
keyInfo.precedenceList[foundIndex].enabled = false;
keyInfo.precedenceList.sort(precedenceComparator);
break;
default:
throw new Error(`${action} is not a valid action for alterSetting.`);
}
if (foundIndex === 0) {
returnItem = await getTopItem(type, key);
}
if (action === "remove" && keyInfo.precedenceList.length === 0) {
delete store.data[type][key];
}
store.saveSoon();
return returnItem;
}
this.ExtensionSettingsStore = {
/**
* Adds a setting to the store, possibly returning the current top precedent
* setting.
*
* @param {Extension} extension
* The extension for which a setting is being added.
* @param {string} type
* The type of setting to be stored.
* @param {string} key
* A string that uniquely identifies the setting.
* @param {string} value
* The value to be stored in the setting.
* @param {function} initialValueCallback
* An function to be called to determine the initial value for the
* setting. This will be passed the value in the callbackArgument
* argument.
* @param {any} callbackArgument
* The value to be passed into the initialValueCallback. It defaults to
* the value of the key argument.
*
* @returns {object | null} Either an object with properties for key and
* value, which corresponds to the item that was
* just added, or null if the item that was just
* added does not need to be set because it is not
* at the top of the precedence list.
*/
async addSetting(extension, type, key, value, initialValueCallback, callbackArgument = key) {
if (typeof initialValueCallback != "function") {
throw new Error("initialValueCallback must be a function.");
}
let id = extension.id;
let store = getStore(type);
if (!store.data[type][key]) {
// The setting for this key does not exist. Set the initial value.
let initialValue = await initialValueCallback(callbackArgument);
store.data[type][key] = {
initialValue,
precedenceList: [],
};
}
let keyInfo = store.data[type][key];
// Check for this item in the precedenceList.
let foundIndex = keyInfo.precedenceList.findIndex(item => item.id == id);
if (foundIndex === -1) {
// No item for this extension, so add a new one.
let addon = await AddonManager.getAddonByID(id);
keyInfo.precedenceList.push({id, installDate: addon.installDate, value, enabled: true});
} else {
// Item already exists or this extension, so update it.
keyInfo.precedenceList[foundIndex].value = value;
}
// Sort the list.
keyInfo.precedenceList.sort(precedenceComparator);
store.saveSoon();
// Check whether this is currently the top item.
if (keyInfo.precedenceList[0].id == id) {
return {key, value};
}
return null;
},
/**
* Removes a setting from the store, possibly returning the current top
* precedent setting.
*
* @param {Extension} extension
* The extension for which a setting is being removed.
* @param {string} type
* The type of setting to be removed.
* @param {string} key
* A string that uniquely identifies the setting.
*
* @returns {object | null}
* Either an object with properties for key and value, which
* corresponds to the current top precedent setting, or null if
* the current top precedent setting has not changed.
*/
async removeSetting(extension, type, key) {
return await alterSetting(extension, type, key, "remove");
},
/**
* Enables a setting in the store, possibly returning the current top
* precedent setting.
*
* @param {Extension} extension
* The extension for which a setting is being enabled.
* @param {string} type
* The type of setting to be enabled.
* @param {string} key
* A string that uniquely identifies the setting.
*
* @returns {object | null}
* Either an object with properties for key and value, which
* corresponds to the current top precedent setting, or null if
* the current top precedent setting has not changed.
*/
async enable(extension, type, key) {
return await alterSetting(extension, type, key, "enable");
},
/**
* Disables a setting in the store, possibly returning the current top
* precedent setting.
*
* @param {Extension} extension
* The extension for which a setting is being disabled.
* @param {string} type
* The type of setting to be disabled.
* @param {string} key
* A string that uniquely identifies the setting.
*
* @returns {object | null}
* Either an object with properties for key and value, which
* corresponds to the current top precedent setting, or null if
* the current top precedent setting has not changed.
*/
async disable(extension, type, key) {
return await alterSetting(extension, type, key, "disable");
},
/**
* Retrieves all settings from the store for a given extension.
*
* @param {Extension} extension The extension for which a settings are being retrieved.
* @param {string} type The type of setting to be returned.
*
* @returns {array} A list of settings which have been stored for the extension.
*/
async getAllForExtension(extension, type) {
let store = getStore(type);
let keysObj = store.data[type];
let items = [];
for (let key in keysObj) {
if (keysObj[key].precedenceList.find(item => item.id == extension.id)) {
items.push(key);
}
}
return items;
},
/**
* Retrieves a setting from the store, returning the current top precedent
* setting for the key.
*
* @param {string} type The type of setting to be returned.
* @param {string} key A string that uniquely identifies the setting.
*
* @returns {object} An object with properties for key and value.
*/
async getSetting(type, key) {
return await getTopItem(type, key);
},
/**
* Return the levelOfControl for a key / extension combo.
* levelOfControl is required by Google's ChromeSetting prototype which
* in turn is used by the privacy API among others.
*
* It informs a caller of the state of a setting with respect to the current
* extension, and can be one of the following values:
*
* controlled_by_other_extensions: controlled by extensions with higher precedence
* controllable_by_this_extension: can be controlled by this extension
* controlled_by_this_extension: controlled by this extension
*
* @param {Extension} extension
* The extension for which levelOfControl is being requested.
* @param {string} type
* The type of setting to be returned. For example `pref`.
* @param {string} key
* A string that uniquely identifies the setting, for example, a
* preference name.
*
* @returns {string}
* The level of control of the extension over the key.
*/
async getLevelOfControl(extension, type, key) {
let store = getStore(type);
let keyInfo = store.data[type][key];
if (!keyInfo || !keyInfo.precedenceList.length) {
return "controllable_by_this_extension";
}
let id = extension.id;
let enabledItems = keyInfo.precedenceList.filter(item => item.enabled);
if (!enabledItems.length) {
return "controllable_by_this_extension";
}
let topItem = enabledItems[0];
if (topItem.id == id) {
return "controlled_by_this_extension";
}
let addon = await AddonManager.getAddonByID(id);
return topItem.installDate > addon.installDate ?
"controlled_by_other_extensions" :
"controllable_by_this_extension";
},
};