Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 "use strict";
9 const {Cc, Ci, Cu, components} = require("chrome");
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
13 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
14 loader.lazyImporter(this, "LayoutHelpers", "resource://gre/modules/devtools/LayoutHelpers.jsm");
16 // TODO: Bug 842672 - toolkit/ imports modules from browser/.
17 // Note that these are only used in JSTermHelpers, see $0 and pprint().
18 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
19 loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
20 loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
21 loader.lazyImporter(this, "DevToolsUtils", "resource://gre/modules/devtools/DevToolsUtils.jsm");
23 // Match the function name from the result of toString() or toSource().
24 //
25 // Examples:
26 // (function foobar(a, b) { ...
27 // function foobar2(a) { ...
28 // function() { ...
29 const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
31 // Match the function arguments from the result of toString() or toSource().
32 const REGEX_MATCH_FUNCTION_ARGS = /^\(?function\s*[^\s(]*\s*\((.+?)\)/;
34 let WebConsoleUtils = {
35 /**
36 * Convenience function to unwrap a wrapped object.
37 *
38 * @param aObject the object to unwrap.
39 * @return aObject unwrapped.
40 */
41 unwrap: function WCU_unwrap(aObject)
42 {
43 try {
44 return XPCNativeWrapper.unwrap(aObject);
45 }
46 catch (ex) {
47 return aObject;
48 }
49 },
51 /**
52 * Wrap a string in an nsISupportsString object.
53 *
54 * @param string aString
55 * @return nsISupportsString
56 */
57 supportsString: function WCU_supportsString(aString)
58 {
59 let str = Cc["@mozilla.org/supports-string;1"].
60 createInstance(Ci.nsISupportsString);
61 str.data = aString;
62 return str;
63 },
65 /**
66 * Clone an object.
67 *
68 * @param object aObject
69 * The object you want cloned.
70 * @param boolean aRecursive
71 * Tells if you want to dig deeper into the object, to clone
72 * recursively.
73 * @param function [aFilter]
74 * Optional, filter function, called for every property. Three
75 * arguments are passed: key, value and object. Return true if the
76 * property should be added to the cloned object. Return false to skip
77 * the property.
78 * @return object
79 * The cloned object.
80 */
81 cloneObject: function WCU_cloneObject(aObject, aRecursive, aFilter)
82 {
83 if (typeof aObject != "object") {
84 return aObject;
85 }
87 let temp;
89 if (Array.isArray(aObject)) {
90 temp = [];
91 Array.forEach(aObject, function(aValue, aIndex) {
92 if (!aFilter || aFilter(aIndex, aValue, aObject)) {
93 temp.push(aRecursive ? WCU_cloneObject(aValue) : aValue);
94 }
95 });
96 }
97 else {
98 temp = {};
99 for (let key in aObject) {
100 let value = aObject[key];
101 if (aObject.hasOwnProperty(key) &&
102 (!aFilter || aFilter(key, value, aObject))) {
103 temp[key] = aRecursive ? WCU_cloneObject(value) : value;
104 }
105 }
106 }
108 return temp;
109 },
111 /**
112 * Copies certain style attributes from one element to another.
113 *
114 * @param nsIDOMNode aFrom
115 * The target node.
116 * @param nsIDOMNode aTo
117 * The destination node.
118 */
119 copyTextStyles: function WCU_copyTextStyles(aFrom, aTo)
120 {
121 let win = aFrom.ownerDocument.defaultView;
122 let style = win.getComputedStyle(aFrom);
123 aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
124 aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
125 aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
126 aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
127 },
129 /**
130 * Gets the ID of the inner window of this DOM window.
131 *
132 * @param nsIDOMWindow aWindow
133 * @return integer
134 * Inner ID for the given aWindow.
135 */
136 getInnerWindowId: function WCU_getInnerWindowId(aWindow)
137 {
138 return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
139 getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
140 },
142 /**
143 * Recursively gather a list of inner window ids given a
144 * top level window.
145 *
146 * @param nsIDOMWindow aWindow
147 * @return Array
148 * list of inner window ids.
149 */
150 getInnerWindowIDsForFrames: function WCU_getInnerWindowIDsForFrames(aWindow)
151 {
152 let innerWindowID = this.getInnerWindowId(aWindow);
153 let ids = [innerWindowID];
155 if (aWindow.frames) {
156 for (let i = 0; i < aWindow.frames.length; i++) {
157 let frame = aWindow.frames[i];
158 ids = ids.concat(this.getInnerWindowIDsForFrames(frame));
159 }
160 }
162 return ids;
163 },
166 /**
167 * Gets the ID of the outer window of this DOM window.
168 *
169 * @param nsIDOMWindow aWindow
170 * @return integer
171 * Outer ID for the given aWindow.
172 */
173 getOuterWindowId: function WCU_getOuterWindowId(aWindow)
174 {
175 return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
176 getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
177 },
179 /**
180 * Abbreviates the given source URL so that it can be displayed flush-right
181 * without being too distracting.
182 *
183 * @param string aSourceURL
184 * The source URL to shorten.
185 * @param object [aOptions]
186 * Options:
187 * - onlyCropQuery: boolean that tells if the URL abbreviation function
188 * should only remove the query parameters and the hash fragment from
189 * the given URL.
190 * @return string
191 * The abbreviated form of the source URL.
192 */
193 abbreviateSourceURL:
194 function WCU_abbreviateSourceURL(aSourceURL, aOptions = {})
195 {
196 if (!aOptions.onlyCropQuery && aSourceURL.substr(0, 5) == "data:") {
197 let commaIndex = aSourceURL.indexOf(",");
198 if (commaIndex > -1) {
199 aSourceURL = "data:" + aSourceURL.substring(commaIndex + 1);
200 }
201 }
203 // Remove any query parameters.
204 let hookIndex = aSourceURL.indexOf("?");
205 if (hookIndex > -1) {
206 aSourceURL = aSourceURL.substring(0, hookIndex);
207 }
209 // Remove any hash fragments.
210 let hashIndex = aSourceURL.indexOf("#");
211 if (hashIndex > -1) {
212 aSourceURL = aSourceURL.substring(0, hashIndex);
213 }
215 // Remove a trailing "/".
216 if (aSourceURL[aSourceURL.length - 1] == "/") {
217 aSourceURL = aSourceURL.replace(/\/+$/, "");
218 }
220 // Remove all but the last path component.
221 if (!aOptions.onlyCropQuery) {
222 let slashIndex = aSourceURL.lastIndexOf("/");
223 if (slashIndex > -1) {
224 aSourceURL = aSourceURL.substring(slashIndex + 1);
225 }
226 }
228 return aSourceURL;
229 },
231 /**
232 * Tells if the given function is native or not.
233 *
234 * @param function aFunction
235 * The function you want to check if it is native or not.
236 * @return boolean
237 * True if the given function is native, false otherwise.
238 */
239 isNativeFunction: function WCU_isNativeFunction(aFunction)
240 {
241 return typeof aFunction == "function" && !("prototype" in aFunction);
242 },
244 /**
245 * Tells if the given property of the provided object is a non-native getter or
246 * not.
247 *
248 * @param object aObject
249 * The object that contains the property.
250 * @param string aProp
251 * The property you want to check if it is a getter or not.
252 * @return boolean
253 * True if the given property is a getter, false otherwise.
254 */
255 isNonNativeGetter: function WCU_isNonNativeGetter(aObject, aProp)
256 {
257 if (typeof aObject != "object") {
258 return false;
259 }
260 let desc = this.getPropertyDescriptor(aObject, aProp);
261 return desc && desc.get && !this.isNativeFunction(desc.get);
262 },
264 /**
265 * Get the property descriptor for the given object.
266 *
267 * @param object aObject
268 * The object that contains the property.
269 * @param string aProp
270 * The property you want to get the descriptor for.
271 * @return object
272 * Property descriptor.
273 */
274 getPropertyDescriptor: function WCU_getPropertyDescriptor(aObject, aProp)
275 {
276 let desc = null;
277 while (aObject) {
278 try {
279 if ((desc = Object.getOwnPropertyDescriptor(aObject, aProp))) {
280 break;
281 }
282 }
283 catch (ex if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" ||
284 ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO" ||
285 ex.name == "TypeError")) {
286 // Native getters throw here. See bug 520882.
287 // null throws TypeError.
288 }
289 try {
290 aObject = Object.getPrototypeOf(aObject);
291 }
292 catch (ex if (ex.name == "TypeError")) {
293 return desc;
294 }
295 }
296 return desc;
297 },
299 /**
300 * Sort function for object properties.
301 *
302 * @param object a
303 * Property descriptor.
304 * @param object b
305 * Property descriptor.
306 * @return integer
307 * -1 if a.name < b.name,
308 * 1 if a.name > b.name,
309 * 0 otherwise.
310 */
311 propertiesSort: function WCU_propertiesSort(a, b)
312 {
313 // Convert the pair.name to a number for later sorting.
314 let aNumber = parseFloat(a.name);
315 let bNumber = parseFloat(b.name);
317 // Sort numbers.
318 if (!isNaN(aNumber) && isNaN(bNumber)) {
319 return -1;
320 }
321 else if (isNaN(aNumber) && !isNaN(bNumber)) {
322 return 1;
323 }
324 else if (!isNaN(aNumber) && !isNaN(bNumber)) {
325 return aNumber - bNumber;
326 }
327 // Sort string.
328 else if (a.name < b.name) {
329 return -1;
330 }
331 else if (a.name > b.name) {
332 return 1;
333 }
334 else {
335 return 0;
336 }
337 },
339 /**
340 * Create a grip for the given value. If the value is an object,
341 * an object wrapper will be created.
342 *
343 * @param mixed aValue
344 * The value you want to create a grip for, before sending it to the
345 * client.
346 * @param function aObjectWrapper
347 * If the value is an object then the aObjectWrapper function is
348 * invoked to give us an object grip. See this.getObjectGrip().
349 * @return mixed
350 * The value grip.
351 */
352 createValueGrip: function WCU_createValueGrip(aValue, aObjectWrapper)
353 {
354 switch (typeof aValue) {
355 case "boolean":
356 return aValue;
357 case "string":
358 return aObjectWrapper(aValue);
359 case "number":
360 if (aValue === Infinity) {
361 return { type: "Infinity" };
362 }
363 else if (aValue === -Infinity) {
364 return { type: "-Infinity" };
365 }
366 else if (Number.isNaN(aValue)) {
367 return { type: "NaN" };
368 }
369 else if (!aValue && 1 / aValue === -Infinity) {
370 return { type: "-0" };
371 }
372 return aValue;
373 case "undefined":
374 return { type: "undefined" };
375 case "object":
376 if (aValue === null) {
377 return { type: "null" };
378 }
379 case "function":
380 return aObjectWrapper(aValue);
381 default:
382 Cu.reportError("Failed to provide a grip for value of " + typeof aValue
383 + ": " + aValue);
384 return null;
385 }
386 },
388 /**
389 * Check if the given object is an iterator or a generator.
390 *
391 * @param object aObject
392 * The object you want to check.
393 * @return boolean
394 * True if the given object is an iterator or a generator, otherwise
395 * false is returned.
396 */
397 isIteratorOrGenerator: function WCU_isIteratorOrGenerator(aObject)
398 {
399 if (aObject === null) {
400 return false;
401 }
403 if (typeof aObject == "object") {
404 if (typeof aObject.__iterator__ == "function" ||
405 aObject.constructor && aObject.constructor.name == "Iterator") {
406 return true;
407 }
409 try {
410 let str = aObject.toString();
411 if (typeof aObject.next == "function" &&
412 str.indexOf("[object Generator") == 0) {
413 return true;
414 }
415 }
416 catch (ex) {
417 // window.history.next throws in the typeof check above.
418 return false;
419 }
420 }
422 return false;
423 },
425 /**
426 * Determine if the given request mixes HTTP with HTTPS content.
427 *
428 * @param string aRequest
429 * Location of the requested content.
430 * @param string aLocation
431 * Location of the current page.
432 * @return boolean
433 * True if the content is mixed, false if not.
434 */
435 isMixedHTTPSRequest: function WCU_isMixedHTTPSRequest(aRequest, aLocation)
436 {
437 try {
438 let requestURI = Services.io.newURI(aRequest, null, null);
439 let contentURI = Services.io.newURI(aLocation, null, null);
440 return (contentURI.scheme == "https" && requestURI.scheme != "https");
441 }
442 catch (ex) {
443 return false;
444 }
445 },
447 /**
448 * Helper function to deduce the name of the provided function.
449 *
450 * @param funtion aFunction
451 * The function whose name will be returned.
452 * @return string
453 * Function name.
454 */
455 getFunctionName: function WCF_getFunctionName(aFunction)
456 {
457 let name = null;
458 if (aFunction.name) {
459 name = aFunction.name;
460 }
461 else {
462 let desc;
463 try {
464 desc = aFunction.getOwnPropertyDescriptor("displayName");
465 }
466 catch (ex) { }
467 if (desc && typeof desc.value == "string") {
468 name = desc.value;
469 }
470 }
471 if (!name) {
472 try {
473 let str = (aFunction.toString() || aFunction.toSource()) + "";
474 name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1];
475 }
476 catch (ex) { }
477 }
478 return name;
479 },
481 /**
482 * Get the object class name. For example, the |window| object has the Window
483 * class name (based on [object Window]).
484 *
485 * @param object aObject
486 * The object you want to get the class name for.
487 * @return string
488 * The object class name.
489 */
490 getObjectClassName: function WCU_getObjectClassName(aObject)
491 {
492 if (aObject === null) {
493 return "null";
494 }
495 if (aObject === undefined) {
496 return "undefined";
497 }
499 let type = typeof aObject;
500 if (type != "object") {
501 // Grip class names should start with an uppercase letter.
502 return type.charAt(0).toUpperCase() + type.substr(1);
503 }
505 let className;
507 try {
508 className = ((aObject + "").match(/^\[object (\S+)\]$/) || [])[1];
509 if (!className) {
510 className = ((aObject.constructor + "").match(/^\[object (\S+)\]$/) || [])[1];
511 }
512 if (!className && typeof aObject.constructor == "function") {
513 className = this.getFunctionName(aObject.constructor);
514 }
515 }
516 catch (ex) { }
518 return className;
519 },
521 /**
522 * Check if the given value is a grip with an actor.
523 *
524 * @param mixed aGrip
525 * Value you want to check if it is a grip with an actor.
526 * @return boolean
527 * True if the given value is a grip with an actor.
528 */
529 isActorGrip: function WCU_isActorGrip(aGrip)
530 {
531 return aGrip && typeof(aGrip) == "object" && aGrip.actor;
532 },
533 };
534 exports.Utils = WebConsoleUtils;
536 //////////////////////////////////////////////////////////////////////////
537 // Localization
538 //////////////////////////////////////////////////////////////////////////
540 WebConsoleUtils.l10n = function WCU_l10n(aBundleURI)
541 {
542 this._bundleUri = aBundleURI;
543 };
545 WebConsoleUtils.l10n.prototype = {
546 _stringBundle: null,
548 get stringBundle()
549 {
550 if (!this._stringBundle) {
551 this._stringBundle = Services.strings.createBundle(this._bundleUri);
552 }
553 return this._stringBundle;
554 },
556 /**
557 * Generates a formatted timestamp string for displaying in console messages.
558 *
559 * @param integer [aMilliseconds]
560 * Optional, allows you to specify the timestamp in milliseconds since
561 * the UNIX epoch.
562 * @return string
563 * The timestamp formatted for display.
564 */
565 timestampString: function WCU_l10n_timestampString(aMilliseconds)
566 {
567 let d = new Date(aMilliseconds ? aMilliseconds : null);
568 let hours = d.getHours(), minutes = d.getMinutes();
569 let seconds = d.getSeconds(), milliseconds = d.getMilliseconds();
570 let parameters = [hours, minutes, seconds, milliseconds];
571 return this.getFormatStr("timestampFormat", parameters);
572 },
574 /**
575 * Retrieve a localized string.
576 *
577 * @param string aName
578 * The string name you want from the Web Console string bundle.
579 * @return string
580 * The localized string.
581 */
582 getStr: function WCU_l10n_getStr(aName)
583 {
584 let result;
585 try {
586 result = this.stringBundle.GetStringFromName(aName);
587 }
588 catch (ex) {
589 Cu.reportError("Failed to get string: " + aName);
590 throw ex;
591 }
592 return result;
593 },
595 /**
596 * Retrieve a localized string formatted with values coming from the given
597 * array.
598 *
599 * @param string aName
600 * The string name you want from the Web Console string bundle.
601 * @param array aArray
602 * The array of values you want in the formatted string.
603 * @return string
604 * The formatted local string.
605 */
606 getFormatStr: function WCU_l10n_getFormatStr(aName, aArray)
607 {
608 let result;
609 try {
610 result = this.stringBundle.formatStringFromName(aName, aArray, aArray.length);
611 }
612 catch (ex) {
613 Cu.reportError("Failed to format string: " + aName);
614 throw ex;
615 }
616 return result;
617 },
618 };
621 //////////////////////////////////////////////////////////////////////////
622 // JS Completer
623 //////////////////////////////////////////////////////////////////////////
625 (function _JSPP(WCU) {
626 const STATE_NORMAL = 0;
627 const STATE_QUOTE = 2;
628 const STATE_DQUOTE = 3;
630 const OPEN_BODY = "{[(".split("");
631 const CLOSE_BODY = "}])".split("");
632 const OPEN_CLOSE_BODY = {
633 "{": "}",
634 "[": "]",
635 "(": ")",
636 };
638 const MAX_COMPLETIONS = 1500;
640 /**
641 * Analyses a given string to find the last statement that is interesting for
642 * later completion.
643 *
644 * @param string aStr
645 * A string to analyse.
646 *
647 * @returns object
648 * If there was an error in the string detected, then a object like
649 *
650 * { err: "ErrorMesssage" }
651 *
652 * is returned, otherwise a object like
653 *
654 * {
655 * state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE,
656 * startPos: index of where the last statement begins
657 * }
658 */
659 function findCompletionBeginning(aStr)
660 {
661 let bodyStack = [];
663 let state = STATE_NORMAL;
664 let start = 0;
665 let c;
666 for (let i = 0; i < aStr.length; i++) {
667 c = aStr[i];
669 switch (state) {
670 // Normal JS state.
671 case STATE_NORMAL:
672 if (c == '"') {
673 state = STATE_DQUOTE;
674 }
675 else if (c == "'") {
676 state = STATE_QUOTE;
677 }
678 else if (c == ";") {
679 start = i + 1;
680 }
681 else if (c == " ") {
682 start = i + 1;
683 }
684 else if (OPEN_BODY.indexOf(c) != -1) {
685 bodyStack.push({
686 token: c,
687 start: start
688 });
689 start = i + 1;
690 }
691 else if (CLOSE_BODY.indexOf(c) != -1) {
692 var last = bodyStack.pop();
693 if (!last || OPEN_CLOSE_BODY[last.token] != c) {
694 return {
695 err: "syntax error"
696 };
697 }
698 if (c == "}") {
699 start = i + 1;
700 }
701 else {
702 start = last.start;
703 }
704 }
705 break;
707 // Double quote state > " <
708 case STATE_DQUOTE:
709 if (c == "\\") {
710 i++;
711 }
712 else if (c == "\n") {
713 return {
714 err: "unterminated string literal"
715 };
716 }
717 else if (c == '"') {
718 state = STATE_NORMAL;
719 }
720 break;
722 // Single quote state > ' <
723 case STATE_QUOTE:
724 if (c == "\\") {
725 i++;
726 }
727 else if (c == "\n") {
728 return {
729 err: "unterminated string literal"
730 };
731 }
732 else if (c == "'") {
733 state = STATE_NORMAL;
734 }
735 break;
736 }
737 }
739 return {
740 state: state,
741 startPos: start
742 };
743 }
745 /**
746 * Provides a list of properties, that are possible matches based on the passed
747 * Debugger.Environment/Debugger.Object and inputValue.
748 *
749 * @param object aDbgObject
750 * When the debugger is not paused this Debugger.Object wraps the scope for autocompletion.
751 * It is null if the debugger is paused.
752 * @param object anEnvironment
753 * When the debugger is paused this Debugger.Environment is the scope for autocompletion.
754 * It is null if the debugger is not paused.
755 * @param string aInputValue
756 * Value that should be completed.
757 * @param number [aCursor=aInputValue.length]
758 * Optional offset in the input where the cursor is located. If this is
759 * omitted then the cursor is assumed to be at the end of the input
760 * value.
761 * @returns null or object
762 * If no completion valued could be computed, null is returned,
763 * otherwise a object with the following form is returned:
764 * {
765 * matches: [ string, string, string ],
766 * matchProp: Last part of the inputValue that was used to find
767 * the matches-strings.
768 * }
769 */
770 function JSPropertyProvider(aDbgObject, anEnvironment, aInputValue, aCursor)
771 {
772 if (aCursor === undefined) {
773 aCursor = aInputValue.length;
774 }
776 let inputValue = aInputValue.substring(0, aCursor);
778 // Analyse the inputValue and find the beginning of the last part that
779 // should be completed.
780 let beginning = findCompletionBeginning(inputValue);
782 // There was an error analysing the string.
783 if (beginning.err) {
784 return null;
785 }
787 // If the current state is not STATE_NORMAL, then we are inside of an string
788 // which means that no completion is possible.
789 if (beginning.state != STATE_NORMAL) {
790 return null;
791 }
793 let completionPart = inputValue.substring(beginning.startPos);
795 // Don't complete on just an empty string.
796 if (completionPart.trim() == "") {
797 return null;
798 }
800 let lastDot = completionPart.lastIndexOf(".");
801 if (lastDot > 0 &&
802 (completionPart[0] == "'" || completionPart[0] == '"') &&
803 completionPart[lastDot - 1] == completionPart[0]) {
804 // We are completing a string literal.
805 let matchProp = completionPart.slice(lastDot + 1);
806 return getMatchedProps(String.prototype, matchProp);
807 }
809 // We are completing a variable / a property lookup.
810 let properties = completionPart.split(".");
811 let matchProp = properties.pop().trimLeft();
812 let obj = aDbgObject;
814 // The first property must be found in the environment if the debugger is
815 // paused.
816 if (anEnvironment) {
817 if (properties.length == 0) {
818 return getMatchedPropsInEnvironment(anEnvironment, matchProp);
819 }
820 obj = getVariableInEnvironment(anEnvironment, properties.shift());
821 }
823 if (!isObjectUsable(obj)) {
824 return null;
825 }
827 // We get the rest of the properties recursively starting from the Debugger.Object
828 // that wraps the first property
829 for (let prop of properties) {
830 prop = prop.trim();
831 if (!prop) {
832 return null;
833 }
835 if (/\[\d+\]$/.test(prop))Â {
836 // The property to autocomplete is a member of array. For example
837 // list[i][j]..[n]. Traverse the array to get the actual element.
838 obj = getArrayMemberProperty(obj, prop);
839 }
840 else {
841 obj = DevToolsUtils.getProperty(obj, prop);
842 }
844 if (!isObjectUsable(obj)) {
845 return null;
846 }
847 }
849 // If the final property is a primitive
850 if (typeof obj != "object") {
851 return getMatchedProps(obj, matchProp);
852 }
854 return getMatchedPropsInDbgObject(obj, matchProp);
855 }
857 /**
858 * Get the array member of aObj for the given aProp. For example, given
859 * aProp='list[0][1]' the element at [0][1] of aObj.list is returned.
860 *
861 * @param object aObj
862 * The object to operate on.
863 * @param string aProp
864 * The property to return.
865 * @return null or Object
866 * Returns null if the property couldn't be located. Otherwise the array
867 * member identified by aProp.
868 */
869 function getArrayMemberProperty(aObj, aProp)
870 {
871 // First get the array.
872 let obj = aObj;
873 let propWithoutIndices = aProp.substr(0, aProp.indexOf("["));
874 obj = DevToolsUtils.getProperty(obj, propWithoutIndices);
875 if (!isObjectUsable(obj)) {
876 return null;
877 }
879 // Then traverse the list of indices to get the actual element.
880 let result;
881 let arrayIndicesRegex = /\[[^\]]*\]/g;
882 while ((result = arrayIndicesRegex.exec(aProp)) !== null) {
883 let indexWithBrackets = result[0];
884 let indexAsText = indexWithBrackets.substr(1, indexWithBrackets.length - 2);
885 let index = parseInt(indexAsText);
887 if (isNaN(index)) {
888 return null;
889 }
891 obj = DevToolsUtils.getProperty(obj, index);
893 if (!isObjectUsable(obj)) {
894 return null;
895 }
896 }
898 return obj;
899 }
901 /**
902 * Check if the given Debugger.Object can be used for autocomplete.
903 *
904 * @param Debugger.Object aObject
905 * The Debugger.Object to check.
906 * @return boolean
907 * True if further inspection into the object is possible, or false
908 * otherwise.
909 */
910 function isObjectUsable(aObject)
911 {
912 if (aObject == null) {
913 return false;
914 }
916 if (typeof aObject == "object" && aObject.class == "DeadObject") {
917 return false;
918 }
920 return true;
921 }
923 /**
924 * @see getExactMatch_impl()
925 */
926 function getVariableInEnvironment(anEnvironment, aName)
927 {
928 return getExactMatch_impl(anEnvironment, aName, DebuggerEnvironmentSupport);
929 }
931 /**
932 * @see getMatchedProps_impl()
933 */
934 function getMatchedPropsInEnvironment(anEnvironment, aMatch)
935 {
936 return getMatchedProps_impl(anEnvironment, aMatch, DebuggerEnvironmentSupport);
937 }
939 /**
940 * @see getMatchedProps_impl()
941 */
942 function getMatchedPropsInDbgObject(aDbgObject, aMatch)
943 {
944 return getMatchedProps_impl(aDbgObject, aMatch, DebuggerObjectSupport);
945 }
947 /**
948 * @see getMatchedProps_impl()
949 */
950 function getMatchedProps(aObj, aMatch)
951 {
952 if (typeof aObj != "object") {
953 aObj = aObj.constructor.prototype;
954 }
955 return getMatchedProps_impl(aObj, aMatch, JSObjectSupport);
956 }
958 /**
959 * Get all properties in the given object (and its parent prototype chain) that
960 * match a given prefix.
961 *
962 * @param mixed aObj
963 * Object whose properties we want to filter.
964 * @param string aMatch
965 * Filter for properties that match this string.
966 * @return object
967 * Object that contains the matchProp and the list of names.
968 */
969 function getMatchedProps_impl(aObj, aMatch, {chainIterator, getProperties})
970 {
971 let matches = new Set();
973 // We need to go up the prototype chain.
974 let iter = chainIterator(aObj);
975 for (let obj of iter) {
976 let props = getProperties(obj);
977 for (let prop of props) {
978 if (prop.indexOf(aMatch) != 0) {
979 continue;
980 }
982 // If it is an array index, we can't take it.
983 // This uses a trick: converting a string to a number yields NaN if
984 // the operation failed, and NaN is not equal to itself.
985 if (+prop != +prop) {
986 matches.add(prop);
987 }
989 if (matches.size > MAX_COMPLETIONS) {
990 break;
991 }
992 }
994 if (matches.size > MAX_COMPLETIONS) {
995 break;
996 }
997 }
999 return {
1000 matchProp: aMatch,
1001 matches: [...matches],
1002 };
1003 }
1005 /**
1006 * Returns a property value based on its name from the given object, by
1007 * recursively checking the object's prototype.
1008 *
1009 * @param object aObj
1010 * An object to look the property into.
1011 * @param string aName
1012 * The property that is looked up.
1013 * @returns object|undefined
1014 * A Debugger.Object if the property exists in the object's prototype
1015 * chain, undefined otherwise.
1016 */
1017 function getExactMatch_impl(aObj, aName, {chainIterator, getProperty})
1018 {
1019 // We need to go up the prototype chain.
1020 let iter = chainIterator(aObj);
1021 for (let obj of iter) {
1022 let prop = getProperty(obj, aName, aObj);
1023 if (prop) {
1024 return prop.value;
1025 }
1026 }
1027 return undefined;
1028 }
1031 let JSObjectSupport = {
1032 chainIterator: function(aObj)
1033 {
1034 while (aObj) {
1035 yield aObj;
1036 aObj = Object.getPrototypeOf(aObj);
1037 }
1038 },
1040 getProperties: function(aObj)
1041 {
1042 return Object.getOwnPropertyNames(aObj);
1043 },
1045 getProperty: function()
1046 {
1047 // getProperty is unsafe with raw JS objects.
1048 throw "Unimplemented!";
1049 },
1050 };
1052 let DebuggerObjectSupport = {
1053 chainIterator: function(aObj)
1054 {
1055 while (aObj) {
1056 yield aObj;
1057 aObj = aObj.proto;
1058 }
1059 },
1061 getProperties: function(aObj)
1062 {
1063 return aObj.getOwnPropertyNames();
1064 },
1066 getProperty: function(aObj, aName, aRootObj)
1067 {
1068 // This is left unimplemented in favor to DevToolsUtils.getProperty().
1069 throw "Unimplemented!";
1070 },
1071 };
1073 let DebuggerEnvironmentSupport = {
1074 chainIterator: function(aObj)
1075 {
1076 while (aObj) {
1077 yield aObj;
1078 aObj = aObj.parent;
1079 }
1080 },
1082 getProperties: function(aObj)
1083 {
1084 return aObj.names();
1085 },
1087 getProperty: function(aObj, aName)
1088 {
1089 // TODO: we should use getVariableDescriptor() here - bug 725815.
1090 let result = aObj.getVariable(aName);
1091 // FIXME: Need actual UI, bug 941287.
1092 if (result.optimizedOut || result.missingArguments) {
1093 return null;
1094 }
1095 return result === undefined ? null : { value: result };
1096 },
1097 };
1100 exports.JSPropertyProvider = DevToolsUtils.makeInfallible(JSPropertyProvider);
1101 })(WebConsoleUtils);
1103 ///////////////////////////////////////////////////////////////////////////////
1104 // The page errors listener
1105 ///////////////////////////////////////////////////////////////////////////////
1107 /**
1108 * The nsIConsoleService listener. This is used to send all of the console
1109 * messages (JavaScript, CSS and more) to the remote Web Console instance.
1110 *
1111 * @constructor
1112 * @param nsIDOMWindow [aWindow]
1113 * Optional - the window object for which we are created. This is used
1114 * for filtering out messages that belong to other windows.
1115 * @param object aListener
1116 * The listener object must have one method:
1117 * - onConsoleServiceMessage(). This method is invoked with one argument,
1118 * the nsIConsoleMessage, whenever a relevant message is received.
1119 */
1120 function ConsoleServiceListener(aWindow, aListener)
1121 {
1122 this.window = aWindow;
1123 this.listener = aListener;
1124 if (this.window) {
1125 this.layoutHelpers = new LayoutHelpers(this.window);
1126 }
1127 }
1128 exports.ConsoleServiceListener = ConsoleServiceListener;
1130 ConsoleServiceListener.prototype =
1131 {
1132 QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
1134 /**
1135 * The content window for which we listen to page errors.
1136 * @type nsIDOMWindow
1137 */
1138 window: null,
1140 /**
1141 * The listener object which is notified of messages from the console service.
1142 * @type object
1143 */
1144 listener: null,
1146 /**
1147 * Initialize the nsIConsoleService listener.
1148 */
1149 init: function CSL_init()
1150 {
1151 Services.console.registerListener(this);
1152 },
1154 /**
1155 * The nsIConsoleService observer. This method takes all the script error
1156 * messages belonging to the current window and sends them to the remote Web
1157 * Console instance.
1158 *
1159 * @param nsIConsoleMessage aMessage
1160 * The message object coming from the nsIConsoleService.
1161 */
1162 observe: function CSL_observe(aMessage)
1163 {
1164 if (!this.listener) {
1165 return;
1166 }
1168 if (this.window) {
1169 if (!(aMessage instanceof Ci.nsIScriptError) ||
1170 !aMessage.outerWindowID ||
1171 !this.isCategoryAllowed(aMessage.category)) {
1172 return;
1173 }
1175 let errorWindow = Services.wm.getOuterWindowWithId(aMessage.outerWindowID);
1176 if (!errorWindow || !this.layoutHelpers.isIncludedInTopLevelWindow(errorWindow)) {
1177 return;
1178 }
1179 }
1181 this.listener.onConsoleServiceMessage(aMessage);
1182 },
1184 /**
1185 * Check if the given message category is allowed to be tracked or not.
1186 * We ignore chrome-originating errors as we only care about content.
1187 *
1188 * @param string aCategory
1189 * The message category you want to check.
1190 * @return boolean
1191 * True if the category is allowed to be logged, false otherwise.
1192 */
1193 isCategoryAllowed: function CSL_isCategoryAllowed(aCategory)
1194 {
1195 if (!aCategory) {
1196 return false;
1197 }
1199 switch (aCategory) {
1200 case "XPConnect JavaScript":
1201 case "component javascript":
1202 case "chrome javascript":
1203 case "chrome registration":
1204 case "XBL":
1205 case "XBL Prototype Handler":
1206 case "XBL Content Sink":
1207 case "xbl javascript":
1208 return false;
1209 }
1211 return true;
1212 },
1214 /**
1215 * Get the cached page errors for the current inner window and its (i)frames.
1216 *
1217 * @param boolean [aIncludePrivate=false]
1218 * Tells if you want to also retrieve messages coming from private
1219 * windows. Defaults to false.
1220 * @return array
1221 * The array of cached messages. Each element is an nsIScriptError or
1222 * an nsIConsoleMessage
1223 */
1224 getCachedMessages: function CSL_getCachedMessages(aIncludePrivate = false)
1225 {
1226 let errors = Services.console.getMessageArray() || [];
1228 // if !this.window, we're in a browser console. Still need to filter
1229 // private messages.
1230 if (!this.window) {
1231 return errors.filter((aError) => {
1232 if (aError instanceof Ci.nsIScriptError) {
1233 if (!aIncludePrivate && aError.isFromPrivateWindow) {
1234 return false;
1235 }
1236 }
1238 return true;
1239 });
1240 }
1242 let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window);
1244 return errors.filter((aError) => {
1245 if (aError instanceof Ci.nsIScriptError) {
1246 if (!aIncludePrivate && aError.isFromPrivateWindow) {
1247 return false;
1248 }
1249 if (ids &&
1250 (ids.indexOf(aError.innerWindowID) == -1 ||
1251 !this.isCategoryAllowed(aError.category))) {
1252 return false;
1253 }
1254 }
1255 else if (ids && ids[0]) {
1256 // If this is not an nsIScriptError and we need to do window-based
1257 // filtering we skip this message.
1258 return false;
1259 }
1261 return true;
1262 });
1263 },
1265 /**
1266 * Remove the nsIConsoleService listener.
1267 */
1268 destroy: function CSL_destroy()
1269 {
1270 Services.console.unregisterListener(this);
1271 this.listener = this.window = null;
1272 },
1273 };
1276 ///////////////////////////////////////////////////////////////////////////////
1277 // The window.console API observer
1278 ///////////////////////////////////////////////////////////////////////////////
1280 /**
1281 * The window.console API observer. This allows the window.console API messages
1282 * to be sent to the remote Web Console instance.
1283 *
1284 * @constructor
1285 * @param nsIDOMWindow aWindow
1286 * Optional - the window object for which we are created. This is used
1287 * for filtering out messages that belong to other windows.
1288 * @param object aOwner
1289 * The owner object must have the following methods:
1290 * - onConsoleAPICall(). This method is invoked with one argument, the
1291 * Console API message that comes from the observer service, whenever
1292 * a relevant console API call is received.
1293 */
1294 function ConsoleAPIListener(aWindow, aOwner)
1295 {
1296 this.window = aWindow;
1297 this.owner = aOwner;
1298 if (this.window) {
1299 this.layoutHelpers = new LayoutHelpers(this.window);
1300 }
1301 }
1302 exports.ConsoleAPIListener = ConsoleAPIListener;
1304 ConsoleAPIListener.prototype =
1305 {
1306 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
1308 /**
1309 * The content window for which we listen to window.console API calls.
1310 * @type nsIDOMWindow
1311 */
1312 window: null,
1314 /**
1315 * The owner object which is notified of window.console API calls. It must
1316 * have a onConsoleAPICall method which is invoked with one argument: the
1317 * console API call object that comes from the observer service.
1318 *
1319 * @type object
1320 * @see WebConsoleActor
1321 */
1322 owner: null,
1324 /**
1325 * Initialize the window.console API observer.
1326 */
1327 init: function CAL_init()
1328 {
1329 // Note that the observer is process-wide. We will filter the messages as
1330 // needed, see CAL_observe().
1331 Services.obs.addObserver(this, "console-api-log-event", false);
1332 },
1334 /**
1335 * The console API message observer. When messages are received from the
1336 * observer service we forward them to the remote Web Console instance.
1337 *
1338 * @param object aMessage
1339 * The message object receives from the observer service.
1340 * @param string aTopic
1341 * The message topic received from the observer service.
1342 */
1343 observe: function CAL_observe(aMessage, aTopic)
1344 {
1345 if (!this.owner) {
1346 return;
1347 }
1349 let apiMessage = aMessage.wrappedJSObject;
1350 if (this.window) {
1351 let msgWindow = Services.wm.getCurrentInnerWindowWithId(apiMessage.innerID);
1352 if (!msgWindow || !this.layoutHelpers.isIncludedInTopLevelWindow(msgWindow)) {
1353 // Not the same window!
1354 return;
1355 }
1356 }
1358 this.owner.onConsoleAPICall(apiMessage);
1359 },
1361 /**
1362 * Get the cached messages for the current inner window and its (i)frames.
1363 *
1364 * @param boolean [aIncludePrivate=false]
1365 * Tells if you want to also retrieve messages coming from private
1366 * windows. Defaults to false.
1367 * @return array
1368 * The array of cached messages.
1369 */
1370 getCachedMessages: function CAL_getCachedMessages(aIncludePrivate = false)
1371 {
1372 let messages = [];
1373 let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
1374 .getService(Ci.nsIConsoleAPIStorage);
1376 // if !this.window, we're in a browser console. Retrieve all events
1377 // for filtering based on privacy.
1378 if (!this.window) {
1379 messages = ConsoleAPIStorage.getEvents();
1380 } else {
1381 let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window);
1382 ids.forEach((id) => {
1383 messages = messages.concat(ConsoleAPIStorage.getEvents(id));
1384 });
1385 }
1387 if (aIncludePrivate) {
1388 return messages;
1389 }
1391 return messages.filter((m) => !m.private);
1392 },
1394 /**
1395 * Destroy the console API listener.
1396 */
1397 destroy: function CAL_destroy()
1398 {
1399 Services.obs.removeObserver(this, "console-api-log-event");
1400 this.window = this.owner = null;
1401 },
1402 };
1406 /**
1407 * JSTerm helper functions.
1408 *
1409 * Defines a set of functions ("helper functions") that are available from the
1410 * Web Console but not from the web page.
1411 *
1412 * A list of helper functions used by Firebug can be found here:
1413 * http://getfirebug.com/wiki/index.php/Command_Line_API
1414 *
1415 * @param object aOwner
1416 * The owning object.
1417 */
1418 function JSTermHelpers(aOwner)
1419 {
1420 /**
1421 * Find a node by ID.
1422 *
1423 * @param string aId
1424 * The ID of the element you want.
1425 * @return nsIDOMNode or null
1426 * The result of calling document.querySelector(aSelector).
1427 */
1428 aOwner.sandbox.$ = function JSTH_$(aSelector)
1429 {
1430 return aOwner.window.document.querySelector(aSelector);
1431 };
1433 /**
1434 * Find the nodes matching a CSS selector.
1435 *
1436 * @param string aSelector
1437 * A string that is passed to window.document.querySelectorAll.
1438 * @return nsIDOMNodeList
1439 * Returns the result of document.querySelectorAll(aSelector).
1440 */
1441 aOwner.sandbox.$$ = function JSTH_$$(aSelector)
1442 {
1443 return aOwner.window.document.querySelectorAll(aSelector);
1444 };
1446 /**
1447 * Runs an xPath query and returns all matched nodes.
1448 *
1449 * @param string aXPath
1450 * xPath search query to execute.
1451 * @param [optional] nsIDOMNode aContext
1452 * Context to run the xPath query on. Uses window.document if not set.
1453 * @return array of nsIDOMNode
1454 */
1455 aOwner.sandbox.$x = function JSTH_$x(aXPath, aContext)
1456 {
1457 let nodes = new aOwner.window.wrappedJSObject.Array();
1458 let doc = aOwner.window.document;
1459 aContext = aContext || doc;
1461 let results = doc.evaluate(aXPath, aContext, null,
1462 Ci.nsIDOMXPathResult.ANY_TYPE, null);
1463 let node;
1464 while ((node = results.iterateNext())) {
1465 nodes.push(node);
1466 }
1468 return nodes;
1469 };
1471 /**
1472 * Returns the currently selected object in the highlighter.
1473 *
1474 * TODO: this implementation crosses the client/server boundaries! This is not
1475 * usable within a remote browser. To implement this feature correctly we need
1476 * support for remote inspection capabilities within the Inspector as well.
1477 * See bug 787975.
1478 *
1479 * @return nsIDOMElement|null
1480 * The DOM element currently selected in the highlighter.
1481 */
1482 Object.defineProperty(aOwner.sandbox, "$0", {
1483 get: function() {
1484 let window = aOwner.chromeWindow();
1485 if (!window) {
1486 return null;
1487 }
1489 let target = null;
1490 try {
1491 target = devtools.TargetFactory.forTab(window.gBrowser.selectedTab);
1492 }
1493 catch (ex) {
1494 // If we report this exception the user will get it in the Browser
1495 // Console every time when she evaluates any string.
1496 }
1498 if (!target) {
1499 return null;
1500 }
1502 let toolbox = gDevTools.getToolbox(target);
1503 let node = toolbox && toolbox.selection ? toolbox.selection.node : null;
1505 return node ? aOwner.makeDebuggeeValue(node) : null;
1506 },
1507 enumerable: true,
1508 configurable: false
1509 });
1511 /**
1512 * Clears the output of the JSTerm.
1513 */
1514 aOwner.sandbox.clear = function JSTH_clear()
1515 {
1516 aOwner.helperResult = {
1517 type: "clearOutput",
1518 };
1519 };
1521 /**
1522 * Returns the result of Object.keys(aObject).
1523 *
1524 * @param object aObject
1525 * Object to return the property names from.
1526 * @return array of strings
1527 */
1528 aOwner.sandbox.keys = function JSTH_keys(aObject)
1529 {
1530 return aOwner.window.wrappedJSObject.Object.keys(WebConsoleUtils.unwrap(aObject));
1531 };
1533 /**
1534 * Returns the values of all properties on aObject.
1535 *
1536 * @param object aObject
1537 * Object to display the values from.
1538 * @return array of string
1539 */
1540 aOwner.sandbox.values = function JSTH_values(aObject)
1541 {
1542 let arrValues = new aOwner.window.wrappedJSObject.Array();
1543 let obj = WebConsoleUtils.unwrap(aObject);
1545 for (let prop in obj) {
1546 arrValues.push(obj[prop]);
1547 }
1549 return arrValues;
1550 };
1552 /**
1553 * Opens a help window in MDN.
1554 */
1555 aOwner.sandbox.help = function JSTH_help()
1556 {
1557 aOwner.helperResult = { type: "help" };
1558 };
1560 /**
1561 * Change the JS evaluation scope.
1562 *
1563 * @param DOMElement|string|window aWindow
1564 * The window object to use for eval scope. This can be a string that
1565 * is used to perform document.querySelector(), to find the iframe that
1566 * you want to cd() to. A DOMElement can be given as well, the
1567 * .contentWindow property is used. Lastly, you can directly pass
1568 * a window object. If you call cd() with no arguments, the current
1569 * eval scope is cleared back to its default (the top window).
1570 */
1571 aOwner.sandbox.cd = function JSTH_cd(aWindow)
1572 {
1573 if (!aWindow) {
1574 aOwner.consoleActor.evalWindow = null;
1575 aOwner.helperResult = { type: "cd" };
1576 return;
1577 }
1579 if (typeof aWindow == "string") {
1580 aWindow = aOwner.window.document.querySelector(aWindow);
1581 }
1582 if (aWindow instanceof Ci.nsIDOMElement && aWindow.contentWindow) {
1583 aWindow = aWindow.contentWindow;
1584 }
1585 if (!(aWindow instanceof Ci.nsIDOMWindow)) {
1586 aOwner.helperResult = { type: "error", message: "cdFunctionInvalidArgument" };
1587 return;
1588 }
1590 aOwner.consoleActor.evalWindow = aWindow;
1591 aOwner.helperResult = { type: "cd" };
1592 };
1594 /**
1595 * Inspects the passed aObject. This is done by opening the PropertyPanel.
1596 *
1597 * @param object aObject
1598 * Object to inspect.
1599 */
1600 aOwner.sandbox.inspect = function JSTH_inspect(aObject)
1601 {
1602 let dbgObj = aOwner.makeDebuggeeValue(aObject);
1603 let grip = aOwner.createValueGrip(dbgObj);
1604 aOwner.helperResult = {
1605 type: "inspectObject",
1606 input: aOwner.evalInput,
1607 object: grip,
1608 };
1609 };
1611 /**
1612 * Prints aObject to the output.
1613 *
1614 * @param object aObject
1615 * Object to print to the output.
1616 * @return string
1617 */
1618 aOwner.sandbox.pprint = function JSTH_pprint(aObject)
1619 {
1620 if (aObject === null || aObject === undefined || aObject === true ||
1621 aObject === false) {
1622 aOwner.helperResult = {
1623 type: "error",
1624 message: "helperFuncUnsupportedTypeError",
1625 };
1626 return null;
1627 }
1629 aOwner.helperResult = { rawOutput: true };
1631 if (typeof aObject == "function") {
1632 return aObject + "\n";
1633 }
1635 let output = [];
1637 let obj = WebConsoleUtils.unwrap(aObject);
1638 for (let name in obj) {
1639 let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {};
1640 if (desc.get || desc.set) {
1641 // TODO: Bug 842672 - toolkit/ imports modules from browser/.
1642 let getGrip = VariablesView.getGrip(desc.get);
1643 let setGrip = VariablesView.getGrip(desc.set);
1644 let getString = VariablesView.getString(getGrip);
1645 let setString = VariablesView.getString(setGrip);
1646 output.push(name + ":", " get: " + getString, " set: " + setString);
1647 }
1648 else {
1649 let valueGrip = VariablesView.getGrip(obj[name]);
1650 let valueString = VariablesView.getString(valueGrip);
1651 output.push(name + ": " + valueString);
1652 }
1653 }
1655 return " " + output.join("\n ");
1656 };
1658 /**
1659 * Print a string to the output, as-is.
1660 *
1661 * @param string aString
1662 * A string you want to output.
1663 * @return void
1664 */
1665 aOwner.sandbox.print = function JSTH_print(aString)
1666 {
1667 aOwner.helperResult = { rawOutput: true };
1668 return String(aString);
1669 };
1670 }
1671 exports.JSTermHelpers = JSTermHelpers;
1674 /**
1675 * A ReflowObserver that listens for reflow events from the page.
1676 * Implements nsIReflowObserver.
1677 *
1678 * @constructor
1679 * @param object aWindow
1680 * The window for which we need to track reflow.
1681 * @param object aOwner
1682 * The listener owner which needs to implement:
1683 * - onReflowActivity(aReflowInfo)
1684 */
1686 function ConsoleReflowListener(aWindow, aListener)
1687 {
1688 this.docshell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
1689 .getInterface(Ci.nsIWebNavigation)
1690 .QueryInterface(Ci.nsIDocShell);
1691 this.listener = aListener;
1692 this.docshell.addWeakReflowObserver(this);
1693 }
1695 exports.ConsoleReflowListener = ConsoleReflowListener;
1697 ConsoleReflowListener.prototype =
1698 {
1699 QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
1700 Ci.nsISupportsWeakReference]),
1701 docshell: null,
1702 listener: null,
1704 /**
1705 * Forward reflow event to listener.
1706 *
1707 * @param DOMHighResTimeStamp aStart
1708 * @param DOMHighResTimeStamp aEnd
1709 * @param boolean aInterruptible
1710 */
1711 sendReflow: function CRL_sendReflow(aStart, aEnd, aInterruptible)
1712 {
1713 let frame = components.stack.caller.caller;
1715 let filename = frame.filename;
1717 if (filename) {
1718 // Because filename could be of the form "xxx.js -> xxx.js -> xxx.js",
1719 // we only take the last part.
1720 filename = filename.split(" ").pop();
1721 }
1723 this.listener.onReflowActivity({
1724 interruptible: aInterruptible,
1725 start: aStart,
1726 end: aEnd,
1727 sourceURL: filename,
1728 sourceLine: frame.lineNumber,
1729 functionName: frame.name
1730 });
1731 },
1733 /**
1734 * On uninterruptible reflow
1735 *
1736 * @param DOMHighResTimeStamp aStart
1737 * @param DOMHighResTimeStamp aEnd
1738 */
1739 reflow: function CRL_reflow(aStart, aEnd)
1740 {
1741 this.sendReflow(aStart, aEnd, false);
1742 },
1744 /**
1745 * On interruptible reflow
1746 *
1747 * @param DOMHighResTimeStamp aStart
1748 * @param DOMHighResTimeStamp aEnd
1749 */
1750 reflowInterruptible: function CRL_reflowInterruptible(aStart, aEnd)
1751 {
1752 this.sendReflow(aStart, aEnd, true);
1753 },
1755 /**
1756 * Unregister listener.
1757 */
1758 destroy: function CRL_destroy()
1759 {
1760 this.docshell.removeWeakReflowObserver(this);
1761 this.listener = this.docshell = null;
1762 },
1763 };
1765 function gSequenceId()
1766 {
1767 return gSequenceId.n++;
1768 }
1769 gSequenceId.n = 0;