/* 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.
/** * 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 (typeofthis._chromeEnabled === "undefined") { this._chromeEnabled = Services.prefs.getBoolPref( "devtools.chrome.enabled"
);
}
returnthis._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) { returnthis.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: _checkOnly }) { thrownew 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) { const listeners = []; const listenersTargets = [];
for (const el of listenersTargets) { const elListeners = Services.els.getListenerInfoFor(el); if (!elListeners) { continue;
} for (const listener of elListeners) { const obj = this.unwrap(listener.listenerObject); if (!obj || !obj[EXCLUDED_LISTENER]) {
listeners.push(listener);
}
}
}
return listeners;
}
getJQuery(node) { if (Cu.isDeadWrapper(node)) { returnnull;
}
const global = this.unwrap(node.ownerGlobal); if (!global) { returnnull;
}
const hasJQuery = global.jQuery?.fn?.jquery;
if (hasJQuery) { return global.jQuery;
} returnnull;
}
// 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. returnfalse;
}
}
}
/** * 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);
}
} elseif (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) { returntrue;
}
// 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) { returnfalse;
} 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) { returnfalse;
} return handlers;
}
eventsObj = entry.events;
}
if (eventsObj) { for (const type in eventsObj) {
let events = eventsObj[type]; // We can get arrays or objects. When we get the latter, // the events are the object values. if (!Array.isArray(events)) {
events = Object.values(events);
} for (const event of 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) { returnfalse;
} return handlers;
}
}
/** * Get or detect jQuery live events.
*/ class JQueryLiveEventCollector extends MainEventCollector { // eslint-disable-next-line complexity
getListeners(node, { checkOnly } = {}) { const jQuery = this.getJQuery(node); const handlers = [];
if (!jQuery) { if (checkOnly) { returnfalse;
} return handlers;
}
const jqueryData = jQuery._data || jQuery.data;
if (jqueryData) { // 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 = jqueryData(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 && node.ownerDocument && node.matches) { for (const eventName in events) { const eventHolder = events[eventName]; for (const idx in eventHolder) { if (typeof idx !== "string" || isNaN(parseInt(idx, 10))) { continue;
}
const event = eventHolder[idx];
let { selector, data } = event;
let matches; try {
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 (!eventInfo.type && data?.live) {
eventInfo.type = event.data.live;
}
handlers.push(eventInfo);
}
}
}
}
}
if (checkOnly) { returnfalse;
} return handlers;
}
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/")) { returntrue;
}
// 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 calleeDS = funcDO.environment?.calleeScript; if (!calleeDS) { returnfalse;
} const calleeName = calleeDS.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 ? funcDO.environment.names() : []; for (const varName of names) { const varDO = handlerDO.environment
? handlerDO.environment.getVariable(varName)
: null; if (!varDO) { continue;
} if (varDO.class == "Function") { return varDO;
}
} returnnull;
}
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?.__reactBoundMethod || prop;
if (typeof listener !== "function") { continue;
}
if (!this.chromeEnabled && this.isChromeHandler(listener)) { continue;
}
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?._currentElement?.props; // React 15
}
} returnnull;
}
normalizeListener(handlerDO, listener) {
let functionText = "";
if (handlerDO.boundTargetFunction) {
handlerDO = handlerDO.boundTargetFunction;
}
const script = handlerDO.script; // Script might be undefined (eg for methods bound several times, see // https://bugzilla.mozilla.org/show_bug.cgi?id=1589658) const introScript = script?.source.introductionScript;
// 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;
/** * 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)) { returntrue;
}
}
returnfalse;
}
/** * We allow displaying chrome events if the page is chrome or if * `devtools.chrome.enabled = true`.
*/
get chromeEnabled() { if (typeofthis._chromeEnabled === "undefined") { this._chromeEnabled = Services.prefs.getBoolPref( "devtools.chrome.enabled"
);
}
returnthis._chromeEnabled;
}
/** * * @param {DOMNode} node * The node for which events are to be gathered. * @return {Array<Object>} * An array containing objects in the following format: * { * {String} type: The event type, e.g. "click" * {Function} handler: The function called when event is triggered. * {Boolean} enabled: Whether the listener is enabled or not (event listeners can * be disabled via the inspector) * {String} tags: Comma separated list of tags displayed inside event bubble (e.g. "JQuery") * {Object} hide: Flags for hiding certain properties. * {Boolean} capturing * } * {Boolean} native * {String|undefined} sourceActor: The sourceActor id of the event listener * {nsIEventListenerInfo|undefined} nsIEventListenerInfo * }
*/
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) { const eventObj = this.processHandlerForEvent(
listener,
dbg,
collector.normalizeListener
); if (eventObj) {
listenerArray.push(eventObj);
}
}
}
listenerArray.sort((a, b) => { return a.type.localeCompare(b.type);
});
return listenerArray;
}
/** * Process an event listener. * * @param {EventListener} listener * The event listener to process. * @param {Debugger} dbg * Debugger instance. * @param {Function|null} normalizeListener * An optional function that will be called to retrieve data about the listener. * It should be a *Collector method. * * @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, * capturing: true, * hide: { * capturing: true * }, * native: false, * enabled: true * sourceActor: "sourceActor.1234", * nsIEventListenerInfo: nsIEventListenerInfo {…}, * }
*/ // eslint-disable-next-line complexity
processHandlerForEvent(listener, dbg, normalizeListener) {
let globalDO;
let eventObj;
try { const { capturing, handler } = listener;
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);
let listenerDO = globalDO.makeDebuggeeValue(handler);
if (normalizeListener) {
listenerDO = normalizeListener(listenerDO, listener);
}
const hide = listener.hide || {}; const override = listener.override || {}; const tags = listener.tags || ""; const type = listener.type || ""; const enabled = !!listener.enabled;
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;
// 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 (script) { const scriptSource = script.source.text;
// NOTE: Debugger.Script.prototype.startColumn is 1-based. // Convert to 0-based, while keeping the wasm's column (1) as is. // (bug 1863878) const columnBase = script.format === "wasm" ? 0 : 1;
line = script.startLine;
column = script.startColumn - columnBase;
url = script.url; const actor = this.targetActor.sourcesManager.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) { const prefix = "function " + name + "()"; const paramString = parameterNames.join(", ");
if (functionSource.startsWith(prefix)) {
functionSource = functionSource.substr(prefix.length);
// 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 +
(line ? ":" + line + (column === null ? "" : ":" + column) : "");
}
// 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 (!sourceActor) {
eventObj.hide.debugger = true;
}
} finally { // Ensure that we always remove the debuggee. if (globalDO) {
dbg.removeDebuggee(globalDO);
}
}
return eventObj;
}
}
exports.EventCollector = EventCollector;
Messung V0.5
¤ Dauer der Verarbeitung: 0.4 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.