Bug 1519597 - Bad event handlers on DOM nodes produce bogus popups when inspected r=pbro,ochameau
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Thu, 24 Jan 2019 09:54:20 +0000
changeset 455232 4afc95f9c7ddd7e53bd8deee384c73a7778eeee3
parent 455231 5797d4eee0ea2bc4b97c0eaefd3d7b579302de34
child 455233 4c814c2de16d3443e2488c376ef7ca30bacf3cb5
push id35426
push useropoprus@mozilla.com
push dateThu, 24 Jan 2019 16:48:02 +0000
treeherdermozilla-central@0aa259de1b77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro, ochameau
bugs1519597
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1519597 - Bad event handlers on DOM nodes produce bogus popups when inspected r=pbro,ochameau Differential Revision: https://phabricator.services.mozilla.com/D17142
devtools/client/inspector/markup/test/browser.ini
devtools/client/inspector/markup/test/browser_markup_events_object_listener.js
devtools/client/inspector/markup/test/doc_markup_events_object_listener.html
devtools/server/actors/inspector/event-parsers.js
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -9,16 +9,17 @@ support-files =
   doc_markup_dragdrop_autoscroll_01.html
   doc_markup_dragdrop_autoscroll_02.html
   doc_markup_edit.html
   doc_markup_events_01.html
   doc_markup_events_02.html
   doc_markup_events_03.html
   doc_markup_events_04.html
   doc_markup_events_jquery.html
+  doc_markup_events_object_listener.html
   doc_markup_events-overflow.html
   doc_markup_events_react_development_15.4.1.html
   doc_markup_events_react_development_15.4.1_jsx.html
   doc_markup_events_react_production_15.3.1.html
   doc_markup_events_react_production_15.3.1_jsx.html
   doc_markup_events_react_production_16.2.0.html
   doc_markup_events_react_production_16.2.0_jsx.html
   doc_markup_events-source_map.html
@@ -119,16 +120,17 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_markup_events_jquery_1.1.js]
 [browser_markup_events_jquery_1.2.js]
 [browser_markup_events_jquery_1.3.js]
 [browser_markup_events_jquery_1.4.js]
 [browser_markup_events_jquery_1.6.js]
 [browser_markup_events_jquery_1.7.js]
 [browser_markup_events_jquery_1.11.1.js]
 [browser_markup_events_jquery_2.1.1.js]
