/* 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 protocol = 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 InspectorUtils = require("InspectorUtils"); const { EXCLUDED_LISTENER, } = require("resource://devtools/server/actors/inspector/constants.js"); loader.lazyRequireGetter( this, [ "getFrameElement", "isAfterPseudoElement", "isAnonymous", "isBeforePseudoElement", "isDirectShadowHostChild", "isMarkerPseudoElement", "isNativeAnonymous", "isFrameBlockedByCSP", "isFrameWithChildTarget", "isShadowHost", "isShadowRoot", "isTemplateElement", "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 ); loader.lazyRequireGetter( this, "hasStyleSheetWatcherSupportForTarget", "resource://devtools/server/actors/utils/stylesheets-manager.js", true ); // ContentDOMReference requires ChromeUtils, which isn't available in worker context. const lazy = {}; if (!isWorker) { ChromeUtils.defineESModuleGetters(lazy, { ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs", }); } 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. */ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, { /** * 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 */ initialize(conn, targetActor, options) { protocol.Actor.prototype.initialize.call(this, conn); 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.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.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 ); for (const { document } of this.targetActor.windows) { document.shadowRootAttachedEventEnabled = 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: {}, }; }, toString() { return "[WalkerActor " + this.actorID + "]"; }, getAnonymousDocumentWalker(node, whatToShow, skipTo) { // Allow native anon content (like