Bug 1745720 - use 'chromelinkclick' command event to avoid instantiating ClickHandlerChild except for link clicks, r=smaug,mconley

Instead of relying on untrusted click/auxclick events anywhere
instantiating the actor, and then having to look for links, after this
patch we'll only instantiate the actor for actual link clicks. This
patch moves to using a chrome-only command event (with type
`chromelinkclick`) dispatched from the link click post-visitor
to accomplish that.

In future we should probably move both this and the
middle-click-to-paste handling into DOM code (or, for the latter,
remove it) but this is a less invasive solution.

This also moves the middle-click-to-paste handling into its own
listener. It needs to listen to page events in general (not just
links) but is disabled everywhere by default, so registering an
actor for everyone doesn't seem like a good trade-off. To avoid
duplicating all the logic (we do need to avoid doing middle-click
navigation based on the clipboard when clicking on links!), as
well as keeping patch size down, the actual control flow goes
through the click handler actor still.

Differential Revision: https://phabricator.services.mozilla.com/D134011
This commit is contained in:
Gijs Kruitbosch 2022-01-13 00:00:57 +00:00
Родитель c150b0a2c0
Коммит 6b4f4672d0
5 изменённых файлов: 143 добавлений и 42 удалений

Просмотреть файл

@ -3,9 +3,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var EXPORTED_SYMBOLS = ["ClickHandlerChild"]; var EXPORTED_SYMBOLS = ["ClickHandlerChild", "MiddleMousePasteHandlerChild"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
ChromeUtils.defineModuleGetter( ChromeUtils.defineModuleGetter(
this, this,
@ -29,15 +32,44 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/BrowserUtils.jsm" "resource://gre/modules/BrowserUtils.jsm"
); );
class ClickHandlerChild extends JSWindowActorChild { class MiddleMousePasteHandlerChild extends JSWindowActorChild {
handleEvent(event) { handleEvent(clickEvent) {
if ( if (
event.defaultPrevented || clickEvent.defaultPrevented ||
event.button == 2 || clickEvent.button != 1 ||
(event.type == "click" && event.button == 1) MiddleMousePasteHandlerChild.autoscrollEnabled
) { ) {
return; return;
} }
this.manager
.getActor("ClickHandler")
.handleClickEvent(
clickEvent,
/* is from middle mouse paste handler */ true
);
}
onProcessedClick(data) {
this.sendAsyncMessage("MiddleClickPaste", data);
}
}
XPCOMUtils.defineLazyPreferenceGetter(
MiddleMousePasteHandlerChild,
"autoscrollEnabled",
"general.autoScroll",
true
);
class ClickHandlerChild extends JSWindowActorChild {
handleEvent(wrapperEvent) {
this.handleClickEvent(wrapperEvent.sourceEvent);
}
handleClickEvent(event, isFromMiddleMousePasteHandler = false) {
if (event.defaultPrevented || event.button == 2) {
return;
}
// Don't do anything on editable things, we shouldn't open links in // Don't do anything on editable things, we shouldn't open links in
// contenteditables, and editor needs to possibly handle middlemouse paste // contenteditables, and editor needs to possibly handle middlemouse paste
let composedTarget = event.composedTarget; let composedTarget = event.composedTarget;
@ -107,7 +139,7 @@ class ClickHandlerChild extends JSWindowActorChild {
), ),
}; };
if (href) { if (href && !isFromMiddleMousePasteHandler) {
try { try {
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal( Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
principal, principal,
@ -157,12 +189,11 @@ class ClickHandlerChild extends JSWindowActorChild {
} }
this.sendAsyncMessage("Content:Click", json); this.sendAsyncMessage("Content:Click", json);
return;
} }
// This might be middle mouse navigation. // This might be middle mouse navigation, in which case pass this back:
if (event.button == 1) { if (!href && event.button == 1 && isFromMiddleMousePasteHandler) {
this.sendAsyncMessage("Content:Click", json); this.manager.getActor("MiddleMousePasteHandler").onProcessedClick(json);
} }
} }
} }

Просмотреть файл

