Как узнать, какие типы прослушивателей событий прикреплены к конкретному элементу HTML в расширении Chrome? - PullRequest
5 голосов
/ 18 января 2012

Я публикую этот вопрос здесь, так как не могу опубликовать его на официальных форумах по расширению Chromium (или есть огромная задержка, пока он не модерируется). Я должен проверить в расширении Chromium, есть ли прослушиватель определенного типа события, прикрепленный к произвольному элементу HTML. В Firefox я мог использовать следующий сервис для получения этой информации:

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);
}

Как я вижу в Chromium, аналогичного API нет. Поэтому я попробовал следующий метод (который был предложен здесь ):

Я создал скрипт 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);

Я объявляю этот скрипт в manifest.json следующим образом:

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

Затем я проверяю свое расширение на следующей HTML-странице:

<!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>

К сожалению, это не работает - я не вижу, что отладчик останавливается внутри моей пользовательской функции addEventListener(). Что я делаю не так?

Спасибо!

EDIT : окончательное (грязное) решение благодаря @ 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);

Каждый элемент HTML, к которому прикреплены прослушиватели событий, будет иметь специальный атрибут "_handlerTypes", который содержит список событий, разделенных запятыми. И этот атрибут доступен из скрипта содержимого расширения Chrome!

Ответы [ 3 ]

1 голос
/ 19 января 2012

Ваш скрипт работает нормально, когда я тестирую его на одном отдельном HTML-файле. Это не работает как расширение Chrome, хотя из-за этой политики:

Сценарии содержимого выполняются в специальной среде, называемой изолированным миром. Они имеют доступ к DOM страницы, в которую они внедрены, но не к каким-либо переменным или функциям JavaScript, созданным этой страницей. Каждый скрипт содержимого выглядит так, как будто на странице, на которой он запущен, нет другого JavaScript-кода. То же самое верно и в обратном: JavaScript, работающий на странице, не может вызывать какие-либо функции или обращаться к переменным, определенным скриптами содержимого. [Источник]

Все в песочнице для безопасности и предотвращения конфликтов. Все взаимодействие между страницей и содержимым скрипта должно осуществляться через DOM.


Похоже, у кого-то была такая же проблема, как и у вас, и она заработала:

Я решил это, заставив свой контент-скрипт добавить элемент на страницу, которая ищет элемент DOM и выбирает его прослушиватели событий, использующие jQuery's $ (node) .data ("events"). Общается вернуться к моему расширению, добавив к себе атрибут с соответствующие данные. Очевидно, это работает только на страницах, которые прикрепляют событие обработчики, использующие API jQuery, но так как это все, что я когда-либо использовал, это ограничение, с которым я могу жить. Ужасный взлом. Я собираюсь притвориться, что никогда написал это. Если вы заинтересованы: github.com/grantyb/Google-Chrome-Link-URL-Extension [источник]

Ваше расширение может работать даже лучше, если вам удастся использовать events_spy.js вместо $(node).data("events"). Кроме того, способ, которым он общается между страницей и содержимым сценария, ужасен. Используйте решение, описанное в документах (раздел «Связь со страницей внедрения»).

0 голосов
/ 25 марта 2014

Потратил много времени, чтобы заставить его работать, поэтому опубликовал мое решение (без jQuery) ...

Прежде всего нам нужно внедрить «исправленную» версию addEventListener и removeEventListener перед первым вызовом javascript страницы. Мы можем сделать это только после загрузки содержимого DOM, но нам это нужно до его анализа.

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);
});

Исправленная версия EventTarget хранит подписанные прослушиватели событий внутри объекта element.__eventListeners, который недоступен из content_script.js. Но это нужно, чтобы получить правильное количество событий. Каждый объект, подписанный на событие, теперь будет иметь атрибут __eventname (например: <div id="test" __click="1" __mouseover="2">, значение указывает количество подписчиков прослушанных событий).

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));
}

В моем решении я использую отдельный файл eventtarget.js, его необходимо включить в раздел web_accessable_resources файла манифеста. Также обратите внимание, что run_at должно быть установлено на document_start, чтобы подписаться на событие DOMContentLoaded. Никаких дополнительных разрешений не требуется.

manifest.json:

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

Маленький пример того, как это работает:

if(document.body.getAttribute('__wheel') > 0)
 console.log('document.body subscribed to mouse wheel event');
0 голосов
/ 13 февраля 2013

Консоль Chrome теперь поддерживает getEventListeners(), см. https://code.google.com/p/accessibility-developer-tools/source/browse/src/audits/UnfocusableElementsWithOnClick.js для примера.

Тем не менее, мой текущий вариант использования - заставить вышеупомянутый скрипт работать в документе Firefox, а не в расширении Chrome, что означает, что мне придется добавлять свои собственные хаки для сбора событий. Я все еще ищу простой, достойный подход. Если я найду один, я выложу другой патч к вышеупомянутому скрипту. (На данный момент я подумываю о создании форка со встроенной поддержкой Firefox и нескольких вспомогательных методов для анализа результатов аудита.)

...