/* 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 { Actor } = require("resource://devtools/shared/protocol.js"); const { walkerSpec } = require("resource://devtools/shared/specs/walker.js"); const { LongStringActor, } = require("resource://devtools/server/actors/string.js"); const { EXCLUDED_LISTENER, } = require("resource://devtools/server/actors/inspector/constants.js"); loader.lazyRequireGetter( this, "nodeFilterConstants", "resource://devtools/shared/dom-node-filter-constants.js" ); loader.lazyRequireGetter( this, [ "getFrameElement", "isAfterPseudoElement", "isBeforePseudoElement", "isDirectShadowHostChild", "isMarkerPseudoElement", "isFrameBlockedByCSP", "isFrameWithChildTarget", "isShadowHost", "isShadowRoot", "loadSheet", ], "resource://devtools/shared/layout/utils.js", true ); loader.lazyRequireGetter( this, "throttle", "resource://devtools/shared/throttle.js", true ); loader.lazyRequireGetter( this, [ "allAnonymousContentTreeWalkerFilter", "findGridParentContainerForNode", "isNodeDead", "noAnonymousContentTreeWalkerFilter", "nodeDocument", "standardTreeWalkerFilter", ], "resource://devtools/server/actors/inspector/utils.js", true ); loader.lazyRequireGetter( this, "CustomElementWatcher", "resource://devtools/server/actors/inspector/custom-element-watcher.js", true ); loader.lazyRequireGetter( this, ["DocumentWalker", "SKIP_TO_SIBLING"], "resource://devtools/server/actors/inspector/document-walker.js", true ); loader.lazyRequireGetter( this, ["NodeActor", "NodeListActor"], "resource://devtools/server/actors/inspector/node.js", true ); loader.lazyRequireGetter( this, "NodePicker", "resource://devtools/server/actors/inspector/node-picker.js", true ); loader.lazyRequireGetter( this, "LayoutActor", "resource://devtools/server/actors/layout.js", true ); loader.lazyRequireGetter( this, ["getLayoutChangesObserver", "releaseLayoutChangesObserver"], "resource://devtools/server/actors/reflow.js", true ); loader.lazyRequireGetter( this, "WalkerSearch", "resource://devtools/server/actors/utils/walker-search.js", true ); // ContentDOMReference requires ChromeUtils, which isn't available in worker context. const lazy = {}; if (!isWorker) { loader.lazyGetter( lazy, "ContentDOMReference", () => ChromeUtils.importESModule( "resource://gre/modules/ContentDOMReference.sys.mjs", // ContentDOMReference needs to be retrieved from the shared global // since it is a shared singleton. { global: "shared" } ).ContentDOMReference ); } loader.lazyServiceGetter( this, "eventListenerService", "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService" ); // Minimum delay between two "new-mutations" events. const MUTATIONS_THROTTLING_DELAY = 100; // List of mutation types that should -not- be throttled. const IMMEDIATE_MUTATIONS = ["pseudoClassLock"]; const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; // The possible completions to a ':' with added score to give certain values // some preference. const PSEUDO_SELECTORS = [ [":active", 1], [":hover", 1], [":focus", 1], [":visited", 0], [":link", 0], [":first-letter", 0], [":first-child", 2], [":before", 2], [":after", 2], [":lang(", 0], [":not(", 3], [":first-of-type", 0], [":last-of-type", 0], [":only-of-type", 0], [":only-child", 2], [":nth-child(", 3], [":nth-last-child(", 0], [":nth-of-type(", 0], [":nth-last-of-type(", 0], [":last-child", 2], [":root", 0], [":empty", 0], [":target", 0], [":enabled", 0], [":disabled", 0], [":checked", 1], ["::selection", 0], ["::marker", 0], ]; const HELPER_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(` .__fx-devtools-hide-shortcut__ { visibility: hidden !important; } `); /** * We only send nodeValue up to a certain size by default. This stuff * controls that size. */ exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50; var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH; exports.getValueSummaryLength = function () { return gValueSummaryLength; }; exports.setValueSummaryLength = function (val) { gValueSummaryLength = val; }; /** * Server side of the DOM walker. */ class WalkerActor extends Actor { /** * Create the WalkerActor * @param {DevToolsServerConnection} conn * The server connection. * @param {TargetActor} targetActor * The top-level Actor for this tab. * @param {Object} options * - {Boolean} showAllAnonymousContent: Show all native anonymous content */ constructor(conn, targetActor, options) { super(conn, walkerSpec); this.targetActor = targetActor; this.rootWin = targetActor.window; this.rootDoc = this.rootWin.document; // Map of already created node actors, keyed by their corresponding DOMNode. this._nodeActorsMap = new Map(); this._pendingMutations = []; this._activePseudoClassLocks = new Set(); this._mutationBreakpoints = new WeakMap(); this._anonParents = new WeakMap(); this.customElementWatcher = new CustomElementWatcher( targetActor.chromeEventHandler ); // In this map, the key-value pairs are the overflow causing elements and their // respective ancestor scrollable node actor. this.overflowCausingElementsMap = new Map(); this.showAllAnonymousContent = options.showAllAnonymousContent; this.walkerSearch = new WalkerSearch(this); // Nodes which have been removed from the client's known // ownership tree are considered "orphaned", and stored in // this set. this._orphaned = new Set(); // The client can tell the walker that it is interested in a node // even when it is orphaned with the `retainNode` method. This // list contains orphaned nodes that were so retained. this._retainedOrphans = new Set(); this.onSubtreeModified = this.onSubtreeModified.bind(this); this.onSubtreeModified[EXCLUDED_LISTENER] = true; this.onNodeRemoved = this.onNodeRemoved.bind(this); this.onNodeRemoved[EXCLUDED_LISTENER] = true; this.onAttributeModified = this.onAttributeModified.bind(this); this.onAttributeModified[EXCLUDED_LISTENER] = true; this.onMutations = this.onMutations.bind(this); this.onSlotchange = this.onSlotchange.bind(this); this.onShadowrootattached = this.onShadowrootattached.bind(this); this.onAnonymousrootcreated = this.onAnonymousrootcreated.bind(this); this.onAnonymousrootremoved = this.onAnonymousrootremoved.bind(this); this.onFrameLoad = this.onFrameLoad.bind(this); this.onFrameUnload = this.onFrameUnload.bind(this); this.onCustomElementDefined = this.onCustomElementDefined.bind(this); this._throttledEmitNewMutations = throttle( this._emitNewMutations.bind(this), MUTATIONS_THROTTLING_DELAY ); targetActor.on("will-navigate", this.onFrameUnload); targetActor.on("window-ready", this.onFrameLoad); this.customElementWatcher.on( "element-defined", this.onCustomElementDefined ); // Keep a reference to the chromeEventHandler for the current targetActor, to make // sure we will be able to remove the listener during the WalkerActor destroy(). this.chromeEventHandler = targetActor.chromeEventHandler; // shadowrootattached is a chrome-only event. We enable it below. this.chromeEventHandler.addEventListener( "shadowrootattached", this.onShadowrootattached ); // anonymousrootcreated is a chrome-only event. We enable it below. this.chromeEventHandler.addEventListener( "anonymousrootcreated", this.onAnonymousrootcreated ); this.chromeEventHandler.addEventListener( "anonymousrootremoved", this.onAnonymousrootremoved ); for (const { document } of this.targetActor.windows) { document.devToolsAnonymousAndShadowEventsEnabled = true; } // Ensure that the root document node actor is ready and // managed. this.rootNode = this.document(); this.layoutChangeObserver = getLayoutChangesObserver(this.targetActor); this._onReflows = this._onReflows.bind(this); this.layoutChangeObserver.on("reflows", this._onReflows); this._onResize = this._onResize.bind(this); this.layoutChangeObserver.on("resize", this._onResize); this._onEventListenerChange = this._onEventListenerChange.bind(this); eventListenerService.addListenerChangeListener(this._onEventListenerChange); } get nodePicker() { if (!this._nodePicker) { this._nodePicker = new NodePicker(this, this.targetActor); } return this._nodePicker; } watchRootNode() { if (this.rootNode) { this.emit("root-available", this.rootNode); } } /** * Callback for eventListenerService.addListenerChangeListener * @param nsISimpleEnumerator changesEnum * enumerator of nsIEventListenerChange */ _onEventListenerChange(changesEnum) { for (const current of changesEnum.enumerate(Ci.nsIEventListenerChange)) { const target = current.target; if (this._nodeActorsMap.has(target)) { const actor = this.getNode(target); const mutation = { type: "events", target: actor.actorID, hasEventListeners: actor._hasEventListeners, }; this.queueMutation(mutation); } } } // Returns the JSON representation of this object over the wire. form() { return { actor: this.actorID, root: this.rootNode.form(), traits: { // @backward-compat { version 125 } Indicate to the client that it can use getIdrefNode. // This trait can be removed once 125 hits release. hasGetIdrefNode: true, }, }; } toString() { return "[WalkerActor " + this.actorID + "]"; } getDocumentWalker(node, skipTo) { // Allow native anon content (like