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;