Issue #2721 - Create special case exception for <A>.Click() outside of DOM

This removes the requirement for there to be a non-null PresShell to
dispatch `Click()` events on `<A>` elements (only), since the exception
to the rule has propagated to the spec.

With these changes it should now be possible do create an anchor and
`Click()` on it from JS without actually first attaching it to the DOM
of the presented document, as abused by scripted downloads in pages
(instead of using the A attribute to custom-name downloads).
This commit is contained in:
Moonchild 2025-06-09 23:45:31 +02:00 • committed by roytam1
commit 37de431ac0
15 changed files with 79 additions and 94 deletions

View file

@ -149,6 +149,8 @@
#include "nsDOMStringMap.h" #include "nsDOMStringMap.h"
#include "DOMIntersectionObserver.h" #include "DOMIntersectionObserver.h"
#include "nsDocShell.h" // for ::Cast
using namespace mozilla; using namespace mozilla;
using namespace mozilla::dom; using namespace mozilla::dom;
@ -2259,12 +2261,15 @@ Element::GetPrimaryFrame(mozFlushType aType)
nsresult nsresult
Element::LeaveLink(nsPresContext* aPresContext) Element::LeaveLink(nsPresContext* aPresContext)
{ {
nsILinkHandler *handler = aPresContext->GetLinkHandler(); if (!aPresContext || !aPresContext->Document()->LinkHandlingEnabled()) {
if (!handler) {
return NS_OK; return NS_OK;
} }
return handler->OnLeaveLink(); nsIDocShell* shell = aPresContext->Document()->GetDocShell();
if (!shell) {
return NS_OK;
}
return nsDocShell::Cast(shell)->OnLeaveLink();
} }
nsresult nsresult
@ -3175,7 +3180,6 @@ Element::CheckHandleEventForLinksPrecondition(EventChainVisitor& aVisitor,
(aVisitor.mEvent->mMessage != eMouseClick) && (aVisitor.mEvent->mMessage != eMouseClick) &&
(aVisitor.mEvent->mMessage != eKeyPress) && (aVisitor.mEvent->mMessage != eKeyPress) &&
(aVisitor.mEvent->mMessage != eLegacyDOMActivate)) || (aVisitor.mEvent->mMessage != eLegacyDOMActivate)) ||
!aVisitor.mPresContext ||
aVisitor.mEvent->mFlags.mMultipleActionsPrevented) { aVisitor.mEvent->mFlags.mMultipleActionsPrevented) {
return false; return false;
} }
@ -3220,7 +3224,7 @@ Element::GetEventTargetParentForLinks(EventChainPreVisitor& aVisitor)
if (!focusEvent || !focusEvent->mIsRefocus) { if (!focusEvent || !focusEvent->mIsRefocus) {
nsAutoString target; nsAutoString target;
GetLinkTarget(target); GetLinkTarget(target);
nsContentUtils::TriggerLink(this, aVisitor.mPresContext, absURI, target, nsContentUtils::TriggerLink(this, absURI, target,
false, true, true); false, true, true);
// Make sure any ancestor links don't also TriggerLink // Make sure any ancestor links don't also TriggerLink
aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
@ -3272,20 +3276,20 @@ Element::PostHandleEventForLinks(EventChainPostVisitor& aVisitor)
switch (aVisitor.mEvent->mMessage) { switch (aVisitor.mEvent->mMessage) {
case eMouseDown: case eMouseDown:
{ {
if (aVisitor.mEvent->AsMouseEvent()->button == if (aVisitor.mEvent->AsMouseEvent()->button == WidgetMouseEvent::eLeftButton &&
WidgetMouseEvent::eLeftButton) { OwnerDoc()->LinkHandlingEnabled()) {
// don't make the link grab the focus if there is no link handler aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
nsILinkHandler *handler = aVisitor.mPresContext->GetLinkHandler();
nsIDocument *document = GetComposedDoc(); if (IsInComposedDoc()) {
if (handler && document) {
nsIFocusManager* fm = nsFocusManager::GetFocusManager(); nsIFocusManager* fm = nsFocusManager::GetFocusManager();
if (fm) { if (fm) {
aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
nsCOMPtr<nsIDOMElement> elem = do_QueryInterface(this); nsCOMPtr<nsIDOMElement> elem = do_QueryInterface(this);
fm->SetFocus(elem, nsIFocusManager::FLAG_BYMOUSE | fm->SetFocus(elem, nsIFocusManager::FLAG_BYMOUSE |
nsIFocusManager::FLAG_NOSCROLL); nsIFocusManager::FLAG_NOSCROLL);
} }
}
if (aVisitor.mPresContext) {
EventStateManager::SetActiveManager( EventStateManager::SetActiveManager(
aVisitor.mPresContext->EventStateManager(), this); aVisitor.mPresContext->EventStateManager(), this);
} }
@ -3302,19 +3306,18 @@ Element::PostHandleEventForLinks(EventChainPostVisitor& aVisitor)
} }
// The default action is simply to dispatch DOMActivate // The default action is simply to dispatch DOMActivate
nsCOMPtr<nsIPresShell> shell = aVisitor.mPresContext->GetPresShell(); nsEventStatus status = nsEventStatus_eIgnore;
if (shell) { // The DOMActive event should be trusted since the activation has actually
// single-click // occurred even if the cause is an untrusted click event.
nsEventStatus status = nsEventStatus_eIgnore; // This is a hack to allow click events to happen on anchors outside document
// DOMActive event should be trusted since the activation is actually // contexts (where there is no presShell)... Thanks, Google, for another ugly one.
// occurred even if the cause is an untrusted click event. InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent);
InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent); actEvent.mDetail = 1;
actEvent.mDetail = 1;
rv = shell->HandleDOMEventWithTarget(this, &actEvent, &status); rv = EventDispatcher::Dispatch(this, aVisitor.mPresContext, &actEvent,
if (NS_SUCCEEDED(rv)) { nullptr, &status);
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; if (NS_SUCCEEDED(rv)) {
} aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
} }
} }
break; break;
@ -3326,7 +3329,7 @@ Element::PostHandleEventForLinks(EventChainPostVisitor& aVisitor)
GetLinkTarget(target); GetLinkTarget(target);
const InternalUIEvent* activeEvent = aVisitor.mEvent->AsUIEvent(); const InternalUIEvent* activeEvent = aVisitor.mEvent->AsUIEvent();
MOZ_ASSERT(activeEvent); MOZ_ASSERT(activeEvent);
nsContentUtils::TriggerLink(this, aVisitor.mPresContext, absURI, target, nsContentUtils::TriggerLink(this, absURI, target,
true, true, activeEvent->IsTrustable()); true, true, activeEvent->IsTrustable());
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
} }

View file

@ -89,6 +89,7 @@
#include "nsCycleCollectionParticipant.h" #include "nsCycleCollectionParticipant.h"
#include "nsCycleCollector.h" #include "nsCycleCollector.h"
#include "nsDataHashtable.h" #include "nsDataHashtable.h"
#include "nsDocShell.h"
#include "nsDocShellCID.h" #include "nsDocShellCID.h"
#include "nsDocument.h" #include "nsDocument.h"
#include "nsDOMCID.h" #include "nsDOMCID.h"
@ -5336,25 +5337,24 @@ nsContentUtils::CombineResourcePrincipals(nsCOMPtr<nsIPrincipal>* aResourcePrinc
/* static */ /* static */
void void
nsContentUtils::TriggerLink(nsIContent *aContent, nsPresContext *aPresContext, nsContentUtils::TriggerLink(nsIContent *aContent,
nsIURI *aLinkURI, const nsString &aTargetSpec, nsIURI *aLinkURI, const nsString &aTargetSpec,
bool aClick, bool aIsUserTriggered, bool aClick, bool aIsUserTriggered,
bool aIsTrusted) bool aIsTrusted)
{ {
NS_ASSERTION(aPresContext, "Need a nsPresContext");
NS_PRECONDITION(aLinkURI, "No link URI"); NS_PRECONDITION(aLinkURI, "No link URI");
if (aContent->IsEditable()) { if (aContent->IsEditable() || !aContent->OwnerDoc()->LinkHandlingEnabled()) {
return; return;
} }
nsILinkHandler *handler = aPresContext->GetLinkHandler(); nsCOMPtr<nsIDocShell> docShell = aContent->OwnerDoc()->GetDocShell();
if (!handler) { if (!docShell) {
return; return;
} }
if (!aClick) { if (!aClick) {
handler->OnOverLink(aContent, aLinkURI, aTargetSpec.get()); nsDocShell::Cast(docShell)->OnOverLink(aContent, aLinkURI, aTargetSpec.get());
return; return;
} }
@ -5397,9 +5397,9 @@ nsContentUtils::TriggerLink(nsIContent *aContent, nsPresContext *aPresContext,
} }
} }
handler->OnLinkClick(aContent, aLinkURI, nsDocShell::Cast(docShell)->OnLinkClick(aContent, aLinkURI,
fileName.IsVoid() ? aTargetSpec.get() : EmptyString().get(), fileName.IsVoid() ? aTargetSpec.get() : EmptyString().get(),
fileName, nullptr, nullptr, aIsTrusted, aContent->NodePrincipal()); fileName, nullptr, nullptr, aIsTrusted, aContent->NodePrincipal());
} }
} }

