/* 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: <string> * attributeNamespace: <optional string> * newValue: <optional string> - 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 <video> controls) if preffed on let nodeFilter = this.showAllAnonymousContent ? allAnonymousContentTreeWalkerFilter : standardTreeWalkerFilter; return new DocumentWalker(node, this.rootWin, whatToShow, nodeFilter); }, destroy: function () { if (this._destroyed) { return; } this._destroyed = true; protocol.Actor.prototype.destroy.call(this); try { this.clearPseudoClassLocks(); this._activePseudoClassLocks = null; this._hoveredNode = null; this.rootWin = null; this.rootDoc = null; this.rootNode = null; this.layoutHelpers = null; this._orphaned = null; this._retainedOrphans = null; this._refMap = null; events.off(this.tabActor, "will-navigate", this.onFrameUnload); events.off(this.tabActor, "navigate", this.onFrameLoad); this.onFrameLoad = null; this.onFrameUnload = null; this.walkerSearch.destroy(); this.layoutChangeObserver.off("reflows", this._onReflows); this.layoutChangeObserver.off("resize", this._onResize); this.layoutChangeObserver = null; releaseLayoutChangesObserver(this.tabActor); eventListenerService.removeListenerChangeListener( this._onEventListenerChange); this.onMutations = null; this.layoutActor = null; this.tabActor = null; events.emit(this, "destroyed"); } catch (e) { console.error(e); } }, release: function () {}, unmanage: function (actor) { if (actor instanceof NodeActor) { if (this._activePseudoClassLocks && this._activePseudoClassLocks.has(actor)) { this.clearPseudoClassLocks(actor); } this._refMap.delete(actor.rawNode); } protocol.Actor.prototype.unmanage.call(this, actor); }, /** * Determine if the walker has come across this DOM node before. * @param {DOMNode} rawNode * @return {Boolean} */ hasNode: function (rawNode) { return this._refMap.has(rawNode); }, /** * If the walker has come across this DOM node before, then get the * corresponding node actor. * @param {DOMNode} rawNode * @return {NodeActor} */ getNode: function (rawNode) { return this._refMap.get(rawNode); }, _ref: function (node) { let actor = this.getNode(node); if (actor) { return actor; } actor = new NodeActor(this, node); // Add the node actor as a child of this walker actor, assigning // it an actorID. this.manage(actor); this._refMap.set(node, actor); if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) { actor.watchDocument(this.onMutations); } return actor; }, _onReflows: function (reflows) { // Going through the nodes the walker knows about, see which ones have // had their display changed and send a display-change event if any let changes = []; for (let [node, actor] of this._refMap) { if (Cu.isDeadWrapper(node)) { continue; } let isDisplayed = actor.isDisplayed; if (isDisplayed !== actor.wasDisplayed) { changes.push(actor); // Updating the original value actor.wasDisplayed = isDisplayed; } } if (changes.length) { events.emit(this, "display-change", changes); } }, /** * When the browser window gets resized, relay the event to the front. */ _onResize: function () { events.emit(this, "resize"); }, /** * This is kept for backward-compatibility reasons with older remote targets. * Targets prior to bug 916443. * * pick/cancelPick are used to pick a node on click on the content * document. But in their implementation prior to bug 916443, they don't allow * highlighting on hover. * The client-side now uses the highlighter actor's pick and cancelPick * methods instead. The client-side uses the the highlightable trait found in * the root actor to determine which version of pick to use. * * As for highlight, the new highlighter actor is used instead of the walker's * highlight method. Same here though, the client-side uses the highlightable * trait to dertermine which to use. * * Keeping these actor methods for now allows newer client-side debuggers to * inspect fxos 1.2 remote targets or older firefox desktop remote targets. */ pick: function () {}, cancelPick: function () {}, highlight: function (node) {}, /** * Ensures that the node is attached and it can be accessed from the root. * * @param {(Node|NodeActor)} nodes The nodes * @return {Object} An object compatible with the disconnectedNode type. */ attachElement: function (node) { let { nodes, newParents } = this.attachElements([node]); return { node: nodes[0], newParents: newParents }; }, /** * Ensures that the nodes are attached and they can be accessed from the root. * * @param {(Node[]|NodeActor[])} nodes The nodes * @return {Object} An object compatible with the disconnectedNodeArray type. */ attachElements: function (nodes) { let nodeActors = []; let newParents = new Set(); for (let node of nodes) { if (!(node instanceof NodeActor)) { // If an anonymous node was passed in and we aren't supposed to know // about it, then consult with the document walker as the source of // truth about which elements exist. if (!this.showAllAnonymousContent && isAnonymous(node)) { node = this.getDocumentWalker(node).currentNode; } node = this._ref(node); } this.ensurePathToRoot(node, newParents); // If nodes may be an array of raw nodes, we're sure to only have // NodeActors with the following array. nodeActors.push(node); } return { nodes: nodeActors, newParents: [...newParents] }; }, /** * Return the document node that contains the given node, * or the root node if no node is specified. * @param NodeActor node * The node whose document is needed, or null to * return the root. */ document: function (node) { let doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode); return this._ref(doc); }, /** * Return the documentElement for the document containing the * given node. * @param NodeActor node * The node whose documentElement is requested, or null * to use the root document. */ documentElement: function (node) { let elt = isNodeDead(node) ? this.rootDoc.documentElement : nodeDocument(node.rawNode).documentElement; return this._ref(elt); }, /** * Return all parents of the given node, ordered from immediate parent * to root. * @param NodeActor node * The node whose parents are requested. * @param object options * Named options, including: * `sameDocument`: If true, parents will be restricted to the same * document as the node. * `sameTypeRootTreeItem`: If true, this will not traverse across * different types of docshells. */ parents: function (node, options = {}) { if (isNodeDead(node)) { return []; } let walker = this.getDocumentWalker(node.rawNode); let parents = []; let cur; while ((cur = walker.parentNode())) { if (options.sameDocument && nodeDocument(cur) != nodeDocument(node.rawNode)) { break; } if (options.sameTypeRootTreeItem && nodeDocshell(cur).sameTypeRootTreeItem != nodeDocshell(node.rawNode).sameTypeRootTreeItem) { break; } parents.push(this._ref(cur)); } return parents; }, parentNode: function (node) { let walker = this.getDocumentWalker(node.rawNode); let parent = walker.parentNode(); if (parent) { return this._ref(parent); } return null; }, /** * If the given NodeActor only has a single text node as a child with a text * content small enough to be inlined, return that child's NodeActor. * * @param NodeActor node */ inlineTextChild: function (node) { // Quick checks to prevent creating a new walker if possible. if (node.isBeforePseudoElement || node.isAfterPseudoElement || node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE || node.rawNode.children.length > 0) { return undefined; } let docWalker = this.getDocumentWalker(node.rawNode); let firstChild = docWalker.firstChild(); // Bail out if: // - more than one child // - unique child is not a text node // - unique child is a text node, but is too long to be inlined if (!firstChild || docWalker.nextSibling() || firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE || firstChild.nodeValue.length > gValueSummaryLength ) { return undefined; } return this._ref(firstChild); }, /** * Mark a node as 'retained'. * * A retained node is not released when `releaseNode` is called on its * parent, or when a parent is released with the `cleanup` option to * `getMutations`. * * When a retained node's parent is released, a retained mode is added to * the walker's "retained orphans" list. * * Retained nodes can be deleted by providing the `force` option to * `releaseNode`. They will also be released when their document * has been destroyed. * * Retaining a node makes no promise about its children; They can * still be removed by normal means. */ retainNode: function (node) { node.retained = true; }, /** * Remove the 'retained' mark from a node. If the node was a * retained orphan, release it. */ unretainNode: function (node) { node.retained = false; if (this._retainedOrphans.has(node)) { this._retainedOrphans.delete(node); this.releaseNode(node); } }, /** * Release actors for a node and all child nodes. */ releaseNode: function (node, options = {}) { if (isNodeDead(node)) { return; } if (node.retained && !options.force) { this._retainedOrphans.add(node); return; } if (node.retained) { // Forcing a retained node to go away. this._retainedOrphans.delete(node); } let walker = this.getDocumentWalker(node.rawNode); let child = walker.firstChild(); while (child) { let childActor = this.getNode(child); if (childActor) { this.releaseNode(childActor, options); } child = walker.nextSibling(); } node.destroy(); }, /** * Add any nodes between `node` and the walker's root node that have not * yet been seen by the client. */ ensurePathToRoot: function (node, newParents = new Set()) { if (!node) { return newParents; } let walker = this.getDocumentWalker(node.rawNode); let cur; while ((cur = walker.parentNode())) { let parent = this.getNode(cur); if (!parent) { // This parent didn't exist, so hasn't been seen by the client yet. newParents.add(this._ref(cur)); } else { // This parent did exist, so the client knows about it. return newParents; } } return newParents; }, /** * Return children of the given node. By default this method will return * all children of the node, but there are options that can restrict this * to a more manageable subset. * * @param NodeActor node * The node whose children you're curious about. * @param object options * Named options: * `maxNodes`: The set of nodes returned by the method will be no longer * than maxNodes. * `start`: If a node is specified, the list of nodes will start * with the given child. Mutally exclusive with `center`. * `center`: If a node is specified, the given node will be as centered * as possible in the list, given how close to the ends of the child * list it is. Mutually exclusive with `start`. * `whatToShow`: A bitmask of node types that should be included. See * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. * * @returns an object with three items: * hasFirst: true if the first child of the node is included in the list. * hasLast: true if the last child of the node is included in the list. * nodes: Child nodes returned by the request. */ children: function (node, options = {}) { if (isNodeDead(node)) { return { hasFirst: true, hasLast: true, nodes: [] }; } if (options.center && options.start) { throw Error("Can't specify both 'center' and 'start' options."); } let maxNodes = options.maxNodes || -1; if (maxNodes == -1) { maxNodes = Number.MAX_VALUE; } // We're going to create a few document walkers with the same filter, // make it easier. let getFilteredWalker = documentWalkerNode => { return this.getDocumentWalker(documentWalkerNode, options.whatToShow); }; // Need to know the first and last child. let rawNode = node.rawNode; let firstChild = getFilteredWalker(rawNode).firstChild(); let lastChild = getFilteredWalker(rawNode).lastChild(); if (!firstChild) { // No children, we're done. return { hasFirst: true, hasLast: true, nodes: [] }; } let start; if (options.center) { start = options.center.rawNode; } else if (options.start) { start = options.start.rawNode; } else { start = firstChild; } let nodes = []; // Start by reading backward from the starting point if we're centering... let backwardWalker = getFilteredWalker(start); if (start != firstChild && options.center) { backwardWalker.previousSibling(); let backwardCount = Math.floor(maxNodes / 2); let backwardNodes = this._readBackward(backwardWalker, backwardCount); nodes = backwardNodes; } // Then read forward by any slack left in the max children... let forwardWalker = getFilteredWalker(start); let forwardCount = maxNodes - nodes.length; nodes = nodes.concat(this._readForward(forwardWalker, forwardCount)); // If there's any room left, it means we've run all the way to the end. // If we're centering, check if there are more items to read at the front. let remaining = maxNodes - nodes.length; if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) { let firstNodes = this._readBackward(backwardWalker, remaining); // Then put it all back together. nodes = firstNodes.concat(nodes); } return { hasFirst: nodes[0].rawNode == firstChild, hasLast: nodes[nodes.length - 1].rawNode == lastChild, nodes: nodes }; }, /** * Return siblings of the given node. By default this method will return * all siblings of the node, but there are options that can restrict this * to a more manageable subset. * * If `start` or `center` are not specified, this method will center on the * node whose siblings are requested. * * @param NodeActor node * The node whose children you're curious about. * @param object options * Named options: * `maxNodes`: The set of nodes returned by the method will be no longer * than maxNodes. * `start`: If a node is specified, the list of nodes will start * with the given child. Mutally exclusive with `center`. * `center`: If a node is specified, the given node will be as centered * as possible in the list, given how close to the ends of the child * list it is. Mutually exclusive with `start`. * `whatToShow`: A bitmask of node types that should be included. See * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. * * @returns an object with three items: * hasFirst: true if the first child of the node is included in the list. * hasLast: true if the last child of the node is included in the list. * nodes: Child nodes returned by the request. */ siblings: function (node, options = {}) { if (isNodeDead(node)) { return { hasFirst: true, hasLast: true, nodes: [] }; } let parentNode = this.getDocumentWalker(node.rawNode, options.whatToShow) .parentNode(); if (!parentNode) { return { hasFirst: true, hasLast: true, nodes: [node] }; } if (!(options.start || options.center)) { options.center = node; } return this.children(this._ref(parentNode), options); }, /** * Get the next sibling of a given node. Getting nodes one at a time * might be inefficient, be careful. * * @param object options * Named options: * `whatToShow`: A bitmask of node types that should be included. See * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. */ nextSibling: function (node, options = {}) { if (isNodeDead(node)) { return null; } let walker = this.getDocumentWalker(node.rawNode, options.whatToShow); let sibling = walker.nextSibling(); return sibling ? this._ref(sibling) : null; }, /** * Get the previous sibling of a given node. Getting nodes one at a time * might be inefficient, be careful. * * @param object options * Named options: * `whatToShow`: A bitmask of node types that should be included. See * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. */ previousSibling: function (node, options = {}) { if (isNodeDead(node)) { return null; } let walker = this.getDocumentWalker(node.rawNode, options.whatToShow); let sibling = walker.previousSibling(); return sibling ? this._ref(sibling) : null; }, /** * Helper function for the `children` method: Read forward in the sibling * list into an array with `count` items, including the current node. */ _readForward: function (walker, count) { let ret = []; let node = walker.currentNode; do { ret.push(this._ref(node)); node = walker.nextSibling(); } while (node && --count); return ret; }, /** * Helper function for the `children` method: Read backward in the sibling * list into an array with `count` items, including the current node. */ _readBackward: function (walker, count) { let ret = []; let node = walker.currentNode; do { ret.push(this._ref(node)); node = walker.previousSibling(); } while (node && --count); ret.reverse(); return ret; }, /** * Return the node that the parent process has asked to * inspect. This node is expected to be stored in gInspectingNode * (which is set by a message manager message to the child.js frame * script). The node is returned over the remote debugging protocol * as a NodeFront. */ findInspectingNode: function () { let node = gInspectingNode; if (!node) { return {}; } return this.attachElement(node); }, /** * Return the first node in the document that matches the given selector. * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector * * @param NodeActor baseNode * @param string selector */ querySelector: function (baseNode, selector) { if (isNodeDead(baseNode)) { return {}; } let node = baseNode.rawNode.querySelector(selector); if (!node) { return {}; } return this.attachElement(node); }, /** * Return a NodeListActor with all nodes that match the given selector. * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll * * @param NodeActor baseNode * @param string selector */ querySelectorAll: function (baseNode, selector) { let nodeList = null; try { nodeList = baseNode.rawNode.querySelectorAll(selector); } catch (e) { // Bad selector. Do nothing as the selector can come from a searchbox. } return new NodeListActor(this, nodeList); }, /** * Get a list of nodes that match the given selector in all known frames of * the current content page. * @param {String} selector. * @return {Array} */ _multiFrameQuerySelectorAll: function (selector) { let nodes = []; for (let {document} of this.tabActor.windows) { try { nodes = [...nodes, ...document.querySelectorAll(selector)]; } catch (e) { // Bad selector. Do nothing as the selector can come from a searchbox. } } return nodes; }, /** * Return a NodeListActor with all nodes that match the given selector in all * frames of the current content page. * @param {String} selector */ multiFrameQuerySelectorAll: function (selector) { return new NodeListActor(this, this._multiFrameQuerySelectorAll(selector)); }, /** * Search the document for a given string. * Results will be searched with the walker-search module (searches through * tag names, attribute names and values, and text contents). * * @returns {searchresult} * - {NodeList} list * - {Array<Object>} metadata. Extra information with indices that * match up with node list. */ search: function (query) { let results = this.walkerSearch.search(query); let nodeList = new NodeListActor(this, results.map(r => r.node)); return { list: nodeList, metadata: [] }; }, /** * Returns a list of matching results for CSS selector autocompletion. * * @param string query * The selector query being completed * @param string completing * The exact token being completed out of the query * @param string selectorState * One of "pseudo", "id", "tag", "class", "null" */ getSuggestionsForQuery: function (query, completing, selectorState) { let sugs = { classes: new Map(), tags: new Map(), ids: new Map() }; let result = []; let nodes = null; // Filtering and sorting the results so that protocol transfer is miminal. switch (selectorState) { case "pseudo": result = PSEUDO_SELECTORS.filter(item => { return item[0].startsWith(":" + completing); }); break; case "class": if (!query) { nodes = this._multiFrameQuerySelectorAll("[class]"); } else { nodes = this._multiFrameQuerySelectorAll(query); } for (let node of nodes) { for (let className of node.classList) { sugs.classes.set(className, (sugs.classes.get(className)|0) + 1); } } sugs.classes.delete(""); sugs.classes.delete(HIDDEN_CLASS); for (let [className, count] of sugs.classes) { if (className.startsWith(completing)) { result.push(["." + CSS.escape(className), count, selectorState]); } } break; case "id": if (!query) { nodes = this._multiFrameQuerySelectorAll("[id]"); } else { nodes = this._multiFrameQuerySelectorAll(query); } for (let node of nodes) { sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1); } for (let [id, count] of sugs.ids) { if (id.startsWith(completing) && id !== "") { result.push(["#" + CSS.escape(id), count, selectorState]); } } break; case "tag": if (!query) { nodes = this._multiFrameQuerySelectorAll("*"); } else { nodes = this._multiFrameQuerySelectorAll(query); } for (let node of nodes) { let tag = node.localName; sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1); } for (let [tag, count] of sugs.tags) { if ((new RegExp("^" + completing + ".*", "i")).test(tag)) { result.push([tag, count, selectorState]); } } // For state 'tag' (no preceding # or .) and when there's no query (i.e. // only one word) then search for the matching classes and ids if (!query) { result = [ ...result, ...this.getSuggestionsForQuery(null, completing, "class") .suggestions, ...this.getSuggestionsForQuery(null, completing, "id") .suggestions ]; } break; case "null": nodes = this._multiFrameQuerySelectorAll(query); for (let node of nodes) { sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1); let tag = node.localName; sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1); for (let className of node.classList) { sugs.classes.set(className, (sugs.classes.get(className)|0) + 1); } } for (let [tag, count] of sugs.tags) { tag && result.push([tag, count]); } for (let [id, count] of sugs.ids) { id && result.push(["#" + id, count]); } sugs.classes.delete(""); sugs.classes.delete(HIDDEN_CLASS); for (let [className, count] of sugs.classes) { className && result.push(["." + className, count]); } } // Sort by count (desc) and name (asc) result = result.sort((a, b) => { // Computed a sortable string with first the inverted count, then the name let sortA = (10000 - a[1]) + a[0]; let sortB = (10000 - b[1]) + b[0]; // Prefixing ids, classes and tags, to group results let firstA = a[0].substring(0, 1); let firstB = b[0].substring(0, 1); if (firstA === "#") { sortA = "2" + sortA; } else if (firstA === ".") { sortA = "1" + sortA; } else { sortA = "0" + sortA; } if (firstB === "#") { sortB = "2" + sortB; } else if (firstB === ".") { sortB = "1" + sortB; } else { sortB = "0" + sortB; } // String compare return sortA.localeCompare(sortB); }); result.slice(0, 25); return { query: query, suggestions: result }; }, /** * Add a pseudo-class lock to a node. * * @param NodeActor node * @param string pseudo * A pseudoclass: ':hover', ':active', ':focus' * @param options * Options object: * `parents`: True if the pseudo-class should be added * to parent nodes. * * @returns An empty packet. A "pseudoClassLock" mutation will * be queued for any changed nodes. */ addPseudoClassLock: function (node, pseudo, options = {}) { if (isNodeDead(node)) { return; } // There can be only one node locked per pseudo, so dismiss all existing // ones for (let locked of this._activePseudoClassLocks) { if (DOMUtils.hasPseudoClassLock(locked.rawNode, pseudo)) { this._removePseudoClassLock(locked, pseudo); } } this._addPseudoClassLock(node, pseudo); if (!options.parents) { return; } let walker = this.getDocumentWalker(node.rawNode); let cur; while ((cur = walker.parentNode())) { let curNode = this._ref(cur); this._addPseudoClassLock(curNode, pseudo); } }, _queuePseudoClassMutation: function (node) { this.queueMutation({ target: node.actorID, type: "pseudoClassLock", pseudoClassLocks: node.writePseudoClassLocks() }); }, _addPseudoClassLock: function (node, pseudo) { if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { return false; } DOMUtils.addPseudoClassLock(node.rawNode, pseudo); this._activePseudoClassLocks.add(node); this._queuePseudoClassMutation(node); return true; }, _installHelperSheet: function (node) { if (!this.installedHelpers) { this.installedHelpers = new WeakMap(); } let win = node.rawNode.ownerDocument.defaultView; if (!this.installedHelpers.has(win)) { let { Style } = require("sdk/stylesheet/style"); let { attach } = require("sdk/content/mod"); let style = Style({source: HELPER_SHEET, type: "agent" }); attach(style, win); this.installedHelpers.set(win, style); } }, hideNode: function (node) { if (isNodeDead(node)) { return; } this._installHelperSheet(node); node.rawNode.classList.add(HIDDEN_CLASS); }, unhideNode: function (node) { if (isNodeDead(node)) { return; } node.rawNode.classList.remove(HIDDEN_CLASS); }, /** * Remove a pseudo-class lock from a node. * * @param NodeActor node * @param string pseudo * A pseudoclass: ':hover', ':active', ':focus' * @param options * Options object: * `parents`: True if the pseudo-class should be removed * from parent nodes. * * @returns An empty response. "pseudoClassLock" mutations * will be emitted for any changed nodes. */ removePseudoClassLock: function (node, pseudo, options = {}) { if (isNodeDead(node)) { return; } this._removePseudoClassLock(node, pseudo); // Remove pseudo class for children as we don't want to allow // turning it on for some childs without setting it on some parents for (let locked of this._activePseudoClassLocks) { if (node.rawNode.contains(locked.rawNode) && DOMUtils.hasPseudoClassLock(locked.rawNode, pseudo)) { this._removePseudoClassLock(locked, pseudo); } } if (!options.parents) { return; } let walker = this.getDocumentWalker(node.rawNode); let cur; while ((cur = walker.parentNode())) { let curNode = this._ref(cur); this._removePseudoClassLock(curNode, pseudo); } }, _removePseudoClassLock: function (node, pseudo) { if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) { return false; } DOMUtils.removePseudoClassLock(node.rawNode, pseudo); if (!node.writePseudoClassLocks()) { this._activePseudoClassLocks.delete(node); } this._queuePseudoClassMutation(node); return true; }, /** * Clear all the pseudo-classes on a given node or all nodes. * @param {NodeActor} node Optional node to clear pseudo-classes on */ clearPseudoClassLocks: function (node) { if (node && isNodeDead(node)) { return; } if (node) { DOMUtils.clearPseudoClassLocks(node.rawNode); this._activePseudoClassLocks.delete(node); this._queuePseudoClassMutation(node); } else { for (let locked of this._activePseudoClassLocks) { DOMUtils.clearPseudoClassLocks(locked.rawNode); this._activePseudoClassLocks.delete(locked); this._queuePseudoClassMutation(locked); } } }, /** * Get a node's innerHTML property. */ innerHTML: function (node) { let html = ""; if (!isNodeDead(node)) { html = node.rawNode.innerHTML; } return LongStringActor(this.conn, html); }, /** * Set a node's innerHTML property. * * @param {NodeActor} node The node. * @param {string} value The piece of HTML content. */ setInnerHTML: function (node, value) { if (isNodeDead(node)) { return; } let rawNode = node.rawNode; if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE) { throw new Error("Can only change innerHTML to element nodes"); } rawNode.innerHTML = value; }, /** * Get a node's outerHTML property. * * @param {NodeActor} node The node. */ outerHTML: function (node) { let outerHTML = ""; if (!isNodeDead(node)) { outerHTML = node.rawNode.outerHTML; } return LongStringActor(this.conn, outerHTML); }, /** * Set a node's outerHTML property. * * @param {NodeActor} node The node. * @param {string} value The piece of HTML content. */ setOuterHTML: function (node, value) { if (isNodeDead(node)) { return; } let parsedDOM = DOMParser.parseFromString(value, "text/html"); let rawNode = node.rawNode; let parentNode = rawNode.parentNode; // Special case for head and body. Setting document.body.outerHTML // creates an extra <head> tag, and document.head.outerHTML creates // an extra <body>. So instead we will call replaceChild with the // parsed DOM, assuming that they aren't trying to set both tags at once. if (rawNode.tagName === "BODY") { if (parsedDOM.head.innerHTML === "") { parentNode.replaceChild(parsedDOM.body, rawNode); } else { rawNode.outerHTML = value; } } else if (rawNode.tagName === "HEAD") { if (parsedDOM.body.innerHTML === "") { parentNode.replaceChild(parsedDOM.head, rawNode); } else { rawNode.outerHTML = value; } } else if (node.isDocumentElement()) { // Unable to set outerHTML on the document element. Fall back by // setting attributes manually, then replace the body and head elements. let finalAttributeModifications = []; let attributeModifications = {}; for (let attribute of rawNode.attributes) { attributeModifications[attribute.name] = null; } for (let attribute of parsedDOM.documentElement.attributes) { attributeModifications[attribute.name] = attribute.value; } for (let key in attributeModifications) { finalAttributeModifications.push({ attributeName: key, newValue: attributeModifications[key] }); } node.modifyAttributes(finalAttributeModifications); rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head")); rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body")); } else { rawNode.outerHTML = value; } }, /** * Insert adjacent HTML to a node. * * @param {Node} node * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd", * "afterEnd" (see Element.insertAdjacentHTML). * @param {string} value The HTML content. */ insertAdjacentHTML: function (node, position, value) { if (isNodeDead(node)) { return {node: [], newParents: []}; } let rawNode = node.rawNode; let isInsertAsSibling = position === "beforeBegin" || position === "afterEnd"; // Don't insert anything adjacent to the document element. if (isInsertAsSibling && node.isDocumentElement()) { throw new Error("Can't insert adjacent element to the root."); } let rawParentNode = rawNode.parentNode; if (!rawParentNode && isInsertAsSibling) { throw new Error("Can't insert as sibling without parent node."); } // We can't use insertAdjacentHTML, because we want to return the nodes // being created (so the front can remove them if the user undoes // the change). So instead, use Range.createContextualFragment(). let range = rawNode.ownerDocument.createRange(); if (position === "beforeBegin" || position === "afterEnd") { range.selectNode(rawNode); } else { range.selectNodeContents(rawNode); } let docFrag = range.createContextualFragment(value); let newRawNodes = Array.from(docFrag.childNodes); switch (position) { case "beforeBegin": rawParentNode.insertBefore(docFrag, rawNode); break; case "afterEnd": // Note: if the second argument is null, rawParentNode.insertBefore // behaves like rawParentNode.appendChild. rawParentNode.insertBefore(docFrag, rawNode.nextSibling); break; case "afterBegin": rawNode.insertBefore(docFrag, rawNode.firstChild); break; case "beforeEnd": rawNode.appendChild(docFrag); break; default: throw new Error("Invalid position value. Must be either " + "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'."); } return this.attachElements(newRawNodes); }, /** * Duplicate a specified node * * @param {NodeActor} node The node to duplicate. */ duplicateNode: function ({rawNode}) { let clonedNode = rawNode.cloneNode(true); rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling); }, /** * Test whether a node is a document or a document element. * * @param {NodeActor} node The node to remove. * @return {boolean} True if the node is a document or a document element. */ isDocumentOrDocumentElementNode: function (node) { return ((node.rawNode.ownerDocument && node.rawNode.ownerDocument.documentElement === this.rawNode) || node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE); }, /** * Removes a node from its parent node. * * @param {NodeActor} node The node to remove. * @returns The node's nextSibling before it was removed. */ removeNode: function (node) { if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) { throw Error("Cannot remove document, document elements or dead nodes."); } let nextSibling = this.nextSibling(node); node.rawNode.remove(); // Mutation events will take care of the rest. return nextSibling; }, /** * Removes an array of nodes from their parent node. * * @param {NodeActor[]} nodes The nodes to remove. */ removeNodes: function (nodes) { // Check that all nodes are valid before processing the removals. for (let node of nodes) { if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) { throw Error("Cannot remove document, document elements or dead nodes"); } } for (let node of nodes) { node.rawNode.remove(); // Mutation events will take care of the rest. } }, /** * Insert a node into the DOM. */ insertBefore: function (node, parent, sibling) { if (isNodeDead(node) || isNodeDead(parent) || (sibling && isNodeDead(sibling))) { return; } let rawNode = node.rawNode; let rawParent = parent.rawNode; let rawSibling = sibling ? sibling.rawNode : null; // Don't bother inserting a node if the document position isn't going // to change. This prevents needless iframes reloading and mutations. if (rawNode.parentNode === rawParent) { let currentNextSibling = this.nextSibling(node); currentNextSibling = currentNextSibling ? currentNextSibling.rawNode : null; if (rawNode === rawSibling || currentNextSibling === rawSibling) { return; } } rawParent.insertBefore(rawNode, rawSibling); }, /** * Editing a node's tagname actually means creating a new node with the same * attributes, removing the node and inserting the new one instead. * This method does not return anything as mutation events are taking care of * informing the consumers about changes. */ editTagName: function (node, tagName) { if (isNodeDead(node)) { return null; } let oldNode = node.rawNode; // Create a new element with the same attributes as the current element and // prepare to replace the current node with it. let newNode; try { newNode = nodeDocument(oldNode).createElement(tagName); } catch (x) { // Failed to create a new element with that tag name, ignore the change, // and signal the error to the front. return Promise.reject(new Error("Could not change node's tagName to " + tagName)); } let attrs = oldNode.attributes; for (let i = 0; i < attrs.length; i++) { newNode.setAttribute(attrs[i].name, attrs[i].value); } // Insert the new node, and transfer the old node's children. oldNode.parentNode.insertBefore(newNode, oldNode); while (oldNode.firstChild) { newNode.appendChild(oldNode.firstChild); } oldNode.remove(); return null; }, /** * Get any pending mutation records. Must be called by the client after * the `new-mutations` notification is received. Returns an array of * mutation records. * * Mutation records have a basic structure: * * { * type: attributes|characterData|childList, * target: <domnode actor ID>, * } * * And additional attributes based on the mutation type: * * `attributes` type: * attributeName: <string> - the attribute that changed * attributeNamespace: <string> - the attribute's namespace URI, if any. * newValue: <string> - The new value of the attribute, if any. * * `characterData` type: * newValue: <string> - the new nodeValue for the node * * `childList` type is returned when the set of children for a node * has changed. Includes extra data, which can be used by the client to * maintain its ownership subtree. * * added: array of <domnode actor ID> - The list of actors *previously * seen by the client* that were added to the target node. * removed: array of <domnode actor ID> The list of actors *previously * seen by the client* that were removed from the target node. * inlineTextChild: If the node now has a single text child, it will * be sent here. * * Actors that are included in a MutationRecord's `removed` but * not in an `added` have been removed from the client's ownership * tree (either by being moved under a node the client has seen yet * or by being removed from the tree entirely), and is considered * 'orphaned'. * * Keep in mind that if a node that the client hasn't seen is moved * into or out of the target node, it will not be included in the * removedNodes and addedNodes list, so if the client is interested * in the new set of children it needs to issue a `children` request. */ getMutations: function (options = {}) { let pending = this._pendingMutations || []; this._pendingMutations = []; if (options.cleanup) { for (let node of this._orphaned) { // Release the orphaned node. Nodes or children that have been // retained will be moved to this._retainedOrphans. this.releaseNode(node); } this._orphaned = new Set(); } return pending; }, queueMutation: function (mutation) { if (!this.actorID || this._destroyed) { // We've been destroyed, don't bother queueing this mutation. return; } // We only send the `new-mutations` notification once, until the client // fetches mutations with the `getMutations` packet. let needEvent = this._pendingMutations.length === 0; this._pendingMutations.push(mutation); if (needEvent) { events.emit(this, "new-mutations"); } }, /** * Handles mutations from the DOM mutation observer API. * * @param array[MutationRecord] mutations * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord */ onMutations: function (mutations) { // Notify any observers that want *all* mutations (even on nodes that aren't // referenced). This is not sent over the protocol so can only be used by // scripts running in the server process. events.emit(this, "any-mutation"); for (let change of mutations) { let targetActor = this.getNode(change.target); if (!targetActor) { continue; } let targetNode = change.target; let type = change.type; let mutation = { type: type, target: targetActor.actorID, }; if (type === "attributes") { mutation.attributeName = change.attributeName; mutation.attributeNamespace = change.attributeNamespace || undefined; mutation.newValue = targetNode.hasAttribute(mutation.attributeName) ? targetNode.getAttribute(mutation.attributeName) : null; } else if (type === "characterData") { mutation.newValue = targetNode.nodeValue; this._maybeQueueInlineTextChildMutation(change, targetNode); } else if (type === "childList" || type === "nativeAnonymousChildList") { // Get the list of removed and added actors that the client has seen // so that it can keep its ownership tree up to date. let removedActors = []; let addedActors = []; for (let removed of change.removedNodes) { let removedActor = this.getNode(removed); if (!removedActor) { // If the client never encountered this actor we don't need to // mention that it was removed. continue; } // While removed from the tree, nodes are saved as orphaned. this._orphaned.add(removedActor); removedActors.push(removedActor.actorID); } for (let added of change.addedNodes) { let addedActor = this.getNode(added); if (!addedActor) { // If the client never encounted this actor we don't need to tell // it about its addition for ownership tree purposes - if the // client wants to see the new nodes it can ask for children. continue; } // The actor is reconnected to the ownership tree, unorphan // it and let the client know so that its ownership tree is up // to date. this._orphaned.delete(addedActor); addedActors.push(addedActor.actorID); } mutation.numChildren = targetActor.numChildren; mutation.removed = removedActors; mutation.added = addedActors; let inlineTextChild = this.inlineTextChild(targetActor); if (inlineTextChild) { mutation.inlineTextChild = inlineTextChild.form(); } } this.queueMutation(mutation); } }, /** * Check if the provided mutation could change the way the target element is * inlined with its parent node. If it might, a custom mutation of type * "inlineTextChild" will be queued. * * @param {MutationRecord} mutation * A characterData type mutation */ _maybeQueueInlineTextChildMutation: function (mutation) { let {oldValue, target} = mutation; let newValue = target.nodeValue; let limit = gValueSummaryLength; if ((oldValue.length <= limit && newValue.length <= limit) || (oldValue.length > limit && newValue.length > limit)) { // Bail out if the new & old values are both below/above the size limit. return; } let parentActor = this.getNode(target.parentNode); if (!parentActor || parentActor.rawNode.children.length > 0) { // If the parent node has other children, a character data mutation will // not change anything regarding inlining text nodes. return; } let inlineTextChild = this.inlineTextChild(parentActor); this.queueMutation({ type: "inlineTextChild", target: parentActor.actorID, inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined }); }, onFrameLoad: function ({ window, isTopLevel }) { if (isTopLevel) { // If we initialize the inspector while the document is loading, // we may already have a root document set in the constructor. if (this.rootDoc && !Cu.isDeadWrapper(this.rootDoc) && this.rootDoc.defaultView) { this.onFrameUnload({ window: this.rootDoc.defaultView }); } this.rootDoc = window.document; this.rootNode = this.document(); this.queueMutation({ type: "newRoot", target: this.rootNode.form() }); return; } let frame = getFrameElement(window); let frameActor = this.getNode(frame); if (!frameActor) { return; } this.queueMutation({ type: "frameLoad", target: frameActor.actorID, }); // Send a childList mutation on the frame. this.queueMutation({ type: "childList", target: frameActor.actorID, added: [], removed: [] }); }, // Returns true if domNode is in window or a subframe. _childOfWindow: function (window, domNode) { let win = nodeDocument(domNode).defaultView; while (win) { if (win === window) { return true; } win = getFrameElement(win); } return false; }, onFrameUnload: function ({ window }) { // Any retained orphans that belong to this document // or its children need to be released, and a mutation sent // to notify of that. let releasedOrphans = []; for (let retained of this._retainedOrphans) { if (Cu.isDeadWrapper(retained.rawNode) || this._childOfWindow(window, retained.rawNode)) { this._retainedOrphans.delete(retained); releasedOrphans.push(retained.actorID); this.releaseNode(retained, { force: true }); } } if (releasedOrphans.length > 0) { this.queueMutation({ target: this.rootNode.actorID, type: "unretained", nodes: releasedOrphans }); } let doc = window.document; let documentActor = this.getNode(doc); if (!documentActor) { return; } if (this.rootDoc === doc) { this.rootDoc = null; this.rootNode = null; } this.queueMutation({ type: "documentUnload", target: documentActor.actorID }); let walker = this.getDocumentWalker(doc); let parentNode = walker.parentNode(); if (parentNode) { // Send a childList mutation on the frame so that clients know // they should reread the children list. this.queueMutation({ type: "childList", target: this.getNode(parentNode).actorID, added: [], removed: [] }); } // Need to force a release of this node, because those nodes can't // be accessed anymore. this.releaseNode(documentActor, { force: true }); }, /** * Check if a node is attached to the DOM tree of the current page. * @param {nsIDomNode} rawNode * @return {Boolean} false if the node is removed from the tree or within a * document fragment */ _isInDOMTree: function (rawNode) { let walker = this.getDocumentWalker(rawNode); let current = walker.currentNode; // Reaching the top of tree while (walker.parentNode()) { current = walker.currentNode; } // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't // attached if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE || current !== this.rootDoc) { return false; } // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc return true; }, /** * @see _isInDomTree */ isInDOMTree: function (node) { if (isNodeDead(node)) { return false; } return this._isInDOMTree(node.rawNode); }, /** * Given an ObjectActor (identified by its ID), commonly used in the debugger, * webconsole and variablesView, return the corresponding inspector's * NodeActor */ getNodeActorFromObjectActor: function (objectActorID) { let actor = this.conn.getActor(objectActorID); if (!actor) { return null; } let debuggerObject = this.conn.getActor(objectActorID).obj; let rawNode = debuggerObject.unsafeDereference(); if (!this._isInDOMTree(rawNode)) { return null; } // This is a special case for the document object whereby it is considered // as document.documentElement (the <html> node) if (rawNode.defaultView && rawNode === rawNode.defaultView.document) { rawNode = rawNode.documentElement; } return this.attachElement(rawNode); }, /** * Given a StyleSheetActor (identified by its ID), commonly used in the * style-editor, get its ownerNode and return the corresponding walker's * NodeActor. * Note that getNodeFromActor was added later and can now be used instead. */ getStyleSheetOwnerNode: function (styleSheetActorID) { return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]); }, /** * This method can be used to retrieve NodeActor for DOM nodes from other * actors in a way that they can later be highlighted in the page, or * selected in the inspector. * If an actor has a reference to a DOM node, and the UI needs to know about * this DOM node (and possibly select it in the inspector), the UI should * first retrieve a reference to the walkerFront: * * // Make sure the inspector/walker have been initialized first. * toolbox.initInspector().then(() => { * // Retrieve the walker. * let walker = toolbox.walker; * }); * * And then call this method: * * // Get the nodeFront from my actor, passing the ID and properties path. * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => { * // Use the nodeFront, e.g. select the node in the inspector. * toolbox.getPanel("inspector").selection.setNodeFront(nodeFront); * }); * * @param {String} actorID The ID for the actor that has a reference to the * DOM node. * @param {Array} path Where, on the actor, is the DOM node stored. If in the * scope of the actor, the node is available as `this.data.node`, then this * should be ["data", "node"]. * @return {NodeActor} The attached NodeActor, or null if it couldn't be * found. */ getNodeFromActor: function (actorID, path) { let actor = this.conn.getActor(actorID); if (!actor) { return null; } let obj = actor; for (let name of path) { if (!(name in obj)) { return null; } obj = obj[name]; } return this.attachElement(obj); }, /** * Returns an instance of the LayoutActor that is used to retrieve CSS layout-related * information. * * @return {LayoutActor} */ getLayoutInspector: function () { if (!this.layoutActor) { this.layoutActor = new LayoutActor(this.conn, this.tabActor, this); } return this.layoutActor; }, }); /** * Server side of the inspector actor, which is used to create * inspector-related actors, including the walker. */ exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, { initialize: function (conn, tabActor) { protocol.Actor.prototype.initialize.call(this, conn); this.tabActor = tabActor; this._onColorPicked = this._onColorPicked.bind(this); this._onColorPickCanceled = this._onColorPickCanceled.bind(this); this.destroyEyeDropper = this.destroyEyeDropper.bind(this); }, destroy: function () { protocol.Actor.prototype.destroy.call(this); this.destroyEyeDropper(); this._highlighterPromise = null; this._pageStylePromise = null; this._walkerPromise = null; this.walker = null; this.tabActor = null; }, // Forces destruction of the actor and all its children // like highlighter, walker and style actors. disconnect: function () { this.destroy(); }, get window() { return this.tabActor.window; }, getWalker: function (options = {}) { if (this._walkerPromise) { return this._walkerPromise; } let deferred = promise.defer(); this._walkerPromise = deferred.promise; let window = this.window; let domReady = () => { let tabActor = this.tabActor; window.removeEventListener("DOMContentLoaded", domReady, true); this.walker = WalkerActor(this.conn, tabActor, options); this.manage(this.walker); events.once(this.walker, "destroyed", () => { this._walkerPromise = null; this._pageStylePromise = null; }); deferred.resolve(this.walker); }; if (window.document.readyState === "loading") { window.addEventListener("DOMContentLoaded", domReady, true); } else { domReady(); } return this._walkerPromise; }, getPageStyle: function () { if (this._pageStylePromise) { return this._pageStylePromise; } this._pageStylePromise = this.getWalker().then(walker => { let pageStyle = PageStyleActor(this); this.manage(pageStyle); return pageStyle; }); return this._pageStylePromise; }, /** * The most used highlighter actor is the HighlighterActor which can be * conveniently retrieved by this method. * The same instance will always be returned by this method when called * several times. * The highlighter actor returned here is used to highlighter elements's * box-models from the markup-view, box model, console, debugger, ... as * well as select elements with the pointer (pick). * * @param {Boolean} autohide Optionally autohide the highlighter after an * element has been picked * @return {HighlighterActor} */ getHighlighter: function (autohide) { if (this._highlighterPromise) { return this._highlighterPromise; } this._highlighterPromise = this.getWalker().then(walker => { let highlighter = HighlighterActor(this, autohide); this.manage(highlighter); return highlighter; }); return this._highlighterPromise; }, /** * If consumers need to display several highlighters at the same time or * different types of highlighters, then this method should be used, passing * the type name of the highlighter needed as argument. * A new instance will be created everytime the method is called, so it's up * to the consumer to release it when it is not needed anymore * * @param {String} type The type of highlighter to create * @return {Highlighter} The highlighter actor instance or null if the * typeName passed doesn't match any available highlighter */ getHighlighterByType: function (typeName) { if (isTypeRegistered(typeName)) { return CustomHighlighterActor(this, typeName); } return null; }, /** * 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 */ getImageDataFromURL: function (url, maxDim) { let img = new this.window.Image(); img.src = url; // imageToImageData waits for the image to load. return imageToImageData(img, maxDim).then(imageData => { return { data: LongStringActor(this.conn, imageData.data), size: imageData.size }; }); }, /** * Resolve a URL to its absolute form, in the scope of a given content window. * @param {String} url. * @param {NodeActor} node If provided, the owner window of this node will be * used to resolve the URL. Otherwise, the top-level content window will be * used instead. * @return {String} url. */ resolveRelativeURL: function (url, node) { let document = isNodeDead(node) ? this.window.document : nodeDocument(node.rawNode); if (!document) { return url; } let baseURI = Services.io.newURI(document.location.href, null, null); return Services.io.newURI(url, null, baseURI).spec; }, /** * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper. * Note that for now, a new instance is created every time to deal with page navigation. */ createEyeDropper: function () { this.destroyEyeDropper(); this._highlighterEnv = new HighlighterEnvironment(); this._highlighterEnv.initFromTabActor(this.tabActor); this._eyeDropper = new EyeDropper(this._highlighterEnv); }, /** * Destroy the current eye-dropper highlighter instance. */ destroyEyeDropper: function () { if (this._eyeDropper) { this.cancelPickColorFromPage(); this._eyeDropper.destroy(); this._eyeDropper = null; this._highlighterEnv.destroy(); this._highlighterEnv = null; } }, /** * Pick a color from the page using the eye-dropper. This method doesn't return anything * but will cause events to be sent to the front when a color is picked or when the user * cancels the picker. * @param {Object} options */ pickColorFromPage: function (options) { this.createEyeDropper(); this._eyeDropper.show(this.window.document.documentElement, options); this._eyeDropper.once("selected", this._onColorPicked); this._eyeDropper.once("canceled", this._onColorPickCanceled); events.once(this.tabActor, "will-navigate", this.destroyEyeDropper); }, /** * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper * highlighter is for the user to click in the page and select a color. If you need to * dismiss the eye-dropper programatically instead, use this method. */ cancelPickColorFromPage: function () { if (this._eyeDropper) { this._eyeDropper.hide(); this._eyeDropper.off("selected", this._onColorPicked); this._eyeDropper.off("canceled", this._onColorPickCanceled); events.off(this.tabActor, "will-navigate", this.destroyEyeDropper); } }, _onColorPicked: function (e, color) { events.emit(this, "color-picked", color); }, _onColorPickCanceled: function () { events.emit(this, "color-pick-canceled"); } }); // Exported for test purposes. exports._documentWalker = DocumentWalker; function nodeDocument(node) { if (Cu.isDeadWrapper(node)) { return null; } return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null); } function nodeDocshell(node) { let doc = node ? nodeDocument(node) : null; let win = doc ? doc.defaultView : null; if (win) { return win.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDocShell); } return null; } function isNodeDead(node) { return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode); } /** * Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods. * See inDeepTreeWalker for more information about the methods. * * @param {DOMNode} node * @param {Window} rootWin * @param {Int} whatToShow See nodeFilterConstants / inIDeepTreeWalker for * options. * @param {Function} filter A custom filter function Taking in a DOMNode * and returning an Int. See WalkerActor.nodeFilter for an example. */ function DocumentWalker(node, rootWin, whatToShow = nodeFilterConstants.SHOW_ALL, filter = standardTreeWalkerFilter) { if (!rootWin.location) { throw new Error("Got an invalid root window in DocumentWalker"); } this.walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"] .createInstance(Ci.inIDeepTreeWalker); this.walker.showAnonymousContent = true; this.walker.showSubDocuments = true; this.walker.showDocumentsAsNodes = true; this.walker.init(rootWin.document, whatToShow); this.filter = filter; // Make sure that the walker knows about the initial node (which could // be skipped due to a filter). Note that simply calling parentNode() // causes currentNode to be updated. this.walker.currentNode = node; while (node && this.filter(node) === nodeFilterConstants.FILTER_SKIP) { node = this.walker.parentNode(); } } DocumentWalker.prototype = { get node() { return this.walker.node; }, get whatToShow() { return this.walker.whatToShow; }, get currentNode() { return this.walker.currentNode; }, set currentNode(val) { this.walker.currentNode = val; }, parentNode: function () { return this.walker.parentNode(); }, nextNode: function () { let node = this.walker.currentNode; if (!node) { return null; } let nextNode = this.walker.nextNode(); while (nextNode && this.filter(nextNode) === nodeFilterConstants.FILTER_SKIP) { nextNode = this.walker.nextNode(); } return nextNode; }, firstChild: function () { let node = this.walker.currentNode; if (!node) { return null; } let firstChild = this.walker.firstChild(); while (firstChild && this.filter(firstChild) === nodeFilterConstants.FILTER_SKIP) { firstChild = this.walker.nextSibling(); } return firstChild; }, lastChild: function () { let node = this.walker.currentNode; if (!node) { return null; } let lastChild = this.walker.lastChild(); while (lastChild && this.filter(lastChild) === nodeFilterConstants.FILTER_SKIP) { lastChild = this.walker.previousSibling(); } return lastChild; }, previousSibling: function () { let node = this.walker.previousSibling(); while (node && this.filter(node) === nodeFilterConstants.FILTER_SKIP) { node = this.walker.previousSibling(); } return node; }, nextSibling: function () { let node = this.walker.nextSibling(); while (node && this.filter(node) === nodeFilterConstants.FILTER_SKIP) { node = this.walker.nextSibling(); } return node; } }; function isInXULDocument(el) { let doc = nodeDocument(el); return doc && doc.documentElement && doc.documentElement.namespaceURI === XUL_NS; } /** * This DeepTreeWalker filter skips whitespace text nodes and anonymous * content with the exception of ::before and ::after and anonymous content * in XUL document (needed to show all elements in the browser toolbox). */ function standardTreeWalkerFilter(node) { // ::before and ::after are native anonymous content, but we always // want to show them if (node.nodeName === "_moz_generated_content_before" || node.nodeName === "_moz_generated_content_after") { return nodeFilterConstants.FILTER_ACCEPT; } // Ignore empty whitespace text nodes that do not impact the layout. if (isWhitespaceTextNode(node)) { return nodeHasSize(node) ? nodeFilterConstants.FILTER_ACCEPT : nodeFilterConstants.FILTER_SKIP; } // Ignore all native and XBL anonymous content inside a non-XUL document if (!isInXULDocument(node) && (isXBLAnonymous(node) || isNativeAnonymous(node))) { // Note: this will skip inspecting the contents of feedSubscribeLine since // that's XUL content injected in an HTML document, but we need to because // this also skips many other elements that need to be skipped - like form // controls, scrollbars, video controls, etc (see bug 1187482). return nodeFilterConstants.FILTER_SKIP; } return nodeFilterConstants.FILTER_ACCEPT; } /** * This DeepTreeWalker filter is like standardTreeWalkerFilter except that * it also includes all anonymous content (like internal form controls). */ function allAnonymousContentTreeWalkerFilter(node) { // Ignore empty whitespace text nodes that do not impact the layout. if (isWhitespaceTextNode(node)) { return nodeHasSize(node) ? nodeFilterConstants.FILTER_ACCEPT : nodeFilterConstants.FILTER_SKIP; } return nodeFilterConstants.FILTER_ACCEPT; } /** * Is the given node a text node composed of whitespace only? * @param {DOMNode} node * @return {Boolean} */ function isWhitespaceTextNode(node) { return node.nodeType == Ci.nsIDOMNode.TEXT_NODE && !/[^\s]/.exec(node.nodeValue); } /** * Does the given node have non-0 width and height? * @param {DOMNode} node * @return {Boolean} */ function nodeHasSize(node) { if (!node.getBoxQuads) { return false; } let quads = node.getBoxQuads(); return quads.length && quads.some(quad => quad.bounds.width && quad.bounds.height); } /** * Returns a promise that is settled once the given HTMLImageElement has * finished loading. * * @param {HTMLImageElement} image - The image element. * @param {Number} timeout - Maximum amount of time the image is allowed to load * before the waiting is aborted. Ignored if flags.testing is set. * * @return {Promise} that is fulfilled once the image has loaded. If the image * fails to load or the load takes too long, the promise is rejected. */ function ensureImageLoaded(image, timeout) { let { HTMLImageElement } = image.ownerDocument.defaultView; if (!(image instanceof HTMLImageElement)) { return promise.reject("image must be an HTMLImageELement"); } if (image.complete) { // The image has already finished loading. return promise.resolve(); } // This image is still loading. let onLoad = AsyncUtils.listenOnce(image, "load"); // Reject if loading fails. let onError = AsyncUtils.listenOnce(image, "error").then(() => { return promise.reject("Image '" + image.src + "' failed to load."); }); // Don't timeout when testing. This is never settled. let onAbort = new Promise(() => {}); if (!flags.testing) { // Tests are not running. Reject the promise after given timeout. onAbort = DevToolsUtils.waitForTime(timeout).then(() => { return promise.reject("Image '" + image.src + "' took too long to load."); }); } // See which happens first. return promise.race([onLoad, onError, onAbort]); } /** * Given an <img> or <canvas> element, return the image data-uri. If @param node * is an <img> element, the method waits a while for the image to load before * the data is generated. If the image does not finish loading in a reasonable * time (IMAGE_FETCHING_TIMEOUT milliseconds) the process aborts. * * @param {HTMLImageElement|HTMLCanvasElement} node - The <img> or <canvas> * element, or Image() object. Other types cause the method to reject. * @param {Number} maxDim - Optionally pass a maximum size you want the longest * side of the image to be resized to before getting the image data. * @return {Promise} A promise that is fulfilled with an object containing the * data-uri and size-related information: * { data: "...", * size: { * naturalWidth: 400, * naturalHeight: 300, * resized: true } * }. * * If something goes wrong, the promise is rejected. */ var imageToImageData = Task.async(function* (node, maxDim) { let { HTMLCanvasElement, HTMLImageElement } = node.ownerDocument.defaultView; let isImg = node instanceof HTMLImageElement; let isCanvas = node instanceof HTMLCanvasElement; if (!isImg && !isCanvas) { throw new Error("node is not a <canvas> or <img> element."); } if (isImg) { // Ensure that the image is ready. yield ensureImageLoaded(node, IMAGE_FETCHING_TIMEOUT); } // Get the image resize ratio if a maxDim was provided let resizeRatio = 1; let imgWidth = node.naturalWidth || node.width; let imgHeight = node.naturalHeight || node.height; let imgMax = Math.max(imgWidth, imgHeight); if (maxDim && imgMax > maxDim) { resizeRatio = maxDim / imgMax; } // Extract the image data let imageData; // The image may already be a data-uri, in which case, save ourselves the // trouble of converting via the canvas.drawImage.toDataURL method, but only // if the image doesn't need resizing if (isImg && node.src.startsWith("data:") && resizeRatio === 1) { imageData = node.src; } else { // Create a canvas to copy the rawNode into and get the imageData from let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas"); canvas.width = imgWidth * resizeRatio; canvas.height = imgHeight * resizeRatio; let ctx = canvas.getContext("2d"); // Copy the rawNode image or canvas in the new canvas and extract data ctx.drawImage(node, 0, 0, canvas.width, canvas.height); imageData = canvas.toDataURL("image/png"); } return { data: imageData, size: { naturalWidth: imgWidth, naturalHeight: imgHeight, resized: resizeRatio !== 1 } }; }); loader.lazyGetter(this, "DOMUtils", function () { return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); });