walker.js (86451B)
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 "use strict"; 6 7 const { Actor } = require("resource://devtools/shared/protocol.js"); 8 const { walkerSpec } = require("resource://devtools/shared/specs/walker.js"); 9 10 const { 11 LongStringActor, 12 } = require("resource://devtools/server/actors/string.js"); 13 const { 14 EXCLUDED_LISTENER, 15 } = require("resource://devtools/server/actors/inspector/constants.js"); 16 17 loader.lazyRequireGetter( 18 this, 19 "nodeFilterConstants", 20 "resource://devtools/shared/dom-node-filter-constants.js" 21 ); 22 23 loader.lazyRequireGetter( 24 this, 25 [ 26 "getFrameElement", 27 "isDirectShadowHostChild", 28 "isFrameBlockedByCSP", 29 "isFrameWithChildTarget", 30 "isShadowHost", 31 "isShadowRoot", 32 "loadSheet", 33 ], 34 "resource://devtools/shared/layout/utils.js", 35 true 36 ); 37 38 loader.lazyRequireGetter( 39 this, 40 "throttle", 41 "resource://devtools/shared/throttle.js", 42 true 43 ); 44 45 loader.lazyRequireGetter( 46 this, 47 [ 48 "allAnonymousContentTreeWalkerFilter", 49 "findGridParentContainerForNode", 50 "isNodeDead", 51 "noAnonymousContentTreeWalkerFilter", 52 "nodeDocument", 53 "standardTreeWalkerFilter", 54 ], 55 "resource://devtools/server/actors/inspector/utils.js", 56 true 57 ); 58 59 loader.lazyRequireGetter( 60 this, 61 "CustomElementWatcher", 62 "resource://devtools/server/actors/inspector/custom-element-watcher.js", 63 true 64 ); 65 loader.lazyRequireGetter( 66 this, 67 ["DocumentWalker", "SKIP_TO_SIBLING"], 68 "resource://devtools/server/actors/inspector/document-walker.js", 69 true 70 ); 71 loader.lazyRequireGetter( 72 this, 73 ["NodeActor", "NodeListActor"], 74 "resource://devtools/server/actors/inspector/node.js", 75 true 76 ); 77 loader.lazyRequireGetter( 78 this, 79 "NodePicker", 80 "resource://devtools/server/actors/inspector/node-picker.js", 81 true 82 ); 83 loader.lazyRequireGetter( 84 this, 85 "LayoutActor", 86 "resource://devtools/server/actors/layout.js", 87 true 88 ); 89 loader.lazyRequireGetter( 90 this, 91 ["getLayoutChangesObserver", "releaseLayoutChangesObserver"], 92 "resource://devtools/server/actors/reflow.js", 93 true 94 ); 95 loader.lazyRequireGetter( 96 this, 97 "WalkerSearch", 98 "resource://devtools/server/actors/utils/walker-search.js", 99 true 100 ); 101 102 // ContentDOMReference requires ChromeUtils, which isn't available in worker context. 103 const lazy = {}; 104 if (!isWorker) { 105 loader.lazyGetter( 106 lazy, 107 "ContentDOMReference", 108 () => 109 ChromeUtils.importESModule( 110 "resource://gre/modules/ContentDOMReference.sys.mjs", 111 // ContentDOMReference needs to be retrieved from the shared global 112 // since it is a shared singleton. 113 { global: "shared" } 114 ).ContentDOMReference 115 ); 116 } 117 118 loader.lazyServiceGetter( 119 this, 120 "eventListenerService", 121 "@mozilla.org/eventlistenerservice;1", 122 "nsIEventListenerService" 123 ); 124 125 // Minimum delay between two "new-mutations" events. 126 const MUTATIONS_THROTTLING_DELAY = 100; 127 // List of mutation types that should -not- be throttled. 128 const IMMEDIATE_MUTATIONS = ["pseudoClassLock"]; 129 130 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; 131 132 // The possible completions to a ':' 133 const PSEUDO_SELECTORS = [ 134 "::marker", 135 "::selection", 136 ":active", 137 ":after", 138 ":before", 139 ":checked", 140 ":disabled", 141 ":empty", 142 ":enabled", 143 ":first-child", 144 ":first-letter", 145 ":first-of-type", 146 ":focus", 147 ":hover", 148 ":lang(", 149 ":last-child", 150 ":last-of-type", 151 ":link", 152 ":not(", 153 ":nth-child(", 154 ":nth-last-child(", 155 ":nth-last-of-type(", 156 ":nth-of-type(", 157 ":only-child", 158 ":only-of-type", 159 ":root", 160 ":target", 161 ":visited", 162 ]; 163 164 const HELPER_SHEET = 165 "data:text/css;charset=utf-8," + 166 encodeURIComponent(` 167 .__fx-devtools-hide-shortcut__ { 168 visibility: hidden !important; 169 } 170 `); 171 172 /** 173 * We only send nodeValue up to a certain size by default. This stuff 174 * controls that size. 175 */ 176 exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50; 177 var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH; 178 179 exports.getValueSummaryLength = function () { 180 return gValueSummaryLength; 181 }; 182 183 exports.setValueSummaryLength = function (val) { 184 gValueSummaryLength = val; 185 }; 186 187 /** 188 * Server side of the DOM walker. 189 */ 190 class WalkerActor extends Actor { 191 /** 192 * Create the WalkerActor 193 * 194 * @param {DevToolsServerConnection} conn 195 * The server connection. 196 * @param {TargetActor} targetActor 197 * The top-level Actor for this tab. 198 * @param {object} options 199 * - {Boolean} showAllAnonymousContent: Show all native anonymous content 200 */ 201 constructor(conn, targetActor, options) { 202 super(conn, walkerSpec); 203 this.targetActor = targetActor; 204 this.rootWin = targetActor.window; 205 this.rootDoc = this.rootWin.document; 206 207 // Map of already created node actors, keyed by their corresponding DOMNode. 208 this._nodeActorsMap = new Map(); 209 210 this._pendingMutations = []; 211 this._activePseudoClassLocks = new Set(); 212 this._mutationBreakpoints = new WeakMap(); 213 this._anonParents = new WeakMap(); 214 this.customElementWatcher = new CustomElementWatcher( 215 targetActor.chromeEventHandler 216 ); 217 218 // In this map, the key-value pairs are the overflow causing elements and their 219 // respective ancestor scrollable node actor. 220 this.overflowCausingElementsMap = new Map(); 221 222 this.showAllAnonymousContent = options.showAllAnonymousContent; 223 // Allow native anonymous content (like <video> controls) if preffed on 224 this.documentWalkerFilter = this.showAllAnonymousContent 225 ? allAnonymousContentTreeWalkerFilter 226 : standardTreeWalkerFilter; 227 228 this.walkerSearch = new WalkerSearch(this); 229 230 // Nodes which have been removed from the client's known 231 // ownership tree are considered "orphaned", and stored in 232 // this set. 233 this._orphaned = new Set(); 234 235 // The client can tell the walker that it is interested in a node 236 // even when it is orphaned with the `retainNode` method. This 237 // list contains orphaned nodes that were so retained. 238 this._retainedOrphans = new Set(); 239 240 this.onSubtreeModified = this.onSubtreeModified.bind(this); 241 this.onSubtreeModified[EXCLUDED_LISTENER] = true; 242 this.onNodeRemoved = this.onNodeRemoved.bind(this); 243 this.onNodeRemoved[EXCLUDED_LISTENER] = true; 244 this.onAttributeModified = this.onAttributeModified.bind(this); 245 this.onAttributeModified[EXCLUDED_LISTENER] = true; 246 247 this.onMutations = this.onMutations.bind(this); 248 this.onSlotchange = this.onSlotchange.bind(this); 249 this.onShadowrootattached = this.onShadowrootattached.bind(this); 250 this.onAnonymousrootcreated = this.onAnonymousrootcreated.bind(this); 251 this.onAnonymousrootremoved = this.onAnonymousrootremoved.bind(this); 252 this.onFrameLoad = this.onFrameLoad.bind(this); 253 this.onFrameUnload = this.onFrameUnload.bind(this); 254 this.onCustomElementDefined = this.onCustomElementDefined.bind(this); 255 this._throttledEmitNewMutations = throttle( 256 this._emitNewMutations.bind(this), 257 MUTATIONS_THROTTLING_DELAY 258 ); 259 260 targetActor.on("will-navigate", this.onFrameUnload); 261 targetActor.on("window-ready", this.onFrameLoad); 262 263 this.customElementWatcher.on( 264 "element-defined", 265 this.onCustomElementDefined 266 ); 267 268 // Keep a reference to the chromeEventHandler for the current targetActor, to make 269 // sure we will be able to remove the listener during the WalkerActor destroy(). 270 this.chromeEventHandler = targetActor.chromeEventHandler; 271 // shadowrootattached is a chrome-only event. We enable it below. 272 this.chromeEventHandler.addEventListener( 273 "shadowrootattached", 274 this.onShadowrootattached 275 ); 276 // anonymousrootcreated is a chrome-only event. We enable it below. 277 this.chromeEventHandler.addEventListener( 278 "anonymousrootcreated", 279 this.onAnonymousrootcreated 280 ); 281 this.chromeEventHandler.addEventListener( 282 "anonymousrootremoved", 283 this.onAnonymousrootremoved 284 ); 285 for (const { document } of this.targetActor.windows) { 286 document.devToolsAnonymousAndShadowEventsEnabled = true; 287 } 288 289 // Ensure that the root document node actor is ready and 290 // managed. 291 this.rootNode = this.document(); 292 293 this.layoutChangeObserver = getLayoutChangesObserver(this.targetActor); 294 this._onReflows = this._onReflows.bind(this); 295 this.layoutChangeObserver.on("reflows", this._onReflows); 296 this._onResize = this._onResize.bind(this); 297 this.layoutChangeObserver.on("resize", this._onResize); 298 299 this._onEventListenerChange = this._onEventListenerChange.bind(this); 300 eventListenerService.addListenerChangeListener(this._onEventListenerChange); 301 } 302 303 get nodePicker() { 304 if (!this._nodePicker) { 305 this._nodePicker = new NodePicker(this, this.targetActor); 306 } 307 308 return this._nodePicker; 309 } 310 311 watchRootNode() { 312 if (this.rootNode) { 313 this.emit("root-available", this.rootNode); 314 } 315 } 316 317 /** 318 * Callback for eventListenerService.addListenerChangeListener 319 * 320 * @param nsISimpleEnumerator changesEnum 321 * enumerator of nsIEventListenerChange 322 */ 323 _onEventListenerChange(changesEnum) { 324 for (const current of changesEnum.enumerate(Ci.nsIEventListenerChange)) { 325 const target = current.target; 326 327 if (this._nodeActorsMap.has(target)) { 328 const actor = this.getNode(target); 329 const mutation = { 330 type: "events", 331 target: actor.actorID, 332 hasEventListeners: actor.hasEventListeners(/* refreshCache */ true), 333 }; 334 this.queueMutation(mutation); 335 } 336 } 337 } 338 339 // Returns the JSON representation of this object over the wire. 340 form() { 341 return { 342 actor: this.actorID, 343 root: this.rootNode.form(), 344 rfpCSSColorScheme: ChromeUtils.shouldResistFingerprinting( 345 "CSSPrefersColorScheme", 346 null 347 ), 348 traits: {}, 349 }; 350 } 351 352 toString() { 353 return "[WalkerActor " + this.actorID + "]"; 354 } 355 356 getDocumentWalker(node, skipTo) { 357 return new DocumentWalker(node, this.rootWin, { 358 filter: this.documentWalkerFilter, 359 skipTo, 360 showAnonymousContent: true, 361 }); 362 } 363 364 destroy() { 365 if (this._destroyed) { 366 return; 367 } 368 this._destroyed = true; 369 super.destroy(); 370 try { 371 this.clearPseudoClassLocks(); 372 this._activePseudoClassLocks = null; 373 374 this.overflowCausingElementsMap.clear(); 375 this.overflowCausingElementsMap = null; 376 377 this._hoveredNode = null; 378 this.rootWin = null; 379 this.rootDoc = null; 380 this.rootNode = null; 381 this.layoutHelpers = null; 382 this._orphaned = null; 383 this._retainedOrphans = null; 384 385 this.targetActor.off("will-navigate", this.onFrameUnload); 386 this.targetActor.off("window-ready", this.onFrameLoad); 387 this.customElementWatcher.off( 388 "element-defined", 389 this.onCustomElementDefined 390 ); 391 392 this.chromeEventHandler.removeEventListener( 393 "shadowrootattached", 394 this.onShadowrootattached 395 ); 396 this.chromeEventHandler.removeEventListener( 397 "anonymousrootcreated", 398 this.onAnonymousrootcreated 399 ); 400 this.chromeEventHandler.removeEventListener( 401 "anonymousrootremoved", 402 this.onAnonymousrootremoved 403 ); 404 405 // This attribute is just for devtools, so we can unset once we're done. 406 for (const { document } of this.targetActor.windows) { 407 document.devToolsAnonymousAndShadowEventsEnabled = false; 408 } 409 410 this.onFrameLoad = null; 411 this.onFrameUnload = null; 412 413 this.customElementWatcher.destroy(); 414 this.customElementWatcher = null; 415 416 this.walkerSearch.destroy(); 417 418 if (this._nodePicker) { 419 this._nodePicker.destroy(); 420 this._nodePicker = null; 421 } 422 423 this.layoutChangeObserver.off("reflows", this._onReflows); 424 this.layoutChangeObserver.off("resize", this._onResize); 425 this.layoutChangeObserver = null; 426 releaseLayoutChangesObserver(this.targetActor); 427 428 eventListenerService.removeListenerChangeListener( 429 this._onEventListenerChange 430 ); 431 432 // Only nullify some key attributes after having removed all the listeners 433 // as they may still be used in the related listeners. 434 this._nodeActorsMap = null; 435 this.onMutations = null; 436 437 this.layoutActor = null; 438 this.targetActor = null; 439 this.chromeEventHandler = null; 440 this.documentWalkerFilter = null; 441 442 this.emit("destroyed"); 443 } catch (e) { 444 console.error(e); 445 } 446 } 447 448 release() {} 449 450 unmanage(actor) { 451 if (actor instanceof NodeActor) { 452 if ( 453 this._activePseudoClassLocks && 454 this._activePseudoClassLocks.has(actor) 455 ) { 456 this.clearPseudoClassLocks(actor); 457 } 458 459 this.customElementWatcher.unmanageNode(actor); 460 461 this._nodeActorsMap.delete(actor.rawNode); 462 } 463 super.unmanage(actor); 464 } 465 466 /** 467 * Determine if the walker has come across this DOM node before. 468 * 469 * @param {DOMNode} rawNode 470 * @return {boolean} 471 */ 472 hasNode(rawNode) { 473 return this._nodeActorsMap.has(rawNode); 474 } 475 476 /** 477 * If the walker has come across this DOM node before, then get the 478 * corresponding node actor. 479 * 480 * @param {DOMNode} rawNode 481 * @return {NodeActor} 482 */ 483 getNode(rawNode) { 484 return this._nodeActorsMap.get(rawNode); 485 } 486 487 /** 488 * Internal helper that will either retrieve the existing NodeActor for the 489 * provided node or create the actor on the fly if it doesn't exist. 490 * This method should only be called when we are sure that the node should be 491 * known by the client and that the parent node is already known. 492 * 493 * Otherwise prefer `getNode` to only retrieve known actors or `attachElement` 494 * to create node actors recursively. 495 * 496 * @param {DOMNode} node 497 * The node for which we want to create or get an actor 498 * @return {NodeActor} The corresponding NodeActor 499 */ 500 _getOrCreateNodeActor(node) { 501 let actor = this.getNode(node); 502 if (actor) { 503 return actor; 504 } 505 506 actor = new NodeActor(this, node); 507 508 // Add the node actor as a child of this walker actor, assigning 509 // it an actorID. 510 this.manage(actor); 511 this._nodeActorsMap.set(node, actor); 512 513 if (node.nodeType === Node.DOCUMENT_NODE) { 514 actor.watchDocument(node, this.onMutations); 515 } 516 517 if (isShadowRoot(actor.rawNode)) { 518 actor.watchDocument(node.ownerDocument, this.onMutations); 519 actor.watchSlotchange(this.onSlotchange); 520 } 521 522 this.customElementWatcher.manageNode(actor); 523 524 return actor; 525 } 526 527 /** 528 * When a custom element is defined, send a customElementDefined mutation for all the 529 * NodeActors using this tag name. 530 */ 531 onCustomElementDefined({ actors }) { 532 actors.forEach(actor => 533 this.queueMutation({ 534 target: actor.actorID, 535 type: "customElementDefined", 536 customElementLocation: actor.getCustomElementLocation(), 537 }) 538 ); 539 } 540 541 _onReflows() { 542 // Going through the nodes the walker knows about, see which ones have had their 543 // containerType, display, scrollable or overflow state changed and send events if any. 544 const containerTypeChanges = []; 545 const displayTypeChanges = []; 546 const scrollableStateChanges = []; 547 const anchorNameChanges = []; 548 549 const currentOverflowCausingElementsMap = new Map(); 550 551 for (const [node, actor] of this._nodeActorsMap) { 552 if (Cu.isDeadWrapper(node)) { 553 continue; 554 } 555 556 const displayType = actor.displayType; 557 const isDisplayed = actor.isDisplayed; 558 559 if ( 560 displayType !== actor.currentDisplayType || 561 isDisplayed !== actor.wasDisplayed 562 ) { 563 displayTypeChanges.push(actor); 564 565 // Updating the original value 566 actor.currentDisplayType = displayType; 567 actor.wasDisplayed = isDisplayed; 568 } 569 570 const isScrollable = actor.isScrollable; 571 if (isScrollable !== actor.wasScrollable) { 572 scrollableStateChanges.push(actor); 573 actor.wasScrollable = isScrollable; 574 } 575 576 if (isScrollable) { 577 this.updateOverflowCausingElements( 578 actor, 579 currentOverflowCausingElementsMap 580 ); 581 } 582 583 const containerType = actor.containerType; 584 if (containerType !== actor.currentContainerType) { 585 containerTypeChanges.push(actor); 586 actor.currentContainerType = containerType; 587 } 588 589 const anchorName = actor.anchorName; 590 if (anchorName !== actor.currentAnchorName) { 591 anchorNameChanges.push(actor); 592 actor.currentAnchorName = anchorName; 593 } 594 } 595 596 // Get the NodeActor for each node in the symmetric difference of 597 // currentOverflowCausingElementsMap and this.overflowCausingElementsMap 598 const overflowStateChanges = [...currentOverflowCausingElementsMap.keys()] 599 .filter(node => !this.overflowCausingElementsMap.has(node)) 600 .concat( 601 [...this.overflowCausingElementsMap.keys()].filter( 602 node => !currentOverflowCausingElementsMap.has(node) 603 ) 604 ) 605 .filter(node => this.hasNode(node)) 606 .map(node => this.getNode(node)); 607 608 this.overflowCausingElementsMap = currentOverflowCausingElementsMap; 609 610 if (overflowStateChanges.length) { 611 this.emit("overflow-change", overflowStateChanges); 612 } 613 614 if (displayTypeChanges.length) { 615 this.emit("display-change", displayTypeChanges); 616 } 617 618 if (scrollableStateChanges.length) { 619 this.emit("scrollable-change", scrollableStateChanges); 620 } 621 622 if (containerTypeChanges.length) { 623 this.emit("container-type-change", containerTypeChanges); 624 } 625 626 if (anchorNameChanges.length) { 627 this.emit("anchor-name-change", anchorNameChanges); 628 } 629 } 630 631 /** 632 * When the browser window gets resized, relay the event to the front. 633 */ 634 _onResize() { 635 this.emit("resize"); 636 } 637 638 /** 639 * Ensures that the node is attached and it can be accessed from the root. 640 * 641 * @param {(Node|NodeActor)} nodes The nodes 642 * @return {object} An object compatible with the disconnectedNode type. 643 */ 644 attachElement(node) { 645 const { nodes, newParents } = this.attachElements([node]); 646 return { 647 node: nodes[0], 648 newParents, 649 }; 650 } 651 652 /** 653 * Ensures that the nodes are attached and they can be accessed from the root. 654 * 655 * @param {(Node[]|NodeActor[])} nodes The nodes 656 * @return {object} An object compatible with the disconnectedNodeArray type. 657 */ 658 attachElements(nodes) { 659 const nodeActors = []; 660 const newParents = new Set(); 661 for (let node of nodes) { 662 if (!(node instanceof NodeActor)) { 663 // If the provided node doesn't match the filter, use the closest ancestor 664 while ( 665 node && 666 this.documentWalkerFilter(node) != nodeFilterConstants.FILTER_ACCEPT 667 ) { 668 node = this.rawParentNode(node); 669 } 670 if (!node) { 671 continue; 672 } 673 674 node = this._getOrCreateNodeActor(node); 675 } 676 677 this.ensurePathToRoot(node, newParents); 678 // If nodes may be an array of raw nodes, we're sure to only have 679 // NodeActors with the following array. 680 nodeActors.push(node); 681 } 682 683 return { 684 nodes: nodeActors, 685 newParents: [...newParents], 686 }; 687 } 688 689 /** 690 * Return the document node that contains the given node, 691 * or the root node if no node is specified. 692 * 693 * @param NodeActor node 694 * The node whose document is needed, or null to 695 * return the root. 696 */ 697 document(node) { 698 const doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode); 699 return this._getOrCreateNodeActor(doc); 700 } 701 702 /** 703 * Return the documentElement for the document containing the 704 * given node. 705 * 706 * @param NodeActor node 707 * The node whose documentElement is requested, or null 708 * to use the root document. 709 */ 710 documentElement(node) { 711 const elt = isNodeDead(node) 712 ? this.rootDoc.documentElement 713 : nodeDocument(node.rawNode).documentElement; 714 return this._getOrCreateNodeActor(elt); 715 } 716 717 parentNode(node) { 718 const parent = this.rawParentNode(node); 719 if (parent) { 720 return this._getOrCreateNodeActor(parent); 721 } 722 723 return null; 724 } 725 726 rawParentNode(node) { 727 const rawNode = node instanceof NodeActor ? node.rawNode : node; 728 if (rawNode == this.rootDoc) { 729 return null; 730 } 731 const parentNode = InspectorUtils.getParentForNode( 732 rawNode, 733 /* anonymous = */ true 734 ); 735 736 if (!parentNode) { 737 return null; 738 } 739 740 // If the parent node is one we should ignore (e.g. :-moz-snapshot-containing-block, 741 // which is the root node for ::view-transition pseudo elements), we want to return 742 // the closest non-ignored parent. 743 if ( 744 this.documentWalkerFilter(parentNode) === 745 nodeFilterConstants.FILTER_ACCEPT_CHILDREN 746 ) { 747 return this.rawParentNode(parentNode); 748 } 749 750 return parentNode; 751 } 752 753 /** 754 * If the given NodeActor only has a single text node as a child with a text 755 * content small enough to be inlined, return that child's NodeActor. 756 * 757 * @param Element rawNode 758 */ 759 inlineTextChild(rawNode) { 760 // Quick checks to prevent creating a new walker if possible. 761 if ( 762 !!rawNode.implementedPseudoElement || 763 isShadowHost(rawNode) || 764 rawNode.nodeType != Node.ELEMENT_NODE || 765 !!rawNode.children.length || 766 isFrameWithChildTarget(this.targetActor, rawNode) || 767 isFrameBlockedByCSP(rawNode) 768 ) { 769 return undefined; 770 } 771 772 const children = this._rawChildren(rawNode, /* includeAssigned = */ true); 773 const firstChild = children[0]; 774 775 // Bail out if: 776 // - more than one child 777 // - unique child is not a text node 778 // - unique child is a text node, but is too long to be inlined 779 // - we are a slot -> these are always represented on their own lines with 780 // a link to the original node. 781 // - we are a flex item -> these are always shown on their own lines so they can be 782 // selected by the flexbox inspector. 783 const isAssignedToSlot = 784 firstChild && 785 rawNode.nodeName === "SLOT" && 786 isDirectShadowHostChild(firstChild); 787 788 const isFlexItem = !!firstChild?.parentFlexElement; 789 790 if ( 791 !firstChild || 792 children.length > 1 || 793 firstChild.nodeType !== Node.TEXT_NODE || 794 firstChild.nodeValue.length > gValueSummaryLength || 795 isAssignedToSlot || 796 isFlexItem 797 ) { 798 return undefined; 799 } 800 801 return this._getOrCreateNodeActor(firstChild); 802 } 803 804 /** 805 * Mark a node as 'retained'. 806 * 807 * A retained node is not released when `releaseNode` is called on its 808 * parent, or when a parent is released with the `cleanup` option to 809 * `getMutations`. 810 * 811 * When a retained node's parent is released, a retained mode is added to 812 * the walker's "retained orphans" list. 813 * 814 * Retained nodes can be deleted by providing the `force` option to 815 * `releaseNode`. They will also be released when their document 816 * has been destroyed. 817 * 818 * Retaining a node makes no promise about its children; They can 819 * still be removed by normal means. 820 */ 821 retainNode(node) { 822 node.retained = true; 823 } 824 825 /** 826 * Remove the 'retained' mark from a node. If the node was a 827 * retained orphan, release it. 828 */ 829 unretainNode(node) { 830 node.retained = false; 831 if (this._retainedOrphans.has(node)) { 832 this._retainedOrphans.delete(node); 833 this.releaseNode(node); 834 } 835 } 836 837 /** 838 * Release actors for a node and all child nodes. 839 */ 840 releaseNode(node, options = {}) { 841 if (isNodeDead(node)) { 842 return; 843 } 844 845 if (node.retained && !options.force) { 846 this._retainedOrphans.add(node); 847 return; 848 } 849 850 if (node.retained) { 851 // Forcing a retained node to go away. 852 this._retainedOrphans.delete(node); 853 } 854 855 for (const child of this._rawChildren(node.rawNode)) { 856 const childActor = this.getNode(child); 857 if (childActor) { 858 this.releaseNode(childActor, options); 859 } 860 } 861 862 node.destroy(); 863 } 864 865 /** 866 * Add any nodes between `node` and the walker's root node that have not 867 * yet been seen by the client. 868 */ 869 ensurePathToRoot(node, newParents = new Set()) { 870 if (!node) { 871 return newParents; 872 } 873 let parent = this.rawParentNode(node); 874 while (parent) { 875 let parentActor = this.getNode(parent); 876 if (parentActor) { 877 // This parent did exist, so the client knows about it. 878 return newParents; 879 } 880 // This parent didn't exist, so hasn't been seen by the client yet. 881 parentActor = this._getOrCreateNodeActor(parent); 882 newParents.add(parentActor); 883 parent = this.rawParentNode(parentActor); 884 } 885 return newParents; 886 } 887 888 /** 889 * Return the number of children under the provided NodeActor. 890 * 891 * @param NodeActor node 892 * See JSDoc for children() 893 * @param object options 894 * See JSDoc for children() 895 * @return Number the number of children 896 */ 897 countChildren(node, options = {}) { 898 return this._getChildren(node, options).nodes.length; 899 } 900 901 /** 902 * Return children of the given node. By default this method will return 903 * all children of the node, but there are options that can restrict this 904 * to a more manageable subset. 905 * 906 * @param NodeActor node 907 * The node whose children you're curious about. 908 * @param object options 909 * Named options: 910 * `maxNodes`: The set of nodes returned by the method will be no longer 911 * than maxNodes. 912 * `start`: If a node is specified, the list of nodes will start 913 * with the given child. Mutally exclusive with `center`. 914 * `center`: If a node is specified, the given node will be as centered 915 * as possible in the list, given how close to the ends of the child 916 * list it is. Mutually exclusive with `start`. 917 * 918 * @returns an object with three items: 919 * hasFirst: true if the first child of the node is included in the list. 920 * hasLast: true if the last child of the node is included in the list. 921 * nodes: Array of NodeActor representing the nodes returned by the request. 922 */ 923 children(node, options = {}) { 924 const { hasFirst, hasLast, nodes } = this._getChildren(node, options); 925 return { 926 hasFirst, 927 hasLast, 928 nodes: nodes.map(n => this._getOrCreateNodeActor(n)), 929 }; 930 } 931 932 /** 933 * Returns the raw children of the DOM node, with anonymous content filtered as needed 934 * 935 * @param Node rawNode. 936 * @param boolean includeAssigned 937 * Whether <slot> assigned children should be returned. See 938 * HTMLSlotElement.assignedNodes(). 939 * @returns Array<Node> the list of children. 940 */ 941 _rawChildren(rawNode, includeAssigned) { 942 const ret = []; 943 const children = InspectorUtils.getChildrenForNode( 944 rawNode, 945 /* anonymous = */ true, 946 includeAssigned 947 ); 948 for (const child of children) { 949 const filterResult = this.documentWalkerFilter(child); 950 if (filterResult == nodeFilterConstants.FILTER_ACCEPT) { 951 ret.push(child); 952 } else if (filterResult == nodeFilterConstants.FILTER_ACCEPT_CHILDREN) { 953 // In some cases, we want to completly ignore a node, and display its children 954 // instead (e.g. for `<div type="::-moz-snapshot-containing-block">`, 955 // we don't want it displayed in the markup view, 956 // but we do want to have its `::view-transition` child) 957 ret.push(...this._rawChildren(child, includeAssigned)); 958 } 959 } 960 return ret; 961 } 962 963 /** 964 * Return chidlren of the given node. Contrary to children children(), this method only 965 * returns DOMNodes. Therefore it will not create NodeActor wrappers and will not 966 * update the nodeActors map for the discovered nodes either. This makes this method 967 * safe to call when you are not sure if the discovered nodes will be communicated to 968 * the client. 969 * 970 * @param NodeActor node 971 * See JSDoc for children() 972 * @param object options 973 * See JSDoc for children() 974 * @return an object with three items: 975 * hasFirst: true if the first child of the node is included in the list. 976 * hasLast: true if the last child of the node is included in the list. 977 * nodes: Array of DOMNodes. 978 */ 979 // eslint-disable-next-line complexity 980 _getChildren(node, options = {}) { 981 if (isNodeDead(node) || isFrameBlockedByCSP(node.rawNode)) { 982 return { hasFirst: true, hasLast: true, nodes: [] }; 983 } 984 985 if (options.center && options.start) { 986 throw Error("Can't specify both 'center' and 'start' options."); 987 } 988 989 let maxNodes = options.maxNodes || -1; 990 if (maxNodes == -1) { 991 maxNodes = Number.MAX_VALUE; 992 } 993 994 let nodes = this._rawChildren(node.rawNode, /* includeAssigned = */ true); 995 let hasFirst = true; 996 let hasLast = true; 997 if (nodes.length > maxNodes) { 998 let startIndex; 999 if (options.center) { 1000 const centerIndex = nodes.indexOf(options.center.rawNode); 1001 const backwardCount = Math.floor(maxNodes / 2); 1002 // If centering would hit the end, just read the last maxNodes nodes. 1003 if (centerIndex - backwardCount + maxNodes >= nodes.length) { 1004 startIndex = nodes.length - maxNodes; 1005 } else { 1006 startIndex = Math.max(0, centerIndex - backwardCount); 1007 } 1008 } else if (options.start) { 1009 startIndex = Math.max(0, nodes.indexOf(options.start.rawNode)); 1010 } else { 1011 startIndex = 0; 1012 } 1013 const endIndex = Math.min(startIndex + maxNodes, nodes.length); 1014 hasFirst = startIndex == 0; 1015 hasLast = endIndex >= nodes.length; 1016 nodes = nodes.slice(startIndex, endIndex); 1017 } 1018 1019 return { hasFirst, hasLast, nodes }; 1020 } 1021 1022 /** 1023 * Get the next sibling of a given node. Getting nodes one at a time 1024 * might be inefficient, be careful. 1025 */ 1026 nextSibling(node) { 1027 if (isNodeDead(node)) { 1028 return null; 1029 } 1030 1031 const walker = this.getDocumentWalker(node.rawNode); 1032 const sibling = walker.nextSibling(); 1033 return sibling ? this._getOrCreateNodeActor(sibling) : null; 1034 } 1035 1036 /** 1037 * Get the previous sibling of a given node. Getting nodes one at a time 1038 * might be inefficient, be careful. 1039 */ 1040 previousSibling(node) { 1041 if (isNodeDead(node)) { 1042 return null; 1043 } 1044 1045 const walker = this.getDocumentWalker(node.rawNode); 1046 const sibling = walker.previousSibling(); 1047 return sibling ? this._getOrCreateNodeActor(sibling) : null; 1048 } 1049 1050 /** 1051 * Helper function for the `children` method: Read forward in the sibling 1052 * list into an array with `count` items, including the current node. 1053 */ 1054 _readForward(walker, count) { 1055 const ret = []; 1056 1057 let node = walker.currentNode; 1058 do { 1059 if (!walker.isSkippedNode(node)) { 1060 // The walker can be on a node that would be filtered out if it didn't find any 1061 // other node to fallback to. 1062 ret.push(node); 1063 } 1064 node = walker.nextSibling(); 1065 } while (node && --count); 1066 return ret; 1067 } 1068 1069 /** 1070 * Return the first node in the document that matches the given selector. 1071 * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector 1072 * 1073 * @param NodeActor baseNode 1074 * @param string selector 1075 */ 1076 querySelector(baseNode, selector) { 1077 if (isNodeDead(baseNode)) { 1078 return {}; 1079 } 1080 1081 const node = baseNode.rawNode.querySelector(selector); 1082 if (!node) { 1083 return {}; 1084 } 1085 1086 return this.attachElement(node); 1087 } 1088 1089 /** 1090 * Return a NodeListActor with all nodes that match the given selector. 1091 * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll 1092 * 1093 * @param NodeActor baseNode 1094 * @param string selector 1095 */ 1096 querySelectorAll(baseNode, selector) { 1097 let nodeList = null; 1098 1099 try { 1100 nodeList = baseNode.rawNode.querySelectorAll(selector); 1101 } catch (e) { 1102 // Bad selector. Do nothing as the selector can come from a searchbox. 1103 } 1104 1105 return new NodeListActor(this, nodeList); 1106 } 1107 1108 /** 1109 * Return the node in the baseNode rootNode matching the passed id referenced in a 1110 * idref/idreflist attribute, as those are scoped within a shadow root. 1111 * 1112 * @param NodeActor baseNode 1113 * @param string id 1114 */ 1115 getIdrefNode(baseNode, id) { 1116 if (isNodeDead(baseNode)) { 1117 return {}; 1118 } 1119 1120 // Get the document or the shadow root for baseNode 1121 const rootNode = baseNode.rawNode.getRootNode({ composed: false }); 1122 if (!rootNode) { 1123 return {}; 1124 } 1125 1126 const node = rootNode.getElementById(id); 1127 if (!node) { 1128 return {}; 1129 } 1130 1131 return this.attachElement(node); 1132 } 1133 1134 /** 1135 * Get a list of nodes that match the given selector in all known frames of 1136 * the current content page. 1137 * 1138 * @param {string} selector. 1139 * @return {Array} 1140 */ 1141 _multiFrameQuerySelectorAll(selector) { 1142 let nodes = []; 1143 1144 for (const { document } of this.targetActor.windows) { 1145 try { 1146 nodes = [...nodes, ...document.querySelectorAll(selector)]; 1147 } catch (e) { 1148 // Bad selector. Do nothing as the selector can come from a searchbox. 1149 } 1150 } 1151 1152 return nodes; 1153 } 1154 1155 /** 1156 * Get a list of nodes that match the given XPath in all known frames of 1157 * the current content page. 1158 * 1159 * @param {string} xPath. 1160 * @return {Array} 1161 */ 1162 _multiFrameXPath(xPath) { 1163 const nodes = []; 1164 1165 for (const window of this.targetActor.windows) { 1166 const document = window.document; 1167 try { 1168 const result = document.evaluate( 1169 xPath, 1170 document.documentElement, 1171 null, 1172 window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 1173 null 1174 ); 1175 1176 for (let i = 0; i < result.snapshotLength; i++) { 1177 nodes.push(result.snapshotItem(i)); 1178 } 1179 } catch (e) { 1180 // Bad XPath. Do nothing as the XPath can come from a searchbox. 1181 } 1182 } 1183 1184 return nodes; 1185 } 1186 1187 /** 1188 * Return a NodeListActor with all nodes that match the given XPath in all 1189 * frames of the current content page. 1190 * 1191 * @param {string} xPath 1192 */ 1193 multiFrameXPath(xPath) { 1194 return new NodeListActor(this, this._multiFrameXPath(xPath)); 1195 } 1196 1197 /** 1198 * Search the document for a given string. 1199 * Results will be searched with the walker-search module (searches through 1200 * tag names, attribute names and values, and text contents). 1201 * 1202 * @returns {searchresult} 1203 * - {NodeList} list 1204 * - {Array<Object>} metadata. Extra information with indices that 1205 * match up with node list. 1206 */ 1207 search(query) { 1208 const results = this.walkerSearch.search(query); 1209 1210 // For now, only return each node once, since the frontend doesn't have a way to 1211 // highlight each result individually (This would change if we fix Bug 1976634). 1212 const seenNodes = new Set(); 1213 for (const { node } of results) { 1214 const isInlinedTextNode = 1215 node.nodeType === Node.TEXT_NODE && 1216 node.parentElement && 1217 this.inlineTextChild(node.parentElement); 1218 1219 // If this is a text node that will be inlined in its parent element, let's directly 1220 // return the parent element already so we don't get multiple results for the same 1221 // Markup view tree node. 1222 seenNodes.add(isInlinedTextNode ? node.parentElement : node); 1223 } 1224 1225 const nodeList = new NodeListActor(this, Array.from(seenNodes)); 1226 1227 return { 1228 list: nodeList, 1229 metadata: [], 1230 }; 1231 } 1232 1233 /** 1234 * Returns a list of matching results for CSS selector autocompletion. 1235 * 1236 * @param string query 1237 * The selector query being completed 1238 * @param string completing 1239 * The exact token being completed out of the query 1240 * @param string selectorState 1241 * One of "pseudo", "id", "tag", "class", "null" 1242 */ 1243 // eslint-disable-next-line complexity 1244 getSuggestionsForQuery(query, completing, selectorState) { 1245 const sugs = { 1246 classes: new Set(), 1247 tags: new Set(), 1248 ids: new Set(), 1249 }; 1250 let result = []; 1251 let nodes = null; 1252 // Filtering and sorting the results so that protocol transfer is minimal. 1253 switch (selectorState) { 1254 case "pseudo": { 1255 const colonPrefixedCompleting = ":" + completing; 1256 for (const pseudo of PSEUDO_SELECTORS) { 1257 if (pseudo.startsWith(colonPrefixedCompleting)) { 1258 result.push([pseudo]); 1259 } 1260 } 1261 break; 1262 } 1263 1264 case "class": 1265 if (!query) { 1266 nodes = this._multiFrameQuerySelectorAll("[class]"); 1267 } else { 1268 nodes = this._multiFrameQuerySelectorAll(query); 1269 } 1270 for (const node of nodes) { 1271 for (const className of node.classList) { 1272 sugs.classes.add(className); 1273 } 1274 } 1275 sugs.classes.delete(""); 1276 sugs.classes.delete(HIDDEN_CLASS); 1277 for (const className of sugs.classes) { 1278 if (className.startsWith(completing)) { 1279 result.push(["." + CSS.escape(className), selectorState]); 1280 } 1281 } 1282 break; 1283 1284 case "id": 1285 if (!query) { 1286 nodes = this._multiFrameQuerySelectorAll("[id]"); 1287 } else { 1288 nodes = this._multiFrameQuerySelectorAll(query); 1289 } 1290 for (const node of nodes) { 1291 sugs.ids.add(node.id); 1292 } 1293 for (const id of sugs.ids) { 1294 if (id.startsWith(completing) && id !== "") { 1295 result.push(["#" + CSS.escape(id), selectorState]); 1296 } 1297 } 1298 break; 1299 1300 case "tag": 1301 if (!query) { 1302 nodes = this._multiFrameQuerySelectorAll("*"); 1303 } else { 1304 nodes = this._multiFrameQuerySelectorAll(query); 1305 } 1306 for (const node of nodes) { 1307 const tag = node.localName; 1308 sugs.tags.add(tag); 1309 } 1310 for (const tag of sugs.tags) { 1311 if (new RegExp("^" + completing + ".*", "i").test(tag)) { 1312 result.push([tag, selectorState]); 1313 } 1314 } 1315 1316 // For state 'tag' (no preceding # or .) and when there's no query (i.e. 1317 // only one word) then search for the matching classes and ids 1318 if (!query) { 1319 result = [ 1320 ...result, 1321 ...this.getSuggestionsForQuery(null, completing, "class") 1322 .suggestions, 1323 ...this.getSuggestionsForQuery(null, completing, "id").suggestions, 1324 ]; 1325 } 1326 1327 break; 1328 1329 case "null": 1330 nodes = this._multiFrameQuerySelectorAll(query); 1331 for (const node of nodes) { 1332 sugs.ids.add(node.id); 1333 const tag = node.localName; 1334 sugs.tags.add(tag); 1335 for (const className of node.classList) { 1336 sugs.classes.add(className); 1337 } 1338 } 1339 for (const tag of sugs.tags) { 1340 tag && result.push([tag]); 1341 } 1342 for (const id of sugs.ids) { 1343 id && result.push(["#" + id]); 1344 } 1345 sugs.classes.delete(""); 1346 sugs.classes.delete(HIDDEN_CLASS); 1347 for (const className of sugs.classes) { 1348 className && result.push(["." + className]); 1349 } 1350 } 1351 1352 // Sort by type (id, class, tag) and name (asc) 1353 result = result.sort((a, b) => { 1354 // Prefixing ids, classes and tags, to group results 1355 const firstA = a[0].substring(0, 1); 1356 const firstB = b[0].substring(0, 1); 1357 1358 const getSortKeyPrefix = firstLetter => { 1359 if (firstLetter === "#") { 1360 return "2"; 1361 } 1362 if (firstLetter === ".") { 1363 return "1"; 1364 } 1365 return "0"; 1366 }; 1367 1368 const sortA = getSortKeyPrefix(firstA) + a[0]; 1369 const sortB = getSortKeyPrefix(firstB) + b[0]; 1370 1371 // String compare 1372 return sortA.localeCompare(sortB); 1373 }); 1374 1375 result = result.slice(0, 25); 1376 1377 return { 1378 query, 1379 suggestions: result, 1380 }; 1381 } 1382 1383 /** 1384 * Add a pseudo-class lock to a node. 1385 * 1386 * @param NodeActor node 1387 * @param string pseudo 1388 * A pseudoclass: ':hover', ':active', ':focus', ':focus-within' 1389 * @param options 1390 * Options object: 1391 * `parents`: True if the pseudo-class should be added 1392 * to parent nodes. 1393 * `enabled`: False if the pseudo-class should be locked 1394 * to 'off'. Defaults to true. 1395 * 1396 * @returns An empty packet. A "pseudoClassLock" mutation will 1397 * be queued for any changed nodes. 1398 */ 1399 addPseudoClassLock(node, pseudo, options = {}) { 1400 if (isNodeDead(node)) { 1401 return; 1402 } 1403 1404 // There can be only one node locked per pseudo, so dismiss all existing 1405 // ones 1406 for (const locked of this._activePseudoClassLocks) { 1407 if (InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) { 1408 this._removePseudoClassLock(locked, pseudo); 1409 } 1410 } 1411 1412 const enabled = options.enabled === undefined || options.enabled; 1413 this._addPseudoClassLock(node, pseudo, enabled); 1414 1415 if (!options.parents) { 1416 return; 1417 } 1418 1419 const walker = this.getDocumentWalker(node.rawNode); 1420 let cur; 1421 while ((cur = walker.parentNode())) { 1422 const curNode = this._getOrCreateNodeActor(cur); 1423 this._addPseudoClassLock(curNode, pseudo, enabled); 1424 } 1425 } 1426 1427 _queuePseudoClassMutation(node) { 1428 this.queueMutation({ 1429 target: node.actorID, 1430 type: "pseudoClassLock", 1431 pseudoClassLocks: node.writePseudoClassLocks(), 1432 }); 1433 } 1434 1435 _addPseudoClassLock(node, pseudo, enabled) { 1436 if (node.rawNode.nodeType !== Node.ELEMENT_NODE) { 1437 return false; 1438 } 1439 InspectorUtils.addPseudoClassLock(node.rawNode, pseudo, enabled); 1440 this._activePseudoClassLocks.add(node); 1441 this._queuePseudoClassMutation(node); 1442 return true; 1443 } 1444 1445 hideNode(node) { 1446 if (isNodeDead(node)) { 1447 return; 1448 } 1449 1450 loadSheet(node.rawNode.ownerGlobal, HELPER_SHEET); 1451 node.rawNode.classList.add(HIDDEN_CLASS); 1452 } 1453 1454 unhideNode(node) { 1455 if (isNodeDead(node)) { 1456 return; 1457 } 1458 1459 node.rawNode.classList.remove(HIDDEN_CLASS); 1460 } 1461 1462 /** 1463 * Remove a pseudo-class lock from a node. 1464 * 1465 * @param NodeActor node 1466 * @param string pseudo 1467 * A pseudoclass: ':hover', ':active', ':focus', ':focus-within' 1468 * @param options 1469 * Options object: 1470 * `parents`: True if the pseudo-class should be removed 1471 * from parent nodes. 1472 * 1473 * @returns An empty response. "pseudoClassLock" mutations 1474 * will be emitted for any changed nodes. 1475 */ 1476 removePseudoClassLock(node, pseudo, options = {}) { 1477 if (isNodeDead(node)) { 1478 return; 1479 } 1480 1481 this._removePseudoClassLock(node, pseudo); 1482 1483 // Remove pseudo class for children as we don't want to allow 1484 // turning it on for some childs without setting it on some parents 1485 for (const locked of this._activePseudoClassLocks) { 1486 if ( 1487 node.rawNode.contains(locked.rawNode) && 1488 InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo) 1489 ) { 1490 this._removePseudoClassLock(locked, pseudo); 1491 } 1492 } 1493 1494 if (!options.parents) { 1495 return; 1496 } 1497 1498 const walker = this.getDocumentWalker(node.rawNode); 1499 let cur; 1500 while ((cur = walker.parentNode())) { 1501 const curNode = this._getOrCreateNodeActor(cur); 1502 this._removePseudoClassLock(curNode, pseudo); 1503 } 1504 } 1505 1506 _removePseudoClassLock(node, pseudo) { 1507 if (node.rawNode.nodeType != Node.ELEMENT_NODE) { 1508 return false; 1509 } 1510 InspectorUtils.removePseudoClassLock(node.rawNode, pseudo); 1511 if (!node.writePseudoClassLocks()) { 1512 this._activePseudoClassLocks.delete(node); 1513 } 1514 1515 this._queuePseudoClassMutation(node); 1516 return true; 1517 } 1518 1519 /** 1520 * Clear all the pseudo-classes on a given node or all nodes. 1521 * 1522 * @param {NodeActor} node Optional node to clear pseudo-classes on 1523 */ 1524 clearPseudoClassLocks(node) { 1525 if (node && isNodeDead(node)) { 1526 return; 1527 } 1528 1529 if (node) { 1530 InspectorUtils.clearPseudoClassLocks(node.rawNode); 1531 this._activePseudoClassLocks.delete(node); 1532 this._queuePseudoClassMutation(node); 1533 } else { 1534 for (const locked of this._activePseudoClassLocks) { 1535 InspectorUtils.clearPseudoClassLocks(locked.rawNode); 1536 this._activePseudoClassLocks.delete(locked); 1537 this._queuePseudoClassMutation(locked); 1538 } 1539 } 1540 } 1541 1542 /** 1543 * Get a node's innerHTML property. 1544 */ 1545 innerHTML(node) { 1546 let html = ""; 1547 if (!isNodeDead(node)) { 1548 html = node.rawNode.innerHTML; 1549 } 1550 return new LongStringActor(this.conn, html); 1551 } 1552 1553 /** 1554 * Set a node's innerHTML property. 1555 * 1556 * @param {NodeActor} node The node. 1557 * @param {string} value The piece of HTML content. 1558 */ 1559 setInnerHTML(node, value) { 1560 if (isNodeDead(node)) { 1561 return; 1562 } 1563 1564 const rawNode = node.rawNode; 1565 if ( 1566 rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE && 1567 rawNode.nodeType !== rawNode.ownerDocument.DOCUMENT_FRAGMENT_NODE 1568 ) { 1569 throw new Error("Can only change innerHTML to element or fragment nodes"); 1570 } 1571 // eslint-disable-next-line no-unsanitized/property 1572 rawNode.innerHTML = value; 1573 } 1574 1575 /** 1576 * Get a node's outerHTML property. 1577 * 1578 * @param {NodeActor} node The node. 1579 */ 1580 outerHTML(node) { 1581 let outerHTML = ""; 1582 if (!isNodeDead(node)) { 1583 outerHTML = node.rawNode.outerHTML; 1584 } 1585 return new LongStringActor(this.conn, outerHTML); 1586 } 1587 1588 /** 1589 * Set a node's outerHTML property. 1590 * 1591 * @param {NodeActor} node The node. 1592 * @param {string} value The piece of HTML content. 1593 */ 1594 setOuterHTML(node, value) { 1595 if (isNodeDead(node)) { 1596 return; 1597 } 1598 1599 const rawNode = node.rawNode; 1600 const doc = nodeDocument(rawNode); 1601 const win = doc.defaultView; 1602 let parser; 1603 if (!win) { 1604 throw new Error("The window object shouldn't be null"); 1605 } else { 1606 // We create DOMParser under window object because we want a content 1607 // DOMParser, which means all the DOM objects created by this DOMParser 1608 // will be in the same DocGroup as rawNode.parentNode. Then the newly 1609 // created nodes can be adopted into rawNode.parentNode. 1610 parser = new win.DOMParser(); 1611 } 1612 1613 const mimeType = rawNode.tagName === "svg" ? "image/svg+xml" : "text/html"; 1614 const parsedDOM = parser.parseFromString(value, mimeType); 1615 const parentNode = rawNode.parentNode; 1616 1617 // Special case for head and body. Setting document.body.outerHTML 1618 // creates an extra <head> tag, and document.head.outerHTML creates 1619 // an extra <body>. So instead we will call replaceChild with the 1620 // parsed DOM, assuming that they aren't trying to set both tags at once. 1621 if (rawNode.tagName === "BODY") { 1622 if (parsedDOM.head.innerHTML === "") { 1623 parentNode.replaceChild(parsedDOM.body, rawNode); 1624 } else { 1625 // eslint-disable-next-line no-unsanitized/property 1626 rawNode.outerHTML = value; 1627 } 1628 } else if (rawNode.tagName === "HEAD") { 1629 if (parsedDOM.body.innerHTML === "") { 1630 parentNode.replaceChild(parsedDOM.head, rawNode); 1631 } else { 1632 // eslint-disable-next-line no-unsanitized/property 1633 rawNode.outerHTML = value; 1634 } 1635 } else if (node.isDocumentElement()) { 1636 // Unable to set outerHTML on the document element. Fall back by 1637 // setting attributes manually. Then replace all the child nodes. 1638 const finalAttributeModifications = []; 1639 const attributeModifications = {}; 1640 for (const attribute of rawNode.attributes) { 1641 attributeModifications[attribute.name] = null; 1642 } 1643 for (const attribute of parsedDOM.documentElement.attributes) { 1644 attributeModifications[attribute.name] = attribute.value; 1645 } 1646 for (const key in attributeModifications) { 1647 finalAttributeModifications.push({ 1648 attributeName: key, 1649 newValue: attributeModifications[key], 1650 }); 1651 } 1652 node.modifyAttributes(finalAttributeModifications); 1653 1654 rawNode.replaceChildren(...parsedDOM.firstElementChild.childNodes); 1655 } else { 1656 // eslint-disable-next-line no-unsanitized/property 1657 rawNode.outerHTML = value; 1658 } 1659 } 1660 1661 /** 1662 * Insert adjacent HTML to a node. 1663 * 1664 * @param {Node} node 1665 * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd", 1666 * "afterEnd" (see Element.insertAdjacentHTML). 1667 * @param {string} value The HTML content. 1668 */ 1669 insertAdjacentHTML(node, position, value) { 1670 if (isNodeDead(node)) { 1671 return { node: [], newParents: [] }; 1672 } 1673 1674 const rawNode = node.rawNode; 1675 const isInsertAsSibling = 1676 position === "beforeBegin" || position === "afterEnd"; 1677 1678 // Don't insert anything adjacent to the document element. 1679 if (isInsertAsSibling && node.isDocumentElement()) { 1680 throw new Error("Can't insert adjacent element to the root."); 1681 } 1682 1683 const rawParentNode = rawNode.parentNode; 1684 if (!rawParentNode && isInsertAsSibling) { 1685 throw new Error("Can't insert as sibling without parent node."); 1686 } 1687 1688 // We can't use insertAdjacentHTML, because we want to return the nodes 1689 // being created (so the front can remove them if the user undoes 1690 // the change). So instead, use Range.createContextualFragment(). 1691 const range = rawNode.ownerDocument.createRange(); 1692 if (position === "beforeBegin" || position === "afterEnd") { 1693 range.selectNode(rawNode); 1694 } else { 1695 range.selectNodeContents(rawNode); 1696 } 1697 // eslint-disable-next-line no-unsanitized/method 1698 const docFrag = range.createContextualFragment(value); 1699 const newRawNodes = Array.from(docFrag.childNodes); 1700 switch (position) { 1701 case "beforeBegin": 1702 rawParentNode.insertBefore(docFrag, rawNode); 1703 break; 1704 case "afterEnd": 1705 // Note: if the second argument is null, rawParentNode.insertBefore 1706 // behaves like rawParentNode.appendChild. 1707 rawParentNode.insertBefore(docFrag, rawNode.nextSibling); 1708 break; 1709 case "afterBegin": 1710 rawNode.insertBefore(docFrag, rawNode.firstChild); 1711 break; 1712 case "beforeEnd": 1713 rawNode.appendChild(docFrag); 1714 break; 1715 default: 1716 throw new Error( 1717 "Invalid position value. Must be either " + 1718 "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'." 1719 ); 1720 } 1721 1722 return this.attachElements(newRawNodes); 1723 } 1724 1725 /** 1726 * Duplicate a specified node 1727 * 1728 * @param {NodeActor} node The node to duplicate. 1729 */ 1730 duplicateNode({ rawNode }) { 1731 const clonedNode = rawNode.cloneNode(true); 1732 rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling); 1733 } 1734 1735 /** 1736 * Test whether a node is a document or a document element. 1737 * 1738 * @param {NodeActor} node The node to remove. 1739 * @return {boolean} True if the node is a document or a document element. 1740 */ 1741 isDocumentOrDocumentElementNode(node) { 1742 return ( 1743 (node.rawNode.ownerDocument && 1744 node.rawNode.ownerDocument.documentElement === this.rawNode) || 1745 node.rawNode.nodeType === Node.DOCUMENT_NODE 1746 ); 1747 } 1748 1749 /** 1750 * Removes a node from its parent node. 1751 * 1752 * @param {NodeActor} node The node to remove. 1753 * @returns The node's nextSibling before it was removed. 1754 */ 1755 removeNode(node) { 1756 if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) { 1757 throw Error("Cannot remove document, document elements or dead nodes."); 1758 } 1759 1760 const nextSibling = this.nextSibling(node); 1761 node.rawNode.remove(); 1762 // Mutation events will take care of the rest. 1763 return nextSibling; 1764 } 1765 1766 /** 1767 * Removes an array of nodes from their parent node. 1768 * 1769 * @param {NodeActor[]} nodes The nodes to remove. 1770 */ 1771 removeNodes(nodes) { 1772 // Check that all nodes are valid before processing the removals. 1773 for (const node of nodes) { 1774 if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) { 1775 throw Error("Cannot remove document, document elements or dead nodes"); 1776 } 1777 } 1778 1779 for (const node of nodes) { 1780 node.rawNode.remove(); 1781 // Mutation events will take care of the rest. 1782 } 1783 } 1784 1785 /** 1786 * Insert a node into the DOM. 1787 */ 1788 insertBefore(node, parent, sibling) { 1789 if ( 1790 isNodeDead(node) || 1791 isNodeDead(parent) || 1792 (sibling && isNodeDead(sibling)) 1793 ) { 1794 return; 1795 } 1796 1797 const rawNode = node.rawNode; 1798 const rawParent = parent.rawNode; 1799 const rawSibling = sibling ? sibling.rawNode : null; 1800 1801 // Don't bother inserting a node if the document position isn't going 1802 // to change. This prevents needless iframes reloading and mutations. 1803 if (rawNode.parentNode === rawParent) { 1804 let currentNextSibling = this.nextSibling(node); 1805 currentNextSibling = currentNextSibling 1806 ? currentNextSibling.rawNode 1807 : null; 1808 1809 if (rawNode === rawSibling || currentNextSibling === rawSibling) { 1810 return; 1811 } 1812 } 1813 1814 rawParent.insertBefore(rawNode, rawSibling); 1815 } 1816 1817 /** 1818 * Editing a node's tagname actually means creating a new node with the same 1819 * attributes, removing the node and inserting the new one instead. 1820 * This method does not return anything as mutation events are taking care of 1821 * informing the consumers about changes. 1822 */ 1823 editTagName(node, tagName) { 1824 if (isNodeDead(node)) { 1825 return null; 1826 } 1827 1828 const oldNode = node.rawNode; 1829 1830 // Create a new element with the same attributes as the current element and 1831 // prepare to replace the current node with it. 1832 let newNode; 1833 try { 1834 newNode = nodeDocument(oldNode).createElement(tagName); 1835 } catch (x) { 1836 // Failed to create a new element with that tag name, ignore the change, 1837 // and signal the error to the front. 1838 return Promise.reject( 1839 new Error("Could not change node's tagName to " + tagName) 1840 ); 1841 } 1842 1843 const attrs = oldNode.attributes; 1844 for (let i = 0; i < attrs.length; i++) { 1845 newNode.setAttribute(attrs[i].name, attrs[i].value); 1846 } 1847 1848 // Insert the new node, and transfer the old node's children. 1849 oldNode.parentNode.insertBefore(newNode, oldNode); 1850 while (oldNode.firstChild) { 1851 newNode.appendChild(oldNode.firstChild); 1852 } 1853 1854 oldNode.remove(); 1855 return null; 1856 } 1857 1858 /** 1859 * Gets the state of the mutation breakpoint types for this actor. 1860 * 1861 * @param {NodeActor} node The node to get breakpoint info for. 1862 */ 1863 getMutationBreakpoints(node) { 1864 let bps; 1865 if (!isNodeDead(node)) { 1866 bps = this._breakpointInfoForNode(node.rawNode); 1867 } 1868 1869 return ( 1870 bps || { 1871 subtree: false, 1872 removal: false, 1873 attribute: false, 1874 } 1875 ); 1876 } 1877 1878 /** 1879 * Set the state of some subset of mutation breakpoint types for this actor. 1880 * 1881 * @param {NodeActor} node The node to set breakpoint info for. 1882 * @param {object} bps A subset of the breakpoints for this actor that 1883 * should be updated to new states. 1884 */ 1885 setMutationBreakpoints(node, bps) { 1886 if (isNodeDead(node)) { 1887 return; 1888 } 1889 const rawNode = node.rawNode; 1890 1891 if ( 1892 rawNode.ownerDocument && 1893 rawNode.getRootNode({ composed: true }) != rawNode.ownerDocument 1894 ) { 1895 // We only allow watching for mutations on nodes that are attached to 1896 // documents. That allows us to clean up our mutation listeners when all 1897 // of the watched nodes have been removed from the document. 1898 return; 1899 } 1900 1901 // This argument has nullable fields so we want to only update boolean 1902 // field values. 1903 const bpsForNode = Object.keys(bps).reduce((obj, bp) => { 1904 if (typeof bps[bp] === "boolean") { 1905 obj[bp] = bps[bp]; 1906 } 1907 return obj; 1908 }, {}); 1909 1910 this._updateMutationBreakpointState("api", rawNode, { 1911 ...this.getMutationBreakpoints(node), 1912 ...bpsForNode, 1913 }); 1914 } 1915 1916 /** 1917 * Update the mutation breakpoint state for the given DOM node. 1918 * 1919 * @param {Node} rawNode The DOM node. 1920 * @param {object} bpsForNode The state of each mutation bp type we support. 1921 */ 1922 _updateMutationBreakpointState(mutationReason, rawNode, bpsForNode) { 1923 const rawDoc = rawNode.ownerDocument || rawNode; 1924 1925 const docMutationBreakpoints = this._mutationBreakpointsForDoc( 1926 rawDoc, 1927 true /* createIfNeeded */ 1928 ); 1929 let originalBpsForNode = this._breakpointInfoForNode(rawNode); 1930 1931 if (!bpsForNode && !originalBpsForNode) { 1932 return; 1933 } 1934 1935 bpsForNode = bpsForNode || {}; 1936 originalBpsForNode = originalBpsForNode || {}; 1937 1938 if (Object.values(bpsForNode).some(Boolean)) { 1939 docMutationBreakpoints.nodes.set(rawNode, bpsForNode); 1940 } else { 1941 docMutationBreakpoints.nodes.delete(rawNode); 1942 } 1943 if (originalBpsForNode.subtree && !bpsForNode.subtree) { 1944 docMutationBreakpoints.counts.subtree -= 1; 1945 } else if (!originalBpsForNode.subtree && bpsForNode.subtree) { 1946 docMutationBreakpoints.counts.subtree += 1; 1947 } 1948 1949 if (originalBpsForNode.removal && !bpsForNode.removal) { 1950 docMutationBreakpoints.counts.removal -= 1; 1951 } else if (!originalBpsForNode.removal && bpsForNode.removal) { 1952 docMutationBreakpoints.counts.removal += 1; 1953 } 1954 1955 if (originalBpsForNode.attribute && !bpsForNode.attribute) { 1956 docMutationBreakpoints.counts.attribute -= 1; 1957 } else if (!originalBpsForNode.attribute && bpsForNode.attribute) { 1958 docMutationBreakpoints.counts.attribute += 1; 1959 } 1960 1961 this._updateDocumentMutationListeners(rawDoc); 1962 1963 const actor = this.getNode(rawNode); 1964 if (actor) { 1965 this.queueMutation({ 1966 target: actor.actorID, 1967 type: "mutationBreakpoint", 1968 mutationBreakpoints: this.getMutationBreakpoints(actor), 1969 mutationReason, 1970 }); 1971 } 1972 } 1973 1974 /** 1975 * Controls whether this DOM document has event listeners attached for 1976 * handling of DOM mutation breakpoints. 1977 * 1978 * @param {Document} rawDoc The DOM document. 1979 */ 1980 _updateDocumentMutationListeners(rawDoc) { 1981 const docMutationBreakpoints = this._mutationBreakpointsForDoc(rawDoc); 1982 if (!docMutationBreakpoints) { 1983 rawDoc.devToolsWatchingDOMMutations = false; 1984 return; 1985 } 1986 1987 const anyBreakpoint = 1988 docMutationBreakpoints.counts.subtree > 0 || 1989 docMutationBreakpoints.counts.removal > 0 || 1990 docMutationBreakpoints.counts.attribute > 0; 1991 1992 rawDoc.devToolsWatchingDOMMutations = anyBreakpoint; 1993 1994 if (docMutationBreakpoints.counts.subtree > 0) { 1995 this.chromeEventHandler.addEventListener( 1996 "devtoolschildinserted", 1997 this.onSubtreeModified, 1998 true /* capture */ 1999 ); 2000 } else { 2001 this.chromeEventHandler.removeEventListener( 2002 "devtoolschildinserted", 2003 this.onSubtreeModified, 2004 true /* capture */ 2005 ); 2006 } 2007 2008 if (anyBreakpoint) { 2009 this.chromeEventHandler.addEventListener( 2010 "devtoolschildremoved", 2011 this.onNodeRemoved, 2012 true /* capture */ 2013 ); 2014 } else { 2015 this.chromeEventHandler.removeEventListener( 2016 "devtoolschildremoved", 2017 this.onNodeRemoved, 2018 true /* capture */ 2019 ); 2020 } 2021 2022 if (docMutationBreakpoints.counts.attribute > 0) { 2023 this.chromeEventHandler.addEventListener( 2024 "devtoolsattrmodified", 2025 this.onAttributeModified, 2026 true /* capture */ 2027 ); 2028 } else { 2029 this.chromeEventHandler.removeEventListener( 2030 "devtoolsattrmodified", 2031 this.onAttributeModified, 2032 true /* capture */ 2033 ); 2034 } 2035 } 2036 2037 _breakOnMutation(mutationType, targetNode, ancestorNode, action) { 2038 this.targetActor.threadActor.pauseForMutationBreakpoint( 2039 mutationType, 2040 targetNode, 2041 ancestorNode, 2042 action 2043 ); 2044 } 2045 2046 _mutationBreakpointsForDoc(rawDoc, createIfNeeded = false) { 2047 let docMutationBreakpoints = this._mutationBreakpoints.get(rawDoc); 2048 if (!docMutationBreakpoints && createIfNeeded) { 2049 docMutationBreakpoints = { 2050 counts: { 2051 subtree: 0, 2052 removal: 0, 2053 attribute: 0, 2054 }, 2055 nodes: new Map(), 2056 }; 2057 this._mutationBreakpoints.set(rawDoc, docMutationBreakpoints); 2058 } 2059 return docMutationBreakpoints; 2060 } 2061 2062 _breakpointInfoForNode(target) { 2063 const docMutationBreakpoints = this._mutationBreakpointsForDoc( 2064 target.ownerDocument || target 2065 ); 2066 return ( 2067 (docMutationBreakpoints && docMutationBreakpoints.nodes.get(target)) || 2068 null 2069 ); 2070 } 2071 2072 onNodeRemoved(evt) { 2073 const mutationBpInfo = this._breakpointInfoForNode(evt.target); 2074 const hasNodeRemovalEvent = mutationBpInfo?.removal; 2075 2076 this._clearMutationBreakpointsFromSubtree(evt.target); 2077 if (hasNodeRemovalEvent) { 2078 this._breakOnMutation("nodeRemoved", evt.target); 2079 } else { 2080 this.onSubtreeModified(evt); 2081 } 2082 } 2083 2084 onAttributeModified(evt) { 2085 const mutationBpInfo = this._breakpointInfoForNode(evt.target); 2086 if (mutationBpInfo?.attribute) { 2087 this._breakOnMutation("attributeModified", evt.target); 2088 } 2089 } 2090 2091 onSubtreeModified(evt) { 2092 const action = evt.type === "devtoolschildinserted" ? "add" : "remove"; 2093 let node = evt.target; 2094 if (node.isNativeAnonymous && !this.showAllAnonymousContent) { 2095 return; 2096 } 2097 while ((node = node.parentNode) !== null) { 2098 const mutationBpInfo = this._breakpointInfoForNode(node); 2099 if (mutationBpInfo?.subtree) { 2100 this._breakOnMutation("subtreeModified", evt.target, node, action); 2101 break; 2102 } 2103 } 2104 } 2105 2106 _clearMutationBreakpointsFromSubtree(targetNode) { 2107 const targetDoc = targetNode.ownerDocument || targetNode; 2108 const docMutationBreakpoints = this._mutationBreakpointsForDoc(targetDoc); 2109 if (!docMutationBreakpoints || docMutationBreakpoints.nodes.size === 0) { 2110 // Bail early for performance. If the doc has no mutation BPs, there is 2111 // no reason to iterate through the children looking for things to detach. 2112 return; 2113 } 2114 2115 // The walker is not limited to the subtree of the argument node, so we 2116 // need to ensure that we stop walking when we leave the subtree. 2117 const nextWalkerSibling = this._getNextTraversalSibling(targetNode); 2118 2119 const walker = new DocumentWalker(targetNode, this.rootWin, { 2120 filter: noAnonymousContentTreeWalkerFilter, 2121 skipTo: SKIP_TO_SIBLING, 2122 }); 2123 2124 do { 2125 this._updateMutationBreakpointState("detach", walker.currentNode, null); 2126 } while (walker.nextNode() && walker.currentNode !== nextWalkerSibling); 2127 } 2128 2129 _getNextTraversalSibling(targetNode) { 2130 const walker = new DocumentWalker(targetNode, this.rootWin, { 2131 filter: noAnonymousContentTreeWalkerFilter, 2132 skipTo: SKIP_TO_SIBLING, 2133 }); 2134 2135 while (!walker.nextSibling()) { 2136 if (!walker.parentNode()) { 2137 // If we try to step past the walker root, there is no next sibling. 2138 return null; 2139 } 2140 } 2141 return walker.currentNode; 2142 } 2143 2144 /** 2145 * Get any pending mutation records. Must be called by the client after 2146 * the `new-mutations` notification is received. Returns an array of 2147 * mutation records. 2148 * 2149 * Mutation records have a basic structure: 2150 * 2151 * { 2152 * type: attributes|characterData|childList, 2153 * target: <domnode actor ID>, 2154 * } 2155 * 2156 * And additional attributes based on the mutation type: 2157 * 2158 * `attributes` type: 2159 * attributeName: <string> - the attribute that changed 2160 * attributeNamespace: <string> - the attribute's namespace URI, if any. 2161 * newValue: <string> - The new value of the attribute, if any. 2162 * 2163 * `characterData` type: 2164 * newValue: <string> - the new nodeValue for the node 2165 * 2166 * `childList` type is returned when the set of children for a node 2167 * has changed. Includes extra data, which can be used by the client to 2168 * maintain its ownership subtree. 2169 * 2170 * added: array of <domnode actor ID> - The list of actors *previously 2171 * seen by the client* that were added to the target node. 2172 * removed: array of <domnode actor ID> The list of actors *previously 2173 * seen by the client* that were removed from the target node. 2174 * inlineTextChild: If the node now has a single text child, it will 2175 * be sent here. 2176 * 2177 * Actors that are included in a MutationRecord's `removed` but 2178 * not in an `added` have been removed from the client's ownership 2179 * tree (either by being moved under a node the client has seen yet 2180 * or by being removed from the tree entirely), and is considered 2181 * 'orphaned'. 2182 * 2183 * Keep in mind that if a node that the client hasn't seen is moved 2184 * into or out of the target node, it will not be included in the 2185 * removedNodes and addedNodes list, so if the client is interested 2186 * in the new set of children it needs to issue a `children` request. 2187 */ 2188 getMutations(options = {}) { 2189 const pending = this._pendingMutations || []; 2190 this._pendingMutations = []; 2191 this._waitingForGetMutations = false; 2192 2193 if (options.cleanup) { 2194 for (const node of this._orphaned) { 2195 // Release the orphaned node. Nodes or children that have been 2196 // retained will be moved to this._retainedOrphans. 2197 this.releaseNode(node); 2198 } 2199 this._orphaned = new Set(); 2200 } 2201 2202 return pending; 2203 } 2204 2205 queueMutation(mutation) { 2206 if (!this.actorID || this._destroyed) { 2207 // We've been destroyed, don't bother queueing this mutation. 2208 return; 2209 } 2210 2211 // Add the mutation to the list of mutations to be retrieved next. 2212 this._pendingMutations.push(mutation); 2213 2214 // Bail out if we already emitted a new-mutations event and are waiting for a client 2215 // to retrieve them. 2216 if (this._waitingForGetMutations) { 2217 return; 2218 } 2219 2220 if (IMMEDIATE_MUTATIONS.includes(mutation.type)) { 2221 this._emitNewMutations(); 2222 } else { 2223 /** 2224 * If many mutations are fired at the same time, clients might sequentially request 2225 * children/siblings for updated nodes, which can be costly. By throttling the calls 2226 * to getMutations, duplicated mutations will be ignored. 2227 */ 2228 this._throttledEmitNewMutations(); 2229 } 2230 } 2231 2232 _emitNewMutations() { 2233 if (!this.actorID || this._destroyed) { 2234 // Bail out if the actor was destroyed after throttling this call. 2235 return; 2236 } 2237 2238 if (this._waitingForGetMutations || !this._pendingMutations.length) { 2239 // Bail out if we already fired the new-mutation event or if no mutations are 2240 // waiting to be retrieved. 2241 return; 2242 } 2243 2244 this._waitingForGetMutations = true; 2245 this.emit("new-mutations"); 2246 } 2247 2248 /** 2249 * Handles mutations from the DOM mutation observer API. 2250 * 2251 * @param array[MutationRecord] mutations 2252 * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord 2253 */ 2254 onMutations(mutations) { 2255 // Don't send a mutation event if the mutation target would be ignored by the walker 2256 // filter function. 2257 if ( 2258 mutations.every( 2259 mutation => 2260 this.documentWalkerFilter(mutation.target) === 2261 nodeFilterConstants.FILTER_SKIP 2262 ) 2263 ) { 2264 return; 2265 } 2266 2267 // Notify any observers that want *all* mutations (even on nodes that aren't 2268 // referenced). This is not sent over the protocol so can only be used by 2269 // scripts running in the server process. 2270 this.emit("any-mutation"); 2271 2272 for (const change of mutations) { 2273 const targetActor = this.getNode(change.target); 2274 if (!targetActor) { 2275 continue; 2276 } 2277 const targetNode = change.target; 2278 const type = change.type; 2279 const mutation = { 2280 type, 2281 target: targetActor.actorID, 2282 }; 2283 2284 if (type === "attributes") { 2285 mutation.attributeName = change.attributeName; 2286 mutation.attributeNamespace = change.attributeNamespace || undefined; 2287 mutation.newValue = targetNode.hasAttribute(mutation.attributeName) 2288 ? targetNode.getAttribute(mutation.attributeName) 2289 : null; 2290 } else if (type === "characterData") { 2291 mutation.newValue = targetNode.nodeValue; 2292 this._maybeQueueInlineTextChildMutation(change, targetNode); 2293 } else if (type === "childList") { 2294 // Get the list of removed and added actors that the client has seen 2295 // so that it can keep its ownership tree up to date. 2296 const removedActors = []; 2297 const addedActors = []; 2298 for (const removed of change.removedNodes) { 2299 this._onMutationsNode(removed, removedActors, "removed"); 2300 } 2301 for (const added of change.addedNodes) { 2302 this._onMutationsNode(added, addedActors, "added"); 2303 } 2304 2305 mutation.numChildren = targetActor.numChildren; 2306 mutation.removed = removedActors; 2307 mutation.added = addedActors; 2308 2309 const inlineTextChild = this.inlineTextChild(targetActor.rawNode); 2310 if (inlineTextChild) { 2311 mutation.inlineTextChild = inlineTextChild.form(); 2312 } 2313 } 2314 this.queueMutation(mutation); 2315 } 2316 } 2317 2318 /** 2319 * Handle a mutation on a node 2320 * 2321 * @param {Element} node 2322 * The element that is added/removed in the mutation 2323 * @param {NodeActor[]} actors 2324 * An array that will be populated by this function with the node actors that 2325 * were added 2326 * @param {string} mutationType 2327 * The type of mutation we're handlign ("added" or "removed") 2328 */ 2329 _onMutationsNode(node, actors, mutationType) { 2330 if (mutationType !== "added" && mutationType !== "removed") { 2331 console.error("Unknown mutation type", mutationType); 2332 return; 2333 } 2334 2335 const actor = this.getNode(node); 2336 if (actor) { 2337 actors.push(actor.actorID); 2338 if (mutationType === "added") { 2339 // The actor is reconnected to the ownership tree, unorphan 2340 // it and let the client know so that its ownership tree is up 2341 // to date. 2342 this._orphaned.delete(actor); 2343 return; 2344 } 2345 if (mutationType === "removed") { 2346 // While removed from the tree, nodes are saved as orphaned. 2347 this._orphaned.add(actor); 2348 return; 2349 } 2350 } 2351 2352 // Here, we might be in a case where a node is remove/added for which we don't have an 2353 // actor for, but do have actors for its children. 2354 if ( 2355 this.documentWalkerFilter(node) !== 2356 nodeFilterConstants.FILTER_ACCEPT_CHILDREN 2357 ) { 2358 // At this point, the client never encountered this actor and the node wasn't ignored, 2359 // so we don't need to tell it about this mutation. 2360 // For added node, if the client wants to see the new nodes it can ask for children. 2361 return; 2362 } 2363 2364 // Otherwise, the node was ignored, so we need to go over its children to find 2365 // actor references we might have. 2366 for (const child of this._rawChildren(node)) { 2367 this._onMutationsNode(child, actors, mutationType); 2368 } 2369 } 2370 2371 /** 2372 * Check if the provided mutation could change the way the target element is 2373 * inlined with its parent node. If it might, a custom mutation of type 2374 * "inlineTextChild" will be queued. 2375 * 2376 * @param {MutationRecord} mutation 2377 * A characterData type mutation 2378 */ 2379 _maybeQueueInlineTextChildMutation(mutation) { 2380 const { oldValue, target } = mutation; 2381 const newValue = target.nodeValue; 2382 const limit = gValueSummaryLength; 2383 2384 if ( 2385 (oldValue.length <= limit && newValue.length <= limit) || 2386 (oldValue.length > limit && newValue.length > limit) 2387 ) { 2388 // Bail out if the new & old values are both below/above the size limit. 2389 return; 2390 } 2391 2392 const parentActor = this.getNode(target.parentNode); 2393 if (!parentActor || parentActor.rawNode.children.length) { 2394 // If the parent node has other children, a character data mutation will 2395 // not change anything regarding inlining text nodes. 2396 return; 2397 } 2398 2399 const inlineTextChild = this.inlineTextChild(parentActor.rawNode); 2400 this.queueMutation({ 2401 type: "inlineTextChild", 2402 target: parentActor.actorID, 2403 inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined, 2404 }); 2405 } 2406 2407 onSlotchange(event) { 2408 const target = event.target; 2409 const targetActor = this.getNode(target); 2410 if (!targetActor) { 2411 return; 2412 } 2413 2414 this.queueMutation({ 2415 type: "slotchange", 2416 target: targetActor.actorID, 2417 }); 2418 } 2419 2420 /** 2421 * Fires when an anonymous root is created. 2422 * This is needed because regular mutation observers don't fire on some kinds 2423 * of NAC creation. We want to treat this like a regular insertion. 2424 */ 2425 onAnonymousrootcreated(event) { 2426 const root = event.target; 2427 2428 // Don't trigger a mutation if the document walker would filter out the element. 2429 if (this.documentWalkerFilter(root) === nodeFilterConstants.FILTER_SKIP) { 2430 return; 2431 } 2432 2433 const parent = this.rawParentNode(root); 2434 if (!parent) { 2435 // These events are async. The node might have been removed already, in 2436 // which case there's nothing to do anymore. 2437 return; 2438 } 2439 // By the time onAnonymousrootremoved fires, the node is already detached 2440 // from its parent, so we need to remember it by hand. 2441 this._anonParents.set(root, parent); 2442 this.onMutations([ 2443 { 2444 type: "childList", 2445 target: parent, 2446 addedNodes: [root], 2447 removedNodes: [], 2448 }, 2449 ]); 2450 } 2451 2452 /** 2453 * @see onAnonymousrootcreated 2454 */ 2455 onAnonymousrootremoved(event) { 2456 const root = event.target; 2457 const parent = this._anonParents.get(root); 2458 if (!parent) { 2459 return; 2460 } 2461 this._anonParents.delete(root); 2462 this.onMutations([ 2463 { 2464 type: "childList", 2465 target: parent, 2466 addedNodes: [], 2467 removedNodes: [root], 2468 }, 2469 ]); 2470 } 2471 2472 onShadowrootattached(event) { 2473 const actor = this.getNode(event.target); 2474 if (!actor) { 2475 return; 2476 } 2477 2478 const mutation = { 2479 type: "shadowRootAttached", 2480 target: actor.actorID, 2481 }; 2482 this.queueMutation(mutation); 2483 } 2484 2485 onFrameLoad({ window, isTopLevel }) { 2486 // By the time we receive the DOMContentLoaded event, we might have been destroyed 2487 if (this._destroyed) { 2488 return; 2489 } 2490 const { readyState } = window.document; 2491 if (readyState != "interactive" && readyState != "complete") { 2492 // The document is not loaded, so we want to register to fire again when the 2493 // DOM has been loaded. 2494 window.addEventListener( 2495 "DOMContentLoaded", 2496 this.onFrameLoad.bind(this, { window, isTopLevel }), 2497 { once: true } 2498 ); 2499 return; 2500 } 2501 2502 window.document.shadowRootAttachedEventEnabled = true; 2503 2504 if (isTopLevel) { 2505 // If we initialize the inspector while the document is loading, 2506 // we may already have a root document set in the constructor. 2507 if ( 2508 this.rootDoc && 2509 this.rootDoc !== window.document && 2510 !Cu.isDeadWrapper(this.rootDoc) && 2511 this.rootDoc.defaultView 2512 ) { 2513 this.onFrameUnload({ window: this.rootDoc.defaultView }); 2514 } 2515 // Update all DOM objects references to target the new document. 2516 this.rootWin = window; 2517 this.rootDoc = window.document; 2518 this.rootNode = this.document(); 2519 this.emit("root-available", this.rootNode); 2520 } else { 2521 const frame = getFrameElement(window); 2522 const frameActor = this.getNode(frame); 2523 if (frameActor) { 2524 // If the parent frame is in the map of known node actors, create the 2525 // actor for the new document and emit a root-available event. 2526 const documentActor = this._getOrCreateNodeActor(window.document); 2527 this.emit("root-available", documentActor); 2528 } 2529 } 2530 } 2531 2532 // Returns true if domNode is in window or a subframe. 2533 _childOfWindow(window, domNode) { 2534 while (domNode) { 2535 const win = nodeDocument(domNode).defaultView; 2536 if (win === window) { 2537 return true; 2538 } 2539 domNode = getFrameElement(win); 2540 } 2541 return false; 2542 } 2543 2544 onFrameUnload({ window }) { 2545 // Any retained orphans that belong to this document 2546 // or its children need to be released, and a mutation sent 2547 // to notify of that. 2548 const releasedOrphans = []; 2549 2550 for (const retained of this._retainedOrphans) { 2551 if ( 2552 Cu.isDeadWrapper(retained.rawNode) || 2553 this._childOfWindow(window, retained.rawNode) 2554 ) { 2555 this._retainedOrphans.delete(retained); 2556 releasedOrphans.push(retained.actorID); 2557 this.releaseNode(retained, { force: true }); 2558 } 2559 } 2560 2561 if (releasedOrphans.length) { 2562 this.queueMutation({ 2563 target: this.rootNode.actorID, 2564 type: "unretained", 2565 nodes: releasedOrphans, 2566 }); 2567 } 2568 2569 const doc = window.document; 2570 const documentActor = this.getNode(doc); 2571 if (!documentActor) { 2572 return; 2573 } 2574 2575 // Removing a frame also removes any mutation breakpoints set on that 2576 // document so that clients can clear their set of active breakpoints. 2577 const mutationBps = this._mutationBreakpointsForDoc(doc); 2578 const nodes = mutationBps ? Array.from(mutationBps.nodes.keys()) : []; 2579 for (const node of nodes) { 2580 this._updateMutationBreakpointState("unload", node, null); 2581 } 2582 2583 this.emit("root-destroyed", documentActor); 2584 2585 // Cleanup root doc references if we just unloaded the top level root 2586 // document. 2587 if (this.rootDoc === doc) { 2588 this.rootDoc = null; 2589 this.rootNode = null; 2590 } 2591 2592 // Release the actor for the unloaded document. 2593 this.releaseNode(documentActor, { force: true }); 2594 } 2595 2596 /** 2597 * Check if a node is attached to the DOM tree of the current page. 2598 * 2599 * @param {Node} rawNode 2600 * @return {boolean} false if the node is removed from the tree or within a 2601 * document fragment 2602 */ 2603 _isInDOMTree(rawNode) { 2604 let walker; 2605 try { 2606 walker = this.getDocumentWalker(rawNode); 2607 } catch (e) { 2608 // The DocumentWalker may throw NS_ERROR_ILLEGAL_VALUE when the node isn't found as a legit children of its parent 2609 // ex: <iframe> manually added as immediate child of another <iframe> 2610 if (e.name == "NS_ERROR_ILLEGAL_VALUE") { 2611 return false; 2612 } 2613 throw e; 2614 } 2615 let current = walker.currentNode; 2616 2617 // Reaching the top of tree 2618 while (walker.parentNode()) { 2619 current = walker.currentNode; 2620 } 2621 2622 // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't 2623 // attached 2624 if ( 2625 current.nodeType === Node.DOCUMENT_FRAGMENT_NODE || 2626 current !== this.rootDoc 2627 ) { 2628 return false; 2629 } 2630 2631 // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc 2632 return true; 2633 } 2634 2635 /** 2636 * @see _isInDomTree 2637 */ 2638 isInDOMTree(node) { 2639 if (isNodeDead(node)) { 2640 return false; 2641 } 2642 return this._isInDOMTree(node.rawNode); 2643 } 2644 2645 /** 2646 * Given a windowID return the NodeActor for the corresponding frameElement, 2647 * unless it's the root window 2648 */ 2649 getNodeActorFromWindowID(windowID) { 2650 let win; 2651 2652 try { 2653 win = Services.wm.getOuterWindowWithId(windowID); 2654 } catch (e) { 2655 // ignore 2656 } 2657 2658 if (!win) { 2659 return { 2660 error: "noWindow", 2661 message: "The related docshell is destroyed or not found", 2662 }; 2663 } else if (!win.frameElement) { 2664 // the frame element of the root document is privileged & thus 2665 // inaccessible, so return the document body/element instead 2666 return this.attachElement( 2667 win.document.body || win.document.documentElement 2668 ); 2669 } 2670 2671 return this.attachElement(win.frameElement); 2672 } 2673 2674 /** 2675 * Given a contentDomReference return the NodeActor for the corresponding frameElement. 2676 */ 2677 getNodeActorFromContentDomReference(contentDomReference) { 2678 let rawNode = lazy.ContentDOMReference.resolve(contentDomReference); 2679 if (!rawNode || !this._isInDOMTree(rawNode)) { 2680 return null; 2681 } 2682 2683 // This is a special case for the document object whereby it is considered 2684 // as document.documentElement (the <html> node) 2685 if (rawNode.defaultView && rawNode === rawNode.defaultView.document) { 2686 rawNode = rawNode.documentElement; 2687 } 2688 2689 return this.attachElement(rawNode); 2690 } 2691 2692 /** 2693 * Given a StyleSheet resource ID, commonly used in the style-editor, get its 2694 * ownerNode and return the corresponding walker's NodeActor. 2695 * Note that getNodeFromActor was added later and can now be used instead. 2696 */ 2697 getStyleSheetOwnerNode(resourceId) { 2698 const manager = this.targetActor.getStyleSheetsManager(); 2699 const ownerNode = manager.getOwnerNode(resourceId); 2700 return this.attachElement(ownerNode); 2701 } 2702 2703 /** 2704 * This method can be used to retrieve NodeActor for DOM nodes from other 2705 * actors in a way that they can later be highlighted in the page, or 2706 * selected in the inspector. 2707 * If an actor has a reference to a DOM node, and the UI needs to know about 2708 * this DOM node (and possibly select it in the inspector), the UI should 2709 * first retrieve a reference to the walkerFront: 2710 * 2711 * // Make sure the inspector/walker have been initialized first. 2712 * const inspectorFront = await toolbox.target.getFront("inspector"); 2713 * // Retrieve the walker. 2714 * const walker = inspectorFront.walker; 2715 * 2716 * And then call this method: 2717 * 2718 * // Get the nodeFront from my actor, passing the ID and properties path. 2719 * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => { 2720 * // Use the nodeFront, e.g. select the node in the inspector. 2721 * toolbox.getPanel("inspector").selection.setNodeFront(nodeFront); 2722 * }); 2723 * 2724 * @param {string} actorID The ID for the actor that has a reference to the 2725 * DOM node. 2726 * @param {Array} path Where, on the actor, is the DOM node stored. If in the 2727 * scope of the actor, the node is available as `this.data.node`, then this 2728 * should be ["data", "node"]. 2729 * @return {NodeActor} The attached NodeActor, or null if it couldn't be 2730 * found. 2731 */ 2732 getNodeFromActor(actorID, path) { 2733 const actor = this.conn.getActor(actorID); 2734 if (!actor) { 2735 return null; 2736 } 2737 2738 let obj = actor; 2739 for (const name of path) { 2740 if (!(name in obj)) { 2741 return null; 2742 } 2743 obj = obj[name]; 2744 } 2745 2746 return this.attachElement(obj); 2747 } 2748 2749 /** 2750 * Returns an instance of the LayoutActor that is used to retrieve CSS layout-related 2751 * information. 2752 * 2753 * @return {LayoutActor} 2754 */ 2755 getLayoutInspector() { 2756 if (!this.layoutActor) { 2757 this.layoutActor = new LayoutActor(this.conn, this.targetActor, this); 2758 } 2759 2760 return this.layoutActor; 2761 } 2762 2763 /** 2764 * Returns the parent grid DOMNode of the given node if it exists, otherwise, it 2765 * returns null. 2766 */ 2767 getParentGridNode(node) { 2768 if (isNodeDead(node)) { 2769 return null; 2770 } 2771 2772 const parentGridNode = findGridParentContainerForNode(node.rawNode); 2773 return parentGridNode ? this._getOrCreateNodeActor(parentGridNode) : null; 2774 } 2775 2776 /** 2777 * Returns the offset parent DOMNode of the given node if it exists, otherwise, it 2778 * returns null. 2779 */ 2780 getOffsetParent(node) { 2781 if (isNodeDead(node)) { 2782 return null; 2783 } 2784 2785 const offsetParent = node.rawNode.offsetParent; 2786 2787 if (!offsetParent) { 2788 return null; 2789 } 2790 2791 return this._getOrCreateNodeActor(offsetParent); 2792 } 2793 2794 getEmbedderElement(browsingContextID) { 2795 const browsingContext = BrowsingContext.get(browsingContextID); 2796 let rawNode = browsingContext.embedderElement; 2797 if (!this._isInDOMTree(rawNode)) { 2798 return null; 2799 } 2800 2801 // This is a special case for the document object whereby it is considered 2802 // as document.documentElement (the <html> node) 2803 if (rawNode.defaultView && rawNode === rawNode.defaultView.document) { 2804 rawNode = rawNode.documentElement; 2805 } 2806 2807 return this.attachElement(rawNode); 2808 } 2809 2810 pick(doFocus, isLocalTab) { 2811 this.nodePicker.pick(doFocus, isLocalTab); 2812 } 2813 2814 cancelPick() { 2815 this.nodePicker.cancelPick(); 2816 } 2817 2818 clearPicker() { 2819 this.nodePicker.resetHoveredNodeReference(); 2820 } 2821 2822 /** 2823 * Given a scrollable node, find its descendants which are causing overflow in it and 2824 * add their raw nodes to the map as keys with the scrollable element as the values. 2825 * 2826 * @param {NodeActor} scrollableNode A scrollable node. 2827 * @param {Map} map The map to which the overflow causing elements are added. 2828 */ 2829 updateOverflowCausingElements(scrollableNode, map) { 2830 if ( 2831 isNodeDead(scrollableNode) || 2832 scrollableNode.rawNode.nodeType !== Node.ELEMENT_NODE 2833 ) { 2834 return; 2835 } 2836 2837 const overflowCausingChildren = [ 2838 ...InspectorUtils.getOverflowingChildrenOfElement(scrollableNode.rawNode), 2839 ]; 2840 2841 for (let overflowCausingChild of overflowCausingChildren) { 2842 // overflowCausingChild is a Node, but not necessarily an Element. 2843 // So, get the containing Element 2844 if (overflowCausingChild.nodeType !== Node.ELEMENT_NODE) { 2845 overflowCausingChild = overflowCausingChild.parentElement; 2846 } 2847 map.set(overflowCausingChild, scrollableNode); 2848 } 2849 } 2850 2851 /** 2852 * Returns an array of the overflow causing elements' NodeActor for the given node. 2853 * 2854 * @param {NodeActor} node The scrollable node. 2855 * @return {Array<NodeActor>} An array of the overflow causing elements. 2856 */ 2857 getOverflowCausingElements(node) { 2858 if ( 2859 isNodeDead(node) || 2860 node.rawNode.nodeType !== Node.ELEMENT_NODE || 2861 !node.isScrollable 2862 ) { 2863 return []; 2864 } 2865 2866 const overflowCausingElements = [ 2867 ...InspectorUtils.getOverflowingChildrenOfElement(node.rawNode), 2868 ].map(overflowCausingChild => { 2869 if (overflowCausingChild.nodeType !== Node.ELEMENT_NODE) { 2870 overflowCausingChild = overflowCausingChild.parentElement; 2871 } 2872 2873 return overflowCausingChild; 2874 }); 2875 2876 return this.attachElements(overflowCausingElements); 2877 } 2878 2879 /** 2880 * Return the scrollable ancestor node which has overflow because of the given node. 2881 * 2882 * @param {NodeActor} overflowCausingNode 2883 */ 2884 getScrollableAncestorNode(overflowCausingNode) { 2885 if ( 2886 isNodeDead(overflowCausingNode) || 2887 !this.overflowCausingElementsMap.has(overflowCausingNode.rawNode) 2888 ) { 2889 return null; 2890 } 2891 2892 return this.overflowCausingElementsMap.get(overflowCausingNode.rawNode); 2893 } 2894 } 2895 2896 exports.WalkerActor = WalkerActor;