--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -1,21 +1,16 @@
/* 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/. */
/* import-globals-from preferences.js */
/* import-globals-from ../../../../toolkit/mozapps/preferences/fontbuilder.js */
/* import-globals-from ../../../base/content/aboutDialog-appUpdater.js */
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
- "resource://gre/modules/ExtensionSettingsStore.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
- "resource://gre/modules/AddonManager.jsm");
-
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/Downloads.jsm");
Components.utils.import("resource://gre/modules/FileUtils.jsm");
Components.utils.import("resource:///modules/ShellService.jsm");
Components.utils.import("resource:///modules/TransientPrefs.jsm");
Components.utils.import("resource://gre/modules/AppConstants.jsm");
Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
Components.utils.import("resource://gre/modules/LoadContextInfo.jsm");
@@ -36,16 +31,23 @@ const PREF_DISABLED_PLUGIN_TYPES = "plug
// Pref for when containers is being controlled
const PREF_CONTAINERS_EXTENSION = "privacy.userContext.extension";
// Preferences that affect which entries to show in the list.
const PREF_SHOW_PLUGINS_IN_LIST = "browser.download.show_plugins_in_list";
const PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS =
"browser.download.hide_plugins_without_extensions";
+// Strings to identify ExtensionSettingsStore overrides
+const PREF_SETTING_TYPE = "prefs";
+const CONTAINERS_KEY = "privacy.containers";
+const HOMEPAGE_OVERRIDE_KEY = "homepage_override";
+const URL_OVERRIDES_TYPE = "url_overrides";
+const NEW_TAB_KEY = "newTabURL";
+
/*
* Preferences where we store handling information about the feed type.
*
* browser.feeds.handler
* - "bookmarks", "reader" (clarified further using the .default preference),
* or "ask" -- indicates the default handler being used to process feeds;
* "bookmarks" is obsolete; to specify that the handler is bookmarks,
* set browser.feeds.handler.default to "bookmarks";
@@ -213,20 +215,20 @@ var gMainPane = {
this.updatePerformanceSettingsBox({ duringChangeEvent: false });
// set up the "use current page" label-changing listener
this._updateUseCurrentButton();
window.addEventListener("focus", this._updateUseCurrentButton.bind(this));
this.updateBrowserStartupLastSession();
- handleControllingExtension("url_overrides", "newTabURL");
+ handleControllingExtension(URL_OVERRIDES_TYPE, NEW_TAB_KEY);
let newTabObserver = {
observe(subject, topic, data) {
- handleControllingExtension("url_overrides", "newTabURL");
+ handleControllingExtension(URL_OVERRIDES_TYPE, NEW_TAB_KEY);
},
};
Services.obs.addObserver(newTabObserver, "newtab-url-changed");
window.addEventListener("unload", () => {
Services.obs.removeObserver(newTabObserver, "newtab-url-changed");
});
if (AppConstants.platform == "win") {
@@ -255,21 +257,21 @@ var gMainPane = {
}
setEventListener("useCurrent", "command",
gMainPane.setHomePageToCurrent);
setEventListener("useBookmark", "command",
gMainPane.setHomePageToBookmark);
setEventListener("restoreDefaultHomePage", "command",
gMainPane.restoreDefaultHomePage);
setEventListener("disableHomePageExtension", "command",
- gMainPane.makeDisableControllingExtension("prefs", "homepage_override"));
+ makeDisableControllingExtension(PREF_SETTING_TYPE, HOMEPAGE_OVERRIDE_KEY));
setEventListener("disableContainersExtension", "command",
- gMainPane.makeDisableControllingExtension("prefs", "privacy.containers"));
+ makeDisableControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY));
setEventListener("disableNewTabExtension", "command",
- gMainPane.makeDisableControllingExtension("url_overrides", "newTabURL"));
+ makeDisableControllingExtension(URL_OVERRIDES_TYPE, NEW_TAB_KEY));
setEventListener("chooseLanguage", "command",
gMainPane.showLanguages);
setEventListener("translationAttributionImage", "click",
gMainPane.openTranslationProviderAttribution);
setEventListener("translateButton", "command",
gMainPane.showTranslationExceptions);
setEventListener("font.language.group", "change",
gMainPane._rebuildFonts);
@@ -479,17 +481,17 @@ var gMainPane = {
readBrowserContainersCheckbox() {
const pref = document.getElementById("privacy.userContext.enabled");
const settings = document.getElementById("browserContainersSettings");
settings.disabled = !pref.value;
const containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
const containersCheckbox = document.getElementById("browserContainersCheckbox");
containersCheckbox.checked = containersEnabled;
- handleControllingExtension("prefs", "privacy.containers")
+ handleControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY)
.then((isControlled) => {
containersCheckbox.disabled = isControlled;
});
},
/**
* Show the Containers UI depending on the privacy.userContext.ui.enabled pref.
*/
@@ -611,21 +613,21 @@ var gMainPane = {
.forEach((element) => {
let isLocked = document.getElementById(element.getAttribute("preference")).locked;
element.disabled = isLocked || isControlled;
});
}
if (homePref.locked) {
// An extension can't control these settings if they're locked.
- hideControllingExtension("homepage_override");
+ hideControllingExtension(HOMEPAGE_OVERRIDE_KEY);
setInputDisabledStates(false);
} else {
// Asynchronously update the extension controlled UI.
- handleControllingExtension("prefs", "homepage_override")
+ handleControllingExtension(PREF_SETTING_TYPE, HOMEPAGE_OVERRIDE_KEY)
.then(setInputDisabledStates);
}
// If the pref is set to about:home or about:newtab, set the value to ""
// to show the placeholder text (about:home title) rather than
// exposing those URLs to users.
let defaultBranch = Services.prefs.getDefaultBranch("");
let defaultValue = defaultBranch.getComplexValue("browser.startup.homepage",
@@ -724,17 +726,17 @@ var gMainPane = {
let tabs = this._getTabsForHomePage();
if (tabs.length > 1)
useCurrent.label = useCurrent.getAttribute("label2");
else
useCurrent.label = useCurrent.getAttribute("label1");
// If the homepage is controlled by an extension then you can't use this.
- if (await getControllingExtensionId("prefs", "homepage_override")) {
+ if (await getControllingExtensionInfo(PREF_SETTING_TYPE, HOMEPAGE_OVERRIDE_KEY)) {
useCurrent.disabled = true;
return;
}
// In this case, the button's disabled state is set by preferences.xml.
let prefName = "pref.browser.homepage.disable_button.current_page";
if (document.getElementById(prefName).locked)
return;
@@ -770,24 +772,16 @@ var gMainPane = {
/**
* Restores the default home page as the user's home page.
*/
restoreDefaultHomePage() {
var homePage = document.getElementById("browser.startup.homepage");
homePage.value = homePage.defaultValue;
},
- makeDisableControllingExtension(type, settingName) {
- return async function disableExtension() {
- let id = await getControllingExtensionId(type, settingName);
- let addon = await AddonManager.getAddonByID(id);
- addon.userDisabled = true;
- };
- },
-
/**
* Utility function to enable/disable the button specified by aButtonID based
* on the value of the Boolean preference specified by aPreferenceID.
*/
updateButtons(aButtonID, aPreferenceID) {
var button = document.getElementById(aButtonID);
var preference = document.getElementById(aPreferenceID);
button.disabled = preference.value != true;
@@ -2584,85 +2578,16 @@ function getLocalHandlerApp(aFile) {
var localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"].
createInstance(Ci.nsILocalHandlerApp);
localHandlerApp.name = getFileDisplayName(aFile);
localHandlerApp.executable = aFile;
return localHandlerApp;
}
-let extensionControlledContentIds = {
- "privacy.containers": "browserContainersExtensionContent",
- "homepage_override": "browserHomePageExtensionContent",
- "newTabURL": "browserNewTabExtensionContent",
-};
-
-/**
- * Check if a pref is being managed by an extension.
- */
-async function getControllingExtensionId(type, settingName) {
- await ExtensionSettingsStore.initialize();
- return ExtensionSettingsStore.getTopExtensionId(type, settingName);
-}
-
-function getControllingExtensionEl(settingName) {
- return document.getElementById(extensionControlledContentIds[settingName]);
-}
-
-async function handleControllingExtension(type, settingName) {
- let controllingExtensionId = await getControllingExtensionId(type, settingName);
- let addon = controllingExtensionId
- && await AddonManager.getAddonByID(controllingExtensionId);
-
- // Sometimes the ExtensionSettingsStore gets in a bad state where it thinks
- // an extension is controlling a setting but the extension has been uninstalled
- // outside of the regular lifecycle. If the extension isn't currently installed
- // then we should treat the setting as not being controlled.
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example.
- if (addon) {
- showControllingExtension(settingName, addon);
- } else {
- hideControllingExtension(settingName);
- }
-
- return !!addon;
-}
-
-async function showControllingExtension(settingName, addon) {
- // Tell the user what extension is controlling the setting.
- let extensionControlledContent = getControllingExtensionEl(settingName);
- const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
- let stringParts = document
- .getElementById("bundlePreferences")
- .getString(`extensionControlled.${settingName}`)
- .split("%S");
- let description = extensionControlledContent.querySelector("description");
-
- // Remove the old content from the description.
- while (description.firstChild) {
- description.firstChild.remove();
- }
-
- // Populate the description.
- description.appendChild(document.createTextNode(stringParts[0]));
- let image = document.createElement("image");
- image.setAttribute("src", addon.iconURL || defaultIcon);
- image.classList.add("extension-controlled-icon");
- description.appendChild(image);
- description.appendChild(document.createTextNode(` ${addon.name}`));
- description.appendChild(document.createTextNode(stringParts[1]));
-
- // Show the controlling extension row and hide the old label.
- extensionControlledContent.hidden = false;
-}
-
-function hideControllingExtension(settingName) {
- getControllingExtensionEl(settingName).hidden = true;
-}
-
/**
* An enumeration of items in a JS array.
*
* FIXME: use ArrayConverter once it lands (bug 380839).
*
* @constructor
*/
function ArrayEnumerator(aItems) {
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -18,16 +18,21 @@ var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
+ "resource://gre/modules/ExtensionSettingsStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
var gLastHash = "";
var gCategoryInits = new Map();
function init_category_if_required(category) {
let categoryInfo = gCategoryInits.get(category);
if (!categoryInfo) {
throw "Unknown in-content prefs category! Can't init " + category;
}
@@ -317,8 +322,93 @@ function confirmRestartPrompt(aRestartTo
function appendSearchKeywords(aId, keywords) {
let element = document.getElementById(aId);
let searchKeywords = element.getAttribute("searchkeywords");
if (searchKeywords) {
keywords.push(searchKeywords);
}
element.setAttribute("searchkeywords", keywords.join(" "));
}
+
+let extensionControlledContentIds = {
+ "privacy.containers": "browserContainersExtensionContent",
+ "homepage_override": "browserHomePageExtensionContent",
+ "newTabURL": "browserNewTabExtensionContent",
+ "defaultSearch": "browserDefaultSearchExtensionContent",
+};
+
+/**
+ * Check if a pref is being managed by an extension.
+ */
+async function getControllingExtensionInfo(type, settingName) {
+ await ExtensionSettingsStore.initialize();
+ return ExtensionSettingsStore.getSetting(type, settingName);
+}
+
+function getControllingExtensionEl(settingName) {
+ return document.getElementById(extensionControlledContentIds[settingName]);
+}
+
+async function handleControllingExtension(type, settingName) {
+ let info = await getControllingExtensionInfo(type, settingName);
+ let addon = info && info.id
+ && await AddonManager.getAddonByID(info.id);
+
+ // Sometimes the ExtensionSettingsStore gets in a bad state where it thinks
+ // an extension is controlling a setting but the extension has been uninstalled
+ // outside of the regular lifecycle. If the extension isn't currently installed
+ // then we should treat the setting as not being controlled.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example.
+ if (addon) {
+ showControllingExtension(settingName, addon);
+ } else {
+ hideControllingExtension(settingName);
+ }
+
+ return !!addon;
+}
+
+async function showControllingExtension(settingName, addon) {
+ // Tell the user what extension is controlling the setting.
+ let extensionControlledContent = getControllingExtensionEl(settingName);
+ extensionControlledContent.classList.remove("extension-controlled-disabled");
+ const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ let stringParts = document
+ .getElementById("bundlePreferences")
+ .getString(`extensionControlled.${settingName}`)
+ .split("%S");
+ let description = extensionControlledContent.querySelector("description");
+
+ // Remove the old content from the description.
+ while (description.firstChild) {
+ description.firstChild.remove();
+ }
+
+ // Populate the description.
+ description.appendChild(document.createTextNode(stringParts[0]));
+ let image = document.createElement("image");
+ image.setAttribute("src", addon.iconURL || defaultIcon);
+ image.classList.add("extension-controlled-icon");
+ description.appendChild(image);
+ description.appendChild(document.createTextNode(` ${addon.name}`));
+ description.appendChild(document.createTextNode(stringParts[1]));
+
+ let disableButton = extensionControlledContent.querySelector("button");
+ if (disableButton) {
+ disableButton.hidden = false;
+ }
+
+ // Show the controlling extension row and hide the old label.
+ extensionControlledContent.hidden = false;
+}
+
+function hideControllingExtension(settingName) {
+ getControllingExtensionEl(settingName).hidden = true;
+}
+
+
+function makeDisableControllingExtension(type, settingName) {
+ return async function disableExtension() {
+ let {id} = await getControllingExtensionInfo(type, settingName);
+ let addon = await AddonManager.getAddonByID(id);
+ addon.userDisabled = true;
+ };
+}
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -2,18 +2,22 @@
* 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/. */
/* import-globals-from preferences.js */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
+ "resource://gre/modules/ExtensionSettingsStore.jsm");
const ENGINE_FLAVOR = "text/x-moz-search-engine";
+const SEARCH_TYPE = "default_search";
+const SEARCH_KEY = "defaultSearch";
var gEngineView = null;
var gSearchPane = {
/**
* Initialize autocomplete to ensure prefs are in sync.
*/
@@ -92,16 +96,27 @@ var gSearchPane = {
item.setAttribute("class", "menuitem-iconic searchengine-menuitem menuitem-with-favicon");
if (e.iconURI) {
item.setAttribute("image", e.iconURI.spec);
}
item.engine = e;
if (e.name == currentEngine)
list.selectedItem = item;
});
+
+ handleControllingExtension(SEARCH_TYPE, SEARCH_KEY);
+ let searchEngineListener = {
+ observe(subject, topic, data) {
+ handleControllingExtension(SEARCH_TYPE, SEARCH_KEY);
+ },
+ };
+ Services.obs.addObserver(searchEngineListener, "browser-search-engine-modified");
+ window.addEventListener("unload", () => {
+ Services.obs.removeObserver(searchEngineListener, "browser-search-engine-modified");
+ });
},
handleEvent(aEvent) {
switch (aEvent.type) {
case "click":
if (aEvent.target.id != "engineChildren" &&
!aEvent.target.classList.contains("searchEngineAction")) {
let engineList = document.getElementById("engineList");
@@ -302,16 +317,17 @@ var gSearchPane = {
}
document.getElementById("browser.search.hiddenOneOffs").value =
hiddenList.join(",");
},
setDefaultEngine() {
Services.search.currentEngine =
document.getElementById("defaultEngine").selectedItem.engine;
+ ExtensionSettingsStore.setByUser(SEARCH_TYPE, SEARCH_KEY);
}
};
function onDragEngineStart(event) {
var selectedIndex = gEngineView.selectedIndex;
var tree = document.getElementById("engineList");
var row = { }, col = { }, child = { };
tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, child);
--- a/browser/components/preferences/in-content/search.xul
+++ b/browser/components/preferences/in-content/search.xul
@@ -39,24 +39,30 @@
<image class="searchBarImage searchBarShownImage" role="presentation"/>
</radiogroup>
</groupbox>
<!-- Default Search Engine -->
<groupbox id="defaultEngineGroup" data-category="paneSearch">
<caption><label>&defaultSearchEngine.label;</label></caption>
<description>&chooseYourDefaultSearchEngine2.label;</description>
+
+ <hbox id="browserDefaultSearchExtensionContent" align="center" hidden="true">
+ <description control="disableDefaultSearchExtension" flex="1"/>
+ </hbox>
+
<hbox>
<!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
<hbox>
<menulist id="defaultEngine">
<menupopup/>
</menulist>
</hbox>
</hbox>
+
<checkbox id="suggestionsInSearchFieldsCheckbox"
label="&provideSearchSuggestions.label;"
accesskey="&provideSearchSuggestions.accesskey;"
preference="browser.search.suggest.enabled"/>
<vbox class="indent">
<checkbox id="urlBarSuggestion" label="&showURLBarSuggestions2.label;"
accesskey="&showURLBarSuggestions2.accesskey;"
preference="browser.urlbar.suggest.searches"/>
--- a/browser/components/preferences/in-content/tests/browser_extension_controlled.js
+++ b/browser/components/preferences/in-content/tests/browser_extension_controlled.js
@@ -270,23 +270,118 @@ add_task(async function testExtensionCon
is(controlledContent.hidden, false, "The extension controlled row is hidden");
// Disable the extension.
doc.getElementById("disableNewTabExtension").click();
await waitForMessageHidden("browserNewTabExtensionContent");
ok(!aboutNewTabService.newTabURL.startsWith("moz-extension:"), "new tab page is set back to default");
- is(controlledContent.hidden, true, "The extension controlled row is hidden");
+ is(controlledContent.hidden, true, "The extension controlled row is shown");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
let addon = await AddonManager.getAddonByID("@set_newtab");
addon.uninstall();
});
+add_task(async function testExtensionControlledDefaultSearch() {
+ await openPreferencesViaOpenPreferencesAPI("paneSearch", {leaveOpen: true});
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ let doc = gBrowser.contentDocument;
+ let extensionId = "@set_default_search";
+ let manifest = {
+ manifest_version: 2,
+ name: "set_default_search",
+ applications: {gecko: {id: extensionId}},
+ description: "set_default_search description",
+ permissions: [],
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Yahoo",
+ search_url: "https://search.yahoo.com/yhs/search?p=%s&ei=UTF-8&hspart=mozilla&hsimp=yhs-002",
+ is_default: true,
+ },
+ }
+ };
+
+ function setEngine(engine) {
+ doc.querySelector(`#defaultEngine menuitem[label="${engine.name}"]`)
+ .doCommand();
+ }
+
+ is(gBrowser.currentURI.spec, "about:preferences#search",
+ "#search should be in the URI for about:preferences");
+
+ let controlledContent = doc.getElementById("browserDefaultSearchExtensionContent");
+ let initialEngine = Services.search.currentEngine;
+
+ // Ensure the controlled content is hidden when not controlled.
+ is(controlledContent.hidden, true, "The extension controlled row is hidden");
+
+ // Install an extension that will set the default search engine.
+ let originalExtension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: Object.assign({}, manifest, {version: "1.0"}),
+ });
+
+ let messageShown = waitForMessageShown("browserDefaultSearchExtensionContent");
+ await originalExtension.startup();
+ await messageShown;
+
+ let addon = await AddonManager.getAddonByID(extensionId);
+ is(addon.version, "1.0", "The addon has the expected version.");
+
+ // The default search engine has been set by the extension and the user is notified.
+ let controlledLabel = controlledContent.querySelector("description");
+ let extensionEngine = Services.search.currentEngine;
+ ok(initialEngine != extensionEngine, "The default engine has changed.");
+ // There are two spaces before "set_default_search" because it's " <image /> set_default_search".
+ is(controlledLabel.textContent,
+ "An extension, set_default_search, has set your default search engine.",
+ "The user is notified that an extension is controlling the default search engine");
+ is(controlledContent.hidden, false, "The extension controlled row is shown");
+
+ // Set the engine back to the initial one, ensure the message is hidden.
+ setEngine(initialEngine);
+ await waitForMessageHidden(controlledContent.id);
+
+ is(initialEngine, Services.search.currentEngine,
+ "default search engine is set back to default");
+ is(controlledContent.hidden, true, "The extension controlled row is hidden");
+
+ // Setting the engine back to the extension's engine does not show the message.
+ setEngine(extensionEngine);
+
+ is(extensionEngine, Services.search.currentEngine,
+ "default search engine is set back to extension");
+ is(controlledContent.hidden, true, "The extension controlled row is still hidden");
+
+ // Set the engine to the initial one and verify an upgrade doesn't change it.
+ setEngine(initialEngine);
+ await waitForMessageHidden(controlledContent.id);
+
+ // Update the extension and wait for "ready".
+ let updatedExtension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: Object.assign({}, manifest, {version: "2.0"}),
+ });
+ await updatedExtension.startup();
+ addon = await AddonManager.getAddonByID(extensionId);
+
+ // Verify the extension is updated and search engine didn't change.
+ is(addon.version, "2.0", "The updated addon has the expected version");
+ is(controlledContent.hidden, true, "The extension controlled row is hidden after update");
+ is(initialEngine, Services.search.currentEngine,
+ "default search engine is still the initial engine after update");
+
+ await originalExtension.unload();
+ await updatedExtension.unload();
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
add_task(async function testExtensionControlledHomepageUninstalledAddon() {
async function checkHomepageEnabled() {
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
// eslint-disable-next-line mozilla/no-cpows-in-tests
let doc = gBrowser.contentDocument;
is(gBrowser.currentURI.spec, "about:preferences#general",
"#general should be in the URI for about:preferences");
let controlledContent = doc.getElementById("browserHomePageExtensionContent");
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -271,12 +271,17 @@ defaultContentProcessCount=%S (default)
# LOCALIZATION NOTE (extensionControlled.homepage_override):
# This string is shown to notify the user that their home page is being controlled by an extension.
extensionControlled.homepage_override = An extension, %S, controls your home page.
# LOCALIZATION NOTE (extensionControlled.newTabURL):
# This string is shown to notify the user that their new tab page is being controlled by an extension.
extensionControlled.newTabURL = An extension, %S, controls your New Tab page.
+# LOCALIZATION NOTE (extensionControlled.defaultSearch):
+# This string is shown to notify the user that the default search engine is being controlled
+# by an extension. %S is the icon and name of the extension.
+extensionControlled.defaultSearch = An extension, %S, has set your default search engine.
+
# LOCALIZATION NOTE (extensionControlled.privacy.containers):
# This string is shown to notify the user that Container Tabs are being enabled by an extension
# %S is the container addon controlling it
extensionControlled.privacy.containers = An extension, %S, requires Container Tabs.
--- a/toolkit/components/extensions/ExtensionPreferencesManager.jsm
+++ b/toolkit/components/extensions/ExtensionPreferencesManager.jsm
@@ -177,29 +177,16 @@ this.ExtensionPreferencesManager = {
*
* @returns {string|number|boolean} The default value of the preference.
*/
getDefaultValue(prefName) {
return defaultPreferences.get(prefName);
},
/**
- * Gets the id of the extension controlling a preference or null if it isn't
- * being controlled.
- *
- * @param {string} prefName The name of the preference.
- *
- * @returns {Promise} Resolves to the id of the extension, or null.
- */
- async getControllingExtensionId(prefName) {
- await ExtensionSettingsStore.initialize();
- return ExtensionSettingsStore.getTopExtensionId(STORE_TYPE, 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
--- a/toolkit/components/extensions/ExtensionSettingsStore.jsm
+++ b/toolkit/components/extensions/ExtensionSettingsStore.jsm
@@ -354,16 +354,37 @@ this.ExtensionSettingsStore = {
* corresponds to the current top precedent setting, or null if
* the current top precedent setting has not changed.
*/
disable(extension, type, key) {
return alterSetting(extension, type, key, "disable");
},
/**
+ * Mark a setting as being controlled by a user's choice. This will disable all of
+ * the extension defined values for the extension.
+ *
+ * @param {string} type The type of the setting.
+ * @param {string} key The key of the setting.
+ */
+ setByUser(type, key) {
+ let {precedenceList} = (_store.data[type] && _store.data[type][key]) || {};
+ if (!precedenceList) {
+ // The setting for this key does not exist. Nothing to do.
+ return;
+ }
+
+ for (let item of precedenceList) {
+ item.enabled = false;
+ }
+
+ _store.saveSoon();
+ },
+
+ /**
* 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.
*/
getAllForExtension(extension, type) {
@@ -449,26 +470,16 @@ this.ExtensionSettingsStore = {
}
let addon = await AddonManager.getAddonByID(id);
return topItem.installDate > addon.installDate.valueOf() ?
"controlled_by_other_extensions" :
"controllable_by_this_extension";
},
- // Return the id of the controlling extension or null if no extension is
- // controlling this setting.
- getTopExtensionId(type, key) {
- let item = getTopItem(type, key);
- if (item) {
- return item.id;
- }
- return null;
- },
-
/**
* Test-only method to force reloading of the JSON file.
*
* Note that this method simply clears the local variable that stores the
* file, so the next time the file is accessed it will be reloaded.
*
* @param {boolean} finalize
* When false, skip finalizing the store (writing current state to file).
--- a/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
@@ -428,16 +428,111 @@ add_task(async function test_settings_st
for (let extension of testExtensions) {
await extension.unload();
}
await promiseShutdownManager();
});
+add_task(async function test_settings_store_setByUser() {
+ await promiseStartupManager();
+
+ // Create an array of test framework extension wrappers to install.
+ let testExtensions = [
+ ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {gecko: {id: "@first"}},
+ },
+ }),
+ ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {gecko: {id: "@second"}},
+ },
+ }),
+ ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {gecko: {id: "@third"}},
+ },
+ }),
+ ];
+
+ let type = "some_type";
+ let key = "some_key";
+
+ for (let extension of testExtensions) {
+ await extension.startup();
+ }
+
+ // Create an array actual Extension objects which correspond to the
+ // test framework extension wrappers.
+ let [one, two, three] = testExtensions.map(extension => extension.extension);
+ let initialCallback = () => "initial";
+
+ // Initialize the SettingsStore.
+ await ExtensionSettingsStore.initialize();
+
+ equal(null, ExtensionSettingsStore.getSetting(type, key),
+ "getSetting is initially null");
+
+ let item = await ExtensionSettingsStore.addSetting(
+ one, type, key, "one", initialCallback);
+ deepEqual({key, value: "one", id: one.id}, item,
+ "addSetting returns the first set item");
+
+ item = await ExtensionSettingsStore.addSetting(
+ two, type, key, "two", initialCallback);
+ deepEqual({key, value: "two", id: two.id}, item,
+ "addSetting returns the second set item");
+
+ item = await ExtensionSettingsStore.addSetting(
+ three, type, key, "three", initialCallback);
+ deepEqual({key, value: "three", id: three.id}, item,
+ "addSetting returns the third set item");
+
+ deepEqual(item, ExtensionSettingsStore.getSetting(type, key),
+ "getSetting returns the third set item");
+
+ ExtensionSettingsStore.setByUser(type, key);
+ deepEqual({key, initialValue: "initial"}, ExtensionSettingsStore.getSetting(type, key),
+ "getSetting returns the initial value after being set by user");
+
+ item = ExtensionSettingsStore.enable(one, type, key);
+ deepEqual({key, value: "one", id: one.id}, item,
+ "enable returns the first set item after enable");
+
+ item = ExtensionSettingsStore.enable(three, type, key);
+ deepEqual({key, value: "three", id: three.id}, item,
+ "enable returns the third set item after enable");
+
+ item = ExtensionSettingsStore.enable(two, type, key);
+ deepEqual(undefined, item,
+ "enable returns undefined after enabling the second item");
+
+ item = ExtensionSettingsStore.getSetting(type, key);
+ deepEqual({key, value: "three", id: three.id}, item,
+ "getSetting returns the third set item after enabling the second item");
+
+ ExtensionSettingsStore.removeSetting(three, type, key);
+ ExtensionSettingsStore.removeSetting(two, type, key);
+ ExtensionSettingsStore.removeSetting(one, type, key);
+
+ equal(null, ExtensionSettingsStore.getSetting(type, key),
+ "getSetting returns null after removing all settings");
+
+ for (let extension of testExtensions) {
+ await extension.unload();
+ }
+
+ await promiseShutdownManager();
+});
+
add_task(async function test_exceptions() {
await ExtensionSettingsStore.initialize();
await Assert.rejects(
ExtensionSettingsStore.addSetting(
1, TEST_TYPE, "key_not_a_function", "val1", "not a function"),
/initialValueCallback must be a function/,
"addSetting rejects with a callback that is not a function.");