diff --git a/browser/actors/ClickHandlerChild.jsm b/browser/actors/ClickHandlerChild.jsm index 22f11081b9bd..0f3bf42e2290 100644 --- a/browser/actors/ClickHandlerChild.jsm +++ b/browser/actors/ClickHandlerChild.jsm @@ -57,13 +57,6 @@ class ClickHandlerChild extends JSWindowActorChild { return; } - // For untrusted events, require a valid transient user gesture activation. - // consumeTransientUserGestureActivation returns false if there isn't one, - // and consumes it otherwise. - if (!event.isTrusted && !ownerDoc.consumeTransientUserGestureActivation()) { - return; - } - // Handle click events from about pages if (event.button == 0) { if (ownerDoc.documentURI.startsWith("about:blocked")) { @@ -71,6 +64,11 @@ class ClickHandlerChild extends JSWindowActorChild { } } + // For untrusted events, require a valid transient user gesture activation. + if (!event.isTrusted && !ownerDoc.hasValidTransientUserGestureActivation) { + return; + } + let [href, node, principal] = BrowserUtils.hrefAndLinkNodeForClickEvent( event ); @@ -119,6 +117,26 @@ class ClickHandlerChild extends JSWindowActorChild { return; } + if ( + !event.isTrusted && + BrowserUtils.whereToOpenLink(event) != "current" + ) { + // If we'll open the link, we want to consume the user gesture + // activation to ensure that we don't allow multiple links to open + // from one user gesture. + // Avoid doing so for links opened in the current tab, which get + // handled later, by gecko, as otherwise its popup blocker will stop + // the link from opening. + // We will do the same check (whereToOpenLink) again in the parent and + // avoid handling the click for such links... but we still need the + // click information in the parent because otherwise places link + // tracking breaks. (bug 1742894 tracks improving this.) + ownerDoc.consumeTransientUserGestureActivation(); + // We don't care about the return value because we already checked that + // hasValidTransientUserGestureActivation was true earlier in this + // function. + } + json.href = href; if (node) { json.title = node.getAttribute("title"); diff --git a/browser/actors/test/browser/browser_untrusted_click_event.js b/browser/actors/test/browser/browser_untrusted_click_event.js index afb162520cc8..7bb579223da0 100644 --- a/browser/actors/test/browser/browser_untrusted_click_event.js +++ b/browser/actors/test/browser/browser_untrusted_click_event.js @@ -23,9 +23,85 @@ add_task(async function test_untrusted_click_opens_tab() { let eventObj = {}; eventObj[AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey"] = true; await BrowserTestUtils.synthesizeMouseAtCenter("#test", eventObj, browser); - info("Waiting for new tab to open; if we timeout the test is broken."); + info( + "Waiting for new tab to open from frontend; if we timeout the test is broken." + ); let newTab = await newTabOpened; ok(newTab, "New tab should be opened."); BrowserTestUtils.removeTab(newTab); }); }); + +/** + * Ensure that click handlers that redirect a modifier-less click into + * an untrusted event without modifiers don't run afoul of the popup + * blocker by having their transient user gesture bit consumed in the + * child by the handling code for modified clicks. + */ +add_task(async function test_unused_click_doesnt_consume_activation() { + // Enable the popup blocker. + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.disable_open_during_load", true], + ["dom.block_multiple_popups", true], + ], + }); + await BrowserTestUtils.withNewTab(TEST_PATH + "click.html", async browser => { + let newTabOpened = BrowserTestUtils.waitForNewTab( + gBrowser, + "https://example.com/", + true + ); + info("clicking button that generates link press."); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#fire-untrusted", + {}, + browser + ); + info( + "Waiting for new tab to open through gecko; if we timeout the test is broken." + ); + let newTab = await newTabOpened; + ok(newTab, "New tab should be opened."); + BrowserTestUtils.removeTab(newTab); + }); +}); + +/** + * Ensure that click handlers redirecting to elements without href don't + * trigger user gesture consumption either. + */ +add_task(async function test_click_without_href_doesnt_consume_activation() { + // Enable the popup blocker. + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.disable_open_during_load", true], + ["dom.block_multiple_popups", true], + ], + }); + await BrowserTestUtils.withNewTab(TEST_PATH + "click.html", async browser => { + let newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, "about:blank"); + info("clicking link that generates link click on target-less link."); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#fire-targetless-link", + {}, + browser + ); + info( + "Waiting for new tab to open after targetless link; if we timeout the test is broken." + ); + let newTab = await newTabOpened; + ok(newTab, "New tab should be opened."); + BrowserTestUtils.removeTab(newTab); + + newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, "about:blank"); + info("clicking link that generates button click."); + await BrowserTestUtils.synthesizeMouseAtCenter("#fire-button", {}, browser); + info( + "Waiting for new tab to open after button redirect; if we timeout the test is broken." + ); + newTab = await newTabOpened; + ok(newTab, "New tab should be opened."); + BrowserTestUtils.removeTab(newTab); + }); +}); diff --git a/browser/actors/test/browser/click.html b/browser/actors/test/browser/click.html index 366e9c924a35..42afdf8050a4 100644 --- a/browser/actors/test/browser/click.html +++ b/browser/actors/test/browser/click.html @@ -6,7 +6,13 @@
-click me +click me