tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

event-collector.js (30010B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 // This file contains event collectors that are then used by developer tools in
      6 // order to find information about events affecting an HTML element.
      7 
      8 "use strict";
      9 
     10 const Debugger = require("Debugger");
     11 const {
     12  EXCLUDED_LISTENER,
     13 } = require("resource://devtools/server/actors/inspector/constants.js");
     14 loader.lazyRequireGetter(
     15  this,
     16  "isUserDefinedEventName",
     17  "resource://devtools/server/actors/events/events.js",
     18  true
     19 );
     20 
     21 // eslint-disable-next-line
     22 const JQUERY_LIVE_REGEX =
     23  /return typeof \w+.*.event\.triggered[\s\S]*\.event\.(dispatch|handle).*arguments/;
     24 
     25 const REACT_EVENT_NAMES = [
     26  "onAbort",
     27  "onAnimationEnd",
     28  "onAnimationIteration",
     29  "onAnimationStart",
     30  "onAuxClick",
     31  "onBeforeInput",
     32  "onBlur",
     33  "onCanPlay",
     34  "onCanPlayThrough",
     35  "onCancel",
     36  "onChange",
     37  "onClick",
     38  "onClose",
     39  "onCompositionEnd",
     40  "onCompositionStart",
     41  "onCompositionUpdate",
     42  "onContextMenu",
     43  "onCopy",
     44  "onCut",
     45  "onDoubleClick",
     46  "onDrag",
     47  "onDragEnd",
     48  "onDragEnter",
     49  "onDragExit",
     50  "onDragLeave",
     51  "onDragOver",
     52  "onDragStart",
     53  "onDrop",
     54  "onDurationChange",
     55  "onEmptied",
     56  "onEncrypted",
     57  "onEnded",
     58  "onError",
     59  "onFocus",
     60  "onGotPointerCapture",
     61  "onInput",
     62  "onInvalid",
     63  "onKeyDown",
     64  "onKeyPress",
     65  "onKeyUp",
     66  "onLoad",
     67  "onLoadStart",
     68  "onLoadedData",
     69  "onLoadedMetadata",
     70  "onLostPointerCapture",
     71  "onMouseDown",
     72  "onMouseEnter",
     73  "onMouseLeave",
     74  "onMouseMove",
     75  "onMouseOut",
     76  "onMouseOver",
     77  "onMouseUp",
     78  "onPaste",
     79  "onPause",
     80  "onPlay",
     81  "onPlaying",
     82  "onPointerCancel",
     83  "onPointerDown",
     84  "onPointerEnter",
     85  "onPointerLeave",
     86  "onPointerMove",
     87  "onPointerOut",
     88  "onPointerOver",
     89  "onPointerUp",
     90  "onProgress",
     91  "onRateChange",
     92  "onReset",
     93  "onScroll",
     94  "onSeeked",
     95  "onSeeking",
     96  "onSelect",
     97  "onStalled",
     98  "onSubmit",
     99  "onSuspend",
    100  "onTimeUpdate",
    101  "onToggle",
    102  "onTouchCancel",
    103  "onTouchEnd",
    104  "onTouchMove",
    105  "onTouchStart",
    106  "onTransitionEnd",
    107  "onVolumeChange",
    108  "onWaiting",
    109  "onWheel",
    110  "onAbortCapture",
    111  "onAnimationEndCapture",
    112  "onAnimationIterationCapture",
    113  "onAnimationStartCapture",
    114  "onAuxClickCapture",
    115  "onBeforeInputCapture",
    116  "onBlurCapture",
    117  "onCanPlayCapture",
    118  "onCanPlayThroughCapture",
    119  "onCancelCapture",
    120  "onChangeCapture",
    121  "onClickCapture",
    122  "onCloseCapture",
    123  "onCompositionEndCapture",
    124  "onCompositionStartCapture",
    125  "onCompositionUpdateCapture",
    126  "onContextMenuCapture",
    127  "onCopyCapture",
    128  "onCutCapture",
    129  "onDoubleClickCapture",
    130  "onDragCapture",
    131  "onDragEndCapture",
    132  "onDragEnterCapture",
    133  "onDragExitCapture",
    134  "onDragLeaveCapture",
    135  "onDragOverCapture",
    136  "onDragStartCapture",
    137  "onDropCapture",
    138  "onDurationChangeCapture",
    139  "onEmptiedCapture",
    140  "onEncryptedCapture",
    141  "onEndedCapture",
    142  "onErrorCapture",
    143  "onFocusCapture",
    144  "onGotPointerCaptureCapture",
    145  "onInputCapture",
    146  "onInvalidCapture",
    147  "onKeyDownCapture",
    148  "onKeyPressCapture",
    149  "onKeyUpCapture",
    150  "onLoadCapture",
    151  "onLoadStartCapture",
    152  "onLoadedDataCapture",
    153  "onLoadedMetadataCapture",
    154  "onLostPointerCaptureCapture",
    155  "onMouseDownCapture",
    156  "onMouseEnterCapture",
    157  "onMouseLeaveCapture",
    158  "onMouseMoveCapture",
    159  "onMouseOutCapture",
    160  "onMouseOverCapture",
    161  "onMouseUpCapture",
    162  "onPasteCapture",
    163  "onPauseCapture",
    164  "onPlayCapture",
    165  "onPlayingCapture",
    166  "onPointerCancelCapture",
    167  "onPointerDownCapture",
    168  "onPointerEnterCapture",
    169  "onPointerLeaveCapture",
    170  "onPointerMoveCapture",
    171  "onPointerOutCapture",
    172  "onPointerOverCapture",
    173  "onPointerUpCapture",
    174  "onProgressCapture",
    175  "onRateChangeCapture",
    176  "onResetCapture",
    177  "onScrollCapture",
    178  "onSeekedCapture",
    179  "onSeekingCapture",
    180  "onSelectCapture",
    181  "onStalledCapture",
    182  "onSubmitCapture",
    183  "onSuspendCapture",
    184  "onTimeUpdateCapture",
    185  "onToggleCapture",
    186  "onTouchCancelCapture",
    187  "onTouchEndCapture",
    188  "onTouchMoveCapture",
    189  "onTouchStartCapture",
    190  "onTransitionEndCapture",
    191  "onVolumeChangeCapture",
    192  "onWaitingCapture",
    193  "onWheelCapture",
    194 ];
    195 
    196 /**
    197 * The base class that all the enent collectors should be based upon.
    198 */
    199 class MainEventCollector {
    200  /**
    201   * We allow displaying chrome events if the page is chrome or if
    202   * `devtools.chrome.enabled = true`.
    203   */
    204  get chromeEnabled() {
    205    if (typeof this._chromeEnabled === "undefined") {
    206      this._chromeEnabled = Services.prefs.getBoolPref(
    207        "devtools.chrome.enabled"
    208      );
    209    }
    210 
    211    return this._chromeEnabled;
    212  }
    213 
    214  /**
    215   * Check if a node has any event listeners attached. Please do not override
    216   * this method... your getListeners() implementation needs to have the
    217   * following signature:
    218   * `getListeners(node, {checkOnly} = {})`
    219   *
    220   * @param  {DOMNode} node
    221   *         The not for which we want to check for event listeners.
    222   * @return {boolean}
    223   *         true if the node has event listeners, false otherwise.
    224   */
    225  hasListeners(node) {
    226    return this.getListeners(node, {
    227      checkOnly: true,
    228    });
    229  }
    230 
    231  /**
    232   * Get all listeners for a node. This method must be overridden.
    233   *
    234   * @param  {DOMNode} node
    235   *         The not for which we want to get event listeners.
    236   * @param  {object} options
    237   *         An object for passing in options.
    238   * @param  {boolean} [options.checkOnly = false]
    239   *         Don't get any listeners but return true when the first event is
    240   *         found.
    241   * @return {Array}
    242   *         An array of event handlers.
    243   */
    244  getListeners(_node, { checkOnly: _checkOnly }) {
    245    throw new Error("You have to implement the method getListeners()!");
    246  }
    247 
    248  /**
    249   * Get unfiltered DOM Event listeners for a node.
    250   * NOTE: These listeners may contain invalid events and events based
    251   *       on C++ rather than JavaScript.
    252   *
    253   * @param  {DOMNode} node
    254   *         The node for which we want to get unfiltered event listeners.
    255   * @return {Array}
    256   *         An array of unfiltered event listeners or an empty array
    257   */
    258  getDOMListeners(node) {
    259    const listeners = [];
    260    const listenersTargets = [];
    261 
    262    if (
    263      typeof node.nodeName !== "undefined" &&
    264      node.nodeName.toLowerCase() === "html"
    265    ) {
    266      listenersTargets.push(node.ownerGlobal, node, node.parentNode);
    267    } else {
    268      listenersTargets.push(node);
    269    }
    270 
    271    for (const el of listenersTargets) {
    272      const elListeners = Services.els.getListenerInfoFor(el);
    273      if (!elListeners) {
    274        continue;
    275      }
    276      for (const listener of elListeners) {
    277        const obj = this.unwrap(listener.listenerObject);
    278        if (!obj || !obj[EXCLUDED_LISTENER]) {
    279          listeners.push(listener);
    280        }
    281      }
    282    }
    283 
    284    return listeners;
    285  }
    286 
    287  getJQuery(node) {
    288    if (Cu.isDeadWrapper(node)) {
    289      return null;
    290    }
    291 
    292    const global = this.unwrap(node.ownerGlobal);
    293    if (!global) {
    294      return null;
    295    }
    296 
    297    const hasJQuery = global.jQuery?.fn?.jquery;
    298 
    299    if (hasJQuery) {
    300      return global.jQuery;
    301    }
    302    return null;
    303  }
    304 
    305  unwrap(obj) {
    306    return Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
    307  }
    308 
    309  isChromeHandler(handler) {
    310    try {
    311      const handlerPrincipal = Cu.getObjectPrincipal(handler);
    312 
    313      // Chrome codebase may register listeners on the page from a frame script or
    314      // JSM <video> tags may also report internal listeners, but they won't be
    315      // coming from the system principal. Instead, they will be using an expanded
    316      // principal.
    317      return (
    318        handlerPrincipal.isSystemPrincipal ||
    319        handlerPrincipal.isExpandedPrincipal
    320      );
    321    } catch (e) {
    322      // Anything from a dead object to a CSP error can leave us here so let's
    323      // return false so that we can fail gracefully.
    324      return false;
    325    }
    326  }
    327 }
    328 
    329 /**
    330 * Get or detect DOM events. These may include DOM events created by libraries
    331 * that enable their custom events to work. At this point we are unable to
    332 * effectively filter them as they may be proxied or wrapped. Although we know
    333 * there is an event, we may not know the true contents until it goes
    334 * through `processHandlerForEvent()`.
    335 */
    336 class DOMEventCollector extends MainEventCollector {
    337  getListeners(node, { checkOnly } = {}) {
    338    const handlers = [];
    339    const listeners = this.getDOMListeners(node);
    340 
    341    for (const listener of listeners) {
    342      // Ignore listeners without a type, e.g.
    343      // node.addEventListener("", function() {})
    344      if (!listener.type) {
    345        continue;
    346      }
    347 
    348      // Get the listener object, either a Function or an Object.
    349      const obj = listener.listenerObject;
    350 
    351      // Ignore listeners without any listener, e.g.
    352      // node.addEventListener("mouseover", null);
    353      if (!obj) {
    354        continue;
    355      }
    356 
    357      let handler = null;
    358 
    359      // An object without a valid handleEvent is not a valid listener.
    360      if (typeof obj === "object") {
    361        const unwrapped = this.unwrap(obj);
    362        if (typeof unwrapped.handleEvent === "function") {
    363          handler = Cu.unwaiveXrays(unwrapped.handleEvent);
    364        }
    365      } else if (typeof obj === "function") {
    366        // Ignore DOM events used to trigger jQuery events as they are only
    367        // useful to the developers of the jQuery library.
    368        if (JQUERY_LIVE_REGEX.test(obj.toString())) {
    369          continue;
    370        }
    371        // Otherwise, the other valid listener type is function.
    372        handler = obj;
    373      }
    374 
    375      // Ignore listeners that have no handler.
    376      if (!handler) {
    377        continue;
    378      }
    379 
    380      // If we shouldn't be showing chrome events due to context and this is a
    381      // chrome handler we can ignore it.
    382      if (!this.chromeEnabled && this.isChromeHandler(handler)) {
    383        continue;
    384      }
    385 
    386      // If this is checking if a node has any listeners then we have found one
    387      // so return now.
    388      if (checkOnly) {
    389        return true;
    390      }
    391 
    392      const eventInfo = {
    393        nsIEventListenerInfo: listener,
    394        capturing: listener.capturing,
    395        type: listener.type,
    396        handler,
    397        enabled: listener.enabled,
    398      };
    399 
    400      handlers.push(eventInfo);
    401    }
    402 
    403    // If this is checking if a node has any listeners then none were found so
    404    // return false.
    405    if (checkOnly) {
    406      return false;
    407    }
    408 
    409    return handlers;
    410  }
    411 }
    412 
    413 /**
    414 * Get or detect jQuery events.
    415 */
    416 class JQueryEventCollector extends MainEventCollector {
    417  // eslint-disable-next-line complexity
    418  getListeners(node, { checkOnly } = {}) {
    419    const jQuery = this.getJQuery(node);
    420    const handlers = [];
    421 
    422    if (!jQuery || node.isNativeAnonymous) {
    423      if (checkOnly) {
    424        return false;
    425      }
    426      return handlers;
    427    }
    428 
    429    let eventsObj = null;
    430    const data = jQuery._data || jQuery.data;
    431 
    432    if (data) {
    433      // jQuery 1.2+
    434      try {
    435        eventsObj = data(node, "events");
    436      } catch (e) {
    437        // We have no access to a JS object. This is probably due to a CORS
    438        // violation. Using try / catch is the only way to avoid this error.
    439      }
    440    } else {
    441      // JQuery 1.0 & 1.1
    442      let entry;
    443      try {
    444        entry = entry = jQuery(node)[0];
    445      } catch (e) {
    446        // We have no access to a JS object. This is probably due to a CORS
    447        // violation. Using try / catch is the only way to avoid this error.
    448      }
    449 
    450      if (!entry || !entry.events) {
    451        if (checkOnly) {
    452          return false;
    453        }
    454        return handlers;
    455      }
    456 
    457      eventsObj = entry.events;
    458    }
    459 
    460    if (eventsObj) {
    461      for (const type in eventsObj) {
    462        let events = eventsObj[type];
    463        // We can get arrays or objects. When we get the latter,
    464        // the events are the object values.
    465        if (!Array.isArray(events)) {
    466          events = Object.values(events);
    467        }
    468        for (const event of events) {
    469          // Skip events that are part of jQueries internals.
    470          if (node.nodeType == node.DOCUMENT_NODE && event.selector) {
    471            continue;
    472          }
    473 
    474          if (typeof event === "function" || typeof event === "object") {
    475            // If we shouldn't be showing chrome events due to context and this
    476            // is a chrome handler we can ignore it.
    477            const handler = event.handler || event;
    478            if (!this.chromeEnabled && this.isChromeHandler(handler)) {
    479              continue;
    480            }
    481 
    482            if (checkOnly) {
    483              return true;
    484            }
    485 
    486            const eventInfo = {
    487              type,
    488              handler,
    489              tags: "jQuery",
    490              hide: {
    491                capturing: true,
    492              },
    493            };
    494 
    495            handlers.push(eventInfo);
    496          }
    497        }
    498      }
    499    }
    500 
    501    if (checkOnly) {
    502      return false;
    503    }
    504    return handlers;
    505  }
    506 }
    507 
    508 /**
    509 * Get or detect jQuery live events.
    510 */
    511 class JQueryLiveEventCollector extends MainEventCollector {
    512  // eslint-disable-next-line complexity
    513  getListeners(node, { checkOnly } = {}) {
    514    const jQuery = this.getJQuery(node);
    515    const handlers = [];
    516 
    517    if (!jQuery) {
    518      if (checkOnly) {
    519        return false;
    520      }
    521      return handlers;
    522    }
    523 
    524    const jqueryData = jQuery._data || jQuery.data;
    525 
    526    if (jqueryData) {
    527      // Live events are added to the document and bubble up to all elements.
    528      // Any element matching the specified selector will trigger the live
    529      // event.
    530      const win = this.unwrap(node.ownerGlobal);
    531      let events = null;
    532 
    533      try {
    534        events = jqueryData(win.document, "events");
    535      } catch (e) {
    536        // We have no access to a JS object. This is probably due to a CORS
    537        // violation. Using try / catch is the only way to avoid this error.
    538      }
    539 
    540      if (events && node.ownerDocument && node.matches) {
    541        for (const eventName in events) {
    542          const eventHolder = events[eventName];
    543          for (const idx in eventHolder) {
    544            if (typeof idx !== "string" || isNaN(parseInt(idx, 10))) {
    545              continue;
    546            }
    547 
    548            const event = eventHolder[idx];
    549            let { selector, data } = event;
    550 
    551            if (!selector && data) {
    552              selector = data.selector || data;
    553            }
    554 
    555            if (!selector) {
    556              continue;
    557            }
    558 
    559            let matches;
    560            try {
    561              matches = node.matches(selector);
    562            } catch (e) {
    563              // Invalid selector, do nothing.
    564            }
    565 
    566            if (!matches) {
    567              continue;
    568            }
    569 
    570            if (typeof event === "function" || typeof event === "object") {
    571              // If we shouldn't be showing chrome events due to context and this
    572              // is a chrome handler we can ignore it.
    573              const handler = event.handler || event;
    574              if (!this.chromeEnabled && this.isChromeHandler(handler)) {
    575                continue;
    576              }
    577 
    578              if (checkOnly) {
    579                return true;
    580              }
    581              const eventInfo = {
    582                type: event.origType || event.type.substr(selector.length + 1),
    583                handler,
    584                tags: "jQuery,Live",
    585                hide: {
    586                  capturing: true,
    587                },
    588              };
    589 
    590              if (!eventInfo.type && data?.live) {
    591                eventInfo.type = event.data.live;
    592              }
    593 
    594              handlers.push(eventInfo);
    595            }
    596          }
    597        }
    598      }
    599    }
    600 
    601    if (checkOnly) {
    602      return false;
    603    }
    604    return handlers;
    605  }
    606 
    607  normalizeListener(handlerDO) {
    608    function isFunctionInProxy(funcDO) {
    609      // If the anonymous function is inside the |proxy| function and the
    610      // function only has guessed atom, the guessed atom should starts with
    611      // "proxy/".
    612      const displayName = funcDO.displayName;
    613      if (displayName && displayName.startsWith("proxy/")) {
    614        return true;
    615      }
    616 
    617      // If the anonymous function is inside the |proxy| function and the
    618      // function gets name at compile time by SetFunctionName, its guessed
    619      // atom doesn't contain "proxy/".  In that case, check if the caller is
    620      // "proxy" function, as a fallback.
    621      const calleeDS = funcDO.environment?.calleeScript;
    622      if (!calleeDS) {
    623        return false;
    624      }
    625      const calleeName = calleeDS.displayName;
    626      return calleeName == "proxy";
    627    }
    628 
    629    function getFirstFunctionVariable(funcDO) {
    630      // The handler function inside the |proxy| function should point the
    631      // unwrapped function via environment variable.
    632      const names = funcDO.environment ? funcDO.environment.names() : [];
    633      for (const varName of names) {
    634        const varDO = handlerDO.environment
    635          ? handlerDO.environment.getVariable(varName)
    636          : null;
    637        if (!varDO) {
    638          continue;
    639        }
    640        if (varDO.class == "Function") {
    641          return varDO;
    642        }
    643      }
    644      return null;
    645    }
    646 
    647    if (!isFunctionInProxy(handlerDO)) {
    648      return handlerDO;
    649    }
    650 
    651    const MAX_NESTED_HANDLER_COUNT = 2;
    652    for (let i = 0; i < MAX_NESTED_HANDLER_COUNT; i++) {
    653      const funcDO = getFirstFunctionVariable(handlerDO);
    654      if (!funcDO) {
    655        return handlerDO;
    656      }
    657 
    658      handlerDO = funcDO;
    659      if (isFunctionInProxy(handlerDO)) {
    660        continue;
    661      }
    662      break;
    663    }
    664 
    665    return handlerDO;
    666  }
    667 }
    668 
    669 /**
    670 * Get or detect React events.
    671 */
    672 class ReactEventCollector extends MainEventCollector {
    673  getListeners(node, { checkOnly } = {}) {
    674    const handlers = [];
    675    const props = this.getProps(node);
    676 
    677    if (props) {
    678      for (const [name, prop] of Object.entries(props)) {
    679        if (REACT_EVENT_NAMES.includes(name)) {
    680          const listener = prop?.__reactBoundMethod || prop;
    681 
    682          if (typeof listener !== "function") {
    683            continue;
    684          }
    685 
    686          if (!this.chromeEnabled && this.isChromeHandler(listener)) {
    687            continue;
    688          }
    689 
    690          if (checkOnly) {
    691            return true;
    692          }
    693 
    694          const handler = {
    695            type: name,
    696            handler: listener,
    697            tags: "React",
    698            override: {
    699              capturing: name.endsWith("Capture"),
    700            },
    701          };
    702 
    703          handlers.push(handler);
    704        }
    705      }
    706    }
    707 
    708    if (checkOnly) {
    709      return false;
    710    }
    711 
    712    return handlers;
    713  }
    714 
    715  getProps(node) {
    716    node = this.unwrap(node);
    717 
    718    for (const key of Object.keys(node)) {
    719      if (key.startsWith("__reactInternalInstance$")) {
    720        const value = node[key];
    721        if (value.memoizedProps) {
    722          return value.memoizedProps; // React 16
    723        }
    724        return value?._currentElement?.props; // React 15
    725      }
    726    }
    727    return null;
    728  }
    729 
    730  normalizeListener(handlerDO, listener) {
    731    let functionText = "";
    732 
    733    if (handlerDO.boundTargetFunction) {
    734      handlerDO = handlerDO.boundTargetFunction;
    735    }
    736 
    737    const script = handlerDO.script;
    738    // Script might be undefined (eg for methods bound several times, see
    739    // https://bugzilla.mozilla.org/show_bug.cgi?id=1589658)
    740    const introScript = script?.source.introductionScript;
    741 
    742    // If this is a Babel transpiled function we have no access to the
    743    // source location so we need to hide the filename and debugger
    744    // icon.
    745    if (introScript && introScript.displayName.endsWith("/transform.run")) {
    746      listener.hide.debugger = true;
    747      listener.hide.filename = true;
    748 
    749      if (!handlerDO.isArrowFunction) {
    750        functionText += "function (";
    751      } else {
    752        functionText += "(";
    753      }
    754 
    755      functionText += handlerDO.parameterNames.join(", ");
    756 
    757      functionText += ") {\n";
    758 
    759      const scriptSource = script.source.text;
    760      functionText += scriptSource.substr(
    761        script.sourceStart,
    762        script.sourceLength
    763      );
    764 
    765      listener.override.handler = functionText;
    766    }
    767 
    768    return handlerDO;
    769  }
    770 }
    771 
    772 /**
    773 * The exposed class responsible for gathering events.
    774 */
    775 class EventCollector {
    776  constructor(targetActor) {
    777    this.targetActor = targetActor;
    778 
    779    // The event collector array. Please preserve the order otherwise there will
    780    // be multiple failing tests.
    781    this.eventCollectors = [
    782      new ReactEventCollector(),
    783      new JQueryLiveEventCollector(),
    784      new JQueryEventCollector(),
    785      new DOMEventCollector(),
    786    ];
    787  }
    788 
    789  /**
    790   * Destructor (must be called manually).
    791   */
    792  destroy() {
    793    this.eventCollectors = null;
    794  }
    795 
    796  /**
    797   * Iterate through all event collectors returning on the first found event.
    798   *
    799   * @param  {DOMNode} node
    800   *         The node to be checked for events.
    801   * @return {boolean}
    802   *         True if the node has event listeners, false otherwise.
    803   */
    804  hasEventListeners(node) {
    805    for (const collector of this.eventCollectors) {
    806      if (collector.hasListeners(node)) {
    807        return true;
    808      }
    809    }
    810 
    811    return false;
    812  }
    813 
    814  /**
    815   * We allow displaying chrome events if the page is chrome or if
    816   * `devtools.chrome.enabled = true`.
    817   */
    818  get chromeEnabled() {
    819    if (typeof this._chromeEnabled === "undefined") {
    820      this._chromeEnabled = Services.prefs.getBoolPref(
    821        "devtools.chrome.enabled"
    822      );
    823    }
    824 
    825    return this._chromeEnabled;
    826  }
    827 
    828  /**
    829   *
    830   * @param  {DOMNode} node
    831   *         The node for which events are to be gathered.
    832   * @return {Array<object>}
    833   *         An array containing objects in the following format:
    834   *         {
    835   *           {String} type: The event type, e.g. "click"
    836   *           {Function} handler: The function called when event is triggered.
    837   *           {Boolean} enabled: Whether the listener is enabled or not (event listeners can
    838   *                     be disabled via the inspector)
    839   *           {String} tags: Comma separated list of tags displayed inside event bubble (e.g. "JQuery")
    840   *           {Object} hide: Flags for hiding certain properties.
    841   *             {Boolean} capturing
    842   *           }
    843   *           {Boolean} native
    844   *           {String|undefined} sourceActor: The sourceActor id of the event listener
    845   *           {nsIEventListenerInfo|undefined} nsIEventListenerInfo
    846   *         }
    847   */
    848  getEventListeners(node) {
    849    const listenerArray = [];
    850    let dbg;
    851    if (!this.chromeEnabled) {
    852      dbg = new Debugger();
    853    } else {
    854      // When the chrome pref is turned on, we may inspect DOM Elements from
    855      // privileged documents.
    856      // But since bug 1517210, the DevTools Server is also loaded in the shared
    857      // privileged global.
    858      // As the Debugger API requires to be used from distinct compartments
    859      // between the debuggee and the debugger modules,
    860      // we have to ensure spawning it from a distinct compartment.
    861      // That's what ChromeDebugger builtin module does.
    862      const ChromeDebugger = require("ChromeDebugger");
    863      dbg = new ChromeDebugger();
    864    }
    865 
    866    for (const collector of this.eventCollectors) {
    867      const listeners = collector.getListeners(node);
    868 
    869      if (!listeners) {
    870        continue;
    871      }
    872 
    873      for (const listener of listeners) {
    874        const eventObj = this.processHandlerForEvent(
    875          listener,
    876          dbg,
    877          collector.normalizeListener
    878        );
    879        if (eventObj) {
    880          listenerArray.push(eventObj);
    881        }
    882      }
    883    }
    884 
    885    listenerArray.sort((a, b) => {
    886      return a.type.localeCompare(b.type);
    887    });
    888 
    889    return listenerArray;
    890  }
    891 
    892  /**
    893   * Process an event listener.
    894   *
    895   * @param  {EventListener} listener
    896   *         The event listener to process.
    897   * @param  {Debugger} dbg
    898   *         Debugger instance.
    899   * @param  {Function|null} normalizeListener
    900   *         An optional function that will be called to retrieve data about the listener.
    901   *         It should be a *Collector method.
    902   *
    903   * @return {Array}
    904   *         An array of objects where a typical object looks like this:
    905   *           {
    906   *             type: "click",
    907   *             handler: function() { doSomething() },
    908   *             origin: "http://www.mozilla.com",
    909   *             tags: tags,
    910   *             capturing: true,
    911   *             hide: {
    912   *               capturing: true
    913   *             },
    914   *             native: false,
    915   *             enabled: true
    916   *             sourceActor: "sourceActor.1234",
    917   *             nsIEventListenerInfo: nsIEventListenerInfo {…},
    918   *             isUserDefined: false,
    919   *           }
    920   */
    921  // eslint-disable-next-line complexity
    922  processHandlerForEvent(listener, dbg, normalizeListener) {
    923    let globalDO;
    924    let eventObj;
    925 
    926    try {
    927      const { capturing, handler } = listener;
    928 
    929      const global = Cu.getGlobalForObject(handler);
    930 
    931      // It is important that we recreate the globalDO for each handler because
    932      // their global object can vary e.g. resource:// URLs on a video control. If
    933      // we don't do this then all chrome listeners simply display "native code."
    934      globalDO = dbg.addDebuggee(global);
    935      let listenerDO = globalDO.makeDebuggeeValue(handler);
    936 
    937      if (normalizeListener) {
    938        listenerDO = normalizeListener(listenerDO, listener);
    939      }
    940 
    941      const hide = listener.hide || {};
    942      const override = listener.override || {};
    943      const tags = listener.tags || "";
    944      const type = override.type || listener.type || "";
    945      const enabled = !!listener.enabled;
    946      let functionSource = handler.toString();
    947      let line = 0;
    948      let column = null;
    949      let native = false;
    950      let url = "";
    951      let sourceActor = "";
    952 
    953      // If the listener is an object with a 'handleEvent' method, use that.
    954      if (
    955        listenerDO.class === "Object" ||
    956        /^XUL\w*Element$/.test(listenerDO.class)
    957      ) {
    958        let desc;
    959 
    960        while (!desc && listenerDO) {
    961          desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
    962          listenerDO = listenerDO.proto;
    963        }
    964 
    965        if (desc?.value) {
    966          listenerDO = desc.value;
    967        }
    968      }
    969 
    970      // If the listener is bound to a different context then we need to switch
    971      // to the bound function.
    972      if (listenerDO.isBoundFunction) {
    973        listenerDO = listenerDO.boundTargetFunction;
    974      }
    975 
    976      const { isArrowFunction, name, script, parameterNames } = listenerDO;
    977 
    978      if (script) {
    979        const scriptSource = script.source.text;
    980 
    981        // NOTE: Debugger.Script.prototype.startColumn is 1-based.
    982        //       Convert to 0-based, while keeping the wasm's column (1) as is.
    983        //       (bug 1863878)
    984        const columnBase = script.format === "wasm" ? 0 : 1;
    985 
    986        line = script.startLine;
    987        column = script.startColumn - columnBase;
    988        url = script.url;
    989        const actor = this.targetActor.sourcesManager.getOrCreateSourceActor(
    990          script.source
    991        );
    992        sourceActor = actor ? actor.actorID : null;
    993 
    994        // Checking for the string "[native code]" is the only way at this point
    995        // to check for native code. Even if this provides a false positive then
    996        // grabbing the source code a second time is harmless.
    997        if (
    998          functionSource === "[object Object]" ||
    999          functionSource === "[object XULElement]" ||
   1000          functionSource.includes("[native code]")
   1001        ) {
   1002          functionSource = scriptSource.substr(
   1003            script.sourceStart,
   1004            script.sourceLength
   1005          );
   1006 
   1007          // At this point the script looks like this:
   1008          // () { ... }
   1009          // We prefix this with "function" if it is not a fat arrow function.
   1010          if (!isArrowFunction) {
   1011            functionSource = "function " + functionSource;
   1012          }
   1013        }
   1014      } else {
   1015        // If the listener is a native one (provided by C++ code) then we have no
   1016        // access to the script. We use the native flag to prevent showing the
   1017        // debugger button because the script is not available.
   1018        native = true;
   1019      }
   1020 
   1021      // Arrow function text always contains the parameters. Function
   1022      // parameters are often missing e.g. if Array.sort is used as a handler.
   1023      // If they are missing we provide the parameters ourselves.
   1024      if (parameterNames && parameterNames.length) {
   1025        const prefix = "function " + name + "()";
   1026        const paramString = parameterNames.join(", ");
   1027 
   1028        if (functionSource.startsWith(prefix)) {
   1029          functionSource = functionSource.substr(prefix.length);
   1030 
   1031          functionSource = `function ${name} (${paramString})${functionSource}`;
   1032        }
   1033      }
   1034 
   1035      // If the listener is native code we display the filename "[native code]."
   1036      // This is the official string and should *not* be translated.
   1037      let origin;
   1038      if (native) {
   1039        origin = "[native code]";
   1040      } else {
   1041        origin =
   1042          url +
   1043          (line ? ":" + line + (column === null ? "" : ":" + column) : "");
   1044      }
   1045 
   1046      eventObj = {
   1047        type,
   1048        handler: override.handler || functionSource.trim(),
   1049        origin: override.origin || origin,
   1050        tags: override.tags || tags,
   1051        capturing:
   1052          typeof override.capturing !== "undefined"
   1053            ? override.capturing
   1054            : capturing,
   1055        hide: typeof override.hide !== "undefined" ? override.hide : hide,
   1056        native,
   1057        sourceActor,
   1058        nsIEventListenerInfo: listener.nsIEventListenerInfo,
   1059        enabled,
   1060        isUserDefined: isUserDefinedEventName(type),
   1061      };
   1062 
   1063      // Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
   1064      // generated dynamically from e.g. an onclick="" attribute so the script
   1065      // doesn't actually exist.
   1066      if (!sourceActor) {
   1067        eventObj.hide.debugger = true;
   1068      }
   1069    } finally {
   1070      // Ensure that we always remove the debuggee.
   1071      if (globalDO) {
   1072        dbg.removeDebuggee(globalDO);
   1073      }
   1074    }
   1075 
   1076    return eventObj;
   1077  }
   1078 }
   1079 
   1080 exports.EventCollector = EventCollector;