-
Notifications
You must be signed in to change notification settings - Fork 298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Proposal] Add EventTarget.getEventListeners() #412
Comments
On balance I think this is probably worth it, but we should note the solid objections to this on a few grounds:
I think both of these objections are solid, but as I said, on balance I think they are overcome. The fact that people are hacking around the current design so much, and that this abstraction appears in other platforms (e.g. jQuery's system, or Node.js's EventEmitter) implies to me it's probably better to expose. It's probable that this subject has been discussed before, maybe pre-GitHub; I wonder if we could find those threads to see how the discussion went then and if anything has changed.
This is weird, but I see the issue; currently the actual listener function is not reified, and it would be unclear what removeEventListener("event", reifiedListener) does (does the content attribute or IDL attribute stick around?).
I think this is a misinterpretation of the situation. Browsers don't have the magic ability to look at all event listeners; just like user code, they only have the ability to dispatch events. The actual event listener list is usually stored as a private variable in the EventTarget backing class, with only equivalents of addEventListener/dispatchEvent exposed to the rest of the codebase. You could always modify a browser to make that private variable public, but that's the same as what we're discussing here for the web. So there's no browser-only magic we're exposing here; this would be a purely new capability, not a piece of bedrock we're unearthing. |
All good points there, thanks for the thoughtful response @domenic! On another note, another possible design would be to have the same signature as |
Someone should probably research https://lists.w3.org/Archives/Public/www-dom/ as I recall this coming up quite a few times in the past and being dismissed each time. I don't recall much else unfortunately. |
This is what I could find: At first glance, it appears the pushback is entirely comprised of the theoretical purity arguments that @domenic mentioned would come up. No implementation difficulties or anything of the like. Tab mentioned an alternative in the last thread: Allowing an option for namespacing and removing events via namespaces, with both strings and symbols allowed. This is a good idea but does not solve all use cases. For example, a major use case is cloning an element with its events. In that case you legitimately want access to listeners you did not bind. |
I also found https://lists.w3.org/Archives/Public/public-webapi/2008Apr/thread.html#msg66 that isn't very conclusive either way, mostly folks not convinced by the use cases given at the time. (Although allowing iteration over them is somewhat novel I suppose.) (The identifier grouping idea is #208.) |
I don't think "theoretical purity" is a fair characterization of those arguments. They are practical arguments from real developers who want to be able to develop components without being afraid that any other piece of code on the page could interfere. The lack of ability to reason about program control flow is an actual issue that affects developers every day, not a theoretical one. |
For example, given the code for TacoButton here, you'd no longer be able to guarantee that all taco-buttons on the page properly forward enter/space keydowns to click events, or that they stop firing click events when disabled. Being unable to reason about your program in this way is a real burden, and it changes your story as a component author from "include my component and it'll work" to "include my component but make sure all your third-party scripts aren't doing some over-aggressive cleanup action or you'll see strange behavior". I still lean toward exposing the functionality, and then you'll just have to accept that you can no longer write encapsulated components in that way, but I think it's a real tradeoff between two different types of developer convenience, not one between theoretical purity and developer convenience. |
It is an important security property that nobody without a reference to my listener function can remove my listeners. If a use case is cloning a node with all listeners, perhaps this could be handled with a "copyListeners" API (taking event names as filters, eg) without the need to expose the listener references directly? What about adding a new "addEventListener" option that opts the listener in to being unprotected? That way the default would remain the same - secure - but users who wanted to waive a bit of security for the convenience of jQuery-like event manipulation could do so? |
I like the idea of namespacing events; this way, I can remove all my handlers later on without needing to keep their references (which is very helpful when binding functions). As far as I understand, when using a Symbol as a namespace one could create a private namespace which could not be intercepted by third parties (which might be important for developers of third-party code). Example:
Combined with lhjarbs idea of a |
To be clear, it is not a security property, because on the web our threat model is not meant to defend against malicious code running on the same page. It is purely a developer-reasoning-about-code property. |
Regardless of the characterization of these arguments, it's a false sense of security, since this is possible today by wrapping |
It's certainly not an intentional security property, but that does not mean it is not an emergent one. I can defend against wrapping addEventListener by caching it in first-run code. I definitely agree it should be done natively - i'm simply suggesting that it be opt in, not by default. |
If you're writing a library, you don't control whether your library is first-run code, so tough luck. If you're not writing a library, then what's the point of defending anything? You control exactly what runs. |
I can extract an untainted Is there a reason that an opt-in mechanism (at the |
One of the most basic HCI (or human psychology in general) principles is that when something is opt-in, few do it. Most of the time because they simply never considered it. Therefore, it renders the feature useless for libraries, and only helps in reducing coupling in one's own code. Another basic HCI principle is sensible defaults. Most listeners have no reason to be hidden, so the default should be non-private. Opt-out might work, like a |
Just my two cents here, but I disagree. As has been pointed out, once you can enumerate event listeners, you can implement something like Node's
In practice, I've found this means that, as a library author, if you vend event-emitting objects to your users whose events you also use to drive internal functionality, you need to separate the EventEmitter interface your users will depend on from the one your internal functionality depends on (otherwise, a EDIT: Sorry, @LeaVerou re-reading what you wrote, I too agree hijacking |
@markandrus The |
@LeaVerou a mechanism like
There are many different types of developers... I've heard bug reports from some who called this function expecting it to remove only "their" event listeners. |
Having overridden |
If you want to copy your event listeners, you can redispatch your events on the old node. That doesn't require destroying the currently existing abstraction.
|
@FremyCompany that fiddle doesn't seem to work in Chrome/Firefox/Safari. You'll also lose trustedness of the event that way and target/currentTarget and such. |
@annevk - it seems to just be missing a |
From a purely implementation point of view I don't think this would be a terrible hassle to implement. Blink internally uses the same EventListener machinery for some internal use cases, like IndexedDB index population and media controls and things. We'd just need some level of filtering or segregating for them. I'm not sure about window. Are there any times you have cross origin iframes and different domains get to stick event listeners on the same window through the window proxy or anything like that? As a recovering? wannabe? web developer I'm mildly anxious about the code that's out there assuming their event listeners, once registered, won't be messed with. On the other hand if you can get a reference to the EventTarget anyway you probably can already cause interesting headaches. |
IMHO the only reason to
For me, this statement has a lot of sense... So, |
I've heard it many times. "If it's in the standard we're supposed to use it because it's the way to go, right?" I think that's why everyone above is worried about it breaking abstractions. Calling this method should not seen an obvious choice for a novice developer so that they put in some effort into managing their listeners instead of looping through the list and removing each one when they clean up. How about making it a "static" method of the DOM node, so that it looks like Object.keys? I'm not sure if hurting discoverability of this feature is really helpful, but noone brought it up before. Additionally, instead of taking this listing function as a solution for cloning nodes, it'd be cleaner to just have a method for cloning with events. OK. Final thought. |
Another use case I came across yesterday: I was in a talk about an accessibility tool that automatically adds keyboard shortcuts to existing web apps. If they cannot read whether there are existing key* events on each command, they cannot avoid controls that already have keyboard shortcuts. (although this argues more for a declarative way to specify keyboard shortcuts than for this…) |
@LeaVerou - plus, having an event listener does not mean it supports keyboard shortcuts. |
Small thread hijack: I'd like to draw the attention of people in this thread to the related, but distinct, proposal in #208. Although it doesn't cover all use cases of this thread, it covers some of them, so the audience may be similar. If you work on a library or app that would use the proposal in #208, let us know over in that thread, as implementers are currently trying to judge developer interest. |
Sorry if this has been mentioned already, but re:security and encapsulation concerns: but developers can already intercept let addEventListener = EventTarget.prototype.addEventListener;
let removeEventListener = EventTarget.prototype.removeEventListener;
EventTarget.prototype.addEventListener = function(type, callback, options) {
/* store args and then… */
addEventListener.call(this, type, callback, options);
};
EventTarget.prototype.removeEventListener = function(type, callback, options) {
/* remove from stored args and then… */
removeEventListener.call(this, type, callback, options);
}; The problem is just that a) it's flimsy, since listeners may have been attached before the code was run and b) it's suboptimal to be overwriting built-ins like that. |
yes, it was mentioned, but on the other side, developers can secure nothing will ever interfere with their intent if your code runs earlier (see polyfills) const {bind: b, call: c} = Function;
const {addEventListener} = EventTarget.prototype;
const addListener = b.call(c, addEventListener);
// any time later on ...
addListener(document.body, 'click', () => { console.log('safe'); }); We do this already in explicit circumstances to avoid evil scripts intercepting, removing, or mutating our listeners behaviors, but extensions can guarantee (in some browser) their code executes before anything else. Again, I think this feature would be a good one for Web Extensions, at least to pave the path, but I also think user land can have a centralized library authors can use to retrieve all listeners, if added/remove through such centralized reference. That would be also fine, like: EventTarget.addPublicEventListener(element, listener, options); Now, that would be cool ... changing a very long time expected behavior where many based their libraries around looks like an unnecessary, imho undesired, breaking change I hope won't land as amend of the current methods. I would be more than OK making it land as new method, but I guess that will also fall into the "opt-in" is less effective than "opt-out", but then again, if this lands as default behavior, we (where I work) need to be sure we patch that default behavior and exclude any of our listeners from being exposed in the wild to undesired evil scripts. |
Any data in the same process can be intercepted with Spectre; there is no security concern. If this is not something you're familiar with then it might be best to read up on that instead of sidetracking this issue. Early running scripts are not protection against that; there is simply no ability to keep secrets within the same process. The issue is only about encapsulation, not security. |
I’m not sidetracking the issue, I’m talking about real world concern our product, and every similar product, has around leaked listeners. I wasn’t aware Spectre can be exploited through JavaScript in a browser page and leak every part of the code, but if that’s the only concern around security then fine, let’s talk encapsulation, no reason to be aggressive though. |
I don't think an opt-in new method would work (if the listener-adding code can follow the same "contract" as the listener-reading code they can just use a helper, most use cases for |
@LeaVerou like I've said, I love the stealth (or closed) idea, but I respectfully disagree on this part:
and the reason is the opposite of:
with NodeJS you "own" the server that runs the code (or the lambda, or whatever) and you know for sure (™️) what script gets in and what not ... you don't have this open world where everyone can do whatever they wont because YOLO or because evil scripts (ads industry is full of these) you cannot control, because you ship one bit of a front end you might never control, as library author, while code used in a server has a different story/use-case for all listeners. That being said, none of my libraries welcome a method that exposes internal listeners, mostly because I use extensively the event listener interface with hadnleEvent which is about attaching a whole state handler to an element, not just a callback, and that's proven to be extremely effective and handy. lit-element, to name others, use this pattern too ... and so do tons of other frameworks. I never want my attached states/behaviors to ever leak to undesired scripts I didn't authorize, so I hope instead of telling me I am sidetracking the issue somebody would understand what is the issue I am talking about. Yet, if this changes, which is a breaking expectations change to me, I need to add that third option everywhere, and say goodbye to legacy compatibility because once you add an object, old browsers take that as a |
Let me make sure I am understanding the current situation:
What about an alternative "enable encapsulation" options like the ones browsers typically use to enable breaking features like |
Also @WebReflection note in Node.js we already shipped an API that lets you get event listeners to our native |
if this is about special CSP flag, then maybe it makes sense ... but I don't see CSP less cumbersome than opt-in, to be honest, yet it's close to my idea of "let's bring this to Web Extensions and see how it goes", so to me that's an option.
as mentioned, Node.js is a different use case. It doesn't get evil scripts out of the box and it doesn't suffer by design XSS.
If it's a new method I am not sure how that would violate the specs but again, in node you don't have DOM listeners to deal with, I am not fully sure why Node.js is so relevant in this breaking change for the Web (or the current state).
Yes, all points reflect my understanding too. |
alternative how about an explicit API that allows to retrieve all listeners from X time on? element.addEventListener('click', myOwnBusiness);
const listeners = element.observeListeners();
[...listeners]; // []
element.addEventListener('click', publicAffair);
[...listeners]; // [publicAffair] or dare I say, something to implement through a mutation observer when listeners are added/removed? still with a possibility to opt-out from certain listeners (as in filterOut property) ... would this work better? (all ideas rushed just to have feedback) |
CSP can also be opt-out it doesn't have to be opt in (right?).
I think anything that doesn't intercept listeners that were there before my code is run is significantly diminished in value. |
that breaks everything by default, if you want to enable this by default and we should ask everyone to add a CSP rule in every site that used some library that most of them didn't even know was relying on non-leaking listeners. At that point I'd personally think {closed} or {stealth} are more reasonable as JS dependencies are easier to update /patch than HTML layouts or server side headers ... yet, this is a breaking change. |
@benjamingr it is absolutely critical for encapsulation that listeners added before your code runs are not interceptable, for reasons discussed at length upthread. |
I guess I’m echoing @ljharb and yet another reason this feature scares me is custom elements suddenly unable to safely register their listeners in the constructor to properly work because "cowboy.js" might suddenly remove all listeners from a node … encapsulation and ownership should be preserved by default as its always been to date. The more I think about this, the more I think opt-in is the best way to move forward, as @ljharb suggested ages ago. |
This is simply not true. In fact, I also mentioned my "hijack library" (see It makes it facile to retrieve and remove (that is the primary scope) event listeners attached by anyone to anything. This very ability / behavior, I think, makes most points above by everyone about expectations around some sort of encapsulation, as well as security moot. Any page relying on third party scripts could load something similar (either intentionally or "inadvertently", via hijacking, sandbox vulnerabilities, or any other method) to make this possible for such (or any other) third party scripts (or, as my intention was, for the "JS app" belonging to / being the "page" itself). Would it be better if this has been implemented natively with some configurable security? Sure, I think. |
@strongholdmedia that's only the case if your hijacking happens before the code meant to be encapsulated; being "first-run" code is the only way to have any sort of guarantees in JS. If event listeners are aded before your hijacking code runs, then it must remain impossible for you to observe them. It's totally fine (and impossible to prevent anyways) if later-running code is unable to prevent observation of its event listeners. |
@strongholdmedia what @ljharb said, but I also do that for a living, I wasn’t here commenting my concerns out of the blue, I use secured code and run it before others daily.
|
wanna echo @ljharb cause his last comment hits on an extremely important general principle of how js "virtualization" works. that other code running first can do this is (kinda confusingly) a key part of the foundation of encapsulation rather than evidence that it's unavailable. but the observability river only runs in one direction. reversing the river on untold amounts of existing code is not okay, but a new method or opt-in flag does not have that problem. |
Instead of returning a function that might potentially include secrets in its string, could some representation of that function be returned instead? |
Just another point I thought about: Even with const origGetEventListeners = EventTarget.prototype.getEventListeners;
Object.defineProperty(EventTarget.prototype, 'getEventListeners', {
value: function myGetEventListeners(eventType) {
return origGetEventListeners.apply(this, arguments).filter(handler => myCustomFilterLogic(handler));
}
}); So encapsulation is still possible just not the default with Lea's proposal. |
@benjamingr youd probably also have to prevent/hijack creation of new iframes and opened windows, all of which would have a fresh copy of getAllEventListeners. The scope of the problem changes pretty drastically if this method is available. |
It's probably(?) fine to make |
Do any other web methods do that? That seems like it’d be a pretty big inconsistency to introduce. |
It would be ... for code that’s yet to be written. |
@benjamingr I think every “just pollute native prototype” argument could be used, and has been used, already to indeed solve the original issue. For the same reason that doesn’t work one way, it doesn’t any other way and it will also defeat the intended goal of this proposal. Now imagine every library trusting secured events doing that trick … |
Assuming there is a link in the document, what is the value of
|
I don't think the
|
Originally posted at WICG: https://discourse.wicg.io/t/proposal-eventtarget-prototype-geteventlisteners/2015
Purpose: This would provide a way to get all event listeners added to an element via
addEventListener
.Signature:
element.getEventListeners([type])
Returns: Array of objects. Each object contains properties for
type
,callback
,options
for the arguments that registered the corresponding event listener. Events registered via inline event handlers are not included.Use cases: This would enable removing events based on arbitrary criteria, instead of requiring a reference to the callback, which causes unnecessary couplings. Typically libraries deal with this by providing their own abstractions for adding events that track the listeners manually. However, this is fragile, as it means listeners not registered via the library cannot be retrieved or removed. Some libraries deal with this by hijacking
addEventListener
to keep track of listeners, but this is very intrusive for a library and it doesn't help with any listeners registered before the library was included. Browsers already keep track of event listeners, so it should be relatively easy to expose them, and is on par with the Extensible Web Manifesto principle of exposing browser "magic" via JS APIs.The text was updated successfully, but these errors were encountered: