--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2209,42 +2209,58 @@ var BrowserHome = BrowserGoHome;
function BrowserGoHome(aEvent) {
if (aEvent && "button" in aEvent &&
aEvent.button == 2) // right-click: do nothing
return;
var homePage = gHomeButton.getHomePage();
var where = whereToOpenLink(aEvent, false, true);
var urls;
+ var notifyObservers;
// Home page should open in a new tab when current tab is an app tab
if (where == "current" &&
gBrowser &&
gBrowser.selectedTab.pinned)
where = "tab";
// openTrustedLinkIn in utilityOverlay.js doesn't handle loading multiple pages
switch (where) {
case "current":
loadOneOrMoreURIs(homePage, Services.scriptSecurityManager.getSystemPrincipal());
gBrowser.selectedBrowser.focus();
+ notifyObservers = true;
break;
case "tabshifted":
case "tab":
urls = homePage.split("|");
var loadInBackground = getBoolPref("browser.tabs.loadBookmarksInBackground", false);
+ // The homepage observer event should only be triggered when the homepage opens
+ // in the foreground. This is mostly to support the homepage changed by extension
+ // doorhanger which doesn't currently support background pages. This may change in
+ // bug 1438396.
+ notifyObservers = !loadInBackground;
gBrowser.loadTabs(urls, {
inBackground: loadInBackground,
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
break;
case "window":
+ // OpenBrowserWindow will trigger the observer event, so no need to do so here.
+ notifyObservers = false;
OpenBrowserWindow();
break;
}
+ if (notifyObservers) {
+ // A notification for when a user has triggered their homepage. This is used
+ // to display a doorhanger explaining that an extension has modified the
+ // homepage, if necessary. Observers are only notified if the homepage
+ // becomes the active page.
+ Services.obs.notifyObservers(null, "browser-open-homepage-start");
+ }
}
function loadOneOrMoreURIs(aURIString, aTriggeringPrincipal) {
// we're not a browser window, pass the URI string to a new browser window
if (window.location.href != getBrowserURL()) {
window.openDialog(getBrowserURL(), "_blank", "all,dialog=no", aURIString);
return;
}
@@ -2318,16 +2334,19 @@ function openLocation() {
}
function BrowserOpenTab(event) {
// A notification intended to be useful for modular peformance tracking
// starting as close as is reasonably possible to the time when the user
// expressed the intent to open a new tab. Since there are a lot of
// entry points, this won't catch every single tab created, but most
// initiated by the user should go through here.
+ //
+ // This is also used to notify a user that an extension has changed the
+ // New Tab page.
Services.obs.notifyObservers(null, "browser-open-newtab-start");
let where = "tab";
let relatedToCurrent = false;
if (event) {
where = whereToOpenLink(event, false, true);
@@ -4288,16 +4307,23 @@ function OpenBrowserWindow(options) {
win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs, charsetArg);
} else {
// forget about the charset information.
win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs);
}
win.addEventListener("MozAfterPaint", () => {
TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj);
+ if (Services.prefs.getIntPref("browser.startup.page") == 1
+ && defaultArgs == handler.startPage) {
+ // A notification for when a user has triggered their homepage. This is used
+ // to display a doorhanger explaining that an extension has modified the
+ // homepage, if necessary.
+ Services.obs.notifyObservers(win, "browser-open-homepage-start");
+ }
}, {once: true});
return win;
}
/**
* Update the global flag that tracks whether or not any edit UI (the Edit menu,
* edit-related items in the context menu, and edit-related toolbar buttons
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -712,9 +712,25 @@
secondarybuttonaccesskey="&newTabControlled.disableButton.accesskey;"
closebuttonhidden="true"
dropmarkerhidden="true"
checkboxhidden="true">
<popupnotificationcontent orient="vertical">
<description id="extension-new-tab-notification-description"/>
</popupnotificationcontent>
</popupnotification>
+ <popupnotification id="extension-homepage-notification"
+ class="extension-controlled-notification"
+ popupid="extension-homepage"
+ hidden="true"
+ label="&homepageControlled.header.message;"
+ buttonlabel="&homepageControlled.keepButton.label;"
+ buttonaccesskey="&homepageControlled.keepButton.accesskey;"
+ secondarybuttonlabel="&homepageControlled.disableButton.label;"
+ secondarybuttonaccesskey="&homepageControlled.disableButton.accesskey;"
+ closebuttonhidden="true"
+ dropmarkerhidden="true"
+ checkboxhidden="true">
+ <popupnotificationcontent orient="vertical">
+ <description id="extension-homepage-notification-description"/>
+ </popupnotificationcontent>
+ </popupnotification>
</panel>
--- a/browser/components/extensions/ExtensionControlledPopup.jsm
+++ b/browser/components/extensions/ExtensionControlledPopup.jsm
@@ -41,17 +41,19 @@ XPCOMUtils.defineLazyGetter(this, "strBu
class ExtensionControlledPopup {
/* Provide necessary options for the popup.
*
* @param {object} opts Options for configuring popup.
* @param {string} opts.confirmedType
* The type to use for storing a user's confirmation in
* ExtensionSettingsStore.
* @param {string} opts.observerTopic
- * An observer topic to trigger the popup on with Services.obs.
+ * An observer topic to trigger the popup on with Services.obs. If the
+ * doorhanger should appear on a specific window include it as the
+ * subject in the observer event.
* @param {string} opts.popupnotificationId
* The id for the popupnotification element in the markup. This
* element should be defined in panelUI.inc.xul.
* @param {string} opts.settingType
* The setting type to check in ExtensionSettingsStore to retrieve
* the controlling extension.
* @param {string} opts.settingKey
* The setting key to check in ExtensionSettingsStore to retrieve
@@ -73,16 +75,18 @@ class ExtensionControlledPopup {
* @param {function} opts.onObserverRemoved
* A callback that is triggered when the observer is removed,
* either because the popup is opening or it was explicitly
* cancelled by calling removeObserver.
* @param {function} opts.beforeDisableAddon
* A function that is called before disabling an extension when the
* user decides to disable the extension. If this function is async
* then the extension won't be disabled until it is fulfilled.
+ * This function gets two arguments, the ExtensionControlledPopup
+ * instance for the panel and the window that the popup appears on.
*/
constructor(opts) {
this.confirmedType = opts.confirmedType;
this.observerTopic = opts.observerTopic;
this.popupnotificationId = opts.popupnotificationId;
this.settingType = opts.settingType;
this.settingKey = opts.settingKey;
this.descriptionId = opts.descriptionId;
@@ -116,17 +120,17 @@ class ExtensionControlledPopup {
}
observe(subject, topic, data) {
// Remove the observer here so we don't get multiple open() calls if we get
// multiple observer events in quick succession.
this.removeObserver();
// Do this work in an idle callback to avoid interfering with new tab performance tracking.
- this.topWindow.requestIdleCallback(() => this.open());
+ this.topWindow.requestIdleCallback(() => this.open(subject));
}
removeObserver() {
if (this.observerRegistered) {
Services.obs.removeObserver(this, this.observerTopic);
this.observerRegistered = false;
if (this.onObserverRemoved) {
this.onObserverRemoved();
@@ -141,17 +145,17 @@ class ExtensionControlledPopup {
Services.obs.addObserver(this, this.observerTopic);
this.observerRegistered = true;
if (this.onObserverAdded) {
this.onObserverAdded();
}
}
}
- async open() {
+ async open(targetWindow) {
await ExtensionSettingsStore.initialize();
// Remove the observer since it would open the same dialog again the next time
// the observer event fires.
this.removeObserver();
let item = ExtensionSettingsStore.getSetting(
this.settingType, this.settingKey);
@@ -160,20 +164,21 @@ class ExtensionControlledPopup {
// the change here, but just to be sure check that it is still controlled
// and the user hasn't already confirmed the change.
// If there is no id, then the extension is no longer in control.
if (!item || !item.id || this.userHasConfirmed(item.id)) {
return;
}
// Find the elements we need.
- let win = this.topWindow;
+ let win = targetWindow || this.topWindow;
let doc = win.document;
let panel = doc.getElementById("extension-notification-panel");
let popupnotification = doc.getElementById(this.popupnotificationId);
+ let urlBarWasFocused = win.gURLBar.focused;
if (!popupnotification) {
throw new Error(`No popupnotification found for id "${this.popupnotificationId}"`);
}
let addon = await AddonManager.getAddonByID(item.id);
this.populateDescription(doc, addon);
@@ -181,20 +186,27 @@ class ExtensionControlledPopup {
let handleCommand = async (event) => {
panel.hidePopup();
if (event.originalTarget.getAttribute("anonid") == "button") {
// Main action is to keep changes.
await this.setConfirmation(item.id);
} else {
// Secondary action is to restore settings.
- await this.beforeDisableAddon(this);
+ await this.beforeDisableAddon(this, win);
addon.userDisabled = true;
}
- win.gURLBar.focus();
+
+ // If the page this is appearing on is the New Tab page then the URL bar may
+ // have been focused when the doorhanger stole focus away from it. Once an
+ // action is taken the focus state should be restored to what the user was
+ // expecting.
+ if (urlBarWasFocused) {
+ win.gURLBar.focus();
+ }
};
panel.addEventListener("command", handleCommand);
panel.addEventListener("popuphidden", () => {
popupnotification.hidden = true;
panel.removeEventListener("command", handleCommand);
}, {once: true});
// Look for a browserAction on the toolbar.
--- a/browser/components/extensions/parent/.eslintrc.js
+++ b/browser/components/extensions/parent/.eslintrc.js
@@ -15,14 +15,16 @@ module.exports = {
"getTargetTabIdForToolbox": true,
"getToolboxEvalOptions": true,
"isContainerCookieStoreId": true,
"isPrivateCookieStoreId": true,
"isValidCookieStoreId": true,
"makeWidgetId": true,
"openOptionsPage": true,
"pageActionFor": true,
+ "replaceUrlInTab": true,
"sidebarActionFor": true,
"tabGetSender": true,
"tabTracker": true,
+ "waitForTabLoaded": true,
"windowTracker": true,
},
};
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -93,16 +93,39 @@ global.openOptionsPage = (extension) =>
};
global.makeWidgetId = id => {
id = id.toLowerCase();
// FIXME: This allows for collisions.
return id.replace(/[^a-z0-9_-]/g, "_");
};
+global.waitForTabLoaded = (tab, url) => {
+ return new Promise(resolve => {
+ windowTracker.addListener("progress", {
+ onLocationChange(browser, webProgress, request, locationURI, flags) {
+ if (webProgress.isTopLevel
+ && browser.ownerGlobal.gBrowser.getTabForBrowser(browser) == tab
+ && (!url || locationURI.spec == url)) {
+ windowTracker.removeListener("progress", this);
+ resolve();
+ }
+ },
+ });
+ });
+};
+
+global.replaceUrlInTab = (gBrowser, tab, url) => {
+ let loaded = waitForTabLoaded(tab, url);
+ gBrowser.loadURI(url, {
+ flags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
+ });
+ return loaded;
+};
+
// Manages tab-specific context data, and dispatching tab select events
// across all windows.
global.TabContext = class extends EventEmitter {
constructor(getDefaults, extension) {
super();
this.extension = extension;
this.getDefaults = getDefaults;
--- a/browser/components/extensions/parent/ext-chrome-settings-overrides.js
+++ b/browser/components/extensions/parent/ext-chrome-settings-overrides.js
@@ -1,25 +1,34 @@
/* 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/. */
-/* globals windowTracker */
-
"use strict";
+ChromeUtils.defineModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+ChromeUtils.defineModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
ChromeUtils.defineModuleGetter(this, "ExtensionPreferencesManager",
"resource://gre/modules/ExtensionPreferencesManager.jsm");
ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
"resource://gre/modules/ExtensionSettingsStore.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionControlledPopup",
+ "resource:///modules/ExtensionControlledPopup.jsm");
const DEFAULT_SEARCH_STORE_TYPE = "default_search";
const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch";
const ENGINE_ADDED_SETTING_NAME = "engineAdded";
+const HOMEPAGE_PREF = "browser.startup.homepage";
+const HOMEPAGE_CONFIRMED_TYPE = "homepageNotification";
+const HOMEPAGE_SETTING_TYPE = "prefs";
+const HOMEPAGE_SETTING_NAME = "homepage_override";
+
// 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;
}
@@ -31,16 +40,75 @@ const searchInitialized = () => {
}
Services.obs.removeObserver(observer, SEARCH_SERVICE_TOPIC);
resolve();
}, SEARCH_SERVICE_TOPIC);
});
};
+XPCOMUtils.defineLazyGetter(this, "homepagePopup", () => {
+ return new ExtensionControlledPopup({
+ confirmedType: HOMEPAGE_CONFIRMED_TYPE,
+ observerTopic: "browser-open-homepage-start",
+ popupnotificationId: "extension-homepage-notification",
+ settingType: HOMEPAGE_SETTING_TYPE,
+ settingKey: HOMEPAGE_SETTING_NAME,
+ descriptionId: "extension-homepage-notification-description",
+ descriptionMessageId: "homepageControlled.message",
+ learnMoreMessageId: "homepageControlled.learnMore",
+ learnMoreLink: "extension-home",
+ async beforeDisableAddon(popup, win) {
+ // Disabling an add-on should remove the tabs that it has open, but we want
+ // to open the new homepage in this tab (which might get closed).
+ // 1. Replace the tab's URL with about:blank, wait for it to change
+ // 2. Now that this tab isn't associated with the add-on, disable the add-on
+ // 3. Trigger the browser's homepage method
+ let gBrowser = win.gBrowser;
+ let tab = gBrowser.selectedTab;
+ await replaceUrlInTab(gBrowser, tab, "about:blank");
+ Services.prefs.addObserver(HOMEPAGE_PREF, async function prefObserver() {
+ Services.prefs.removeObserver(HOMEPAGE_PREF, prefObserver);
+ let loaded = waitForTabLoaded(tab);
+ win.BrowserGoHome();
+ await loaded;
+ // Manually trigger an event in case this is controlled again.
+ popup.open();
+ });
+ },
+ });
+});
+
+// When the browser starts up it will trigger the observer topic we're expecting
+// but that happens before our observer has been registered. To handle the
+// startup case we need to check if the preferences are set to load the homepage
+// and check if the homepage is active, then show the doorhanger in that case.
+async function handleInitialHomepagePopup(extensionId, homepageUrl) {
+ // browser.startup.page == 1 is show homepage.
+ if (Services.prefs.getIntPref("browser.startup.page") == 1) {
+ let {gBrowser} = windowTracker.topWindow;
+ let tab = gBrowser.selectedTab;
+ let currentUrl = gBrowser.currentURI.spec;
+ // When the first window is still loading the URL might be about:blank.
+ // Wait for that the actual page to load before checking the URL, unless
+ // the homepage is set to about:blank.
+ if (currentUrl != homepageUrl && currentUrl == "about:blank") {
+ await waitForTabLoaded(tab);
+ currentUrl = gBrowser.currentURI.spec;
+ }
+ // Once the page has loaded, if necessary and the active tab hasn't changed,
+ // then show the popup now.
+ if (currentUrl == homepageUrl && gBrowser.selectedTab == tab) {
+ homepagePopup.open();
+ return;
+ }
+ }
+ homepagePopup.addObserver(extensionId);
+}
+
this.chrome_settings_overrides = class extends ExtensionAPI {
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 &&
@@ -86,17 +154,20 @@ this.chrome_settings_overrides = class e
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.
- return this.removeSearchSettings(id);
+ return Promise.all([
+ this.removeSearchSettings(id),
+ homepagePopup.clearConfirmation(id),
+ ]);
}
static onUpdate(id, manifest) {
let haveHomepage = manifest && manifest.chrome_settings_overrides &&
manifest.chrome_settings_overrides.homepage;
if (!haveHomepage) {
ExtensionPreferencesManager.removeSetting(id, "homepage_override");
}
@@ -108,19 +179,44 @@ this.chrome_settings_overrides = class e
}
}
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);
+
+ let homepageUrl = manifest.chrome_settings_overrides.homepage;
+
+ if (homepageUrl) {
+ let inControl;
+ if (extension.startupReason == "ADDON_INSTALL") {
+ inControl = await ExtensionPreferencesManager.setSetting(
+ extension.id, "homepage_override", homepageUrl);
+ } else {
+ let item = await ExtensionPreferencesManager.getSetting("homepage_override");
+ inControl = item.id == extension.id;
+ }
+ // We need to add the listener here too since onPrefsChanged won't trigger on a
+ // restart (the prefs are already set).
+ if (inControl) {
+ if (extension.startupReason == "APP_STARTUP") {
+ handleInitialHomepagePopup(extension.id, homepageUrl);
+ } else {
+ homepagePopup.addObserver(extension.id);
+ }
+ }
+ extension.callOnClose({
+ close: () => {
+ if (extension.shutdownReason == "ADDON_DISABLE") {
+ homepagePopup.clearConfirmation(extension.id);
+ }
+ },
+ });
}
if (manifest.chrome_settings_overrides.search_provider) {
await searchInitialized();
extension.callOnClose({
close: () => {
if (extension.shutdownReason == "ADDON_DISABLE") {
chrome_settings_overrides.processDefaultSearchSetting("disable", extension.id);
chrome_settings_overrides.removeEngine(extension.id);
@@ -233,16 +329,28 @@ this.chrome_settings_overrides = class e
return false;
}
return true;
}
};
ExtensionPreferencesManager.addSetting("homepage_override", {
prefNames: [
- "browser.startup.homepage",
+ HOMEPAGE_PREF,
],
+ // ExtensionPreferencesManager will call onPrefsChanged when control changes
+ // and it updates the preferences. We are passed the item from
+ // ExtensionSettingsStore that details what is in control. If there is an id
+ // then control has changed to an extension, if there is no id then control
+ // has been returned to the user.
+ onPrefsChanged(item) {
+ if (item.id) {
+ homepagePopup.addObserver(item.id);
+ } else {
+ homepagePopup.removeObserver();
+ }
+ },
setCallback(value) {
return {
- "browser.startup.homepage": value,
+ [HOMEPAGE_PREF]: value,
};
},
});
--- a/browser/components/extensions/parent/ext-url-overrides.js
+++ b/browser/components/extensions/parent/ext-url-overrides.js
@@ -16,36 +16,16 @@ ChromeUtils.defineModuleGetter(this, "Ex
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
"@mozilla.org/browser/aboutnewtab-service;1",
"nsIAboutNewTabService");
const STORE_TYPE = "url_overrides";
const NEW_TAB_SETTING_NAME = "newTabURL";
const NEW_TAB_CONFIRMED_TYPE = "newTabNotification";
-function replaceUrlInTab(gBrowser, tab, url) {
- let loaded = new Promise(resolve => {
- windowTracker.addListener("progress", {
- onLocationChange(browser, webProgress, request, locationURI, flags) {
- if (webProgress.isTopLevel
- && browser.ownerGlobal.gBrowser.getTabForBrowser(browser) == tab
- && locationURI.spec == url) {
- windowTracker.removeListener(this);
- resolve();
- }
- },
- });
- });
- gBrowser.loadURI(url, {
- flags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
- });
- return loaded;
-}
-
-
XPCOMUtils.defineLazyGetter(this, "newTabPopup", () => {
return new ExtensionControlledPopup({
confirmedType: NEW_TAB_CONFIRMED_TYPE,
observerTopic: "browser-open-newtab-start",
popupnotificationId: "extension-new-tab-notification",
settingType: STORE_TYPE,
settingKey: NEW_TAB_SETTING_NAME,
descriptionId: "extension-new-tab-notification-description",
@@ -53,24 +33,24 @@ XPCOMUtils.defineLazyGetter(this, "newTa
learnMoreMessageId: "newTabControlled.learnMore",
learnMoreLink: "extension-home",
onObserverAdded() {
aboutNewTabService.willNotifyUser = true;
},
onObserverRemoved() {
aboutNewTabService.willNotifyUser = false;
},
- async beforeDisableAddon(popup) {
+ async beforeDisableAddon(popup, win) {
// ExtensionControlledPopup will disable the add-on once this function completes.
// Disabling an add-on should remove the tabs that it has open, but we want
// to open the new New Tab in this tab (which might get closed).
// 1. Replace the tab's URL with about:blank
// 2. Return control to ExtensionControlledPopup once about:blank has loaded
// 3. Once the New Tab URL has changed, replace the tab's URL with the new New Tab URL
- let gBrowser = windowTracker.topWindow.gBrowser;
+ let gBrowser = win.gBrowser;
let tab = gBrowser.selectedTab;
await replaceUrlInTab(gBrowser, tab, "about:blank");
Services.obs.addObserver({
async observe() {
await replaceUrlInTab(gBrowser, tab, aboutNewTabService.newTabURL);
// Now that the New Tab is loading, try to open the popup again. This
// will only open the popup if a new extension is controlling the New Tab.
popup.open();
--- a/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
+++ b/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
@@ -1,15 +1,17 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
ChromeUtils.defineModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
+ "resource://gre/modules/ExtensionSettingsStore.jsm");
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
// Named this way so they correspond to the extensions
const HOME_URI_2 = "http://example.com/";
const HOME_URI_3 = "http://example.org/";
const HOME_URI_4 = "http://example.net/";
@@ -19,16 +21,21 @@ const NOT_CONTROLLABLE = "not_controllab
const HOMEPAGE_URL_PREF = "browser.startup.homepage";
const getHomePageURL = () => {
return Services.prefs.getComplexValue(
HOMEPAGE_URL_PREF, Ci.nsIPrefLocalizedString).data;
};
+function isConfirmed(id) {
+ let item = ExtensionSettingsStore.getSetting("homepageNotification", id);
+ return !!(item && item.value);
+}
+
add_task(async function test_multiple_extensions_overriding_home_page() {
let defaultHomePage = getHomePageURL();
function background() {
browser.test.onMessage.addListener(async msg => {
switch (msg) {
case "checkHomepage":
let homepage = await browser.browserSettings.homepageOverride.get({});
@@ -285,8 +292,134 @@ add_task(async function test_multiple()
await prefPromise;
is(getHomePageURL(),
"https://mozilla.org/%7Chttps://developer.mozilla.org/%7Chttps://addons.mozilla.org/",
"The homepage encodes | so only one homepage is allowed");
await extension.unload();
});
+
+add_task(async function test_doorhanger_homepage_button() {
+ let defaultHomePage = getHomePageURL();
+ // These extensions are temporarily loaded so that the AddonManager can see
+ // them and the extension's shutdown handlers are called.
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {"chrome_settings_overrides": {"homepage": "ext1.html"}},
+ files: {"ext1.html": "<h1>1</h1>"},
+ useAddonManager: "temporary",
+ });
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {"chrome_settings_overrides": {"homepage": "ext2.html"}},
+ files: {"ext2.html": "<h1>2</h1>"},
+ useAddonManager: "temporary",
+ });
+
+ let panel = document.getElementById("extension-notification-panel");
+ let popupnotification = document.getElementById("extension-homepage-notification");
+
+ await ext1.startup();
+ await ext2.startup();
+
+ let popupShown = promisePopupShown(panel);
+ BrowserGoHome();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await popupShown;
+
+ ok(gURLBar.value.endsWith("ext2.html"), "ext2 is in control");
+
+ // Click Restore Settings.
+ let popupHidden = promisePopupHidden(panel);
+ let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ document.getAnonymousElementByAttribute(
+ popupnotification, "anonid", "secondarybutton").click();
+ await prefPromise;
+ await popupHidden;
+
+ // Expect a new doorhanger for the next extension.
+ await promisePopupShown(panel);
+
+ ok(gURLBar.value.endsWith("ext1.html"), "ext1 is in control");
+
+ // Click Restore Settings again.
+ popupHidden = promisePopupHidden(panel);
+ prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ document.getAnonymousElementByAttribute(popupnotification, "anonid", "secondarybutton").click();
+ await popupHidden;
+ await prefPromise;
+
+ is(getHomePageURL(), defaultHomePage, "The homepage is set back to default");
+
+ await ext1.unload();
+ await ext2.unload();
+});
+
+add_task(async function test_doorhanger_new_window() {
+ // These extensions are temporarily loaded so that the AddonManager can see
+ // them and the extension's shutdown handlers are called.
+ let ext1Id = "ext1@mochi.test";
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {"homepage": "ext1.html"},
+ applications: {
+ gecko: {id: ext1Id},
+ },
+ name: "Ext1",
+ },
+ files: {"ext1.html": "<h1>1</h1>"},
+ useAddonManager: "temporary",
+ });
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {homepage: "ext2.html"},
+ name: "Ext2",
+ },
+ files: {"ext2.html": "<h1>2</h1>"},
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await ext2.startup();
+
+ await SpecialPowers.pushPrefEnv({set: [["browser.startup.page", 1]]});
+
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow();
+ await windowOpenedPromise;
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ let doc = win.document;
+ let description = doc.getElementById("extension-homepage-notification-description");
+ let panel = doc.getElementById("extension-notification-panel");
+ await promisePopupShown(panel);
+
+ ok(win.gURLBar.value.endsWith("ext2.html"), "ext2 is in control");
+ is(description.textContent,
+ "An extension, Ext2, changed what you see when you open your homepage and new windows.Learn more",
+ "The extension name is in the popup");
+
+ // Click Restore Settings.
+ let popupHidden = promisePopupHidden(panel);
+ let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
+ let popupnotification = doc.getElementById("extension-homepage-notification");
+ doc.getAnonymousElementByAttribute(popupnotification, "anonid", "secondarybutton").click();
+ await prefPromise;
+ await popupHidden;
+
+ // Expect a new doorhanger for the next extension.
+ await promisePopupShown(panel);
+
+ ok(win.gURLBar.value.endsWith("ext1.html"), "ext1 is in control");
+ is(description.textContent,
+ "An extension, Ext1, changed what you see when you open your homepage and new windows.Learn more",
+ "The extension name is in the popup");
+
+ // Click Keep Changes.
+ doc.getAnonymousElementByAttribute(popupnotification, "anonid", "button").click();
+ await TestUtils.waitForCondition(() => isConfirmed(ext1Id));
+
+ ok(getHomePageURL().endsWith("ext1.html"), "The homepage is still the set");
+
+ await BrowserTestUtils.closeWindow(win);
+ await ext1.unload();
+ await ext2.unload();
+
+ ok(!isConfirmed(ext1Id), "The confirmation is cleaned up on uninstall");
+});
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -979,16 +979,23 @@ you can use these alternative items. Oth
<!ENTITY updateRestart.panelUI.label2 "Restart to update &brandShorterName;">
<!ENTITY newTabControlled.header.message "Your New Tab has changed.">
<!ENTITY newTabControlled.keepButton.label "Keep Changes">
<!ENTITY newTabControlled.keepButton.accesskey "K">
<!ENTITY newTabControlled.disableButton.label "Disable Extension">
<!ENTITY newTabControlled.disableButton.accesskey "D">
+<!ENTITY homepageControlled.message "An extension has changed what you see as your home page. You can restore your settings if you do not want this change.">
+<!ENTITY homepageControlled.header.message "Your home page has changed.">
+<!ENTITY homepageControlled.keepButton.label "Keep Changes">
+<!ENTITY homepageControlled.keepButton.accesskey "K">
+<!ENTITY homepageControlled.disableButton.label "Disable Extension">
+<!ENTITY homepageControlled.disableButton.accesskey "D">
+
<!ENTITY pageActionButton.tooltip "Page actions">
<!ENTITY pageAction.addToUrlbar.label "Add to Address Bar">
<!ENTITY pageAction.removeFromUrlbar.label "Remove from Address Bar">
<!ENTITY pageAction.allowInUrlbar.label "Show in Address Bar">
<!ENTITY pageAction.disallowInUrlbar.label "Don’t Show in Address Bar">
<!ENTITY pageAction.manageExtension.label "Manage Extension…">
<!ENTITY pageAction.sendTabToDevice.label "Send Tab to Device">
--- a/toolkit/components/extensions/ExtensionPreferencesManager.jsm
+++ b/toolkit/components/extensions/ExtensionPreferencesManager.jsm
@@ -76,30 +76,39 @@ function initialValueCallback() {
return initialValue;
}
/**
* Loops through a set of prefs, either setting or resetting them.
*
* @param {Object} setting
* An object that represents a setting, which will have a setCallback
- * property.
+ * property. If a onPrefsChanged function is provided it will be called
+ * with item when the preferences change.
* @param {Object} item
* An object that represents an item handed back from the setting store
* from which the new pref values can be calculated.
*/
function setPrefs(setting, item) {
let prefs = item.initialValue || setting.setCallback(item.value);
+ let changed = false;
for (let pref in prefs) {
if (prefs[pref] === undefined) {
- Preferences.reset(pref);
- } else {
+ if (Preferences.isSet(pref)) {
+ changed = true;
+ Preferences.reset(pref);
+ }
+ } else if (Preferences.get(pref) != prefs[pref]) {
Preferences.set(pref, prefs[pref]);
+ changed = true;
}
}
+ if (changed && typeof setting.onPrefsChanged == "function") {
+ setting.onPrefsChanged(item);
+ }
}
/**
* Commits a change to a setting and conditionally sets preferences.
*
* If the change to the setting causes a different extension to gain
* control of the pref (or removes all extensions with control over the pref)
* then the prefs should be updated, otherwise they should not be.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_extensionPreferencesManager.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extensionPreferencesManager.js
@@ -16,16 +16,18 @@ const {
promiseShutdownManager,
promiseStartupManager,
} = AddonTestUtils;
AddonTestUtils.init(this);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+let lastSetPref;
+
const STORE_TYPE = "prefs";
// Test settings to use with the preferences manager.
const SETTINGS = {
multiple_prefs: {
prefNames: ["my.pref.1", "my.pref.2", "my.pref.3"],
initalValues: ["value1", "value2", "value3"],
@@ -45,16 +47,20 @@ const SETTINGS = {
singlePref: {
prefNames: [
"my.single.pref",
],
initalValues: ["value1"],
+ onPrefsChanged(item) {
+ lastSetPref = item;
+ },
+
valueFn(pref, value) {
return value;
},
setCallback(value) {
return {[this.prefNames[0]]: this.valueFn(null, value)};
},
},
@@ -72,16 +78,25 @@ for (let setting in SETTINGS) {
}
function checkPrefs(settingObj, value, msg) {
for (let pref of settingObj.prefNames) {
equal(Preferences.get(pref), settingObj.valueFn(pref, value), msg);
}
}
+function checkOnPrefsChanged(setting, value, msg) {
+ if (value) {
+ deepEqual(lastSetPref, value, msg);
+ lastSetPref = null;
+ } else {
+ ok(!lastSetPref, msg);
+ }
+}
+
add_task(async function test_preference_manager() {
await promiseStartupManager();
// Create an array of test framework extension wrappers to install.
let testExtensions = [
ExtensionTestUtils.loadExtension({
useAddonManager: "temporary",
manifest: {},
@@ -100,76 +115,124 @@ add_task(async function test_preference_
// test framework extension wrappers.
let extensions = testExtensions.map(extension => extension.extension);
for (let setting in SETTINGS) {
let settingObj = SETTINGS[setting];
let newValue1 = "newValue1";
let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
extensions[1].id, setting);
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(setting, null, "onPrefsChanged has not been called yet");
+ }
equal(levelOfControl, "controllable_by_this_extension",
"getLevelOfControl returns correct levelOfControl with no settings set.");
let prefsChanged = await ExtensionPreferencesManager.setSetting(
extensions[1].id, setting, newValue1);
ok(prefsChanged, "setSetting returns true when the pref(s) have been set.");
checkPrefs(settingObj, newValue1,
"setSetting sets the prefs for the first extension.");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, {id: extensions[1].id, value: newValue1, key: setting},
+ "onPrefsChanged is called when pref changes");
+ }
levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(extensions[1].id, setting);
equal(
levelOfControl,
"controlled_by_this_extension",
"getLevelOfControl returns correct levelOfControl when a pref has been set.");
let checkSetting = await ExtensionPreferencesManager.getSetting(setting);
equal(checkSetting.value, newValue1, "getSetting returns the expected value.");
let newValue2 = "newValue2";
prefsChanged = await ExtensionPreferencesManager.setSetting(extensions[0].id, setting, newValue2);
ok(!prefsChanged, "setSetting returns false when the pref(s) have not been set.");
checkPrefs(settingObj, newValue1,
"setSetting does not set the pref(s) for an earlier extension.");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, null, "onPrefsChanged isn't called without control change");
+ }
prefsChanged = await ExtensionPreferencesManager.disableSetting(extensions[0].id, setting);
ok(!prefsChanged, "disableSetting returns false when the pref(s) have not been set.");
checkPrefs(settingObj, newValue1,
"disableSetting does not change the pref(s) for the non-top extension.");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, null, "onPrefsChanged isn't called without control change on disable");
+ }
prefsChanged = await ExtensionPreferencesManager.enableSetting(extensions[0].id, setting);
ok(!prefsChanged, "enableSetting returns false when the pref(s) have not been set.");
checkPrefs(settingObj, newValue1,
"enableSetting does not change the pref(s) for the non-top extension.");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, null, "onPrefsChanged isn't called without control change on enable");
+ }
prefsChanged = await ExtensionPreferencesManager.removeSetting(extensions[0].id, setting);
ok(!prefsChanged, "removeSetting returns false when the pref(s) have not been set.");
checkPrefs(settingObj, newValue1,
"removeSetting does not change the pref(s) for the non-top extension.");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, null, "onPrefsChanged isn't called without control change on remove");
+ }
prefsChanged = await ExtensionPreferencesManager.setSetting(extensions[0].id, setting, newValue2);
ok(!prefsChanged, "setSetting returns false when the pref(s) have not been set.");
checkPrefs(settingObj, newValue1,
"setSetting does not set the pref(s) for an earlier extension.");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, null, "onPrefsChanged isn't called without control change again");
+ }
prefsChanged = await ExtensionPreferencesManager.disableSetting(extensions[1].id, setting);
ok(prefsChanged, "disableSetting returns true when the pref(s) have been set.");
checkPrefs(settingObj, newValue2,
"disableSetting sets the pref(s) to the next value when disabling the top extension.");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, {id: extensions[0].id, key: setting, value: newValue2},
+ "onPrefsChanged is called when control changes on disable");
+ }
prefsChanged = await ExtensionPreferencesManager.enableSetting(extensions[1].id, setting);
ok(prefsChanged, "enableSetting returns true when the pref(s) have been set.");
checkPrefs(settingObj, newValue1,
"enableSetting sets the pref(s) to the previous value(s).");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, {id: extensions[1].id, key: setting, value: newValue1},
+ "onPrefsChanged is called when control changes on enable");
+ }
prefsChanged = await ExtensionPreferencesManager.removeSetting(extensions[1].id, setting);
ok(prefsChanged, "removeSetting returns true when the pref(s) have been set.");
checkPrefs(settingObj, newValue2,
"removeSetting sets the pref(s) to the next value when removing the top extension.");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, {id: extensions[0].id, key: setting, value: newValue2},
+ "onPrefsChanged is called when control changes on remove");
+ }
prefsChanged = await ExtensionPreferencesManager.removeSetting(extensions[0].id, setting);
ok(prefsChanged, "removeSetting returns true when the pref(s) have been set.");
+ if (settingObj.onPrefsChanged) {
+ checkOnPrefsChanged(
+ setting, {key: setting, initialValue: {"my.single.pref": "value1"}},
+ "onPrefsChanged is called when control is entirely removed");
+ }
for (let i = 0; i < settingObj.prefNames.length; i++) {
equal(Preferences.get(settingObj.prefNames[i]), settingObj.initalValues[i],
"removeSetting sets the pref(s) to the initial value(s) when removing the last extension.");
}
checkSetting = await ExtensionPreferencesManager.getSetting(setting);
equal(checkSetting, null, "getSetting returns null when nothing has been set.");
}
--- a/toolkit/locales/en-US/chrome/global/extensions.properties
+++ b/toolkit/locales/en-US/chrome/global/extensions.properties
@@ -28,8 +28,12 @@ uninstall.confirmation.message = The extension “%S” is requesting to be uninstalled. What would you like to do?
uninstall.confirmation.button-0.label = Uninstall
uninstall.confirmation.button-1.label = Keep Installed
saveaspdf.saveasdialog.title = Save As
#LOCALIZATION NOTE (newTabControlled.message2) %S is the icon and name of the extension which updated the New Tab page.
newTabControlled.message2 = An extension, %S, changed the page you see when you open a new tab.
newTabControlled.learnMore = Learn more
+
+#LOCALIZATION NOTE (homepageControlled.message) %S is the icon and name of the extension which updated the homepage.
+homepageControlled.message = An extension, %S, changed what you see when you open your homepage and new windows.
+homepageControlled.learnMore = Learn more