--- a/browser/components/extensions/ext-browser.json
+++ b/browser/components/extensions/ext-browser.json
@@ -22,16 +22,17 @@
"scopes": ["addon_parent"],
"paths": [
["browsingData"]
]
},
"chrome_settings_overrides": {
"url": "chrome://browser/content/ext-chrome-settings-overrides.js",
"scopes": [],
+ "events": ["update", "uninstall"],
"schema": "chrome://browser/content/schemas/chrome_settings_overrides.json",
"manifest": ["chrome_settings_overrides"]
},
"commands": {
"url": "chrome://browser/content/ext-commands.js",
"schema": "chrome://browser/content/schemas/commands.json",
"scopes": ["addon_parent"],
"manifest": ["commands"],
--- a/browser/components/extensions/ext-chrome-settings-overrides.js
+++ b/browser/components/extensions/ext-chrome-settings-overrides.js
@@ -10,17 +10,22 @@ const {classes: Cc, interfaces: Ci, util
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPreferencesManager",
"resource://gre/modules/ExtensionPreferencesManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
"resource://gre/modules/ExtensionSettingsStore.jsm");
const DEFAULT_SEARCH_STORE_TYPE = "default_search";
const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch";
+const ENGINE_ADDED_SETTING_NAME = "engineAdded";
+// This promise is used to wait for the search service to be initialized.
+// None of the code in this module requests that initialization. It is assumed
+// that it is started at some point. If tests start to fail because this
+// promise never resolves, that's likely the cause.
const searchInitialized = () => {
if (Services.search.isInitialized) {
return;
}
return new Promise(resolve => {
const SEARCH_SERVICE_TOPIC = "browser-search-service";
Services.obs.addObserver(function observer(subject, topic, data) {
if (data != "init-complete") {
@@ -29,94 +34,118 @@ const searchInitialized = () => {
Services.obs.removeObserver(observer, SEARCH_SERVICE_TOPIC);
resolve();
}, SEARCH_SERVICE_TOPIC);
});
};
this.chrome_settings_overrides = class extends ExtensionAPI {
- processDefaultSearchSetting(action) {
- let {extension} = this;
+ static async processDefaultSearchSetting(action, id) {
+ await ExtensionSettingsStore.initialize();
let item = ExtensionSettingsStore.getSetting(DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
if (!item) {
return;
}
if (Services.search.currentEngine.name != item.value &&
Services.search.currentEngine.name != item.initialValue) {
// The current engine is not the same as the value that the ExtensionSettingsStore has.
// This means that the user changed the engine, so we shouldn't control it anymore.
// Do nothing and remove our entry from the ExtensionSettingsStore.
- ExtensionSettingsStore.removeSetting(extension.id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
+ ExtensionSettingsStore.removeSetting(id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
return;
}
- item = ExtensionSettingsStore[action](extension.id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
+ item = ExtensionSettingsStore[action](id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
if (item) {
try {
let engine = Services.search.getEngineByName(item.value || item.initialValue);
if (engine) {
Services.search.currentEngine = engine;
}
} catch (e) {
Components.utils.reportError(e);
}
}
}
+ static async removeEngine(id) {
+ await ExtensionSettingsStore.initialize();
+ let item = await ExtensionSettingsStore.getSetting(
+ DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME, id);
+ if (item) {
+ ExtensionSettingsStore.removeSetting(
+ id, DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME);
+ await searchInitialized();
+ let engine = Services.search.getEngineByName(item.value);
+ try {
+ Services.search.removeEngine(engine);
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+ }
+
+ static removeSearchSettings(id) {
+ this.processDefaultSearchSetting("removeSetting", id);
+ this.removeEngine(id);
+ }
+
+ static onUninstall(id) {
+ // Note: We do not have to deal with homepage here as it is managed by
+ // the ExtensionPreferencesManager.
+ this.removeSearchSettings(id);
+ }
+
+ static onUpdate(id, manifest) {
+ let haveHomepage = manifest && manifest.chrome_settings_overrides &&
+ manifest.chrome_settings_overrides.homepage;
+ if (!haveHomepage) {
+ ExtensionPreferencesManager.removeSetting(id, "homepage_override");
+ }
+
+ let haveSearchProvider = manifest && manifest.chrome_settings_overrides &&
+ manifest.chrome_settings_overrides.search_provider;
+ if (!haveSearchProvider) {
+ this.removeSearchSettings(id);
+ }
+ }
+
async onManifestEntry(entryName) {
let {extension} = this;
let {manifest} = extension;
await ExtensionSettingsStore.initialize();
if (manifest.chrome_settings_overrides.homepage) {
ExtensionPreferencesManager.setSetting(extension.id, "homepage_override",
manifest.chrome_settings_overrides.homepage);
}
if (manifest.chrome_settings_overrides.search_provider) {
await searchInitialized();
extension.callOnClose({
close: () => {
- if (extension.shutdownReason == "ADDON_DISABLE" ||
- extension.shutdownReason == "ADDON_UNINSTALL") {
- switch (extension.shutdownReason) {
- case "ADDON_DISABLE":
- this.processDefaultSearchSetting("disable");
- break;
-
- case "ADDON_UNINSTALL":
- this.processDefaultSearchSetting("removeSetting");
- break;
- }
- // We shouldn't need to wait for search initialized here
- // because the search service should be ready to go.
- let engines = Services.search.getEnginesByExtensionID(extension.id);
- for (let engine of engines) {
- try {
- Services.search.removeEngine(engine);
- } catch (e) {
- Components.utils.reportError(e);
- }
- }
+ if (extension.shutdownReason == "ADDON_DISABLE") {
+ chrome_settings_overrides.processDefaultSearchSetting("disable", extension.id);
+ chrome_settings_overrides.removeEngine(extension.id);
}
},
});
let searchProvider = manifest.chrome_settings_overrides.search_provider;
let engineName = searchProvider.name.trim();
if (searchProvider.is_default) {
let engine = Services.search.getEngineByName(engineName);
if (engine && Services.search.getDefaultEngines().includes(engine)) {
// Needs to be called every time to handle reenabling, but
// only sets default for install or enable.
await this.setDefault(engineName);
// For built in search engines, we don't do anything further
return;
}
}
- this.addSearchEngine(searchProvider);
+ await this.addSearchEngine(searchProvider);
if (searchProvider.is_default) {
if (extension.startupReason === "ADDON_INSTALL") {
// Don't ask if it already the current engine
let engine = Services.search.getEngineByName(engineName);
if (Services.search.currentEngine != engine) {
let allow = await new Promise(resolve => {
let subject = {
wrappedJSObject: {
@@ -139,36 +168,35 @@ this.chrome_settings_overrides = class e
}
}
// Needs to be called every time to handle reenabling, but
// only sets default for install or enable.
await this.setDefault(engineName);
} else if (ExtensionSettingsStore.hasSetting(
extension.id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME)) {
// is_default has been removed, but we still have a setting. Remove it.
- // This won't cover the case where the entire search_provider is removed.
- this.processDefaultSearchSetting("removeSetting");
+ chrome_settings_overrides.processDefaultSearchSetting("removeSetting", extension.id);
}
}
}
async setDefault(engineName) {
let {extension} = this;
if (extension.startupReason === "ADDON_INSTALL") {
let item = await ExtensionSettingsStore.addSetting(
extension.id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME, engineName, () => {
return Services.search.currentEngine.name;
});
Services.search.currentEngine = Services.search.getEngineByName(item.value);
} else if (extension.startupReason === "ADDON_ENABLE") {
- this.processDefaultSearchSetting("enable");
+ chrome_settings_overrides.processDefaultSearchSetting("enable", extension.id);
}
}
- addSearchEngine(searchProvider) {
+ async addSearchEngine(searchProvider) {
let {extension} = this;
let isCurrent = false;
let index = -1;
if (extension.startupReason === "ADDON_UPGRADE") {
let engines = Services.search.getEnginesByExtensionID(extension.id);
if (engines.length > 0) {
// There can be only one engine right now
isCurrent = Services.search.currentEngine == engines[0];
@@ -182,16 +210,19 @@ this.chrome_settings_overrides = class e
template: searchProvider.search_url,
iconURL: searchProvider.favicon_url,
alias: searchProvider.keyword,
extensionID: extension.id,
suggestURL: searchProvider.suggest_url,
queryCharset: "UTF-8",
};
Services.search.addEngineWithDetails(searchProvider.name.trim(), params);
+ await ExtensionSettingsStore.addSetting(
+ extension.id, DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME,
+ searchProvider.name.trim());
if (extension.startupReason === "ADDON_UPGRADE") {
let engine = Services.search.getEngineByName(searchProvider.name.trim());
if (isCurrent) {
Services.search.currentEngine = engine;
}
if (index != -1) {
Services.search.moveEngine(engine, index);
}
--- a/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js
+++ b/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js
@@ -24,20 +24,26 @@ function awaitEvent(eventName, id) {
resolve(...args);
}
};
Management.on(eventName, listener);
});
}
+let defaultEngineName = Services.search.currentEngine.name;
+
+function restoreDefaultEngine() {
+ let engine = Services.search.getEngineByName(defaultEngineName);
+ Services.search.currentEngine = engine;
+}
+registerCleanupFunction(restoreDefaultEngine);
+
/* This tests setting a default engine. */
add_task(async function test_extension_setting_default_engine() {
- let defaultEngineName = Services.search.currentEngine.name;
-
let ext1 = ExtensionTestUtils.loadExtension({
manifest: {
"chrome_settings_overrides": {
"search_provider": {
"name": "DuckDuckGo",
"search_url": "https://example.com/?q={searchTerms}",
"is_default": true,
},
@@ -53,17 +59,16 @@ add_task(async function test_extension_s
await ext1.unload();
is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`);
});
/* This tests that uninstalling add-ons maintains the proper
* search default. */
add_task(async function test_extension_setting_multiple_default_engine() {
- let defaultEngineName = Services.search.currentEngine.name;
let ext1 = ExtensionTestUtils.loadExtension({
manifest: {
"chrome_settings_overrides": {
"search_provider": {
"name": "DuckDuckGo",
"search_url": "https://example.com/?q={searchTerms}",
"is_default": true,
},
@@ -100,17 +105,16 @@ add_task(async function test_extension_s
await ext1.unload();
is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`);
});
/* This tests that uninstalling add-ons in reverse order maintains the proper
* search default. */
add_task(async function test_extension_setting_multiple_default_engine_reversed() {
- let defaultEngineName = Services.search.currentEngine.name;
let ext1 = ExtensionTestUtils.loadExtension({
manifest: {
"chrome_settings_overrides": {
"search_provider": {
"name": "DuckDuckGo",
"search_url": "https://example.com/?q={searchTerms}",
"is_default": true,
},
@@ -170,16 +174,17 @@ add_task(async function test_user_changi
is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo");
let engine = Services.search.getEngineByName("Twitter");
Services.search.currentEngine = engine;
await ext1.unload();
is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter");
+ restoreDefaultEngine();
});
/* This tests that when the user changes the search engine while it is
* disabled, user choice is maintained when the add-on is reenabled. */
add_task(async function test_user_change_with_disabling() {
let ext1 = ExtensionTestUtils.loadExtension({
manifest: {
applications: {
@@ -215,23 +220,23 @@ add_task(async function test_user_change
is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter");
let enabledPromise = awaitEvent("ready", EXTENSION1_ID);
addon.userDisabled = false;
await enabledPromise;
is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter");
await ext1.unload();
+ restoreDefaultEngine();
});
/* This tests that when two add-ons are installed that change default
* search and the first one is disabled, before the second one is installed,
* when the first one is reenabled, the second add-on keeps the search. */
add_task(async function test_two_addons_with_first_disabled_before_second() {
- let defaultEngineName = Services.search.currentEngine.name;
let ext1 = ExtensionTestUtils.loadExtension({
manifest: {
applications: {
gecko: {
id: EXTENSION1_ID,
},
},
"chrome_settings_overrides": {
@@ -290,17 +295,16 @@ add_task(async function test_two_addons_
is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`);
});
/* This tests that when two add-ons are installed that change default
* search and the first one is disabled, the second one maintains
* the search. */
add_task(async function test_two_addons_with_first_disabled() {
- let defaultEngineName = Services.search.currentEngine.name;
let ext1 = ExtensionTestUtils.loadExtension({
manifest: {
applications: {
gecko: {
id: EXTENSION1_ID,
},
},
"chrome_settings_overrides": {
@@ -359,17 +363,16 @@ add_task(async function test_two_addons_
is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`);
});
/* This tests that when two add-ons are installed that change default
* search and the second one is disabled, the first one properly
* gets the search. */
add_task(async function test_two_addons_with_second_disabled() {
- let defaultEngineName = Services.search.currentEngine.name;
let ext1 = ExtensionTestUtils.loadExtension({
manifest: {
applications: {
gecko: {
id: EXTENSION1_ID,
},
},
"chrome_settings_overrides": {
--- a/browser/components/extensions/test/browser/browser_ext_settings_overrides_search.js
+++ b/browser/components/extensions/test/browser/browser_ext_settings_overrides_search.js
@@ -122,9 +122,8 @@ add_task(async function test_upgrade_def
is(Services.search.getEngines().indexOf(engine), 1, "Engine is in position 1");
await ext2.unload();
await ext1.unload();
engine = Services.search.getEngineByName("MozSearch");
ok(!engine, "Engine should not exist");
});
-
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
@@ -0,0 +1,102 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://testing-common/AddonTestUtils.jsm");
+
+const {
+ createAppInfo,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+add_task(async function test_overrides_update_removal() {
+ /* This tests the scenario where the manifest key for homepage and/or
+ * search_provider are removed between updates and therefore the
+ * settings are expected to revert. */
+
+ const EXTENSION_ID = "test_overrides_update@tests.mozilla.org";
+ const HOMEPAGE_URI = "webext-homepage-1.html";
+
+ const HOMEPAGE_URL_PREF = "browser.startup.homepage";
+
+ const getHomePageURL = () => {
+ return Services.prefs.getComplexValue(
+ HOMEPAGE_URL_PREF, Ci.nsIPrefLocalizedString).data;
+ };
+
+ function promisePrefChanged(value) {
+ return new Promise((resolve, reject) => {
+ Services.prefs.addObserver(HOMEPAGE_URL_PREF, function observer() {
+ if (getHomePageURL().endsWith(value)) {
+ Services.prefs.removeObserver(HOMEPAGE_URL_PREF, observer);
+ resolve();
+ }
+ });
+ });
+ }
+
+ await promiseStartupManager();
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ },
+ },
+ "chrome_settings_overrides": {
+ "homepage": HOMEPAGE_URI,
+ "search_provider": {
+ "name": "DuckDuckGo",
+ "search_url": "https://example.com/?q={searchTerms}",
+ "is_default": true,
+ },
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let defaultHomepageURL = getHomePageURL();
+ let defaultEngineName = Services.search.currentEngine.name;
+
+ let prefPromise = promisePrefChanged(HOMEPAGE_URI);
+ await extension.startup();
+ await prefPromise;
+
+ equal(extension.version, "1.0", "The installed addon has the expected version.");
+ ok(getHomePageURL().endsWith(HOMEPAGE_URI),
+ "Home page url is overriden by the extension.");
+ equal(Services.search.currentEngine.name, "DuckDuckGo",
+ "Default engine is overriden by the extension");
+
+ extensionInfo.manifest = {
+ "version": "2.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ },
+ },
+ };
+
+ prefPromise = promisePrefChanged(defaultHomepageURL);
+ await extension.upgrade(extensionInfo);
+ await prefPromise;
+
+ equal(extension.version, "2.0", "The updated addon has the expected version.");
+ equal(getHomePageURL(), defaultHomepageURL,
+ "Home page url reverted to the default after update.");
+ equal(Services.search.currentEngine.name, defaultEngineName,
+ "Default engine reverted to the default after update.");
+
+ await extension.unload();
+
+ await promiseShutdownManager();
+});
--- a/browser/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/browser/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -1,11 +1,12 @@
[test_ext_bookmarks.js]
[test_ext_browsingData.js]
[test_ext_browsingData_cookies_cache.js]
[test_ext_browsingData_downloads.js]
[test_ext_browsingData_passwords.js]
[test_ext_browsingData_settings.js]
+[test_ext_chrome_settings_overrides_update.js]
[test_ext_geckoProfiler_control.js]
[test_ext_history.js]
[test_ext_url_overrides_newtab.js]
[test_ext_url_overrides_newtab_update.js]
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -968,17 +968,17 @@ const shutdownPromises = new Map();
class BootstrapScope {
install(data, reason) {}
uninstall(data, reason) {
Management.emit("uninstall", {id: data.id});
}
update(data, reason) {
- Management.emit("update", {id: data.id});
+ Management.emit("update", {id: data.id, resourceURI: data.resourceURI});
}
startup(data, reason) {
this.extension = new Extension(data, this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
return this.extension.startup();
}
shutdown(data, reason) {
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -18,16 +18,17 @@ this.EXPORTED_SYMBOLS = ["ExtensionParen
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.jsm",
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
DeferredTask: "resource://gre/modules/DeferredTask.jsm",
E10SUtils: "resource:///modules/E10SUtils.jsm",
+ ExtensionData: "resource://gre/modules/Extension.jsm",
MessageChannel: "resource://gre/modules/MessageChannel.jsm",
OS: "resource://gre/modules/osfile.jsm",
NativeApp: "resource://gre/modules/NativeMessaging.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
Schemas: "resource://gre/modules/Schemas.jsm",
});
XPCOMUtils.defineLazyServiceGetters(this, {
@@ -72,28 +73,53 @@ let StartupCache;
const global = this;
// This object loads the ext-*.js scripts that define the extension API.
let apiManager = new class extends SchemaAPIManager {
constructor() {
super("main");
this.initialized = null;
- this.on("startup", (event, extension) => { // eslint-disable-line mozilla/balanced-listeners
+ /* eslint-disable mozilla/balanced-listeners */
+ this.on("startup", (e, extension) => {
let promises = [];
for (let apiName of this.eventModules.get("startup")) {
promises.push(this.asyncGetAPI(apiName, extension).then(api => {
if (api) {
- api.onStartup(extension.startupReason);
+ api.onStartup();
}
}));
}
return Promise.all(promises);
});
+
+ this.on("update", async (e, {id, resourceURI}) => {
+ let modules = this.eventModules.get("update");
+ if (modules.size == 0) {
+ return;
+ }
+
+ let extension = new ExtensionData(resourceURI);
+ await extension.loadManifest();
+
+ return Promise.all(Array.from(modules).map(async apiName => {
+ let module = await this.asyncLoadModule(apiName);
+ module.onUpdate(id, extension.manifest);
+ }));
+ });
+
+ this.on("uninstall", (e, {id}) => {
+ let modules = this.eventModules.get("uninstall");
+ return Promise.all(Array.from(modules).map(async apiName => {
+ let module = await this.asyncLoadModule(apiName);
+ module.onUninstall(id);
+ }));
+ });
+ /* eslint-enable mozilla/balanced-listeners */
}
getModuleJSONURLs() {
return Array.from(XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_MODULES),
([name, url]) => url);
}
// Loads all the ext-*.js scripts currently registered.
--- a/toolkit/components/extensions/ExtensionSettingsStore.jsm
+++ b/toolkit/components/extensions/ExtensionSettingsStore.jsm
@@ -105,26 +105,49 @@ function ensureType(type) {
}
// Ensure a property exists for the given type.
if (!_store.data[type]) {
_store.data[type] = {};
}
}
-// Return an object with properties for key, value|initialValue, id|null, or
-// null if no setting has been stored for that key.
-function getTopItem(type, key) {
+/**
+ * Return an object with properties for key, value|initialValue, id|null, or
+ * null if no setting has been stored for that key.
+ *
+ * If no id is passed then return the highest priority item for the key.
+ *
+ * @param {string} type
+ * The type of setting to be retrieved.
+ * @param {string} key
+ * A string that uniquely identifies the setting.
+ * @param {string} id
+ * The id of the extension for which the item is being retrieved.
+ * If no id is passed, then the highest priority item for the key
+ * is returned.
+ *
+ * @returns {object | null}
+ * Either an object with properties for key and value, or
+ * null if no key is found.
+ */
+function getItem(type, key, id) {
ensureType(type);
let keyInfo = _store.data[type][key];
if (!keyInfo) {
return null;
}
+ if (id) {
+ // Return the item that corresponds to the extension with id of id.
+ let item = keyInfo.precedenceList.find(item => item.id === id);
+ return item ? {key, value: item.value, id} : null;
+ }
+
// Find the highest precedence, enabled setting.
for (let item of keyInfo.precedenceList) {
if (item.enabled) {
return {key, value: item.value, id: item.id};
}
}
// Nothing found in the precedenceList, return the initialValue.
@@ -200,17 +223,17 @@ function alterSetting(id, type, key, act
keyInfo.precedenceList.sort(precedenceComparator);
break;
default:
throw new Error(`${action} is not a valid action for alterSetting.`);
}
if (foundIndex === 0) {
- returnItem = getTopItem(type, key);
+ returnItem = getItem(type, key);
}
if (action === "remove" && keyInfo.precedenceList.length === 0) {
delete _store.data[type][key];
}
_store.saveSoon();
@@ -238,30 +261,30 @@ this.ExtensionSettingsStore = {
* The id of 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
+ * A function to be called to determine the initial value for the
* setting. This will be passed the value in the callbackArgument
- * argument.
+ * argument. If omitted the initial value will be undefined.
* @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(id, type, key, value, initialValueCallback, callbackArgument = key) {
+ async addSetting(id, type, key, value, initialValueCallback = () => undefined, callbackArgument = key) {
if (typeof initialValueCallback != "function") {
throw new Error("initialValueCallback must be a function.");
}
ensureType(type);
if (!_store.data[type][key]) {
// The setting for this key does not exist. Set the initial value.
@@ -397,26 +420,29 @@ this.ExtensionSettingsStore = {
if (keysObj[key].precedenceList.find(item => item.id == id)) {
items.push(key);
}
}
return items;
},
/**
- * Retrieves a setting from the store, returning the current top precedent
- * setting for the key.
+ * Retrieves a setting from the store, either for a specific extension,
+ * or 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.
+ * @param {string} id
+ * The id of the extension for which the setting is being retrieved.
+ * Defaults to undefined, in which case the top setting is returned.
*
- * @returns {object} An object with properties for key and value.
+ * @returns {object} An object with properties for key, value and id.
*/
- getSetting(type, key) {
- return getTopItem(type, key);
+ getSetting(type, key, id) {
+ return getItem(type, key, id);
},
/**
* Returns whether an extension currently has a stored setting for a given
* key.
*
* @param {string} id The id of the extension which is being checked.
* @param {string} type The type of setting to be checked.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
@@ -109,16 +109,21 @@ add_task(async function test_settings_st
"getSetting returns correct item with only one item in the list.");
levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex].id, TEST_TYPE, key);
equal(
levelOfControl,
"controlled_by_this_extension",
"getLevelOfControl returns correct levelOfControl with only one item in the list.");
ok(ExtensionSettingsStore.hasSetting(extensions[extensionIndex].id, TEST_TYPE, key),
"hasSetting returns the correct value when an extension has a setting set.");
+ item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key, extensions[extensionIndex].id);
+ deepEqual(
+ item,
+ itemToAdd,
+ "getSetting with id returns correct item with only one item in the list.");
}
// Add a setting for the oldest extension.
for (let key of KEY_LIST) {
let extensionIndex = 0;
let itemToAdd = ITEMS[key][extensionIndex];
let item = await ExtensionSettingsStore.addSetting(
extensions[extensionIndex].id, TEST_TYPE, itemToAdd.key, itemToAdd.value, initialValue);
@@ -131,16 +136,21 @@ add_task(async function test_settings_st
item,
ITEMS[key][1],
"getSetting returns correct item with more than one item in the list.");
let levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex].id, TEST_TYPE, key);
equal(
levelOfControl,
"controlled_by_other_extensions",
"getLevelOfControl returns correct levelOfControl when another extension is in control.");
+ item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key, extensions[extensionIndex].id);
+ deepEqual(
+ item,
+ itemToAdd,
+ "getSetting with id returns correct item with more than one item in the list.");
}
// Reload the settings store to emulate a browser restart.
await ExtensionSettingsStore._reloadFile();
// Add a setting for the newest extension.
for (let key of KEY_LIST) {
let extensionIndex = 2;
@@ -154,23 +164,28 @@ add_task(async function test_settings_st
extensions[extensionIndex].id, TEST_TYPE, itemToAdd.key, itemToAdd.value, initialValue);
equal(callbackCount,
expectedCallbackCount,
"initialValueCallback called the expected number of times.");
deepEqual(item, itemToAdd, "Adding item for most recent extension returns that item.");
item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key);
deepEqual(
item,
- ITEMS[key][2],
+ itemToAdd,
"getSetting returns correct item with more than one item in the list.");
levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[extensionIndex].id, TEST_TYPE, key);
equal(
levelOfControl,
"controlled_by_this_extension",
"getLevelOfControl returns correct levelOfControl when this extension is in control.");
+ item = await ExtensionSettingsStore.getSetting(TEST_TYPE, key, extensions[extensionIndex].id);
+ deepEqual(
+ item,
+ itemToAdd,
+ "getSetting with id returns correct item with more than one item in the list.");
}
for (let extension of extensions) {
let items = await ExtensionSettingsStore.getAllForExtension(extension.id, TEST_TYPE);
deepEqual(items, KEY_LIST, "getAllForExtension returns expected keys.");
}
// Attempting to remove a setting that has not been set should *not* throw an exception.