From 1c600e44b62bc4a65bef5706a59ed3271bb9beda Mon Sep 17 00:00:00 2001 From: Olli Pettay Date: Fri, 16 Jul 2021 12:04:37 +0000 Subject: [PATCH] Bug 1720574 - Add a way to disable event listeners, r=edgar,Honza The old test for the service is reused here. Added some basic tests there. Differential Revision: https://phabricator.services.mozilla.com/D119921 --- dom/events/EventListenerManager.cpp | 78 +++++++++++++++++++++++++- dom/events/EventListenerManager.h | 22 +++++++- dom/events/EventListenerService.cpp | 29 ++++++++-- dom/events/EventListenerService.h | 8 ++- dom/events/nsIEventListenerService.idl | 6 ++ dom/events/test/test_bug448602.html | 78 +++++++++++++++++++++++++- 6 files changed, 209 insertions(+), 12 deletions(-) diff --git a/dom/events/EventListenerManager.cpp b/dom/events/EventListenerManager.cpp index e65a86622107..dbccad41baa9 100644 --- a/dom/events/EventListenerManager.cpp +++ b/dom/events/EventListenerManager.cpp @@ -665,6 +665,12 @@ bool EventListenerManager::ListenerCanHandle(const Listener* aListener, if (aListener->mListenerType == Listener::eNoListener) { return false; } + + // The listener has been disabled, for example by devtools. + if (!aListener->mEnabled) { + return false; + } + // This is slightly different from EVENT_TYPE_EQUALS in that it returns // true even when aEvent->mMessage == eUnidentifiedEvent and // aListener=>mEventMessage != eUnidentifiedEvent as long as the atoms are @@ -1589,13 +1595,81 @@ nsresult EventListenerManager::GetListenerInfo( } RefPtr info = new EventListenerInfo( - eventType, callback, callbackGlobal, listener.mFlags.mCapture, - listener.mFlags.mAllowUntrustedEvents, listener.mFlags.mInSystemGroup); + this, eventType, callback, callbackGlobal, listener.mFlags.mCapture, + listener.mFlags.mAllowUntrustedEvents, listener.mFlags.mInSystemGroup, + listener.mListenerIsHandler); aList.AppendElement(info.forget()); } return NS_OK; } +EventListenerManager::Listener* EventListenerManager::GetListenerFor( + nsAString& aType, JSObject* aListener, bool aCapturing, + bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler) { + NS_ENSURE_TRUE(aListener, nullptr); + + for (Listener& listener : mListeners.ForwardRange()) { + if ((aType.IsVoid() && !listener.mAllEvents) || + !Substring(nsDependentAtomString(listener.mTypeAtom), 2) + .Equals(aType) || + listener.mListenerType == Listener::eNoListener) { + continue; + } + + if (listener.mFlags.mCapture != aCapturing || + listener.mFlags.mAllowUntrustedEvents != aAllowsUntrusted || + listener.mFlags.mInSystemGroup != aInSystemEventGroup) { + continue; + } + + if (aIsHandler) { + if (JSEventHandler* handler = listener.GetJSEventHandler()) { + if (handler->GetTypedEventHandler().HasEventHandler()) { + if (handler->GetTypedEventHandler().Ptr()->CallableOrNull() == + aListener) { + return &listener; + } + } + } + } else if (listener.mListenerType == Listener::eWebIDLListener && + listener.mListener.GetWebIDLCallback()->CallbackOrNull() == + aListener) { + return &listener; + } + } + return nullptr; +} + +nsresult EventListenerManager::IsListenerEnabled( + nsAString& aType, JSObject* aListener, bool aCapturing, + bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler, + bool* aEnabled) { + Listener* listener = + GetListenerFor(aType, aListener, aCapturing, aAllowsUntrusted, + aInSystemEventGroup, aIsHandler); + NS_ENSURE_TRUE(listener, NS_ERROR_NOT_AVAILABLE); + *aEnabled = listener->mEnabled; + return NS_OK; +} + +nsresult EventListenerManager::SetListenerEnabled( + nsAString& aType, JSObject* aListener, bool aCapturing, + bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler, + bool aEnabled) { + Listener* listener = + GetListenerFor(aType, aListener, aCapturing, aAllowsUntrusted, + aInSystemEventGroup, aIsHandler); + NS_ENSURE_TRUE(listener, NS_ERROR_NOT_AVAILABLE); + listener->mEnabled = aEnabled; + if (aEnabled) { + // We may have enabled some listener, clear the cache for which events + // we don't have listeners. + mNoListenerForEvent = eVoidEvent; + mNoListenerForEventAtom = nullptr; + } + return NS_OK; +} + bool EventListenerManager::HasUnloadListeners() { uint32_t count = mListeners.Length(); for (uint32_t i = 0; i < count; ++i) { diff --git a/dom/events/EventListenerManager.h b/dom/events/EventListenerManager.h index 14712381e477..1fd3d38ab06f 100644 --- a/dom/events/EventListenerManager.h +++ b/dom/events/EventListenerManager.h @@ -221,6 +221,7 @@ class EventListenerManager final : public EventListenerManagerBase { bool mHandlerIsString : 1; bool mAllEvents : 1; bool mIsChrome : 1; + bool mEnabled : 1; EventListenerFlags mFlags; @@ -236,7 +237,8 @@ class EventListenerManager final : public EventListenerManagerBase { mListenerIsHandler(false), mHandlerIsString(false), mAllEvents(false), - mIsChrome(false) {} + mIsChrome(false), + mEnabled(true) {} Listener(Listener&& aOther) : mSignalFollower(std::move(aOther.mSignalFollower)), @@ -247,13 +249,15 @@ class EventListenerManager final : public EventListenerManagerBase { mListenerIsHandler(aOther.mListenerIsHandler), mHandlerIsString(aOther.mHandlerIsString), mAllEvents(aOther.mAllEvents), - mIsChrome(aOther.mIsChrome) { + mIsChrome(aOther.mIsChrome), + mEnabled(aOther.mEnabled) { aOther.mEventMessage = eVoidEvent; aOther.mListenerType = eNoListener; aOther.mListenerIsHandler = false; aOther.mHandlerIsString = false; aOther.mAllEvents = false; aOther.mIsChrome = false; + aOther.mEnabled = true; } ~Listener() { @@ -444,6 +448,16 @@ class EventListenerManager final : public EventListenerManagerBase { */ nsresult GetListenerInfo(nsTArray>& aList); + nsresult IsListenerEnabled(nsAString& aType, JSObject* aListener, + bool aCapturing, bool aAllowsUntrusted, + bool aInSystemEventGroup, bool aIsHandler, + bool* aEnabled); + + nsresult SetListenerEnabled(nsAString& aType, JSObject* aListener, + bool aCapturing, bool aAllowsUntrusted, + bool aInSystemEventGroup, bool aIsHandler, + bool aEnabled); + uint32_t GetIdentifierForEvent(nsAtom* aEvent); /** @@ -574,6 +588,10 @@ class EventListenerManager final : public EventListenerManagerBase { bool HasListenersForInternal(nsAtom* aEventNameWithOn, bool aIgnoreSystemGroup) const; + Listener* GetListenerFor(nsAString& aType, JSObject* aListener, + bool aCapturing, bool aAllowsUntrusted, + bool aInSystemEventGroup, bool aIsHandler); + public: /** * Set the "inline" event listener for aEventName to aHandler. If diff --git a/dom/events/EventListenerService.cpp b/dom/events/EventListenerService.cpp index 349148c5475f..a61c773f4d11 100644 --- a/dom/events/EventListenerService.cpp +++ b/dom/events/EventListenerService.cpp @@ -75,15 +75,18 @@ EventListenerChange::GetCountOfEventListenerChangesAffectingAccessibility( ******************************************************************************/ EventListenerInfo::EventListenerInfo( - const nsAString& aType, JS::Handle aScriptedListener, + EventListenerManager* aListenerManager, const nsAString& aType, + JS::Handle aScriptedListener, JS::Handle aScriptedListenerGlobal, bool aCapturing, - bool aAllowsUntrusted, bool aInSystemEventGroup) - : mType(aType), + bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler) + : mListenerManager(aListenerManager), + mType(aType), mScriptedListener(aScriptedListener), mScriptedListenerGlobal(aScriptedListenerGlobal), mCapturing(aCapturing), mAllowsUntrusted(aAllowsUntrusted), - mInSystemEventGroup(aInSystemEventGroup) { + mInSystemEventGroup(aInSystemEventGroup), + mIsHandler(aIsHandler) { if (aScriptedListener) { MOZ_ASSERT(JS_IsGlobalObject(aScriptedListenerGlobal)); js::AssertSameCompartment(aScriptedListener, aScriptedListenerGlobal); @@ -97,9 +100,11 @@ EventListenerInfo::~EventListenerInfo() { DropJSObjects(this); } NS_IMPL_CYCLE_COLLECTION_CLASS(EventListenerInfo) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EventListenerInfo) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListenerManager) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EventListenerInfo) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mListenerManager) tmp->mScriptedListener = nullptr; tmp->mScriptedListenerGlobal = nullptr; NS_IMPL_CYCLE_COLLECTION_UNLINK_END @@ -141,6 +146,22 @@ EventListenerInfo::GetInSystemEventGroup(bool* aInSystemEventGroup) { return NS_OK; } +NS_IMETHODIMP +EventListenerInfo::GetEnabled(bool* aEnabled) { + NS_ENSURE_STATE(mListenerManager); + return mListenerManager->IsListenerEnabled( + mType, mScriptedListener, mCapturing, mAllowsUntrusted, + mInSystemEventGroup, mIsHandler, aEnabled); +} + +NS_IMETHODIMP +EventListenerInfo::SetEnabled(bool aEnabled) { + NS_ENSURE_STATE(mListenerManager); + return mListenerManager->SetListenerEnabled( + mType, mScriptedListener, mCapturing, mAllowsUntrusted, + mInSystemEventGroup, mIsHandler, aEnabled); +} + NS_IMETHODIMP EventListenerInfo::GetListenerObject(JSContext* aCx, JS::MutableHandle aObject) { diff --git a/dom/events/EventListenerService.h b/dom/events/EventListenerService.h index 4d5cbebccac0..8b0b95f13003 100644 --- a/dom/events/EventListenerService.h +++ b/dom/events/EventListenerService.h @@ -10,6 +10,7 @@ #include "jsapi.h" #include "mozilla/Attributes.h" #include "nsCycleCollectionParticipant.h" +#include "EventListenerManager.h" #include "nsIEventListenerService.h" #include "nsString.h" #include "nsTObserverArray.h" @@ -43,11 +44,12 @@ class EventListenerChange final : public nsIEventListenerChange { class EventListenerInfo final : public nsIEventListenerInfo { public: - EventListenerInfo(const nsAString& aType, + EventListenerInfo(EventListenerManager* aListenerManager, + const nsAString& aType, JS::Handle aScriptedListener, JS::Handle aScriptedListenerGlobal, bool aCapturing, bool aAllowsUntrusted, - bool aInSystemEventGroup); + bool aInSystemEventGroup, bool aIsHandler); NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(EventListenerInfo) @@ -59,6 +61,7 @@ class EventListenerInfo final : public nsIEventListenerInfo { bool GetJSVal(JSContext* aCx, Maybe& aAr, JS::MutableHandle aJSVal); + RefPtr mListenerManager; nsString mType; JS::Heap mScriptedListener; // May be null. // mScriptedListener may be a cross-compartment wrapper so we cannot use it @@ -69,6 +72,7 @@ class EventListenerInfo final : public nsIEventListenerInfo { bool mCapturing; bool mAllowsUntrusted; bool mInSystemEventGroup; + bool mIsHandler; }; class EventListenerService final : public nsIEventListenerService { diff --git a/dom/events/nsIEventListenerService.idl b/dom/events/nsIEventListenerService.idl index 3e7e21c28cf4..333f45175a91 100644 --- a/dom/events/nsIEventListenerService.idl +++ b/dom/events/nsIEventListenerService.idl @@ -44,6 +44,12 @@ interface nsIEventListenerInfo : nsISupports readonly attribute boolean allowsUntrusted; readonly attribute boolean inSystemEventGroup; + /** + * Changing the enabled state works only with listeners implemented in + * JS. An error is thrown for native listeners. + */ + attribute boolean enabled; + /** * The underlying JS object of the event listener, if this listener * has one. Null otherwise. diff --git a/dom/events/test/test_bug448602.html b/dom/events/test/test_bug448602.html index c4c8b42ce2db..5c076894277f 100644 --- a/dom/events/test/test_bug448602.html +++ b/dom/events/test/test_bug448602.html @@ -21,6 +21,16 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=448602 var els, root, l2, l3; +var handlerCalled = false; +var capturingListenerCalled = false; +var bubblingListenerCalled = false; + +function clearListenerStates() { + handlerCalled = false; + capturingListenerCalled = false; + bubblingListenerCalled = false; +} + function runTests() { els = SpecialPowers.Cc["@mozilla.org/eventlistenerservice;1"] .getService(SpecialPowers.Ci.nsIEventListenerService); @@ -30,7 +40,7 @@ function runTests() { var infos = els.getListenerInfoFor(root); is(infos.length, 0, "Element shouldn't have listeners (1)"); - var listenerSource = 'alert(event);'; + var listenerSource = 'handlerCalled = true;'; root.setAttribute("onclick", listenerSource); infos = els.getListenerInfoFor(root); is(infos.length, 1, "Element should have listeners (1)"); @@ -42,7 +52,72 @@ function runTests() { is(SpecialPowers.unwrap(infos[0].listenerObject), root.onclick, "Should have the right listener object (1)"); + // Test disabling and enabling the listener. + ok(!handlerCalled); + root.click(); + ok(handlerCalled); + + clearListenerStates() + infos[0].enabled = false; + root.click(); + ok(!handlerCalled); + + clearListenerStates() + infos[0].enabled = true; + root.click(); + ok(handlerCalled); + clearListenerStates(); + + function capturingListener() { + capturingListenerCalled = true; + } + function bubblingListener() { + bubblingListenerCalled = true; + } + root.addEventListener("click", capturingListener, true); + root.addEventListener("click", bubblingListener); + root.addEventListener("fooevent", capturingListener, true); + root.addEventListener("fooevent", bubblingListener); + infos = els.getListenerInfoFor(root); + // Use a child node to dispatch events so that both capturing and bubbling + // listeners get called. + l2 = document.getElementById("testlevel2"); + l2.click(); + ok(handlerCalled); + ok(capturingListenerCalled); + ok(bubblingListenerCalled); + clearListenerStates(); + + infos[0].enabled = false; + l2.click(); + ok(!handlerCalled); + ok(capturingListenerCalled); + ok(bubblingListenerCalled); + clearListenerStates(); + infos[0].enabled = true; + + infos[1].enabled = false; + l2.click(); + ok(handlerCalled); + ok(!capturingListenerCalled); + ok(bubblingListenerCalled); + clearListenerStates(); + infos[1].enabled = true; + + infos[2].enabled = false; + l2.click(); + ok(handlerCalled); + ok(capturingListenerCalled); + ok(!bubblingListenerCalled); + clearListenerStates(); + infos[2].enabled = true; + + root.removeEventListener("click", capturingListener, true); + root.removeEventListener("click", bubblingListener); + root.removeEventListener("fooevent", capturingListener, true); + root.removeEventListener("fooevent", bubblingListener); root.removeAttribute("onclick"); + root.setAttribute("onclick", "...invalid script..."); SimpleTest.expectUncaughtException(true); infos = els.getListenerInfoFor(root); @@ -91,7 +166,6 @@ function runTests() { "Should have the right listener object (4)"); // Event target chain tests - l2 = document.getElementById("testlevel2"); l3 = document.getElementById("testlevel3"); var textnode = l3.firstChild; var chain = els.getEventTargetChainFor(textnode, true);