How to find out what event listener types attached

2019-05-02 10:57发布

问题:

I'm posting this question here, since I can't get it posted on the official Chromium extension forums (or there's a terrific delay until it's being moderated). I have to check in Chromium extension whether there's a listener of a specific event type attached to an arbitrary HTML element. In Firefox I could use the following service to get this information:

var listenerService = Components.classes["@mozilla.org/eventlistenerservice;1"]
            .getService(Components.interfaces.nsIEventListenerService);
var infos = listenerService.getListenerInfoFor(element, {});
var types = [];
for ( var i = 0; i < infos.length; ++i) {
  var info = infos[i].QueryInterface(Components.interfaces.nsIEventListenerInfo);
  types.push(info.type);
}

As I see in Chromium there's no similar API. Therefore I've tried the following technique (which was suggested here):

I've created script events_spy.js:

(function(original) {
  Element.prototype.addEventListener = function(type, listener, useCapture) {
    if (typeof (this._handlerTypes) == 'undefined') {
      this._handlerTypes = {};
    }
    this._handlerTypes[type] = true;
    return original.apply(this, arguments);
  }
})(Element.prototype.addEventListener);

(function(original) {
  Element.prototype.removeEventListener = function(type, listener,useCapture) {
    if (typeof (this._handlerTypes) != 'undefined') {
      delete this._handlerTypes[type];
    }
    return original.apply(this, arguments);
  }
})(Element.prototype.removeEventListener);

I declare this script in manifest.json as follows:

"content_scripts" : [{
  "matches" : [ "http://*/*", "https://*/*" ],
  "js" : [ "content/events_spy.js" ],
  "run_at" : "document_start",
  "all_frames" : true
},
...
]

Then, I test my extension on the following HTML page:

<!DOCTYPE html>
<html>
 <head>
 </head>
 <body>
  <a id="test" href="#">Click here</a>
  <script type="application/javascript">
       document.getElementById("test").addEventListener("click", function()
{ alert("clicked"); }, false);
  </script>
 </body>
</html>

Unfortunately, this doesn't work - I can't see that debugger stops inside of my custom addEventListener() function. What am I doing wrong?

Thanks!

EDIT: Final (dirty) solution, thanks to @kdzwinel

var injectedJS = "\
(function(original) { \
  Element.prototype.addEventListener = function(type, listener, useCapture) { \
    var attr = this.getAttribute('_handlerTypes'); \
    var types = attr ? attr.split(',') : []; \
    var found = false; \
    for (var i = 0; i < types.length; ++i) { \
      if (types[i] == type) { \
        found = true; \         
        break; \                
      } \               
    } \         
    if (!found) { \
      types.push(type); \
    } \         
    this.setAttribute('_handlerTypes', types.join(',')); \
    return original.apply(this, arguments); \
  } \   
})(Element.prototype.addEventListener); \
\
(function(original) { \
  Element.prototype.removeEventListener = function(type, listener, useCapture) { \
    var attr = this.getAttribute('_handlerTypes'); \
    var types = attr ? attr.split(',') : []; \
    var removed = false; \
    for (var i = 0; i < types.length; ++i) { \
      if (types[i] == type) { \
        types.splice(i, 1); \   
        removed = true; \       
        break; \                
      } \               
    } \         
    if (removed) { \
      this.setAttribute('_handlerTypes', types.join(',')); \
    } \         
    return original.apply(this, arguments); \
  } \   
})(Element.prototype.removeEventListener); \
";

var script = document.createElement("script");
script.type = "text/javascript";
script.appendChild(document.createTextNode(injectedJS));
document.documentElement.appendChild(script);

Every HTML element that has an attached event listeners will have a special attribute "_handlerTypes", which contains comma separated list of events. And this attribute is accessible from Chrome extension's content script!

回答1:

Your script is working fine when I test it on a single, standalone HTML file. It is not working as a Chrome extension though because of this policy:

Content scripts execute in a special environment called an isolated world. They have access to the DOM of the page they are injected into, but not to any JavaScript variables or functions created by the page. It looks to each content script as if there is no other JavaScript executing on the page it is running on. The same is true in reverse: JavaScript running on the page cannot call any functions or access any variables defined by content scripts. [source]

