diff options
Diffstat (limited to '')
25 files changed, 3525 insertions, 0 deletions
diff --git a/browser/base/content/test/permissions/browser.toml b/browser/base/content/test/permissions/browser.toml new file mode 100644 index 0000000000..31892b0358 --- /dev/null +++ b/browser/base/content/test/permissions/browser.toml @@ -0,0 +1,51 @@ +[DEFAULT] +support-files = [ + "head.js", + "permissions.html", + "temporary_permissions_subframe.html", + "temporary_permissions_frame.html", +] + +["browser_autoplay_blocked.js"] +support-files = [ + "browser_autoplay_blocked.html", + "browser_autoplay_blocked_slow.sjs", + "browser_autoplay_js.html", + "browser_autoplay_muted.html", + "../general/audio.ogg", +] +skip-if = ["true"] # Bug 1538602 + +["browser_canvas_fingerprinting_resistance.js"] +skip-if = [ + "debug", + "os == 'linux' && asan", # Bug 1522069 +] + +["browser_canvas_rfp_exclusion.js"] + +["browser_permission_delegate_geo.js"] + +["browser_permissions.js"] + +["browser_permissions_delegate_vibrate.js"] +support-files = ["empty.html"] + +["browser_permissions_handling_user_input.js"] +support-files = ["dummy.js"] + +["browser_permissions_postPrompt.js"] +support-files = ["dummy.js"] + +["browser_reservedkey.js"] + +["browser_site_scoped_permissions.js"] + +["browser_temporary_permissions.js"] +support-files = ["../webrtc/get_user_media.html"] + +["browser_temporary_permissions_expiry.js"] + +["browser_temporary_permissions_navigation.js"] + +["browser_temporary_permissions_tabs.js"] diff --git a/browser/base/content/test/permissions/browser_autoplay_blocked.html b/browser/base/content/test/permissions/browser_autoplay_blocked.html new file mode 100644 index 0000000000..8c3b058890 --- /dev/null +++ b/browser/base/content/test/permissions/browser_autoplay_blocked.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + <audio autoplay="autoplay" > + <source src="audio.ogg" /> + </audio> + </body> +</html> diff --git a/browser/base/content/test/permissions/browser_autoplay_blocked.js b/browser/base/content/test/permissions/browser_autoplay_blocked.js new file mode 100644 index 0000000000..d81481d6a5 --- /dev/null +++ b/browser/base/content/test/permissions/browser_autoplay_blocked.js @@ -0,0 +1,357 @@ +/* + * Test that a blocked request to autoplay media is shown to the user + */ + +const AUTOPLAY_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "browser_autoplay_blocked.html"; + +const AUTOPLAY_JS_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "browser_autoplay_js.html"; + +const SLOW_AUTOPLAY_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "browser_autoplay_blocked_slow.sjs"; + +const MUTED_AUTOPLAY_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "browser_autoplay_muted.html"; + +const EMPTY_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "empty.html"; + +const AUTOPLAY_PREF = "media.autoplay.default"; +const AUTOPLAY_PERM = "autoplay-media"; + +function autoplayBlockedIcon() { + return document.querySelector( + "#blocked-permissions-container " + + ".blocked-permission-icon.autoplay-media-icon" + ); +} + +function permissionListBlockedIcons() { + return document.querySelectorAll( + "image.permission-popup-permission-icon.blocked-permission-icon" + ); +} + +function sleep(ms) { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function blockedIconShown() { + await TestUtils.waitForCondition(() => { + return BrowserTestUtils.isVisible(autoplayBlockedIcon()); + }, "Blocked icon is shown"); +} + +async function blockedIconHidden() { + await TestUtils.waitForCondition(() => { + return BrowserTestUtils.isHidden(autoplayBlockedIcon()); + }, "Blocked icon is hidden"); +} + +function testPermListHasEntries(expectEntries) { + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let listEntryCount = permissionsList.querySelectorAll( + ".permission-popup-permission-item" + ).length; + if (expectEntries) { + ok(listEntryCount, "List of permissions is not empty"); + return; + } + ok(!listEntryCount, "List of permissions is empty"); +} + +add_setup(async function () { + registerCleanupFunction(() => { + Services.perms.removeAll(); + Services.prefs.clearUserPref(AUTOPLAY_PREF); + }); +}); + +add_task(async function testMainViewVisible() { + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.ALLOWED); + + await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function () { + ok( + BrowserTestUtils.isHidden(autoplayBlockedIcon()), + "Blocked icon not shown" + ); + + await openPermissionPopup(); + testPermListHasEntries(false); + await closePermissionPopup(); + }); + + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED); + + await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function (browser) { + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + + await blockedIconShown(); + + await openPermissionPopup(); + testPermListHasEntries(true); + + let labelText = SitePermissions.getPermissionLabel(AUTOPLAY_PERM); + let labels = permissionsList.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 1, "One permission visible in main view"); + is(labels[0].textContent, labelText, "Correct value"); + + let menulist = document.getElementById("permission-popup-menulist"); + Assert.equal(menulist.label, "Block Audio"); + + await EventUtils.synthesizeMouseAtCenter(menulist, { type: "mousedown" }); + await TestUtils.waitForCondition(() => { + return ( + menulist.getElementsByTagName("menuitem")[0].label === + "Allow Audio and Video" + ); + }); + + let menuitem = menulist.getElementsByTagName("menuitem")[0]; + Assert.equal(menuitem.getAttribute("label"), "Allow Audio and Video"); + + menuitem.click(); + menulist.menupopup.hidePopup(); + await closePermissionPopup(); + + let uri = Services.io.newURI(AUTOPLAY_PAGE); + let state = PermissionTestUtils.getPermissionObject( + uri, + AUTOPLAY_PERM + ).capability; + Assert.equal(state, Services.perms.ALLOW_ACTION); + }); + + Services.perms.removeAll(); +}); + +add_task(async function testGloballyBlockedOnNewWindow() { + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED); + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + AUTOPLAY_PAGE + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + AUTOPLAY_PAGE + ); + await blockedIconShown(); + + Assert.deepEqual( + SitePermissions.getForPrincipal( + principal, + AUTOPLAY_PERM, + tab.linkedBrowser + ), + { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_PERSISTENT, + } + ); + + let promiseWin = BrowserTestUtils.waitForNewWindow(); + gBrowser.replaceTabWithWindow(tab); + let win = await promiseWin; + tab = win.gBrowser.selectedTab; + + Assert.deepEqual( + SitePermissions.getForPrincipal( + principal, + AUTOPLAY_PERM, + tab.linkedBrowser + ), + { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_PERSISTENT, + } + ); + + SitePermissions.removeFromPrincipal( + principal, + AUTOPLAY_PERM, + tab.linkedBrowser + ); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function testBFCache() { + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED); + + await BrowserTestUtils.withNewTab("about:home", async function (browser) { + BrowserTestUtils.startLoadingURIString(browser, AUTOPLAY_PAGE); + await blockedIconShown(); + + gBrowser.goBack(); + await blockedIconHidden(); + + // Not sure why using `gBrowser.goForward()` doesn't trigger document's + // visibility changes in some debug build on try server, which makes us not + // to receive the blocked event. + await SpecialPowers.spawn(browser, [], () => { + content.history.forward(); + }); + await blockedIconShown(); + }); + + Services.perms.removeAll(); +}); + +add_task(async function testBlockedIconFromCORSIframe() { + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED); + + await BrowserTestUtils.withNewTab(EMPTY_PAGE, async browser => { + const blockedIconShownPromise = blockedIconShown(); + const CORS_AUTOPLAY_PAGE = AUTOPLAY_PAGE.replace( + "example.com", + "example.org" + ); + info(`Load CORS autoplay on an iframe`); + await SpecialPowers.spawn(browser, [CORS_AUTOPLAY_PAGE], async url => { + const iframe = content.document.createElement("iframe"); + iframe.src = url; + content.document.body.appendChild(iframe); + info("Wait until iframe finishes loading"); + await new Promise(r => (iframe.onload = r)); + }); + await blockedIconShownPromise; + ok(true, "Blocked icon shown for the CORS autoplay iframe"); + }); + + Services.perms.removeAll(); +}); + +add_task(async function testChangingBlockingSettingDuringNavigation() { + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED); + + await BrowserTestUtils.withNewTab("about:home", async function (browser) { + await blockedIconHidden(); + BrowserTestUtils.startLoadingURIString(browser, AUTOPLAY_PAGE); + await blockedIconShown(); + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.ALLOWED); + + gBrowser.goBack(); + await blockedIconHidden(); + + gBrowser.goForward(); + + // Sleep here to prevent false positives, the icon gets shown with an + // async `GloballyAutoplayBlocked` event. The sleep gives it a little + // time for it to show otherwise there is a chance it passes before it + // would have shown. + await sleep(100); + ok( + BrowserTestUtils.isHidden(autoplayBlockedIcon()), + "Blocked icon is hidden" + ); + }); + + Services.perms.removeAll(); +}); + +add_task(async function testSlowLoadingPage() { + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED); + + let tab1 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home" + ); + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + SLOW_AUTOPLAY_PAGE + ); + await blockedIconShown(); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + // Wait until the blocked icon is hidden by switching tabs + await blockedIconHidden(); + await BrowserTestUtils.switchTab(gBrowser, tab2); + await blockedIconShown(); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + + Services.perms.removeAll(); +}); + +add_task(async function testBlockedAll() { + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED_ALL); + + await BrowserTestUtils.withNewTab("about:home", async function (browser) { + await blockedIconHidden(); + BrowserTestUtils.startLoadingURIString(browser, MUTED_AUTOPLAY_PAGE); + await blockedIconShown(); + + await openPermissionPopup(); + + Assert.equal( + permissionListBlockedIcons().length, + 1, + "Blocked icon is shown" + ); + + let menulist = document.getElementById("permission-popup-menulist"); + await EventUtils.synthesizeMouseAtCenter(menulist, { type: "mousedown" }); + await TestUtils.waitForCondition(() => { + return ( + menulist.getElementsByTagName("menuitem")[1].label === "Block Audio" + ); + }); + + let menuitem = menulist.getElementsByTagName("menuitem")[0]; + menuitem.click(); + menulist.menupopup.hidePopup(); + await closePermissionPopup(); + gBrowser.reload(); + await blockedIconHidden(); + }); + Services.perms.removeAll(); +}); + +add_task(async function testMultiplePlayNotificationsFromJS() { + Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED); + + await BrowserTestUtils.withNewTab("about:home", async function (browser) { + let count = 0; + browser.addEventListener("GloballyAutoplayBlocked", function () { + is(++count, 1, "Shouldn't get more than one autoplay blocked event"); + }); + + await blockedIconHidden(); + + BrowserTestUtils.startLoadingURIString(browser, AUTOPLAY_JS_PAGE); + + await blockedIconShown(); + + // Sleep here a bit to ensure that multiple events don't arrive. + await sleep(100); + + is(count, 1, "Shouldn't have got more events"); + }); + + Services.perms.removeAll(); +}); diff --git a/browser/base/content/test/permissions/browser_autoplay_blocked_slow.sjs b/browser/base/content/test/permissions/browser_autoplay_blocked_slow.sjs new file mode 100644 index 0000000000..12929760f7 --- /dev/null +++ b/browser/base/content/test/permissions/browser_autoplay_blocked_slow.sjs @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const DELAY_MS = 200; + +const AUTOPLAY_HTML = `<!DOCTYPE HTML> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + <audio autoplay="autoplay" > + <source src="audio.ogg" /> + </audio> + <script> + document.location.href = '#foo'; + </script> + </body> +</html>`; + +function handleRequest(req, resp) { + resp.processAsync(); + resp.setHeader("Cache-Control", "no-cache", false); + resp.setHeader("Content-Type", "text/html;charset=utf-8", false); + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + resp.write(AUTOPLAY_HTML); + timer.init( + () => { + resp.write(""); + resp.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/browser/base/content/test/permissions/browser_autoplay_js.html b/browser/base/content/test/permissions/browser_autoplay_js.html new file mode 100644 index 0000000000..9782487ee9 --- /dev/null +++ b/browser/base/content/test/permissions/browser_autoplay_js.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<meta charset="utf8"> +<audio> + <source src="audio.ogg" /> +</audio> +<script> +onload = function() { + let audio = document.querySelector("audio"); + for (let i = 0; i < 100; ++i) { + audio.play(); + } +}; +</script> diff --git a/browser/base/content/test/permissions/browser_autoplay_muted.html b/browser/base/content/test/permissions/browser_autoplay_muted.html new file mode 100644 index 0000000000..4f9d1ca846 --- /dev/null +++ b/browser/base/content/test/permissions/browser_autoplay_muted.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + <audio autoplay="autoplay" muted> + <source src="audio.ogg" /> + </audio> + </body> +</html> diff --git a/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js b/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js new file mode 100644 index 0000000000..dbb2d1ea32 --- /dev/null +++ b/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js @@ -0,0 +1,383 @@ +/** + * When "privacy.resistFingerprinting" is set to true, user permission is + * required for canvas data extraction. + * This tests whether the site permission prompt for canvas data extraction + * works properly. + * When "privacy.resistFingerprinting.randomDataOnCanvasExtract" is true, + * canvas data extraction results in random data, and when it is false, canvas + * data extraction results in all-white data. + */ +"use strict"; + +const kUrl = "https://example.com/"; +const kPrincipal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(kUrl), + {} +); +const kPermission = "canvas"; + +function initTab() { + let contentWindow = content.wrappedJSObject; + + let drawCanvas = (fillStyle, id) => { + let contentDocument = contentWindow.document; + let width = 64, + height = 64; + let canvas = contentDocument.createElement("canvas"); + if (id) { + canvas.setAttribute("id", id); + } + canvas.setAttribute("width", width); + canvas.setAttribute("height", height); + contentDocument.body.appendChild(canvas); + + let context = canvas.getContext("2d"); + context.fillStyle = fillStyle; + context.fillRect(0, 0, width, height); + + if (id) { + let button = contentDocument.createElement("button"); + button.addEventListener("click", function () { + canvas.toDataURL(); + }); + button.setAttribute("id", "clickme"); + button.innerHTML = "Click Me!"; + contentDocument.body.appendChild(button); + } + + return canvas; + }; + + let placeholder = drawCanvas("white"); + contentWindow.kPlaceholderData = placeholder.toDataURL(); + let canvas = drawCanvas("cyan", "canvas-id-canvas"); + contentWindow.kPlacedData = canvas.toDataURL(); + is( + canvas.toDataURL(), + contentWindow.kPlacedData, + "privacy.resistFingerprinting = false, canvas data == placed data" + ); + isnot( + canvas.toDataURL(), + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = false, canvas data != placeholder data" + ); +} + +function enableResistFingerprinting( + randomDataOnCanvasExtract, + autoDeclineNoInput +) { + return SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + [ + "privacy.resistFingerprinting.randomDataOnCanvasExtract", + randomDataOnCanvasExtract, + ], + [ + "privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts", + autoDeclineNoInput, + ], + ], + }); +} + +function promisePopupShown() { + return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); +} + +function promisePopupHidden() { + return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden"); +} + +function extractCanvasData(randomDataOnCanvasExtract, grantPermission) { + let contentWindow = content.wrappedJSObject; + let canvas = contentWindow.document.getElementById("canvas-id-canvas"); + let canvasData = canvas.toDataURL(); + if (grantPermission) { + is( + canvasData, + contentWindow.kPlacedData, + "privacy.resistFingerprinting = true, permission granted, canvas data == placed data" + ); + if (!randomDataOnCanvasExtract) { + isnot( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission granted, canvas data != placeholderdata" + ); + } + } else if (grantPermission === false) { + isnot( + canvasData, + contentWindow.kPlacedData, + "privacy.resistFingerprinting = true, permission denied, canvas data != placed data" + ); + if (!randomDataOnCanvasExtract) { + is( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission denied, canvas data == placeholderdata" + ); + } else { + isnot( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, permission denied, canvas data != placeholderdata" + ); + } + } else { + isnot( + canvasData, + contentWindow.kPlacedData, + "privacy.resistFingerprinting = true, requesting permission, canvas data != placed data" + ); + if (!randomDataOnCanvasExtract) { + is( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, requesting permission, canvas data == placeholderdata" + ); + } else { + isnot( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, requesting permission, canvas data != placeholderdata" + ); + } + } +} + +function triggerCommand(button) { + let notifications = PopupNotifications.panel.children; + let notification = notifications[0]; + EventUtils.synthesizeMouseAtCenter(notification[button], {}); +} + +function triggerMainCommand() { + triggerCommand("button"); +} + +function triggerSecondaryCommand() { + triggerCommand("secondaryButton"); +} + +function testPermission() { + return Services.perms.testPermissionFromPrincipal(kPrincipal, kPermission); +} + +async function withNewTabNoInput( + randomDataOnCanvasExtract, + grantPermission, + browser +) { + await SpecialPowers.spawn(browser, [], initTab); + await enableResistFingerprinting(randomDataOnCanvasExtract, false); + let popupShown = promisePopupShown(); + await SpecialPowers.spawn( + browser, + [randomDataOnCanvasExtract], + extractCanvasData + ); + await popupShown; + let popupHidden = promisePopupHidden(); + if (grantPermission) { + triggerMainCommand(); + await popupHidden; + is(testPermission(), Services.perms.ALLOW_ACTION, "permission granted"); + } else { + triggerSecondaryCommand(); + await popupHidden; + is(testPermission(), Services.perms.DENY_ACTION, "permission denied"); + } + await SpecialPowers.spawn( + browser, + [randomDataOnCanvasExtract, grantPermission], + extractCanvasData + ); + await SpecialPowers.popPrefEnv(); +} + +async function doTestNoInput(randomDataOnCanvasExtract, grantPermission) { + await BrowserTestUtils.withNewTab( + kUrl, + withNewTabNoInput.bind(null, randomDataOnCanvasExtract, grantPermission) + ); + Services.perms.removeFromPrincipal(kPrincipal, kPermission); +} + +// With auto-declining disabled (not the default) +// Tests clicking "Don't Allow" button of the permission prompt. +add_task(doTestNoInput.bind(null, true, false)); +add_task(doTestNoInput.bind(null, false, false)); + +// Tests clicking "Allow" button of the permission prompt. +add_task(doTestNoInput.bind(null, true, true)); +add_task(doTestNoInput.bind(null, false, true)); + +async function withNewTabAutoBlockNoInput(randomDataOnCanvasExtract, browser) { + await SpecialPowers.spawn(browser, [], initTab); + await enableResistFingerprinting(randomDataOnCanvasExtract, true); + + let noShowHandler = () => { + ok(false, "The popup notification should not show in this case."); + }; + PopupNotifications.panel.addEventListener("popupshown", noShowHandler, { + once: true, + }); + + let promisePopupObserver = TestUtils.topicObserved( + "PopupNotifications-updateNotShowing" + ); + + // Try to extract canvas data without user inputs. + await SpecialPowers.spawn( + browser, + [randomDataOnCanvasExtract], + extractCanvasData + ); + + await promisePopupObserver; + info("There should be no popup shown on the panel."); + + // Check that the icon of canvas permission is shown. + let canvasNotification = PopupNotifications.getNotification( + "canvas-permissions-prompt", + browser + ); + + is( + canvasNotification.anchorElement.getAttribute("showing"), + "true", + "The canvas permission icon is correctly shown." + ); + PopupNotifications.panel.removeEventListener("popupshown", noShowHandler); + + await SpecialPowers.popPrefEnv(); +} + +async function doTestAutoBlockNoInput(randomDataOnCanvasExtract) { + await BrowserTestUtils.withNewTab( + kUrl, + withNewTabAutoBlockNoInput.bind(null, randomDataOnCanvasExtract) + ); +} + +add_task(doTestAutoBlockNoInput.bind(null, true)); +add_task(doTestAutoBlockNoInput.bind(null, false)); + +function extractCanvasDataUserInput( + randomDataOnCanvasExtract, + grantPermission +) { + let contentWindow = content.wrappedJSObject; + let canvas = contentWindow.document.getElementById("canvas-id-canvas"); + let canvasData = canvas.toDataURL(); + if (grantPermission) { + is( + canvasData, + contentWindow.kPlacedData, + "privacy.resistFingerprinting = true, permission granted, canvas data == placed data" + ); + if (!randomDataOnCanvasExtract) { + isnot( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission granted, canvas data != placeholderdata" + ); + } + } else if (grantPermission === false) { + isnot( + canvasData, + contentWindow.kPlacedData, + "privacy.resistFingerprinting = true, permission denied, canvas data != placed data" + ); + if (!randomDataOnCanvasExtract) { + is( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission denied, canvas data == placeholderdata" + ); + } else { + isnot( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, permission denied, canvas data != placeholderdata" + ); + } + } else { + isnot( + canvasData, + contentWindow.kPlacedData, + "privacy.resistFingerprinting = true, requesting permission, canvas data != placed data" + ); + if (!randomDataOnCanvasExtract) { + is( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, requesting permission, canvas data == placeholderdata" + ); + } else { + isnot( + canvasData, + contentWindow.kPlaceholderData, + "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, requesting permission, canvas data != placeholderdata" + ); + } + } +} + +async function withNewTabInput( + randomDataOnCanvasExtract, + grantPermission, + browser +) { + await SpecialPowers.spawn(browser, [], initTab); + await enableResistFingerprinting(randomDataOnCanvasExtract, true); + let popupShown = promisePopupShown(); + await SpecialPowers.spawn(browser, [], function (host) { + E10SUtils.wrapHandlingUserInput(content, true, function () { + var button = content.document.getElementById("clickme"); + button.click(); + }); + }); + await popupShown; + let popupHidden = promisePopupHidden(); + if (grantPermission) { + triggerMainCommand(); + await popupHidden; + is(testPermission(), Services.perms.ALLOW_ACTION, "permission granted"); + } else { + triggerSecondaryCommand(); + await popupHidden; + is(testPermission(), Services.perms.DENY_ACTION, "permission denied"); + } + await SpecialPowers.spawn( + browser, + [randomDataOnCanvasExtract, grantPermission], + extractCanvasDataUserInput + ); + await SpecialPowers.popPrefEnv(); +} + +async function doTestInput( + randomDataOnCanvasExtract, + grantPermission, + autoDeclineNoInput +) { + await BrowserTestUtils.withNewTab( + kUrl, + withNewTabInput.bind(null, randomDataOnCanvasExtract, grantPermission) + ); + Services.perms.removeFromPrincipal(kPrincipal, kPermission); +} + +// With auto-declining enabled (the default) +// Tests clicking "Don't Allow" button of the permission prompt. +add_task(doTestInput.bind(null, true, false)); +add_task(doTestInput.bind(null, false, false)); + +// Tests clicking "Allow" button of the permission prompt. +add_task(doTestInput.bind(null, true, true)); +add_task(doTestInput.bind(null, false, true)); diff --git a/browser/base/content/test/permissions/browser_canvas_rfp_exclusion.js b/browser/base/content/test/permissions/browser_canvas_rfp_exclusion.js new file mode 100644 index 0000000000..61c9c5bf84 --- /dev/null +++ b/browser/base/content/test/permissions/browser_canvas_rfp_exclusion.js @@ -0,0 +1,194 @@ +/* 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/. + * + * Adapted from browser_canvas_fingerprinting_resistance.js + */ +"use strict"; + +const kUrl = "https://example.com/"; +var gPlacedData = false; + +function initTab(performReadbackTest) { + let contentWindow = content.wrappedJSObject; + + let drawCanvas = (fillStyle, id) => { + let contentDocument = contentWindow.document; + let width = 64, + height = 64; + let canvas = contentDocument.createElement("canvas"); + if (id) { + canvas.setAttribute("id", id); + } + canvas.setAttribute("width", width); + canvas.setAttribute("height", height); + contentDocument.body.appendChild(canvas); + + let context = canvas.getContext("2d"); + context.fillStyle = fillStyle; + context.fillRect(0, 0, width, height); + return canvas; + }; + + let canvas = drawCanvas("cyan", "canvas-id-canvas"); + + let placedData = canvas.toDataURL(); + if (performReadbackTest) { + is( + canvas.toDataURL(), + placedData, + "Reading the placed data twice didn't match" + ); + return placedData; + } + return undefined; +} + +function disableResistFingerprinting() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", false], + ["privacy.resistFingerprinting.pbmode", false], + ], + }); +} + +function enableResistFingerprinting(RfpNonPbmExclusion, RfpDomainExclusion) { + if (RfpNonPbmExclusion && RfpDomainExclusion) { + return SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.pbmode", true], + ["privacy.resistFingerprinting.exemptedDomains", "example.com"], + ], + }); + } else if (RfpNonPbmExclusion) { + return SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting.pbmode", true]], + }); + } else if (RfpDomainExclusion) { + return SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + ["privacy.resistFingerprinting.exemptedDomains", "example.com"], + ], + }); + } + return SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); +} + +function extractCanvasData( + placedData, + isPbm, + RfpNonPbmExclusion, + RfpDomainExclusion +) { + let contentWindow = content.wrappedJSObject; + let canvas = contentWindow.document.getElementById("canvas-id-canvas"); + let canvasData = canvas.toDataURL(); + + if (RfpDomainExclusion) { + is( + canvasData, + placedData, + `A: RFP, domain exempted, canvas data == placed data (isPbm: ${isPbm}, RfpNonPbmExclusion: ${RfpNonPbmExclusion}, RfpDomainExclusion: ${RfpDomainExclusion})` + ); + } else if (!isPbm && RfpNonPbmExclusion) { + is( + canvasData, + placedData, + `B: RFP, nonPBM exempted, not in PBM, canvas data == placed data (isPbm: ${isPbm}, RfpNonPbmExclusion: ${RfpNonPbmExclusion}, RfpDomainExclusion: ${RfpDomainExclusion})` + ); + } else if (isPbm && RfpNonPbmExclusion) { + isnot( + canvasData, + placedData, + `C: RFP, nonPBM exempted, in PBM, canvas data != placed data (isPbm: ${isPbm}, RfpNonPbmExclusion: ${RfpNonPbmExclusion}, RfpDomainExclusion: ${RfpDomainExclusion})` + ); + } else { + isnot( + canvasData, + placedData, + `D: RFP, domain not exempted, nonPBM not exempted, canvas data != placed data (isPbm: ${isPbm}, RfpNonPbmExclusion: ${RfpNonPbmExclusion}, RfpDomainExclusion: ${RfpDomainExclusion})` + ); + } +} + +async function populatePlacedData() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await disableResistFingerprinting(); + await BrowserTestUtils.withNewTab( + { + gBrowser: win.gBrowser, + url: kUrl, + }, + async function () { + let browser = win.gBrowser.selectedBrowser; + gPlacedData = await SpecialPowers.spawn( + browser, + [/* performReadbackTest= */ true], + initTab + ); + } + ); + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +} + +async function rfpExclusionTestOnCanvas( + win, + placedData, + isPbm, + RfpNonPbmExclusion, + RfpDomainExclusion +) { + let browser = win.gBrowser.selectedBrowser; + await SpecialPowers.spawn( + browser, + [/* performReadbackTest= */ false], + initTab + ); + await SpecialPowers.spawn( + browser, + [placedData, isPbm, RfpNonPbmExclusion, RfpDomainExclusion], + extractCanvasData + ); +} + +async function testCanvasRfpExclusion( + isPbm, + RfpNonPbmExclusion, + RfpDomainExclusion +) { + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: isPbm, + }); + await enableResistFingerprinting(RfpNonPbmExclusion, RfpDomainExclusion); + await BrowserTestUtils.withNewTab( + { + gBrowser: win.gBrowser, + url: kUrl, + }, + rfpExclusionTestOnCanvas.bind( + null, + win, + gPlacedData, + isPbm, + RfpNonPbmExclusion, + RfpDomainExclusion + ) + ); + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +} + +add_task(populatePlacedData.bind(null)); +add_task(testCanvasRfpExclusion.bind(null, false, false, false)); +add_task(testCanvasRfpExclusion.bind(null, false, false, true)); +add_task(testCanvasRfpExclusion.bind(null, false, true, false)); +add_task(testCanvasRfpExclusion.bind(null, false, true, true)); +add_task(testCanvasRfpExclusion.bind(null, true, false, false)); +add_task(testCanvasRfpExclusion.bind(null, true, false, true)); +add_task(testCanvasRfpExclusion.bind(null, true, true, false)); +add_task(testCanvasRfpExclusion.bind(null, true, true, true)); diff --git a/browser/base/content/test/permissions/browser_permission_delegate_geo.js b/browser/base/content/test/permissions/browser_permission_delegate_geo.js new file mode 100644 index 0000000000..4dbbe1ea97 --- /dev/null +++ b/browser/base/content/test/permissions/browser_permission_delegate_geo.js @@ -0,0 +1,278 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIGIN = "https://example.com"; +const CROSS_SUBFRAME_PAGE = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) + + "temporary_permissions_subframe.html"; + +const CROSS_FRAME_PAGE = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) + + "temporary_permissions_frame.html"; + +const PromptResult = { + ALLOW: "allow", + DENY: "deny", + PROMPT: "prompt", +}; + +var Perms = Services.perms; +var uri = NetUtil.newURI(ORIGIN); +var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); + +async function checkNotificationBothOrigins( + firstPartyOrigin, + thirdPartyOrigin +) { + // Notification is shown, check label and deny to clean + let popuphidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + + let notification = PopupNotifications.panel.firstElementChild; + // Check the label of the notificaiton should be the first party + is( + PopupNotifications.getNotification("geolocation").options.name, + firstPartyOrigin, + "Use first party's origin" + ); + + // Check the second name of the notificaiton should be the third party + is( + PopupNotifications.getNotification("geolocation").options.secondName, + thirdPartyOrigin, + "Use third party's origin" + ); + + // Check remember checkbox is hidden + let checkbox = notification.checkbox; + ok(!!checkbox, "checkbox is present"); + ok(checkbox.hidden, "checkbox is not visible"); + ok(!checkbox.checked, "checkbox not checked"); + + EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {}); + await popuphidden; +} + +async function checkGeolocation(browser, frameId, expect) { + let isPrompt = expect == PromptResult.PROMPT; + let waitForPrompt; + if (isPrompt) { + waitForPrompt = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + } + + await SpecialPowers.spawn( + browser, + [{ frameId, expect, isPrompt }], + async args => { + let frame = content.document.getElementById(args.frameId); + + let waitForNoPrompt = new Promise(resolve => { + function onMessage(event) { + // Check the result right here because there's no notification + Assert.equal( + event.data, + args.expect, + "Correct expectation for third party" + ); + content.window.removeEventListener("message", onMessage); + resolve(); + } + + if (!args.isPrompt) { + content.window.addEventListener("message", onMessage); + } + }); + + await content.SpecialPowers.spawn(frame, [], async () => { + const { E10SUtils } = ChromeUtils.importESModule( + "resource://gre/modules/E10SUtils.sys.mjs" + ); + + E10SUtils.wrapHandlingUserInput(this.content, true, function () { + let frameDoc = this.content.document; + frameDoc.getElementById("geo").click(); + }); + }); + + if (!args.isPrompt) { + await waitForNoPrompt; + } + } + ); + + if (isPrompt) { + await waitForPrompt; + } +} + +add_setup(async function () { + await new Promise(r => { + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.security.featurePolicy.header.enabled", true], + ["dom.security.featurePolicy.webidl.enabled", true], + // This is the amount of time before the repeating + // NetworkGeolocationProvider timer is stopped. + // It needs to be less than 5000ms, or the timer will be + // reported as left behind by the test. + ["geo.timeout", 4000], + ], + }, + r + ); + }); +}); + +// Test that temp blocked permissions in first party affect the third party +// iframe. +add_task(async function testUseTempPermissionsFirstParty() { + await BrowserTestUtils.withNewTab( + CROSS_SUBFRAME_PAGE, + async function (browser) { + SitePermissions.setForPrincipal( + principal, + "geo", + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + browser + ); + + await checkGeolocation(browser, "frame", PromptResult.DENY); + + SitePermissions.removeFromPrincipal(principal, "geo", browser); + } + ); +}); + +// Test that persistent permissions in first party affect the third party +// iframe. +add_task(async function testUsePersistentPermissionsFirstParty() { + await BrowserTestUtils.withNewTab( + CROSS_SUBFRAME_PAGE, + async function (browser) { + async function checkPermission(aPermission, aExpect) { + PermissionTestUtils.add(uri, "geo", aPermission); + await checkGeolocation(browser, "frame", aExpect); + + if (aExpect == PromptResult.PROMPT) { + // Notification is shown, check label and deny to clean + let popuphidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + + let notification = PopupNotifications.panel.firstElementChild; + // Check the label of the notificaiton should be the first party + is( + PopupNotifications.getNotification("geolocation").options.name, + uri.host, + "Use first party's origin" + ); + + EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {}); + + await popuphidden; + SitePermissions.removeFromPrincipal(null, "geo", browser); + } + + PermissionTestUtils.remove(uri, "geo"); + } + + await checkPermission(Perms.PROMPT_ACTION, PromptResult.PROMPT); + await checkPermission(Perms.DENY_ACTION, PromptResult.DENY); + await checkPermission(Perms.ALLOW_ACTION, PromptResult.ALLOW); + } + ); +}); + +// Test that we do not prompt for maybe unsafe permission delegation if the +// origin of the page is the original src origin. +add_task(async function testPromptInMaybeUnsafePermissionDelegation() { + await BrowserTestUtils.withNewTab( + CROSS_SUBFRAME_PAGE, + async function (browser) { + // Persistent allow top level origin + PermissionTestUtils.add(uri, "geo", Perms.ALLOW_ACTION); + + await checkGeolocation(browser, "frameAllowsAll", PromptResult.ALLOW); + + SitePermissions.removeFromPrincipal(null, "geo", browser); + PermissionTestUtils.remove(uri, "geo"); + } + ); +}); + +// Test that we should prompt if we are in unsafe permission delegation and +// change location to origin which is not explicitly trusted. The prompt popup +// should include both first and third party origin. +add_task(async function testPromptChangeLocationUnsafePermissionDelegation() { + await BrowserTestUtils.withNewTab( + CROSS_SUBFRAME_PAGE, + async function (browser) { + // Persistent allow top level origin + PermissionTestUtils.add(uri, "geo", Perms.ALLOW_ACTION); + + let iframe = await SpecialPowers.spawn(browser, [], () => { + return content.document.getElementById("frameAllowsAll") + .browsingContext; + }); + + let otherURI = + "https://test1.example.com/browser/browser/base/content/test/permissions/permissions.html"; + let loaded = BrowserTestUtils.browserLoaded(browser, true, otherURI); + await SpecialPowers.spawn(iframe, [otherURI], async function (_otherURI) { + content.location = _otherURI; + }); + await loaded; + + await checkGeolocation(browser, "frameAllowsAll", PromptResult.PROMPT); + await checkNotificationBothOrigins(uri.host, "test1.example.com"); + + SitePermissions.removeFromPrincipal(null, "geo", browser); + PermissionTestUtils.remove(uri, "geo"); + } + ); +}); + +// If we are in unsafe permission delegation and the origin is explicitly +// trusted in ancestor chain. Do not need prompt +add_task(async function testExplicitlyAllowedInChain() { + await BrowserTestUtils.withNewTab(CROSS_FRAME_PAGE, async function (browser) { + // Persistent allow top level origin + PermissionTestUtils.add(uri, "geo", Perms.ALLOW_ACTION); + + let iframeAncestor = await SpecialPowers.spawn(browser, [], () => { + return content.document.getElementById("frameAncestor").browsingContext; + }); + + let iframe = await SpecialPowers.spawn(iframeAncestor, [], () => { + return content.document.getElementById("frameAllowsAll").browsingContext; + }); + + // Change location to check that we actually look at the ancestor chain + // instead of just considering the "same origin as src" rule. + let otherURI = + "https://test2.example.com/browser/browser/base/content/test/permissions/permissions.html"; + let loaded = BrowserTestUtils.browserLoaded(browser, true, otherURI); + await SpecialPowers.spawn(iframe, [otherURI], async function (_otherURI) { + content.location = _otherURI; + }); + await loaded; + + await checkGeolocation( + iframeAncestor, + "frameAllowsAll", + PromptResult.ALLOW + ); + + PermissionTestUtils.remove(uri, "geo"); + }); +}); diff --git a/browser/base/content/test/permissions/browser_permissions.js b/browser/base/content/test/permissions/browser_permissions.js new file mode 100644 index 0000000000..5f5ab006fb --- /dev/null +++ b/browser/base/content/test/permissions/browser_permissions.js @@ -0,0 +1,698 @@ +/* + * Test the Permissions section in the Control Center. + */ + +const PERMISSIONS_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "permissions.html"; + +function testPermListHasEntries(expectEntries) { + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let listEntryCount = permissionsList.querySelectorAll( + ".permission-popup-permission-item" + ).length; + if (expectEntries) { + ok(listEntryCount, "List of permissions is not empty"); + return; + } + ok(!listEntryCount, "List of permissions is empty"); +} + +add_task(async function testMainViewVisible() { + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function () { + await openPermissionPopup(); + + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + testPermListHasEntries(false); + + await closePermissionPopup(); + + PermissionTestUtils.add( + gBrowser.currentURI, + "camera", + Services.perms.ALLOW_ACTION + ); + + await openPermissionPopup(); + + testPermListHasEntries(true); + + let labelText = SitePermissions.getPermissionLabel("camera"); + let labels = permissionsList.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 1, "One permission visible in main view"); + is(labels[0].innerHTML, labelText, "Correct value"); + + let img = permissionsList.querySelector( + "image.permission-popup-permission-icon" + ); + ok(img, "There is an image for the permissions"); + ok(img.classList.contains("camera-icon"), "proper class is in image class"); + + await closePermissionPopup(); + + PermissionTestUtils.remove(gBrowser.currentURI, "camera"); + + // We intentionally turn off a11y_checks, because the following function + // is expected to click a toolbar button that may be already hidden + // with "display:none;". The permissions panel anchor is hidden because + // the last permission was removed, however we force opening the panel + // anyways in order to test that the list has been properly emptied: + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + await openPermissionPopup(); + AccessibilityUtils.resetEnv(); + + testPermListHasEntries(false); + + await closePermissionPopup(); + }); +}); + +add_task(async function testIdentityIcon() { + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, function () { + PermissionTestUtils.add( + gBrowser.currentURI, + "geo", + Services.perms.ALLOW_ACTION + ); + + ok( + gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"), + "identity-box signals granted permissions" + ); + + PermissionTestUtils.remove(gBrowser.currentURI, "geo"); + + ok( + !gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"), + "identity-box doesn't signal granted permissions" + ); + + PermissionTestUtils.add( + gBrowser.currentURI, + "not-a-site-permission", + Services.perms.ALLOW_ACTION + ); + + ok( + !gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"), + "identity-box doesn't signal granted permissions" + ); + + PermissionTestUtils.add( + gBrowser.currentURI, + "cookie", + Ci.nsICookiePermission.ACCESS_SESSION + ); + + ok( + gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"), + "identity-box signals granted permissions" + ); + + PermissionTestUtils.remove(gBrowser.currentURI, "cookie"); + + PermissionTestUtils.add( + gBrowser.currentURI, + "cookie", + Ci.nsICookiePermission.ACCESS_DENY + ); + + ok( + gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"), + "identity-box signals granted permissions" + ); + + PermissionTestUtils.add( + gBrowser.currentURI, + "cookie", + Ci.nsICookiePermission.ACCESS_DEFAULT + ); + + ok( + !gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"), + "identity-box doesn't signal granted permissions" + ); + + PermissionTestUtils.remove(gBrowser.currentURI, "geo"); + PermissionTestUtils.remove(gBrowser.currentURI, "not-a-site-permission"); + PermissionTestUtils.remove(gBrowser.currentURI, "cookie"); + }); +}); + +add_task(async function testCancelPermission() { + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function () { + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + + PermissionTestUtils.add( + gBrowser.currentURI, + "geo", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + gBrowser.currentURI, + "camera", + Services.perms.DENY_ACTION + ); + + await openPermissionPopup(); + + testPermListHasEntries(true); + + permissionsList + .querySelector(".permission-popup-permission-remove-button") + .click(); + + is( + permissionsList.querySelectorAll(".permission-popup-permission-label") + .length, + 1, + "First permission should be removed" + ); + + permissionsList + .querySelector(".permission-popup-permission-remove-button") + .click(); + + is( + permissionsList.querySelectorAll(".permission-popup-permission-label") + .length, + 0, + "Second permission should be removed" + ); + + await closePermissionPopup(); + }); +}); + +add_task(async function testPermissionHints() { + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) { + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let reloadHint = document.getElementById( + "permission-popup-permission-reload-hint" + ); + + await openPermissionPopup(); + + ok(BrowserTestUtils.isHidden(reloadHint), "Reload hint is hidden"); + + await closePermissionPopup(); + + PermissionTestUtils.add( + gBrowser.currentURI, + "geo", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + gBrowser.currentURI, + "camera", + Services.perms.DENY_ACTION + ); + + await openPermissionPopup(); + + ok(BrowserTestUtils.isHidden(reloadHint), "Reload hint is hidden"); + + let cancelButtons = permissionsList.querySelectorAll( + ".permission-popup-permission-remove-button" + ); + PermissionTestUtils.remove(gBrowser.currentURI, "camera"); + + cancelButtons[0].click(); + ok(!BrowserTestUtils.isHidden(reloadHint), "Reload hint is visible"); + + cancelButtons[1].click(); + ok(!BrowserTestUtils.isHidden(reloadHint), "Reload hint is visible"); + + await closePermissionPopup(); + let loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.startLoadingURIString(browser, PERMISSIONS_PAGE); + await loaded; + await openPermissionPopup(); + + ok( + BrowserTestUtils.isHidden(reloadHint), + "Reload hint is hidden after reloading" + ); + + await closePermissionPopup(); + }); +}); + +add_task(async function testPermissionIcons() { + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, function () { + PermissionTestUtils.add( + gBrowser.currentURI, + "camera", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + gBrowser.currentURI, + "geo", + Services.perms.DENY_ACTION + ); + + let geoIcon = gPermissionPanel._identityPermissionBox.querySelector( + ".blocked-permission-icon[data-permission-id='geo']" + ); + ok(geoIcon.hasAttribute("showing"), "blocked permission icon is shown"); + + let cameraIcon = gPermissionPanel._identityPermissionBox.querySelector( + ".blocked-permission-icon[data-permission-id='camera']" + ); + ok( + !cameraIcon.hasAttribute("showing"), + "allowed permission icon is not shown" + ); + + PermissionTestUtils.remove(gBrowser.currentURI, "geo"); + + ok( + !geoIcon.hasAttribute("showing"), + "blocked permission icon is not shown after reset" + ); + + PermissionTestUtils.remove(gBrowser.currentURI, "camera"); + }); +}); + +add_task(async function testPermissionShortcuts() { + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) { + browser.focus(); + + await new Promise(r => { + SpecialPowers.pushPrefEnv( + { set: [["permissions.default.shortcuts", 0]] }, + r + ); + }); + + async function tryKey(desc, expectedValue) { + await EventUtils.synthesizeAndWaitKey("c", { accelKey: true }); + let result = await SpecialPowers.spawn(browser, [], function () { + return { + keydowns: content.wrappedJSObject.gKeyDowns, + keypresses: content.wrappedJSObject.gKeyPresses, + }; + }); + is( + result.keydowns, + expectedValue, + "keydown event was fired or not fired as expected, " + desc + ); + is( + result.keypresses, + 0, + "keypress event shouldn't be fired for shortcut key, " + desc + ); + } + + await tryKey("pressed with default permissions", 1); + + PermissionTestUtils.add( + gBrowser.currentURI, + "shortcuts", + Services.perms.DENY_ACTION + ); + await tryKey("pressed when site blocked", 1); + + PermissionTestUtils.add( + gBrowser.currentURI, + "shortcuts", + PermissionTestUtils.ALLOW + ); + await tryKey("pressed when site allowed", 2); + + PermissionTestUtils.remove(gBrowser.currentURI, "shortcuts"); + await new Promise(r => { + SpecialPowers.pushPrefEnv( + { set: [["permissions.default.shortcuts", 2]] }, + r + ); + }); + + await tryKey("pressed when globally blocked", 2); + PermissionTestUtils.add( + gBrowser.currentURI, + "shortcuts", + Services.perms.ALLOW_ACTION + ); + await tryKey("pressed when globally blocked but site allowed", 3); + + PermissionTestUtils.add( + gBrowser.currentURI, + "shortcuts", + Services.perms.DENY_ACTION + ); + await tryKey("pressed when globally blocked and site blocked", 3); + + PermissionTestUtils.remove(gBrowser.currentURI, "shortcuts"); + }); +}); + +// Test the control center UI when policy permissions are set. +add_task(async function testPolicyPermission() { + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.disable_open_during_load", true]], + }); + + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + PermissionTestUtils.add( + gBrowser.currentURI, + "popup", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_POLICY + ); + + await openPermissionPopup(); + + // Check if the icon, nameLabel and stateLabel are visible. + let img, labelText, labels; + + img = permissionsList.querySelector( + "image.permission-popup-permission-icon" + ); + ok(img, "There is an image for the popup permission"); + ok(img.classList.contains("popup-icon"), "proper class is in image class"); + + labelText = SitePermissions.getPermissionLabel("popup"); + labels = permissionsList.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 1, "One permission visible in main view"); + is(labels[0].innerHTML, labelText, "Correct name label value"); + + labelText = SitePermissions.getCurrentStateLabel( + SitePermissions.ALLOW, + SitePermissions.SCOPE_POLICY + ); + labels = permissionsList.querySelectorAll( + ".permission-popup-permission-state-label" + ); + is(labels[0].innerHTML, labelText, "Correct state label value"); + + // Check if the menulist and the remove button are hidden. + // The menulist is specific to the "popup" permission. + let menulist = document.getElementById("permission-popup-menulist"); + Assert.equal( + menulist, + null, + "The popup permission menulist is not visible" + ); + + let removeButton = permissionsList.querySelector( + ".permission-popup-permission-remove-button" + ); + Assert.equal( + removeButton, + null, + "The permission remove button is not visible" + ); + + Services.perms.removeAll(); + await closePermissionPopup(); + }); +}); + +add_task(async function testHiddenAfterRefresh() { + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) { + ok( + BrowserTestUtils.isHidden(gPermissionPanel._permissionPopup), + "Popup is hidden" + ); + + await openPermissionPopup(); + + ok( + !BrowserTestUtils.isHidden(gPermissionPanel._permissionPopup), + "Popup is shown" + ); + + let reloaded = BrowserTestUtils.browserLoaded( + browser, + false, + PERMISSIONS_PAGE + ); + EventUtils.synthesizeKey("VK_F5", {}, browser.ownerGlobal); + await reloaded; + + ok( + BrowserTestUtils.isHidden(gPermissionPanel._permissionPopup), + "Popup is hidden" + ); + }); +}); + +async function helper3rdPartyStoragePermissionTest(permissionID) { + // 3rdPartyStorage permissions are listed under an anchor container - test + // that this works correctly, i.e. the permission items are added to the + // anchor when relevant, and other permission items are added to the default + // anchor, and adding/removing permissions preserves this behavior correctly. + + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) { + await openPermissionPopup(); + + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let storagePermissionAnchor = permissionsList.querySelector( + `.permission-popup-permission-list-anchor[anchorfor="3rdPartyStorage"]` + ); + + testPermListHasEntries(false); + + ok( + BrowserTestUtils.isHidden(storagePermissionAnchor.firstElementChild), + "Anchor header is hidden" + ); + + await closePermissionPopup(); + + let storagePermissionID = `${permissionID}^https://example2.com`; + PermissionTestUtils.add( + browser.currentURI, + storagePermissionID, + Services.perms.ALLOW_ACTION + ); + + await openPermissionPopup(); + + testPermListHasEntries(true); + ok( + BrowserTestUtils.isVisible(storagePermissionAnchor.firstElementChild), + "Anchor header is visible" + ); + + let labelText = SitePermissions.getPermissionLabel(storagePermissionID); + let labels = storagePermissionAnchor.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 1, "One permission visible in 3rdPartyStorage anchor"); + is( + labels[0].getAttribute("value"), + labelText, + "Permission label has the correct value" + ); + + await closePermissionPopup(); + + PermissionTestUtils.add( + browser.currentURI, + "camera", + Services.perms.ALLOW_ACTION + ); + + await openPermissionPopup(); + + testPermListHasEntries(true); + ok( + BrowserTestUtils.isVisible(storagePermissionAnchor.firstElementChild), + "Anchor header is visible" + ); + + labels = permissionsList.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 2, "Two permissions visible in main view"); + labels = storagePermissionAnchor.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 1, "One permission visible in 3rdPartyStorage anchor"); + + storagePermissionAnchor + .querySelector(".permission-popup-permission-remove-button") + .click(); + is( + storagePermissionAnchor.querySelectorAll( + ".permission-popup-permission-label" + ).length, + 0, + "Permission item should be removed" + ); + is( + PermissionTestUtils.testPermission( + browser.currentURI, + storagePermissionID + ), + SitePermissions.UNKNOWN, + "Permission removed from permission manager" + ); + + await closePermissionPopup(); + + await openPermissionPopup(); + + testPermListHasEntries(true); + ok( + BrowserTestUtils.isHidden(storagePermissionAnchor.firstElementChild), + "Anchor header is hidden" + ); + + labels = permissionsList.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 1, "One permission visible in main view"); + + await closePermissionPopup(); + + PermissionTestUtils.remove(browser.currentURI, "camera"); + + await openPermissionPopup(); + + testPermListHasEntries(false); + ok( + BrowserTestUtils.isHidden(storagePermissionAnchor.firstElementChild), + "Anchor header is hidden" + ); + + await closePermissionPopup(); + }); +} + +add_task(async function test3rdPartyStoragePermission() { + await helper3rdPartyStoragePermissionTest("3rdPartyStorage"); +}); + +add_task(async function test3rdPartyFrameStoragePermission() { + await helper3rdPartyStoragePermissionTest("3rdPartyFrameStorage"); +}); + +add_task(async function test3rdPartyBothStoragePermission() { + // Test the handling of both types of 3rdParty(Frame)?Storage permissions together + + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async function (browser) { + await openPermissionPopup(); + + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let storagePermissionAnchor = permissionsList.querySelector( + `.permission-popup-permission-list-anchor[anchorfor="3rdPartyStorage"]` + ); + + testPermListHasEntries(false); + + ok( + BrowserTestUtils.isHidden(storagePermissionAnchor.firstElementChild), + "Anchor header is hidden" + ); + + await closePermissionPopup(); + + let storagePermissionID = "3rdPartyFrameStorage^https://example2.com"; + PermissionTestUtils.add( + browser.currentURI, + storagePermissionID, + Services.perms.ALLOW_ACTION + ); + + await openPermissionPopup(); + + testPermListHasEntries(true); + ok( + BrowserTestUtils.isVisible(storagePermissionAnchor.firstElementChild), + "Anchor header is visible" + ); + + let labelText = SitePermissions.getPermissionLabel(storagePermissionID); + let labels = storagePermissionAnchor.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 1, "One permission visible in 3rdPartyStorage anchor"); + is( + labels[0].getAttribute("value"), + labelText, + "Permission label has the correct value" + ); + + await closePermissionPopup(); + + PermissionTestUtils.add( + browser.currentURI, + "3rdPartyStorage^https://www.example2.com", + Services.perms.ALLOW_ACTION + ); + + await openPermissionPopup(); + + testPermListHasEntries(true); + ok( + BrowserTestUtils.isVisible(storagePermissionAnchor.firstElementChild), + "Anchor header is visible" + ); + + labels = permissionsList.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 1, "One permissions visible in main view"); + labels = storagePermissionAnchor.querySelectorAll( + ".permission-popup-permission-label" + ); + is(labels.length, 1, "One permission visible in 3rdPartyStorage anchor"); + + storagePermissionAnchor + .querySelector(".permission-popup-permission-remove-button") + .click(); + is( + storagePermissionAnchor.querySelectorAll( + ".permission-popup-permission-label" + ).length, + 0, + "Permission item should be removed" + ); + is( + PermissionTestUtils.testPermission( + browser.currentURI, + storagePermissionID + ), + SitePermissions.UNKNOWN, + "Permission removed from permission manager" + ); + is( + PermissionTestUtils.testPermission( + browser.currentURI, + "3rdPartyStorage^https://www.example2.com" + ), + SitePermissions.UNKNOWN, + "3rdPartyStorage permission removed from permission manager" + ); + + await closePermissionPopup(); + }); +}); diff --git a/browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js b/browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js new file mode 100644 index 0000000000..9f0066f8e1 --- /dev/null +++ b/browser/base/content/test/permissions/browser_permissions_delegate_vibrate.js @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TEST_PAGE = + "https://example.com/browser/browser/base/content/test/permissions/empty.html"; + +add_task(async function testNoPermissionPrompt() { + info("Creating tab"); + await BrowserTestUtils.withNewTab(TEST_PAGE, async function (browser) { + await new Promise(r => { + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.vibrator.enabled", true], + ["dom.security.featurePolicy.header.enabled", true], + ["dom.security.featurePolicy.webidl.enabled", true], + ], + }, + r + ); + }); + + await ContentTask.spawn(browser, null, async function () { + let frame = content.document.createElement("iframe"); + // Cross origin src + frame.src = + "https://example.org/browser/browser/base/content/test/permissions/empty.html"; + await new Promise(resolve => { + frame.addEventListener("load", () => { + resolve(); + }); + content.document.body.appendChild(frame); + }); + + await content.SpecialPowers.spawn(frame, [], async function () { + // Request a permission. + let result = this.content.navigator.vibrate([100, 100]); + Assert.equal(result, false, "navigator.vibrate has been denied"); + }); + content.document.body.removeChild(frame); + }); + }); +}); diff --git a/browser/base/content/test/permissions/browser_permissions_handling_user_input.js b/browser/base/content/test/permissions/browser_permissions_handling_user_input.js new file mode 100644 index 0000000000..94b69c4998 --- /dev/null +++ b/browser/base/content/test/permissions/browser_permissions_handling_user_input.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIGIN = "https://example.com"; +const PERMISSIONS_PAGE = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) + + "permissions.html"; + +function assertShown(task) { + return BrowserTestUtils.withNewTab( + PERMISSIONS_PAGE, + async function (browser) { + let popupshown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + await SpecialPowers.spawn(browser, [], task); + + await popupshown; + + ok(true, "Notification permission prompt was shown"); + } + ); +} + +function assertNotShown(task) { + return BrowserTestUtils.withNewTab( + PERMISSIONS_PAGE, + async function (browser) { + let popupshown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + await SpecialPowers.spawn(browser, [], task); + + let sawPrompt = await Promise.race([ + popupshown.then(() => true), + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + new Promise(c => setTimeout(() => c(false), 1000)), + ]); + + is(sawPrompt, false, "Notification permission prompt was not shown"); + } + ); +} + +// Tests that notification permissions are automatically denied without user interaction. +add_task(async function testNotificationPermission() { + Services.prefs.setBoolPref( + "dom.webnotifications.requireuserinteraction", + true + ); + + // First test that when user interaction is required, requests + // with user interaction will show the permission prompt. + + await assertShown(function () { + content.document.notifyUserGestureActivation(); + content.document.getElementById("desktop-notification").click(); + }); + + await assertShown(function () { + content.document.notifyUserGestureActivation(); + content.document.getElementById("push").click(); + }); + + // Now test that requests without user interaction will fail. + + await assertNotShown(function () { + content.postMessage("push", "*"); + }); + + await assertNotShown(async function () { + let response = await content.Notification.requestPermission(); + is(response, "default", "The request was automatically denied"); + }); + + Services.prefs.setBoolPref( + "dom.webnotifications.requireuserinteraction", + false + ); + + // Finally test that those requests will show a prompt again + // if the pref has been set to false. + + await assertShown(function () { + content.postMessage("push", "*"); + }); + + await assertShown(function () { + content.Notification.requestPermission(); + }); + + Services.prefs.clearUserPref("dom.webnotifications.requireuserinteraction"); +}); diff --git a/browser/base/content/test/permissions/browser_permissions_postPrompt.js b/browser/base/content/test/permissions/browser_permissions_postPrompt.js new file mode 100644 index 0000000000..e306cb1058 --- /dev/null +++ b/browser/base/content/test/permissions/browser_permissions_postPrompt.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIGIN = "https://example.com"; +const PERMISSIONS_PAGE = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) + + "permissions.html"; + +function testPostPrompt(task) { + let uri = Services.io.newURI(PERMISSIONS_PAGE); + return BrowserTestUtils.withNewTab( + PERMISSIONS_PAGE, + async function (browser) { + let icon = document.getElementById("web-notifications-notification-icon"); + ok( + !BrowserTestUtils.isVisible(icon), + "notifications icon is not visible at first" + ); + + await SpecialPowers.spawn(browser, [], task); + + await TestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(icon), + "notifications icon is visible" + ); + ok( + !PopupNotifications.panel.hasAttribute("panelopen"), + "only the icon is showing, the panel is not open" + ); + + let popupshown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + icon.click(); + await popupshown; + + ok(true, "Notification permission prompt was shown"); + + let notification = PopupNotifications.panel.firstElementChild; + EventUtils.synthesizeMouseAtCenter(notification.button, {}); + + is( + PermissionTestUtils.testPermission(uri, "desktop-notification"), + Ci.nsIPermissionManager.ALLOW_ACTION, + "User can override the default deny by using the prompt" + ); + + PermissionTestUtils.remove(uri, "desktop-notification"); + } + ); +} + +add_task(async function testNotificationPermission() { + Services.prefs.setBoolPref( + "dom.webnotifications.requireuserinteraction", + true + ); + Services.prefs.setBoolPref( + "permissions.desktop-notification.postPrompt.enabled", + true + ); + + // Now test that requests without user interaction will post-prompt when the + // user interaction requirement is set. + + await testPostPrompt(function () { + content.postMessage("push", "*"); + }); + + await testPostPrompt(async function () { + let response = await content.Notification.requestPermission(); + is(response, "default", "The request was automatically denied"); + }); + + Services.prefs.clearUserPref("dom.webnotifications.requireuserinteraction"); + Services.prefs.clearUserPref( + "permissions.desktop-notification.postPrompt.enabled" + ); +}); diff --git a/browser/base/content/test/permissions/browser_reservedkey.js b/browser/base/content/test/permissions/browser_reservedkey.js new file mode 100644 index 0000000000..c8eb0ab6c6 --- /dev/null +++ b/browser/base/content/test/permissions/browser_reservedkey.js @@ -0,0 +1,312 @@ +add_task(async function test_reserved_shortcuts() { + let keyset = document.createXULElement("keyset"); + let key1 = document.createXULElement("key"); + key1.setAttribute("id", "kt_reserved"); + key1.setAttribute("modifiers", "shift"); + key1.setAttribute("key", "O"); + key1.setAttribute("reserved", "true"); + key1.setAttribute("count", "0"); + key1.addEventListener("command", () => { + let attribute = key1.getAttribute("count"); + key1.setAttribute("count", Number(attribute) + 1); + }); + + let key2 = document.createXULElement("key"); + key2.setAttribute("id", "kt_notreserved"); + key2.setAttribute("modifiers", "shift"); + key2.setAttribute("key", "P"); + key2.setAttribute("reserved", "false"); + key2.setAttribute("count", "0"); + key2.addEventListener("command", () => { + let attribute = key2.getAttribute("count"); + key2.setAttribute("count", Number(attribute) + 1); + }); + + let key3 = document.createXULElement("key"); + key3.setAttribute("id", "kt_reserveddefault"); + key3.setAttribute("modifiers", "shift"); + key3.setAttribute("key", "Q"); + key3.setAttribute("count", "0"); + key3.addEventListener("command", () => { + let attribute = key3.getAttribute("count"); + key3.setAttribute("count", Number(attribute) + 1); + }); + + keyset.appendChild(key1); + keyset.appendChild(key2); + keyset.appendChild(key3); + let container = document.createXULElement("box"); + container.appendChild(keyset); + document.documentElement.appendChild(container); + + const pageUrl = + "data:text/html,<body onload='document.body.firstElementChild.focus();'><div onkeydown='event.preventDefault();' tabindex=0>Test</div></body>"; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + + EventUtils.sendString("OPQ"); + + is( + document.getElementById("kt_reserved").getAttribute("count"), + "1", + "reserved='true' with preference off" + ); + is( + document.getElementById("kt_notreserved").getAttribute("count"), + "0", + "reserved='false' with preference off" + ); + is( + document.getElementById("kt_reserveddefault").getAttribute("count"), + "0", + "default reserved with preference off" + ); + + // Now try with reserved shortcut key handling enabled. + await new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { set: [["permissions.default.shortcuts", 2]] }, + resolve + ); + }); + + EventUtils.sendString("OPQ"); + + is( + document.getElementById("kt_reserved").getAttribute("count"), + "2", + "reserved='true' with preference on" + ); + is( + document.getElementById("kt_notreserved").getAttribute("count"), + "0", + "reserved='false' with preference on" + ); + is( + document.getElementById("kt_reserveddefault").getAttribute("count"), + "1", + "default reserved with preference on" + ); + + document.documentElement.removeChild(container); + + BrowserTestUtils.removeTab(tab); +}); + +// This test checks that Alt+<key> and F10 cannot be blocked when the preference is set. +if (!navigator.platform.includes("Mac")) { + add_task(async function test_accesskeys_menus() { + await new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { set: [["permissions.default.shortcuts", 2]] }, + resolve + ); + }); + + const uri = + 'data:text/html,<body onkeydown=\'if (event.key == "H" || event.key == "F10") event.preventDefault();\'>'; + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri); + + // Pressing Alt+H should open the Help menu. + let helpPopup = document.getElementById("menu_HelpPopup"); + let popupShown = BrowserTestUtils.waitForEvent(helpPopup, "popupshown"); + EventUtils.synthesizeKey("KEY_Alt", { type: "keydown" }); + EventUtils.synthesizeKey("h", { altKey: true }); + EventUtils.synthesizeKey("KEY_Alt", { type: "keyup" }); + await popupShown; + + ok(true, "Help menu opened"); + + let popupHidden = BrowserTestUtils.waitForEvent(helpPopup, "popuphidden"); + helpPopup.hidePopup(); + await popupHidden; + + // Pressing F10 should focus the menubar. On Linux, the file menu should open, but on Windows, + // pressing Down will open the file menu. + let menubar = document.getElementById("main-menubar"); + let menubarActive = BrowserTestUtils.waitForEvent( + menubar, + "DOMMenuBarActive" + ); + EventUtils.synthesizeKey("KEY_F10"); + await menubarActive; + + let filePopup = document.getElementById("menu_FilePopup"); + popupShown = BrowserTestUtils.waitForEvent(filePopup, "popupshown"); + if (navigator.platform.includes("Win")) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await popupShown; + + ok(true, "File menu opened"); + + popupHidden = BrowserTestUtils.waitForEvent(filePopup, "popuphidden"); + filePopup.hidePopup(); + await popupHidden; + + BrowserTestUtils.removeTab(tab1); + }); +} + +// There is a <key> element for Backspace and delete with reserved="false", +// so make sure that it is not treated as a blocked shortcut key. +add_task(async function test_backspace_delete() { + await new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { set: [["permissions.default.shortcuts", 2]] }, + resolve + ); + }); + + // The input field is autofocused. If this test fails, backspace can go back + // in history so cancel the beforeunload event and adjust the field to make the test fail. + const uri = + 'data:text/html,<body onbeforeunload=\'document.getElementById("field").value = "failed";\'>' + + "<input id='field' value='something'></body>"; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.document.getElementById("field").focus(); + + // Add a promise that resolves when the backspace key gets received + // so we can ensure the key gets received before checking the result. + content.keysPromise = new Promise(resolve => { + content.addEventListener("keyup", event => { + if (event.code == "Backspace") { + resolve(content.document.getElementById("field").value); + } + }); + }); + }); + + // Move the caret so backspace will delete the first character. + EventUtils.synthesizeKey("KEY_ArrowRight", {}); + EventUtils.synthesizeKey("KEY_Backspace", {}); + + let fieldValue = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + return content.keysPromise; + } + ); + is(fieldValue, "omething", "backspace not prevented"); + + // now do the same thing for the delete key: + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.document.getElementById("field").focus(); + + // Add a promise that resolves when the backspace key gets received + // so we can ensure the key gets received before checking the result. + content.keysPromise = new Promise(resolve => { + content.addEventListener("keyup", event => { + if (event.code == "Delete") { + resolve(content.document.getElementById("field").value); + } + }); + }); + }); + + // Move the caret so backspace will delete the first character. + EventUtils.synthesizeKey("KEY_Delete", {}); + + fieldValue = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + return content.keysPromise; + } + ); + is(fieldValue, "mething", "delete not prevented"); + + BrowserTestUtils.removeTab(tab); +}); + +// TODO: Make this to run on Windows too to have automated tests also there. +if ( + navigator.platform.includes("Mac") || + navigator.platform.includes("Linux") +) { + add_task( + async function test_reserved_shortcuts_conflict_with_user_settings() { + await new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { set: [["test.events.async.enabled", true]] }, + resolve + ); + }); + + const keyset = document.createXULElement("keyset"); + const key = document.createXULElement("key"); + key.setAttribute("id", "conflict_with_known_native_key_binding"); + if (navigator.platform.includes("Mac")) { + // Select to end of the paragraph + key.setAttribute("modifiers", "ctrl,shift"); + key.setAttribute("key", "E"); + } else { + // Select All + key.setAttribute("modifiers", "ctrl"); + key.setAttribute("key", "a"); + } + key.setAttribute("reserved", "true"); + key.setAttribute("count", "0"); + key.addEventListener("command", () => { + const attribute = key.getAttribute("count"); + key.setAttribute("count", Number(attribute) + 1); + }); + + keyset.appendChild(key); + const container = document.createXULElement("box"); + container.appendChild(keyset); + document.documentElement.appendChild(container); + + const pageUrl = + "data:text/html,<body onload='document.body.firstChild.focus(); getSelection().collapse(document.body.firstChild, 0)'><div contenteditable>Test</div></body>"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + pageUrl + ); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [key.getAttribute("key")], + async function (aExpectedKeyValue) { + content.promiseTestResult = new Promise(resolve => { + content.addEventListener("keyup", event => { + if (event.key.toLowerCase() == aExpectedKeyValue.toLowerCase()) { + resolve(content.getSelection().getRangeAt(0).toString()); + } + }); + }); + } + ); + + EventUtils.synthesizeKey(key.getAttribute("key"), { + ctrlKey: key.getAttribute("modifiers").includes("ctrl"), + shiftKey: key.getAttribute("modifiers").includes("shift"), + }); + + const selectedText = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + return content.promiseTestResult; + } + ); + is( + selectedText, + "Test", + "The shortcut key should select all text in the editor" + ); + + is( + key.getAttribute("count"), + "0", + "The reserved shortcut key should be consumed by the focused editor instead" + ); + + document.documentElement.removeChild(container); + + BrowserTestUtils.removeTab(tab); + } + ); +} diff --git a/browser/base/content/test/permissions/browser_site_scoped_permissions.js b/browser/base/content/test/permissions/browser_site_scoped_permissions.js new file mode 100644 index 0000000000..7a8953de47 --- /dev/null +++ b/browser/base/content/test/permissions/browser_site_scoped_permissions.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const EMPTY_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "empty.html"; + +const SUBDOMAIN_EMPTY_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://www.example.com" + ) + "empty.html"; + +add_task(async function testSiteScopedPermissionSubdomainAffectsBaseDomain() { + let subdomainOrigin = "https://www.example.com"; + let subdomainPrincipal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + subdomainOrigin + ); + let id = "3rdPartyStorage^https://example.org"; + + await BrowserTestUtils.withNewTab(EMPTY_PAGE, async function (browser) { + Services.perms.addFromPrincipal( + subdomainPrincipal, + id, + SitePermissions.ALLOW + ); + + await openPermissionPopup(); + + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let listEntryCount = permissionsList.querySelectorAll( + ".permission-popup-permission-item" + ).length; + is( + listEntryCount, + 1, + "Permission exists on base domain when set on subdomain" + ); + + closePermissionPopup(); + + Services.perms.removeFromPrincipal(subdomainPrincipal, id); + + // We intentionally turn off a11y_checks, because the following function + // is expected to click a toolbar button that may be already hidden + // with "display:none;". The permissions panel anchor is hidden because + // the last permission was removed, however we force opening the panel + // anyways in order to test that the list has been properly emptied: + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + await openPermissionPopup(); + AccessibilityUtils.resetEnv(); + + listEntryCount = permissionsList.querySelectorAll( + ".permission-popup-permission-item-3rdPartyStorage" + ).length; + is( + listEntryCount, + 0, + "Permission removed on base domain when removed on subdomain" + ); + + await closePermissionPopup(); + }); +}); + +add_task(async function testSiteScopedPermissionBaseDomainAffectsSubdomain() { + let origin = "https://example.com"; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let id = "3rdPartyStorage^https://example.org"; + + await BrowserTestUtils.withNewTab( + SUBDOMAIN_EMPTY_PAGE, + async function (browser) { + Services.perms.addFromPrincipal(principal, id, SitePermissions.ALLOW); + await openPermissionPopup(); + + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let listEntryCount = permissionsList.querySelectorAll( + ".permission-popup-permission-item" + ).length; + is( + listEntryCount, + 1, + "Permission exists on base domain when set on subdomain" + ); + + closePermissionPopup(); + + Services.perms.removeFromPrincipal(principal, id); + + // We intentionally turn off a11y_checks, because the following function + // is expected to click a toolbar button that may be already hidden + // with "display:none;". The permissions panel anchor is hidden because + // the last permission was removed, however we force opening the panel + // anyways in order to test that the list has been properly emptied: + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + await openPermissionPopup(); + AccessibilityUtils.resetEnv(); + + listEntryCount = permissionsList.querySelectorAll( + ".permission-popup-permission-item-3rdPartyStorage" + ).length; + is( + listEntryCount, + 0, + "Permission removed on base domain when removed on subdomain" + ); + + await closePermissionPopup(); + } + ); +}); diff --git a/browser/base/content/test/permissions/browser_temporary_permissions.js b/browser/base/content/test/permissions/browser_temporary_permissions.js new file mode 100644 index 0000000000..83f7e49d56 --- /dev/null +++ b/browser/base/content/test/permissions/browser_temporary_permissions.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIGIN = "https://example.com"; +const PERMISSIONS_PAGE = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) + + "permissions.html"; +const SUBFRAME_PAGE = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) + + "temporary_permissions_subframe.html"; + +// Test that setting temp permissions triggers a change in the identity block. +add_task(async function testTempPermissionChangeEvents() { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN); + let id = "geo"; + + await BrowserTestUtils.withNewTab(ORIGIN, function (browser) { + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + browser + ); + + Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + }); + + let geoIcon = document.querySelector( + ".blocked-permission-icon[data-permission-id=geo]" + ); + + Assert.notEqual( + geoIcon.getBoundingClientRect().width, + 0, + "geo anchor should be visible" + ); + + SitePermissions.removeFromPrincipal(principal, id, browser); + + Assert.equal( + geoIcon.getBoundingClientRect().width, + 0, + "geo anchor should not be visible" + ); + }); +}); + +// Test that temp blocked permissions requested by subframes (with a different URI) affect the whole page. +add_task(async function testTempPermissionSubframes() { + let uri = NetUtil.newURI(ORIGIN); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + let id = "geo"; + + await BrowserTestUtils.withNewTab(SUBFRAME_PAGE, async function (browser) { + let popupshown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + await new Promise(r => { + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.security.featurePolicy.header.enabled", true], + ["dom.security.featurePolicy.webidl.enabled", true], + ], + }, + r + ); + }); + + // Request a permission. + await SpecialPowers.spawn(browser, [uri.host], async function (host0) { + let frame = content.document.getElementById("frame"); + + await content.SpecialPowers.spawn(frame, [host0], async function (host) { + const { E10SUtils } = ChromeUtils.importESModule( + "resource://gre/modules/E10SUtils.sys.mjs" + ); + + E10SUtils.wrapHandlingUserInput(this.content, true, function () { + let frameDoc = this.content.document; + + // Make sure that the origin of our test page is different. + Assert.notEqual(frameDoc.location.host, host); + + frameDoc.getElementById("geo").click(); + }); + }); + }); + + await popupshown; + + let popuphidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + + let notification = PopupNotifications.panel.firstElementChild; + EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {}); + + await popuphidden; + + Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + }); + }); +}); diff --git a/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js b/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js new file mode 100644 index 0000000000..e323f769cd --- /dev/null +++ b/browser/base/content/test/permissions/browser_temporary_permissions_expiry.js @@ -0,0 +1,208 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +const ORIGIN = "https://example.com"; +const PERMISSIONS_PAGE = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) + + "permissions.html"; + +// Ignore promise rejection caused by clicking Deny button. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally(/The request is not allowed/); + +const EXPIRE_TIME_MS = 100; +const TIMEOUT_MS = 500; + +const EXPIRE_TIME_CUSTOM_MS = 1000; +const TIMEOUT_CUSTOM_MS = 1500; + +const kVREnabled = SpecialPowers.getBoolPref("dom.vr.enabled"); + +// Test that temporary permissions can be re-requested after they expired +// and that the identity block is updated accordingly. +add_task(async function testTempPermissionRequestAfterExpiry() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.temporary_permission_expire_time_ms", EXPIRE_TIME_MS], + ["media.navigator.permission.fake", true], + ["dom.vr.always_support_vr", true], + ], + }); + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN); + let ids = ["geo", "camera"]; + + if (kVREnabled) { + ids.push("xr"); + } + + for (let id of ids) { + await BrowserTestUtils.withNewTab( + PERMISSIONS_PAGE, + async function (browser) { + let blockedIcon = gPermissionPanel._identityPermissionBox.querySelector( + `.blocked-permission-icon[data-permission-id='${id}']` + ); + + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + browser + ); + + Assert.deepEqual( + SitePermissions.getForPrincipal(principal, id, browser), + { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + } + ); + + ok( + blockedIcon.hasAttribute("showing"), + "blocked permission icon is shown" + ); + + await new Promise(c => setTimeout(c, TIMEOUT_MS)); + + Assert.deepEqual( + SitePermissions.getForPrincipal(principal, id, browser), + { + state: SitePermissions.UNKNOWN, + scope: SitePermissions.SCOPE_PERSISTENT, + } + ); + + let popupshown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + // Request a permission; + await BrowserTestUtils.synthesizeMouseAtCenter(`#${id}`, {}, browser); + + await popupshown; + + ok( + !blockedIcon.hasAttribute("showing"), + "blocked permission icon is not shown" + ); + + let popuphidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + + let notification = PopupNotifications.panel.firstElementChild; + EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {}); + + await popuphidden; + + SitePermissions.removeFromPrincipal(principal, id, browser); + } + ); + } +}); + +/** + * Test whether the identity UI shows the permission granted state. + * @param {boolean} state - true = Shows permission granted, false otherwise. + */ +async function testIdentityPermissionGrantedState(state) { + let hasAttribute; + let msg = `Identity permission box ${ + state ? "shows" : "does not show" + } granted permissions.`; + await TestUtils.waitForCondition(() => { + hasAttribute = + gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"); + return hasAttribute == state; + }, msg); + is(hasAttribute, state, msg); +} + +// Test that temporary permissions can have custom expiry time and the identity +// block is updated correctly on expiry. +add_task(async function testTempPermissionCustomExpiry() { + const TEST_ID = "geo"; + // Set a default expiry time which is lower than the custom one we'll set. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.temporary_permission_expire_time_ms", EXPIRE_TIME_MS]], + }); + + await BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, async browser => { + Assert.deepEqual( + SitePermissions.getForPrincipal(null, TEST_ID, browser), + { + state: SitePermissions.UNKNOWN, + scope: SitePermissions.SCOPE_PERSISTENT, + }, + "Permission not set initially" + ); + + await testIdentityPermissionGrantedState(false); + + // Set permission with custom expiry time. + SitePermissions.setForPrincipal( + null, + "geo", + SitePermissions.ALLOW, + SitePermissions.SCOPE_TEMPORARY, + browser, + EXPIRE_TIME_CUSTOM_MS + ); + + await testIdentityPermissionGrantedState(true); + + // We've set the permission, start the timer promise. + let timeout = new Promise(resolve => + setTimeout(resolve, TIMEOUT_CUSTOM_MS) + ); + + Assert.deepEqual( + SitePermissions.getForPrincipal(null, TEST_ID, browser), + { + state: SitePermissions.ALLOW, + scope: SitePermissions.SCOPE_TEMPORARY, + }, + "We should see the temporary permission we just set." + ); + + // Wait for half of the expiry time. + await new Promise(resolve => + setTimeout(resolve, EXPIRE_TIME_CUSTOM_MS / 2) + ); + Assert.deepEqual( + SitePermissions.getForPrincipal(null, TEST_ID, browser), + { + state: SitePermissions.ALLOW, + scope: SitePermissions.SCOPE_TEMPORARY, + }, + "Temporary permission should not have expired yet." + ); + + // Wait until permission expiry. + await timeout; + + // Identity permission section should have updated by now. It should do this + // without relying on side-effects of the SitePermissions getter. + await testIdentityPermissionGrantedState(false); + + Assert.deepEqual( + SitePermissions.getForPrincipal(null, TEST_ID, browser), + { + state: SitePermissions.UNKNOWN, + scope: SitePermissions.SCOPE_PERSISTENT, + }, + "Permission should have expired" + ); + }); +}); diff --git a/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js b/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js new file mode 100644 index 0000000000..7da79b1810 --- /dev/null +++ b/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js @@ -0,0 +1,239 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that temporary permissions are removed on user initiated reload only. +add_task(async function testTempPermissionOnReload() { + let origin = "https://example.com/"; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let id = "geo"; + + await BrowserTestUtils.withNewTab(origin, async function (browser) { + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + browser + ); + + let reloaded = BrowserTestUtils.browserLoaded(browser, false, origin); + + Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + }); + + // Reload through the page (should not remove the temp permission). + await SpecialPowers.spawn(browser, [], () => + content.document.location.reload() + ); + + await reloaded; + + Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + }); + + reloaded = BrowserTestUtils.browserLoaded(browser, false, origin); + + // Reload as a user (should remove the temp permission). + BrowserReload(); + + await reloaded; + + Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), { + state: SitePermissions.UNKNOWN, + scope: SitePermissions.SCOPE_PERSISTENT, + }); + + // Set the permission again. + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + browser + ); + + // Open the tab context menu. + let contextMenu = document.getElementById("tabContextMenu"); + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, { + type: "contextmenu", + button: 2, + }); + await popupShownPromise; + + let reloadMenuItem = document.getElementById("context_reloadTab"); + + reloaded = BrowserTestUtils.browserLoaded(browser, false, origin); + + // Reload as a user through the context menu (should remove the temp permission). + contextMenu.activateItem(reloadMenuItem); + + await reloaded; + + Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), { + state: SitePermissions.UNKNOWN, + scope: SitePermissions.SCOPE_PERSISTENT, + }); + + // Set the permission again. + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + browser + ); + + // Reload as user via return key in urlbar (should remove the temp permission) + let urlBarInput = document.getElementById("urlbar-input"); + await EventUtils.synthesizeMouseAtCenter(urlBarInput, {}); + + reloaded = BrowserTestUtils.browserLoaded(browser, false, origin); + + EventUtils.synthesizeAndWaitKey("VK_RETURN", {}); + + await reloaded; + + Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), { + state: SitePermissions.UNKNOWN, + scope: SitePermissions.SCOPE_PERSISTENT, + }); + + SitePermissions.removeFromPrincipal(principal, id, browser); + }); +}); + +// Test that temporary permissions are not removed when reloading all tabs. +add_task(async function testTempPermissionOnReloadAllTabs() { + let origin = "https://example.com/"; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let id = "geo"; + + await BrowserTestUtils.withNewTab(origin, async function (browser) { + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + browser + ); + + // Select all tabs before opening the context menu. + gBrowser.selectAllTabs(); + + // Open the tab context menu. + let contextMenu = document.getElementById("tabContextMenu"); + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, { + type: "contextmenu", + button: 2, + }); + await popupShownPromise; + + let reloadMenuItem = document.getElementById("context_reloadSelectedTabs"); + + let reloaded = Promise.all( + gBrowser.visibleTabs.map(tab => + BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab)) + ) + ); + contextMenu.activateItem(reloadMenuItem); + await reloaded; + + Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + }); + + SitePermissions.removeFromPrincipal(principal, id, browser); + }); +}); + +// Test that temporary permissions are persisted through navigation in a tab. +add_task(async function testTempPermissionOnNavigation() { + let origin = "https://example.com/"; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let id = "geo"; + + await BrowserTestUtils.withNewTab(origin, async function (browser) { + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + browser + ); + + Assert.deepEqual(SitePermissions.getForPrincipal(principal, id, browser), { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + }); + + let loaded = BrowserTestUtils.browserLoaded( + browser, + false, + "https://example.org/" + ); + + // Navigate to another domain. + await SpecialPowers.spawn( + browser, + [], + () => (content.document.location = "https://example.org/") + ); + + await loaded; + + // The temporary permissions for the current URI should be reset. + Assert.deepEqual( + SitePermissions.getForPrincipal(browser.contentPrincipal, id, browser), + { + state: SitePermissions.UNKNOWN, + scope: SitePermissions.SCOPE_PERSISTENT, + } + ); + + loaded = BrowserTestUtils.browserLoaded(browser, false, origin); + + // Navigate to the original domain. + await SpecialPowers.spawn( + browser, + [], + () => (content.document.location = "https://example.com/") + ); + + await loaded; + + // The temporary permissions for the original URI should still exist. + Assert.deepEqual( + SitePermissions.getForPrincipal(browser.contentPrincipal, id, browser), + { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + } + ); + + SitePermissions.removeFromPrincipal(browser.contentPrincipal, id, browser); + }); +}); diff --git a/browser/base/content/test/permissions/browser_temporary_permissions_tabs.js b/browser/base/content/test/permissions/browser_temporary_permissions_tabs.js new file mode 100644 index 0000000000..a4347f9671 --- /dev/null +++ b/browser/base/content/test/permissions/browser_temporary_permissions_tabs.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that temp permissions are persisted through moving tabs to new windows. +add_task(async function testTempPermissionOnTabMove() { + let origin = "https://example.com/"; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let id = "geo"; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin); + + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + tab.linkedBrowser + ); + + Assert.deepEqual( + SitePermissions.getForPrincipal(principal, id, tab.linkedBrowser), + { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + } + ); + + let promiseWin = BrowserTestUtils.waitForNewWindow(); + gBrowser.replaceTabWithWindow(tab); + let win = await promiseWin; + tab = win.gBrowser.selectedTab; + + Assert.deepEqual( + SitePermissions.getForPrincipal(principal, id, tab.linkedBrowser), + { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + } + ); + + SitePermissions.removeFromPrincipal(principal, id, tab.linkedBrowser); + await BrowserTestUtils.closeWindow(win); +}); + +// Test that temp permissions don't affect other tabs of the same URI. +add_task(async function testTempPermissionMultipleTabs() { + let origin = "https://example.com/"; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let id = "geo"; + + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin); + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin); + + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + tab2.linkedBrowser + ); + + Assert.deepEqual( + SitePermissions.getForPrincipal(principal, id, tab2.linkedBrowser), + { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + } + ); + + Assert.deepEqual( + SitePermissions.getForPrincipal(principal, id, tab1.linkedBrowser), + { + state: SitePermissions.UNKNOWN, + scope: SitePermissions.SCOPE_PERSISTENT, + } + ); + + let geoIcon = document.querySelector( + ".blocked-permission-icon[data-permission-id=geo]" + ); + + Assert.notEqual( + geoIcon.getBoundingClientRect().width, + 0, + "geo anchor should be visible" + ); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + + Assert.equal( + geoIcon.getBoundingClientRect().width, + 0, + "geo anchor should not be visible" + ); + + SitePermissions.removeFromPrincipal(principal, id, tab2.linkedBrowser); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Test that temp permissions are cleared when closing tabs. +add_task(async function testTempPermissionOnTabClose() { + let origin = "https://example.com/"; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let id = "geo"; + + ok( + !SitePermissions._temporaryPermissions._stateByBrowser.size, + "Temporary permission map should be empty initially." + ); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, origin); + + SitePermissions.setForPrincipal( + principal, + id, + SitePermissions.BLOCK, + SitePermissions.SCOPE_TEMPORARY, + tab.linkedBrowser + ); + + Assert.deepEqual( + SitePermissions.getForPrincipal(principal, id, tab.linkedBrowser), + { + state: SitePermissions.BLOCK, + scope: SitePermissions.SCOPE_TEMPORARY, + } + ); + + ok( + SitePermissions._temporaryPermissions._stateByBrowser.has( + tab.linkedBrowser + ), + "Temporary permission map should have an entry for the browser." + ); + + BrowserTestUtils.removeTab(tab); + + ok( + !SitePermissions._temporaryPermissions._stateByBrowser.size, + "Temporary permission map should be empty after closing the tab." + ); +}); diff --git a/browser/base/content/test/permissions/dummy.js b/browser/base/content/test/permissions/dummy.js new file mode 100644 index 0000000000..c45ec0a714 --- /dev/null +++ b/browser/base/content/test/permissions/dummy.js @@ -0,0 +1 @@ +// Just a dummy file for testing. diff --git a/browser/base/content/test/permissions/empty.html b/browser/base/content/test/permissions/empty.html new file mode 100644 index 0000000000..1ad28bb1f7 --- /dev/null +++ b/browser/base/content/test/permissions/empty.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Empty file</title> +</head> +<body> +</body> +</html> diff --git a/browser/base/content/test/permissions/head.js b/browser/base/content/test/permissions/head.js new file mode 100644 index 0000000000..847386b7e2 --- /dev/null +++ b/browser/base/content/test/permissions/head.js @@ -0,0 +1,28 @@ +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +SpecialPowers.addTaskImport( + "E10SUtils", + "resource://gre/modules/E10SUtils.sys.mjs" +); + +function openPermissionPopup() { + let promise = BrowserTestUtils.waitForEvent( + gBrowser.ownerGlobal, + "popupshown", + true, + event => event.target == gPermissionPanel._permissionPopup + ); + gPermissionPanel._identityPermissionBox.click(); + return promise; +} + +function closePermissionPopup() { + let promise = BrowserTestUtils.waitForEvent( + gPermissionPanel._permissionPopup, + "popuphidden" + ); + gPermissionPanel._permissionPopup.hidePopup(); + return promise; +} diff --git a/browser/base/content/test/permissions/permissions.html b/browser/base/content/test/permissions/permissions.html new file mode 100644 index 0000000000..97286914e7 --- /dev/null +++ b/browser/base/content/test/permissions/permissions.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> +<script> +var gKeyDowns = 0; +var gKeyPresses = 0; + +navigator.serviceWorker.register("dummy.js"); + +function requestPush() { + return navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) { + serviceWorkerRegistration.pushManager.subscribe(); + }); +} + +function requestGeo() { + return navigator.geolocation.getCurrentPosition(() => { + parent.postMessage("allow", "*"); + }, error => { + // PERMISSION_DENIED = 1 + parent.postMessage(error.code == 1 ? "deny" : "allow", "*"); + }); +} + + +window.onmessage = function(event) { + switch (event.data) { + case "push": + requestPush(); + break; + } +}; + +</script> + <body onkeydown="gKeyDowns++;" onkeypress="gKeyPresses++"> + <!-- This page could eventually request permissions from content + and make sure that chrome responds appropriately --> + <button id="geo" onclick="requestGeo()">Geolocation</button> + <button id="xr" onclick="navigator.getVRDisplays()">XR</button> + <button id="desktop-notification" onclick="Notification.requestPermission()">Notifications</button> + <button id="push" onclick="requestPush()">Push Notifications</button> + <button id="camera" onclick="navigator.mediaDevices.getUserMedia({video: true, fake: true})">Camera</button> + </body> +</html> diff --git a/browser/base/content/test/permissions/temporary_permissions_frame.html b/browser/base/content/test/permissions/temporary_permissions_frame.html new file mode 100644 index 0000000000..25aede980f --- /dev/null +++ b/browser/base/content/test/permissions/temporary_permissions_frame.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Permissions Subframe Test</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> + <iframe id="frameAncestor" + src="https://test1.example.com/browser/browser/base/content/test/permissions/temporary_permissions_subframe.html" + allow="geolocation https://test1.example.com https://test2.example.com"></iframe> +</body> +</html> diff --git a/browser/base/content/test/permissions/temporary_permissions_subframe.html b/browser/base/content/test/permissions/temporary_permissions_subframe.html new file mode 100644 index 0000000000..4ff13f2e91 --- /dev/null +++ b/browser/base/content/test/permissions/temporary_permissions_subframe.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Temporary Permissions Subframe Test</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> + <iframe id="frame" src="https://example.org/browser/browser/base/content/test/permissions/permissions.html" allow="geolocation"></iframe> + <iframe id="frameAllowsAll" src="https://example.org/browser/browser/base/content/test/permissions/permissions.html" allow="geolocation *"></iframe> +</body> +</html> |