node.js (17218B)
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 { 8 FrontClassWithSpec, 9 types, 10 registerFront, 11 } = require("resource://devtools/shared/protocol.js"); 12 const { 13 nodeSpec, 14 nodeListSpec, 15 } = require("resource://devtools/shared/specs/node.js"); 16 const { 17 SimpleStringFront, 18 } = require("resource://devtools/client/fronts/string.js"); 19 20 loader.lazyRequireGetter( 21 this, 22 "nodeConstants", 23 "resource://devtools/shared/dom-node-constants.js" 24 ); 25 26 const { XPCOMUtils } = ChromeUtils.importESModule( 27 "resource://gre/modules/XPCOMUtils.sys.mjs" 28 ); 29 30 XPCOMUtils.defineLazyPreferenceGetter( 31 this, 32 "browserToolboxScope", 33 "devtools.browsertoolbox.scope" 34 ); 35 36 const BROWSER_TOOLBOX_SCOPE_EVERYTHING = "everything"; 37 38 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; 39 40 /** 41 * Client side of a node list as returned by querySelectorAll() 42 */ 43 class NodeListFront extends FrontClassWithSpec(nodeListSpec) { 44 marshallPool() { 45 return this.getParent(); 46 } 47 48 // Update the object given a form representation off the wire. 49 form(json) { 50 this.length = json.length; 51 } 52 53 item(index) { 54 return super.item(index).then(response => { 55 return response.node; 56 }); 57 } 58 59 items(start, end) { 60 return super.items(start, end).then(response => { 61 return response.nodes; 62 }); 63 } 64 } 65 66 registerFront(NodeListFront); 67 68 /** 69 * Convenience API for building a list of attribute modifications 70 * for the `modifyAttributes` request. 71 */ 72 class AttributeModificationList { 73 constructor(node) { 74 this.node = node; 75 this.modifications = []; 76 } 77 78 apply() { 79 const ret = this.node.modifyAttributes(this.modifications); 80 return ret; 81 } 82 83 destroy() { 84 this.node = null; 85 this.modification = null; 86 } 87 88 setAttributeNS(ns, name, value) { 89 this.modifications.push({ 90 attributeNamespace: ns, 91 attributeName: name, 92 newValue: value, 93 }); 94 } 95 96 setAttribute(name, value) { 97 this.setAttributeNS(undefined, name, value); 98 } 99 100 removeAttributeNS(ns, name) { 101 this.setAttributeNS(ns, name, undefined); 102 } 103 104 removeAttribute(name) { 105 this.setAttributeNS(undefined, name, undefined); 106 } 107 } 108 109 /** 110 * Client side of the node actor. 111 * 112 * Node fronts are strored in a tree that mirrors the DOM tree on the 113 * server, but with a few key differences: 114 * - Not all children will be necessary loaded for each node. 115 * - The order of children isn't guaranteed to be the same as the DOM. 116 * Children are stored in a doubly-linked list, to make addition/removal 117 * and traversal quick. 118 * 119 * Due to the order/incompleteness of the child list, it is safe to use 120 * the parent node from clients, but the `children` request should be used 121 * to traverse children. 122 */ 123 class NodeFront extends FrontClassWithSpec(nodeSpec) { 124 constructor(conn, targetFront, parentFront) { 125 super(conn, targetFront, parentFront); 126 // The parent node 127 this._parent = null; 128 // The first child of this node. 129 this._child = null; 130 // The next sibling of this node. 131 this._next = null; 132 // The previous sibling of this node. 133 this._prev = null; 134 // Store the flag to use it after destroy, where targetFront is set to null. 135 this._hasParentProcessTarget = targetFront.isParentProcess; 136 } 137 138 /** 139 * Destroy a node front. The node must have been removed from the 140 * ownership tree before this is called, unless the whole walker front 141 * is being destroyed. 142 */ 143 destroy() { 144 super.destroy(); 145 } 146 147 // Update the object given a form representation off the wire. 148 form(form, ctx) { 149 // backward-compatibility: shortValue indicates we are connected to old server 150 if (form.shortValue) { 151 // If the value is not complete, set nodeValue to null, it will be fetched 152 // when calling getNodeValue() 153 form.nodeValue = form.incompleteValue ? null : form.shortValue; 154 } 155 156 this.traits = form.traits || {}; 157 158 // Shallow copy of the form. We could just store a reference, but 159 // eventually we'll want to update some of the data. 160 this._form = Object.assign({}, form); 161 this._form.attrs = this._form.attrs ? this._form.attrs.slice() : []; 162 163 if (form.parent) { 164 // Get the owner actor for this actor (the walker), and find the 165 // parent node of this actor from it, creating a standin node if 166 // necessary. 167 const owner = ctx.marshallPool(); 168 if (typeof owner.ensureDOMNodeFront === "function") { 169 const parentNodeFront = owner.ensureDOMNodeFront(form.parent); 170 this.reparent(parentNodeFront); 171 } 172 } 173 174 if (form.host) { 175 const owner = ctx.marshallPool(); 176 if (typeof owner.ensureDOMNodeFront === "function") { 177 this.host = owner.ensureDOMNodeFront(form.host); 178 } 179 } 180 181 if (form.inlineTextChild) { 182 this.inlineTextChild = types 183 .getType("domnode") 184 .read(form.inlineTextChild, ctx); 185 } else { 186 this.inlineTextChild = undefined; 187 } 188 } 189 190 /** 191 * Returns the parent NodeFront for this NodeFront. 192 */ 193 parentNode() { 194 return this._parent; 195 } 196 197 /** 198 * Returns the NodeFront corresponding to the parentNode of this NodeFront, or the 199 * NodeFront corresponding to the host element for shadowRoot elements. 200 */ 201 parentOrHost() { 202 return this.isShadowRoot ? this.host : this._parent; 203 } 204 205 /** 206 * Returns the owner DocumentElement|ShadowRootElement NodeFront for this NodeFront, 207 * or null if such element can't be found. 208 * 209 * @returns {NodeFront|null} 210 */ 211 getOwnerRootNodeFront() { 212 let currentNode = this; 213 while (currentNode) { 214 if ( 215 currentNode.isShadowRoot || 216 currentNode.nodeType === Node.DOCUMENT_NODE 217 ) { 218 return currentNode; 219 } 220 221 currentNode = currentNode.parentNode(); 222 } 223 224 return null; 225 } 226 227 /** 228 * Process a mutation entry as returned from the walker's `getMutations` 229 * request. Only tries to handle changes of the node's contents 230 * themselves (character data and attribute changes), the walker itself 231 * will keep the ownership tree up to date. 232 */ 233 updateMutation(change) { 234 if (change.type === "attributes") { 235 // We'll need to lazily reparse the attributes after this change. 236 this._attrMap = undefined; 237 238 // Update any already-existing attributes. 239 let found = false; 240 for (let i = 0; i < this.attributes.length; i++) { 241 const attr = this.attributes[i]; 242 if ( 243 attr.name == change.attributeName && 244 attr.namespace == change.attributeNamespace 245 ) { 246 if (change.newValue !== null) { 247 attr.value = change.newValue; 248 } else { 249 this.attributes.splice(i, 1); 250 } 251 found = true; 252 break; 253 } 254 } 255 // This is a new attribute. The null check is because of Bug 1192270, 256 // in the case of a newly added then removed attribute 257 if (!found && change.newValue !== null) { 258 this.attributes.push({ 259 name: change.attributeName, 260 namespace: change.attributeNamespace, 261 value: change.newValue, 262 }); 263 } 264 } else if (change.type === "characterData") { 265 this._form.nodeValue = change.newValue; 266 } else if (change.type === "pseudoClassLock") { 267 this._form.pseudoClassLocks = change.pseudoClassLocks; 268 } else if (change.type === "events") { 269 this._form.hasEventListeners = change.hasEventListeners; 270 } else if (change.type === "mutationBreakpoint") { 271 this._form.mutationBreakpoints = change.mutationBreakpoints; 272 } 273 } 274 275 // Some accessors to make NodeFront feel more like a Node 276 277 get id() { 278 return this.getAttribute("id"); 279 } 280 281 get nodeType() { 282 return this._form.nodeType; 283 } 284 get namespaceURI() { 285 return this._form.namespaceURI; 286 } 287 get nodeName() { 288 return this._form.nodeName; 289 } 290 get displayName() { 291 // @backward-compat { version 147 } When 147 reaches release, we can remove this 'if' block. 292 // The form's displayName will be populated correctly for pseudo elements. 293 if ( 294 this.isPseudoElement && 295 !this.traits.hasPseudoElementNameInDisplayName 296 ) { 297 if (this.isMarkerPseudoElement) { 298 return "::marker"; 299 } 300 if (this.isBeforePseudoElement) { 301 return "::before"; 302 } 303 if (this.isAfterPseudoElement) { 304 return "::after"; 305 } 306 } 307 308 return this._form.displayName; 309 } 310 get doctypeString() { 311 return ( 312 "<!DOCTYPE " + 313 this._form.name + 314 (this._form.publicId ? ' PUBLIC "' + this._form.publicId + '"' : "") + 315 (this._form.systemId ? ' "' + this._form.systemId + '"' : "") + 316 ">" 317 ); 318 } 319 320 get baseURI() { 321 return this._form.baseURI; 322 } 323 324 get browsingContextID() { 325 return this._form.browsingContextID; 326 } 327 328 get className() { 329 return this.getAttribute("class") || ""; 330 } 331 332 // Check if the node has children but the current DevTools session is unable 333 // to retrieve them. 334 // Typically: a <frame> or <browser> element which loads a document in another 335 // process, but the toolbox' configuration prevents to inspect it (eg the 336 // parent-process only Browser Toolbox). 337 get childrenUnavailable() { 338 return ( 339 // If form.useChildTargetToFetchChildren is true, it means the node HAS 340 // children in another target. 341 // Note: useChildTargetToFetchChildren might be undefined, force 342 // conversion to boolean. See Bug 1783613 to try and improve this. 343 !!this._form.useChildTargetToFetchChildren && 344 // But if useChildTargetToFetchChildren is false, it means the client 345 // configuration prevents from displaying such children. 346 // This is the only case where children are considered as unavailable: 347 // they exist, but can't be retrieved by configuration. 348 !this.useChildTargetToFetchChildren 349 ); 350 } 351 get hasChildren() { 352 return this.numChildren > 0; 353 } 354 get numChildren() { 355 if (this.childrenUnavailable) { 356 return 0; 357 } 358 359 return this._form.numChildren; 360 } 361 get useChildTargetToFetchChildren() { 362 if ( 363 this._hasParentProcessTarget && 364 browserToolboxScope != BROWSER_TOOLBOX_SCOPE_EVERYTHING 365 ) { 366 return false; 367 } 368 369 return !!this._form.useChildTargetToFetchChildren; 370 } 371 get hasEventListeners() { 372 return this._form.hasEventListeners; 373 } 374 375 get isMarkerPseudoElement() { 376 return this._form.isMarkerPseudoElement; 377 } 378 get isBeforePseudoElement() { 379 return this._form.isBeforePseudoElement; 380 } 381 get isAfterPseudoElement() { 382 return this._form.isAfterPseudoElement; 383 } 384 get isPseudoElement() { 385 // @backward-compat { version 147 } When 147 reaches release, we can remove this 'if' block, 386 // as well as the isXXXPseudoElement getters. 387 // The form isPseudoElement property will be populated correctly. 388 if (!this.traits.hasPseudoElementNameInDisplayName) { 389 return ( 390 this.isBeforePseudoElement || 391 this.isAfterPseudoElement || 392 this.isMarkerPseudoElement 393 ); 394 } 395 396 return this._form.isPseudoElement; 397 } 398 get isNativeAnonymous() { 399 return this._form.isNativeAnonymous; 400 } 401 get isInHTMLDocument() { 402 return this._form.isInHTMLDocument; 403 } 404 get tagName() { 405 return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null; 406 } 407 408 get isDocumentElement() { 409 return !!this._form.isDocumentElement; 410 } 411 412 get isTopLevelDocument() { 413 return this._form.isTopLevelDocument; 414 } 415 416 get isShadowRoot() { 417 return this._form.isShadowRoot; 418 } 419 420 get shadowRootMode() { 421 return this._form.shadowRootMode; 422 } 423 424 get isShadowHost() { 425 return this._form.isShadowHost; 426 } 427 428 get customElementLocation() { 429 return this._form.customElementLocation; 430 } 431 432 get isDirectShadowHostChild() { 433 return this._form.isDirectShadowHostChild; 434 } 435 436 // doctype properties 437 get name() { 438 return this._form.name; 439 } 440 get publicId() { 441 return this._form.publicId; 442 } 443 get systemId() { 444 return this._form.systemId; 445 } 446 447 getAttribute(name) { 448 const attr = this._getAttribute(name); 449 return attr ? attr.value : null; 450 } 451 hasAttribute(name) { 452 this._cacheAttributes(); 453 return name in this._attrMap; 454 } 455 456 get hidden() { 457 const cls = this.getAttribute("class"); 458 return cls && cls.indexOf(HIDDEN_CLASS) > -1; 459 } 460 461 get attributes() { 462 return this._form.attrs; 463 } 464 465 get mutationBreakpoints() { 466 return this._form.mutationBreakpoints; 467 } 468 469 get pseudoClassLocks() { 470 return this._form.pseudoClassLocks || []; 471 } 472 hasPseudoClassLock(pseudo) { 473 return this.pseudoClassLocks.some(locked => locked === pseudo); 474 } 475 476 get displayType() { 477 return this._form.displayType; 478 } 479 480 get isDisplayed() { 481 return this._form.isDisplayed; 482 } 483 484 get isScrollable() { 485 return this._form.isScrollable; 486 } 487 488 get causesOverflow() { 489 return this._form.causesOverflow; 490 } 491 492 get containerType() { 493 return this._form.containerType; 494 } 495 496 get anchorName() { 497 return this._form.anchorName; 498 } 499 500 get isTreeDisplayed() { 501 let parent = this; 502 while (parent) { 503 if (!parent.isDisplayed) { 504 return false; 505 } 506 parent = parent.parentNode(); 507 } 508 return true; 509 } 510 511 get inspectorFront() { 512 return this.parentFront.parentFront; 513 } 514 515 get walkerFront() { 516 return this.parentFront; 517 } 518 519 getNodeValue() { 520 // backward-compatibility: if nodevalue is null and shortValue is defined, the actual 521 // value of the node needs to be fetched on the server. 522 if (this._form.nodeValue === null && this._form.shortValue) { 523 return super.getNodeValue(); 524 } 525 526 const str = this._form.nodeValue || ""; 527 return Promise.resolve(new SimpleStringFront(str)); 528 } 529 530 /** 531 * Return a new AttributeModificationList for this node. 532 */ 533 startModifyingAttributes() { 534 return new AttributeModificationList(this); 535 } 536 537 _cacheAttributes() { 538 if (typeof this._attrMap != "undefined") { 539 return; 540 } 541 this._attrMap = {}; 542 for (const attr of this.attributes) { 543 this._attrMap[attr.name] = attr; 544 } 545 } 546 547 _getAttribute(name) { 548 this._cacheAttributes(); 549 return this._attrMap[name] || undefined; 550 } 551 552 /** 553 * Set this node's parent. Note that the children saved in 554 * this tree are unordered and incomplete, so shouldn't be used 555 * instead of a `children` request. 556 */ 557 reparent(parent) { 558 if (this._parent === parent) { 559 return; 560 } 561 562 if (this._parent && this._parent._child === this) { 563 this._parent._child = this._next; 564 } 565 if (this._prev) { 566 this._prev._next = this._next; 567 } 568 if (this._next) { 569 this._next._prev = this._prev; 570 } 571 this._next = null; 572 this._prev = null; 573 this._parent = parent; 574 if (!parent) { 575 // Subtree is disconnected, we're done 576 return; 577 } 578 this._next = parent._child; 579 if (this._next) { 580 this._next._prev = this; 581 } 582 parent._child = this; 583 } 584 585 /** 586 * Return all the known children of this node. 587 */ 588 treeChildren() { 589 const ret = []; 590 for (let child = this._child; child != null; child = child._next) { 591 ret.push(child); 592 } 593 return ret; 594 } 595 596 /** 597 * Do we use a local target? 598 * Useful to know if a rawNode is available or not. 599 * 600 * This will, one day, be removed. External code should 601 * not need to know if the target is remote or not. 602 */ 603 isLocalToBeDeprecated() { 604 return !!this.conn._transport._serverConnection; 605 } 606 607 /** 608 * Get a Node for the given node front. This only works locally, 609 * and is only intended as a stopgap during the transition to the remote 610 * protocol. If you depend on this you're likely to break soon. 611 */ 612 rawNode() { 613 if (!this.isLocalToBeDeprecated()) { 614 console.warn("Tried to use rawNode on a remote connection."); 615 return null; 616 } 617 const { 618 DevToolsServer, 619 } = require("resource://devtools/server/devtools-server.js"); 620 const actor = DevToolsServer.searchAllConnectionsForActor(this.actorID); 621 if (!actor) { 622 // Can happen if we try to get the raw node for an already-expired 623 // actor. 624 return null; 625 } 626 return actor.rawNode; 627 } 628 629 async connectToFrame() { 630 if (!this.useChildTargetToFetchChildren) { 631 console.warn("Tried to open connection to an invalid frame."); 632 return null; 633 } 634 if ( 635 this._childBrowsingContextTarget && 636 !this._childBrowsingContextTarget.isDestroyed() 637 ) { 638 return this._childBrowsingContextTarget; 639 } 640 641 // Get the target for this frame element 642 this._childBrowsingContextTarget = 643 await this.targetFront.getWindowGlobalTarget( 644 this._form.browsingContextID 645 ); 646 647 // Bug 1776250: When the target is destroyed, we need to easily find the 648 // parent node front so that we can update its frontend container in the 649 // markup-view. 650 this._childBrowsingContextTarget.setParentNodeFront(this); 651 652 return this._childBrowsingContextTarget; 653 } 654 } 655 656 exports.NodeFront = NodeFront; 657 registerFront(NodeFront);