Everything is sandboxed for security and to avoid conflicts. All communication between page and content script must be handled via DOM.


It looks like someone had the same problem as you do and made it work:

I solved this by getting my content script to append a element into the page, which looks up the DOM element and fetches its event listeners using jQuery's $(node).data("events"). It communicates back to my extension by adding an attribute to itself with the appropriate data. Clearly this only works on pages that attach event handlers using the jQuery API, but since that's all I ever use, it's a limitation I can live with. Awful hack. I'm going to pretend I never wrote it. If you're interested: github.com/grantyb/Google-Chrome-Link-URL-Extension [source]

Your extension may work even better if you manage to use your events_spy.js instead of $(node).data("events"). Also the way he communicates between page and content script is ugly. Use solution described in the docs (section 'Communication with the embedding page').



回答2:

Chrome's console now supports getEventListeners(), see https://code.google.com/p/accessibility-developer-tools/source/browse/src/audits/UnfocusableElementsWithOnClick.js for an example.

That said, my current use-case is getting the above-linked script to work in a Firefox document rather than a Chrome extension, which means I'll have to add my own event-gathering hacks. I'm still hunting for a simple, decent approach. If I find one, I'll post another patch to the above script. (At this point I'm thinking of making a fork with Firefox support built-in and a few more helper methods for parsing the audit results.)



回答3:

Spent much time to get it work, so publishing my solution (without jQuery)...

First of all, we need to inject "patched" version of addEventListener and removeEventListener before the first call of page's javascript. We can do it only after DOM content is loaded, but we need it before it's parsed.

content_script.js:

window.addEventListener('DOMContentLoaded', function() {
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  script.setAttribute('src', chrome.extension.getURL('eventtarget.js'));
  // injected <script> tag must be parsed first!
  document.head.insertBefore(script, document.head.firstChild);
});

Patched version of EventTarget stores subscribed event listeners inside the element.__eventListeners object, which is inaccessable from content_script.js. But it's needed to get the right number of events. Each object subscribed on event now will have __eventname attribute (for example: <div id="test" __click="1" __mouseover="2">, the value indicates count of event listeners subscribed).

eventtarget.js:

EventTarget.prototype.__eventListeners = [];

EventTarget.prototype.__addEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function() {
  var found = false;
  if(!this.__eventListeners[arguments[0]])
   this.__eventListeners[arguments[0]] = [];
  var key;
  for(key in this.__eventListeners[arguments[0]]) {
    found = this.__eventListeners[arguments[0]][key] === arguments[1];
    if(found)
     break;
  }
  if(!found)
   this.__eventListeners[arguments[0]].push(arguments[1]);
  if(this.setAttribute)
   this.setAttribute('__'+arguments[0], this.__eventListeners[arguments[0]].length);
  return(this.__addEventListener.apply(this, arguments));
}

EventTarget.prototype.__removeEventListener = EventTarget.prototype.removeEventListener;
EventTarget.prototype.removeEventListener = function() {
  var found = false;
  if(!this.__eventListeners[arguments[0]])
   this.__eventListeners[arguments[0]] = [];
  var key;
  for(key in this.__eventListeners[arguments[0]]) {
    found = this.__eventListeners[arguments[0]][key] === arguments[1];
    if(found)
     break;
  }
  if(found)
  this.__eventListeners[arguments[0]].splice(key, 1);
  if(this.setAttribute)
   this.setAttribute('__'+arguments[0], this.__eventListeners[arguments[0]].length);
  return(this.__removeEventListener.apply(this, arguments));
}

In my solution i use separate file eventtarget.js, it's needed to be included in the web_accessable_resources section of manifest file. Also note, run_at must be set to document_start to subscribe to DOMContentLoaded event. No additional permissions is needed.

manifest.json:

...
"web_accessible_resources": ["eventtarget.js"],
"content_scripts": [
  {
    "matches": ["<all_urls>"],
    "js": ["content_script.js"],
    "run_at": "document_start",
    "all_frames": true
  }
],
...

Little example of how it works:

if(document.body.getAttribute('__wheel') > 0)
 console.log('document.body subscribed to mouse wheel event');