Bug 1890721 - Prevent events from reaching the content page. r=sfoster,smaug

Differential Revision: https://phabricator.services.mozilla.com/D207749
This commit is contained in:
Niklas Baumgardner 2024-05-15 20:17:21 +00:00
parent 592ca16351
commit da75251680
7 changed files with 251 additions and 27 deletions

View File

@ -10,10 +10,14 @@ ChromeUtils.defineESModuleGetters(lazy, {
ScreenshotsOverlay: "resource:///modules/ScreenshotsOverlayChild.sys.mjs",
});
const SCREENSHOTS_PREVENT_CONTENT_EVENTS_PREF =
"screenshots.browser.component.preventContentEvents";
export class ScreenshotsComponentChild extends JSWindowActorChild {
#resizeTask;
#scrollTask;
#overlay;
#preventableEventsAdded = false;
static OVERLAY_EVENTS = [
"click",
@ -24,6 +28,21 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
"keydown",
];
// The following events are only listened to so we can prevent them from
// reaching the content page. The events in OVERLAY_EVENTS are also prevented.
static PREVENTABLE_EVENTS = [
"mousemove",
"mousedown",
"mouseup",
"touchstart",
"touchmove",
"touchend",
"dblclick",
"auxclick",
"keypress",
"contextmenu",
];
get overlay() {
return this.#overlay;
}
@ -62,19 +81,28 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
return;
}
// Handle overlay events here
if (
ScreenshotsComponentChild.OVERLAY_EVENTS.includes(event.type) ||
ScreenshotsComponentChild.PREVENTABLE_EVENTS.includes(event.type)
) {
if (!this.overlay?.initialized) {
return;
}
// Preventing a pointerdown event throws an error in debug builds.
// See https://searchfox.org/mozilla-central/rev/b41bb321fe4bd7d03926083698ac498ebec0accf/widget/WidgetEventImpl.cpp#566-572
// Don't prevent the default context menu.
if (!["contextmenu", "pointerdown"].includes(event.type)) {
event.preventDefault();
}
event.stopImmediatePropagation();
this.overlay.handleEvent(event);
return;
}
switch (event.type) {
case "click":
case "pointerdown":
case "pointermove":
case "pointerup":
case "keyup":
case "keydown":
case "selectionchange":
if (!this.overlay?.initialized) {
return;
}
this.overlay.handleEvent(event);
break;
case "beforeunload":
this.requestCancelScreenshot("navigation");
break;
@ -226,7 +254,16 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
chromeEventHandler.addEventListener(event, this, true);
}
this.document.addEventListener("selectionchange", this);
if (Services.prefs.getBoolPref(SCREENSHOTS_PREVENT_CONTENT_EVENTS_PREF)) {
for (let event of ScreenshotsComponentChild.PREVENTABLE_EVENTS) {
chromeEventHandler.addEventListener(event, this, true);
}
this.#preventableEventsAdded = true;
}
}
/**
@ -264,7 +301,16 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
chromeEventHandler.removeEventListener(event, this, true);
}
this.document.removeEventListener("selectionchange", this);
if (this.#preventableEventsAdded) {
for (let event of ScreenshotsComponentChild.PREVENTABLE_EVENTS) {
chromeEventHandler.removeEventListener(event, this, true);
}
}
this.#preventableEventsAdded = false;
}
/**

View File

@ -2441,6 +2441,9 @@ pref("screenshots.browser.component.enabled", true);
// Preference that determines what button to focus
pref("screenshots.browser.component.last-saved-method", "download");
// Preference that prevents events from reaching the content page.
pref("screenshots.browser.component.preventContentEvents", true);
// DoH Rollout: whether to clear the mode value at shutdown.
pref("doh-rollout.clearModeOnShutdown", false);

View File

@ -74,6 +74,8 @@ skip-if = ["!crashreporter"]
["browser_test_moving_tab_to_new_window.js"]
["browser_test_prevent_events.js"]
["browser_test_resize.js"]
["browser_test_selection_size_text.js"]

View File

@ -16,13 +16,6 @@ add_task(async function test() {
},
async browser => {
await clearAllTelemetryEvents();
await SpecialPowers.spawn(browser, [SHORT_TEST_PAGE], url => {
let a = content.document.createElement("a");
a.id = "clickMe";
a.href = url;
a.textContent = "Click me to unload page";
content.document.querySelector("body").appendChild(a);
});
let helper = new ScreenshotsHelper(browser);
@ -31,7 +24,7 @@ add_task(async function test() {
await helper.waitForOverlay();
await SpecialPowers.spawn(browser, [], () => {
content.document.querySelector("#clickMe").click();
content.location.reload();
});
await helper.waitForOverlayClosed();

View File

@ -0,0 +1,84 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_events_prevented() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: SHORT_TEST_PAGE,
},
async browser => {
let helper = new ScreenshotsHelper(browser);
await ContentTask.spawn(browser, null, async () => {
let { ScreenshotsComponentChild } = ChromeUtils.importESModule(
"resource:///actors/ScreenshotsComponentChild.sys.mjs"
);
let allOverlayEvents = ScreenshotsComponentChild.OVERLAY_EVENTS.concat(
ScreenshotsComponentChild.PREVENTABLE_EVENTS
);
content.eventsReceived = [];
function eventListener(event) {
content.window.eventsReceived.push(event.type);
}
for (let eventName of [...allOverlayEvents, "wheel"]) {
content.addEventListener(eventName, eventListener, true);
}
});
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
// key events
await key.down("s");
await key.up("s");
await key.press("s");
// touch events
await touch.start(10, 10);
await touch.move(20, 20);
await touch.end(20, 20);
// pointermove/mousemove, pointerdown/mousedown, pointerup/mouseup events
await helper.clickTestPageElement();
// click events and contextmenu
await mouse.dblclick(100, 100);
await mouse.auxclick(100, 100, { button: 1 });
await mouse.click(100, 100);
await mouse.contextmenu(100, 100);
let wheelEventPromise = helper.waitForContentEventOnce("wheel");
await ContentTask.spawn(browser, null, () => {
content.dispatchEvent(new content.WheelEvent("wheel"));
});
await wheelEventPromise;
let contentEventsReceived = await ContentTask.spawn(
browser,
null,
async () => {
return content.eventsReceived;
}
);
// Events are synchronous so if we only have 1 wheel at the end,
// we did not receive any other events
is(
contentEventsReceived.length,
1,
"Only 1 wheel event should reach the content document because everything else was prevent and stopped propagation"
);
is(
contentEventsReceived[0],
"wheel",
"Only 1 wheel event should reach the content document because everything else was prevent and stopped propagation"
);
}
);
});

View File

@ -34,16 +34,31 @@ const gScreenshotUISelectors = {
copyButton: "button.#copy",
};
// MouseEvents is for the mouse events on the Anonymous content
const MouseEvents = {
// AnonymousContentEvents is for the mouse, keyboard, and touch events on the Anonymous content
const AnonymousContentEvents = {
mouse: new Proxy(
{},
{
get: (target, name) =>
async function (x, y, options = {}, browser) {
if (name === "click") {
if (name.includes("click")) {
this.down(x, y, options, browser);
this.up(x, y, options, browser);
if (name.includes("dbl")) {
this.down(x, y, options, browser);
this.up(x, y, options, browser);
}
} else if (name === "contextmenu") {
await safeSynthesizeMouseEventInContentPage(
":root",
x,
y,
{
type: name,
...options,
},
browser
);
} else {
await safeSynthesizeMouseEventInContentPage(
":root",
@ -59,9 +74,40 @@ const MouseEvents = {
},
}
),
key: new Proxy(
{},
{
get: (target, name) =>
async function (key, options = {}, browser) {
await safeSynthesizeKeyEventInContentPage(
key,
{ type: "key" + name, ...options },
browser
);
},
}
),
touch: new Proxy(
{},
{
get: (target, name) =>
async function (x, y, options = {}, browser) {
await safeSynthesizeTouchEventInContentPage(
":root",
x,
y,
{
type: "touch" + name,
...options,
},
browser
);
},
}
),
};
const { mouse } = MouseEvents;
const { mouse, key, touch } = AnonymousContentEvents;
class ScreenshotsHelper {
constructor(browser) {
@ -821,6 +867,14 @@ class ScreenshotsHelper {
});
}
waitForContentEventOnce(event) {
return ContentTask.spawn(this.browser, event, eventType => {
return new Promise(resolve => {
content.addEventListener(eventType, resolve, { once: true });
});
});
}
/**
* Copied from screenshots extension
* A helper that returns the size of the image that was just put into the clipboard by the
@ -955,7 +1009,7 @@ function getRawClipboardData(flavor) {
}
/**
* Synthesize a mouse event on an element, after ensuring that it is visible
* Synthesize a mouse event on an element
* in the viewport.
*
* @param {String} selector: The node selector to get the node target for the event.
@ -976,7 +1030,49 @@ async function safeSynthesizeMouseEventInContentPage(
} else {
context = browser.browsingContext;
}
BrowserTestUtils.synthesizeMouse(selector, x, y, options, context);
await BrowserTestUtils.synthesizeMouse(selector, x, y, options, context);
}
/**
* Synthesize a key event on an element
* in the viewport.
*
* @param {string} key The key
* @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeKey
*/
async function safeSynthesizeKeyEventInContentPage(aKey, options, browser) {
let context;
if (!browser) {
context = gBrowser.selectedBrowser.browsingContext;
} else {
context = browser.browsingContext;
}
await BrowserTestUtils.synthesizeKey(aKey, options, context);
}
/**
* Synthesize a touch event on an element
* in the viewport.
*
* @param {String} selector: The node selector to get the node target for the event.
* @param {number} x
* @param {number} y
* @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeTouch
*/
async function safeSynthesizeTouchEventInContentPage(
selector,
x,
y,
options = {},
browser
) {
let context;
if (!browser) {
context = gBrowser.selectedBrowser.browsingContext;
} else {
context = browser.browsingContext;
}
await BrowserTestUtils.synthesizeTouch(selector, x, y, options, context);
}
add_setup(async () => {

View File

@ -3,6 +3,6 @@
<title>Screenshots</title>
</head>
<body>
<div style="height:500px; width:500px; background-color: blue;"></div>
<div id="testPageElement" style="height:500px; width:500px; background-color: blue;"></div>
</body>
</html>