/* 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, Cr} = require("chrome"); const Services = require("Services"); const protocol = require("devtools/server/protocol"); const {Arg, Option, method, RetVal, types} = protocol; const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); const object = require("sdk/util/object"); const events = require("sdk/event/core"); const {Unknown} = require("sdk/platform/xpcom"); const {Class} = require("sdk/core/heritage"); const {PageStyleActor, getFontPreviewData} = require("devtools/server/actors/styles"); const { HighlighterActor, CustomHighlighterActor, isTypeRegistered, } = require("devtools/server/actors/highlighter"); const {getLayoutChangesObserver, releaseLayoutChangesObserver} = require("devtools/server/actors/layout"); const {EventParsers} = require("devtools/toolkit/event-parsers"); 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; // 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] ]; let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } "; HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } "; Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); loader.lazyImporter(this, "gDevTools", "resource://gre/modules/devtools/gDevTools.jsm"); 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/styleinspector/css-logic").CssLogic); // XXX: A poor man's makeInfallible until we move it out of transport.js // Which should be very soon. function makeInfallible(handler) { return function(...args) { try { return handler.apply(this, args); } catch(ex) { console.error(ex); } return undefined; } } // A resolve that hits the main loop first. function delayedResolve(value) { let deferred = promise.defer(); Services.tm.mainThread.dispatch(makeInfallible(function delayedResolveHandler() { deferred.resolve(value); }), 0); return deferred.promise; } types.addDictType("imageData", { // The image data data: "nullable:longstring", // The original image dimensions size: "json" }); /** * 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; }; /** * Server side of the node actor. */ var NodeActor = exports.NodeActor = protocol.ActorClass({ typeName: "domnode", 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() this.walker.conn, isDocumentElement: function() { return this.rawNode.ownerDocument && this.rawNode.ownerDocument.documentElement === this.rawNode; }, // 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 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, numChildren: this.numChildren, // doctype attributes name: this.rawNode.name, publicId: this.rawNode.publicId, systemId: this.rawNode.systemId, attrs: this.writeAttrs(), isBeforePseudoElement: this.isBeforePseudoElement, isAfterPseudoElement: this.isAfterPseudoElement, isAnonymous: LayoutHelpers.isAnonymous(this.rawNode), isNativeAnonymous: LayoutHelpers.isNativeAnonymous(this.rawNode), isXBLAnonymous: LayoutHelpers.isXBLAnonymous(this.rawNode), isShadowAnonymous: LayoutHelpers.isShadowAnonymous(this.rawNode), pseudoClassLocks: this.writePseudoClassLocks(), isDisplayed: this.isDisplayed, hasEventListeners: this._hasEventListeners, }; if (this.isDocumentElement()) { form.isDocumentElement = true; } if (this.rawNode.nodeValue) { // We only include a short version of the value if it's longer than // gValueSummaryLength if (this.rawNode.nodeValue.length > gValueSummaryLength) { form.shortValue = this.rawNode.nodeValue.substring(0, gValueSummaryLength); form.incompleteValue = true; } else { form.shortValue = this.rawNode.nodeValue; } } return form; }, 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 numChildren = this.rawNode.childNodes.length; if (numChildren === 0 && (this.rawNode.contentDocument || this.rawNode.getSVGDocument)) { // This might be an iframe with virtual children. numChildren = 1; } // Count any anonymous children if (this.rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE) { let anonChildren = this.rawNode.ownerDocument.getAnonymousNodes(this.rawNode); if (anonChildren) { numChildren += anonChildren.length; } } // Normal counting misses ::before/::after, so we have to check to make sure // we aren't missing anything if (numChildren === 0) { 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 (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE || this.isAfterPseudoElement || this.isBeforePseudoElement) { return true; } let style = this.computedStyle; if (!style) { return true; } else { 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 [{namespace: attr.namespace, name: attr.name, value: attr.value } for (attr of this.rawNode.attributes)]; }, 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 events = []; 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, events, dbg, eventInfo); } } catch(e) { // An object attached to the node looked like a listener but wasn't... // do nothing. } } return events; }, /** * 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, events, 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: () { doSomething(); } So we need to work back to the preceeding \n, ; or } so we can get the appropriate function info e.g.: () => { doSomething(); } function doit() { doSomething(); } doit: function() { doSomething(); } */ let scriptBeforeFunc = scriptSource.substr(0, script.sourceStart); let lastEnding = Math.max( scriptBeforeFunc.lastIndexOf(";"), scriptBeforeFunc.lastIndexOf("}"), scriptBeforeFunc.lastIndexOf("{"), scriptBeforeFunc.lastIndexOf("("), scriptBeforeFunc.lastIndexOf(","), scriptBeforeFunc.lastIndexOf("!") ); if (lastEnding !== -1) { let functionPrefix = scriptBeforeFunc.substr(lastEnding + 1); functionSource = functionPrefix + 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 }; events.push(eventObj); dbg.removeDebuggee(globalDO); }, /** * Returns a LongStringActor with the node's value. */ getNodeValue: method(function() { return new LongStringActor(this.conn, this.rawNode.nodeValue || ""); }, { request: {}, response: { value: RetVal("longstring") } }), /** * Set the node's value to a given string. */ setNodeValue: method(function(value) { this.rawNode.nodeValue = value; }, { request: { value: Arg(0) }, response: {} }), /** * Get a unique selector string for this node. */ getUniqueSelector: method(function() { return CssLogic.findCssSelector(this.rawNode); }, { request: {}, response: { value: RetVal("string") } }), /** * 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: method(function(maxDim) { // imageToImageData may fail if the node isn't an image try { let imageData = imageToImageData(this.rawNode, maxDim); return promise.resolve({ data: LongStringActor(this.conn, imageData.data), size: imageData.size }); } catch(e) { return promise.reject(new Error("Image not available")); } }, { request: {maxDim: Arg(0, "nullable:number")}, response: RetVal("imageData") }), /** * Get all event listeners that are listening on this node. */ getEventListenerInfo: method(function() { if (this.rawNode.nodeName.toLowerCase() === "html") { return this.getEventListeners(this.rawNode.ownerGlobal); } return this.getEventListeners(this.rawNode); }, { request: {}, response: { events: RetVal("json") } }), /** * 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: method(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); } } } }, { request: { modifications: Arg(0, "array:json") }, response: {} }), /** * 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: method(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 }; }, { request: {font: Arg(0, "string"), fillStyle: Arg(1, "nullable:string")}, response: RetVal("imageData") }) }); /** * 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. */ let NodeFront = protocol.FrontClass(NodeActor, { initialize: function(conn, form, detail, ctx) { this._parent = null; // The parent node this._child = null; // The first child of this node. this._next = null; // The next sibling of this node. this._prev = null; // The previous sibling of this node. protocol.Front.prototype.initialize.call(this, conn, form, detail, ctx); }, /** * 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: function() { // If an observer was added on this node, shut it down. if (this.observer) { this.observer.disconnect(); this.observer = null; } protocol.Front.prototype.destroy.call(this); }, // Update the object given a form representation off the wire. form: function(form, detail, ctx) { if (detail === "actorid") { this.actorID = form; return; } // 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.merge(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. let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent); this.reparent(parentNodeFront); } }, /** * Returns the parent NodeFront for this NodeFront. */ parentNode: function() { return this._parent; }, /** * 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: function(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++) { let 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. if (!found) { this.attributes.push({ name: change.attributeName, namespace: change.attributeNamespace, value: change.newValue }); } } else if (change.type === "characterData") { this._form.shortValue = change.newValue; this._form.incompleteValue = change.incompleteValue; } else if (change.type === "pseudoClassLock") { this._form.pseudoClassLocks = change.pseudoClassLocks; } }, // Some accessors to make NodeFront feel more like an nsIDOMNode get id() this.getAttribute("id"), get nodeType() this._form.nodeType, get namespaceURI() this._form.namespaceURI, get nodeName() this._form.nodeName, get baseURI() this._form.baseURI, get className() { return this.getAttribute("class") || ''; }, get hasChildren() this._form.numChildren > 0, get numChildren() this._form.numChildren, get hasEventListeners() this._form.hasEventListeners, get isBeforePseudoElement() this._form.isBeforePseudoElement, get isAfterPseudoElement() this._form.isAfterPseudoElement, get isPseudoElement() this.isBeforePseudoElement || this.isAfterPseudoElement, get isAnonymous() this._form.isAnonymous, get tagName() this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null, get shortValue() this._form.shortValue, get incompleteValue() !!this._form.incompleteValue, get isDocumentElement() !!this._form.isDocumentElement, // doctype properties get name() this._form.name, get publicId() this._form.publicId, get systemId() this._form.systemId, getAttribute: function(name) { let attr = this._getAttribute(name); return attr ? attr.value : null; }, hasAttribute: function(name) { this._cacheAttributes(); return (name in this._attrMap); }, get hidden() { let cls = this.getAttribute("class"); return cls && cls.indexOf(HIDDEN_CLASS) > -1; }, get attributes() this._form.attrs, get pseudoClassLocks() this._form.pseudoClassLocks || [], hasPseudoClassLock: function(pseudo) { return this.pseudoClassLocks.some(locked => locked === pseudo); }, get isDisplayed() { // The NodeActor's form contains the isDisplayed information as a boolean // starting from FF32. Before that, the property is missing return "isDisplayed" in this._form ? this._form.isDisplayed : true; }, getNodeValue: protocol.custom(function() { if (!this.incompleteValue) { return delayedResolve(new ShortLongString(this.shortValue)); } else { return this._getNodeValue(); } }, { impl: "_getNodeValue" }), /** * Return a new AttributeModificationList for this node. */ startModifyingAttributes: function() { return AttributeModificationList(this); }, _cacheAttributes: function() { if (typeof(this._attrMap) != "undefined") { return; } this._attrMap = {}; for (let attr of this.attributes) { this._attrMap[attr.name] = attr; } }, _getAttribute: function(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: function(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: function() { let 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. */ isLocal_toBeDeprecated: function() { return !!this.conn._transport._serverConnection; }, /** * Get an nsIDOMNode 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: function(rawNode) { if (!this.conn._transport._serverConnection) { console.warn("Tried to use rawNode on a remote connection."); return null; } let actor = this.conn._transport._serverConnection.getActor(this.actorID); if (!actor) { // Can happen if we try to get the raw node for an already-expired // actor. return null; } return actor.rawNode; } }); /** * Returned from any call that might return a node that isn't connected to root by * nodes the child has seen, such as querySelector. */ types.addDictType("disconnectedNode", { // The actual node to return node: "domnode", // Nodes that are needed to connect the node to a node the client has already seen newParents: "array:domnode" }); types.addDictType("disconnectedNodeArray", { // The actual node list to return nodes: "array:domnode", // Nodes that are needed to connect those nodes to the root. newParents: "array:domnode" }); types.addDictType("dommutation", {}); /** * Server side of a node list as returned by querySelectorAll() */ var NodeListActor = exports.NodeListActor = protocol.ActorClass({ 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.length } }, /** * Get a single node from the node list. */ item: method(function(index) { return this.walker.attachElement(this.nodeList[index]); }, { request: { item: Arg(0) }, response: RetVal("disconnectedNode") }), /** * Get a range of the items from the node list. */ items: method(function(start=0, end=this.nodeList.length) { let items = [this.walker._ref(item) for (item of Array.prototype.slice.call(this.nodeList, start, end))]; return this.walker.attachElements(items); }, { request: { start: Arg(0, "nullable:number"), end: Arg(1, "nullable:number") }, response: RetVal("disconnectedNodeArray") }), release: method(function() {}, { release: true }) }); /** * Client side of a node list as returned by querySelectorAll() */ var NodeListFront = exports.NodeListFront = protocol.FrontClass(NodeListActor, { initialize: function(client, form) { protocol.Front.prototype.initialize.call(this, client, form); }, destroy: function() { protocol.Front.prototype.destroy.call(this); }, marshallPool: function() { return this.parent(); }, // Update the object given a form representation off the wire. form: function(json) { this.length = json.length; }, item: protocol.custom(function(index) { return this._item(index).then(response => { return response.node; }); }, { impl: "_item" }), items: protocol.custom(function(start, end) { return this._items(start, end).then(response => { return response.nodes; }); }, { impl: "_items" }) }); // Some common request/response templates for the dom walker let nodeArrayMethod = { request: { node: Arg(0, "domnode"), maxNodes: Option(1), center: Option(1, "domnode"), start: Option(1, "domnode"), whatToShow: Option(1) }, response: RetVal(types.addDictType("domtraversalarray", { nodes: "array:domnode" })) }; let traversalMethod = { request: { node: Arg(0, "domnode"), whatToShow: Option(1) }, response: { node: RetVal("nullable:domnode") } } /** * Server side of the DOM walker. */ var WalkerActor = protocol.ActorClass({ typeName: "domwalker", events: { "new-mutations" : { type: "newMutations" }, "picker-node-picked" : { type: "pickerNodePicked", node: Arg(0, "disconnectedNode") }, "picker-node-hovered" : { type: "pickerNodeHovered", node: Arg(0, "disconnectedNode") }, "highlighter-ready" : { type: "highlighter-ready" }, "highlighter-hide" : { type: "highlighter-hide" }, "display-change" : { type: "display-change", nodes: Arg(0, "array:domnode") } }, /** * 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.layoutHelpers = new LayoutHelpers(this.rootWin); // 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.reflowObserver = getLayoutChangesObserver(this.tabActor); this._onReflows = this._onReflows.bind(this); this.reflowObserver.on("reflows", this._onReflows); }, // Returns the JSON representation of this object over the wire. form: function() { return { actor: this.actorID, root: this.rootNode.form() } }, toString: function() { return "[WalkerActor " + this.actorID + "]"; }, getDocumentWalker: function(node, whatToShow) { // Allow native anon content (like