View file

@ -1711,7 +1711,6 @@ public:
* security check using aContent's principal. * security check using aContent's principal.
* *
* @param aContent the node on which a link was triggered. * @param aContent the node on which a link was triggered.
* @param aPresContext the pres context, must be non-null.
* @param aLinkURI the URI of the link, must be non-null. * @param aLinkURI the URI of the link, must be non-null.
* @param aTargetSpec the target (like target=, may be empty). * @param aTargetSpec the target (like target=, may be empty).
* @param aClick whether this was a click or not (if false, this method * @param aClick whether this was a click or not (if false, this method
@ -1722,7 +1721,7 @@ public:
* @param aIsTrusted If false, JS Context will be pushed to stack * @param aIsTrusted If false, JS Context will be pushed to stack
* when the link is triggered. * when the link is triggered.
*/ */
static void TriggerLink(nsIContent *aContent, nsPresContext *aPresContext, static void TriggerLink(nsIContent *aContent,
nsIURI *aLinkURI, const nsString& aTargetSpec, nsIURI *aLinkURI, const nsString& aTargetSpec,
bool aClick, bool aIsUserTriggered, bool aClick, bool aIsUserTriggered,
bool aIsTrusted); bool aIsTrusted);

View file

@ -1288,6 +1288,7 @@ nsIDocument::nsIDocument()
nsDocument::nsDocument(const char* aContentType) nsDocument::nsDocument(const char* aContentType)
: nsIDocument() : nsIDocument()
, mLinksEnabled(true)
, mViewportType(Unknown) , mViewportType(Unknown)
{ {
SetContentTypeInternal(nsDependentCString(aContentType)); SetContentTypeInternal(nsDependentCString(aContentType));

View file

@ -412,6 +412,14 @@ public:
virtual void RemoveIDTargetObserver(nsIAtom* aID, IDTargetObserver aObserver, virtual void RemoveIDTargetObserver(nsIAtom* aID, IDTargetObserver aObserver,
void* aData, bool aForImage) override; void* aData, bool aForImage) override;
virtual void SetLinkHandlingEnabled(bool aValue) override {
mLinksEnabled = aValue;
}
virtual bool LinkHandlingEnabled() override {
return mLinksEnabled;
}
/** /**
* Access HTTP header data (this may also get set from other sources, like * Access HTTP header data (this may also get set from other sources, like
* HTML META tags). * HTML META tags).
@ -1223,6 +1231,10 @@ public:
// Recorded time of change to 'loading' state. // Recorded time of change to 'loading' state.
mozilla::TimeStamp mLoadingTimeStamp; mozilla::TimeStamp mLoadingTimeStamp;
// False if we've disabled link handling for elements inside this document,
// true otherwise.
bool mLinksEnabled : 1;
// True if the document has been detached from its content viewer. // True if the document has been detached from its content viewer.
bool mIsGoingAway:1; bool mIsGoingAway:1;
// True if the document is being destroyed. // True if the document is being destroyed.

View file

@ -767,6 +767,9 @@ public:
mSandboxFlags = sandboxFlags; mSandboxFlags = sandboxFlags;
} }
virtual void SetLinkHandlingEnabled(bool aValue) = 0;
virtual bool LinkHandlingEnabled() = 0;
/** /**
* Access HTTP header data (this may also get set from other * Access HTTP header data (this may also get set from other
* sources, like HTML META tags). * sources, like HTML META tags).

View file

@ -199,16 +199,9 @@ HTMLAnchorElement::IsHTMLFocusable(bool aWithMouse,
} }
// cannot focus links if there is no link handler // cannot focus links if there is no link handler
nsIDocument* doc = GetComposedDoc(); if (!OwnerDoc()->LinkHandlingEnabled()) {
if (doc) { *aIsFocusable = false;
nsIPresShell* presShell = doc->GetShell(); return false;
if (presShell) {
nsPresContext* presContext = presShell->GetPresContext();
if (presContext && !presContext->GetLinkHandler()) {
*aIsFocusable = false;
return false;
}
}
} }
// Links that are in an editable region should never be focusable, even if // Links that are in an editable region should never be focusable, even if

View file

@ -581,17 +581,16 @@ nsGenericHTMLElement::FindAncestorForm(HTMLFormElement* aCurrentForm)
} }
bool bool
nsGenericHTMLElement::CheckHandleEventForAnchorsPreconditions( nsGenericHTMLElement::CheckHandleEventForAnchorsPreconditions(EventChainVisitor& aVisitor)
EventChainVisitor& aVisitor)
{ {
NS_PRECONDITION(nsCOMPtr<Link>(do_QueryObject(this)), NS_PRECONDITION(nsCOMPtr<Link>(do_QueryObject(this)),
"should be called only when |this| implements |Link|"); "should be called only when |this| implements |Link|");
if (!aVisitor.mPresContext) { if (!aVisitor.mPresContext) {
// We need a pres context to do link stuff. Some events (e.g. mutation // When not in the composed document DOM, only <a> should navigate away per
// events) don't have one. // the exception in https://html.spec.whatwg.org/#cannot-navigate
// XXX: ideally, shouldn't we be able to do what we need without one? // Thanks, Google, for another ugly one. :|
return false; return IsInComposedDoc() || IsHTMLElement(nsGkAtoms::a);
} }
//Need to check if we hit an imagemap area and if so see if we're handling //Need to check if we hit an imagemap area and if so see if we're handling

View file

@ -165,11 +165,10 @@ HTMLEditor::~HTMLEditor()
// free any default style propItems // free any default style propItems
RemoveAllDefaultProperties(); RemoveAllDefaultProperties();
if (mLinkHandler && IsInitialized()) { if (mDisabledLinkHandling) {
nsCOMPtr<nsIPresShell> ps = GetPresShell(); nsCOMPtr<nsIDocument> doc = GetDocument();
if (doc) {
if (ps && ps->GetPresContext()) { doc->SetLinkHandlingEnabled(mOldLinkHandlingEnabled);
ps->GetPresContext()->SetLinkHandler(mLinkHandler);
} }
} }
@ -290,13 +289,14 @@ HTMLEditor::Init(nsIDOMDocument* aDoc,
mCSSEditUtils = MakeUnique<CSSEditUtils>(this); mCSSEditUtils = MakeUnique<CSSEditUtils>(this);
// disable links // disable links
nsCOMPtr<nsIPresShell> presShell = GetPresShell(); nsCOMPtr<nsIDocument> doc = GetDocument();
NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); if (NS_WARN_IF(!doc)) {
nsPresContext *context = presShell->GetPresContext(); return NS_ERROR_FAILURE;
NS_ENSURE_TRUE(context, NS_ERROR_NULL_POINTER); }
if (!IsPlaintextEditor() && !IsInteractionAllowed()) { if (!IsPlaintextEditor() && !IsInteractionAllowed()) {
mLinkHandler = context->GetLinkHandler(); mDisabledLinkHandling = true;
context->SetLinkHandler(nullptr); mOldLinkHandlingEnabled = doc->LinkHandlingEnabled();
doc->SetLinkHandlingEnabled(false);
} }
// init the type-in state // init the type-in state

View file

@ -41,7 +41,6 @@ class nsDocumentFragment;
class nsIDOMKeyEvent; class nsIDOMKeyEvent;
class nsITransferable; class nsITransferable;
class nsIClipboard; class nsIClipboard;
class nsILinkHandler;
class nsTableWrapperFrame; class nsTableWrapperFrame;
class nsIDOMRange; class nsIDOMRange;
class nsRange; class nsRange;
@ -1079,7 +1078,8 @@ protected:
void AddMouseClickListener(nsIDOMElement* aElement); void AddMouseClickListener(nsIDOMElement* aElement);
void RemoveMouseClickListener(nsIDOMElement* aElement); void RemoveMouseClickListener(nsIDOMElement* aElement);
nsCOMPtr<nsILinkHandler> mLinkHandler; bool mDisabledLinkHandling = false;
bool mOldLinkHandlingEnabled = false;
public: public:
friend class HTMLEditorEventListener; friend class HTMLEditorEventListener;

View file

@ -910,12 +910,7 @@ nsDocumentViewer::InitInternal(nsIWidget* aParentWidget,
nsCOMPtr<nsIInterfaceRequestor> requestor(mContainer); nsCOMPtr<nsIInterfaceRequestor> requestor(mContainer);
if (requestor) { if (requestor) {
if (mPresContext) { if (mPresContext) {
nsCOMPtr<nsILinkHandler> linkHandler;
requestor->GetInterface(NS_GET_IID(nsILinkHandler),
getter_AddRefs(linkHandler));
mPresContext->SetContainer(mContainer); mPresContext->SetContainer(mContainer);
mPresContext->SetLinkHandler(linkHandler);
} }
// Set script-context-owner in the document // Set script-context-owner in the document
@ -1421,8 +1416,6 @@ AttachContainerRecurse(nsIDocShell* aShell)
viewer->GetPresContext(getter_AddRefs(pc)); viewer->GetPresContext(getter_AddRefs(pc));
if (pc) { if (pc) {
pc->SetContainer(static_cast<nsDocShell*>(aShell)); pc->SetContainer(static_cast<nsDocShell*>(aShell));
nsCOMPtr<nsILinkHandler> handler = do_QueryInterface(aShell);
pc->SetLinkHandler(handler);
} }
nsCOMPtr<nsIPresShell> presShell; nsCOMPtr<nsIPresShell> presShell;
viewer->GetPresShell(getter_AddRefs(presShell)); viewer->GetPresShell(getter_AddRefs(presShell));
@ -2146,12 +2139,6 @@ nsDocumentViewer::Show(void)
return rv; return rv;
if (mPresContext && base_win) { if (mPresContext && base_win) {
nsCOMPtr<nsILinkHandler> linkHandler(do_GetInterface(base_win));
if (linkHandler) {
mPresContext->SetLinkHandler(linkHandler);
}
mPresContext->SetContainer(mContainer); mPresContext->SetContainer(mContainer);
} }

View file

@ -1504,7 +1504,6 @@ nsPresContext::GetDocShell() const
nsPresContext::Detach() nsPresContext::Detach()
{ {
SetContainer(nullptr); SetContainer(nullptr);
SetLinkHandler(nullptr);
if (mShell) { if (mShell) {
mShell->CancelInvalidatePresShellIfHidden(); mShell->CancelInvalidatePresShellIfHidden();
} }

View file

@ -457,13 +457,9 @@ public:
nsIDocShell* GetDocShell() const; nsIDocShell* GetDocShell() const;
// XXX this are going to be replaced with set/get container
void SetLinkHandler(nsILinkHandler* aHandler) { mLinkHandler = aHandler; }
nsILinkHandler* GetLinkHandler() { return mLinkHandler; }
/** /**
* Detach this pres context - i.e. cancel relevant timers, * Detach this pres context - i.e. cancel relevant timers,
* SetLinkHandler(null), SetContainer(null) etc. * SetContainer(null) etc.
* Only to be used by the DocumentViewer. * Only to be used by the DocumentViewer.
*/ */
virtual void Detach(); virtual void Detach();
@ -1245,10 +1241,6 @@ protected:
nsIAtom* MOZ_UNSAFE_REF("always a static atom") mMedium; // initialized by subclass ctors nsIAtom* MOZ_UNSAFE_REF("always a static atom") mMedium; // initialized by subclass ctors
nsCOMPtr<nsIAtom> mMediaEmulated; nsCOMPtr<nsIAtom> mMediaEmulated;
// This pointer is nulled out through SetLinkHandler() in the destructors of
// the classes which set it. (using SetLinkHandler() again).
nsILinkHandler* MOZ_NON_OWNING_REF mLinkHandler;
// Formerly mLangGroup; moving from charset-oriented langGroup to // Formerly mLangGroup; moving from charset-oriented langGroup to
// maintaining actual language settings everywhere (see bug 524107). // maintaining actual language settings everywhere (see bug 524107).
// This may in fact hold a langGroup such as x-western rather than // This may in fact hold a langGroup such as x-western rather than

View file

@ -1309,9 +1309,6 @@ PresShell::Destroy()
// to us. To avoid the pres context having a dangling reference, set its // to us. To avoid the pres context having a dangling reference, set its
// pres shell to nullptr // pres shell to nullptr
mPresContext->DetachShell(); mPresContext->DetachShell();
// Clear the link handler (weak reference) as well
mPresContext->SetLinkHandler(nullptr);
} }
mHaveShutDown = true; mHaveShutDown = true;

View file

@ -2046,7 +2046,7 @@ nsImageFrame::HandleEvent(nsPresContext* aPresContext,
*aEventStatus = nsEventStatus_eConsumeDoDefault; *aEventStatus = nsEventStatus_eConsumeDoDefault;
clicked = true; clicked = true;
} }
nsContentUtils::TriggerLink(anchorNode, aPresContext, uri, target, nsContentUtils::TriggerLink(anchorNode, uri, target,
clicked, true, true); clicked, true, true);
} }
} }