/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { FrontClassWithSpec, types, registerFront, } = require("resource://devtools/shared/protocol.js"); const { nodeSpec, nodeListSpec, } = require("resource://devtools/shared/specs/node.js"); const { SimpleStringFront, } = require("resource://devtools/client/fronts/string.js"); loader.lazyRequireGetter( this, "nodeConstants", "resource://devtools/shared/dom-node-constants.js" ); const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); XPCOMUtils.defineLazyPreferenceGetter( this, "browserToolboxScope", "devtools.browsertoolbox.scope" ); const BROWSER_TOOLBOX_SCOPE_EVERYTHING = "everything"; const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; /** * Client side of a node list as returned by querySelectorAll() */ class NodeListFront extends FrontClassWithSpec(nodeListSpec) { marshallPool() { return this.getParent(); } // Update the object given a form representation off the wire. form(json) { this.length = json.length; } item(index) { return super.item(index).then(response => { return response.node; }); } items(start, end) { return super.items(start, end).then(response => { return response.nodes; }); } } exports.NodeListFront = NodeListFront; registerFront(NodeListFront); /** * Convenience API for building a list of attribute modifications * for the `modifyAttributes` request. */ class AttributeModificationList { constructor(node) { this.node = node; this.modifications = []; } apply() { const ret = this.node.modifyAttributes(this.modifications); return ret; } destroy() { this.node = null; this.modification = null; } setAttributeNS(ns, name, value) { this.modifications.push({ attributeNamespace: ns, attributeName: name, newValue: value, }); } setAttribute(name, value) { this.setAttributeNS(undefined, name, value); } removeAttributeNS(ns, name) { this.setAttributeNS(ns, name, undefined); } removeAttribute(name) { this.setAttributeNS(undefined, name, undefined); } } /** * Client side of the node actor. * * Node fronts are strored in a tree that mirrors the DOM tree on the * server, but with a few key differences: * - Not all children will be necessary loaded for each node. * - The order of children isn't guaranteed to be the same as the DOM. * Children are stored in a doubly-linked list, to make addition/removal * and traversal quick. * * Due to the order/incompleteness of the child list, it is safe to use * the parent node from clients, but the `children` request should be used * to traverse children. */ class NodeFront extends FrontClassWithSpec(nodeSpec) { constructor(conn, targetFront, parentFront) { super(conn, targetFront, parentFront); // The parent node this._parent = null; // The first child of this node. this._child = null; // The next sibling of this node. this._next = null; // The previous sibling of this node. this._prev = null; // Store the flag to use it after destroy, where targetFront is set to null. this._hasParentProcessTarget = targetFront.isParentProcess; } /** * Destroy a node front. The node must have been removed from the * ownership tree before this is called, unless the whole walker front * is being destroyed. */ destroy() { super.destroy(); } // Update the object given a form representation off the wire. form(form, ctx) { // backward-compatibility: shortValue indicates we are connected to old server if (form.shortValue) { // If the value is not complete, set nodeValue to null, it will be fetched // when calling getNodeValue() form.nodeValue = form.incompleteValue ? null : form.shortValue; } this.traits = form.traits || {}; // Shallow copy of the form. We could just store a reference, but // eventually we'll want to update some of the data. this._form = Object.assign({}, form); this._form.attrs = this._form.attrs ? this._form.attrs.slice() : []; if (form.parent) { // Get the owner actor for this actor (the walker), and find the // parent node of this actor from it, creating a standin node if // necessary. const owner = ctx.marshallPool(); if (typeof owner.ensureDOMNodeFront === "function") { const parentNodeFront = owner.ensureDOMNodeFront(form.parent); this.reparent(parentNodeFront); } } if (form.host) { const owner = ctx.marshallPool(); if (typeof owner.ensureDOMNodeFront === "function") { this.host = owner.ensureDOMNodeFront(form.host); } } if (form.inlineTextChild) { this.inlineTextChild = types .getType("domnode") .read(form.inlineTextChild, ctx); } else { this.inlineTextChild = undefined; } } /** * Returns the parent NodeFront for this NodeFront. */ parentNode() { return this._parent; } /** * Returns the NodeFront corresponding to the parentNode of this NodeFront, or the * NodeFront corresponding to the host element for shadowRoot elements. */ parentOrHost() { return this.isShadowRoot ? this.host : this._parent; } /** * Returns the owner DocumentElement|ShadowRootElement NodeFront for this NodeFront, * or null if such element can't be found. * * @returns {NodeFront|null} */ getOwnerRootNodeFront() { let currentNode = this; while (currentNode) { if ( currentNode.isShadowRoot || currentNode.nodeType === Node.DOCUMENT_NODE ) { return currentNode; } currentNode = currentNode.parentNode(); } return null; } /** * Process a mutation entry as returned from the walker's `getMutations` * request. Only tries to handle changes of the node's contents * themselves (character data and attribute changes), the walker itself * will keep the ownership tree up to date. */ updateMutation(change) { if (change.type === "attributes") { // We'll need to lazily reparse the attributes after this change. this._attrMap = undefined; // Update any already-existing attributes. let found = false; for (let i = 0; i < this.attributes.length; i++) { const attr = this.attributes[i]; if ( attr.name == change.attributeName && attr.namespace == change.attributeNamespace ) { if (change.newValue !== null) { attr.value = change.newValue; } else { this.attributes.splice(i, 1); } found = true; break; } } // This is a new attribute. The null check is because of Bug 1192270, // in the case of a newly added then removed attribute if (!found && change.newValue !== null) { this.attributes.push({ name: change.attributeName, namespace: change.attributeNamespace, value: change.newValue, }); } } else if (change.type === "characterData") { this._form.nodeValue = change.newValue; } else if (change.type === "pseudoClassLock") { this._form.pseudoClassLocks = change.pseudoClassLocks; } else if (change.type === "events") { this._form.hasEventListeners = change.hasEventListeners; } else if (change.type === "mutationBreakpoint") { this._form.mutationBreakpoints = change.mutationBreakpoints; } } // Some accessors to make NodeFront feel more like a Node get id() { return this.getAttribute("id"); } get nodeType() { return this._form.nodeType; } get namespaceURI() { return this._form.namespaceURI; } get nodeName() { return this._form.nodeName; } get displayName() { const { displayName, nodeName } = this._form; // Keep `nodeName.toLowerCase()` for backward compatibility return displayName || nodeName.toLowerCase(); } get doctypeString() { return ( "" ); } get baseURI() { return this._form.baseURI; } get browsingContextID() { return this._form.browsingContextID; } get className() { return this.getAttribute("class") || ""; } // Check if the node has children but the current DevTools session is unable // to retrieve them. // Typically: a or element which loads a document in another // process, but the toolbox' configuration prevents to inspect it (eg the // parent-process only Browser Toolbox). get childrenUnavailable() { return ( // If form.useChildTargetToFetchChildren is true, it means the node HAS // children in another target. // Note: useChildTargetToFetchChildren might be undefined, force // conversion to boolean. See Bug 1783613 to try and improve this. !!this._form.useChildTargetToFetchChildren && // But if useChildTargetToFetchChildren is false, it means the client // configuration prevents from displaying such children. // This is the only case where children are considered as unavailable: // they exist, but can't be retrieved by configuration. !this.useChildTargetToFetchChildren ); } get hasChildren() { return this.numChildren > 0; } get numChildren() { if (this.childrenUnavailable) { return 0; } return this._form.numChildren; } get useChildTargetToFetchChildren() { if ( this._hasParentProcessTarget && browserToolboxScope != BROWSER_TOOLBOX_SCOPE_EVERYTHING ) { return false; } return !!this._form.useChildTargetToFetchChildren; } get hasEventListeners() { return this._form.hasEventListeners; } get isMarkerPseudoElement() { return this._form.isMarkerPseudoElement; } get isBeforePseudoElement() { return this._form.isBeforePseudoElement; } get isAfterPseudoElement() { return this._form.isAfterPseudoElement; } get isPseudoElement() { return ( this.isBeforePseudoElement || this.isAfterPseudoElement || this.isMarkerPseudoElement ); } get isAnonymous() { return this._form.isAnonymous; } get isInHTMLDocument() { return this._form.isInHTMLDocument; } get tagName() { return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null; } get isDocumentElement() { return !!this._form.isDocumentElement; } get isTopLevelDocument() { return this._form.isTopLevelDocument; } get isShadowRoot() { return this._form.isShadowRoot; } get shadowRootMode() { return this._form.shadowRootMode; } get isShadowHost() { return this._form.isShadowHost; } get customElementLocation() { return this._form.customElementLocation; } get isDirectShadowHostChild() { return this._form.isDirectShadowHostChild; } // doctype properties get name() { return this._form.name; } get publicId() { return this._form.publicId; } get systemId() { return this._form.systemId; } getAttribute(name) { const attr = this._getAttribute(name); return attr ? attr.value : null; } hasAttribute(name) { this._cacheAttributes(); return name in this._attrMap; } get hidden() { const cls = this.getAttribute("class"); return cls && cls.indexOf(HIDDEN_CLASS) > -1; } get attributes() { return this._form.attrs; } get mutationBreakpoints() { return this._form.mutationBreakpoints; } get pseudoClassLocks() { return this._form.pseudoClassLocks || []; } hasPseudoClassLock(pseudo) { return this.pseudoClassLocks.some(locked => locked === pseudo); } get displayType() { return this._form.displayType; } get isDisplayed() { return this._form.isDisplayed; } get isScrollable() { return this._form.isScrollable; } get causesOverflow() { return this._form.causesOverflow; } get containerType() { return this._form.containerType; } get isTreeDisplayed() { let parent = this; while (parent) { if (!parent.isDisplayed) { return false; } parent = parent.parentNode(); } return true; } get inspectorFront() { return this.parentFront.parentFront; } get walkerFront() { return this.parentFront; } getNodeValue() { // backward-compatibility: if nodevalue is null and shortValue is defined, the actual // value of the node needs to be fetched on the server. if (this._form.nodeValue === null && this._form.shortValue) { return super.getNodeValue(); } const str = this._form.nodeValue || ""; return Promise.resolve(new SimpleStringFront(str)); } /** * Return a new AttributeModificationList for this node. */ startModifyingAttributes() { return new AttributeModificationList(this); } _cacheAttributes() { if (typeof this._attrMap != "undefined") { return; } this._attrMap = {}; for (const attr of this.attributes) { this._attrMap[attr.name] = attr; } } _getAttribute(name) { this._cacheAttributes(); return this._attrMap[name] || undefined; } /** * Set this node's parent. Note that the children saved in * this tree are unordered and incomplete, so shouldn't be used * instead of a `children` request. */ reparent(parent) { if (this._parent === parent) { return; } if (this._parent && this._parent._child === this) { this._parent._child = this._next; } if (this._prev) { this._prev._next = this._next; } if (this._next) { this._next._prev = this._prev; } this._next = null; this._prev = null; this._parent = parent; if (!parent) { // Subtree is disconnected, we're done return; } this._next = parent._child; if (this._next) { this._next._prev = this; } parent._child = this; } /** * Return all the known children of this node. */ treeChildren() { const ret = []; for (let child = this._child; child != null; child = child._next) { ret.push(child); } return ret; } /** * Do we use a local target? * Useful to know if a rawNode is available or not. * * This will, one day, be removed. External code should * not need to know if the target is remote or not. */ isLocalToBeDeprecated() { return !!this.conn._transport._serverConnection; } /** * Get a Node for the given node front. This only works locally, * and is only intended as a stopgap during the transition to the remote * protocol. If you depend on this you're likely to break soon. */ rawNode() { if (!this.isLocalToBeDeprecated()) { console.warn("Tried to use rawNode on a remote connection."); return null; } const { DevToolsServer, } = require("resource://devtools/server/devtools-server.js"); const actor = DevToolsServer.searchAllConnectionsForActor(this.actorID); if (!actor) { // Can happen if we try to get the raw node for an already-expired // actor. return null; } return actor.rawNode; } async connectToFrame() { if (!this.useChildTargetToFetchChildren) { console.warn("Tried to open connection to an invalid frame."); return null; } if ( this._childBrowsingContextTarget && !this._childBrowsingContextTarget.isDestroyed() ) { return this._childBrowsingContextTarget; } // Get the target for this frame element this._childBrowsingContextTarget = await this.targetFront.getWindowGlobalTarget( this._form.browsingContextID ); // Bug 1776250: When the target is destroyed, we need to easily find the // parent node front so that we can update its frontend container in the // markup-view. this._childBrowsingContextTarget.setParentNodeFront(this); return this._childBrowsingContextTarget; } } exports.NodeFront = NodeFront; registerFront(NodeFront);