зеркало из https://github.com/mozilla/gecko-dev.git
1074 строки
29 KiB
JavaScript
1074 строки
29 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* 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 collectors 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");
|
|
const {
|
|
isAfterPseudoElement,
|
|
isBeforePseudoElement,
|
|
isMarkerPseudoElement,
|
|
isNativeAnonymous,
|
|
} = require("devtools/shared/layout/utils");
|
|
const Debugger = require("Debugger");
|
|
const ReplayInspector = require("devtools/server/actors/replay/inspector");
|
|
const {
|
|
EXCLUDED_LISTENER,
|
|
} = require("devtools/server/actors/inspector/constants");
|
|
|
|
// eslint-disable-next-line
|
|
const JQUERY_LIVE_REGEX = /return typeof \w+.*.event\.triggered[\s\S]*\.event\.(dispatch|handle).*arguments/;
|
|
|
|
const REACT_EVENT_NAMES = [
|
|
"onAbort",
|
|
"onAnimationEnd",
|
|
"onAnimationIteration",
|
|
"onAnimationStart",
|
|
"onAuxClick",
|
|
"onBeforeInput",
|
|
"onBlur",
|
|
"onCanPlay",
|
|
"onCanPlayThrough",
|
|
"onCancel",
|
|
"onChange",
|
|
"onClick",
|
|
"onClose",
|
|
"onCompositionEnd",
|
|
"onCompositionStart",
|
|
"onCompositionUpdate",
|
|
"onContextMenu",
|
|
"onCopy",
|
|
"onCut",
|
|
"onDoubleClick",
|
|
"onDrag",
|
|
"onDragEnd",
|
|
"onDragEnter",
|
|
"onDragExit",
|
|
"onDragLeave",
|
|
"onDragOver",
|
|
"onDragStart",
|
|
"onDrop",
|
|
"onDurationChange",
|
|
"onEmptied",
|
|
"onEncrypted",
|
|
"onEnded",
|
|
"onError",
|
|
"onFocus",
|
|
"onGotPointerCapture",
|
|
"onInput",
|
|
"onInvalid",
|
|
"onKeyDown",
|
|
"onKeyPress",
|
|
"onKeyUp",
|
|
"onLoad",
|
|
"onLoadStart",
|
|
"onLoadedData",
|
|
"onLoadedMetadata",
|
|
"onLostPointerCapture",
|
|
"onMouseDown",
|
|
"onMouseEnter",
|
|
"onMouseLeave",
|
|
"onMouseMove",
|
|
"onMouseOut",
|
|
"onMouseOver",
|
|
"onMouseUp",
|
|
"onPaste",
|
|
"onPause",
|
|
"onPlay",
|
|
"onPlaying",
|
|
"onPointerCancel",
|
|
"onPointerDown",
|
|
"onPointerEnter",
|
|
"onPointerLeave",
|
|
"onPointerMove",
|
|
"onPointerOut",
|
|
"onPointerOver",
|
|
"onPointerUp",
|
|
"onProgress",
|
|
"onRateChange",
|
|
"onReset",
|
|
"onScroll",
|
|
"onSeeked",
|
|
"onSeeking",
|
|
"onSelect",
|
|
"onStalled",
|
|
"onSubmit",
|
|
"onSuspend",
|
|
"onTimeUpdate",
|
|
"onToggle",
|
|
"onTouchCancel",
|
|
"onTouchEnd",
|
|
"onTouchMove",
|
|
"onTouchStart",
|
|
"onTransitionEnd",
|
|
"onVolumeChange",
|
|
"onWaiting",
|
|
"onWheel",
|
|
"onAbortCapture",
|
|
"onAnimationEndCapture",
|
|
"onAnimationIterationCapture",
|
|
"onAnimationStartCapture",
|
|
"onAuxClickCapture",
|
|
"onBeforeInputCapture",
|
|
"onBlurCapture",
|
|
"onCanPlayCapture",
|
|
"onCanPlayThroughCapture",
|
|
"onCancelCapture",
|
|
"onChangeCapture",
|
|
"onClickCapture",
|
|
"onCloseCapture",
|
|
"onCompositionEndCapture",
|
|
"onCompositionStartCapture",
|
|
"onCompositionUpdateCapture",
|
|
"onContextMenuCapture",
|
|
"onCopyCapture",
|
|
"onCutCapture",
|
|
"onDoubleClickCapture",
|
|
"onDragCapture",
|
|
"onDragEndCapture",
|
|
"onDragEnterCapture",
|
|
"onDragExitCapture",
|
|
"onDragLeaveCapture",
|
|
"onDragOverCapture",
|
|
"onDragStartCapture",
|
|
"onDropCapture",
|
|
"onDurationChangeCapture",
|
|
"onEmptiedCapture",
|
|
"onEncryptedCapture",
|
|
"onEndedCapture",
|
|
"onErrorCapture",
|
|
"onFocusCapture",
|
|
"onGotPointerCaptureCapture",
|
|
"onInputCapture",
|
|
"onInvalidCapture",
|
|
"onKeyDownCapture",
|
|
"onKeyPressCapture",
|
|
"onKeyUpCapture",
|
|
"onLoadCapture",
|
|
"onLoadStartCapture",
|
|
"onLoadedDataCapture",
|
|
"onLoadedMetadataCapture",
|
|
"onLostPointerCaptureCapture",
|
|
"onMouseDownCapture",
|
|
"onMouseEnterCapture",
|
|
"onMouseLeaveCapture",
|
|
"onMouseMoveCapture",
|
|
"onMouseOutCapture",
|
|
"onMouseOverCapture",
|
|
"onMouseUpCapture",
|
|
"onPasteCapture",
|
|
"onPauseCapture",
|
|
"onPlayCapture",
|
|
"onPlayingCapture",
|
|
"onPointerCancelCapture",
|
|
"onPointerDownCapture",
|
|
"onPointerEnterCapture",
|
|
"onPointerLeaveCapture",
|
|
"onPointerMoveCapture",
|
|
"onPointerOutCapture",
|
|
"onPointerOverCapture",
|
|
"onPointerUpCapture",
|
|
"onProgressCapture",
|
|
"onRateChangeCapture",
|
|
"onResetCapture",
|
|
"onScrollCapture",
|
|
"onSeekedCapture",
|
|
"onSeekingCapture",
|
|
"onSelectCapture",
|
|
"onStalledCapture",
|
|
"onSubmitCapture",
|
|
"onSuspendCapture",
|
|
"onTimeUpdateCapture",
|
|
"onToggleCapture",
|
|
"onTouchCancelCapture",
|
|
"onTouchEndCapture",
|
|
"onTouchMoveCapture",
|
|
"onTouchStartCapture",
|
|
"onTransitionEndCapture",
|
|
"onVolumeChangeCapture",
|
|
"onWaitingCapture",
|
|
"onWheelCapture",
|
|
];
|
|
|
|
/**
|
|
* 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
|
|
* following signature:
|
|
* `getListeners(node, {checkOnly} = {})`
|
|
*
|
|
* @param {DOMNode} node
|
|
* The not for which we want to check for event listeners.
|
|
* @return {Boolean}
|
|
* true if the node has event listeners, false otherwise.
|
|
*/
|
|
hasListeners(node) {
|
|
return this.getListeners(node, {
|
|
checkOnly: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all listeners for a node. This method must be overridden.
|
|
*
|
|
* @param {DOMNode} node
|
|
* The not for which we want to get event listeners.
|
|
* @param {Object} options
|
|
* An object for passing in options.
|
|
* @param {Boolean} [options.checkOnly = false]
|
|
* Don't get any listeners but return true when the first event is
|
|
* found.
|
|
* @return {Array}
|
|
* An array of event handlers.
|
|
*/
|
|
getListeners(node, { checkOnly }) {
|
|
throw new Error("You have to implement the method getListeners()!");
|
|
}
|
|
|
|
/**
|
|
* Get unfiltered DOM Event listeners for a node.
|
|
* NOTE: These listeners may contain invalid events and events based
|
|
* on C++ rather than JavaScript.
|
|
*
|
|
* @param {DOMNode} node
|
|
* The node for which we want to get unfiltered event listeners.
|
|
* @return {Array}
|
|
* An array of unfiltered event listeners or an empty array
|
|
*/
|
|
getDOMListeners(node) {
|
|
let listeners;
|
|
if (
|
|
typeof node.nodeName !== "undefined" &&
|
|
node.nodeName.toLowerCase() === "html"
|
|
) {
|
|
const winListeners =
|
|
Services.els.getListenerInfoFor(node.ownerGlobal) || [];
|
|
const docElementListeners = Services.els.getListenerInfoFor(node) || [];
|
|
const docListeners =
|
|
Services.els.getListenerInfoFor(node.parentNode) || [];
|
|
|
|
listeners = [...winListeners, ...docElementListeners, ...docListeners];
|
|
} else {
|
|
listeners = Services.els.getListenerInfoFor(node) || [];
|
|
}
|
|
|
|
return listeners.filter(listener => {
|
|
const obj = this.unwrap(listener.listenerObject);
|
|
return !obj || !obj[EXCLUDED_LISTENER];
|
|
});
|
|
}
|
|
|
|
getJQuery(node) {
|
|
if (Cu.isDeadWrapper(node)) {
|
|
return null;
|
|
}
|
|
|
|
const global = this.unwrap(node.ownerGlobal);
|
|
if (!global) {
|
|
return null;
|
|
}
|
|
|
|
const hasJQuery =
|
|
global.jQuery && global.jQuery.fn && global.jQuery.fn.jquery;
|
|
|
|
if (hasJQuery) {
|
|
return global.jQuery;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
unwrap(obj) {
|
|
return Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
|
|
}
|
|
|
|
isChromeHandler(handler) {
|
|
try {
|
|
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
|
|
);
|
|
} catch (e) {
|
|
// Anything from a dead object to a CSP error can leave us here so let's
|
|
// return false so that we can fail gracefully.
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or detect DOM events. These may include DOM events created by libraries
|
|
* that enable their custom events to work. At this point we are unable to
|
|
* effectively filter them as they may be proxied or wrapped. Although we know
|
|
* there is an event, we may not know the true contents until it goes
|
|
* through `processHandlerForEvent()`.
|
|
*/
|
|
class DOMEventCollector extends MainEventCollector {
|
|
getListeners(node, { checkOnly } = {}) {
|
|
const handlers = [];
|
|
const listeners = this.getDOMListeners(node);
|
|
|
|
for (const listener of listeners) {
|
|
// Ignore listeners without a type, e.g.
|
|
// node.addEventListener("", function() {})
|
|
if (!listener.type) {
|
|
continue;
|
|
}
|
|
|
|
// Get the listener object, either a Function or an Object.
|
|
const obj = listener.listenerObject;
|
|
|
|
// Ignore listeners without any listener, e.g.
|
|
// node.addEventListener("mouseover", null);
|
|
if (!obj) {
|
|
continue;
|
|
}
|
|
|
|
let handler = null;
|
|
|
|
// An object without a valid handleEvent is not a valid listener.
|
|
if (typeof obj === "object") {
|
|
const unwrapped = this.unwrap(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;
|
|
}
|
|
|
|
// Ignore listeners that have no handler.
|
|
if (!handler) {
|
|
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) {
|
|
return true;
|
|
}
|
|
|
|
const eventInfo = {
|
|
capturing: listener.capturing,
|
|
type: listener.type,
|
|
handler: handler,
|
|
};
|
|
|
|
handlers.push(eventInfo);
|
|
}
|
|
|
|
// If this is checking if a node has any listeners then none were found so
|
|
// return false.
|
|
if (checkOnly) {
|
|
return false;
|
|
}
|
|
|
|
return handlers;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or detect jQuery events.
|
|
*/
|
|
class JQueryEventCollector extends MainEventCollector {
|
|
/* eslint-disable complexity */
|
|
getListeners(node, { checkOnly } = {}) {
|
|
const jQuery = this.getJQuery(node);
|
|
const handlers = [];
|
|
|
|
// If jQuery is not on the page, if this is an anonymous node or a pseudo
|
|
// element we need to return early.
|
|
if (
|
|
!jQuery ||
|
|
isNativeAnonymous(node) ||
|
|
isMarkerPseudoElement(node) ||
|
|
isBeforePseudoElement(node) ||
|
|
isAfterPseudoElement(node)
|
|
) {
|
|
if (checkOnly) {
|
|
return false;
|
|
}
|
|
return handlers;
|
|
}
|
|
|
|
let eventsObj = null;
|
|
const data = jQuery._data || jQuery.data;
|
|
|
|
if (data) {
|
|
// jQuery 1.2+
|
|
try {
|
|
eventsObj = data(node, "events");
|
|
} catch (e) {
|
|
// We have no access to a JS object. This is probably due to a CORS
|
|
// violation. Using try / catch is the only way to avoid this error.
|
|
}
|
|
} else {
|
|
// JQuery 1.0 & 1.1
|
|
let entry;
|
|
try {
|
|
entry = entry = jQuery(node)[0];
|
|
} catch (e) {
|
|
// We have no access to a JS object. This is probably due to a CORS
|
|
// violation. Using try / catch is the only way to avoid this error.
|
|
}
|
|
|
|
if (!entry || !entry.events) {
|
|
if (checkOnly) {
|
|
return false;
|
|
}
|
|
return handlers;
|
|
}
|
|
|
|
eventsObj = entry.events;
|
|
}
|
|
|
|
if (eventsObj) {
|
|
for (const [type, events] of Object.entries(eventsObj)) {
|
|
for (const [, event] of Object.entries(events)) {
|
|
// Skip events that are part of jQueries internals.
|
|
if (node.nodeType == node.DOCUMENT_NODE && event.selector) {
|
|
continue;
|
|
}
|
|
|
|
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: handler,
|
|
tags: "jQuery",
|
|
hide: {
|
|
capturing: true,
|
|
dom0: true,
|
|
},
|
|
};
|
|
|
|
handlers.push(eventInfo);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (checkOnly) {
|
|
return false;
|
|
}
|
|
return handlers;
|
|
}
|
|
/* eslint-enable complexity */
|
|
}
|
|
|
|
/**
|
|
* Get or detect jQuery live events.
|
|
*/
|
|
class JQueryLiveEventCollector extends MainEventCollector {
|
|
/* eslint-disable complexity */
|
|
getListeners(node, { checkOnly } = {}) {
|
|
const jQuery = this.getJQuery(node);
|
|
const handlers = [];
|
|
|
|
if (!jQuery) {
|
|
if (checkOnly) {
|
|
return false;
|
|
}
|
|
return handlers;
|
|
}
|
|
|
|
const data = jQuery._data || jQuery.data;
|
|
|
|
if (data) {
|
|
// Live events are added to the document and bubble up to all elements.
|
|
// Any element matching the specified selector will trigger the live
|
|
// event.
|
|
const win = this.unwrap(node.ownerGlobal);
|
|
let events = null;
|
|
|
|
try {
|
|
events = data(win.document, "events");
|
|
} catch (e) {
|
|
// We have no access to a JS object. This is probably due to a CORS
|
|
// violation. Using try / catch is the only way to avoid this error.
|
|
}
|
|
|
|
if (events) {
|
|
for (const [, eventHolder] of Object.entries(events)) {
|
|
for (const [idx, event] of Object.entries(eventHolder)) {
|
|
if (typeof idx !== "string" || isNaN(parseInt(idx, 10))) {
|
|
continue;
|
|
}
|
|
|
|
let selector = event.selector;
|
|
|
|
if (!selector && event.data) {
|
|
selector = event.data.selector || event.data || event.selector;
|
|
}
|
|
|
|
if (!selector || !node.ownerDocument) {
|
|
continue;
|
|
}
|
|
|
|
let matches;
|
|
try {
|
|
matches = node.matches && node.matches(selector);
|
|
} catch (e) {
|
|
// Invalid selector, do nothing.
|
|
}
|
|
|
|
if (!matches) {
|
|
continue;
|
|
}
|
|
|
|
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: handler,
|
|
tags: "jQuery,Live",
|
|
hide: {
|
|
dom0: true,
|
|
capturing: true,
|
|
},
|
|
};
|
|
|
|
if (!eventInfo.type && event.data && event.data.live) {
|
|
eventInfo.type = event.data.live;
|
|
}
|
|
|
|
handlers.push(eventInfo);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (checkOnly) {
|
|
return false;
|
|
}
|
|
return handlers;
|
|
}
|
|
/* eslint-enable complexity */
|
|
|
|
normalizeListener(handlerDO) {
|
|
function isFunctionInProxy(funcDO) {
|
|
// If the anonymous function is inside the |proxy| function and the
|
|
// function only has guessed atom, the guessed atom should starts with
|
|
// "proxy/".
|
|
const displayName = funcDO.displayName;
|
|
if (displayName && displayName.startsWith("proxy/")) {
|
|
return true;
|
|
}
|
|
|
|
// If the anonymous function is inside the |proxy| function and the
|
|
// function gets name at compile time by SetFunctionName, its guessed
|
|
// atom doesn't contain "proxy/". In that case, check if the caller is
|
|
// "proxy" function, as a fallback.
|
|
const calleeDO = funcDO.environment.callee;
|
|
if (!calleeDO) {
|
|
return false;
|
|
}
|
|
const calleeName = calleeDO.displayName;
|
|
return calleeName == "proxy";
|
|
}
|
|
|
|
function getFirstFunctionVariable(funcDO) {
|
|
// The handler function inside the |proxy| function should point the
|
|
// unwrapped function via environment variable.
|
|
const names = funcDO.environment.names();
|
|
for (const varName of names) {
|
|
const varDO = handlerDO.environment.getVariable(varName);
|
|
if (!varDO) {
|
|
continue;
|
|
}
|
|
if (varDO.class == "Function") {
|
|
return varDO;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (!isFunctionInProxy(handlerDO)) {
|
|
return handlerDO;
|
|
}
|
|
|
|
const MAX_NESTED_HANDLER_COUNT = 2;
|
|
for (let i = 0; i < MAX_NESTED_HANDLER_COUNT; i++) {
|
|
const funcDO = getFirstFunctionVariable(handlerDO);
|
|
if (!funcDO) {
|
|
return handlerDO;
|
|
}
|
|
|
|
handlerDO = funcDO;
|
|
if (isFunctionInProxy(handlerDO)) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return handlerDO;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or detect React events.
|
|
*/
|
|
class ReactEventCollector extends MainEventCollector {
|
|
getListeners(node, { checkOnly } = {}) {
|
|
const handlers = [];
|
|
const props = this.getProps(node);
|
|
|
|
if (props) {
|
|
for (const [name, prop] of Object.entries(props)) {
|
|
if (REACT_EVENT_NAMES.includes(name)) {
|
|
const listener = (prop && prop.__reactBoundMethod) || prop;
|
|
|
|
if (typeof listener !== "function") {
|
|
continue;
|
|
}
|
|
|
|
if (!this.chromeEnabled && this.isChromeHandler(listener)) {
|
|
continue;
|
|
}
|
|
|
|
if (checkOnly) {
|
|
return true;
|
|
}
|
|
|
|
const handler = {
|
|
type: name,
|
|
handler: listener,
|
|
tags: "React",
|
|
hide: {
|
|
dom0: true,
|
|
},
|
|
override: {
|
|
capturing: name.endsWith("Capture"),
|
|
},
|
|
};
|
|
|
|
handlers.push(handler);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (checkOnly) {
|
|
return false;
|
|
}
|
|
|
|
return handlers;
|
|
}
|
|
|
|
getProps(node) {
|
|
node = this.unwrap(node);
|
|
|
|
for (const key of Object.keys(node)) {
|
|
if (key.startsWith("__reactInternalInstance$")) {
|
|
const value = node[key];
|
|
if (value.memoizedProps) {
|
|
return value.memoizedProps; // React 16
|
|
}
|
|
return value && value._currentElement && value._currentElement.props; // React 15
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
normalizeListener(handlerDO, listener) {
|
|
let functionText = "";
|
|
|
|
if (handlerDO.boundTargetFunction) {
|
|
handlerDO = handlerDO.boundTargetFunction;
|
|
}
|
|
|
|
const introScript = handlerDO.script.source.introductionScript;
|
|
const script = handlerDO.script;
|
|
|
|
// If this is a Babel transpiled function we have no access to the
|
|
// source location so we need to hide the filename and debugger
|
|
// icon.
|
|
if (introScript && introScript.displayName.endsWith("/transform.run")) {
|
|
listener.hide.debugger = true;
|
|
listener.hide.filename = true;
|
|
|
|
if (!handlerDO.isArrowFunction) {
|
|
functionText += "function (";
|
|
} else {
|
|
functionText += "(";
|
|
}
|
|
|
|
functionText += handlerDO.parameterNames.join(", ");
|
|
|
|
functionText += ") {\n";
|
|
|
|
const scriptSource = script.source.text;
|
|
functionText += scriptSource.substr(
|
|
script.sourceStart,
|
|
script.sourceLength
|
|
);
|
|
|
|
listener.override.handler = functionText;
|
|
}
|
|
|
|
return handlerDO;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The exposed class responsible for gathering events.
|
|
*/
|
|
class EventCollector {
|
|
constructor(targetActor) {
|
|
this.targetActor = targetActor;
|
|
|
|
// The event collector array. Please preserve the order otherwise there will
|
|
// be multiple failing tests.
|
|
this.eventCollectors = [
|
|
new ReactEventCollector(),
|
|
new JQueryLiveEventCollector(),
|
|
new JQueryEventCollector(),
|
|
new DOMEventCollector(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Destructor (must be called manually).
|
|
*/
|
|
destroy() {
|
|
this.eventCollectors = null;
|
|
}
|
|
|
|
/**
|
|
* Iterate through all event collectors returning on the first found event.
|
|
*
|
|
* @param {DOMNode} node
|
|
* The node to be checked for events.
|
|
* @return {Boolean}
|
|
* True if the node has event listeners, false otherwise.
|
|
*/
|
|
hasEventListeners(node) {
|
|
for (const collector of this.eventCollectors) {
|
|
if (collector.hasListeners(node)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {DOMNode} node
|
|
* The node for which events are to be gathered.
|
|
* @return {Array}
|
|
* An array containing objects in the following format:
|
|
* {
|
|
* type: type, // e.g. "click"
|
|
* handler: handler, // The function called when event is triggered.
|
|
* tags: "jQuery", // Comma separated list of tags displayed
|
|
* // inside event bubble.
|
|
* hide: { // Flags for hiding certain properties.
|
|
* capturing: true,
|
|
* dom0: true,
|
|
* }
|
|
* }
|
|
*/
|
|
getEventListeners(node) {
|
|
const listenerArray = [];
|
|
let dbg;
|
|
if (!this.chromeEnabled) {
|
|
dbg = new Debugger();
|
|
} else {
|
|
// When the chrome pref is turned on, we may try to debug system compartments.
|
|
// But since bug 1517210, the server is also loaded using the system principal
|
|
// and so here, we have to ensure using a special Debugger instance, loaded
|
|
// in a compartment flagged with invisibleToDebugger=true. This helps the Debugger
|
|
// know about the precise boundary between debuggee and debugger code.
|
|
const ChromeDebugger = require("ChromeDebugger");
|
|
dbg = new ChromeDebugger();
|
|
}
|
|
|
|
for (const collector of this.eventCollectors) {
|
|
const listeners = collector.getListeners(node);
|
|
|
|
if (!listeners) {
|
|
continue;
|
|
}
|
|
|
|
for (const listener of listeners) {
|
|
if (collector.normalizeListener) {
|
|
listener.normalizeListener = collector.normalizeListener;
|
|
}
|
|
this.processHandlerForEvent(listenerArray, listener, dbg);
|
|
}
|
|
}
|
|
|
|
listenerArray.sort((a, b) => {
|
|
return a.type.localeCompare(b.type);
|
|
});
|
|
|
|
return listenerArray;
|
|
}
|
|
|
|
/**
|
|
* Process an event listener.
|
|
*
|
|
* @param {Array} listenerArray
|
|
* listenerArray contains all event objects that we have gathered
|
|
* so far.
|
|
* @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:
|
|
* {
|
|
* type: "click",
|
|
* handler: function() { doSomething() },
|
|
* origin: "http://www.mozilla.com",
|
|
* tags: tags,
|
|
* DOM0: true,
|
|
* capturing: true,
|
|
* hide: {
|
|
* DOM0: true
|
|
* },
|
|
* native: false
|
|
* }
|
|
*/
|
|
/* eslint-disable complexity */
|
|
processHandlerForEvent(listenerArray, listener, dbg) {
|
|
let globalDO;
|
|
|
|
try {
|
|
const { capturing, handler } = listener;
|
|
|
|
let listenerDO;
|
|
if (isReplaying) {
|
|
listenerDO = ReplayInspector.getDebuggerObject(handler);
|
|
} else {
|
|
const global = Cu.getGlobalForObject(handler);
|
|
|
|
// 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);
|
|
listenerDO = globalDO.makeDebuggeeValue(handler);
|
|
}
|
|
|
|
const { normalizeListener } = listener;
|
|
|
|
if (normalizeListener) {
|
|
listenerDO = normalizeListener(listenerDO, listener);
|
|
}
|
|
|
|
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 column = null;
|
|
let native = false;
|
|
let url = "";
|
|
let sourceActor = "";
|
|
|
|
// 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 (desc && desc.value) {
|
|
listenerDO = desc.value;
|
|
}
|
|
}
|
|
|
|
// If the listener is bound to a different context then we need to switch
|
|
// to the bound function.
|
|
if (listenerDO.isBoundFunction) {
|
|
listenerDO = listenerDO.boundTargetFunction;
|
|
}
|
|
|
|
const { isArrowFunction, name, script, parameterNames } = listenerDO;
|
|
|
|
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;
|
|
column = script.startColumn;
|
|
url = script.url;
|
|
const actor = this.targetActor.sources.getOrCreateSourceActor(
|
|
script.source
|
|
);
|
|
sourceActor = actor ? actor.actorID : null;
|
|
|
|
// 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 + (column === null ? "" : ":" + column));
|
|
}
|
|
|
|
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,
|
|
sourceActor,
|
|
};
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
/* eslint-enable complexity */
|
|
}
|
|
|
|
exports.EventCollector = EventCollector;
|