diff options
Diffstat (limited to 'browser/components/preferences/tests/browser_extension_controlled.js')
-rw-r--r-- | browser/components/preferences/tests/browser_extension_controlled.js | 1450 |
1 files changed, 1450 insertions, 0 deletions
diff --git a/browser/components/preferences/tests/browser_extension_controlled.js b/browser/components/preferences/tests/browser_extension_controlled.js new file mode 100644 index 0000000000..81793ee9ba --- /dev/null +++ b/browser/components/preferences/tests/browser_extension_controlled.js @@ -0,0 +1,1450 @@ +/* eslint-env webextensions */ + +const PROXY_PREF = "network.proxy.type"; +const HOMEPAGE_URL_PREF = "browser.startup.homepage"; +const HOMEPAGE_OVERRIDE_KEY = "homepage_override"; +const URL_OVERRIDES_TYPE = "url_overrides"; +const NEW_TAB_KEY = "newTabURL"; +const PREF_SETTING_TYPE = "prefs"; + +ChromeUtils.defineModuleGetter( + this, + "ExtensionSettingsStore", + "resource://gre/modules/ExtensionSettingsStore.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "AboutNewTab", + "resource:///modules/AboutNewTab.jsm" +); + +XPCOMUtils.defineLazyPreferenceGetter(this, "proxyType", PROXY_PREF); + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); +AddonTestUtils.initMochitest(this); + +const { ExtensionPreferencesManager } = ChromeUtils.import( + "resource://gre/modules/ExtensionPreferencesManager.jsm" +); + +const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +const CHROME_URL_ROOT = TEST_DIR + "/"; +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml"; +let sitePermissionsDialog; + +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 waitForMessageChange( + element, + cb, + opts = { attributes: true, attributeFilter: ["hidden"] } +) { + return waitForMutation(element, opts, cb); +} + +function getElement(id, doc = gBrowser.contentDocument) { + return doc.getElementById(id); +} + +function waitForMessageHidden(messageId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => target.hidden + ); +} + +function waitForMessageShown(messageId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => !target.hidden + ); +} + +function waitForEnableMessage(messageId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => target.classList.contains("extension-controlled-disabled"), + { attributeFilter: ["class"], attributes: true } + ); +} + +function waitForMessageContent(messageId, l10nId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => doc.l10n.getAttributes(target).id === l10nId, + { childList: true } + ); +} + +async function openNotificationsPermissionDialog() { + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + let doc = content.document; + let settingsButton = doc.getElementById("notificationSettingsButton"); + settingsButton.click(); + }); + + sitePermissionsDialog = await dialogOpened; + await sitePermissionsDialog.document.mozSubdialogReady; +} + +async function disableExtensionViaClick(labelId, disableButtonId, doc) { + let controlledLabel = doc.getElementById(labelId); + + let enableMessageShown = waitForEnableMessage(labelId, doc); + doc.getElementById(disableButtonId).click(); + await enableMessageShown; + + let controlledDescription = controlledLabel.querySelector("description"); + is( + doc.l10n.getAttributes(controlledDescription.querySelector("label")).id, + "extension-controlled-enable", + "The user is notified of how to enable the extension again." + ); + + // The user can dismiss the enable instructions. + let hidden = waitForMessageHidden(labelId, doc); + controlledLabel.querySelector("image:last-of-type").click(); + await hidden; +} + +async function reEnableExtension(addon, labelId) { + let controlledMessageShown = waitForMessageShown(labelId); + await addon.enable(); + await controlledMessageShown; +} + +add_task(async function testExtensionControlledHomepage() { + const ADDON_ID = "@set_homepage"; + const SECOND_ADDON_ID = "@second_set_homepage"; + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let homepagePref = () => Services.prefs.getCharPref(HOMEPAGE_URL_PREF); + let originalHomepagePref = homepagePref(); + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + let doc = gBrowser.contentDocument; + let homeModeEl = doc.getElementById("homeMode"); + let customSettingsSection = doc.getElementById("customSettings"); + + is(homeModeEl.itemCount, 3, "The menu list starts with 3 options"); + + let promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount === 4, + "wait for the addon option to be added as an option in the menu list" + ); + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "set_homepage", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + }, + }, + chrome_settings_overrides: { homepage: "/home.html" }, + }, + }); + await extension.startup(); + await promise; + + // The homepage is set to the default and the custom settings section is hidden + is(homeModeEl.disabled, false, "The homepage menulist is enabled"); + is( + customSettingsSection.hidden, + true, + "The custom settings element is hidden" + ); + + let addon = await AddonManager.getAddonByID(ADDON_ID); + is( + homeModeEl.value, + addon.id, + "the home select menu's value is set to the addon" + ); + + promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + // Set the Menu to the default value + homeModeEl.value = "0"; + homeModeEl.dispatchEvent(new Event("command")); + await promise; + is(homepagePref(), originalHomepagePref, "homepage is set back to default"); + let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "not_controllable", + "getLevelOfControl returns not_controllable." + ); + let setting = await ExtensionPreferencesManager.getSetting( + HOMEPAGE_OVERRIDE_KEY + ); + ok(!setting.value, "the setting is not set."); + + promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + // Set the menu to the addon value + homeModeEl.value = ADDON_ID; + homeModeEl.dispatchEvent(new Event("command")); + await promise; + ok( + homepagePref().startsWith("moz-extension") && + homepagePref().endsWith("home.html"), + "Home url should be provided by the extension." + ); + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("home.html"), + "The setting value is the same as the extension." + ); + + // Add a second extension, ensure it is added to the menulist and selected. + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 5, + "addon option is added as an option in the menu list" + ); + let secondExtension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "second_set_homepage", + browser_specific_settings: { + gecko: { + id: SECOND_ADDON_ID, + }, + }, + chrome_settings_overrides: { homepage: "/home2.html" }, + }, + }); + await secondExtension.startup(); + await promise; + + let secondAddon = await AddonManager.getAddonByID(SECOND_ADDON_ID); + is(homeModeEl.value, SECOND_ADDON_ID, "home menulist is set to the add-on"); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + secondAddon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("home2.html"), + "The setting value is the same as the extension." + ); + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 4, + "addon option is no longer an option in the menu list after disable, even if it was not selected" + ); + await addon.disable(); + await promise; + + // Ensure that re-enabling an addon adds it back to the menulist + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 5, + "addon option is added again to the menulist when enabled" + ); + await addon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 4, + "addon option is no longer an option in the menu list after disable" + ); + await secondAddon.disable(); + await promise; + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 5, + "addon option is added again to the menulist when enabled" + ); + await secondAddon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 3, + "addon options are no longer an option in the menu list after disabling all addons" + ); + await secondAddon.disable(); + await addon.disable(); + await promise; + + is(homeModeEl.value, "0", "addon option is not selected in the menu list"); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + secondAddon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controllable_by_this_extension", + "getLevelOfControl returns controllable_by_this_extension." + ); + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok(!setting.value, "The setting value is back to default."); + + // The homepage elements are reset to their original state. + is(homepagePref(), originalHomepagePref, "homepage is set back to default"); + is(homeModeEl.disabled, false, "The homepage menulist is enabled"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await extension.unload(); + await secondExtension.unload(); +}); + +add_task(async function testPrefLockedHomepage() { + const ADDON_ID = "@set_homepage"; + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + let homePagePref = "browser.startup.homepage"; + let buttonPrefs = [ + "pref.browser.homepage.disable_button.current_page", + "pref.browser.homepage.disable_button.bookmark_page", + "pref.browser.homepage.disable_button.restore_default", + ]; + let homeModeEl = doc.getElementById("homeMode"); + let homePageInput = doc.getElementById("homePageUrl"); + let prefs = Services.prefs.getDefaultBranch(null); + let mutationOpts = { attributes: true, attributeFilter: ["disabled"] }; + + // Helper functions. + let getButton = pref => + doc.querySelector(`.homepage-button[preference="${pref}"`); + let waitForAllMutations = () => + Promise.all( + buttonPrefs + .map(pref => waitForMutation(getButton(pref), mutationOpts)) + .concat([ + waitForMutation(homeModeEl, mutationOpts), + waitForMutation(homePageInput, mutationOpts), + ]) + ); + let getHomepage = () => + Services.prefs.getCharPref("browser.startup.homepage"); + + let originalHomepage = getHomepage(); + let extensionHomepage = "https://developer.mozilla.org/"; + let lockedHomepage = "http://www.yahoo.com"; + + let lockPrefs = () => { + buttonPrefs.forEach(pref => { + prefs.setBoolPref(pref, true); + prefs.lockPref(pref); + }); + // Do the homepage last since that's the only pref that triggers a UI update. + prefs.setCharPref(homePagePref, lockedHomepage); + prefs.lockPref(homePagePref); + }; + let unlockPrefs = () => { + buttonPrefs.forEach(pref => { + prefs.unlockPref(pref); + prefs.setBoolPref(pref, false); + }); + // Do the homepage last since that's the only pref that triggers a UI update. + prefs.unlockPref(homePagePref); + prefs.setCharPref(homePagePref, originalHomepage); + }; + + // Lock or unlock prefs then wait for all mutations to finish. + // Expects a bool indicating if we should lock or unlock. + let waitForLockMutations = lock => { + let mutationsDone = waitForAllMutations(); + if (lock) { + lockPrefs(); + } else { + unlockPrefs(); + } + return mutationsDone; + }; + + ok( + originalHomepage != extensionHomepage, + "The extension will change the homepage" + ); + + // Install an extension that sets the homepage to MDN. + let promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "set_homepage", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + }, + }, + chrome_settings_overrides: { homepage: "https://developer.mozilla.org/" }, + }, + }); + await extension.startup(); + await promise; + + // Check that everything is still disabled, homepage didn't change. + is( + getHomepage(), + extensionHomepage, + "The reported homepage is set by the extension" + ); + is( + homePageInput.value, + extensionHomepage, + "The homepage is set by the extension" + ); + + // Lock all of the prefs, wait for the UI to update. + await waitForLockMutations(true); + + // Check that everything is now disabled. + is(getHomepage(), lockedHomepage, "The reported homepage is set by the pref"); + is(homePageInput.value, lockedHomepage, "The homepage is set by the pref"); + is( + homePageInput.disabled, + true, + "The homepage is disabed when the pref is locked" + ); + + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + true, + `The ${pref} button is disabled when locked` + ); + }); + + let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + ADDON_ID, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "not_controllable", + "getLevelOfControl returns not_controllable, the pref is locked." + ); + + // Verify that the UI is selecting the extension's Id in the menulist + let unlockedPromise = TestUtils.waitForCondition( + () => homeModeEl.value == ADDON_ID, + "Homepage menulist value is equal to the extension ID" + ); + // Unlock the prefs, wait for the UI to update. + unlockPrefs(); + await unlockedPromise; + + is( + homeModeEl.disabled, + false, + "the home select element is not disabled when the pref is not locked" + ); + is( + homePageInput.disabled, + false, + "The homepage is enabled when the pref is unlocked" + ); + is( + getHomepage(), + extensionHomepage, + "The homepage is reset to extension page" + ); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + ADDON_ID, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension after prefs are unlocked." + ); + let setting = await ExtensionPreferencesManager.getSetting( + HOMEPAGE_OVERRIDE_KEY + ); + is( + setting.value, + extensionHomepage, + "The setting value is equal to the extensionHomepage." + ); + + // Uninstall the add-on. + promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + await extension.unload(); + await promise; + + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok(!setting, "The setting is gone after the addon is uninstalled."); + + // Check that everything is now enabled again. + is( + getHomepage(), + originalHomepage, + "The reported homepage is reset to original value" + ); + is(homePageInput.value, "", "The homepage is empty"); + is( + homePageInput.disabled, + false, + "The homepage is enabled after clearing lock" + ); + is( + homeModeEl.disabled, + false, + "Homepage menulist is enabled after clearing lock" + ); + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + false, + `The ${pref} button is enabled when unlocked` + ); + }); + + // Lock the prefs without an extension. + await waitForLockMutations(true); + + // Check that everything is now disabled. + is(getHomepage(), lockedHomepage, "The reported homepage is set by the pref"); + is(homePageInput.value, lockedHomepage, "The homepage is set by the pref"); + is( + homePageInput.disabled, + true, + "The homepage is disabed when the pref is locked" + ); + is( + homeModeEl.disabled, + true, + "Homepage menulist is disabled when pref is locked" + ); + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + true, + `The ${pref} button is disabled when locked` + ); + }); + + // Unlock the prefs without an extension. + await waitForLockMutations(false); + + // Check that everything is enabled again. + is( + getHomepage(), + originalHomepage, + "The homepage is reset to the original value" + ); + is(homePageInput.value, "", "The homepage is clear after being unlocked"); + is( + homePageInput.disabled, + false, + "The homepage is enabled after clearing lock" + ); + is( + homeModeEl.disabled, + false, + "Homepage menulist is enabled after clearing lock" + ); + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + false, + `The ${pref} button is enabled when unlocked` + ); + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledNewTab() { + const ADDON_ID = "@set_newtab"; + const SECOND_ADDON_ID = "@second_set_newtab"; + const DEFAULT_NEWTAB = "about:newtab"; + const NEWTAB_CONTROLLED_PREF = "browser.newtab.extensionControlled"; + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + let doc = gBrowser.contentDocument; + let newTabMenuList = doc.getElementById("newTabMode"); + // The new tab page is set to the default. + is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default"); + + let promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 3, + "addon option is added as an option in the menu list" + ); + // Install an extension that will set the new tab page. + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "set_newtab", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + }, + }, + chrome_url_overrides: { newtab: "/newtab.html" }, + }, + }); + await extension.startup(); + + await promise; + let addon = await AddonManager.getAddonByID(ADDON_ID); + + is(newTabMenuList.value, ADDON_ID, "New tab menulist is set to the add-on"); + + let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + NEW_TAB_KEY, + URL_OVERRIDES_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + let setting = ExtensionSettingsStore.getSetting( + URL_OVERRIDES_TYPE, + NEW_TAB_KEY + ); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("newtab.html"), + "The url_overrides is set by this extension" + ); + + promise = TestUtils.waitForPrefChange(NEWTAB_CONTROLLED_PREF); + // Set the menu to the default value + newTabMenuList.value = "0"; + newTabMenuList.dispatchEvent(new Event("command")); + await promise; + let newTabPref = Services.prefs.getBoolPref(NEWTAB_CONTROLLED_PREF, false); + is(newTabPref, false, "the new tab is not controlled"); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + NEW_TAB_KEY, + URL_OVERRIDES_TYPE + ); + is( + levelOfControl, + "not_controllable", + "getLevelOfControl returns not_controllable." + ); + setting = ExtensionSettingsStore.getSetting(URL_OVERRIDES_TYPE, NEW_TAB_KEY); + ok(!setting.value, "The url_overrides is not set by this extension"); + + promise = TestUtils.waitForPrefChange(NEWTAB_CONTROLLED_PREF); + // Set the menu to a the addon value + newTabMenuList.value = ADDON_ID; + newTabMenuList.dispatchEvent(new Event("command")); + await promise; + newTabPref = Services.prefs.getBoolPref(NEWTAB_CONTROLLED_PREF, false); + is(newTabPref, true, "the new tab is controlled"); + + // Add a second extension, ensure it is added to the menulist and selected. + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 4, + "addon option is added as an option in the menu list" + ); + let secondExtension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "second_set_newtab", + browser_specific_settings: { + gecko: { + id: SECOND_ADDON_ID, + }, + }, + chrome_url_overrides: { newtab: "/newtab2.html" }, + }, + }); + await secondExtension.startup(); + await promise; + let secondAddon = await AddonManager.getAddonByID(SECOND_ADDON_ID); + is( + newTabMenuList.value, + SECOND_ADDON_ID, + "New tab menulist is set to the add-on" + ); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + secondAddon.id, + NEW_TAB_KEY, + URL_OVERRIDES_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + setting = ExtensionSettingsStore.getSetting(URL_OVERRIDES_TYPE, NEW_TAB_KEY); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("newtab2.html"), + "The url_overrides is set by the second extension" + ); + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 3, + "addon option is no longer an option in the menu list after disable, even if it was not selected" + ); + await addon.disable(); + await promise; + + // Ensure that re-enabling an addon adds it back to the menulist + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 4, + "addon option is added again to the menulist when enabled" + ); + await addon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 3, + "addon option is no longer an option in the menu list after disable" + ); + await secondAddon.disable(); + await promise; + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 4, + "addon option is added again to the menulist when enabled" + ); + await secondAddon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 2, + "addon options are all removed after disabling all" + ); + await addon.disable(); + await secondAddon.disable(); + await promise; + is( + AboutNewTab.newTabURL, + DEFAULT_NEWTAB, + "new tab page is set back to default" + ); + + // Cleanup the tabs and add-on. + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await extension.unload(); + await secondExtension.unload(); +}); + +add_task(async function testExtensionControlledWebNotificationsPermission() { + let manifest = { + manifest_version: 2, + name: "TestExtension", + version: "1.0", + description: "Testing WebNotificationsDisable", + browser_specific_settings: { gecko: { id: "@web_notifications_disable" } }, + permissions: ["browserSettings"], + browser_action: { + default_title: "Testing", + }, + }; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openNotificationsPermissionDialog(); + + let doc = sitePermissionsDialog.document; + let extensionControlledContent = doc.getElementById( + "browserNotificationsPermissionExtensionContent" + ); + + // Test that extension content is initially hidden. + ok( + extensionControlledContent.hidden, + "Extension content is initially hidden" + ); + + // Install an extension that will disable web notifications permission. + let messageShown = waitForMessageShown( + "browserNotificationsPermissionExtensionContent", + doc + ); + let extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "permanent", + background() { + browser.browserSettings.webNotificationsDisabled.set({ value: true }); + browser.test.sendMessage("load-extension"); + }, + }); + await extension.startup(); + await extension.awaitMessage("load-extension"); + await messageShown; + + let controlledDesc = extensionControlledContent.querySelector("description"); + Assert.deepEqual( + doc.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-web-notifications", + args: { + name: "TestExtension", + }, + }, + "The user is notified that an extension is controlling the web notifications permission" + ); + is( + extensionControlledContent.hidden, + false, + "The extension controlled row is not hidden" + ); + + // Disable the extension. + doc.getElementById("disableNotificationsPermissionExtension").click(); + + // Verify the user is notified how to enable the extension. + await waitForEnableMessage(extensionControlledContent.id, doc); + is( + doc.l10n.getAttributes(controlledDesc.querySelector("label")).id, + "extension-controlled-enable", + "The user is notified of how to enable the extension again" + ); + + // Verify the enable message can be dismissed. + let hidden = waitForMessageHidden(extensionControlledContent.id, doc); + let dismissButton = controlledDesc.querySelector("image:last-of-type"); + dismissButton.click(); + await hidden; + + // Verify that the extension controlled content in hidden again. + is( + extensionControlledContent.hidden, + true, + "The extension controlled row is now hidden" + ); + + await extension.unload(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledHomepageUninstalledAddon() { + async function checkHomepageEnabled() { + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + // The homepage is enabled. + let homepageInput = doc.getElementById("homePageUrl"); + is(homepageInput.disabled, false, "The homepage input is enabled"); + is(homepageInput.value, "", "The homepage input is empty"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + + await ExtensionSettingsStore.initialize(); + + // Verify the setting isn't reported as controlled and the inputs are enabled. + is( + ExtensionSettingsStore.getSetting("prefs", "homepage_override"), + null, + "The homepage_override is not set" + ); + await checkHomepageEnabled(); + + // Disarm any pending writes before we modify the JSONFile directly. + await ExtensionSettingsStore._reloadFile(false); + + // Write out a bad store file. + let storeData = { + prefs: { + homepage_override: { + initialValue: "", + precedenceList: [ + { + id: "bad@mochi.test", + installDate: 1508802672, + value: "https://developer.mozilla.org", + enabled: true, + }, + ], + }, + }, + }; + let jsonFileName = "extension-settings.json"; + let storePath = PathUtils.join(PathUtils.profileDir, jsonFileName); + + await IOUtils.writeUTF8(storePath, JSON.stringify(storeData)); + + // Reload the ExtensionSettingsStore so it will read the file on disk. Don't + // finalize the current store since it will overwrite our file. + await ExtensionSettingsStore._reloadFile(false); + + // Verify that the setting is reported as set, but the homepage is still enabled + // since there is no matching installed extension. + is( + ExtensionSettingsStore.getSetting("prefs", "homepage_override").value, + "https://developer.mozilla.org", + "The homepage_override appears to be set" + ); + await checkHomepageEnabled(); + + // Remove the bad store file that we used. + await IOUtils.remove(storePath); + + // Reload the ExtensionSettingsStore again so it clears the data we added. + // Don't finalize the current store since it will write out the bad data. + await ExtensionSettingsStore._reloadFile(false); + + is( + ExtensionSettingsStore.getSetting("prefs", "homepage_override"), + null, + "The ExtensionSettingsStore is left empty." + ); +}); + +add_task(async function testExtensionControlledTrackingProtection() { + const TP_PREF = "privacy.trackingprotection.enabled"; + const TP_DEFAULT = false; + const EXTENSION_ID = "@set_tp"; + const CONTROLLED_LABEL_ID = + "contentBlockingTrackingProtectionExtensionContentLabel"; + const CONTROLLED_BUTTON_ID = + "contentBlockingDisableTrackingProtectionExtension"; + + let tpEnabledPref = () => Services.prefs.getBoolPref(TP_PREF); + + await SpecialPowers.pushPrefEnv({ set: [[TP_PREF, TP_DEFAULT]] }); + + function background() { + browser.privacy.websites.trackingProtectionMode.set({ value: "always" }); + } + + function verifyState(isControlled) { + is(tpEnabledPref(), isControlled, "TP pref is set to the expected value."); + + let controlledLabel = doc.getElementById(CONTROLLED_LABEL_ID); + let controlledButton = doc.getElementById(CONTROLLED_BUTTON_ID); + + is( + controlledLabel.hidden, + !isControlled, + "The extension controlled row's visibility is as expected." + ); + is( + controlledButton.hidden, + !isControlled, + "The disable extension button's visibility is as expected." + ); + if (isControlled) { + let controlledDesc = controlledLabel.querySelector("description"); + Assert.deepEqual( + doc.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-websites-content-blocking-all-trackers", + args: { + name: "set_tp", + }, + }, + "The user is notified that an extension is controlling TP." + ); + } + + is( + doc.getElementById("trackingProtectionMenu").disabled, + isControlled, + "TP control is enabled." + ); + } + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + is( + gBrowser.currentURI.spec, + "about:preferences#privacy", + "#privacy should be in the URI for about:preferences" + ); + + verifyState(false); + + // Install an extension that sets Tracking Protection. + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + name: "set_tp", + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + permissions: ["privacy"], + }, + background, + }); + + let messageShown = waitForMessageShown(CONTROLLED_LABEL_ID); + await extension.startup(); + await messageShown; + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + + verifyState(true); + + await disableExtensionViaClick( + CONTROLLED_LABEL_ID, + CONTROLLED_BUTTON_ID, + doc + ); + + verifyState(false); + + // Enable the extension so we get the UNINSTALL event, which is needed by + // ExtensionPreferencesManager to clean up properly. + // TODO: BUG 1408226 + await reEnableExtension(addon, CONTROLLED_LABEL_ID); + + await extension.unload(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledPasswordManager() { + const PASSWORD_MANAGER_ENABLED_PREF = "signon.rememberSignons"; + const PASSWORD_MANAGER_ENABLED_DEFAULT = true; + const CONTROLLED_BUTTON_ID = "disablePasswordManagerExtension"; + const CONTROLLED_LABEL_ID = "passwordManagerExtensionContent"; + const EXTENSION_ID = "@remember_signons"; + let manifest = { + manifest_version: 2, + name: "testPasswordManagerExtension", + version: "1.0", + description: "Testing rememberSignons", + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + permissions: ["privacy"], + browser_action: { + default_title: "Testing rememberSignons", + }, + }; + + let passwordManagerEnabledPref = () => + Services.prefs.getBoolPref(PASSWORD_MANAGER_ENABLED_PREF); + + await SpecialPowers.pushPrefEnv({ + set: [[PASSWORD_MANAGER_ENABLED_PREF, PASSWORD_MANAGER_ENABLED_DEFAULT]], + }); + is( + passwordManagerEnabledPref(), + true, + "Password manager is enabled by default." + ); + + function verifyState(isControlled) { + is( + passwordManagerEnabledPref(), + !isControlled, + "Password manager pref is set to the expected value." + ); + let controlledLabel = gBrowser.contentDocument.getElementById( + CONTROLLED_LABEL_ID + ); + let controlledButton = gBrowser.contentDocument.getElementById( + CONTROLLED_BUTTON_ID + ); + is( + controlledLabel.hidden, + !isControlled, + "The extension's controlled row visibility is as expected." + ); + is( + controlledButton.hidden, + !isControlled, + "The extension's controlled button visibility is as expected." + ); + if (isControlled) { + let controlledDesc = controlledLabel.querySelector("description"); + Assert.deepEqual( + gBrowser.contentDocument.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-password-saving", + args: { + name: "testPasswordManagerExtension", + }, + }, + "The user is notified that an extension is controlling the remember signons pref." + ); + } + } + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + info("Verify that no extension is controlling the password manager pref."); + verifyState(false); + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "permanent", + background() { + browser.privacy.services.passwordSavingEnabled.set({ value: false }); + }, + }); + let messageShown = waitForMessageShown(CONTROLLED_LABEL_ID); + await extension.startup(); + await messageShown; + + info( + "Verify that the test extension is controlling the password manager pref." + ); + verifyState(true); + + info("Verify that the extension shows as controlled when loaded again."); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + verifyState(true); + + await disableExtensionViaClick( + CONTROLLED_LABEL_ID, + CONTROLLED_BUTTON_ID, + gBrowser.contentDocument + ); + + info( + "Verify that disabling the test extension removes the lock on the password manager pref." + ); + verifyState(false); + + await extension.unload(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledProxyConfig() { + const proxySvc = Ci.nsIProtocolProxyService; + const PROXY_DEFAULT = proxySvc.PROXYCONFIG_SYSTEM; + const EXTENSION_ID = "@set_proxy"; + const CONTROLLED_SECTION_ID = "proxyExtensionContent"; + const CONTROLLED_BUTTON_ID = "disableProxyExtension"; + const CONNECTION_SETTINGS_DESC_ID = "connectionSettingsDescription"; + const PANEL_URL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + + await SpecialPowers.pushPrefEnv({ set: [[PROXY_PREF, PROXY_DEFAULT]] }); + + function background() { + browser.proxy.settings.set({ value: { proxyType: "none" } }); + } + + function expectedConnectionSettingsMessage(doc, isControlled) { + return isControlled + ? "extension-controlling-proxy-config" + : "network-proxy-connection-description"; + } + + function connectionSettingsMessagePromise(doc, isControlled) { + return waitForMessageContent( + CONNECTION_SETTINGS_DESC_ID, + expectedConnectionSettingsMessage(doc, isControlled), + doc + ); + } + + function verifyProxyState(doc, isControlled) { + let isPanel = doc.getElementById(CONTROLLED_BUTTON_ID); + is( + proxyType === proxySvc.PROXYCONFIG_DIRECT, + isControlled, + "Proxy pref is set to the expected value." + ); + + if (isPanel) { + let controlledSection = doc.getElementById(CONTROLLED_SECTION_ID); + + is( + controlledSection.hidden, + !isControlled, + "The extension controlled row's visibility is as expected." + ); + if (isPanel) { + is( + doc.getElementById(CONTROLLED_BUTTON_ID).hidden, + !isControlled, + "The disable extension button's visibility is as expected." + ); + } + if (isControlled) { + let controlledDesc = controlledSection.querySelector("description"); + Assert.deepEqual( + doc.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-proxy-config", + args: { + name: "set_proxy", + }, + }, + "The user is notified that an extension is controlling proxy settings." + ); + } + function getProxyControls() { + let controlGroup = doc.getElementById("networkProxyType"); + let manualControlContainer = controlGroup.querySelector("#proxy-grid"); + return { + manualControls: [ + ...manualControlContainer.querySelectorAll( + "label[data-l10n-id]:not([control=networkProxyNone])" + ), + ...manualControlContainer.querySelectorAll("input"), + ...manualControlContainer.querySelectorAll("checkbox"), + ...doc.querySelectorAll("#networkProxySOCKSVersion > radio"), + ], + pacControls: [doc.getElementById("networkProxyAutoconfigURL")], + otherControls: [ + doc.querySelector("label[control=networkProxyNone]"), + doc.getElementById("networkProxyNone"), + ...controlGroup.querySelectorAll(":scope > radio"), + ...doc.querySelectorAll("#ConnectionsDialogPane > checkbox"), + ], + }; + } + let controlState = isControlled ? "disabled" : "enabled"; + let controls = getProxyControls(); + for (let element of controls.manualControls) { + let disabled = + isControlled || proxyType !== proxySvc.PROXYCONFIG_MANUAL; + is( + element.disabled, + disabled, + `Manual proxy controls should be ${controlState} - control: ${element.outerHTML}.` + ); + } + for (let element of controls.pacControls) { + let disabled = isControlled || proxyType !== proxySvc.PROXYCONFIG_PAC; + is( + element.disabled, + disabled, + `PAC proxy controls should be ${controlState} - control: ${element.outerHTML}.` + ); + } + for (let element of controls.otherControls) { + is( + element.disabled, + isControlled, + `Other proxy controls should be ${controlState} - control: ${element.outerHTML}.` + ); + } + } else { + let elem = doc.getElementById(CONNECTION_SETTINGS_DESC_ID); + is( + doc.l10n.getAttributes(elem).id, + expectedConnectionSettingsMessage(doc, isControlled), + "The connection settings description is as expected." + ); + } + } + + async function reEnableProxyExtension(addon) { + let messageChanged = connectionSettingsMessagePromise(mainDoc, true); + await addon.enable(); + await messageChanged; + } + + async function openProxyPanel() { + let panel = await openAndLoadSubDialog(PANEL_URL); + let closingPromise = BrowserTestUtils.waitForEvent( + panel.document.getElementById("ConnectionsDialog"), + "dialogclosing" + ); + ok(panel, "Proxy panel opened."); + return { panel, closingPromise }; + } + + async function closeProxyPanel(panelObj) { + let dialog = panelObj.panel.document.getElementById("ConnectionsDialog"); + dialog.cancelDialog(); + let panelClosingEvent = await panelObj.closingPromise; + ok(panelClosingEvent, "Proxy panel closed."); + } + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let mainDoc = gBrowser.contentDocument; + + is( + gBrowser.currentURI.spec, + "about:preferences#general", + "#general should be in the URI for about:preferences" + ); + + verifyProxyState(mainDoc, false); + + // Open the connections panel. + let panelObj = await openProxyPanel(); + let panelDoc = panelObj.panel.document; + + verifyProxyState(panelDoc, false); + + await closeProxyPanel(panelObj); + + verifyProxyState(mainDoc, false); + + // Install an extension that controls proxy settings. The extension needs + // incognitoOverride because controlling the proxy.settings requires private + // browsing access. + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + useAddonManager: "permanent", + manifest: { + name: "set_proxy", + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + permissions: ["proxy"], + }, + background, + }); + + let messageChanged = connectionSettingsMessagePromise(mainDoc, true); + await extension.startup(); + await messageChanged; + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + + verifyProxyState(mainDoc, true); + messageChanged = connectionSettingsMessagePromise(mainDoc, false); + + panelObj = await openProxyPanel(); + panelDoc = panelObj.panel.document; + + verifyProxyState(panelDoc, true); + + await disableExtensionViaClick( + CONTROLLED_SECTION_ID, + CONTROLLED_BUTTON_ID, + panelDoc + ); + + verifyProxyState(panelDoc, false); + + await closeProxyPanel(panelObj); + await messageChanged; + + verifyProxyState(mainDoc, false); + + await reEnableProxyExtension(addon); + + verifyProxyState(mainDoc, true); + messageChanged = connectionSettingsMessagePromise(mainDoc, false); + + panelObj = await openProxyPanel(); + panelDoc = panelObj.panel.document; + + verifyProxyState(panelDoc, true); + + await disableExtensionViaClick( + CONTROLLED_SECTION_ID, + CONTROLLED_BUTTON_ID, + panelDoc + ); + + verifyProxyState(panelDoc, false); + + await closeProxyPanel(panelObj); + await messageChanged; + + verifyProxyState(mainDoc, false); + + // Enable the extension so we get the UNINSTALL event, which is needed by + // ExtensionPreferencesManager to clean up properly. + // TODO: BUG 1408226 + await reEnableProxyExtension(addon); + + await extension.unload(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test that the newtab menu selection is correct when loading about:preferences +add_task(async function testMenuSyncFromPrefs() { + const DEFAULT_NEWTAB = "about:newtab"; + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + let doc = gBrowser.contentDocument; + let newTabMenuList = doc.getElementById("newTabMode"); + // The new tab page is set to the default. + is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default"); + + is(newTabMenuList.value, "0", "New tab menulist is set to the default"); + + newTabMenuList.value = "1"; + newTabMenuList.dispatchEvent(new Event("command")); + is(newTabMenuList.value, "1", "New tab menulist is set to blank"); + + gBrowser.reloadTab(gBrowser.selectedTab); + + await TestUtils.waitForCondition( + () => gBrowser.contentDocument.getElementById("newTabMode"), + "wait until element exists in new contentDoc" + ); + + is( + gBrowser.contentDocument.getElementById("newTabMode").value, + "1", + "New tab menulist is still set to blank" + ); + + // Cleanup + newTabMenuList.value = "0"; + newTabMenuList.dispatchEvent(new Event("command")); + is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); |