Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
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);
1007 }
1008 return null;
1009 },
1011 /**
1012 * Mark a node as 'retained'.
1013 *
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`.
1017 *
1018 * When a retained node's parent is released, a retained mode is added to
1019 * the walker's "retained orphans" list.
1020 *
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.
1024 *
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);
1044 }
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;
1057 }
1059 if (node.retained) {
1060 // Forcing a retained node to go away.
1061 this._retainedOrphans.delete(node);
1062 }
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);
1071 }
1072 child = walker.nextSibling();
1073 }
1075 node.destroy();
1076 }, {
1077 request: {
1078 node: Arg(0, "domnode"),
1079 force: Option(1)
1080 }
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;
1090 }
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;
1101 }
1102 }
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.
1110 *
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.
1124 *
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.");
1133 }
1134 let maxNodes = options.maxNodes || -1;
1135 if (maxNodes == -1) {
1136 maxNodes = Number.MAX_VALUE;
1137 }
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: [] };
1153 }
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;
1162 }
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;
1173 }
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);
1188 }
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.
1201 *
1202 * If `start` or `center` are not specified, this method will center on the
1203 * node whose siblings are requested.
1204 *
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.
1218 *
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 };
1232 }
1234 if (!(options.start || options.center)) {
1235 options.center = node;
1236 }
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.
1244 *
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.
1259 *
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
1303 *
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
1329 *
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.
1340 }
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")
1350 }
1351 }),
1353 /**
1354 * Returns a list of matching results for CSS selector autocompletion.
1355 *
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]");
1381 }
1382 else {
1383 nodes = this.rootDoc.querySelectorAll(query);
1384 }
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);
1388 }
1389 }
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]);
1399 }
1400 }
1401 break;
1403 case "id":
1404 if (!query) {
1405 nodes = this.rootDoc.querySelectorAll("[id]");
1406 }
1407 else {
1408 nodes = this.rootDoc.querySelectorAll(query);
1409 }
1410 for (let node of nodes) {
1411 if (node.id.startsWith(completing)) {
1412 result.push(["#" + node.id, 1]);
1413 }
1414 }
1415 break;
1417 case "tag":
1418 if (!query) {
1419 nodes = this.rootDoc.getElementsByTagName("*");
1420 }
1421 else {
1422 nodes = this.rootDoc.querySelectorAll(query);
1423 }
1424 for (let node of nodes) {
1425 let tag = node.tagName.toLowerCase();
1426 sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
1427 }
1428 for (let [tag, count] of sugs.tags) {
1429 if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
1430 result.push([tag, count]);
1431 }
1432 }
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);
1443 }
1444 }
1445 for (let [tag, count] of sugs.tags) {
1446 tag && result.push([tag, count]);
1447 }
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]);
1456 }
1457 }
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")
1480 }
1481 }),
1483 /**
1484 * Add a pseudo-class lock to a node.
1485 *
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.
1493 *
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;
1502 }
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);
1509 }
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;
1530 }
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;
1540 }
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);
1548 }
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.
1566 *
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.
1574 *
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;
1583 }
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);
1590 }
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;
1603 }
1604 DOMUtils.removePseudoClassLock(node.rawNode, pseudo);
1605 if (!node.writePseudoClassLocks()) {
1606 this._activePseudoClassLocks.delete(node);
1607 }
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);
1626 }
1627 }
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")
1646 }
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")
1660 }
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;
1680 }
1681 } else if (rawNode.tagName === "HEAD") {
1682 if (parsedDOM.body.innerHTML === "") {
1683 parentNode.replaceChild(parsedDOM.head, rawNode);
1684 } else {
1685 rawNode.outerHTML = value;
1686 }
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;
1694 }
1695 for (let attribute of parsedDOM.documentElement.attributes) {
1696 attributeModifications[attribute.name] = attribute.value;
1697 }
1698 for (let key in attributeModifications) {
1699 finalAttributeModifications.push({
1700 attributeName: key,
1701 newValue: attributeModifications[key]
1702 });
1703 }
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;
1709 }
1710 }, {
1711 request: {
1712 node: Arg(0, "domnode"),
1713 value: Arg(1),
1714 },
1715 response: {
1716 }
1717 }),
1719 /**
1720 * Removes a node from its parent node.
1721 *
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.");
1729 }
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.
1734 }
1735 return nextSibling;
1736 }, {
1737 request: {
1738 node: Arg(0, "domnode")
1739 },
1740 response: {
1741 nextSibling: RetVal("nullable:domnode")
1742 }
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.
1763 *
1764 * Mutation records have a basic structure:
1765 *
1766 * {
1767 * type: attributes|characterData|childList,
1768 * target: <domnode actor ID>,
1769 * }
1770 *
1771 * And additional attributes based on the mutation type:
1772 *
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.
1777 *
1778 * `characterData` type:
1779 * newValue: <string> - the new shortValue for the node
1780 * [incompleteValue: true] - True if the shortValue was truncated.
1781 *
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.
1785 *
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.
1790 *
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'.
1796 *
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);
1811 }
1812 this._orphaned = new Set();
1813 }
1815 return pending;
1816 }, {
1817 request: {
1818 cleanup: Option(0)
1819 },
1820 response: {
1821 mutations: RetVal("array:dommutation")
1822 }
1823 }),
1825 queueMutation: function(mutation) {
1826 if (!this.actorID) {
1827 // We've been destroyed, don't bother queueing this mutation.
1828 return;
1829 }
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");
1838 }
1839 },
1841 /**
1842 * Handles mutations from the DOM mutation observer API.
1843 *
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;
1852 }
1853 let targetNode = change.target;
1854 let mutation = {
1855 type: change.type,
1856 target: targetActor.actorID,
1857 }
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;
1869 }
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;
1881 }
1882 // While removed from the tree, nodes are saved as orphaned.
1883 this._orphaned.add(removedActor);
1884 removedActors.push(removedActor.actorID);
1885 }
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;
1893 }
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);
1899 }
1900 mutation.numChildren = change.target.childNodes.length;
1901 mutation.removed = removedActors;
1902 mutation.added = addedActors;
1903 }
1904 this.queueMutation(mutation);
1905 }
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 });
1916 }
1917 let frame = this.layoutHelpers.getFrameElement(window);
1918 let frameActor = this._refMap.get(frame);
1919 if (!frameActor) {
1920 return;
1921 }
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;
1943 }
1944 win = this.layoutHelpers.getFrameElement(win);
1945 }
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 });
1961 }
1962 }
1964 if (releasedOrphans.length > 0) {
1965 this.queueMutation({
1966 target: this.rootNode.actorID,
1967 type: "unretained",
1968 nodes: releasedOrphans
1969 });
1970 }
1972 let doc = window.document;
1973 let documentActor = this._refMap.get(doc);
1974 if (!documentActor) {
1975 return;
1976 }
1978 if (this.rootDoc === doc) {
1979 this.rootDoc = null;
1980 this.rootNode = null;
1981 }
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 });
1999 }
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;
2019 }
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;
2026 }
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;
2052 }
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;
2058 }
2060 return this.attachElement(rawNode);
2061 }, {
2062 request: {
2063 objectActorID: Arg(0, "string")
2064 },
2065 response: {
2066 nodeFront: RetVal("nullable:disconnectedNode")
2067 }
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;
2139 }
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.
2147 *
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.
2154 *
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.
2158 *
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);
2178 }
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;
2215 }
2217 if (node.retained) {
2218 // Forcing a removal.
2219 this._retainedOrphans.delete(node);
2220 }
2222 // Release any children
2223 for (let child of node.treeChildren()) {
2224 this._releaseFront(child, force);
2225 }
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);
2251 }
2253 if (!targetFront) {
2254 console.trace("Got a mutation for an unexpected actor: " + targetID + ", please file a bug on bugzilla.mozilla.org!");
2255 continue;
2256 }
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;
2269 }
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);
2277 }
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;
2283 }
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);
2290 }
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!");
2303 }
2304 }
2305 } else if (change.type === "documentUnload") {
2306 if (targetFront === this.rootNode) {
2307 this._createRootNodePromise();
2308 }
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);
2324 }
2325 } else {
2326 targetFront.updateMutation(change);
2327 }
2329 emitMutations.push(emittedMutation);
2330 }
2332 if (options.cleanup) {
2333 for (let node of this._orphaned) {
2334 // This will move retained nodes to this._retainedOrphans.
2335 this._releaseFront(node);
2336 }
2337 this._orphaned = new Set();
2338 }
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;
2365 }
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);
2369 }
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);
2379 }
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));
2385 }
2386 return returnNode;
2387 }
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);
2428 }
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;
2447 }
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();
2468 }
2470 return this._walkerPromise;
2471 }, {
2472 request: {},
2473 response: {
2474 walker: RetVal("domwalker")
2475 }
2476 }),
2478 getPageStyle: method(function() {
2479 if (this._pageStylePromise) {
2480 return this._pageStylePromise;
2481 }
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")
2491 }
2492 }),
2494 getHighlighter: method(function (autohide) {
2495 if (this._highlighterPromise) {
2496 return this._highlighterPromise;
2497 }
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")
2507 }
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
2516 *
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"));
2536 }
2537 }
2539 // If the URL doesn't point to a resource, reject
2540 img.onerror = () => {
2541 deferred.reject(new Error("Image " + url+ " not available"));
2542 }
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);
2550 }
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;
2596 }
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);
2608 }
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);
2615 }
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.
2620 *
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;
2630 }
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;
2647 }
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);
2668 }
2669 }
2670 return null;
2671 }
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());
2684 }
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());
2696 }
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;
2713 }
2714 }
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;
2731 }
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;
2740 }
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");
2758 }
2760 return {
2761 data: imageData,
2762 size: {
2763 naturalWidth: imgWidth,
2764 naturalHeight: imgHeight,
2765 resized: resizeRatio !== 1
2766 }
2767 }
2768 }
2770 loader.lazyGetter(this, "DOMUtils", function () {
2771 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
2772 });