/* 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"; /** * Here's the server side of the remote inspector. * * The WalkerActor is the client's view of the debuggee's DOM. It's gives * the client a tree of NodeActor objects. * * The walker presents the DOM tree mostly unmodified from the source DOM * tree, but with a few key differences: * * - Empty text nodes are ignored. This is pretty typical of developer * tools, but maybe we should reconsider that on the server side. * - iframes with documents loaded have the loaded document as the child, * the walker provides one big tree for the whole document tree. * * There are a few ways to get references to NodeActors: * * - When you first get a WalkerActor reference, it comes with a free * reference to the root document's node. * - Given a node, you can ask for children, siblings, and parents. * - You can issue querySelector and querySelectorAll requests to find * other elements. * - Requests that return arbitrary nodes from the tree (like querySelector * and querySelectorAll) will also return any nodes the client hasn't * seen in order to have a complete set of parents. * * Once you have a NodeFront, you should be able to answer a few questions * without further round trips, like the node's name, namespace/tagName, * attributes, etc. Other questions (like a text node's full nodeValue) * might require another round trip. * * The protocol guarantees that the client will always know the parent of * any node that is returned by the server. This means that some requests * (like querySelector) will include the extra nodes needed to satisfy this * requirement. The client keeps track of this parent relationship, so the * node fronts form a tree that is a subset of the actual DOM tree. * * * We maintain this guarantee to support the ability to release subtrees on * the client - when a node is disconnected from the DOM tree we want to be * able to free the client objects for all the children nodes. * * So to be able to answer "all the children of a given node that we have * seen on the client side", we guarantee that every time we've seen a node, * we connect it up through its parents. */ const {Cc, Ci, Cu} = require("chrome"); const Services = require("Services"); const protocol = require("devtools/shared/protocol"); const {LayoutActor} = require("devtools/server/actors/layout"); const {LongStringActor} = require("devtools/server/actors/string"); const promise = require("promise"); const {Task} = require("devtools/shared/task"); const events = require("sdk/event/core"); const {WalkerSearch} = require("devtools/server/actors/utils/walker-search"); const {PageStyleActor, getFontPreviewData} = require("devtools/server/actors/styles"); const { HighlighterActor, CustomHighlighterActor, isTypeRegistered, HighlighterEnvironment } = require("devtools/server/actors/highlighters"); const {EyeDropper} = require("devtools/server/actors/highlighters/eye-dropper"); const { isAnonymous, isNativeAnonymous, isXBLAnonymous, isShadowAnonymous, getFrameElement } = require("devtools/shared/layout/utils"); const {getLayoutChangesObserver, releaseLayoutChangesObserver} = require("devtools/server/actors/reflow"); const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants"); const {EventParsers} = require("devtools/server/event-parsers"); const {nodeSpec, nodeListSpec, walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector"); const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog"; const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20; const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; const XHTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const IMAGE_FETCHING_TIMEOUT = 500; const RX_FUNC_NAME = /((var|const|let)\s+)?([\w$.]+\s*[:=]\s*)*(function)?\s*\*?\s*([\w$]+)?\s*$/; // 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] ]; var HELPER_SHEET = ` .__fx-devtools-hide-shortcut__ { visibility: hidden !important; } :-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px !important; } `; const flags = require("devtools/shared/flags"); loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils"); loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils"); loader.lazyGetter(this, "DOMParser", function () { return Cc["@mozilla.org/xmlextras/domparser;1"] .createInstance(Ci.nsIDOMParser); }); loader.lazyGetter(this, "eventListenerService", function () { return Cc["@mozilla.org/eventlistenerservice;1"] .getService(Ci.nsIEventListenerService); }); loader.lazyGetter(this, "CssLogic", () => require("devtools/server/css-logic").CssLogic); /** * 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; }; // When the user selects a node to inspect in e10s, the parent process // has a CPOW that wraps the node being inspected. It uses the // message manager to send this node to the child, which stores the // node in gInspectingNode. Then a findInspectingNode request is sent // over the remote debugging protocol, and gInspectingNode is returned // to the parent as a NodeFront. var gInspectingNode = null; // We expect this function to be called from the child.js frame script // when it receives the node to be inspected over the message manager. exports.setInspectingNode = function (val) { gInspectingNode = val; }; /** * Returns the properly cased version of the node's tag name, which can be * used when displaying said name in the UI. * * @param {Node} rawNode * Node for which we want the display name * @return {String} * Properly cased version of the node tag name */ const getNodeDisplayName = function (rawNode) { if (rawNode.nodeName && !rawNode.localName) { // The localName & prefix APIs have been moved from the Node interface to the Element // interface. Use Node.nodeName as a fallback. return rawNode.nodeName; } return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName; }; exports.getNodeDisplayName = getNodeDisplayName; /** * Server side of the node actor. */ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, { initialize: function (walker, node) { protocol.Actor.prototype.initialize.call(this, null); this.walker = walker; this.rawNode = node; this._eventParsers = new EventParsers().parsers; // Storing the original display of the node, to track changes when reflows // occur this.wasDisplayed = this.isDisplayed; }, toString: function () { return "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]"; }, /** * Instead of storing a connection object, the NodeActor gets its connection * from its associated walker. */ get conn() { return this.walker.conn; }, isDocumentElement: function () { return this.rawNode.ownerDocument && this.rawNode.ownerDocument.documentElement === this.rawNode; }, destroy: function () { protocol.Actor.prototype.destroy.call(this); if (this.mutationObserver) { if (!Cu.isDeadWrapper(this.mutationObserver)) { this.mutationObserver.disconnect(); } this.mutationObserver = null; } this.rawNode = null; this.walker = null; }, // Returns the JSON representation of this object over the wire. form: function (detail) { if (detail === "actorid") { return this.actorID; } let parentNode = this.walker.parentNode(this); let inlineTextChild = this.walker.inlineTextChild(this); let form = { actor: this.actorID, baseURI: this.rawNode.baseURI, parent: parentNode ? parentNode.actorID : undefined, nodeType: this.rawNode.nodeType, namespaceURI: this.rawNode.namespaceURI, nodeName: this.rawNode.nodeName, nodeValue: this.rawNode.nodeValue, displayName: getNodeDisplayName(this.rawNode), numChildren: this.numChildren, inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined, // doctype attributes name: this.rawNode.name, publicId: this.rawNode.publicId, systemId: this.rawNode.systemId, attrs: this.writeAttrs(), isBeforePseudoElement: this.isBeforePseudoElement, isAfterPseudoElement: this.isAfterPseudoElement, isAnonymous: isAnonymous(this.rawNode), isNativeAnonymous: isNativeAnonymous(this.rawNode), isXBLAnonymous: isXBLAnonymous(this.rawNode), isShadowAnonymous: isShadowAnonymous(this.rawNode), pseudoClassLocks: this.writePseudoClassLocks(), isDisplayed: this.isDisplayed, isInHTMLDocument: this.rawNode.ownerDocument && this.rawNode.ownerDocument.contentType === "text/html", hasEventListeners: this._hasEventListeners, }; if (this.isDocumentElement()) { form.isDocumentElement = true; } // Add an extra API for custom properties added by other // modules/extensions. form.setFormProperty = (name, value) => { if (!form.props) { form.props = {}; } form.props[name] = value; }; // Fire an event so, other modules can create its own properties // that should be passed to the client (within the form.props field). events.emit(NodeActor, "form", { target: this, data: form }); return form; }, /** * Watch the given document node for mutations using the DOM observer * API. */ watchDocument: function (callback) { let node = this.rawNode; // Create the observer on the node's actor. The node will make sure // the observer is cleaned up when the actor is released. let observer = new node.defaultView.MutationObserver(callback); observer.mergeAttributeRecords = true; observer.observe(node, { nativeAnonymousChildList: true, attributes: true, characterData: true, characterDataOldValue: true, childList: true, subtree: true }); this.mutationObserver = observer; }, get isBeforePseudoElement() { return this.rawNode.nodeName === "_moz_generated_content_before"; }, get isAfterPseudoElement() { return this.rawNode.nodeName === "_moz_generated_content_after"; }, // Estimate the number of children that the walker will return without making // a call to children() if possible. get numChildren() { // For pseudo elements, childNodes.length returns 1, but the walker // will return 0. if (this.isBeforePseudoElement || this.isAfterPseudoElement) { return 0; } let rawNode = this.rawNode; let numChildren = rawNode.childNodes.length; let hasAnonChildren = rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE && rawNode.ownerDocument.getAnonymousNodes(rawNode); let hasContentDocument = rawNode.contentDocument; let hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument(); if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) { // This might be an iframe with virtual children. numChildren = 1; } // Normal counting misses ::before/::after. Also, some anonymous children // may ultimately be skipped, so we have to consult with the walker. if (numChildren === 0 || hasAnonChildren) { numChildren = this.walker.children(this).nodes.length; } return numChildren; }, get computedStyle() { return CssLogic.getComputedStyle(this.rawNode); }, /** * Is the node's display computed style value other than "none" */ get isDisplayed() { // Consider all non-element nodes as displayed. if (isNodeDead(this) || this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE || this.isAfterPseudoElement || this.isBeforePseudoElement) { return true; } let style = this.computedStyle; if (!style) { return true; } return style.display !== "none"; }, /** * Are there event listeners that are listening on this node? This method * uses all parsers registered via event-parsers.js.registerEventParser() to * check if there are any event listeners. */ get _hasEventListeners() { let parsers = this._eventParsers; for (let [, {hasListeners}] of parsers) { try { if (hasListeners && hasListeners(this.rawNode)) { return true; } } catch (e) { // An object attached to the node looked like a listener but wasn't... // do nothing. } } return false; }, writeAttrs: function () { if (!this.rawNode.attributes) { return undefined; } return [...this.rawNode.attributes].map(attr => { return {namespace: attr.namespace, name: attr.name, value: attr.value }; }); }, writePseudoClassLocks: function () { if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { return undefined; } let ret = undefined; for (let pseudo of PSEUDO_CLASSES) { if (DOMUtils.hasPseudoClassLock(this.rawNode, pseudo)) { ret = ret || []; ret.push(pseudo); } } return ret; }, /** * Gets event listeners and adds their information to the events array. * * @param {Node} node * Node for which we are to get listeners. */ getEventListeners: function (node) { let parsers = this._eventParsers; let dbg = this.parent().tabActor.makeDebugger(); let listeners = []; for (let [, {getListeners, normalizeHandler}] of parsers) { try { let eventInfos = getListeners(node); if (!eventInfos) { continue; } for (let eventInfo of eventInfos) { if (normalizeHandler) { eventInfo.normalizeHandler = normalizeHandler; } this.processHandlerForEvent(node, listeners, dbg, eventInfo); } } catch (e) { // An object attached to the node looked like a listener but wasn't... // do nothing. } } listeners.sort((a, b) => { return a.type.localeCompare(b.type); }); return listeners; }, /** * Process a handler * * @param {Node} node * The node for which we want information. * @param {Array} events * The events array contains all event objects that we have gathered * so far. * @param {Debugger} dbg * JSDebugger instance. * @param {Object} eventInfo * See event-parsers.js.registerEventParser() for a description of the * eventInfo object. * * @return {Array} * An array of objects where a typical object looks like this: * { * type: "click", * handler: function() { doSomething() }, * origin: "http://www.mozilla.com", * searchString: 'onclick="doSomething()"', * tags: tags, * DOM0: true, * capturing: true, * hide: { * dom0: true * } * } */ processHandlerForEvent: function (node, listeners, dbg, eventInfo) { let type = eventInfo.type || ""; let handler = eventInfo.handler; let tags = eventInfo.tags || ""; let hide = eventInfo.hide || {}; let override = eventInfo.override || {}; let global = Cu.getGlobalForObject(handler); let globalDO = dbg.addDebuggee(global); let listenerDO = globalDO.makeDebuggeeValue(handler); if (eventInfo.normalizeHandler) { listenerDO = eventInfo.normalizeHandler(listenerDO); } // If the listener is an object with a 'handleEvent' method, use that. if (listenerDO.class === "Object" || listenerDO.class === "XULElement") { let desc; while (!desc && listenerDO) { desc = listenerDO.getOwnPropertyDescriptor("handleEvent"); listenerDO = listenerDO.proto; } if (desc && desc.value) { listenerDO = desc.value; } } if (listenerDO.isBoundFunction) { listenerDO = listenerDO.boundTargetFunction; } let script = listenerDO.script; let scriptSource = script.source.text; let functionSource = scriptSource.substr(script.sourceStart, script.sourceLength); /* The script returned is the whole script and scriptSource.substr(script.sourceStart, script.sourceLength) returns something like this: () { doSomething(); } So we need to use some regex magic to get the appropriate function info e.g.: () => { ... } function doit() { ... } doit: function() { ... } es6func() { ... } var|let|const foo = function () { ... } function generator*() { ... } */ let scriptBeforeFunc = scriptSource.substr(0, script.sourceStart); let matches = scriptBeforeFunc.match(RX_FUNC_NAME); if (matches && matches.length > 0) { functionSource = matches[0].trim() + functionSource; } let dom0 = false; if (typeof node.hasAttribute !== "undefined") { dom0 = !!node.hasAttribute("on" + type); } else { dom0 = !!node["on" + type]; } let line = script.startLine; let url = script.url; let origin = url + (dom0 ? "" : ":" + line); let searchString; if (dom0) { searchString = "on" + type + "=\"" + script.source.text + "\""; } else { scriptSource = " " + scriptSource; } let eventObj = { type: typeof override.type !== "undefined" ? override.type : type, handler: functionSource.trim(), origin: typeof override.origin !== "undefined" ? override.origin : origin, searchString: typeof override.searchString !== "undefined" ? override.searchString : searchString, tags: tags, DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0, capturing: typeof override.capturing !== "undefined" ? override.capturing : eventInfo.capturing, hide: hide }; listeners.push(eventObj); dbg.removeDebuggee(globalDO); }, /** * Returns a LongStringActor with the node's value. */ getNodeValue: function () { return new LongStringActor(this.conn, this.rawNode.nodeValue || ""); }, /** * Set the node's value to a given string. */ setNodeValue: function (value) { this.rawNode.nodeValue = value; }, /** * Get a unique selector string for this node. */ getUniqueSelector: function () { if (Cu.isDeadWrapper(this.rawNode)) { return ""; } return CssLogic.findCssSelector(this.rawNode); }, /** * Get the full CSS path for this node. * * @return {String} A CSS selector with a part for the node and each of its ancestors. */ getCssPath: function () { if (Cu.isDeadWrapper(this.rawNode)) { return ""; } return CssLogic.getCssPath(this.rawNode); }, /** * Scroll the selected node into view. */ scrollIntoView: function () { this.rawNode.scrollIntoView(true); }, /** * Get the node's image data if any (for canvas and img nodes). * Returns an imageData object with the actual data being a LongStringActor * and a size json object. * The image data is transmitted as a base64 encoded png data-uri. * The method rejects if the node isn't an image or if the image is missing * * Accepts a maxDim request parameter to resize images that are larger. This * is important as the resizing occurs server-side so that image-data being * transfered in the longstring back to the client will be that much smaller */ getImageData: function (maxDim) { return imageToImageData(this.rawNode, maxDim).then(imageData => { return { data: LongStringActor(this.conn, imageData.data), size: imageData.size }; }); }, /** * Get all event listeners that are listening on this node. */ getEventListenerInfo: function () { if (this.rawNode.nodeName.toLowerCase() === "html") { return this.getEventListeners(this.rawNode.ownerGlobal); } return this.getEventListeners(this.rawNode); }, /** * Modify a node's attributes. Passed an array of modifications * similar in format to "attributes" mutations. * { * attributeName: * attributeNamespace: * newValue: - If null or undefined, the attribute * will be removed. * } * * Returns when the modifications have been made. Mutations will * be queued for any changes made. */ modifyAttributes: function (modifications) { let rawNode = this.rawNode; for (let change of modifications) { if (change.newValue == null) { if (change.attributeNamespace) { rawNode.removeAttributeNS(change.attributeNamespace, change.attributeName); } else { rawNode.removeAttribute(change.attributeName); } } else if (change.attributeNamespace) { rawNode.setAttributeNS(change.attributeNamespace, change.attributeName, change.newValue); } else { rawNode.setAttribute(change.attributeName, change.newValue); } } }, /** * Given the font and fill style, get the image data of a canvas with the * preview text and font. * Returns an imageData object with the actual data being a LongStringActor * and the width of the text as a string. * The image data is transmitted as a base64 encoded png data-uri. */ getFontFamilyDataURL: function (font, fillStyle = "black") { let doc = this.rawNode.ownerDocument; let options = { previewText: FONT_FAMILY_PREVIEW_TEXT, previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE, fillStyle: fillStyle }; let { dataURL, size } = getFontPreviewData(font, doc, options); return { data: LongStringActor(this.conn, dataURL), size: size }; } }); /** * Server side of a node list as returned by querySelectorAll() */ var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, { typeName: "domnodelist", initialize: function (walker, nodeList) { protocol.Actor.prototype.initialize.call(this); this.walker = walker; this.nodeList = nodeList || []; }, destroy: function () { protocol.Actor.prototype.destroy.call(this); }, /** * Instead of storing a connection object, the NodeActor gets its connection * from its associated walker. */ get conn() { return this.walker.conn; }, /** * Items returned by this actor should belong to the parent walker. */ marshallPool: function () { return this.walker; }, // Returns the JSON representation of this object over the wire. form: function () { return { actor: this.actorID, length: this.nodeList ? this.nodeList.length : 0 }; }, /** * Get a single node from the node list. */ item: function (index) { return this.walker.attachElement(this.nodeList[index]); }, /** * Get a range of the items from the node list. */ items: function (start = 0, end = this.nodeList.length) { let items = Array.prototype.slice.call(this.nodeList, start, end) .map(item => this.walker._ref(item)); return this.walker.attachElements(items); }, release: function () {} }); /** * Server side of the DOM walker. */ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, { /** * Create the WalkerActor * @param DebuggerServerConnection conn * The server connection. */ initialize: function (conn, tabActor, options) { protocol.Actor.prototype.initialize.call(this, conn); this.tabActor = tabActor; this.rootWin = tabActor.window; this.rootDoc = this.rootWin.document; this._refMap = new Map(); this._pendingMutations = []; this._activePseudoClassLocks = new Set(); 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.onMutations = this.onMutations.bind(this); this.onFrameLoad = this.onFrameLoad.bind(this); this.onFrameUnload = this.onFrameUnload.bind(this); events.on(tabActor, "will-navigate", this.onFrameUnload); events.on(tabActor, "navigate", this.onFrameLoad); // Ensure that the root document node actor is ready and // managed. this.rootNode = this.document(); this.layoutChangeObserver = getLayoutChangesObserver(this.tabActor); 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); }, /** * Callback for eventListenerService.addListenerChangeListener * @param nsISimpleEnumerator changesEnum * enumerator of nsIEventListenerChange */ _onEventListenerChange: function (changesEnum) { let changes = changesEnum.enumerate(); while (changes.hasMoreElements()) { let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange); let target = current.target; if (this._refMap.has(target)) { let actor = this.getNode(target); let mutation = { type: "events", target: actor.actorID, hasEventListeners: actor._hasEventListeners }; this.queueMutation(mutation); } } }, // Returns the JSON representation of this object over the wire. form: function () { return { actor: this.actorID, root: this.rootNode.form(), traits: { // FF42+ Inspector starts managing the Walker, while the inspector also // starts cleaning itself up automatically on client disconnection. // So that there is no need to manually release the walker anymore. autoReleased: true, // XXX: It seems silly that we need to tell the front which capabilities // its actor has in this way when the target can use actorHasMethod. If // this was ported to the protocol (Bug 1157048) we could call that // inside of custom front methods and not need to do traits for this. multiFrameQuerySelectorAll: true, textSearch: true, } }; }, toString: function () { return "[WalkerActor " + this.actorID + "]"; }, getDocumentWalker: function (node, whatToShow) { // Allow native anon content (like