--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -1,16 +1,21 @@
/* 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, "ExtensionPreferencesManager",
+ "resource://gre/modules/ExtensionPreferencesManager.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/Services.jsm");
Components.utils.import("resource://gre/modules/AppConstants.jsm");
Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
@@ -231,16 +236,18 @@ var gMainPane = {
gMainPane.setDefaultBrowser);
}
setEventListener("useCurrent", "command",
gMainPane.setHomePageToCurrent);
setEventListener("useBookmark", "command",
gMainPane.setHomePageToBookmark);
setEventListener("restoreDefaultHomePage", "command",
gMainPane.restoreDefaultHomePage);
+ setEventListener("disableHomePageExtension", "command",
+ gMainPane.makeDisableControllingExtension("homepage_override"));
setEventListener("chooseLanguage", "command",
gMainPane.showLanguages);
setEventListener("translationAttributionImage", "click",
gMainPane.openTranslationProviderAttribution);
setEventListener("translateButton", "command",
gMainPane.showTranslationExceptions);
setEventListener("font.language.group", "change",
gMainPane._rebuildFonts);
@@ -615,16 +622,29 @@ var gMainPane = {
* The deprecated option is not exposed in UI; however, if the user has it
* selected and doesn't change the UI for this preference, the deprecated
* option is preserved.
*/
syncFromHomePref() {
let homePref = document.getElementById("browser.startup.homepage");
+ // Set the "Use Current Page(s)" button's text and enabled state.
+ this._updateUseCurrentButton();
+
+ // This is an async task.
+ handleControllingExtension("homepage_override")
+ .then((isControlled) => {
+ // Disable or enable the inputs based on if this is controlled by an extension.
+ document.querySelectorAll("#browserHomePage, .homepage-button")
+ .forEach((button) => {
+ button.disabled = isControlled;
+ });
+ });
+
// 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",
Ci.nsIPrefLocalizedString).data;
let currentValue = homePref.value.toLowerCase();
if (currentValue == "about:home" ||
@@ -690,27 +710,31 @@ var gMainPane = {
homePage.value = rv.urls.join("|");
}
},
/**
* Switches the "Use Current Page" button between its singular and plural
* forms.
*/
- _updateUseCurrentButton() {
+ async _updateUseCurrentButton() {
let useCurrent = document.getElementById("useCurrent");
-
-
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("homepage_override")) {
+ 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;
useCurrent.disabled = !tabs.length
},
@@ -744,16 +768,24 @@ 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(pref) {
+ return async function disableExtension() {
+ let id = await getControllingExtensionId(pref);
+ 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;
@@ -2549,16 +2581,76 @@ 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 = {
+ "homepage_override": "browserHomePageExtensionContent",
+};
+
+/**
+ * Check if a pref is being managed by an extension.
+ */
+function getControllingExtensionId(settingName) {
+ return ExtensionPreferencesManager.getControllingExtensionId(settingName);
+}
+
+function getControllingExtensionEl(settingName) {
+ return document.getElementById(extensionControlledContentIds[settingName]);
+}
+
+async function handleControllingExtension(prefName) {
+ let controllingExtensionId = await getControllingExtensionId(prefName);
+
+ if (controllingExtensionId) {
+ showControllingExtension(prefName, controllingExtensionId);
+ } else {
+ hideControllingExtension(prefName);
+ }
+
+ return !!controllingExtensionId;
+}
+
+async function showControllingExtension(settingName, extensionId) {
+ let extensionControlledContent = getControllingExtensionEl(settingName);
+ // Tell the user what extension is controlling the homepage.
+ let addon = await AddonManager.getAddonByID(extensionId);
+ 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/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -343,16 +343,23 @@
</groupbox>
<!-- Home Page -->
<groupbox id="homepageGroup"
data-category="paneGeneral"
hidden="true">
<caption><label>&homepage2.label;</label></caption>
+ <hbox id="browserHomePageExtensionContent" align="center">
+ <description control="disableHomePageExtension" flex="1" />
+ <button id="disableHomePageExtension"
+ class="extension-controlled-button accessory-button"
+ label="&disableExtension.label;" />
+ </hbox>
+
<vbox>
<textbox id="browserHomePage"
class="uri-element"
type="autocomplete"
autocompletesearch="unifiedcomplete"
onsyncfrompreference="return gMainPane.syncFromHomePref();"
onsynctopreference="return gMainPane.syncToHomePref(this.value);"
placeholder="&abouthome.pageTitle;"
new file mode 100644
index 0000000000000000000000000000000000000000..9aff671021e1a247201555ccdbe751f83c5df5aa
GIT binary patch
literal 5156
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
OhaKYBC!C`GBmEB;!Sz!B
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -1,13 +1,14 @@
[DEFAULT]
support-files =
head.js
privacypane_tests_perwindow.js
site_data_test.html
+ addons/set_homepage.xpi
offline/offline.html
offline/manifest.appcache
[browser_applications_selection.js]
skip-if = os == 'linux' # bug 1382057
[browser_advanced_update.js]
skip-if = !updater
[browser_basic_rebuild_fonts_test.js]
--- a/browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js
+++ b/browser/components/preferences/in-content/tests/browser_homepages_filter_aboutpreferences.js
@@ -1,9 +1,9 @@
-add_task(async function() {
+add_task(async function testSetHomepageUseCurrent() {
is(gBrowser.currentURI.spec, "about:blank", "Test starts with about:blank open");
await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
let doc = gBrowser.contentDocument;
is(gBrowser.currentURI.spec, "about:preferences#general",
"#general should be in the URI for about:preferences");
let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage");
@@ -13,8 +13,99 @@ add_task(async function() {
is(gBrowser.tabs.length, 3, "Three tabs should be open");
is(Services.prefs.getCharPref("browser.startup.homepage"), "about:blank|about:home",
"about:blank and about:home should be the only homepages set");
Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref);
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
+
+const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+const CHROME_URL_ROOT = TEST_DIR + "/";
+
+function getSupportsFile(path) {
+ let cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIChromeRegistry);
+ let uri = Services.io.newURI(CHROME_URL_ROOT + path);
+ let fileurl = cr.convertChromeURL(uri);
+ return fileurl.QueryInterface(Ci.nsIFileURL);
+}
+
+function installAddon() {
+ let filePath = getSupportsFile("addons/set_homepage.xpi").file;
+ return new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(filePath, install => {
+ if (!install) {
+ throw new Error(`An install was not created for ${filePath}`);
+ }
+ install.addListener({
+ onDownloadFailed: reject,
+ onDownloadCancelled: reject,
+ onInstallFailed: reject,
+ onInstallCancelled: reject,
+ onInstallEnded: resolve
+ });
+ install.install();
+ });
+ });
+}
+
+function waitForMessageChange(cb) {
+ return new Promise((resolve) => {
+ let target = gBrowser.contentDocument.getElementById("browserHomePageExtensionContent");
+ let observer = new MutationObserver(() => {
+ if (cb(target)) {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(target, { attributes: true, attributeFilter: ["hidden"] });
+ });
+}
+
+function waitForMessageHidden() {
+ return waitForMessageChange(target => target.hidden);
+}
+
+function waitForMessageShown() {
+ return waitForMessageChange(target => !target.hidden);
+}
+
+add_task(async function testExtensionControlledHomepage() {
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+ let doc = gBrowser.contentDocument;
+ is(gBrowser.currentURI.spec, "about:preferences#general",
+ "#general should be in the URI for about:preferences");
+ let homepagePref = () => Services.prefs.getCharPref("browser.startup.homepage");
+ let originalHomepagePref = homepagePref();
+ let extensionHomepage = "https://developer.mozilla.org/";
+ let controlledContent = doc.getElementById("browserHomePageExtensionContent");
+
+ // The homepage is set to the default and editable.
+ ok(originalHomepagePref != extensionHomepage, "homepage is empty by default");
+ is(doc.getElementById("browserHomePage").disabled, false, "The homepage input is enabled");
+ is(controlledContent.hidden, true, "The extension controlled row is hidden");
+
+ // Install an extension that will set the homepage.
+ await installAddon();
+ await waitForMessageShown();
+
+ // The homepage has been set by the extension, the user is notified and it isn't editable.
+ let controlledLabel = controlledContent.querySelector("description");
+ is(homepagePref(), extensionHomepage, "homepage is set by extension");
+ // There are two spaces before "set_homepage" because it's " <image /> set_homepage".
+ is(controlledLabel.textContent, "An extension, set_homepage, controls your home page.",
+ "The user is notified that an extension is controlling the homepage");
+ is(controlledContent.hidden, false, "The extension controlled row is hidden");
+ is(doc.getElementById("browserHomePage").disabled, true, "The homepage input is disabled");
+
+ // Disable the extension.
+ doc.getElementById("disableHomePageExtension").click();
+
+ await waitForMessageHidden();
+
+ is(homepagePref(), originalHomepagePref, "homepage is set back to default");
+ is(doc.getElementById("browserHomePage").disabled, false, "The homepage input is enabled");
+ is(controlledContent.hidden, true, "The extension controlled row is hidden");
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
--- a/browser/locales/en-US/chrome/browser/preferences/main.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/main.dtd
@@ -15,16 +15,18 @@
<!ENTITY useCurrentPage.label "Use Current Page">
<!ENTITY useCurrentPage.accesskey "C">
<!ENTITY useMultiple.label "Use Current Pages">
<!ENTITY chooseBookmark.label "Use Bookmark…">
<!ENTITY chooseBookmark.accesskey "B">
<!ENTITY restoreDefault.label "Restore to Default">
<!ENTITY restoreDefault.accesskey "R">
+<!ENTITY disableExtension.label "Disable Extension">
+
<!ENTITY downloads.label "Downloads">
<!ENTITY saveTo.label "Save files to">
<!ENTITY saveTo.accesskey "v">
<!ENTITY chooseFolderWin.label "Browse…">
<!ENTITY chooseFolderWin.accesskey "o">
<!ENTITY chooseFolderMac.label "Choose…">
<!ENTITY chooseFolderMac.accesskey "e">
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -272,8 +272,12 @@ searchInput.labelUnix=Find in Preference
searchResults.sorryMessageWin=Sorry! There are no results in Options for “%S”.
searchResults.sorryMessageUnix=Sorry! There are no results in Preferences for “%S”.
# LOCALIZATION NOTE (searchResults.needHelp2): %1$S is a link to SUMO, %2$S is
# the browser name
searchResults.needHelp2=Need help? Visit <html:a id="need-help-link" target="_blank" href="%1$S">%2$S Support</html:a>
# LOCALIZATION NOTE %S is the default value of the `dom.ipc.processCount` pref.
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.
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -154,16 +154,23 @@ separator.thin:not([orient="vertical"])
.homepage-button:first-of-type {
margin-inline-start: 0;
}
.homepage-button:last-of-type {
margin-inline-end: 0;
}
+.extension-controlled-icon {
+ height: 20px;
+ margin-bottom: 6px;
+ vertical-align: bottom;
+ width: 20px;
+}
+
#getStarted {
font-size: 90%;
}
#downloadFolder {
margin-inline-start: 0;
}
--- a/toolkit/components/extensions/ExtensionPreferencesManager.jsm
+++ b/toolkit/components/extensions/ExtensionPreferencesManager.jsm
@@ -164,16 +164,29 @@ 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
@@ -103,30 +103,30 @@ 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 and value|initialValue, or null
-// if no setting has been stored for that key.
+// 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) {
ensureType(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};
+ return {key, value: item.value, id: item.id};
}
}
// Nothing found in the precedenceList, return the initialValue.
return {key, initialValue: keyInfo.initialValue};
}
// Comparator used when sorting the precedence list.
@@ -286,17 +286,17 @@ this.ExtensionSettingsStore = {
// 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 {id, key, value};
}
return null;
},
/**
* Removes a setting from the store, possibly returning the current top
* precedent setting.
*
@@ -447,16 +447,26 @@ 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.
*
* @returns {Promise}
* A promise that resolves once the settings store has been cleared.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionSettingsStore.js
@@ -17,24 +17,24 @@ AddonTestUtils.init(this);
// Allow for unsigned addons.
AddonTestUtils.overrideCertDB();
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
const ITEMS = {
key1: [
- {key: "key1", value: "val1"},
- {key: "key1", value: "val2"},
- {key: "key1", value: "val3"},
+ {key: "key1", value: "val1", id: "@first"},
+ {key: "key1", value: "val2", id: "@second"},
+ {key: "key1", value: "val3", id: "@third"},
],
key2: [
- {key: "key2", value: "val1-2"},
- {key: "key2", value: "val2-2"},
- {key: "key2", value: "val3-2"},
+ {key: "key2", value: "val1-2", id: "@first"},
+ {key: "key2", value: "val2-2", id: "@second"},
+ {key: "key2", value: "val3-2", id: "@third"},
],
};
const KEY_LIST = Object.keys(ITEMS);
const TEST_TYPE = "myType";
let callbackCount = 0;
function initialValue(key) {
@@ -44,25 +44,31 @@ function initialValue(key) {
add_task(async function test_settings_store() {
await promiseStartupManager();
// Create an array of test framework extension wrappers to install.
let testExtensions = [
ExtensionTestUtils.loadExtension({
useAddonManager: "temporary",
- manifest: {},
+ manifest: {
+ applications: {gecko: {id: "@first"}},
+ },
}),
ExtensionTestUtils.loadExtension({
useAddonManager: "temporary",
- manifest: {},
+ manifest: {
+ applications: {gecko: {id: "@second"}},
+ },
}),
ExtensionTestUtils.loadExtension({
useAddonManager: "temporary",
- manifest: {},
+ manifest: {
+ applications: {gecko: {id: "@third"}},
+ },
}),
];
for (let extension of testExtensions) {
await extension.startup();
}
// Create an array actual Extension objects which correspond to the
@@ -296,17 +302,17 @@ add_task(async function test_settings_st
"getSetting returns correct item after a removal.");
levelOfControl = await ExtensionSettingsStore.getLevelOfControl(extensions[2], TEST_TYPE, key);
equal(
levelOfControl,
"controllable_by_this_extension",
"getLevelOfControl returns correct levelOfControl after removal of top item.");
// Add a setting for the current top item.
- let itemToAdd = {key, value: `new-${key}`};
+ let itemToAdd = {key, value: `new-${key}`, id: "@second"};
item = await ExtensionSettingsStore.addSetting(
extensions[1], TEST_TYPE, itemToAdd.key, itemToAdd.value, initialValue);
equal(callbackCount,
expectedCallbackCount,
"initialValueCallback called the expected number of times.");
deepEqual(
item,
itemToAdd,