diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js index e224ad7df356..4dbf60b4d4f0 100644 --- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -28,25 +28,14 @@ Object.defineProperty(this, "BROWSER_NEW_TAB_URL", { !aboutNewTabService.overridden) { return "about:privatebrowsing"; } - // If the extension does not have private browsing permission, - // use about:privatebrowsing. - let extensionInfo; - try { - extensionInfo = ExtensionSettingsStore.getSetting("url_overrides", "newTabURL"); - } catch (e) { - // ExtensionSettings may not be initialized if no extensions are enabled. If - // we have some indication that an extension controls the homepage, return - // the defaults instead. - if (aboutNewTabService.newTabURL.startsWith("moz-extension://")) { - return "about:privatebrowsing"; - } - } - - if (extensionInfo) { - let policy = WebExtensionPolicy.getByID(extensionInfo.id); - if (!policy || !policy.privateBrowsingAllowed) { - return "about:privatebrowsing"; - } + // If an extension controls the setting and does not have private + // browsing permission, use the default setting. + let extensionControlled = Services.prefs.getBoolPref("browser.newtab.extensionControlled", false); + let privateAllowed = Services.prefs.getBoolPref("browser.newtab.privateAllowed", false); + // There is a potential on upgrade that the prefs are not set yet, so we double check + // for moz-extension. + if (!privateAllowed && (extensionControlled || aboutNewTabService.newTabURL.startsWith("moz-extension://"))) { + return "about:privatebrowsing"; } } return aboutNewTabService.newTabURL; diff --git a/browser/components/extensions/parent/ext-chrome-settings-overrides.js b/browser/components/extensions/parent/ext-chrome-settings-overrides.js index 97e4b52afe8c..9319ef68091d 100644 --- a/browser/components/extensions/parent/ext-chrome-settings-overrides.js +++ b/browser/components/extensions/parent/ext-chrome-settings-overrides.js @@ -5,6 +5,7 @@ "use strict"; var {ExtensionPreferencesManager} = ChromeUtils.import("resource://gre/modules/ExtensionPreferencesManager.jsm"); +var {ExtensionParent} = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm"); ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore", "resource://gre/modules/ExtensionSettingsStore.jsm"); @@ -16,6 +17,8 @@ const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch"; const ENGINE_ADDED_SETTING_NAME = "engineAdded"; const HOMEPAGE_PREF = "browser.startup.homepage"; +const HOMEPAGE_PRIVATE_ALLOWED = "browser.startup.homepage_override.privateAllowed"; +const HOMEPAGE_EXTENSION_CONTROLLED = "browser.startup.homepage_override.extensionControlled"; const HOMEPAGE_CONFIRMED_TYPE = "homepageNotification"; const HOMEPAGE_SETTING_TYPE = "prefs"; const HOMEPAGE_SETTING_NAME = "homepage_override"; @@ -186,12 +189,36 @@ this.chrome_settings_overrides = class extends ExtensionAPI { // We need to add the listener here too since onPrefsChanged won't trigger on a // restart (the prefs are already set). if (inControl) { + Services.prefs.setBoolPref(HOMEPAGE_PRIVATE_ALLOWED, extension.privateBrowsingAllowed); + // Also set this now as an upgraded browser will need this. + Services.prefs.setBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, true); if (extension.startupReason == "APP_STARTUP") { handleInitialHomepagePopup(extension.id, homepageUrl); } else { homepagePopup.addObserver(extension.id); } } + + // We need to monitor permission change and update the preferences. + // eslint-disable-next-line mozilla/balanced-listeners + extension.on("add-permissions", async (ignoreEvent, permissions) => { + if (permissions.permissions.includes("internal:privateBrowsingAllowed")) { + let item = await ExtensionPreferencesManager.getSetting("homepage_override"); + if (item.id == extension.id) { + Services.prefs.setBoolPref(HOMEPAGE_PRIVATE_ALLOWED, true); + } + } + }); + // eslint-disable-next-line mozilla/balanced-listeners + extension.on("remove-permissions", async (ignoreEvent, permissions) => { + if (permissions.permissions.includes("internal:privateBrowsingAllowed")) { + let item = await ExtensionPreferencesManager.getSetting("homepage_override"); + if (item.id == extension.id) { + Services.prefs.setBoolPref(HOMEPAGE_PRIVATE_ALLOWED, false); + } + } + }); + extension.callOnClose({ close: () => { if (extension.shutdownReason == "ADDON_DISABLE") { @@ -344,6 +371,7 @@ this.chrome_settings_overrides = class extends ExtensionAPI { ExtensionPreferencesManager.addSetting("homepage_override", { prefNames: [ HOMEPAGE_PREF, + HOMEPAGE_EXTENSION_CONTROLLED, ], // ExtensionPreferencesManager will call onPrefsChanged when control changes // and it updates the preferences. We are passed the item from @@ -353,13 +381,21 @@ ExtensionPreferencesManager.addSetting("homepage_override", { onPrefsChanged(item) { if (item.id) { homepagePopup.addObserver(item.id); + + let policy = ExtensionParent.WebExtensionPolicy.getByID(item.id); + Services.prefs.setBoolPref(HOMEPAGE_PRIVATE_ALLOWED, policy && policy.privateBrowsingAllowed); + Services.prefs.setBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, true); } else { homepagePopup.removeObserver(); + + Services.prefs.clearUserPref(HOMEPAGE_PRIVATE_ALLOWED); + Services.prefs.clearUserPref(HOMEPAGE_EXTENSION_CONTROLLED); } }, setCallback(value) { return { [HOMEPAGE_PREF]: value, + [HOMEPAGE_EXTENSION_CONTROLLED]: !!value, }; }, }); diff --git a/browser/components/extensions/parent/ext-url-overrides.js b/browser/components/extensions/parent/ext-url-overrides.js index 86d2405bc341..fea9ea813552 100644 --- a/browser/components/extensions/parent/ext-url-overrides.js +++ b/browser/components/extensions/parent/ext-url-overrides.js @@ -4,6 +4,8 @@ "use strict"; +var {ExtensionParent} = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm"); + ChromeUtils.defineModuleGetter(this, "ExtensionControlledPopup", "resource:///modules/ExtensionControlledPopup.jsm"); ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore", @@ -16,6 +18,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService", const STORE_TYPE = "url_overrides"; const NEW_TAB_SETTING_NAME = "newTabURL"; const NEW_TAB_CONFIRMED_TYPE = "newTabNotification"; +const NEW_TAB_PRIVATE_ALLOWED = "browser.newtab.privateAllowed"; +const NEW_TAB_EXTENSION_CONTROLLED = "browser.newtab.extensionControlled"; XPCOMUtils.defineLazyGetter(this, "newTabPopup", () => { return new ExtensionControlledPopup({ @@ -60,12 +64,34 @@ XPCOMUtils.defineLazyGetter(this, "newTabPopup", () => { function setNewTabURL(extensionId, url) { if (extensionId) { newTabPopup.addObserver(extensionId); + let policy = ExtensionParent.WebExtensionPolicy.getByID(extensionId); + Services.prefs.setBoolPref(NEW_TAB_PRIVATE_ALLOWED, policy && policy.privateBrowsingAllowed); + Services.prefs.setBoolPref(NEW_TAB_EXTENSION_CONTROLLED, true); } else { newTabPopup.removeObserver(); + Services.prefs.clearUserPref(NEW_TAB_PRIVATE_ALLOWED); + Services.prefs.clearUserPref(NEW_TAB_EXTENSION_CONTROLLED); + } + if (url) { + aboutNewTabService.newTabURL = url; } - aboutNewTabService.newTabURL = url; } +// eslint-disable-next-line mozilla/balanced-listeners +ExtensionParent.apiManager.on("extension-setting-changed", async (eventName, setting) => { + let extensionId, url; + if (setting.type === STORE_TYPE && setting.key === NEW_TAB_SETTING_NAME) { + if (setting.action === "enable" || setting.item) { + let {item} = setting; + // If setting.item exists, it is the new value. If it doesn't exist, and an + // extension is being enabled, we use the id. + extensionId = (item && item.id) || setting.id; + url = item && (item.value || item.initialValue); + } + } + setNewTabURL(extensionId, url); +}); + this.urlOverrides = class extends ExtensionAPI { static onUninstall(id) { // TODO: This can be removed once bug 1438364 is fixed and all data is cleaned up. @@ -74,10 +100,7 @@ this.urlOverrides = class extends ExtensionAPI { processNewTabSetting(action) { let {extension} = this; - let item = ExtensionSettingsStore[action](extension.id, STORE_TYPE, NEW_TAB_SETTING_NAME); - if (item) { - setNewTabURL(item.id, item.value || item.initialValue); - } + ExtensionSettingsStore[action](extension.id, STORE_TYPE, NEW_TAB_SETTING_NAME); } async onManifestEntry(entryName) { @@ -125,6 +148,26 @@ this.urlOverrides = class extends ExtensionAPI { if (item) { setNewTabURL(item.id, item.value || item.initialValue); } + + // We need to monitor permission change and update the preferences. + // eslint-disable-next-line mozilla/balanced-listeners + extension.on("add-permissions", async (ignoreEvent, permissions) => { + if (permissions.permissions.includes("internal:privateBrowsingAllowed")) { + let item = await ExtensionSettingsStore.getSetting(STORE_TYPE, NEW_TAB_SETTING_NAME); + if (item && item.id == extension.id) { + Services.prefs.setBoolPref(NEW_TAB_PRIVATE_ALLOWED, true); + } + } + }); + // eslint-disable-next-line mozilla/balanced-listeners + extension.on("remove-permissions", async (ignoreEvent, permissions) => { + if (permissions.permissions.includes("internal:privateBrowsingAllowed")) { + let item = await ExtensionSettingsStore.getSetting(STORE_TYPE, NEW_TAB_SETTING_NAME); + if (item && item.id == extension.id) { + Services.prefs.setBoolPref(NEW_TAB_PRIVATE_ALLOWED, false); + } + } + }); } } }; diff --git a/browser/components/extensions/test/browser/browser-private.ini b/browser/components/extensions/test/browser/browser-private.ini index c9d5c7d2353a..13effc5c7e2f 100644 --- a/browser/components/extensions/test/browser/browser-private.ini +++ b/browser/components/extensions/test/browser/browser-private.ini @@ -8,3 +8,4 @@ support-files = head.js [browser_ext_tabs_cookieStoreId_private.js] +[browser_ext_tabs_newtab_private.js] diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js b/browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js new file mode 100644 index 000000000000..ee497884872c --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js @@ -0,0 +1,79 @@ +"use strict"; + +const {GlobalManager} = ChromeUtils.import("resource://gre/modules/Extension.jsm", null); +const {ExtensionPermissions} = ChromeUtils.import("resource://gre/modules/ExtensionPermissions.jsm"); + +const NEWTAB_PRIVATE_ALLOWED = "browser.newtab.privateAllowed"; +const NEWTAB_EXTENSION_CONTROLLED = "browser.newtab.extensionControlled"; +const NEWTAB_URI = "webext-newtab-1.html"; + +function promisePrefChange(pref) { + return new Promise((resolve, reject) => { + Services.prefs.addObserver(pref, function observer() { + Services.prefs.removeObserver(pref, observer); + resolve(arguments); + }); + }); +} + +function verifyPrefSettings(controlled, allowed) { + is(Services.prefs.getBoolPref(NEWTAB_EXTENSION_CONTROLLED, false), controlled, "newtab extension controlled"); + is(Services.prefs.getBoolPref(NEWTAB_PRIVATE_ALLOWED, false), allowed, "newtab private permission after permission change"); + + if (controlled) { + ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI), "Newtab url is overridden by the extension."); + } + if (controlled && allowed) { + ok(BROWSER_NEW_TAB_URL.endsWith(NEWTAB_URI), "active newtab url is overridden by the extension."); + } else { + let expectednewTab = controlled ? "about:privatebrowsing" : "about:newtab"; + is(BROWSER_NEW_TAB_URL, expectednewTab, "active newtab url is default."); + } +} + +async function promiseUpdatePrivatePermission(allowed, extension) { + info(`update private allowed permission`); + let ext = GlobalManager.extensionMap.get(extension.id); + await Promise.all([ + promisePrefChange(NEWTAB_PRIVATE_ALLOWED), + ExtensionPermissions[allowed ? "add" : "remove"](extension.id, + {permissions: ["internal:privateBrowsingAllowed"], origins: []}, + ext), + ]); + + verifyPrefSettings(true, allowed); +} + +add_task(async function test_new_tab_private() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "applications": { + "gecko": { + "id": "@private-newtab", + }, + }, + "chrome_url_overrides": { + newtab: NEWTAB_URI, + }, + }, + files: { + NEWTAB_URI: ` + +
+ + + + + + `, + }, + useAddonManager: "permanent", + }); + await extension.startup(); + + verifyPrefSettings(true, false); + + promiseUpdatePrivatePermission(true, extension); + + await extension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js b/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js new file mode 100644 index 000000000000..e6ca0e280f52 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js @@ -0,0 +1,117 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm"); +const {HomePage} = ChromeUtils.import("resource:///modules/HomePage.jsm"); +const {ExtensionPermissions} = ChromeUtils.import("resource://gre/modules/ExtensionPermissions.jsm"); + +const { + createAppInfo, + promiseShutdownManager, + promiseStartupManager, +} = AddonTestUtils; + +const EXTENSION_ID = "test_overrides@tests.mozilla.org"; +const HOMEPAGE_EXTENSION_CONTROLLED = "browser.startup.homepage_override.extensionControlled"; +const HOMEPAGE_PRIVATE_ALLOWED = "browser.startup.homepage_override.privateAllowed"; +const HOMEPAGE_URL_PREF = "browser.startup.homepage"; +const HOMEPAGE_URI = "webext-homepage-1.html"; + +Services.prefs.setBoolPref("browser.privatebrowsing.autostart", true); +Services.prefs.setBoolPref("extensions.allowPrivateBrowsingByDefault", false); + +AddonTestUtils.init(this); +AddonTestUtils.usePrivilegedSignatures = false; +AddonTestUtils.overrideCertDB(); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +function promisePrefChange(pref) { + return new Promise((resolve, reject) => { + Services.prefs.addObserver(pref, function observer() { + Services.prefs.removeObserver(pref, observer); + resolve(arguments); + }); + }); +} + +let defaultHomepageURL; + +function verifyPrefSettings(controlled, allowed) { + equal(Services.prefs.getBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, false), controlled, "homepage extension controlled"); + equal(Services.prefs.getBoolPref(HOMEPAGE_PRIVATE_ALLOWED, false), allowed, "homepage private permission after permission change"); + + if (controlled && allowed) { + ok(HomePage.get().endsWith(HOMEPAGE_URI), "Home page url is overridden by the extension"); + } else { + equal(HomePage.get(), defaultHomepageURL, "Home page url is default."); + } +} + +async function promiseUpdatePrivatePermission(allowed, extension) { + info(`update private allowed permission`); + await Promise.all([ + promisePrefChange(HOMEPAGE_PRIVATE_ALLOWED), + ExtensionPermissions[allowed ? "add" : "remove"](extension.id, + {permissions: ["internal:privateBrowsingAllowed"], origins: []}, + extension), + ]); + + verifyPrefSettings(true, allowed); +} + +add_task(async function test_overrides_private() { + await promiseStartupManager(); + + let extensionInfo = { + useAddonManager: "permanent", + manifest: { + "version": "1.0", + "applications": { + "gecko": { + "id": EXTENSION_ID, + }, + }, + "chrome_settings_overrides": { + "homepage": HOMEPAGE_URI, + }, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionInfo); + + defaultHomepageURL = HomePage.get(); + + await extension.startup(); + + verifyPrefSettings(true, false); + + equal(HomePage.get(), defaultHomepageURL, "Home page url is default."); + + info("add permission to extension"); + await promiseUpdatePrivatePermission(true, extension.extension); + info("remove permission from extension"); + await promiseUpdatePrivatePermission(false, extension.extension); + // set back to true to test upgrade removing extension control + info("add permission back to prepare for upgrade test"); + await promiseUpdatePrivatePermission(true, extension.extension); + + extensionInfo.manifest = { + "version": "2.0", + "applications": { + "gecko": { + "id": EXTENSION_ID, + }, + }, + }; + + await Promise.all([ + promisePrefChange(HOMEPAGE_URL_PREF), + extension.upgrade(extensionInfo), + ]); + + verifyPrefSettings(false, false); + + await extension.unload(); + await promiseShutdownManager(); +}); diff --git a/browser/components/extensions/test/xpcshell/xpcshell-common.ini b/browser/components/extensions/test/xpcshell/xpcshell-common.ini index 68578ac701ca..43f1087b0168 100644 --- a/browser/components/extensions/test/xpcshell/xpcshell-common.ini +++ b/browser/components/extensions/test/xpcshell/xpcshell-common.ini @@ -12,4 +12,5 @@ [test_ext_settings_overrides_shutdown.js] [test_ext_url_overrides_newtab.js] [test_ext_url_overrides_newtab_update.js] +[test_ext_homepage_overrides_private.js] diff --git a/browser/modules/HomePage.jsm b/browser/modules/HomePage.jsm index eaa60fc68638..f15814beafe8 100644 --- a/browser/modules/HomePage.jsm +++ b/browser/modules/HomePage.jsm @@ -12,8 +12,6 @@ var EXPORTED_SYMBOLS = ["HomePage"]; const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); -ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore", - "resource://gre/modules/ExtensionSettingsStore.jsm"); const kPrefName = "browser.startup.homepage"; @@ -53,23 +51,12 @@ let HomePage = { (aWindow && PrivateBrowsingUtils.isWindowPrivate(aWindow))) { // If an extension controls the setting and does not have private // browsing permission, use the default setting. - let extensionInfo; - try { - extensionInfo = ExtensionSettingsStore.getSetting("prefs", "homepage_override"); - } catch (e) { - // ExtensionSettings may not be initialized if no extensions are enabled. If - // we have some indication that an extension controls the homepage, return - // the defaults instead. - if (homePages.includes("moz-extension://")) { - return this.getDefault(); - } - } - - if (extensionInfo) { - let policy = WebExtensionPolicy.getByID(extensionInfo.id); - if (!policy || !policy.privateBrowsingAllowed) { - return this.getDefault(); - } + let extensionControlled = Services.prefs.getBoolPref("browser.startup.homepage_override.extensionControlled", false); + let privateAllowed = Services.prefs.getBoolPref("browser.startup.homepage_override.privateAllowed", false); + // There is a potential on upgrade that the prefs are not set yet, so we double check + // for moz-extension. + if (!privateAllowed && (extensionControlled || homePages.includes("moz-extension://"))) { + return this.getDefault(); } } diff --git a/toolkit/components/extensions/ExtensionSettingsStore.jsm b/toolkit/components/extensions/ExtensionSettingsStore.jsm index a4792e5b110d..7c95c337a304 100644 --- a/toolkit/components/extensions/ExtensionSettingsStore.jsm +++ b/toolkit/components/extensions/ExtensionSettingsStore.jsm @@ -49,6 +49,8 @@ ChromeUtils.defineModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); ChromeUtils.defineModuleGetter(this, "JSONFile", "resource://gre/modules/JSONFile.jsm"); +ChromeUtils.defineModuleGetter(this, "ExtensionParent", + "resource://gre/modules/ExtensionParent.jsm"); const JSON_FILE_NAME = "extension-settings.json"; const JSON_FILE_VERSION = 2; @@ -236,7 +238,7 @@ function alterSetting(id, type, key, action) { } _store.saveSoon(); - + ExtensionParent.apiManager.emit("extension-setting-changed", {action, id, type, key, item: returnItem}); return returnItem; } @@ -399,6 +401,7 @@ var ExtensionSettingsStore = { for (let item of precedenceList) { item.enabled = false; } + ExtensionParent.apiManager.emit("extension-setting-changed", {action: "disable", type, key}); _store.saveSoon(); },