+[browser_markup_events_object_listener.js]
 [browser_markup_events-overflow.js]
 skip-if = true # Bug 1177550
 [browser_markup_events_react_development_15.4.1.js]
 [browser_markup_events_react_development_15.4.1_jsx.js]
 [browser_markup_events_react_production_15.3.1.js]
 [browser_markup_events_react_production_15.3.1_jsx.js]
 [browser_markup_events_react_production_16.2.0.js]
 [browser_markup_events_react_production_16.2.0_jsx.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events_object_listener.js
@@ -0,0 +1,53 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_events_test_runner.js */
+
+"use strict";
+
+// Test that markup view event bubbles show the correct event info for object
+// style event listeners and that no bubbles are shown for objects without any
+// handleEvent method.
+
+const TEST_URL = URL_ROOT + "doc_markup_events_object_listener.html";
+
+loadHelperScript("helper_events_test_runner.js");
+
+const TEST_DATA = [ // eslint-disable-line
+  {
+    selector: "#valid-object-listener",
+    expected: [
+      {
+        type: "click",
+        filename: TEST_URL + ":17",
+        attributes: [
+          "Bubbling",
+          "DOM2",
+        ],
+        handler: `() => {\n` +
+                 `  console.log("handleEvent");\n` +
+                 `}`,
+      },
+    ],
+  },
+  {
+    selector: "#valid-invalid-object-listeners",
+    expected: [
+      {
+        type: "click",
+        filename: TEST_URL + ":24",
+        attributes: [
+          "Bubbling",
+          "DOM2",
+        ],
+        handler: `() => {\n` +
+                 `  console.log("handleEvent");\n` +
+                 `}`,
+      },
+    ],
+  },
+];
+
+add_task(async function() {
+  await runEventPopupTests(TEST_URL, TEST_DATA);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_events_object_listener.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <style>
+      div {
+        border: 1px solid #000;
+      }
+    </style>
+    <script>
+      function init() {
+        const valid = document.querySelector("#valid-object-listener");
+        const validInvalid = document.querySelector("#valid-invalid-object-listeners");
+
+        // Add a valid event to #valid.
+        valid.addEventListener('click', {
+          handleEvent: () => {
+            console.log("handleEvent");
+          }
+        });
+
+        // Add valid and invalid events to #validInvalid.
+        validInvalid.addEventListener('click', {
+          handleEvent: () => {
+            console.log("handleEvent");
+          }
+        });
+        validInvalid.addEventListener('dblclick', {});
+      }
+    </script>
+  </head>
+  <body onload="init();">
+    <h1>Events test with event object listeners</h1>
+    <div id="valid-object-listener">Valid object listener</div>
+    <div id="valid-invalid-object-listeners">Valid and invalid object listeners</div>
+  </body>
+</html>
--- a/devtools/server/actors/inspector/event-parsers.js
+++ b/devtools/server/actors/inspector/event-parsers.js
@@ -2,24 +2,79 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // This file contains event parsers that are then used by developer tools in
 // order to find information about events affecting an HTML element.
 
 "use strict";
 
+const { Cu } = require("chrome");
 const Services = require("Services");
 
 // eslint-disable-next-line
 const JQUERY_LIVE_REGEX = /return typeof \w+.*.event\.triggered[\s\S]*\.event\.(dispatch|handle).*arguments/;
 
 var parsers = [
   {
     id: "jQuery events",
+    hasListeners: function(node) {
+      const global = node.ownerGlobal.wrappedJSObject;
+      const hasJQuery = global.jQuery && global.jQuery.fn && global.jQuery.fn.jquery;
+
+      if (!hasJQuery) {
+        return false;
+      }
+
+      const jQuery = global.jQuery;
+      const handlers = [];
+
+      // jQuery 1.2+
+      const data = jQuery._data || jQuery.data;
+      if (data) {
+        const eventsObj = data(node, "events");
+        for (const type in eventsObj) {
+          const events = eventsObj[type];
+          for (const key in events) {
+            const event = events[key];
+
+            if (node.wrappedJSObject == global.document && event.selector) {
+              continue;
+            }
+
+            if (typeof event === "object" || typeof event === "function") {
+              return true;
+            }
+          }
+        }
+      }
+
+      // JQuery 1.0 & 1.1
+      const entry = jQuery(node)[0];
+      if (!entry) {
+        return handlers;
+      }
+
+      for (const type in entry.events) {
+        const events = entry.events[type];
+        for (const key in events) {
+          const event = events[key];
+
+          if (node.wrappedJSObject == global.document && event.selector) {
+            continue;
+          }
+
+          if (typeof events[key] === "function") {
+            return true;
+          }
+        }
+      }
+
+      return false;
+    },
     getListeners: function(node) {
       const global = node.ownerGlobal.wrappedJSObject;
       const hasJQuery = global.jQuery && global.jQuery.fn && global.jQuery.fn.jquery;
 
       if (!hasJQuery) {
         return undefined;
       }
 
@@ -172,52 +227,76 @@ var parsers = [
           Services.els.getListenerInfoFor(node.parentNode) || [];
 
         listeners = [...winListeners, ...docElementListeners, ...docListeners];
       } else {
         listeners = Services.els.getListenerInfoFor(node) || [];
       }
 
       for (const listener of listeners) {
-        if (listener.listenerObject && listener.type) {
+        if (isValidDOMListener(listener)) {
           return true;
         }
       }
 
       return false;
     },
     getListeners: function(node) {
       const handlers = [];
       const listeners = Services.els.getListenerInfoFor(node);
 
       // The Node actor's getEventListenerInfo knows that when an html tag has
       // been passed we need the window object so we don't need to account for
       // event hoisting here as we did in hasListeners.
 
-      for (const listenerObj of listeners) {
-        const listener = listenerObj.listenerObject;
+      for (const listener of listeners) {
+        if (!isValidDOMListener(listener)) {
+          continue;
+        }
+
+        // Get the listener object, either a Function or an Object.
+        let obj = listener.listenerObject;
+
+        // Unwrap the listener in order to see content objects.
+        if (Cu.isXrayWrapper(obj)) {
+          obj = listener.listenerObject.wrappedJSObject;
+        }
+
+        let handler = null;
 
-        // If there is no JS event listener skip this.
-        if (!listener || JQUERY_LIVE_REGEX.test(listener.toString())) {
+        // An object without a valid handleEvent is not a valid listener.
+        if (typeof obj === "object") {
+          const unwrapped = Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
+          if (typeof unwrapped.handleEvent === "function") {
+            handler = Cu.unwaiveXrays(unwrapped.handleEvent);
+          }
+        } else if (typeof obj === "function") {
+          // Ignore DOM events used to trigger jQuery events as they are only
+          // useful to the developers of the jQuery library.
+          if (JQUERY_LIVE_REGEX.test(obj.toString())) {
+            continue;
+          }
+          // Otherwise, the other valid listener type is function.
+          handler = obj;
+        } else {
           continue;
         }
 
         const eventInfo = {
-          capturing: listenerObj.capturing,
-          type: listenerObj.type,
-          handler: listener,
+          capturing: listener.capturing,
+          type: listener.type,
+          handler: handler,
         };
 
         handlers.push(eventInfo);
       }
 
       return handlers;
     },
   },
-
   {
     id: "React events",
     hasListeners: function(node) {
       return reactGetListeners(node, true);
     },
 
     getListeners: function(node) {
       return reactGetListeners(node, false);
@@ -394,16 +473,53 @@ function jQueryLiveGetListeners(node, bo
   }
 
   if (boolOnEventFound) {
     return false;
   }
   return handlers;
 }
 
+function isValidDOMListener(listener) {
+  // Ignore listeners without a type, e.g.
+  // node.addEventListener("", function() {})
+  if (!listener.type) {
+    return false;
+  }
+
+  // Get the listener object, either a Function or an Object.
+  let obj = listener.listenerObject;
+
+  // Ignore listeners without any listener, e.g.
+  // node.addEventListener("mouseover", null);
+  if (!obj) {
+    return false;
+  }
+
+  // Unwrap the listener in order to see content objects.
+  if (Cu.isXrayWrapper(obj)) {
+    obj = listener.listenerObject.wrappedJSObject;
+  }
+
+  // An object without a valid handleEvent is not a valid listener.
+  if (typeof obj === "object") {
+    const unwrapped = Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
+    if (typeof unwrapped.handleEvent === "function") {
+      return Cu.unwaiveXrays(unwrapped.handleEvent);
+    }
+    return false;
+  } else if (typeof obj === "function") {
+    if (JQUERY_LIVE_REGEX.test(obj.toString())) {
+      return false;
+    }
+    return obj;
+  }
+  return false;
+}
+
 this.EventParsers = function EventParsers() {
   if (this._eventParsers.size === 0) {
     for (const parserObj of parsers) {
       this.registerEventParser(parserObj);
     }
   }
 };