/* 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 parsers that are then used by developer tools in // order to find information about events affecting an HTML element. "use strict"; const {Cc, Ci, Cu} = require("chrome"); loader.lazyGetter(this, "eventListenerService", () => { return Cc["@mozilla.org/eventlistenerservice;1"] .getService(Ci.nsIEventListenerService); }); var parsers = [ { id: "jQuery events", getListeners: function (node) { let global = node.ownerGlobal.wrappedJSObject; let hasJQuery = global.jQuery && global.jQuery.fn && global.jQuery.fn.jquery; if (!hasJQuery) { return; } let jQuery = global.jQuery; let handlers = []; // jQuery 1.2+ let data = jQuery._data || jQuery.data; if (data) { let eventsObj = data(node, "events"); for (let type in eventsObj) { let events = eventsObj[type]; for (let key in events) { let event = events[key]; if (typeof event === "object" || typeof event === "function") { let eventInfo = { type: type, handler: event.handler || event, tags: "jQuery", hide: { capturing: true, dom0: true } }; handlers.push(eventInfo); } } } } // JQuery 1.0 & 1.1 let entry = jQuery(node)[0]; if (!entry) { return handlers; } for (let type in entry.events) { let events = entry.events[type]; for (let key in events) { if (typeof events[key] === "function") { let eventInfo = { type: type, handler: events[key], tags: "jQuery", hide: { capturing: true, dom0: true } }; handlers.push(eventInfo); } } } return handlers; } }, { id: "jQuery live events", hasListeners: function (node) { return jQueryLiveGetListeners(node, true); }, getListeners: function (node) { return jQueryLiveGetListeners(node, false); }, normalizeHandler: function (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/". let 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. let calleeDO = funcDO.environment.callee; if (!calleeDO) { return false; } let calleeName = calleeDO.displayName; return calleeName == "proxy"; } function getFirstFunctionVariable(funcDO) { // The handler function inside the |proxy| function should point the // unwrapped function via environment variable. let names = funcDO.environment.names(); for (let varName of names) { let 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++) { let funcDO = getFirstFunctionVariable(handlerDO); if (!funcDO) return handlerDO; handlerDO = funcDO; if (isFunctionInProxy(handlerDO)) { continue; } break; } return handlerDO; } }, { id: "DOM events", hasListeners: function (node) { let listeners; if (node.nodeName.toLowerCase() === "html") { listeners = eventListenerService.getListenerInfoFor(node.ownerGlobal) || []; } else { listeners = eventListenerService.getListenerInfoFor(node) || []; } for (let listener of listeners) { if (listener.listenerObject && listener.type) { return true; } } return false; }, getListeners: function (node) { let handlers = []; let listeners = eventListenerService.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 (let listenerObj of listeners) { let listener = listenerObj.listenerObject; // If there is no JS event listener skip this. if (!listener) { continue; } let eventInfo = { capturing: listenerObj.capturing, type: listenerObj.type, handler: listener }; handlers.push(eventInfo); } return handlers; } } ]; function jQueryLiveGetListeners(node, boolOnEventFound) { let global = node.ownerGlobal.wrappedJSObject; let hasJQuery = global.jQuery && global.jQuery.fn && global.jQuery.fn.jquery; if (!hasJQuery) { return; } let jQuery = global.jQuery; let handlers = []; let 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. let events = data(global.document, "events"); for (let type in events) { let eventHolder = events[type]; for (let idx in eventHolder) { if (typeof idx !== "string" || isNaN(parseInt(idx, 10))) { continue; } let event = eventHolder[idx]; 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 (boolOnEventFound && matches) { return true; } if (!matches) { continue; } if (!boolOnEventFound && (typeof event === "object" || typeof event === "function")) { let eventInfo = { type: event.origType || event.type.substr(selector.length + 1), handler: event.handler || event, 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 (boolOnEventFound) { return false; } return handlers; } this.EventParsers = function EventParsers() { if (this._eventParsers.size === 0) { for (let parserObj of parsers) { this.registerEventParser(parserObj); } } }; exports.EventParsers = EventParsers; EventParsers.prototype = { _eventParsers: new Map(), // NOTE: This is shared amongst all instances. get parsers() { return this._eventParsers; }, /** * Register a new event parser to be used in the processing of event info. * * @param {Object} parserObj * Each parser must contain the following properties: * - parser, which must take the following form: * { * id {String}: "jQuery events", // Unique id. * getListeners: function(node) { }, // Function that takes a node and * // returns an array of eventInfo * // objects (see below). * * hasListeners: function(node) { }, // Optional function that takes a * // node and returns a boolean * // indicating whether a node has * // listeners attached. * * normalizeHandler: function(fnDO) { }, // Optional function that takes a * // Debugger.Object instance and * // climbs the scope chain to get * // the function that should be * // displayed in the event bubble * // see the following url for * // details: * // https://developer.mozilla.org/ * // docs/Tools/Debugger-API/ * // Debugger.Object * } * * An eventInfo object should take the following form: * { * type {String}: "click", * handler {Function}: event handler, * tags {String}: "jQuery,Live", // These tags will be displayed as * // attributes in the events popup. * hide: { // Hide or show fields: * debugger: false, // Debugger icon * type: false, // Event type e.g. click * filename: false, // Filename * capturing: false, // Capturing * dom0: false // DOM 0 * }, * * override: { // The following can be overridden: * type: "click", * origin: "http://www.mozilla.com", * searchString: 'onclick="doSomething()"', * DOM0: true, * capturing: true * } * } */ registerEventParser: function (parserObj) { let parserId = parserObj.id; if (!parserId) { throw new Error("Cannot register new event parser with id " + parserId); } if (this._eventParsers.has(parserId)) { throw new Error("Duplicate event parser id " + parserId); } this._eventParsers.set(parserId, { getListeners: parserObj.getListeners, hasListeners: parserObj.hasListeners, normalizeHandler: parserObj.normalizeHandler }); }, /** * Removes parser that matches a given parserId. * * @param {String} parserId * id of the event parser to unregister. */ unregisterEventParser: function (parserId) { this._eventParsers.delete(parserId); }, /** * Tidy up parsers. */ destroy: function () { for (let [id] of this._eventParsers) { this.unregisterEventParser(id, true); } } };