Bug 1042082 - Chrome event bubbles are displayed when they shouldn't be r=ochameau

Differential Revision: https://phabricator.services.mozilla.com/D18358

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Michael Ratcliffe 2019-02-22 11:17:02 +00:00
Родитель bf8987e947
Коммит 11f246e18a
7 изменённых файлов: 296 добавлений и 127 удалений

Просмотреть файл

@ -12,6 +12,7 @@ support-files =
doc_markup_events_02.html
doc_markup_events_03.html
doc_markup_events_04.html
doc_markup_events_chrome_listeners.html
doc_markup_events_jquery.html
doc_markup_events_object_listener.html
doc_markup_events-overflow.html
@ -114,6 +115,8 @@ skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32
[browser_markup_events_02.js]
[browser_markup_events_03.js]
[browser_markup_events_04.js]
[browser_markup_events_chrome_blocked.js]
[browser_markup_events_chrome_not_blocked.js]
[browser_markup_events_click_to_close.js]
[browser_markup_events_jquery_1.0.js]
[browser_markup_events_jquery_1.1.js]

Просмотреть файл

@ -0,0 +1,26 @@
/* 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 are hidden for <video> tags in the
// content process when devtools.chrome.enabled=false.
// <video> tags have 22 chrome listeners.
const TEST_URL = URL_ROOT + "doc_markup_events_chrome_listeners.html";
loadHelperScript("helper_events_test_runner.js");
const TEST_DATA = [
{
selector: "video",
expected: [ ],
},
];
add_task(async function() {
await pushPref("devtools.chrome.enabled", false);
await runEventPopupTests(TEST_URL, TEST_DATA);
});

Просмотреть файл

@ -0,0 +1,74 @@
/* 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 are shown for <video> tags in the
// content process when devtools.chrome.enabled=true.
const TEST_URL = URL_ROOT + "doc_markup_events_chrome_listeners.html";
loadHelperScript("helper_events_test_runner.js");
const TEST_DATA = [
{
selector: "video",
expected: [
createEvent("canplay"),
createEvent("canplaythrough"),
createEvent("emptied"),
createEvent("ended"),
createEvent("error"),
createEvent("keypress"),
createEvent("loadeddata"),
createEvent("loadedmetadata"),
createEvent("loadstart"),
createEvent("mozvideoonlyseekbegin"),
createEvent("mozvideoonlyseekcompleted"),
createEvent("pause"),
createEvent("play"),
createEvent("playing"),
createEvent("progress"),
createEvent("seeked"),
createEvent("seeking"),
createEvent("stalled"),
createEvent("suspend"),
createEvent("timeupdate"),
createEvent("volumechange"),
createEvent("waiting"),
],
},
];
function createEvent(type) {
return {
type: type,
filename: "chrome://global/content/elements/videocontrols.js:437",
attributes: [
"Capturing",
"DOM2",
],
handler: `
${type === "play" ? "function" : "handleEvent"}(aEvent) {
if (!aEvent.isTrusted) {
this.log("Drop untrusted event ----> " + aEvent.type);
return;
}
this.log("Got event ----> " + aEvent.type);
if (this.videoEvents.includes(aEvent.type)) {
this.handleVideoEvent(aEvent);
} else {
this.handleControlEvent(aEvent);
}
}`,
};
}
add_task(async function() {
await pushPref("devtools.chrome.enabled", true);
await runEventPopupTests(TEST_URL, TEST_DATA);
});

Просмотреть файл

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<video controls></video>
</body>
</html>

Просмотреть файл

@ -9,7 +9,6 @@
const { Cu } = require("chrome");
const Services = require("Services");
const makeDebugger = require("devtools/server/actors/utils/make-debugger");
const {
isAfterPseudoElement,
isBeforePseudoElement,
@ -194,6 +193,18 @@ const REACT_EVENT_NAMES = [
* The base class that all the enent collectors should be based upon.
*/
class MainEventCollector {
/**
* We allow displaying chrome events if the page is chrome or if
* `devtools.chrome.enabled = true`.
*/
get chromeEnabled() {
if (typeof this._chromeEnabled === "undefined") {
this._chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
}
return this._chromeEnabled;
}
/**
* Check if a node has any event listeners attached. Please do not override
* this method... your getListeners() implementation needs to have the
@ -265,6 +276,16 @@ class MainEventCollector {
unwrap(obj) {
return Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
}
isChromeHandler(handler) {
const handlerPrincipal = Cu.getObjectPrincipal(handler);
// Chrome codebase may register listeners on the page from a frame script or
// JSM <video> tags may also report internal listeners, but they won't be
// coming from the system principal. Instead, they will be using an expanded
// principal.
return handlerPrincipal.isSystemPrincipal || handlerPrincipal.isExpandedPrincipal;
}
}
/**
@ -318,6 +339,12 @@ class DOMEventCollector extends MainEventCollector {
continue;
}
// If we shouldn't be showing chrome events due to context and this is a
// chrome handler we can ignore it.
if (!this.chromeEnabled && this.isChromeHandler(handler)) {
continue;
}
// If this is checking if a node has any listeners then we have found one
// so return now.
if (checkOnly) {
@ -390,13 +417,20 @@ class JQueryEventCollector extends MainEventCollector {
}
if (typeof event === "function" || typeof event === "object") {
// If we shouldn't be showing chrome events due to context and this
// is a chrome handler we can ignore it.
const handler = event.handler || event;
if (!this.chromeEnabled && this.isChromeHandler(handler)) {
continue;
}
if (checkOnly) {
return true;
}
const eventInfo = {
type: type,
handler: event.handler || event,
handler: handler,
tags: "jQuery",
hide: {
capturing: true,
@ -470,12 +504,19 @@ class JQueryLiveEventCollector extends MainEventCollector {
}
if (typeof event === "function" || typeof event === "object") {
// If we shouldn't be showing chrome events due to context and this
// is a chrome handler we can ignore it.
const handler = event.handler || event;
if (!this.chromeEnabled && this.isChromeHandler(handler)) {
continue;
}
if (checkOnly) {
return true;
}
const eventInfo = {
type: event.origType || event.type.substr(selector.length + 1),
handler: event.handler || event,
handler: handler,
tags: "jQuery,Live",
hide: {
dom0: true,
@ -577,6 +618,10 @@ class ReactEventCollector extends MainEventCollector {
continue;
}
if (!this.chromeEnabled && this.isChromeHandler(listener)) {
continue;
}
if (checkOnly) {
return true;
}
@ -671,12 +716,6 @@ class EventCollector {
new JQueryEventCollector(),
new DOMEventCollector(),
];
// Add a method to create a simple debugger.
this.makeDebuggerForContent = makeDebugger.bind(null, {
findDebuggees: dbg => [],
shouldAddNewGlobalAsDebuggee: global => true,
});
}
/**
@ -684,7 +723,6 @@ class EventCollector {
*/
destroy() {
this.eventCollectors = null;
this.makeDebuggerForContent = null;
}
/**
@ -701,6 +739,7 @@ class EventCollector {
return true;
}
}
return false;
}
@ -713,7 +752,7 @@ class EventCollector {
* {
* type: type, // e.g. "click"
* handler: handler, // The function called when event is triggered.
* tags: "jQuery", // Comma seperated list of tags displayed
* tags: "jQuery", // Comma separated list of tags displayed
* // inside event bubble.
* hide: { // Flags for hiding certain properties.
* capturing: true,
@ -723,9 +762,7 @@ class EventCollector {
*/
getEventListeners(node) {
const listenerArray = [];
const dbg = this.makeDebuggerForContent();
const global = Cu.getGlobalForObject(node);
const globalDO = dbg.addDebuggee(global);
const dbg = new Debugger();
for (const collector of this.eventCollectors) {
const listeners = collector.getListeners(node);
@ -738,12 +775,10 @@ class EventCollector {
if (collector.normalizeListener) {
listener.normalizeListener = collector.normalizeListener;
}
this.processHandlerForEvent(listenerArray, listener, globalDO);
this.processHandlerForEvent(listenerArray, listener, dbg);
}
}
dbg.removeDebuggee(globalDO);
listenerArray.sort((a, b) => {
return a.type.localeCompare(b.type);
});
@ -757,9 +792,10 @@ class EventCollector {
* @param {Array} listenerArray
* listenerArray contains all event objects that we have gathered
* so far.
* @param {Object} eventInfo
* See event-parsers.js.registerEventParser() for a description of the
* eventInfo object.
* @param {EventListener} listener
* The event listener to process.
* @param {Debugger} dbg
* Debugger instance.
*
* @return {Array}
* An array of objects where a typical object looks like this:
@ -776,128 +812,142 @@ class EventCollector {
* native: false
* }
*/
processHandlerForEvent(listenerArray, listener, globalDO) {
const { capturing, handler } = listener;
let listenerDO = globalDO.makeDebuggeeValue(handler);
processHandlerForEvent(listenerArray, listener, dbg) {
let globalDO;
const { normalizeListener } = listener;
try {
const { capturing, handler } = listener;
const global = Cu.getGlobalForObject(handler);
if (normalizeListener) {
listenerDO = normalizeListener(listenerDO, listener);
}
// It is important that we recreate the globalDO for each handler because
// their global object can vary e.g. resource:// URLs on a video control. If
// we don't do this then all chrome listeners simply display "native code."
globalDO = dbg.addDebuggee(global);
let listenerDO = globalDO.makeDebuggeeValue(handler);
const hide = listener.hide || {};
const override = listener.override || {};
const tags = listener.tags || "";
const type = listener.type || "";
let dom0 = false;
let functionSource = handler.toString();
let line = 0;
let native = false;
let url = "";
const { normalizeListener } = listener;
// If the listener is an object with a 'handleEvent' method, use that.
if (listenerDO.class === "Object" || /^XUL\w*Element$/.test(listenerDO.class)) {
let desc;
while (!desc && listenerDO) {
desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
listenerDO = listenerDO.proto;
if (normalizeListener) {
listenerDO = normalizeListener(listenerDO, listener);
}
if (desc && desc.value) {
listenerDO = desc.value;
}
}
const hide = listener.hide || {};
const override = listener.override || {};
const tags = listener.tags || "";
const type = listener.type || "";
let dom0 = false;
let functionSource = handler.toString();
let line = 0;
let native = false;
let url = "";
// If the listener is bound to a different context then we need to switch
// to the bound function.
if (listenerDO.isBoundFunction) {
listenerDO = listenerDO.boundTargetFunction;
}
// If the listener is an object with a 'handleEvent' method, use that.
if (listenerDO.class === "Object" || /^XUL\w*Element$/.test(listenerDO.class)) {
let desc;
const { isArrowFunction, name, script, parameterNames } = listenerDO;
while (!desc && listenerDO) {
desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
listenerDO = listenerDO.proto;
}
if (script) {
const scriptSource = script.source.text;
// Scripts are provided via script tags. If it wasn't provided by a
// script tag it must be a DOM0 event.
if (script.source.element) {
dom0 = script.source.element.class !== "HTMLScriptElement";
} else {
dom0 = false;
}
line = script.startLine;
url = script.url;
// Checking for the string "[native code]" is the only way at this point
// to check for native code. Even if this provides a false positive then
// grabbing the source code a second time is harmless.
if (functionSource === "[object Object]" ||
functionSource === "[object XULElement]" ||
functionSource.includes("[native code]")) {
functionSource =
scriptSource.substr(script.sourceStart, script.sourceLength);
// At this point the script looks like this:
// () { ... }
// We prefix this with "function" if it is not a fat arrow function.
if (!isArrowFunction) {
functionSource = "function " + functionSource;
if (desc && desc.value) {
listenerDO = desc.value;
}
}
} else {
// If the listener is a native one (provided by C++ code) then we have no
// access to the script. We use the native flag to prevent showing the
// debugger button because the script is not available.
native = true;
}
// Arrow function text always contains the parameters. Function
// parameters are often missing e.g. if Array.sort is used as a handler.
// If they are missing we provide the parameters ourselves.
if (parameterNames && parameterNames.length > 0) {
const prefix = "function " + name + "()";
const paramString = parameterNames.join(", ");
// If the listener is bound to a different context then we need to switch
// to the bound function.
if (listenerDO.isBoundFunction) {
listenerDO = listenerDO.boundTargetFunction;
}
if (functionSource.startsWith(prefix)) {
functionSource = functionSource.substr(prefix.length);
const { isArrowFunction, name, script, parameterNames } = listenerDO;
functionSource = `function ${name} (${paramString})${functionSource}`;
if (script) {
const scriptSource = script.source.text;
// Scripts are provided via script tags. If it wasn't provided by a
// script tag it must be a DOM0 event.
if (script.source.element) {
dom0 = script.source.element.class !== "HTMLScriptElement";
} else {
dom0 = false;
}
line = script.startLine;
url = script.url;
// Checking for the string "[native code]" is the only way at this point
// to check for native code. Even if this provides a false positive then
// grabbing the source code a second time is harmless.
if (functionSource === "[object Object]" ||
functionSource === "[object XULElement]" ||
functionSource.includes("[native code]")) {
functionSource =
scriptSource.substr(script.sourceStart, script.sourceLength);
// At this point the script looks like this:
// () { ... }
// We prefix this with "function" if it is not a fat arrow function.
if (!isArrowFunction) {
functionSource = "function " + functionSource;
}
}
} else {
// If the listener is a native one (provided by C++ code) then we have no
// access to the script. We use the native flag to prevent showing the
// debugger button because the script is not available.
native = true;
}
// Arrow function text always contains the parameters. Function
// parameters are often missing e.g. if Array.sort is used as a handler.
// If they are missing we provide the parameters ourselves.
if (parameterNames && parameterNames.length > 0) {
const prefix = "function " + name + "()";
const paramString = parameterNames.join(", ");
if (functionSource.startsWith(prefix)) {
functionSource = functionSource.substr(prefix.length);
functionSource = `function ${name} (${paramString})${functionSource}`;
}
}
// If the listener is native code we display the filename "[native code]."
// This is the official string and should *not* be translated.
let origin;
if (native) {
origin = "[native code]";
} else {
origin = url + ((dom0 || line === 0) ? "" : ":" + line);
}
const eventObj = {
type: override.type || type,
handler: override.handler || functionSource.trim(),
origin: override.origin || origin,
tags: override.tags || tags,
DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
capturing: typeof override.capturing !== "undefined" ?
override.capturing : capturing,
hide: typeof override.hide !== "undefined" ? override.hide : hide,
native,
};
// Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
// generated dynamically from e.g. an onclick="" attribute so the script
// doesn't actually exist.
if (native || dom0) {
eventObj.hide.debugger = true;
}
listenerArray.push(eventObj);
} finally {
// Ensure that we always remove the debuggee.
if (globalDO) {
dbg.removeDebuggee(globalDO);
}
}
// If the listener is native code we display the filename "[native code]."
// This is the official string and should *not* be translated.
let origin;
if (native) {
origin = "[native code]";
} else {
origin = url + ((dom0 || line === 0) ? "" : ":" + line);
}
const eventObj = {
type: override.type || type,
handler: override.handler || functionSource.trim(),
origin: override.origin || origin,
tags: override.tags || tags,
DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
capturing: typeof override.capturing !== "undefined" ?
override.capturing : capturing,
hide: typeof override.hide !== "undefined" ? override.hide : hide,
native,
};
// Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
// generated dynamically from e.g. an onclick="" attribute so the script
// doesn't actually exist.
if (native || dom0) {
eventObj.hide.debugger = true;
}
listenerArray.push(eventObj);
}
}

Просмотреть файл

@ -289,7 +289,10 @@ const NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
* check if there are any event listeners.
*/
get _hasEventListeners() {
return this._eventCollector.hasEventListeners(this.rawNode);
// We need to pass a debugger instance from this compartment because
// otherwise we can't make use of it inside the event-collector module.
const dbg = this.parent().targetActor.makeDebugger();
return this._eventCollector.hasEventListeners(this.rawNode, dbg);
},
writeAttrs: function() {

Просмотреть файл

@ -15,6 +15,9 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1157469
window.onload = function() {
SimpleTest.waitForExplicitFinish();
const prevPrefValue = Services.prefs.getBoolPref("devtools.chrome.enabled");
Services.prefs.setBoolPref("devtools.chrome.enabled", true);
let inspectee = null;
let inspector = null;
let walker = null;
@ -148,6 +151,7 @@ window.onload = function() {
is(mutations.length, 1, "expect only one mutation");
isnot(mutations.type, "events", "mutation type should not be events");
Services.prefs.setBoolPref("devtools.chrome.enabled", prevPrefValue);
runNextTest();
});