toolkit/devtools/server/actors/inspector.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 /**
     8  * Here's the server side of the remote inspector.
     9  *
    10  * The WalkerActor is the client's view of the debuggee's DOM.  It's gives
    11  * the client a tree of NodeActor objects.
    12  *
    13  * The walker presents the DOM tree mostly unmodified from the source DOM
    14  * tree, but with a few key differences:
    15  *
    16  *  - Empty text nodes are ignored.  This is pretty typical of developer
    17  *    tools, but maybe we should reconsider that on the server side.
    18  *  - iframes with documents loaded have the loaded document as the child,
    19  *    the walker provides one big tree for the whole document tree.
    20  *
    21  * There are a few ways to get references to NodeActors:
    22  *
    23  *   - When you first get a WalkerActor reference, it comes with a free
    24  *     reference to the root document's node.
    25  *   - Given a node, you can ask for children, siblings, and parents.
    26  *   - You can issue querySelector and querySelectorAll requests to find
    27  *     other elements.
    28  *   - Requests that return arbitrary nodes from the tree (like querySelector
    29  *     and querySelectorAll) will also return any nodes the client hasn't
    30  *     seen in order to have a complete set of parents.
    31  *
    32  * Once you have a NodeFront, you should be able to answer a few questions
    33  * without further round trips, like the node's name, namespace/tagName,
    34  * attributes, etc.  Other questions (like a text node's full nodeValue)
    35  * might require another round trip.
    36  *
    37  * The protocol guarantees that the client will always know the parent of
    38  * any node that is returned by the server.  This means that some requests
    39  * (like querySelector) will include the extra nodes needed to satisfy this
    40  * requirement.  The client keeps track of this parent relationship, so the
    41  * node fronts form a tree that is a subset of the actual DOM tree.
    42  *
    43  *
    44  * We maintain this guarantee to support the ability to release subtrees on
    45  * the client - when a node is disconnected from the DOM tree we want to be
    46  * able to free the client objects for all the children nodes.
    47  *
    48  * So to be able to answer "all the children of a given node that we have
    49  * seen on the client side", we guarantee that every time we've seen a node,
    50  * we connect it up through its parents.
    51  */
    53 const {Cc, Ci, Cu, Cr} = require("chrome");
    54 const Services = require("Services");
    55 const protocol = require("devtools/server/protocol");
    56 const {Arg, Option, method, RetVal, types} = protocol;
    57 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
    58 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
    59 const object = require("sdk/util/object");
    60 const events = require("sdk/event/core");
    61 const {Unknown} = require("sdk/platform/xpcom");
    62 const {Class} = require("sdk/core/heritage");
    63 const {PageStyleActor} = require("devtools/server/actors/styles");
    64 const {HighlighterActor} = require("devtools/server/actors/highlighter");
    66 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
    67 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
    68 const XHTML_NS = "http://www.w3.org/1999/xhtml";
    69 const IMAGE_FETCHING_TIMEOUT = 500;
    70 // The possible completions to a ':' with added score to give certain values
    71 // some preference.
    72 const PSEUDO_SELECTORS = [
    73   [":active", 1],
    74   [":hover", 1],
    75   [":focus", 1],
    76   [":visited", 0],
    77   [":link", 0],
    78   [":first-letter", 0],
    79   [":first-child", 2],
    80   [":before", 2],
    81   [":after", 2],
    82   [":lang(", 0],
    83   [":not(", 3],
    84   [":first-of-type", 0],
    85   [":last-of-type", 0],
    86   [":only-of-type", 0],
    87   [":only-child", 2],
    88   [":nth-child(", 3],
    89   [":nth-last-child(", 0],
    90   [":nth-of-type(", 0],
    91   [":nth-last-of-type(", 0],
    92   [":last-child", 2],
    93   [":root", 0],
    94   [":empty", 0],
    95   [":target", 0],
    96   [":enabled", 0],
    97   [":disabled", 0],
    98   [":checked", 1],
    99   ["::selection", 0]
   100 ];
   103 let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
   104 HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
   106 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
   108 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
   110 loader.lazyGetter(this, "DOMParser", function() {
   111   return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
   112 });
   114 exports.register = function(handle) {
   115   handle.addGlobalActor(InspectorActor, "inspectorActor");
   116   handle.addTabActor(InspectorActor, "inspectorActor");
   117 };
   119 exports.unregister = function(handle) {
   120   handle.removeGlobalActor(InspectorActor);
   121   handle.removeTabActor(InspectorActor);
   122 };
   124 // XXX: A poor man's makeInfallible until we move it out of transport.js
   125 // Which should be very soon.
   126 function makeInfallible(handler) {
   127   return function(...args) {
   128     try {
   129       return handler.apply(this, args);
   130     } catch(ex) {
   131       console.error(ex);
   132     }
   133     return undefined;
   134   }
   135 }
   137 // A resolve that hits the main loop first.
   138 function delayedResolve(value) {
   139   let deferred = promise.defer();
   140   Services.tm.mainThread.dispatch(makeInfallible(function delayedResolveHandler() {
   141     deferred.resolve(value);
   142   }), 0);
   143   return deferred.promise;
   144 }
   146 types.addDictType("imageData", {
   147   // The image data
   148   data: "nullable:longstring",
   149   // The original image dimensions
   150   size: "json"
   151 });
   153 /**
   154  * We only send nodeValue up to a certain size by default.  This stuff
   155  * controls that size.
   156  */
   157 exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
   158 var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
   160 exports.getValueSummaryLength = function() {
   161   return gValueSummaryLength;
   162 };
   164 exports.setValueSummaryLength = function(val) {
   165   gValueSummaryLength = val;
   166 };
   168 /**
   169  * Server side of the node actor.
   170  */
   171 var NodeActor = exports.NodeActor = protocol.ActorClass({
   172   typeName: "domnode",
   174   initialize: function(walker, node) {
   175     protocol.Actor.prototype.initialize.call(this, null);
   176     this.walker = walker;
   177     this.rawNode = node;
   178   },
   180   toString: function() {
   181     return "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]";
   182   },
   184   /**
   185    * Instead of storing a connection object, the NodeActor gets its connection
   186    * from its associated walker.
   187    */
   188   get conn() this.walker.conn,
   190   isDocumentElement: function() {
   191     return this.rawNode.ownerDocument &&
   192         this.rawNode.ownerDocument.documentElement === this.rawNode;
   193   },
   195   // Returns the JSON representation of this object over the wire.
   196   form: function(detail) {
   197     if (detail === "actorid") {
   198       return this.actorID;
   199     }
   201     let parentNode = this.walker.parentNode(this);
   203     // Estimate the number of children.
   204     let numChildren = this.rawNode.childNodes.length;
   205     if (numChildren === 0 &&
   206         (this.rawNode.contentDocument || this.rawNode.getSVGDocument)) {
   207       // This might be an iframe with virtual children.
   208       numChildren = 1;
   209     }
   211     let form = {
   212       actor: this.actorID,
   213       baseURI: this.rawNode.baseURI,
   214       parent: parentNode ? parentNode.actorID : undefined,
   215       nodeType: this.rawNode.nodeType,
   216       namespaceURI: this.rawNode.namespaceURI,
   217       nodeName: this.rawNode.nodeName,
   218       numChildren: numChildren,
   220       // doctype attributes
   221       name: this.rawNode.name,
   222       publicId: this.rawNode.publicId,
   223       systemId: this.rawNode.systemId,
   225       attrs: this.writeAttrs(),
   227       pseudoClassLocks: this.writePseudoClassLocks(),
   228     };
   230     if (this.isDocumentElement()) {
   231       form.isDocumentElement = true;
   232     }
   234     if (this.rawNode.nodeValue) {
   235       // We only include a short version of the value if it's longer than
   236       // gValueSummaryLength
   237       if (this.rawNode.nodeValue.length > gValueSummaryLength) {
   238         form.shortValue = this.rawNode.nodeValue.substring(0, gValueSummaryLength);
   239         form.incompleteValue = true;
   240       } else {
   241         form.shortValue = this.rawNode.nodeValue;
   242       }
   243     }
   245     return form;
   246   },
   248   writeAttrs: function() {
   249     if (!this.rawNode.attributes) {
   250       return undefined;
   251     }
   252     return [{namespace: attr.namespace, name: attr.name, value: attr.value }
   253             for (attr of this.rawNode.attributes)];
   254   },
   256   writePseudoClassLocks: function() {
   257     if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
   258       return undefined;
   259     }
   260     let ret = undefined;
   261     for (let pseudo of PSEUDO_CLASSES) {
   262       if (DOMUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
   263         ret = ret || [];
   264         ret.push(pseudo);
   265       }
   266     }
   267     return ret;
   268   },
   270   /**
   271    * Returns a LongStringActor with the node's value.
   272    */
   273   getNodeValue: method(function() {
   274     return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
   275   }, {
   276     request: {},
   277     response: {
   278       value: RetVal("longstring")
   279     }
   280   }),
   282   /**
   283    * Set the node's value to a given string.
   284    */
   285   setNodeValue: method(function(value) {
   286     this.rawNode.nodeValue = value;
   287   }, {
   288     request: { value: Arg(0) },
   289     response: {}
   290   }),
   292   /**
   293    * Get the node's image data if any (for canvas and img nodes).
   294    * Returns an imageData object with the actual data being a LongStringActor
   295    * and a size json object.
   296    * The image data is transmitted as a base64 encoded png data-uri.
   297    * The method rejects if the node isn't an image or if the image is missing
   298    *
   299    * Accepts a maxDim request parameter to resize images that are larger. This
   300    * is important as the resizing occurs server-side so that image-data being
   301    * transfered in the longstring back to the client will be that much smaller
   302    */
   303   getImageData: method(function(maxDim) {
   304     // imageToImageData may fail if the node isn't an image
   305     try {
   306       let imageData = imageToImageData(this.rawNode, maxDim);
   307       return promise.resolve({
   308         data: LongStringActor(this.conn, imageData.data),
   309         size: imageData.size
   310       });
   311     } catch(e) {
   312       return promise.reject(new Error("Image not available"));
   313     }
   314   }, {
   315     request: {maxDim: Arg(0, "nullable:number")},
   316     response: RetVal("imageData")
   317   }),
   319   /**
   320    * Modify a node's attributes.  Passed an array of modifications
   321    * similar in format to "attributes" mutations.
   322    * {
   323    *   attributeName: <string>
   324    *   attributeNamespace: <optional string>
   325    *   newValue: <optional string> - If null or undefined, the attribute
   326    *     will be removed.
   327    * }
   328    *
   329    * Returns when the modifications have been made.  Mutations will
   330    * be queued for any changes made.
   331    */
   332   modifyAttributes: method(function(modifications) {
   333     let rawNode = this.rawNode;
   334     for (let change of modifications) {
   335       if (change.newValue == null) {
   336         if (change.attributeNamespace) {
   337           rawNode.removeAttributeNS(change.attributeNamespace, change.attributeName);
   338         } else {
   339           rawNode.removeAttribute(change.attributeName);
   340         }
   341       } else {
   342         if (change.attributeNamespace) {
   343           rawNode.setAttributeNS(change.attributeNamespace, change.attributeName, change.newValue);
   344         } else {
   345           rawNode.setAttribute(change.attributeName, change.newValue);
   346         }
   347       }
   348     }
   349   }, {
   350     request: {
   351       modifications: Arg(0, "array:json")
   352     },
   353     response: {}
   354   })
   355 });
   357 /**
   358  * Client side of the node actor.
   359  *
   360  * Node fronts are strored in a tree that mirrors the DOM tree on the
   361  * server, but with a few key differences:
   362  *  - Not all children will be necessary loaded for each node.
   363  *  - The order of children isn't guaranteed to be the same as the DOM.
   364  * Children are stored in a doubly-linked list, to make addition/removal
   365  * and traversal quick.
   366  *
   367  * Due to the order/incompleteness of the child list, it is safe to use
   368  * the parent node from clients, but the `children` request should be used
   369  * to traverse children.
   370  */
   371 let NodeFront = protocol.FrontClass(NodeActor, {
   372   initialize: function(conn, form, detail, ctx) {
   373     this._parent = null; // The parent node
   374     this._child = null;  // The first child of this node.
   375     this._next = null;   // The next sibling of this node.
   376     this._prev = null;   // The previous sibling of this node.
   377     protocol.Front.prototype.initialize.call(this, conn, form, detail, ctx);
   378   },
   380   /**
   381    * Destroy a node front.  The node must have been removed from the
   382    * ownership tree before this is called, unless the whole walker front
   383    * is being destroyed.
   384    */
   385   destroy: function() {
   386     // If an observer was added on this node, shut it down.
   387     if (this.observer) {
   388       this.observer.disconnect();
   389       this.observer = null;
   390     }
   392     protocol.Front.prototype.destroy.call(this);
   393   },
   395   // Update the object given a form representation off the wire.
   396   form: function(form, detail, ctx) {
   397     if (detail === "actorid") {
   398       this.actorID = form;
   399       return;
   400     }
   401     // Shallow copy of the form.  We could just store a reference, but
   402     // eventually we'll want to update some of the data.
   403     this._form = object.merge(form);
   404     this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
   406     if (form.parent) {
   407       // Get the owner actor for this actor (the walker), and find the
   408       // parent node of this actor from it, creating a standin node if
   409       // necessary.
   410       let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent);
   411       this.reparent(parentNodeFront);
   412     }
   413   },
   415   /**
   416    * Returns the parent NodeFront for this NodeFront.
   417    */
   418   parentNode: function() {
   419     return this._parent;
   420   },
   422   /**
   423    * Process a mutation entry as returned from the walker's `getMutations`
   424    * request.  Only tries to handle changes of the node's contents
   425    * themselves (character data and attribute changes), the walker itself
   426    * will keep the ownership tree up to date.
   427    */
   428   updateMutation: function(change) {
   429     if (change.type === "attributes") {
   430       // We'll need to lazily reparse the attributes after this change.
   431       this._attrMap = undefined;
   433       // Update any already-existing attributes.
   434       let found = false;
   435       for (let i = 0; i < this.attributes.length; i++) {
   436         let attr = this.attributes[i];
   437         if (attr.name == change.attributeName &&
   438             attr.namespace == change.attributeNamespace) {
   439           if (change.newValue !== null) {
   440             attr.value = change.newValue;
   441           } else {
   442             this.attributes.splice(i, 1);
   443           }
   444           found = true;
   445           break;
   446         }
   447       }
   448       // This is a new attribute.
   449       if (!found)  {
   450         this.attributes.push({
   451           name: change.attributeName,
   452           namespace: change.attributeNamespace,
   453           value: change.newValue
   454         });
   455       }
   456     } else if (change.type === "characterData") {
   457       this._form.shortValue = change.newValue;
   458       this._form.incompleteValue = change.incompleteValue;
   459     } else if (change.type === "pseudoClassLock") {
   460       this._form.pseudoClassLocks = change.pseudoClassLocks;
   461     }
   462   },
   464   // Some accessors to make NodeFront feel more like an nsIDOMNode
   466   get id() this.getAttribute("id"),
   468   get nodeType() this._form.nodeType,
   469   get namespaceURI() this._form.namespaceURI,
   470   get nodeName() this._form.nodeName,
   472   get baseURI() this._form.baseURI,
   474   get className() {
   475     return this.getAttribute("class") || '';
   476   },
   478   get hasChildren() this._form.numChildren > 0,
   479   get numChildren() this._form.numChildren,
   481   get tagName() this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null,
   482   get shortValue() this._form.shortValue,
   483   get incompleteValue() !!this._form.incompleteValue,
   485   get isDocumentElement() !!this._form.isDocumentElement,
   487   // doctype properties
   488   get name() this._form.name,
   489   get publicId() this._form.publicId,
   490   get systemId() this._form.systemId,
   492   getAttribute: function(name) {
   493     let attr = this._getAttribute(name);
   494     return attr ? attr.value : null;
   495   },
   496   hasAttribute: function(name) {
   497     this._cacheAttributes();
   498     return (name in this._attrMap);
   499   },
   501   get hidden() {
   502     let cls = this.getAttribute("class");
   503     return cls && cls.indexOf(HIDDEN_CLASS) > -1;
   504   },
   506   get attributes() this._form.attrs,
   508   get pseudoClassLocks() this._form.pseudoClassLocks || [],
   509   hasPseudoClassLock: function(pseudo) {
   510     return this.pseudoClassLocks.some(locked => locked === pseudo);
   511   },
   513   getNodeValue: protocol.custom(function() {
   514     if (!this.incompleteValue) {
   515       return delayedResolve(new ShortLongString(this.shortValue));
   516     } else {
   517       return this._getNodeValue();
   518     }
   519   }, {
   520     impl: "_getNodeValue"
   521   }),
   523   /**
   524    * Return a new AttributeModificationList for this node.
   525    */
   526   startModifyingAttributes: function() {
   527     return AttributeModificationList(this);
   528   },
   530   _cacheAttributes: function() {
   531     if (typeof(this._attrMap) != "undefined") {
   532       return;
   533     }
   534     this._attrMap = {};
   535     for (let attr of this.attributes) {
   536       this._attrMap[attr.name] = attr;
   537     }
   538   },
   540   _getAttribute: function(name) {
   541     this._cacheAttributes();
   542     return this._attrMap[name] || undefined;
   543   },
   545   /**
   546    * Set this node's parent.  Note that the children saved in
   547    * this tree are unordered and incomplete, so shouldn't be used
   548    * instead of a `children` request.
   549    */
   550   reparent: function(parent) {
   551     if (this._parent === parent) {
   552       return;
   553     }
   555     if (this._parent && this._parent._child === this) {
   556       this._parent._child = this._next;
   557     }
   558     if (this._prev) {
   559       this._prev._next = this._next;
   560     }
   561     if (this._next) {
   562       this._next._prev = this._prev;
   563     }
   564     this._next = null;
   565     this._prev = null;
   566     this._parent = parent;
   567     if (!parent) {
   568       // Subtree is disconnected, we're done
   569       return;
   570     }
   571     this._next = parent._child;
   572     if (this._next) {
   573       this._next._prev = this;
   574     }
   575     parent._child = this;
   576   },
   578   /**
   579    * Return all the known children of this node.
   580    */
   581   treeChildren: function() {
   582     let ret = [];
   583     for (let child = this._child; child != null; child = child._next) {
   584       ret.push(child);
   585     }
   586     return ret;
   587   },
   589   /**
   590    * Do we use a local target?
   591    * Useful to know if a rawNode is available or not.
   592    *
   593    * This will, one day, be removed. External code should
   594    * not need to know if the target is remote or not.
   595    */
   596   isLocal_toBeDeprecated: function() {
   597     return !!this.conn._transport._serverConnection;
   598   },
   600   /**
   601    * Get an nsIDOMNode for the given node front.  This only works locally,
   602    * and is only intended as a stopgap during the transition to the remote
   603    * protocol.  If you depend on this you're likely to break soon.
   604    */
   605   rawNode: function(rawNode) {
   606     if (!this.conn._transport._serverConnection) {
   607       console.warn("Tried to use rawNode on a remote connection.");
   608       return null;
   609     }
   610     let actor = this.conn._transport._serverConnection.getActor(this.actorID);
   611     if (!actor) {
   612       // Can happen if we try to get the raw node for an already-expired
   613       // actor.
   614       return null;
   615     }
   616     return actor.rawNode;
   617   }
   618 });
   620 /**
   621  * Returned from any call that might return a node that isn't connected to root by
   622  * nodes the child has seen, such as querySelector.
   623  */
   624 types.addDictType("disconnectedNode", {
   625   // The actual node to return
   626   node: "domnode",
   628   // Nodes that are needed to connect the node to a node the client has already seen
   629   newParents: "array:domnode"
   630 });
   632 types.addDictType("disconnectedNodeArray", {
   633   // The actual node list to return
   634   nodes: "array:domnode",
   636   // Nodes that are needed to connect those nodes to the root.
   637   newParents: "array:domnode"
   638 });
   640 types.addDictType("dommutation", {});
   642 /**
   643  * Server side of a node list as returned by querySelectorAll()
   644  */
   645 var NodeListActor = exports.NodeListActor = protocol.ActorClass({
   646   typeName: "domnodelist",
   648   initialize: function(walker, nodeList) {
   649     protocol.Actor.prototype.initialize.call(this);
   650     this.walker = walker;
   651     this.nodeList = nodeList;
   652   },
   654   destroy: function() {
   655     protocol.Actor.prototype.destroy.call(this);
   656   },
   658   /**
   659    * Instead of storing a connection object, the NodeActor gets its connection
   660    * from its associated walker.
   661    */
   662   get conn() {
   663     return this.walker.conn;
   664   },
   666   /**
   667    * Items returned by this actor should belong to the parent walker.
   668    */
   669   marshallPool: function() {
   670     return this.walker;
   671   },
   673   // Returns the JSON representation of this object over the wire.
   674   form: function() {
   675     return {
   676       actor: this.actorID,
   677       length: this.nodeList.length
   678     }
   679   },
   681   /**
   682    * Get a single node from the node list.
   683    */
   684   item: method(function(index) {
   685     return this.walker.attachElement(this.nodeList[index]);
   686   }, {
   687     request: { item: Arg(0) },
   688     response: RetVal("disconnectedNode")
   689   }),
   691   /**
   692    * Get a range of the items from the node list.
   693    */
   694   items: method(function(start=0, end=this.nodeList.length) {
   695     let items = [this.walker._ref(item) for (item of Array.prototype.slice.call(this.nodeList, start, end))];
   696     let newParents = new Set();
   697     for (let item of items) {
   698       this.walker.ensurePathToRoot(item, newParents);
   699     }
   700     return {
   701       nodes: items,
   702       newParents: [node for (node of newParents)]
   703     }
   704   }, {
   705     request: {
   706       start: Arg(0, "nullable:number"),
   707       end: Arg(1, "nullable:number")
   708     },
   709     response: RetVal("disconnectedNodeArray")
   710   }),
   712   release: method(function() {}, { release: true })
   713 });
   715 /**
   716  * Client side of a node list as returned by querySelectorAll()
   717  */
   718 var NodeListFront = exports.NodeListFront = protocol.FrontClass(NodeListActor, {
   719   initialize: function(client, form) {
   720     protocol.Front.prototype.initialize.call(this, client, form);
   721   },
   723   destroy: function() {
   724     protocol.Front.prototype.destroy.call(this);
   725   },
   727   marshallPool: function() {
   728     return this.parent();
   729   },
   731   // Update the object given a form representation off the wire.
   732   form: function(json) {
   733     this.length = json.length;
   734   },
   736   item: protocol.custom(function(index) {
   737     return this._item(index).then(response => {
   738       return response.node;
   739     });
   740   }, {
   741     impl: "_item"
   742   }),
   744   items: protocol.custom(function(start, end) {
   745     return this._items(start, end).then(response => {
   746       return response.nodes;
   747     });
   748   }, {
   749     impl: "_items"
   750   })
   751 });
   753 // Some common request/response templates for the dom walker
   755 let nodeArrayMethod = {
   756   request: {
   757     node: Arg(0, "domnode"),
   758     maxNodes: Option(1),
   759     center: Option(1, "domnode"),
   760     start: Option(1, "domnode"),
   761     whatToShow: Option(1)
   762   },
   763   response: RetVal(types.addDictType("domtraversalarray", {
   764     nodes: "array:domnode"
   765   }))
   766 };
   768 let traversalMethod = {
   769   request: {
   770     node: Arg(0, "domnode"),
   771     whatToShow: Option(1)
   772   },
   773   response: {
   774     node: RetVal("nullable:domnode")
   775   }
   776 }
   778 /**
   779  * Server side of the DOM walker.
   780  */
   781 var WalkerActor = protocol.ActorClass({
   782   typeName: "domwalker",
   784   events: {
   785     "new-mutations" : {
   786       type: "newMutations"
   787     },
   788     "picker-node-picked" : {
   789       type: "pickerNodePicked",
   790       node: Arg(0, "disconnectedNode")
   791     },
   792     "picker-node-hovered" : {
   793       type: "pickerNodeHovered",
   794       node: Arg(0, "disconnectedNode")
   795     },
   796     "highlighter-ready" : {
   797       type: "highlighter-ready"
   798     },
   799     "highlighter-hide" : {
   800       type: "highlighter-hide"
   801     }
   802   },
   804   /**
   805    * Create the WalkerActor
   806    * @param DebuggerServerConnection conn
   807    *    The server connection.
   808    */
   809   initialize: function(conn, tabActor, options) {
   810     protocol.Actor.prototype.initialize.call(this, conn);
   811     this.tabActor = tabActor;
   812     this.rootWin = tabActor.window;
   813     this.rootDoc = this.rootWin.document;
   814     this._refMap = new Map();
   815     this._pendingMutations = [];
   816     this._activePseudoClassLocks = new Set();
   818     this.layoutHelpers = new LayoutHelpers(this.rootWin);
   820     // Nodes which have been removed from the client's known
   821     // ownership tree are considered "orphaned", and stored in
   822     // this set.
   823     this._orphaned = new Set();
   825     // The client can tell the walker that it is interested in a node
   826     // even when it is orphaned with the `retainNode` method.  This
   827     // list contains orphaned nodes that were so retained.
   828     this._retainedOrphans = new Set();
   830     this.onMutations = this.onMutations.bind(this);
   831     this.onFrameLoad = this.onFrameLoad.bind(this);
   832     this.onFrameUnload = this.onFrameUnload.bind(this);
   834     events.on(tabActor, "will-navigate", this.onFrameUnload);
   835     events.on(tabActor, "navigate", this.onFrameLoad);
   837     // Ensure that the root document node actor is ready and
   838     // managed.
   839     this.rootNode = this.document();
   840   },
   842   // Returns the JSON representation of this object over the wire.
   843   form: function() {
   844     return {
   845       actor: this.actorID,
   846       root: this.rootNode.form()
   847     }
   848   },
   850   toString: function() {
   851     return "[WalkerActor " + this.actorID + "]";
   852   },
   854   destroy: function() {
   855     this._hoveredNode = null;
   856     this.clearPseudoClassLocks();
   857     this._activePseudoClassLocks = null;
   858     this.rootDoc = null;
   859     events.emit(this, "destroyed");
   860     protocol.Actor.prototype.destroy.call(this);
   861   },
   863   release: method(function() {}, { release: true }),
   865   unmanage: function(actor) {
   866     if (actor instanceof NodeActor) {
   867       if (this._activePseudoClassLocks &&
   868           this._activePseudoClassLocks.has(actor)) {
   869         this.clearPsuedoClassLocks(actor);
   870       }
   871       this._refMap.delete(actor.rawNode);
   872     }
   873     protocol.Actor.prototype.unmanage.call(this, actor);
   874   },
   876   _ref: function(node) {
   877     let actor = this._refMap.get(node);
   878     if (actor) return actor;
   880     actor = new NodeActor(this, node);
   882     // Add the node actor as a child of this walker actor, assigning
   883     // it an actorID.
   884     this.manage(actor);
   885     this._refMap.set(node, actor);
   887     if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
   888       this._watchDocument(actor);
   889     }
   890     return actor;
   891   },
   893   /**
   894    * This is kept for backward-compatibility reasons with older remote targets.
   895    * Targets prior to bug 916443.
   896    *
   897    * pick/cancelPick are used to pick a node on click on the content
   898    * document. But in their implementation prior to bug 916443, they don't allow
   899    * highlighting on hover.
   900    * The client-side now uses the highlighter actor's pick and cancelPick
   901    * methods instead. The client-side uses the the highlightable trait found in
   902    * the root actor to determine which version of pick to use.
   903    *
   904    * As for highlight, the new highlighter actor is used instead of the walker's
   905    * highlight method. Same here though, the client-side uses the highlightable
   906    * trait to dertermine which to use.
   907    *
   908    * Keeping these actor methods for now allows newer client-side debuggers to
   909    * inspect fxos 1.2 remote targets or older firefox desktop remote targets.
   910    */
   911   pick: method(function() {}, {request: {}, response: RetVal("disconnectedNode")}),
   912   cancelPick: method(function() {}),
   913   highlight: method(function(node) {}, {request: {node: Arg(0, "nullable:domnode")}}),
   915   attachElement: function(node) {
   916     node = this._ref(node);
   917     let newParents = this.ensurePathToRoot(node);
   918     return {
   919       node: node,
   920       newParents: [parent for (parent of newParents)]
   921     };
   922   },
   924   /**
   925    * Watch the given document node for mutations using the DOM observer
   926    * API.
   927    */
   928   _watchDocument: function(actor) {
   929     let node = actor.rawNode;
   930     // Create the observer on the node's actor.  The node will make sure
   931     // the observer is cleaned up when the actor is released.
   932     actor.observer = new actor.rawNode.defaultView.MutationObserver(this.onMutations);
   933     actor.observer.observe(node, {
   934       attributes: true,
   935       characterData: true,
   936       childList: true,
   937       subtree: true
   938     });
   939   },
   941   /**
   942    * Return the document node that contains the given node,
   943    * or the root node if no node is specified.
   944    * @param NodeActor node
   945    *        The node whose document is needed, or null to
   946    *        return the root.
   947    */
   948   document: method(function(node) {
   949     let doc = node ? nodeDocument(node.rawNode) : this.rootDoc;
   950     return this._ref(doc);
   951   }, {
   952     request: { node: Arg(0, "nullable:domnode") },
   953     response: { node: RetVal("domnode") },
   954   }),
   956   /**
   957    * Return the documentElement for the document containing the
   958    * given node.
   959    * @param NodeActor node
   960    *        The node whose documentElement is requested, or null
   961    *        to use the root document.
   962    */
   963   documentElement: method(function(node) {
   964     let elt = node ? nodeDocument(node.rawNode).documentElement : this.rootDoc.documentElement;
   965     return this._ref(elt);
   966   }, {
   967     request: { node: Arg(0, "nullable:domnode") },
   968     response: { node: RetVal("domnode") },
   969   }),
   971   /**
   972    * Return all parents of the given node, ordered from immediate parent
   973    * to root.
   974    * @param NodeActor node
   975    *    The node whose parents are requested.
   976    * @param object options
   977    *    Named options, including:
   978    *    `sameDocument`: If true, parents will be restricted to the same
   979    *      document as the node.
   980    */
   981   parents: method(function(node, options={}) {
   982     let walker = documentWalker(node.rawNode, this.rootWin);
   983     let parents = [];
   984     let cur;
   985     while((cur = walker.parentNode())) {
   986       if (options.sameDocument && cur.ownerDocument != node.rawNode.ownerDocument) {
   987         break;
   988       }
   989       parents.push(this._ref(cur));
   990     }
   991     return parents;
   992   }, {
   993     request: {
   994       node: Arg(0, "domnode"),
   995       sameDocument: Option(1)
   996     },
   997     response: {
   998       nodes: RetVal("array:domnode")
   999     },
  1000   }),
  1002   parentNode: function(node) {
  1003     let walker = documentWalker(node.rawNode, this.rootWin);
  1004     let parent = walker.parentNode();
  1005     if (parent) {
  1006       return this._ref(parent);
  1008     return null;
  1009   },
  1011   /**
  1012    * Mark a node as 'retained'.
  1014    * A retained node is not released when `releaseNode` is called on its
  1015    * parent, or when a parent is released with the `cleanup` option to
  1016    * `getMutations`.
  1018    * When a retained node's parent is released, a retained mode is added to
  1019    * the walker's "retained orphans" list.
  1021    * Retained nodes can be deleted by providing the `force` option to
  1022    * `releaseNode`.  They will also be released when their document
  1023    * has been destroyed.
  1025    * Retaining a node makes no promise about its children;  They can
  1026    * still be removed by normal means.
  1027    */
  1028   retainNode: method(function(node) {
  1029     node.retained = true;
  1030   }, {
  1031     request: { node: Arg(0, "domnode") },
  1032     response: {}
  1033   }),
  1035   /**
  1036    * Remove the 'retained' mark from a node.  If the node was a
  1037    * retained orphan, release it.
  1038    */
  1039   unretainNode: method(function(node) {
  1040     node.retained = false;
  1041     if (this._retainedOrphans.has(node)) {
  1042       this._retainedOrphans.delete(node);
  1043       this.releaseNode(node);
  1045   }, {
  1046     request: { node: Arg(0, "domnode") },
  1047     response: {},
  1048   }),
  1050   /**
  1051    * Release actors for a node and all child nodes.
  1052    */
  1053   releaseNode: method(function(node, options={}) {
  1054     if (node.retained && !options.force) {
  1055       this._retainedOrphans.add(node);
  1056       return;
  1059     if (node.retained) {
  1060       // Forcing a retained node to go away.
  1061       this._retainedOrphans.delete(node);
  1064     let walker = documentWalker(node.rawNode, this.rootWin);
  1066     let child = walker.firstChild();
  1067     while (child) {
  1068       let childActor = this._refMap.get(child);
  1069       if (childActor) {
  1070         this.releaseNode(childActor, options);
  1072       child = walker.nextSibling();
  1075     node.destroy();
  1076   }, {
  1077     request: {
  1078       node: Arg(0, "domnode"),
  1079       force: Option(1)
  1081   }),
  1083   /**
  1084    * Add any nodes between `node` and the walker's root node that have not
  1085    * yet been seen by the client.
  1086    */
  1087   ensurePathToRoot: function(node, newParents=new Set()) {
  1088     if (!node) {
  1089       return newParents;
  1091     let walker = documentWalker(node.rawNode, this.rootWin);
  1092     let cur;
  1093     while ((cur = walker.parentNode())) {
  1094       let parent = this._refMap.get(cur);
  1095       if (!parent) {
  1096         // This parent didn't exist, so hasn't been seen by the client yet.
  1097         newParents.add(this._ref(cur));
  1098       } else {
  1099         // This parent did exist, so the client knows about it.
  1100         return newParents;
  1103     return newParents;
  1104   },
  1106   /**
  1107    * Return children of the given node.  By default this method will return
  1108    * all children of the node, but there are options that can restrict this
  1109    * to a more manageable subset.
  1111    * @param NodeActor node
  1112    *    The node whose children you're curious about.
  1113    * @param object options
  1114    *    Named options:
  1115    *    `maxNodes`: The set of nodes returned by the method will be no longer
  1116    *       than maxNodes.
  1117    *    `start`: If a node is specified, the list of nodes will start
  1118    *       with the given child.  Mutally exclusive with `center`.
  1119    *    `center`: If a node is specified, the given node will be as centered
  1120    *       as possible in the list, given how close to the ends of the child
  1121    *       list it is.  Mutually exclusive with `start`.
  1122    *    `whatToShow`: A bitmask of node types that should be included.  See
  1123    *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
  1125    * @returns an object with three items:
  1126    *    hasFirst: true if the first child of the node is included in the list.
  1127    *    hasLast: true if the last child of the node is included in the list.
  1128    *    nodes: Child nodes returned by the request.
  1129    */
  1130   children: method(function(node, options={}) {
  1131     if (options.center && options.start) {
  1132       throw Error("Can't specify both 'center' and 'start' options.");
  1134     let maxNodes = options.maxNodes || -1;
  1135     if (maxNodes == -1) {
  1136       maxNodes = Number.MAX_VALUE;
  1139     // We're going to create a few document walkers with the same filter,
  1140     // make it easier.
  1141     let filteredWalker = (node) => {
  1142       return documentWalker(node, this.rootWin, options.whatToShow);
  1143     };
  1145     // Need to know the first and last child.
  1146     let rawNode = node.rawNode;
  1147     let firstChild = filteredWalker(rawNode).firstChild();
  1148     let lastChild = filteredWalker(rawNode).lastChild();
  1150     if (!firstChild) {
  1151       // No children, we're done.
  1152       return { hasFirst: true, hasLast: true, nodes: [] };
  1155     let start;
  1156     if (options.center) {
  1157       start = options.center.rawNode;
  1158     } else if (options.start) {
  1159       start = options.start.rawNode;
  1160     } else {
  1161       start = firstChild;
  1164     let nodes = [];
  1166     // Start by reading backward from the starting point if we're centering...
  1167     let backwardWalker = filteredWalker(start);
  1168     if (start != firstChild && options.center) {
  1169       backwardWalker.previousSibling();
  1170       let backwardCount = Math.floor(maxNodes / 2);
  1171       let backwardNodes = this._readBackward(backwardWalker, backwardCount);
  1172       nodes = backwardNodes;
  1175     // Then read forward by any slack left in the max children...
  1176     let forwardWalker = filteredWalker(start);
  1177     let forwardCount = maxNodes - nodes.length;
  1178     nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
  1180     // If there's any room left, it means we've run all the way to the end.
  1181     // If we're centering, check if there are more items to read at the front.
  1182     let remaining = maxNodes - nodes.length;
  1183     if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
  1184       let firstNodes = this._readBackward(backwardWalker, remaining);
  1186       // Then put it all back together.
  1187       nodes = firstNodes.concat(nodes);
  1190     return {
  1191       hasFirst: nodes[0].rawNode == firstChild,
  1192       hasLast: nodes[nodes.length - 1].rawNode == lastChild,
  1193       nodes: nodes
  1194     };
  1195   }, nodeArrayMethod),
  1197   /**
  1198    * Return siblings of the given node.  By default this method will return
  1199    * all siblings of the node, but there are options that can restrict this
  1200    * to a more manageable subset.
  1202    * If `start` or `center` are not specified, this method will center on the
  1203    * node whose siblings are requested.
  1205    * @param NodeActor node
  1206    *    The node whose children you're curious about.
  1207    * @param object options
  1208    *    Named options:
  1209    *    `maxNodes`: The set of nodes returned by the method will be no longer
  1210    *       than maxNodes.
  1211    *    `start`: If a node is specified, the list of nodes will start
  1212    *       with the given child.  Mutally exclusive with `center`.
  1213    *    `center`: If a node is specified, the given node will be as centered
  1214    *       as possible in the list, given how close to the ends of the child
  1215    *       list it is.  Mutually exclusive with `start`.
  1216    *    `whatToShow`: A bitmask of node types that should be included.  See
  1217    *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
  1219    * @returns an object with three items:
  1220    *    hasFirst: true if the first child of the node is included in the list.
  1221    *    hasLast: true if the last child of the node is included in the list.
  1222    *    nodes: Child nodes returned by the request.
  1223    */
  1224   siblings: method(function(node, options={}) {
  1225     let parentNode = documentWalker(node.rawNode, this.rootWin).parentNode();
  1226     if (!parentNode) {
  1227       return {
  1228         hasFirst: true,
  1229         hasLast: true,
  1230         nodes: [node]
  1231       };
  1234     if (!(options.start || options.center)) {
  1235       options.center = node;
  1238     return this.children(this._ref(parentNode), options);
  1239   }, nodeArrayMethod),
  1241   /**
  1242    * Get the next sibling of a given node.  Getting nodes one at a time
  1243    * might be inefficient, be careful.
  1245    * @param object options
  1246    *    Named options:
  1247    *    `whatToShow`: A bitmask of node types that should be included.  See
  1248    *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
  1249    */
  1250   nextSibling: method(function(node, options={}) {
  1251     let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
  1252     let sibling = walker.nextSibling();
  1253     return sibling ? this._ref(sibling) : null;
  1254   }, traversalMethod),
  1256   /**
  1257    * Get the previous sibling of a given node.  Getting nodes one at a time
  1258    * might be inefficient, be careful.
  1260    * @param object options
  1261    *    Named options:
  1262    *    `whatToShow`: A bitmask of node types that should be included.  See
  1263    *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
  1264    */
  1265   previousSibling: method(function(node, options={}) {
  1266     let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
  1267     let sibling = walker.previousSibling();
  1268     return sibling ? this._ref(sibling) : null;
  1269   }, traversalMethod),
  1271   /**
  1272    * Helper function for the `children` method: Read forward in the sibling
  1273    * list into an array with `count` items, including the current node.
  1274    */
  1275   _readForward: function(walker, count) {
  1276     let ret = [];
  1277     let node = walker.currentNode;
  1278     do {
  1279       ret.push(this._ref(node));
  1280       node = walker.nextSibling();
  1281     } while (node && --count);
  1282     return ret;
  1283   },
  1285   /**
  1286    * Helper function for the `children` method: Read backward in the sibling
  1287    * list into an array with `count` items, including the current node.
  1288    */
  1289   _readBackward: function(walker, count) {
  1290     let ret = [];
  1291     let node = walker.currentNode;
  1292     do {
  1293       ret.push(this._ref(node));
  1294       node = walker.previousSibling();
  1295     } while(node && --count);
  1296     ret.reverse();
  1297     return ret;
  1298   },
  1300   /**
  1301    * Return the first node in the document that matches the given selector.
  1302    * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
  1304    * @param NodeActor baseNode
  1305    * @param string selector
  1306    */
  1307   querySelector: method(function(baseNode, selector) {
  1308     if (!baseNode) {
  1309       return {}
  1310     };
  1311     let node = baseNode.rawNode.querySelector(selector);
  1313     if (!node) {
  1314       return {}
  1315     };
  1317     return this.attachElement(node);
  1318   }, {
  1319     request: {
  1320       node: Arg(0, "domnode"),
  1321       selector: Arg(1)
  1322     },
  1323     response: RetVal("disconnectedNode")
  1324   }),
  1326   /**
  1327    * Return a NodeListActor with all nodes that match the given selector.
  1328    * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
  1330    * @param NodeActor baseNode
  1331    * @param string selector
  1332    */
  1333   querySelectorAll: method(function(baseNode, selector) {
  1334     let nodeList = null;
  1336     try {
  1337       nodeList = baseNode.rawNode.querySelectorAll(selector);
  1338     } catch(e) {
  1339       // Bad selector. Do nothing as the selector can come from a searchbox.
  1342     return new NodeListActor(this, nodeList);
  1343   }, {
  1344     request: {
  1345       node: Arg(0, "domnode"),
  1346       selector: Arg(1)
  1347     },
  1348     response: {
  1349       list: RetVal("domnodelist")
  1351   }),
  1353   /**
  1354    * Returns a list of matching results for CSS selector autocompletion.
  1356    * @param string query
  1357    *        The selector query being completed
  1358    * @param string completing
  1359    *        The exact token being completed out of the query
  1360    * @param string selectorState
  1361    *        One of "pseudo", "id", "tag", "class", "null"
  1362    */
  1363   getSuggestionsForQuery: method(function(query, completing, selectorState) {
  1364     let sugs = {
  1365       classes: new Map,
  1366       tags: new Map
  1367     };
  1368     let result = [];
  1369     let nodes = null;
  1370     // Filtering and sorting the results so that protocol transfer is miminal.
  1371     switch (selectorState) {
  1372       case "pseudo":
  1373         result = PSEUDO_SELECTORS.filter(item => {
  1374           return item[0].startsWith(":" + completing);
  1375         });
  1376         break;
  1378       case "class":
  1379         if (!query) {
  1380           nodes = this.rootDoc.querySelectorAll("[class]");
  1382         else {
  1383           nodes = this.rootDoc.querySelectorAll(query);
  1385         for (let node of nodes) {
  1386           for (let className of node.className.split(" ")) {
  1387             sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
  1390         sugs.classes.delete("");
  1391         // Editing the style editor may make the stylesheet have errors and
  1392         // thus the page's elements' styles start changing with a transition.
  1393         // That transition comes from the `moz-styleeditor-transitioning` class.
  1394         sugs.classes.delete("moz-styleeditor-transitioning");
  1395         sugs.classes.delete(HIDDEN_CLASS);
  1396         for (let [className, count] of sugs.classes) {
  1397           if (className.startsWith(completing)) {
  1398             result.push(["." + className, count]);
  1401         break;
  1403       case "id":
  1404         if (!query) {
  1405           nodes = this.rootDoc.querySelectorAll("[id]");
  1407         else {
  1408           nodes = this.rootDoc.querySelectorAll(query);
  1410         for (let node of nodes) {
  1411           if (node.id.startsWith(completing)) {
  1412             result.push(["#" + node.id, 1]);
  1415         break;
  1417       case "tag":
  1418         if (!query) {
  1419           nodes = this.rootDoc.getElementsByTagName("*");
  1421         else {
  1422           nodes = this.rootDoc.querySelectorAll(query);
  1424         for (let node of nodes) {
  1425           let tag = node.tagName.toLowerCase();
  1426           sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
  1428         for (let [tag, count] of sugs.tags) {
  1429           if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
  1430             result.push([tag, count]);
  1433         break;
  1435       case "null":
  1436         nodes = this.rootDoc.querySelectorAll(query);
  1437         for (let node of nodes) {
  1438           node.id && result.push(["#" + node.id, 1]);
  1439           let tag = node.tagName.toLowerCase();
  1440           sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
  1441           for (let className of node.className.split(" ")) {
  1442             sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
  1445         for (let [tag, count] of sugs.tags) {
  1446           tag && result.push([tag, count]);
  1448         sugs.classes.delete("");
  1449         // Editing the style editor may make the stylesheet have errors and
  1450         // thus the page's elements' styles start changing with a transition.
  1451         // That transition comes from the `moz-styleeditor-transitioning` class.
  1452         sugs.classes.delete("moz-styleeditor-transitioning");
  1453         sugs.classes.delete(HIDDEN_CLASS);
  1454         for (let [className, count] of sugs.classes) {
  1455           className && result.push(["." + className, count]);
  1459     // Sort alphabetically in increaseing order.
  1460     result = result.sort();
  1461     // Sort based on count in decreasing order.
  1462     result = result.sort(function(a, b) {
  1463       return b[1] - a[1];
  1464     });
  1466     result.slice(0, 25);
  1468     return {
  1469       query: query,
  1470       suggestions: result
  1471     };
  1472   }, {
  1473     request: {
  1474       query: Arg(0),
  1475       completing: Arg(1),
  1476       selectorState: Arg(2)
  1477     },
  1478     response: {
  1479       list: RetVal("array:array:string")
  1481   }),
  1483   /**
  1484    * Add a pseudo-class lock to a node.
  1486    * @param NodeActor node
  1487    * @param string pseudo
  1488    *    A pseudoclass: ':hover', ':active', ':focus'
  1489    * @param options
  1490    *    Options object:
  1491    *    `parents`: True if the pseudo-class should be added
  1492    *      to parent nodes.
  1494    * @returns An empty packet.  A "pseudoClassLock" mutation will
  1495    *    be queued for any changed nodes.
  1496    */
  1497   addPseudoClassLock: method(function(node, pseudo, options={}) {
  1498     this._addPseudoClassLock(node, pseudo);
  1500     if (!options.parents) {
  1501       return;
  1504     let walker = documentWalker(node.rawNode, this.rootWin);
  1505     let cur;
  1506     while ((cur = walker.parentNode())) {
  1507       let curNode = this._ref(cur);
  1508       this._addPseudoClassLock(curNode, pseudo);
  1510   }, {
  1511     request: {
  1512       node: Arg(0, "domnode"),
  1513       pseudoClass: Arg(1),
  1514       parents: Option(2)
  1515     },
  1516     response: {}
  1517   }),
  1519   _queuePseudoClassMutation: function(node) {
  1520     this.queueMutation({
  1521       target: node.actorID,
  1522       type: "pseudoClassLock",
  1523       pseudoClassLocks: node.writePseudoClassLocks()
  1524     });
  1525   },
  1527   _addPseudoClassLock: function(node, pseudo) {
  1528     if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
  1529       return false;
  1531     DOMUtils.addPseudoClassLock(node.rawNode, pseudo);
  1532     this._activePseudoClassLocks.add(node);
  1533     this._queuePseudoClassMutation(node);
  1534     return true;
  1535   },
  1537   _installHelperSheet: function(node) {
  1538     if (!this.installedHelpers) {
  1539       this.installedHelpers = new WeakMap;
  1541     let win = node.rawNode.ownerDocument.defaultView;
  1542     if (!this.installedHelpers.has(win)) {
  1543       let { Style } = require("sdk/stylesheet/style");
  1544       let { attach } = require("sdk/content/mod");
  1545       let style = Style({source: HELPER_SHEET, type: "agent" });
  1546       attach(style, win);
  1547       this.installedHelpers.set(win, style);
  1549   },
  1551   hideNode: method(function(node) {
  1552     this._installHelperSheet(node);
  1553     node.rawNode.classList.add(HIDDEN_CLASS);
  1554   }, {
  1555     request: { node: Arg(0, "domnode") }
  1556   }),
  1558   unhideNode: method(function(node) {
  1559     node.rawNode.classList.remove(HIDDEN_CLASS);
  1560   }, {
  1561     request: { node: Arg(0, "domnode") }
  1562   }),
  1564   /**
  1565    * Remove a pseudo-class lock from a node.
  1567    * @param NodeActor node
  1568    * @param string pseudo
  1569    *    A pseudoclass: ':hover', ':active', ':focus'
  1570    * @param options
  1571    *    Options object:
  1572    *    `parents`: True if the pseudo-class should be removed
  1573    *      from parent nodes.
  1575    * @returns An empty response.  "pseudoClassLock" mutations
  1576    *    will be emitted for any changed nodes.
  1577    */
  1578   removePseudoClassLock: method(function(node, pseudo, options={}) {
  1579     this._removePseudoClassLock(node, pseudo);
  1581     if (!options.parents) {
  1582       return;
  1585     let walker = documentWalker(node.rawNode, this.rootWin);
  1586     let cur;
  1587     while ((cur = walker.parentNode())) {
  1588       let curNode = this._ref(cur);
  1589       this._removePseudoClassLock(curNode, pseudo);
  1591   }, {
  1592     request: {
  1593       node: Arg(0, "domnode"),
  1594       pseudoClass: Arg(1),
  1595       parents: Option(2)
  1596     },
  1597     response: {}
  1598   }),
  1600   _removePseudoClassLock: function(node, pseudo) {
  1601     if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
  1602       return false;
  1604     DOMUtils.removePseudoClassLock(node.rawNode, pseudo);
  1605     if (!node.writePseudoClassLocks()) {
  1606       this._activePseudoClassLocks.delete(node);
  1608     this._queuePseudoClassMutation(node);
  1609     return true;
  1610   },
  1612   /**
  1613    * Clear all the pseudo-classes on a given node
  1614    * or all nodes.
  1615    */
  1616   clearPseudoClassLocks: method(function(node) {
  1617     if (node) {
  1618       DOMUtils.clearPseudoClassLocks(node.rawNode);
  1619       this._activePseudoClassLocks.delete(node);
  1620       this._queuePseudoClassMutation(node);
  1621     } else {
  1622       for (let locked of this._activePseudoClassLocks) {
  1623         DOMUtils.clearPseudoClassLocks(locked.rawNode);
  1624         this._activePseudoClassLocks.delete(locked);
  1625         this._queuePseudoClassMutation(locked);
  1628   }, {
  1629     request: {
  1630       node: Arg(0, "nullable:domnode")
  1631     },
  1632     response: {}
  1633   }),
  1635   /**
  1636    * Get a node's innerHTML property.
  1637    */
  1638   innerHTML: method(function(node) {
  1639     return LongStringActor(this.conn, node.rawNode.innerHTML);
  1640   }, {
  1641     request: {
  1642       node: Arg(0, "domnode")
  1643     },
  1644     response: {
  1645       value: RetVal("longstring")
  1647   }),
  1649   /**
  1650    * Get a node's outerHTML property.
  1651    */
  1652   outerHTML: method(function(node) {
  1653     return LongStringActor(this.conn, node.rawNode.outerHTML);
  1654   }, {
  1655     request: {
  1656       node: Arg(0, "domnode")
  1657     },
  1658     response: {
  1659       value: RetVal("longstring")
  1661   }),
  1663   /**
  1664    * Set a node's outerHTML property.
  1665    */
  1666   setOuterHTML: method(function(node, value) {
  1667     let parsedDOM = DOMParser.parseFromString(value, "text/html");
  1668     let rawNode = node.rawNode;
  1669     let parentNode = rawNode.parentNode;
  1671     // Special case for head and body.  Setting document.body.outerHTML
  1672     // creates an extra <head> tag, and document.head.outerHTML creates
  1673     // an extra <body>.  So instead we will call replaceChild with the
  1674     // parsed DOM, assuming that they aren't trying to set both tags at once.
  1675     if (rawNode.tagName === "BODY") {
  1676       if (parsedDOM.head.innerHTML === "") {
  1677         parentNode.replaceChild(parsedDOM.body, rawNode);
  1678       } else {
  1679         rawNode.outerHTML = value;
  1681     } else if (rawNode.tagName === "HEAD") {
  1682       if (parsedDOM.body.innerHTML === "") {
  1683         parentNode.replaceChild(parsedDOM.head, rawNode);
  1684       } else {
  1685         rawNode.outerHTML = value;
  1687     } else if (node.isDocumentElement()) {
  1688       // Unable to set outerHTML on the document element.  Fall back by
  1689       // setting attributes manually, then replace the body and head elements.
  1690       let finalAttributeModifications = [];
  1691       let attributeModifications = {};
  1692       for (let attribute of rawNode.attributes) {
  1693         attributeModifications[attribute.name] = null;
  1695       for (let attribute of parsedDOM.documentElement.attributes) {
  1696         attributeModifications[attribute.name] = attribute.value;
  1698       for (let key in attributeModifications) {
  1699         finalAttributeModifications.push({
  1700           attributeName: key,
  1701           newValue: attributeModifications[key]
  1702         });
  1704       node.modifyAttributes(finalAttributeModifications);
  1705       rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
  1706       rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
  1707     } else {
  1708       rawNode.outerHTML = value;
  1710   }, {
  1711     request: {
  1712       node: Arg(0, "domnode"),
  1713       value: Arg(1),
  1714     },
  1715     response: {
  1717   }),
  1719   /**
  1720    * Removes a node from its parent node.
  1722    * @returns The node's nextSibling before it was removed.
  1723    */
  1724   removeNode: method(function(node) {
  1725     if ((node.rawNode.ownerDocument &&
  1726          node.rawNode.ownerDocument.documentElement === this.rawNode) ||
  1727          node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
  1728       throw Error("Cannot remove document or document elements.");
  1730     let nextSibling = this.nextSibling(node);
  1731     if (node.rawNode.parentNode) {
  1732       node.rawNode.parentNode.removeChild(node.rawNode);
  1733       // Mutation events will take care of the rest.
  1735     return nextSibling;
  1736   }, {
  1737     request: {
  1738       node: Arg(0, "domnode")
  1739     },
  1740     response: {
  1741       nextSibling: RetVal("nullable:domnode")
  1743   }),
  1745   /**
  1746    * Insert a node into the DOM.
  1747    */
  1748   insertBefore: method(function(node, parent, sibling) {
  1749     parent.rawNode.insertBefore(node.rawNode, sibling ? sibling.rawNode : null);
  1750   }, {
  1751     request: {
  1752       node: Arg(0, "domnode"),
  1753       parent: Arg(1, "domnode"),
  1754       sibling: Arg(2, "nullable:domnode")
  1755     },
  1756     response: {}
  1757   }),
  1759   /**
  1760    * Get any pending mutation records.  Must be called by the client after
  1761    * the `new-mutations` notification is received.  Returns an array of
  1762    * mutation records.
  1764    * Mutation records have a basic structure:
  1766    * {
  1767    *   type: attributes|characterData|childList,
  1768    *   target: <domnode actor ID>,
  1769    * }
  1771    * And additional attributes based on the mutation type:
  1773    * `attributes` type:
  1774    *   attributeName: <string> - the attribute that changed
  1775    *   attributeNamespace: <string> - the attribute's namespace URI, if any.
  1776    *   newValue: <string> - The new value of the attribute, if any.
  1778    * `characterData` type:
  1779    *   newValue: <string> - the new shortValue for the node
  1780    *   [incompleteValue: true] - True if the shortValue was truncated.
  1782    * `childList` type is returned when the set of children for a node
  1783    * has changed.  Includes extra data, which can be used by the client to
  1784    * maintain its ownership subtree.
  1786    *   added: array of <domnode actor ID> - The list of actors *previously
  1787    *     seen by the client* that were added to the target node.
  1788    *   removed: array of <domnode actor ID> The list of actors *previously
  1789    *     seen by the client* that were removed from the target node.
  1791    * Actors that are included in a MutationRecord's `removed` but
  1792    * not in an `added` have been removed from the client's ownership
  1793    * tree (either by being moved under a node the client has seen yet
  1794    * or by being removed from the tree entirely), and is considered
  1795    * 'orphaned'.
  1797    * Keep in mind that if a node that the client hasn't seen is moved
  1798    * into or out of the target node, it will not be included in the
  1799    * removedNodes and addedNodes list, so if the client is interested
  1800    * in the new set of children it needs to issue a `children` request.
  1801    */
  1802   getMutations: method(function(options={}) {
  1803     let pending = this._pendingMutations || [];
  1804     this._pendingMutations = [];
  1806     if (options.cleanup) {
  1807       for (let node of this._orphaned) {
  1808         // Release the orphaned node.  Nodes or children that have been
  1809         // retained will be moved to this._retainedOrphans.
  1810         this.releaseNode(node);
  1812       this._orphaned = new Set();
  1815     return pending;
  1816   }, {
  1817     request: {
  1818       cleanup: Option(0)
  1819     },
  1820     response: {
  1821       mutations: RetVal("array:dommutation")
  1823   }),
  1825   queueMutation: function(mutation) {
  1826     if (!this.actorID) {
  1827       // We've been destroyed, don't bother queueing this mutation.
  1828       return;
  1830     // We only send the `new-mutations` notification once, until the client
  1831     // fetches mutations with the `getMutations` packet.
  1832     let needEvent = this._pendingMutations.length === 0;
  1834     this._pendingMutations.push(mutation);
  1836     if (needEvent) {
  1837       events.emit(this, "new-mutations");
  1839   },
  1841   /**
  1842    * Handles mutations from the DOM mutation observer API.
  1844    * @param array[MutationRecord] mutations
  1845    *    See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
  1846    */
  1847   onMutations: function(mutations) {
  1848     for (let change of mutations) {
  1849       let targetActor = this._refMap.get(change.target);
  1850       if (!targetActor) {
  1851         continue;
  1853       let targetNode = change.target;
  1854       let mutation = {
  1855         type: change.type,
  1856         target: targetActor.actorID,
  1859       if (mutation.type === "attributes") {
  1860         mutation.attributeName = change.attributeName;
  1861         mutation.attributeNamespace = change.attributeNamespace || undefined;
  1862         mutation.newValue = targetNode.getAttribute(mutation.attributeName);
  1863       } else if (mutation.type === "characterData") {
  1864         if (targetNode.nodeValue.length > gValueSummaryLength) {
  1865           mutation.newValue = targetNode.nodeValue.substring(0, gValueSummaryLength);
  1866           mutation.incompleteValue = true;
  1867         } else {
  1868           mutation.newValue = targetNode.nodeValue;
  1870       } else if (mutation.type === "childList") {
  1871         // Get the list of removed and added actors that the client has seen
  1872         // so that it can keep its ownership tree up to date.
  1873         let removedActors = [];
  1874         let addedActors = [];
  1875         for (let removed of change.removedNodes) {
  1876           let removedActor = this._refMap.get(removed);
  1877           if (!removedActor) {
  1878             // If the client never encountered this actor we don't need to
  1879             // mention that it was removed.
  1880             continue;
  1882           // While removed from the tree, nodes are saved as orphaned.
  1883           this._orphaned.add(removedActor);
  1884           removedActors.push(removedActor.actorID);
  1886         for (let added of change.addedNodes) {
  1887           let addedActor = this._refMap.get(added);
  1888           if (!addedActor) {
  1889             // If the client never encounted this actor we don't need to tell
  1890             // it about its addition for ownership tree purposes - if the
  1891             // client wants to see the new nodes it can ask for children.
  1892             continue;
  1894           // The actor is reconnected to the ownership tree, unorphan
  1895           // it and let the client know so that its ownership tree is up
  1896           // to date.
  1897           this._orphaned.delete(addedActor);
  1898           addedActors.push(addedActor.actorID);
  1900         mutation.numChildren = change.target.childNodes.length;
  1901         mutation.removed = removedActors;
  1902         mutation.added = addedActors;
  1904       this.queueMutation(mutation);
  1906   },
  1908   onFrameLoad: function({ window, isTopLevel }) {
  1909     if (!this.rootDoc && isTopLevel) {
  1910       this.rootDoc = window.document;
  1911       this.rootNode = this.document();
  1912       this.queueMutation({
  1913         type: "newRoot",
  1914         target: this.rootNode.form()
  1915       });
  1917     let frame = this.layoutHelpers.getFrameElement(window);
  1918     let frameActor = this._refMap.get(frame);
  1919     if (!frameActor) {
  1920       return;
  1923     this.queueMutation({
  1924       type: "frameLoad",
  1925       target: frameActor.actorID,
  1926     });
  1928     // Send a childList mutation on the frame.
  1929     this.queueMutation({
  1930       type: "childList",
  1931       target: frameActor.actorID,
  1932       added: [],
  1933       removed: []
  1934     })
  1935   },
  1937   // Returns true if domNode is in window or a subframe.
  1938   _childOfWindow: function(window, domNode) {
  1939     let win = nodeDocument(domNode).defaultView;
  1940     while (win) {
  1941       if (win === window) {
  1942         return true;
  1944       win = this.layoutHelpers.getFrameElement(win);
  1946     return false;
  1947   },
  1949   onFrameUnload: function({ window }) {
  1950     // Any retained orphans that belong to this document
  1951     // or its children need to be released, and a mutation sent
  1952     // to notify of that.
  1953     let releasedOrphans = [];
  1955     for (let retained of this._retainedOrphans) {
  1956       if (Cu.isDeadWrapper(retained.rawNode) ||
  1957           this._childOfWindow(window, retained.rawNode)) {
  1958         this._retainedOrphans.delete(retained);
  1959         releasedOrphans.push(retained.actorID);
  1960         this.releaseNode(retained, { force: true });
  1964     if (releasedOrphans.length > 0) {
  1965       this.queueMutation({
  1966         target: this.rootNode.actorID,
  1967         type: "unretained",
  1968         nodes: releasedOrphans
  1969       });
  1972     let doc = window.document;
  1973     let documentActor = this._refMap.get(doc);
  1974     if (!documentActor) {
  1975       return;
  1978     if (this.rootDoc === doc) {
  1979       this.rootDoc = null;
  1980       this.rootNode = null;
  1983     this.queueMutation({
  1984       type: "documentUnload",
  1985       target: documentActor.actorID
  1986     });
  1988     let walker = documentWalker(doc, this.rootWin);
  1989     let parentNode = walker.parentNode();
  1990     if (parentNode) {
  1991       // Send a childList mutation on the frame so that clients know
  1992       // they should reread the children list.
  1993       this.queueMutation({
  1994         type: "childList",
  1995         target: this._refMap.get(parentNode).actorID,
  1996         added: [],
  1997         removed: []
  1998       });
  2001     // Need to force a release of this node, because those nodes can't
  2002     // be accessed anymore.
  2003     this.releaseNode(documentActor, { force: true });
  2004   },
  2006   /**
  2007    * Check if a node is attached to the DOM tree of the current page.
  2008    * @param {nsIDomNode} rawNode
  2009    * @return {Boolean} false if the node is removed from the tree or within a
  2010    * document fragment
  2011    */
  2012   _isInDOMTree: function(rawNode) {
  2013     let walker = documentWalker(rawNode, this.rootWin);
  2014     let current = walker.currentNode;
  2016     // Reaching the top of tree
  2017     while (walker.parentNode()) {
  2018       current = walker.currentNode;
  2021     // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
  2022     // attached
  2023     if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
  2024         current !== this.rootDoc) {
  2025       return false;
  2028     // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
  2029     return true;
  2030   },
  2032   /**
  2033    * @see _isInDomTree
  2034    */
  2035   isInDOMTree: method(function(node) {
  2036     return node ? this._isInDOMTree(node.rawNode) : false;
  2037   }, {
  2038     request: { node: Arg(0, "domnode") },
  2039     response: { attached: RetVal("boolean") }
  2040   }),
  2042   /**
  2043    * Given an ObjectActor (identified by its ID), commonly used in the debugger,
  2044    * webconsole and variablesView, return the corresponding inspector's NodeActor
  2045    */
  2046   getNodeActorFromObjectActor: method(function(objectActorID) {
  2047     let debuggerObject = this.conn.getActor(objectActorID).obj;
  2048     let rawNode = debuggerObject.unsafeDereference();
  2050     if (!this._isInDOMTree(rawNode)) {
  2051       return null;
  2054     // This is a special case for the document object whereby it is considered
  2055     // as document.documentElement (the <html> node)
  2056     if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
  2057       rawNode = rawNode.documentElement;
  2060     return this.attachElement(rawNode);
  2061   }, {
  2062     request: {
  2063       objectActorID: Arg(0, "string")
  2064     },
  2065     response: {
  2066       nodeFront: RetVal("nullable:disconnectedNode")
  2068   }),
  2069 });
  2071 /**
  2072  * Client side of the DOM walker.
  2073  */
  2074 var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
  2075   // Set to true if cleanup should be requested after every mutation list.
  2076   autoCleanup: true,
  2078   /**
  2079    * This is kept for backward-compatibility reasons with older remote target.
  2080    * Targets previous to bug 916443
  2081    */
  2082   pick: protocol.custom(function() {
  2083     return this._pick().then(response => {
  2084       return response.node;
  2085     });
  2086   }, {impl: "_pick"}),
  2088   initialize: function(client, form) {
  2089     this._createRootNodePromise();
  2090     protocol.Front.prototype.initialize.call(this, client, form);
  2091     this._orphaned = new Set();
  2092     this._retainedOrphans = new Set();
  2093   },
  2095   destroy: function() {
  2096     protocol.Front.prototype.destroy.call(this);
  2097   },
  2099   // Update the object given a form representation off the wire.
  2100   form: function(json) {
  2101     this.actorID = json.actor;
  2102     this.rootNode = types.getType("domnode").read(json.root, this);
  2103     this._rootNodeDeferred.resolve(this.rootNode);
  2104   },
  2106   /**
  2107    * Clients can use walker.rootNode to get the current root node of the
  2108    * walker, but during a reload the root node might be null.  This
  2109    * method returns a promise that will resolve to the root node when it is
  2110    * set.
  2111    */
  2112   getRootNode: function() {
  2113     return this._rootNodeDeferred.promise;
  2114   },
  2116   /**
  2117    * Create the root node promise, triggering the "new-root" notification
  2118    * on resolution.
  2119    */
  2120   _createRootNodePromise: function() {
  2121     this._rootNodeDeferred = promise.defer();
  2122     this._rootNodeDeferred.promise.then(() => {
  2123       events.emit(this, "new-root");
  2124     });
  2125   },
  2127   /**
  2128    * When reading an actor form off the wire, we want to hook it up to its
  2129    * parent front.  The protocol guarantees that the parent will be seen
  2130    * by the client in either a previous or the current request.
  2131    * So if we've already seen this parent return it, otherwise create
  2132    * a bare-bones stand-in node.  The stand-in node will be updated
  2133    * with a real form by the end of the deserialization.
  2134    */
  2135   ensureParentFront: function(id) {
  2136     let front = this.get(id);
  2137     if (front) {
  2138       return front;
  2141     return types.getType("domnode").read({ actor: id }, this, "standin");
  2142   },
  2144   /**
  2145    * See the documentation for WalkerActor.prototype.retainNode for
  2146    * information on retained nodes.
  2148    * From the client's perspective, `retainNode` can fail if the node in
  2149    * question is removed from the ownership tree before the `retainNode`
  2150    * request reaches the server.  This can only happen if the client has
  2151    * asked the server to release nodes but hasn't gotten a response
  2152    * yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
  2153    * set is outstanding.
  2155    * If either of those requests is outstanding AND releases the retained
  2156    * node, this request will fail with noSuchActor, but the ownership tree
  2157    * will stay in a consistent state.
  2159    * Because the protocol guarantees that requests will be processed and
  2160    * responses received in the order they were sent, we get the right
  2161    * semantics by setting our local retained flag on the node only AFTER
  2162    * a SUCCESSFUL retainNode call.
  2163    */
  2164   retainNode: protocol.custom(function(node) {
  2165     return this._retainNode(node).then(() => {
  2166       node.retained = true;
  2167     });
  2168   }, {
  2169     impl: "_retainNode",
  2170   }),
  2172   unretainNode: protocol.custom(function(node) {
  2173     return this._unretainNode(node).then(() => {
  2174       node.retained = false;
  2175       if (this._retainedOrphans.has(node)) {
  2176         this._retainedOrphans.delete(node);
  2177         this._releaseFront(node);
  2179     });
  2180   }, {
  2181     impl: "_unretainNode"
  2182   }),
  2184   releaseNode: protocol.custom(function(node, options={}) {
  2185     // NodeFront.destroy will destroy children in the ownership tree too,
  2186     // mimicking what the server will do here.
  2187     let actorID = node.actorID;
  2188     this._releaseFront(node, !!options.force);
  2189     return this._releaseNode({ actorID: actorID });
  2190   }, {
  2191     impl: "_releaseNode"
  2192   }),
  2194   querySelector: protocol.custom(function(queryNode, selector) {
  2195     return this._querySelector(queryNode, selector).then(response => {
  2196       return response.node;
  2197     });
  2198   }, {
  2199     impl: "_querySelector"
  2200   }),
  2202   getNodeActorFromObjectActor: protocol.custom(function(objectActorID) {
  2203     return this._getNodeActorFromObjectActor(objectActorID).then(response => {
  2204       return response ? response.node : null;
  2205     });
  2206   }, {
  2207     impl: "_getNodeActorFromObjectActor"
  2208   }),
  2210   _releaseFront: function(node, force) {
  2211     if (node.retained && !force) {
  2212       node.reparent(null);
  2213       this._retainedOrphans.add(node);
  2214       return;
  2217     if (node.retained) {
  2218       // Forcing a removal.
  2219       this._retainedOrphans.delete(node);
  2222     // Release any children
  2223     for (let child of node.treeChildren()) {
  2224       this._releaseFront(child, force);
  2227     // All children will have been removed from the node by this point.
  2228     node.reparent(null);
  2229     node.destroy();
  2230   },
  2232   /**
  2233    * Get any unprocessed mutation records and process them.
  2234    */
  2235   getMutations: protocol.custom(function(options={}) {
  2236     return this._getMutations(options).then(mutations => {
  2237       let emitMutations = [];
  2238       for (let change of mutations) {
  2239         // The target is only an actorID, get the associated front.
  2240         let targetID;
  2241         let targetFront;
  2243         if (change.type === "newRoot") {
  2244           this.rootNode = types.getType("domnode").read(change.target, this);
  2245           this._rootNodeDeferred.resolve(this.rootNode);
  2246           targetID = this.rootNode.actorID;
  2247           targetFront = this.rootNode;
  2248         } else {
  2249           targetID = change.target;
  2250           targetFront = this.get(targetID);
  2253         if (!targetFront) {
  2254           console.trace("Got a mutation for an unexpected actor: " + targetID + ", please file a bug on bugzilla.mozilla.org!");
  2255           continue;
  2258         let emittedMutation = object.merge(change, { target: targetFront });
  2260         if (change.type === "childList") {
  2261           // Update the ownership tree according to the mutation record.
  2262           let addedFronts = [];
  2263           let removedFronts = [];
  2264           for (let removed of change.removed) {
  2265             let removedFront = this.get(removed);
  2266             if (!removedFront) {
  2267               console.error("Got a removal of an actor we didn't know about: " + removed);
  2268               continue;
  2270             // Remove from the ownership tree
  2271             removedFront.reparent(null);
  2273             // This node is orphaned unless we get it in the 'added' list
  2274             // eventually.
  2275             this._orphaned.add(removedFront);
  2276             removedFronts.push(removedFront);
  2278           for (let added of change.added) {
  2279             let addedFront = this.get(added);
  2280             if (!addedFront) {
  2281               console.error("Got an addition of an actor we didn't know about: " + added);
  2282               continue;
  2284             addedFront.reparent(targetFront)
  2286             // The actor is reconnected to the ownership tree, unorphan
  2287             // it.
  2288             this._orphaned.delete(addedFront);
  2289             addedFronts.push(addedFront);
  2291           // Before passing to users, replace the added and removed actor
  2292           // ids with front in the mutation record.
  2293           emittedMutation.added = addedFronts;
  2294           emittedMutation.removed = removedFronts;
  2295           targetFront._form.numChildren = change.numChildren;
  2296         } else if (change.type === "frameLoad") {
  2297           // Nothing we need to do here, except verify that we don't have any
  2298           // document children, because we should have gotten a documentUnload
  2299           // first.
  2300           for (let child of targetFront.treeChildren()) {
  2301             if (child.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
  2302               console.trace("Got an unexpected frameLoad in the inspector, please file a bug on bugzilla.mozilla.org!");
  2305         } else if (change.type === "documentUnload") {
  2306           if (targetFront === this.rootNode) {
  2307             this._createRootNodePromise();
  2310           // We try to give fronts instead of actorIDs, but these fronts need
  2311           // to be destroyed now.
  2312           emittedMutation.target = targetFront.actorID;
  2313           emittedMutation.targetParent = targetFront.parentNode();
  2315           // Release the document node and all of its children, even retained.
  2316           this._releaseFront(targetFront, true);
  2317         } else if (change.type === "unretained") {
  2318           // Retained orphans were force-released without the intervention of
  2319           // client (probably a navigated frame).
  2320           for (let released of change.nodes) {
  2321             let releasedFront = this.get(released);
  2322             this._retainedOrphans.delete(released);
  2323             this._releaseFront(releasedFront, true);
  2325         } else {
  2326           targetFront.updateMutation(change);
  2329         emitMutations.push(emittedMutation);
  2332       if (options.cleanup) {
  2333         for (let node of this._orphaned) {
  2334           // This will move retained nodes to this._retainedOrphans.
  2335           this._releaseFront(node);
  2337         this._orphaned = new Set();
  2340       events.emit(this, "mutations", emitMutations);
  2341     });
  2342   }, {
  2343     impl: "_getMutations"
  2344   }),
  2346   /**
  2347    * Handle the `new-mutations` notification by fetching the
  2348    * available mutation records.
  2349    */
  2350   onMutations: protocol.preEvent("new-mutations", function() {
  2351     // Fetch and process the mutations.
  2352     this.getMutations({cleanup: this.autoCleanup}).then(null, console.error);
  2353   }),
  2355   isLocal: function() {
  2356     return !!this.conn._transport._serverConnection;
  2357   },
  2359   // XXX hack during transition to remote inspector: get a proper NodeFront
  2360   // for a given local node.  Only works locally.
  2361   frontForRawNode: function(rawNode) {
  2362     if (!this.isLocal()) {
  2363       console.warn("Tried to use frontForRawNode on a remote connection.");
  2364       return null;
  2366     let walkerActor = this.conn._transport._serverConnection.getActor(this.actorID);
  2367     if (!walkerActor) {
  2368       throw Error("Could not find client side for actor " + this.actorID);
  2370     let nodeActor = walkerActor._ref(rawNode);
  2372     // Pass the node through a read/write pair to create the client side actor.
  2373     let nodeType = types.getType("domnode");
  2374     let returnNode = nodeType.read(nodeType.write(nodeActor, walkerActor), this);
  2375     let top = returnNode;
  2376     let extras = walkerActor.parents(nodeActor);
  2377     for (let extraActor of extras) {
  2378       top = nodeType.read(nodeType.write(extraActor, walkerActor), this);
  2381     if (top !== this.rootNode) {
  2382       // Imported an already-orphaned node.
  2383       this._orphaned.add(top);
  2384       walkerActor._orphaned.add(this.conn._transport._serverConnection.getActor(top.actorID));
  2386     return returnNode;
  2388 });
  2390 /**
  2391  * Convenience API for building a list of attribute modifications
  2392  * for the `modifyAttributes` request.
  2393  */
  2394 var AttributeModificationList = Class({
  2395   initialize: function(node) {
  2396     this.node = node;
  2397     this.modifications = [];
  2398   },
  2400   apply: function() {
  2401     let ret = this.node.modifyAttributes(this.modifications);
  2402     return ret;
  2403   },
  2405   destroy: function() {
  2406     this.node = null;
  2407     this.modification = null;
  2408   },
  2410   setAttributeNS: function(ns, name, value) {
  2411     this.modifications.push({
  2412       attributeNamespace: ns,
  2413       attributeName: name,
  2414       newValue: value
  2415     });
  2416   },
  2418   setAttribute: function(name, value) {
  2419     this.setAttributeNS(undefined, name, value);
  2420   },
  2422   removeAttributeNS: function(ns, name) {
  2423     this.setAttributeNS(ns, name, undefined);
  2424   },
  2426   removeAttribute: function(name) {
  2427     this.setAttributeNS(undefined, name, undefined);
  2429 })
  2431 /**
  2432  * Server side of the inspector actor, which is used to create
  2433  * inspector-related actors, including the walker.
  2434  */
  2435 var InspectorActor = protocol.ActorClass({
  2436   typeName: "inspector",
  2437   initialize: function(conn, tabActor) {
  2438     protocol.Actor.prototype.initialize.call(this, conn);
  2439     this.tabActor = tabActor;
  2440   },
  2442   get window() this.tabActor.window,
  2444   getWalker: method(function(options={}) {
  2445     if (this._walkerPromise) {
  2446       return this._walkerPromise;
  2449     let deferred = promise.defer();
  2450     this._walkerPromise = deferred.promise;
  2452     let window = this.window;
  2453     var domReady = () => {
  2454       let tabActor = this.tabActor;
  2455       window.removeEventListener("DOMContentLoaded", domReady, true);
  2456       this.walker = WalkerActor(this.conn, tabActor, options);
  2457       events.once(this.walker, "destroyed", () => {
  2458         this._walkerPromise = null;
  2459         this._pageStylePromise = null;
  2460       });
  2461       deferred.resolve(this.walker);
  2462     };
  2464     if (window.document.readyState === "loading") {
  2465       window.addEventListener("DOMContentLoaded", domReady, true);
  2466     } else {
  2467       domReady();
  2470     return this._walkerPromise;
  2471   }, {
  2472     request: {},
  2473     response: {
  2474       walker: RetVal("domwalker")
  2476   }),
  2478   getPageStyle: method(function() {
  2479     if (this._pageStylePromise) {
  2480       return this._pageStylePromise;
  2483     this._pageStylePromise = this.getWalker().then(walker => {
  2484       return PageStyleActor(this);
  2485     });
  2486     return this._pageStylePromise;
  2487   }, {
  2488     request: {},
  2489     response: {
  2490       pageStyle: RetVal("pagestyle")
  2492   }),
  2494   getHighlighter: method(function (autohide) {
  2495     if (this._highlighterPromise) {
  2496       return this._highlighterPromise;
  2499     this._highlighterPromise = this.getWalker().then(walker => {
  2500       return HighlighterActor(this, autohide);
  2501     });
  2502     return this._highlighterPromise;
  2503   }, {
  2504     request: { autohide: Arg(0, "boolean") },
  2505     response: {
  2506       highligter: RetVal("highlighter")
  2508   }),
  2510   /**
  2511    * Get the node's image data if any (for canvas and img nodes).
  2512    * Returns an imageData object with the actual data being a LongStringActor
  2513    * and a size json object.
  2514    * The image data is transmitted as a base64 encoded png data-uri.
  2515    * The method rejects if the node isn't an image or if the image is missing
  2517    * Accepts a maxDim request parameter to resize images that are larger. This
  2518    * is important as the resizing occurs server-side so that image-data being
  2519    * transfered in the longstring back to the client will be that much smaller
  2520    */
  2521   getImageDataFromURL: method(function(url, maxDim) {
  2522     let deferred = promise.defer();
  2523     let img = new this.window.Image();
  2525     // On load, get the image data and send the response
  2526     img.onload = () => {
  2527       // imageToImageData throws an error if the image is missing
  2528       try {
  2529         let imageData = imageToImageData(img, maxDim);
  2530         deferred.resolve({
  2531           data: LongStringActor(this.conn, imageData.data),
  2532           size: imageData.size
  2533         });
  2534       } catch (e) {
  2535         deferred.reject(new Error("Image " + url+ " not available"));
  2539     // If the URL doesn't point to a resource, reject
  2540     img.onerror = () => {
  2541       deferred.reject(new Error("Image " + url+ " not available"));
  2544     // If the request hangs for too long, kill it to avoid queuing up other requests
  2545     // to the same actor, except if we're running tests
  2546     if (!gDevTools.testing) {
  2547       this.window.setTimeout(() => {
  2548         deferred.reject(new Error("Image " + url + " could not be retrieved in time"));
  2549       }, IMAGE_FETCHING_TIMEOUT);
  2552     img.src = url;
  2554     return deferred.promise;
  2555   }, {
  2556     request: {url: Arg(0), maxDim: Arg(1, "nullable:number")},
  2557     response: RetVal("imageData")
  2558   })
  2559 });
  2561 /**
  2562  * Client side of the inspector actor, which is used to create
  2563  * inspector-related actors, including the walker.
  2564  */
  2565 var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor, {
  2566   initialize: function(client, tabForm) {
  2567     protocol.Front.prototype.initialize.call(this, client);
  2568     this.actorID = tabForm.inspectorActor;
  2570     // XXX: This is the first actor type in its hierarchy to use the protocol
  2571     // library, so we're going to self-own on the client side for now.
  2572     client.addActorPool(this);
  2573     this.manage(this);
  2574   },
  2576   destroy: function() {
  2577     delete this.walker;
  2578     protocol.Front.prototype.destroy.call(this);
  2579   },
  2581   getWalker: protocol.custom(function() {
  2582     return this._getWalker().then(walker => {
  2583       this.walker = walker;
  2584       return walker;
  2585     });
  2586   }, {
  2587     impl: "_getWalker"
  2588   }),
  2590   getPageStyle: protocol.custom(function() {
  2591     return this._getPageStyle().then(pageStyle => {
  2592       // We need a walker to understand node references from the
  2593       // node style.
  2594       if (this.walker) {
  2595         return pageStyle;
  2597       return this.getWalker().then(() => {
  2598         return pageStyle;
  2599       });
  2600     });
  2601   }, {
  2602     impl: "_getPageStyle"
  2603   })
  2604 });
  2606 function documentWalker(node, rootWin, whatToShow=Ci.nsIDOMNodeFilter.SHOW_ALL) {
  2607   return new DocumentWalker(node, rootWin, whatToShow, whitespaceTextFilter, false);
  2610 // Exported for test purposes.
  2611 exports._documentWalker = documentWalker;
  2613 function nodeDocument(node) {
  2614   return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
  2617 /**
  2618  * Similar to a TreeWalker, except will dig in to iframes and it doesn't
  2619  * implement the good methods like previousNode and nextNode.
  2621  * See TreeWalker documentation for explanations of the methods.
  2622  */
  2623 function DocumentWalker(aNode, aRootWin, aShow, aFilter, aExpandEntityReferences) {
  2624   let doc = nodeDocument(aNode);
  2625   this.layoutHelpers = new LayoutHelpers(aRootWin);
  2626   this.walker = doc.createTreeWalker(doc,
  2627     aShow, aFilter, aExpandEntityReferences);
  2628   this.walker.currentNode = aNode;
  2629   this.filter = aFilter;
  2632 DocumentWalker.prototype = {
  2633   get node() this.walker.node,
  2634   get whatToShow() this.walker.whatToShow,
  2635   get expandEntityReferences() this.walker.expandEntityReferences,
  2636   get currentNode() this.walker.currentNode,
  2637   set currentNode(aVal) this.walker.currentNode = aVal,
  2639   /**
  2640    * Called when the new node is in a different document than
  2641    * the current node, creates a new treewalker for the document we've
  2642    * run in to.
  2643    */
  2644   _reparentWalker: function(aNewNode) {
  2645     if (!aNewNode) {
  2646       return null;
  2648     let doc = nodeDocument(aNewNode);
  2649     let walker = doc.createTreeWalker(doc,
  2650       this.whatToShow, this.filter, this.expandEntityReferences);
  2651     walker.currentNode = aNewNode;
  2652     this.walker = walker;
  2653     return aNewNode;
  2654   },
  2656   parentNode: function() {
  2657     let currentNode = this.walker.currentNode;
  2658     let parentNode = this.walker.parentNode();
  2660     if (!parentNode) {
  2661       if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE
  2662           && currentNode.defaultView) {
  2664         let window = currentNode.defaultView;
  2665         let frame = this.layoutHelpers.getFrameElement(window);
  2666         if (frame) {
  2667           return this._reparentWalker(frame);
  2670       return null;
  2673     return parentNode;
  2674   },
  2676   firstChild: function() {
  2677     let node = this.walker.currentNode;
  2678     if (!node)
  2679       return null;
  2680     if (node.contentDocument) {
  2681       return this._reparentWalker(node.contentDocument);
  2682     } else if (node.getSVGDocument) {
  2683       return this._reparentWalker(node.getSVGDocument());
  2685     return this.walker.firstChild();
  2686   },
  2688   lastChild: function() {
  2689     let node = this.walker.currentNode;
  2690     if (!node)
  2691       return null;
  2692     if (node.contentDocument) {
  2693       return this._reparentWalker(node.contentDocument);
  2694     } else if (node.getSVGDocument) {
  2695       return this._reparentWalker(node.getSVGDocument());
  2697     return this.walker.lastChild();
  2698   },
  2700   previousSibling: function DW_previousSibling() this.walker.previousSibling(),
  2701   nextSibling: function DW_nextSibling() this.walker.nextSibling()
  2702 };
  2704 /**
  2705  * A tree walker filter for avoiding empty whitespace text nodes.
  2706  */
  2707 function whitespaceTextFilter(aNode) {
  2708     if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
  2709         !/[^\s]/.exec(aNode.nodeValue)) {
  2710       return Ci.nsIDOMNodeFilter.FILTER_SKIP;
  2711     } else {
  2712       return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
  2716 /**
  2717  * Given an image DOMNode, return the image data-uri.
  2718  * @param {DOMNode} node The image node
  2719  * @param {Number} maxDim Optionally pass a maximum size you want the longest
  2720  * side of the image to be resized to before getting the image data.
  2721  * @return {Object} An object containing the data-uri and size-related information
  2722  * {data: "...", size: {naturalWidth: 400, naturalHeight: 300, resized: true}}
  2723  * @throws an error if the node isn't an image or if the image is missing
  2724  */
  2725 function imageToImageData(node, maxDim) {
  2726   let isImg = node.tagName.toLowerCase() === "img";
  2727   let isCanvas = node.tagName.toLowerCase() === "canvas";
  2729   if (!isImg && !isCanvas) {
  2730     return null;
  2733   // Get the image resize ratio if a maxDim was provided
  2734   let resizeRatio = 1;
  2735   let imgWidth = node.naturalWidth || node.width;
  2736   let imgHeight = node.naturalHeight || node.height;
  2737   let imgMax = Math.max(imgWidth, imgHeight);
  2738   if (maxDim && imgMax > maxDim) {
  2739     resizeRatio = maxDim / imgMax;
  2742   // Extract the image data
  2743   let imageData;
  2744   // The image may already be a data-uri, in which case, save ourselves the
  2745   // trouble of converting via the canvas.drawImage.toDataURL method
  2746   if (isImg && node.src.startsWith("data:")) {
  2747     imageData = node.src;
  2748   } else {
  2749     // Create a canvas to copy the rawNode into and get the imageData from
  2750     let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
  2751     canvas.width = imgWidth * resizeRatio;
  2752     canvas.height = imgHeight * resizeRatio;
  2753     let ctx = canvas.getContext("2d");
  2755     // Copy the rawNode image or canvas in the new canvas and extract data
  2756     ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
  2757     imageData = canvas.toDataURL("image/png");
  2760   return {
  2761     data: imageData,
  2762     size: {
  2763       naturalWidth: imgWidth,
  2764       naturalHeight: imgHeight,
  2765       resized: resizeRatio !== 1
  2770 loader.lazyGetter(this, "DOMUtils", function () {
  2771   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
  2772 });

mercurial