@ -5,9 +5,7 @@
"use strict"; "use strict";
var EXPORTED_SYMBOLS = ["ClickHandlerParent"]; var EXPORTED_SYMBOLS = ["ClickHandlerParent", "MiddleMousePasteHandlerParent"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter( ChromeUtils.defineModuleGetter(
this, this,
@ -27,6 +25,22 @@ ChromeUtils.defineModuleGetter(
let gContentClickListeners = new Set(); let gContentClickListeners = new Set();
class MiddleMousePasteHandlerParent extends JSWindowActorParent {
receiveMessage(message) {
if (message.name == "MiddleClickPaste") {
// This is heavily based on contentAreaClick from browser.js (Bug 903016)
// The data is set up in a way to look like an Event.
let browser = this.manager.browsingContext.top.embedderElement;
if (!browser) {
// Can be null if the tab disappeared by the time we got the message.
// Just bail.
return;
}
browser.ownerGlobal.middleMousePaste(message.data);
}
}
}
class ClickHandlerParent extends JSWindowActorParent { class ClickHandlerParent extends JSWindowActorParent {
static addContentClickListener(listener) { static addContentClickListener(listener) {
gContentClickListeners.add(listener); gContentClickListeners.add(listener);
@ -62,17 +76,6 @@ class ClickHandlerParent extends JSWindowActorParent {
} }
let window = browser.ownerGlobal; let window = browser.ownerGlobal;
if (!data.href) {
// Might be middle mouse navigation.
if (
Services.prefs.getBoolPref("middlemouse.contentLoadURL") &&
!Services.prefs.getBoolPref("general.autoScroll")
) {
window.middleMousePaste(data);
}
return;
}
// If the browser is not in a place where we can open links, bail out. // If the browser is not in a place where we can open links, bail out.
// This can happen in osx sheets, dialogs, etc. that are not browser // This can happen in osx sheets, dialogs, etc. that are not browser
// windows. Specifically the payments UI is in an osx sheet. // windows. Specifically the payments UI is in an osx sheet.

Просмотреть файл

@ -393,14 +393,34 @@ let JSWINDOWACTORS = {
child: { child: {
moduleURI: "resource:///actors/ClickHandlerChild.jsm", moduleURI: "resource:///actors/ClickHandlerChild.jsm",
events: { events: {
click: { capture: true, mozSystemGroup: true, wantUntrusted: true }, chromelinkclick: { capture: true, mozSystemGroup: true },
auxclick: { capture: true, mozSystemGroup: true, wantUntrusted: true },
}, },
}, },
allFrames: true, allFrames: true,
}, },
/* Note: this uses the same JSMs as ClickHandler, but because it
* relies on "normal" click events anywhere on the page (not just
* links) and is expensive, and only does something for the
* small group of people who have the feature enabled, it is its
* own actor which is only registered if the pref is enabled.
*/
MiddleMousePasteHandler: {
parent: {
moduleURI: "resource:///actors/ClickHandlerParent.jsm",
},
child: {
moduleURI: "resource:///actors/ClickHandlerChild.jsm",
events: {
auxclick: { capture: true, mozSystemGroup: true },
},
},
enablePreference: "middlemouse.contentLoadURL",
allFrames: true,
},
ContentSearch: { ContentSearch: {
parent: { parent: {
moduleURI: "resource:///actors/ContentSearchParent.jsm", moduleURI: "resource:///actors/ContentSearchParent.jsm",

Просмотреть файл

@ -88,6 +88,7 @@
#include "mozilla/dom/HTMLTemplateElement.h" #include "mozilla/dom/HTMLTemplateElement.h"
#include "mozilla/dom/KeyframeAnimationOptionsBinding.h" #include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
#include "mozilla/dom/KeyframeEffect.h" #include "mozilla/dom/KeyframeEffect.h"
#include "mozilla/dom/MouseEvent.h"
#include "mozilla/dom/MouseEventBinding.h" #include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/MutationEventBinding.h" #include "mozilla/dom/MutationEventBinding.h"
#include "mozilla/dom/MutationObservers.h" #include "mozilla/dom/MutationObservers.h"
@ -100,6 +101,7 @@
#include "mozilla/dom/ShadowRoot.h" #include "mozilla/dom/ShadowRoot.h"
#include "mozilla/dom/Text.h" #include "mozilla/dom/Text.h"
#include "mozilla/dom/WindowBinding.h" #include "mozilla/dom/WindowBinding.h"
#include "mozilla/dom/XULCommandEvent.h"
#include "mozilla/dom/nsCSPContext.h" #include "mozilla/dom/nsCSPContext.h"
#include "mozilla/gfx/BasePoint.h" #include "mozilla/gfx/BasePoint.h"
#include "mozilla/gfx/BaseRect.h" #include "mozilla/gfx/BaseRect.h"
@ -3068,12 +3070,51 @@ void Element::GetEventTargetParentForLinks(EventChainPreVisitor& aVisitor) {
} }
} }
// This dispatches a 'chromelinkclick' CustomEvent to chrome-only listeners,
// so that frontend can handle middle-clicks and ctrl/cmd/shift/etc.-clicks
// on links, without getting a call for every single click the user makes.
// Only supported for click or auxclick events.
void Element::DispatchChromeOnlyLinkClickEvent(
EventChainPostVisitor& aVisitor) {
MOZ_ASSERT(aVisitor.mEvent->mMessage == eMouseAuxClick ||
aVisitor.mEvent->mMessage == eMouseClick,
"DispatchChromeOnlyLinkClickEvent supports only click and "
"auxclick source events");
Document* doc = OwnerDoc();
RefPtr<XULCommandEvent> event =
new XULCommandEvent(doc, aVisitor.mPresContext, nullptr);
RefPtr<dom::Event> mouseDOMEvent = aVisitor.mDOMEvent;
if (!mouseDOMEvent) {
mouseDOMEvent = EventDispatcher::CreateEvent(
aVisitor.mEvent->mOriginalTarget, aVisitor.mPresContext,
aVisitor.mEvent, u""_ns);
aVisitor.mDOMEvent = mouseDOMEvent;
}
MouseEvent* mouseEvent = mouseDOMEvent->AsMouseEvent();
event->InitCommandEvent(
u"chromelinkclick"_ns, /* CanBubble */ true,
/* Cancelable */ true, nsGlobalWindowInner::Cast(doc->GetInnerWindow()),
0, mouseEvent->CtrlKey(), mouseEvent->AltKey(), mouseEvent->ShiftKey(),
mouseEvent->MetaKey(), mouseEvent->Button(), mouseDOMEvent,
mouseEvent->MozInputSource(), IgnoreErrors());
// Note: we're always trusted, but the event we pass as the `sourceEvent`
// might not be. Frontend code will check that event's trusted property to
// make that determination; doing it this way means we don't also start
// acting on web-generated custom 'chromelinkclick' events which would
// provide additional attack surface for a malicious actor.
event->SetTrusted(true);
event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
DispatchEvent(*event);
}
nsresult Element::PostHandleEventForLinks(EventChainPostVisitor& aVisitor) { nsresult Element::PostHandleEventForLinks(EventChainPostVisitor& aVisitor) {
// Optimisation: return early if this event doesn't interest us. // Optimisation: return early if this event doesn't interest us.
// IMPORTANT: this switch and the switch below it must be kept in sync! // IMPORTANT: this switch and the switch below it must be kept in sync!
switch (aVisitor.mEvent->mMessage) { switch (aVisitor.mEvent->mMessage) {
case eMouseDown: case eMouseDown:
case eMouseClick: case eMouseClick:
case eMouseAuxClick:
case eLegacyDOMActivate: case eLegacyDOMActivate:
case eKeyPress: case eKeyPress:
break; break;
@ -3128,26 +3169,30 @@ nsresult Element::PostHandleEventForLinks(EventChainPostVisitor& aVisitor) {
case eMouseClick: { case eMouseClick: {
WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
if (mouseEvent->IsLeftClickEvent()) { if (mouseEvent->IsLeftClickEvent()) {
if (mouseEvent->IsControl() || mouseEvent->IsMeta() || if (!mouseEvent->IsControl() && !mouseEvent->IsMeta() &&
mouseEvent->IsAlt() || mouseEvent->IsShift()) { !mouseEvent->IsAlt() && !mouseEvent->IsShift()) {
break; // The default action is simply to dispatch DOMActivate
nsEventStatus status = nsEventStatus_eIgnore;
// DOMActivate event should be trusted since the activation is
// actually occurred even if the cause is an untrusted click event.
InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent);
actEvent.mDetail = 1;
rv = EventDispatcher::Dispatch(this, aVisitor.mPresContext, &actEvent,
nullptr, &status);
if (NS_SUCCEEDED(rv)) {
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
}
} }
// The default action is simply to dispatch DOMActivate DispatchChromeOnlyLinkClickEvent(aVisitor);
nsEventStatus status = nsEventStatus_eIgnore;
// DOMActive event should be trusted since the activation is actually
// occurred even if the cause is an untrusted click event.
InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent);
actEvent.mDetail = 1;
rv = EventDispatcher::Dispatch(this, aVisitor.mPresContext, &actEvent,
nullptr, &status);
if (NS_SUCCEEDED(rv)) {
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
}
} }
break; break;
} }
case eMouseAuxClick: {
DispatchChromeOnlyLinkClickEvent(aVisitor);
break;
}
case eLegacyDOMActivate: { case eLegacyDOMActivate: {
if (aVisitor.mEvent->mOriginalTarget == this) { if (aVisitor.mEvent->mOriginalTarget == this) {
nsAutoString target; nsAutoString target;

Просмотреть файл

@ -1950,6 +1950,8 @@ class Element : public FragmentOrElement {
*/ */
void GetEventTargetParentForLinks(EventChainPreVisitor& aVisitor); void GetEventTargetParentForLinks(EventChainPreVisitor& aVisitor);
void DispatchChromeOnlyLinkClickEvent(EventChainPostVisitor& aVisitor);
/** /**
* Handle default actions for link event if the event isn't consumed yet. * Handle default actions for link event if the event isn't consumed yet.
*/ */