Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
2 /* vim: set 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
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 "use strict";
9 const {Cc, Ci, Cu} = require("chrome");
11 let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
13 loader.lazyServiceGetter(this, "clipboardHelper",
14 "@mozilla.org/widget/clipboardhelper;1",
15 "nsIClipboardHelper");
16 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
17 loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
18 loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
19 loader.lazyGetter(this, "AutocompletePopup",
20 () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
21 loader.lazyGetter(this, "ToolSidebar",
22 () => require("devtools/framework/sidebar").ToolSidebar);
23 loader.lazyGetter(this, "NetworkPanel",
24 () => require("devtools/webconsole/network-panel").NetworkPanel);
25 loader.lazyGetter(this, "ConsoleOutput",
26 () => require("devtools/webconsole/console-output").ConsoleOutput);
27 loader.lazyGetter(this, "Messages",
28 () => require("devtools/webconsole/console-output").Messages);
29 loader.lazyImporter(this, "EnvironmentClient", "resource://gre/modules/devtools/dbg-client.jsm");
30 loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm");
31 loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
32 loader.lazyImporter(this, "VariablesViewController", "resource:///modules/devtools/VariablesViewController.jsm");
33 loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
34 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
36 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
37 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
39 const XHTML_NS = "http://www.w3.org/1999/xhtml";
41 const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent";
43 const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords";
45 const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security";
47 const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
49 const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
51 const CONSOLE_DIR_VIEW_HEIGHT = 0.6;
53 const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"];
55 // The amount of time in milliseconds that we wait before performing a live
56 // search.
57 const SEARCH_DELAY = 200;
59 // The number of lines that are displayed in the console output by default, for
60 // each category. The user can change this number by adjusting the hidden
61 // "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences.
62 const DEFAULT_LOG_LIMIT = 200;
64 // The various categories of messages. We start numbering at zero so we can
65 // use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below.
66 const CATEGORY_NETWORK = 0;
67 const CATEGORY_CSS = 1;
68 const CATEGORY_JS = 2;
69 const CATEGORY_WEBDEV = 3;
70 const CATEGORY_INPUT = 4; // always on
71 const CATEGORY_OUTPUT = 5; // always on
72 const CATEGORY_SECURITY = 6;
74 // The possible message severities. As before, we start at zero so we can use
75 // these as indexes into MESSAGE_PREFERENCE_KEYS.
76 const SEVERITY_ERROR = 0;
77 const SEVERITY_WARNING = 1;
78 const SEVERITY_INFO = 2;
79 const SEVERITY_LOG = 3;
81 // The fragment of a CSS class name that identifies each category.
82 const CATEGORY_CLASS_FRAGMENTS = [
83 "network",
84 "cssparser",
85 "exception",
86 "console",
87 "input",
88 "output",
89 "security",
90 ];
92 // The fragment of a CSS class name that identifies each severity.
93 const SEVERITY_CLASS_FRAGMENTS = [
94 "error",
95 "warn",
96 "info",
97 "log",
98 ];
100 // The preference keys to use for each category/severity combination, indexed
101 // first by category (rows) and then by severity (columns).
102 //
103 // Most of these rather idiosyncratic names are historical and predate the
104 // division of message type into "category" and "severity".
105 const MESSAGE_PREFERENCE_KEYS = [
106 // Error Warning Info Log
107 [ "network", "netwarn", null, "networkinfo", ], // Network
108 [ "csserror", "cssparser", null, "csslog", ], // CSS
109 [ "exception", "jswarn", null, "jslog", ], // JS
110 [ "error", "warn", "info", "log", ], // Web Developer
111 [ null, null, null, null, ], // Input
112 [ null, null, null, null, ], // Output
113 [ "secerror", "secwarn", null, null, ], // Security
114 ];
116 // A mapping from the console API log event levels to the Web Console
117 // severities.
118 const LEVELS = {
119 error: SEVERITY_ERROR,
120 exception: SEVERITY_ERROR,
121 assert: SEVERITY_ERROR,
122 warn: SEVERITY_WARNING,
123 info: SEVERITY_INFO,
124 log: SEVERITY_LOG,
125 trace: SEVERITY_LOG,
126 debug: SEVERITY_LOG,
127 dir: SEVERITY_LOG,
128 group: SEVERITY_LOG,
129 groupCollapsed: SEVERITY_LOG,
130 groupEnd: SEVERITY_LOG,
131 time: SEVERITY_LOG,
132 timeEnd: SEVERITY_LOG,
133 count: SEVERITY_LOG
134 };
136 // The lowest HTTP response code (inclusive) that is considered an error.
137 const MIN_HTTP_ERROR_CODE = 400;
138 // The highest HTTP response code (inclusive) that is considered an error.
139 const MAX_HTTP_ERROR_CODE = 599;
141 // Constants used for defining the direction of JSTerm input history navigation.
142 const HISTORY_BACK = -1;
143 const HISTORY_FORWARD = 1;
145 // The indent of a console group in pixels.
146 const GROUP_INDENT = 12;
148 // The number of messages to display in a single display update. If we display
149 // too many messages at once we slow the Firefox UI too much.
150 const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT;
152 // The delay between display updates - tells how often we should *try* to push
153 // new messages to screen. This value is optimistic, updates won't always
154 // happen. Keep this low so the Web Console output feels live.
155 const OUTPUT_INTERVAL = 50; // milliseconds
157 // When the output queue has more than MESSAGES_IN_INTERVAL items we throttle
158 // output updates to this number of milliseconds. So during a lot of output we
159 // update every N milliseconds given here.
160 const THROTTLE_UPDATES = 1000; // milliseconds
162 // The preference prefix for all of the Web Console filters.
163 const FILTER_PREFS_PREFIX = "devtools.webconsole.filter.";
165 // The minimum font size.
166 const MIN_FONT_SIZE = 10;
168 const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
169 const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
170 const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
172 /**
173 * A WebConsoleFrame instance is an interactive console initialized *per target*
174 * that displays console log data as well as provides an interactive terminal to
175 * manipulate the target's document content.
176 *
177 * The WebConsoleFrame is responsible for the actual Web Console UI
178 * implementation.
179 *
180 * @constructor
181 * @param object aWebConsoleOwner
182 * The WebConsole owner object.
183 */
184 function WebConsoleFrame(aWebConsoleOwner)
185 {
186 this.owner = aWebConsoleOwner;
187 this.hudId = this.owner.hudId;
188 this.window = this.owner.iframeWindow;
190 this._repeatNodes = {};
191 this._outputQueue = [];
192 this._pruneCategoriesQueue = {};
193 this._networkRequests = {};
194 this.filterPrefs = {};
196 this.output = new ConsoleOutput(this);
198 this._toggleFilter = this._toggleFilter.bind(this);
199 this._onPanelSelected = this._onPanelSelected.bind(this);
200 this._flushMessageQueue = this._flushMessageQueue.bind(this);
201 this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
203 this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
204 this._outputTimerInitialized = false;
206 EventEmitter.decorate(this);
207 }
208 exports.WebConsoleFrame = WebConsoleFrame;
210 WebConsoleFrame.prototype = {
211 /**
212 * The WebConsole instance that owns this frame.
213 * @see hudservice.js::WebConsole
214 * @type object
215 */
216 owner: null,
218 /**
219 * Proxy between the Web Console and the remote Web Console instance. This
220 * object holds methods used for connecting, listening and disconnecting from
221 * the remote server, using the remote debugging protocol.
222 *
223 * @see WebConsoleConnectionProxy
224 * @type object
225 */
226 proxy: null,
228 /**
229 * Getter for the xul:popupset that holds any popups we open.
230 * @type nsIDOMElement
231 */
232 get popupset() this.owner.mainPopupSet,
234 /**
235 * Holds the initialization promise object.
236 * @private
237 * @type object
238 */
239 _initDefer: null,
241 /**
242 * Holds the network requests currently displayed by the Web Console. Each key
243 * represents the connection ID and the value is network request information.
244 * @private
245 * @type object
246 */
247 _networkRequests: null,
249 /**
250 * Last time when we displayed any message in the output.
251 *
252 * @private
253 * @type number
254 * Timestamp in milliseconds since the Unix epoch.
255 */
256 _lastOutputFlush: 0,
258 /**
259 * Message nodes are stored here in a queue for later display.
260 *
261 * @private
262 * @type array
263 */
264 _outputQueue: null,
266 /**
267 * Keep track of the categories we need to prune from time to time.
268 *
269 * @private
270 * @type array
271 */
272 _pruneCategoriesQueue: null,
274 /**
275 * Function invoked whenever the output queue is emptied. This is used by some
276 * tests.
277 *
278 * @private
279 * @type function
280 */
281 _flushCallback: null,
283 /**
284 * Timer used for flushing the messages output queue.
285 *
286 * @private
287 * @type nsITimer
288 */
289 _outputTimer: null,
290 _outputTimerInitialized: null,
292 /**
293 * Store for tracking repeated nodes.
294 * @private
295 * @type object
296 */
297 _repeatNodes: null,
299 /**
300 * Preferences for filtering messages by type.
301 * @see this._initDefaultFilterPrefs()
302 * @type object
303 */
304 filterPrefs: null,
306 /**
307 * Prefix used for filter preferences.
308 * @private
309 * @type string
310 */
311 _filterPrefsPrefix: FILTER_PREFS_PREFIX,
313 /**
314 * The nesting depth of the currently active console group.
315 */
316 groupDepth: 0,
318 /**
319 * The current target location.
320 * @type string
321 */
322 contentLocation: "",
324 /**
325 * The JSTerm object that manage the console's input.
326 * @see JSTerm
327 * @type object
328 */
329 jsterm: null,
331 /**
332 * The element that holds all of the messages we display.
333 * @type nsIDOMElement
334 */
335 outputNode: null,
337 /**
338 * The ConsoleOutput instance that manages all output.
339 * @type object
340 */
341 output: null,
343 /**
344 * The input element that allows the user to filter messages by string.
345 * @type nsIDOMElement
346 */
347 filterBox: null,
349 /**
350 * Getter for the debugger WebConsoleClient.
351 * @type object
352 */
353 get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null,
355 _destroyer: null,
357 // Used in tests.
358 _saveRequestAndResponseBodies: false,
360 // Chevron width at the starting of Web Console's input box.
361 _chevronWidth: 0,
362 // Width of the monospace characters in Web Console's input box.
363 _inputCharWidth: 0,
365 /**
366 * Tells whether to save the bodies of network requests and responses.
367 * Disabled by default to save memory.
368 *
369 * @return boolean
370 * The saveRequestAndResponseBodies pref value.
371 */
372 getSaveRequestAndResponseBodies:
373 function WCF_getSaveRequestAndResponseBodies() {
374 let deferred = promise.defer();
375 let toGet = [
376 "NetworkMonitor.saveRequestAndResponseBodies"
377 ];
379 // Make sure the web console client connection is established first.
380 this.webConsoleClient.getPreferences(toGet, aResponse => {
381 if (!aResponse.error) {
382 this._saveRequestAndResponseBodies = aResponse.preferences[toGet[0]];
383 deferred.resolve(this._saveRequestAndResponseBodies);
384 }
385 else {
386 deferred.reject(aResponse.error);
387 }
388 });
390 return deferred.promise;
391 },
393 /**
394 * Setter for saving of network request and response bodies.
395 *
396 * @param boolean aValue
397 * The new value you want to set.
398 */
399 setSaveRequestAndResponseBodies:
400 function WCF_setSaveRequestAndResponseBodies(aValue) {
401 if (!this.webConsoleClient) {
402 // Don't continue if the webconsole disconnected.
403 return promise.resolve(null);
404 }
406 let deferred = promise.defer();
407 let newValue = !!aValue;
408 let toSet = {
409 "NetworkMonitor.saveRequestAndResponseBodies": newValue,
410 };
412 // Make sure the web console client connection is established first.
413 this.webConsoleClient.setPreferences(toSet, aResponse => {
414 if (!aResponse.error) {
415 this._saveRequestAndResponseBodies = newValue;
416 deferred.resolve(aResponse);
417 }
418 else {
419 deferred.reject(aResponse.error);
420 }
421 });
423 return deferred.promise;
424 },
426 /**
427 * Getter for the persistent logging preference.
428 * @type boolean
429 */
430 get persistLog() {
431 return Services.prefs.getBoolPref(PREF_PERSISTLOG);
432 },
434 /**
435 * Initialize the WebConsoleFrame instance.
436 * @return object
437 * A promise object for the initialization.
438 */
439 init: function WCF_init()
440 {
441 this._initUI();
442 return this._initConnection();
443 },
445 /**
446 * Connect to the server using the remote debugging protocol.
447 *
448 * @private
449 * @return object
450 * A promise object that is resolved/reject based on the connection
451 * result.
452 */
453 _initConnection: function WCF__initConnection()
454 {
455 if (this._initDefer) {
456 return this._initDefer.promise;
457 }
459 this._initDefer = promise.defer();
460 this.proxy = new WebConsoleConnectionProxy(this, this.owner.target);
462 this.proxy.connect().then(() => { // on success
463 this._initDefer.resolve(this);
464 }, (aReason) => { // on failure
465 let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR,
466 aReason.error + ": " + aReason.message);
467 this.outputMessage(CATEGORY_JS, node);
468 this._initDefer.reject(aReason);
469 }).then(() => {
470 let id = WebConsoleUtils.supportsString(this.hudId);
471 Services.obs.notifyObservers(id, "web-console-created", null);
472 });
474 return this._initDefer.promise;
475 },
477 /**
478 * Find the Web Console UI elements and setup event listeners as needed.
479 * @private
480 */
481 _initUI: function WCF__initUI()
482 {
483 this.document = this.window.document;
484 this.rootElement = this.document.documentElement;
486 this._initDefaultFilterPrefs();
488 // Register the controller to handle "select all" properly.
489 this._commandController = new CommandController(this);
490 this.window.controllers.insertControllerAt(0, this._commandController);
492 this._contextMenuHandler = new ConsoleContextMenu(this);
494 let doc = this.document;
496 this.filterBox = doc.querySelector(".hud-filter-box");
497 this.outputNode = doc.getElementById("output-container");
498 this.completeNode = doc.querySelector(".jsterm-complete-node");
499 this.inputNode = doc.querySelector(".jsterm-input-node");
501 this._setFilterTextBoxEvents();
502 this._initFilterButtons();
504 let fontSize = this.owner._browserConsole ?
505 Services.prefs.getIntPref("devtools.webconsole.fontSize") : 0;
507 if (fontSize != 0) {
508 fontSize = Math.max(MIN_FONT_SIZE, fontSize);
510 this.outputNode.style.fontSize = fontSize + "px";
511 this.completeNode.style.fontSize = fontSize + "px";
512 this.inputNode.style.fontSize = fontSize + "px";
513 }
515 if (this.owner._browserConsole) {
516 for (let id of ["Enlarge", "Reduce", "Reset"]) {
517 this.document.getElementById("cmd_fullZoom" + id)
518 .removeAttribute("disabled");
519 }
520 }
522 // Update the character width and height needed for the popup offset
523 // calculations.
524 this._updateCharSize();
526 let updateSaveBodiesPrefUI = (aElement) => {
527 this.getSaveRequestAndResponseBodies().then(aValue => {
528 aElement.setAttribute("checked", aValue);
529 this.emit("save-bodies-ui-toggled");
530 });
531 }
533 let reverseSaveBodiesPref = ({ target: aElement }) => {
534 this.getSaveRequestAndResponseBodies().then(aValue => {
535 this.setSaveRequestAndResponseBodies(!aValue);
536 aElement.setAttribute("checked", aValue);
537 this.emit("save-bodies-pref-reversed");
538 });
539 }
541 let saveBodies = doc.getElementById("saveBodies");
542 saveBodies.addEventListener("command", reverseSaveBodiesPref);
543 saveBodies.disabled = !this.getFilterState("networkinfo") &&
544 !this.getFilterState("network");
546 let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu");
547 saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref);
548 saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") &&
549 !this.getFilterState("network");
551 saveBodies.parentNode.addEventListener("popupshowing", () => {
552 updateSaveBodiesPrefUI(saveBodies);
553 saveBodies.disabled = !this.getFilterState("networkinfo") &&
554 !this.getFilterState("network");
555 });
557 saveBodiesContextMenu.parentNode.addEventListener("popupshowing", () => {
558 updateSaveBodiesPrefUI(saveBodiesContextMenu);
559 saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") &&
560 !this.getFilterState("network");
561 });
563 let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0];
564 clearButton.addEventListener("command", () => {
565 this.owner._onClearButton();
566 this.jsterm.clearOutput(true);
567 });
569 this.jsterm = new JSTerm(this);
570 this.jsterm.init();
572 let toolbox = gDevTools.getToolbox(this.owner.target);
573 if (toolbox) {
574 toolbox.on("webconsole-selected", this._onPanelSelected);
575 }
577 /*
578 * Focus input line whenever the output area is clicked.
579 * Reusing _addMEssageLinkCallback since it correctly filters
580 * drag and select events.
581 */
582 this._addFocusCallback(this.outputNode, (evt) => {
583 if ((evt.target.nodeName.toLowerCase() != "a") &&
584 (evt.target.parentNode.nodeName.toLowerCase() != "a")) {
585 this.jsterm.inputNode.focus();
586 }
587 });
589 // Toggle the timestamp on preference change
590 gDevTools.on("pref-changed", this._onToolboxPrefChanged);
591 this._onToolboxPrefChanged("pref-changed", {
592 pref: PREF_MESSAGE_TIMESTAMP,
593 newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP),
594 });
596 // focus input node
597 this.jsterm.inputNode.focus();
598 },
600 /**
601 * Sets the focus to JavaScript input field when the web console tab is
602 * selected or when there is a split console present.
603 * @private
604 */
605 _onPanelSelected: function WCF__onPanelSelected(evt, id)
606 {
607 this.jsterm.inputNode.focus();
608 },
610 /**
611 * Initialize the default filter preferences.
612 * @private
613 */
614 _initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs()
615 {
616 let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
617 "exception", "jswarn", "jslog", "error", "info", "warn", "log",
618 "secerror", "secwarn", "netwarn"];
619 for (let pref of prefs) {
620 this.filterPrefs[pref] = Services.prefs
621 .getBoolPref(this._filterPrefsPrefix + pref);
622 }
623 },
625 /**
626 * Attach / detach reflow listeners depending on the checked status
627 * of the `CSS > Log` menuitem.
628 *
629 * @param function [aCallback=null]
630 * Optional function to invoke when the listener has been
631 * added/removed.
632 *
633 */
634 _updateReflowActivityListener:
635 function WCF__updateReflowActivityListener(aCallback)
636 {
637 if (this.webConsoleClient) {
638 let pref = this._filterPrefsPrefix + "csslog";
639 if (Services.prefs.getBoolPref(pref)) {
640 this.webConsoleClient.startListeners(["ReflowActivity"], aCallback);
641 } else {
642 this.webConsoleClient.stopListeners(["ReflowActivity"], aCallback);
643 }
644 }
645 },
647 /**
648 * Sets the events for the filter input field.
649 * @private
650 */
651 _setFilterTextBoxEvents: function WCF__setFilterTextBoxEvents()
652 {
653 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
654 let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this);
656 let onChange = function _onChange() {
657 // To improve responsiveness, we let the user finish typing before we
658 // perform the search.
659 timer.cancel();
660 timer.initWithCallback(timerEvent, SEARCH_DELAY,
661 Ci.nsITimer.TYPE_ONE_SHOT);
662 };
664 this.filterBox.addEventListener("command", onChange, false);
665 this.filterBox.addEventListener("input", onChange, false);
666 },
668 /**
669 * Creates one of the filter buttons on the toolbar.
670 *
671 * @private
672 * @param nsIDOMNode aParent
673 * The node to which the filter button should be appended.
674 * @param object aDescriptor
675 * A descriptor that contains info about the button. Contains "name",
676 * "category", and "prefKey" properties, and optionally a "severities"
677 * property.
678 */
679 _initFilterButtons: function WCF__initFilterButtons()
680 {
681 let categories = this.document
682 .querySelectorAll(".webconsole-filter-button[category]");
683 Array.forEach(categories, function(aButton) {
684 aButton.addEventListener("click", this._toggleFilter, false);
686 let someChecked = false;
687 let severities = aButton.querySelectorAll("menuitem[prefKey]");
688 Array.forEach(severities, function(aMenuItem) {
689 aMenuItem.addEventListener("command", this._toggleFilter, false);
691 let prefKey = aMenuItem.getAttribute("prefKey");
692 let checked = this.filterPrefs[prefKey];
693 aMenuItem.setAttribute("checked", checked);
694 someChecked = someChecked || checked;
695 }, this);
697 aButton.setAttribute("checked", someChecked);
698 }, this);
700 if (!this.owner._browserConsole) {
701 // The Browser Console displays nsIConsoleMessages which are messages that
702 // end up in the JS category, but they are not errors or warnings, they
703 // are just log messages. The Web Console does not show such messages.
704 let jslog = this.document.querySelector("menuitem[prefKey=jslog]");
705 jslog.hidden = true;
706 }
708 if (Services.appinfo.OS == "Darwin") {
709 let net = this.document.querySelector("toolbarbutton[category=net]");
710 let accesskey = net.getAttribute("accesskeyMacOSX");
711 net.setAttribute("accesskey", accesskey);
713 let logging = this.document.querySelector("toolbarbutton[category=logging]");
714 logging.removeAttribute("accesskey");
715 }
716 },
718 /**
719 * Increase, decrease or reset the font size.
720 *
721 * @param string size
722 * The size of the font change. Accepted values are "+" and "-".
723 * An unmatched size assumes a font reset.
724 */
725 changeFontSize: function WCF_changeFontSize(aSize)
726 {
727 let fontSize = this.window
728 .getComputedStyle(this.outputNode, null)
729 .getPropertyValue("font-size").replace("px", "");
731 if (this.outputNode.style.fontSize) {
732 fontSize = this.outputNode.style.fontSize.replace("px", "");
733 }
735 if (aSize == "+" || aSize == "-") {
736 fontSize = parseInt(fontSize, 10);
738 if (aSize == "+") {
739 fontSize += 1;
740 }
741 else {
742 fontSize -= 1;
743 }
745 if (fontSize < MIN_FONT_SIZE) {
746 fontSize = MIN_FONT_SIZE;
747 }
749 Services.prefs.setIntPref("devtools.webconsole.fontSize", fontSize);
750 fontSize = fontSize + "px";
752 this.completeNode.style.fontSize = fontSize;
753 this.inputNode.style.fontSize = fontSize;
754 this.outputNode.style.fontSize = fontSize;
755 }
756 else {
757 this.completeNode.style.fontSize = "";
758 this.inputNode.style.fontSize = "";
759 this.outputNode.style.fontSize = "";
760 Services.prefs.clearUserPref("devtools.webconsole.fontSize");
761 }
762 this._updateCharSize();
763 },
765 /**
766 * Calculates the width and height of a single character of the input box.
767 * This will be used in opening the popup at the correct offset.
768 *
769 * @private
770 */
771 _updateCharSize: function WCF__updateCharSize()
772 {
773 let doc = this.document;
774 let tempLabel = doc.createElementNS(XHTML_NS, "span");
775 let style = tempLabel.style;
776 style.position = "fixed";
777 style.padding = "0";
778 style.margin = "0";
779 style.width = "auto";
780 style.color = "transparent";
781 WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel);
782 tempLabel.textContent = "x";
783 doc.documentElement.appendChild(tempLabel);
784 this._inputCharWidth = tempLabel.offsetWidth;
785 tempLabel.parentNode.removeChild(tempLabel);
786 // Calculate the width of the chevron placed at the beginning of the input
787 // box. Remove 4 more pixels to accomodate the padding of the popup.
788 this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode)
789 .paddingLeft.replace(/[^0-9.]/g, "") - 4;
790 },
792 /**
793 * The event handler that is called whenever a user switches a filter on or
794 * off.
795 *
796 * @private
797 * @param nsIDOMEvent aEvent
798 * The event that triggered the filter change.
799 */
800 _toggleFilter: function WCF__toggleFilter(aEvent)
801 {
802 let target = aEvent.target;
803 let tagName = target.tagName;
804 if (tagName != aEvent.currentTarget.tagName) {
805 return;
806 }
808 switch (tagName) {
809 case "toolbarbutton": {
810 let originalTarget = aEvent.originalTarget;
811 let classes = originalTarget.classList;
813 if (originalTarget.localName !== "toolbarbutton") {
814 // Oddly enough, the click event is sent to the menu button when
815 // selecting a menu item with the mouse. Detect this case and bail
816 // out.
817 break;
818 }
820 if (!classes.contains("toolbarbutton-menubutton-button") &&
821 originalTarget.getAttribute("type") === "menu-button") {
822 // This is a filter button with a drop-down. The user clicked the
823 // drop-down, so do nothing. (The menu will automatically appear
824 // without our intervention.)
825 break;
826 }
828 // Toggle on the targeted filter button, and if the user alt clicked,
829 // toggle off all other filter buttons and their associated filters.
830 let state = target.getAttribute("checked") !== "true";
831 if (aEvent.getModifierState("Alt")) {
832 let buttons = this.document
833 .querySelectorAll(".webconsole-filter-button");
834 Array.forEach(buttons, (button) => {
835 if (button !== target) {
836 button.setAttribute("checked", false);
837 this._setMenuState(button, false);
838 }
839 });
840 state = true;
841 }
842 target.setAttribute("checked", state);
844 // This is a filter button with a drop-down, and the user clicked the
845 // main part of the button. Go through all the severities and toggle
846 // their associated filters.
847 this._setMenuState(target, state);
849 // CSS reflow logging can decrease web page performance.
850 // Make sure the option is always unchecked when the CSS filter button is selected.
851 // See bug 971798.
852 if (target.getAttribute("category") == "css" && state) {
853 let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]");
854 csslogMenuItem.setAttribute("checked", false);
855 this.setFilterState("csslog", false);
856 }
858 break;
859 }
861 case "menuitem": {
862 let state = target.getAttribute("checked") !== "true";
863 target.setAttribute("checked", state);
865 let prefKey = target.getAttribute("prefKey");
866 this.setFilterState(prefKey, state);
868 // Disable the log response and request body if network logging is off.
869 if (prefKey == "networkinfo" || prefKey == "network") {
870 let checkState = !this.getFilterState("networkinfo") &&
871 !this.getFilterState("network");
872 this.document.getElementById("saveBodies").disabled = checkState;
873 this.document.getElementById("saveBodiesContextMenu").disabled = checkState;
874 }
876 // Adjust the state of the button appropriately.
877 let menuPopup = target.parentNode;
879 let someChecked = false;
880 let menuItem = menuPopup.firstChild;
881 while (menuItem) {
882 if (menuItem.hasAttribute("prefKey") &&
883 menuItem.getAttribute("checked") === "true") {
884 someChecked = true;
885 break;
886 }
887 menuItem = menuItem.nextSibling;
888 }
889 let toolbarButton = menuPopup.parentNode;
890 toolbarButton.setAttribute("checked", someChecked);
891 break;
892 }
893 }
894 },
896 /**
897 * Set the menu attributes for a specific toggle button.
898 *
899 * @private
900 * @param XULElement aTarget
901 * Button with drop down items to be toggled.
902 * @param boolean aState
903 * True if the menu item is being toggled on, and false otherwise.
904 */
905 _setMenuState: function WCF__setMenuState(aTarget, aState)
906 {
907 let menuItems = aTarget.querySelectorAll("menuitem");
908 Array.forEach(menuItems, (item) => {
909 item.setAttribute("checked", aState);
910 let prefKey = item.getAttribute("prefKey");
911 this.setFilterState(prefKey, aState);
912 });
913 },
915 /**
916 * Set the filter state for a specific toggle button.
917 *
918 * @param string aToggleType
919 * @param boolean aState
920 * @returns void
921 */
922 setFilterState: function WCF_setFilterState(aToggleType, aState)
923 {
924 this.filterPrefs[aToggleType] = aState;
925 this.adjustVisibilityForMessageType(aToggleType, aState);
926 Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState);
927 this._updateReflowActivityListener();
928 },
930 /**
931 * Get the filter state for a specific toggle button.
932 *
933 * @param string aToggleType
934 * @returns boolean
935 */
936 getFilterState: function WCF_getFilterState(aToggleType)
937 {
938 return this.filterPrefs[aToggleType];
939 },
941 /**
942 * Check that the passed string matches the filter arguments.
943 *
944 * @param String aString
945 * to search for filter words in.
946 * @param String aFilter
947 * is a string containing all of the words to filter on.
948 * @returns boolean
949 */
950 stringMatchesFilters: function WCF_stringMatchesFilters(aString, aFilter)
951 {
952 if (!aFilter || !aString) {
953 return true;
954 }
956 let searchStr = aString.toLowerCase();
957 let filterStrings = aFilter.toLowerCase().split(/\s+/);
958 return !filterStrings.some(function (f) {
959 return searchStr.indexOf(f) == -1;
960 });
961 },
963 /**
964 * Turns the display of log nodes on and off appropriately to reflect the
965 * adjustment of the message type filter named by @aPrefKey.
966 *
967 * @param string aPrefKey
968 * The preference key for the message type being filtered: one of the
969 * values in the MESSAGE_PREFERENCE_KEYS table.
970 * @param boolean aState
971 * True if the filter named by @aMessageType is being turned on; false
972 * otherwise.
973 * @returns void
974 */
975 adjustVisibilityForMessageType:
976 function WCF_adjustVisibilityForMessageType(aPrefKey, aState)
977 {
978 let outputNode = this.outputNode;
979 let doc = this.document;
981 // Look for message nodes (".message") with the given preference key
982 // (filter="error", filter="cssparser", etc.) and add or remove the
983 // "filtered-by-type" class, which turns on or off the display.
985 let xpath = ".//*[contains(@class, 'message') and " +
986 "@filter='" + aPrefKey + "']";
987 let result = doc.evaluate(xpath, outputNode, null,
988 Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
989 for (let i = 0; i < result.snapshotLength; i++) {
990 let node = result.snapshotItem(i);
991 if (aState) {
992 node.classList.remove("filtered-by-type");
993 }
994 else {
995 node.classList.add("filtered-by-type");
996 }
997 }
998 },
1000 /**
1001 * Turns the display of log nodes on and off appropriately to reflect the
1002 * adjustment of the search string.
1003 */
1004 adjustVisibilityOnSearchStringChange:
1005 function WCF_adjustVisibilityOnSearchStringChange()
1006 {
1007 let nodes = this.outputNode.getElementsByClassName("message");
1008 let searchString = this.filterBox.value;
1010 for (let i = 0, n = nodes.length; i < n; ++i) {
1011 let node = nodes[i];
1013 // hide nodes that match the strings
1014 let text = node.textContent;
1016 // if the text matches the words in aSearchString...
1017 if (this.stringMatchesFilters(text, searchString)) {
1018 node.classList.remove("filtered-by-string");
1019 }
1020 else {
1021 node.classList.add("filtered-by-string");
1022 }
1023 }
1024 },
1026 /**
1027 * Applies the user's filters to a newly-created message node via CSS
1028 * classes.
1029 *
1030 * @param nsIDOMNode aNode
1031 * The newly-created message node.
1032 * @return boolean
1033 * True if the message was filtered or false otherwise.
1034 */
1035 filterMessageNode: function WCF_filterMessageNode(aNode)
1036 {
1037 let isFiltered = false;
1039 // Filter by the message type.
1040 let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity];
1041 if (prefKey && !this.getFilterState(prefKey)) {
1042 // The node is filtered by type.
1043 aNode.classList.add("filtered-by-type");
1044 isFiltered = true;
1045 }
1047 // Filter on the search string.
1048 let search = this.filterBox.value;
1049 let text = aNode.clipboardText;
1051 // if string matches the filter text
1052 if (!this.stringMatchesFilters(text, search)) {
1053 aNode.classList.add("filtered-by-string");
1054 isFiltered = true;
1055 }
1057 if (isFiltered && aNode.classList.contains("inlined-variables-view")) {
1058 aNode.classList.add("hidden-message");
1059 }
1061 return isFiltered;
1062 },
1064 /**
1065 * Merge the attributes of the two nodes that are about to be filtered.
1066 * Increment the number of repeats of aOriginal.
1067 *
1068 * @param nsIDOMNode aOriginal
1069 * The Original Node. The one being merged into.
1070 * @param nsIDOMNode aFiltered
1071 * The node being filtered out because it is repeated.
1072 */
1073 mergeFilteredMessageNode:
1074 function WCF_mergeFilteredMessageNode(aOriginal, aFiltered)
1075 {
1076 let repeatNode = aOriginal.getElementsByClassName("message-repeats")[0];
1077 if (!repeatNode) {
1078 return; // no repeat node, return early.
1079 }
1081 let occurrences = parseInt(repeatNode.getAttribute("value")) + 1;
1082 repeatNode.setAttribute("value", occurrences);
1083 repeatNode.textContent = occurrences;
1084 let str = l10n.getStr("messageRepeats.tooltip2");
1085 repeatNode.title = PluralForm.get(occurrences, str)
1086 .replace("#1", occurrences);
1087 },
1089 /**
1090 * Filter the message node from the output if it is a repeat.
1091 *
1092 * @private
1093 * @param nsIDOMNode aNode
1094 * The message node to be filtered or not.
1095 * @returns nsIDOMNode|null
1096 * Returns the duplicate node if the message was filtered, null
1097 * otherwise.
1098 */
1099 _filterRepeatedMessage: function WCF__filterRepeatedMessage(aNode)
1100 {
1101 let repeatNode = aNode.getElementsByClassName("message-repeats")[0];
1102 if (!repeatNode) {
1103 return null;
1104 }
1106 let uid = repeatNode._uid;
1107 let dupeNode = null;
1109 if (aNode.category == CATEGORY_CSS ||
1110 aNode.category == CATEGORY_SECURITY) {
1111 dupeNode = this._repeatNodes[uid];
1112 if (!dupeNode) {
1113 this._repeatNodes[uid] = aNode;
1114 }
1115 }
1116 else if ((aNode.category == CATEGORY_WEBDEV ||
1117 aNode.category == CATEGORY_JS) &&
1118 aNode.category != CATEGORY_NETWORK &&
1119 !aNode.classList.contains("inlined-variables-view")) {
1120 let lastMessage = this.outputNode.lastChild;
1121 if (!lastMessage) {
1122 return null;
1123 }
1125 let lastRepeatNode = lastMessage.getElementsByClassName("message-repeats")[0];
1126 if (lastRepeatNode && lastRepeatNode._uid == uid) {
1127 dupeNode = lastMessage;
1128 }
1129 }
1131 if (dupeNode) {
1132 this.mergeFilteredMessageNode(dupeNode, aNode);
1133 return dupeNode;
1134 }
1136 return null;
1137 },
1139 /**
1140 * Display cached messages that may have been collected before the UI is
1141 * displayed.
1142 *
1143 * @param array aRemoteMessages
1144 * Array of cached messages coming from the remote Web Console
1145 * content instance.
1146 */
1147 displayCachedMessages: function WCF_displayCachedMessages(aRemoteMessages)
1148 {
1149 if (!aRemoteMessages.length) {
1150 return;
1151 }
1153 aRemoteMessages.forEach(function(aMessage) {
1154 switch (aMessage._type) {
1155 case "PageError": {
1156 let category = Utils.categoryForScriptError(aMessage);
1157 this.outputMessage(category, this.reportPageError,
1158 [category, aMessage]);
1159 break;
1160 }
1161 case "LogMessage":
1162 this.handleLogMessage(aMessage);
1163 break;
1164 case "ConsoleAPI":
1165 this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
1166 [aMessage]);
1167 break;
1168 }
1169 }, this);
1170 },
1172 /**
1173 * Logs a message to the Web Console that originates from the Web Console
1174 * server.
1175 *
1176 * @param object aMessage
1177 * The message received from the server.
1178 * @return nsIDOMElement|null
1179 * The message element to display in the Web Console output.
1180 */
1181 logConsoleAPIMessage: function WCF_logConsoleAPIMessage(aMessage)
1182 {
1183 let body = null;
1184 let clipboardText = null;
1185 let sourceURL = aMessage.filename;
1186 let sourceLine = aMessage.lineNumber;
1187 let level = aMessage.level;
1188 let args = aMessage.arguments;
1189 let objectActors = new Set();
1190 let node = null;
1192 // Gather the actor IDs.
1193 args.forEach((aValue) => {
1194 if (WebConsoleUtils.isActorGrip(aValue)) {
1195 objectActors.add(aValue.actor);
1196 }
1197 });
1199 switch (level) {
1200 case "log":
1201 case "info":
1202 case "warn":
1203 case "error":
1204 case "exception":
1205 case "assert":
1206 case "debug": {
1207 let msg = new Messages.ConsoleGeneric(aMessage);
1208 node = msg.init(this.output).render().element;
1209 break;
1210 }
1211 case "trace": {
1212 let msg = new Messages.ConsoleTrace(aMessage);
1213 node = msg.init(this.output).render().element;
1214 break;
1215 }
1216 case "dir": {
1217 body = { arguments: args };
1218 let clipboardArray = [];
1219 args.forEach((aValue) => {
1220 clipboardArray.push(VariablesView.getString(aValue));
1221 });
1222 clipboardText = clipboardArray.join(" ");
1223 break;
1224 }
1226 case "group":
1227 case "groupCollapsed":
1228 clipboardText = body = aMessage.groupName;
1229 this.groupDepth++;
1230 break;
1232 case "groupEnd":
1233 if (this.groupDepth > 0) {
1234 this.groupDepth--;
1235 }
1236 break;
1238 case "time": {
1239 let timer = aMessage.timer;
1240 if (!timer) {
1241 return null;
1242 }
1243 if (timer.error) {
1244 Cu.reportError(l10n.getStr(timer.error));
1245 return null;
1246 }
1247 body = l10n.getFormatStr("timerStarted", [timer.name]);
1248 clipboardText = body;
1249 break;
1250 }
1252 case "timeEnd": {
1253 let timer = aMessage.timer;
1254 if (!timer) {
1255 return null;
1256 }
1257 let duration = Math.round(timer.duration * 100) / 100;
1258 body = l10n.getFormatStr("timeEnd", [timer.name, duration]);
1259 clipboardText = body;
1260 break;
1261 }
1263 case "count": {
1264 let counter = aMessage.counter;
1265 if (!counter) {
1266 return null;
1267 }
1268 if (counter.error) {
1269 Cu.reportError(l10n.getStr(counter.error));
1270 return null;
1271 }
1272 let msg = new Messages.ConsoleGeneric(aMessage);
1273 node = msg.init(this.output).render().element;
1274 break;
1275 }
1277 default:
1278 Cu.reportError("Unknown Console API log level: " + level);
1279 return null;
1280 }
1282 // Release object actors for arguments coming from console API methods that
1283 // we ignore their arguments.
1284 switch (level) {
1285 case "group":
1286 case "groupCollapsed":
1287 case "groupEnd":
1288 case "time":
1289 case "timeEnd":
1290 case "count":
1291 for (let actor of objectActors) {
1292 this._releaseObject(actor);
1293 }
1294 objectActors.clear();
1295 }
1297 if (level == "groupEnd") {
1298 return null; // no need to continue
1299 }
1301 if (!node) {
1302 node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body,
1303 sourceURL, sourceLine, clipboardText,
1304 level, aMessage.timeStamp);
1305 if (aMessage.private) {
1306 node.setAttribute("private", true);
1307 }
1308 }
1310 if (objectActors.size > 0) {
1311 node._objectActors = objectActors;
1313 if (!node._messageObject) {
1314 let repeatNode = node.getElementsByClassName("message-repeats")[0];
1315 repeatNode._uid += [...objectActors].join("-");
1316 }
1317 }
1319 return node;
1320 },
1322 /**
1323 * Handle ConsoleAPICall objects received from the server. This method outputs
1324 * the window.console API call.
1325 *
1326 * @param object aMessage
1327 * The console API message received from the server.
1328 */
1329 handleConsoleAPICall: function WCF_handleConsoleAPICall(aMessage)
1330 {
1331 this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]);
1332 },
1334 /**
1335 * Reports an error in the page source, either JavaScript or CSS.
1336 *
1337 * @param nsIScriptError aScriptError
1338 * The error message to report.
1339 * @return nsIDOMElement|undefined
1340 * The message element to display in the Web Console output.
1341 */
1342 reportPageError: function WCF_reportPageError(aCategory, aScriptError)
1343 {
1344 // Warnings and legacy strict errors become warnings; other types become
1345 // errors.
1346 let severity = SEVERITY_ERROR;
1347 if (aScriptError.warning || aScriptError.strict) {
1348 severity = SEVERITY_WARNING;
1349 }
1351 let objectActors = new Set();
1353 // Gather the actor IDs.
1354 for (let prop of ["errorMessage", "lineText"]) {
1355 let grip = aScriptError[prop];
1356 if (WebConsoleUtils.isActorGrip(grip)) {
1357 objectActors.add(grip.actor);
1358 }
1359 }
1361 let errorMessage = aScriptError.errorMessage;
1362 if (errorMessage.type && errorMessage.type == "longString") {
1363 errorMessage = errorMessage.initial;
1364 }
1366 let node = this.createMessageNode(aCategory, severity,
1367 errorMessage,
1368 aScriptError.sourceName,
1369 aScriptError.lineNumber, null, null,
1370 aScriptError.timeStamp);
1372 // Select the body of the message node that is displayed in the console
1373 let msgBody = node.getElementsByClassName("message-body")[0];
1374 // Add the more info link node to messages that belong to certain categories
1375 this.addMoreInfoLink(msgBody, aScriptError);
1377 if (aScriptError.private) {
1378 node.setAttribute("private", true);
1379 }
1381 if (objectActors.size > 0) {
1382 node._objectActors = objectActors;
1383 }
1385 return node;
1386 },
1388 /**
1389 * Handle PageError objects received from the server. This method outputs the
1390 * given error.
1391 *
1392 * @param nsIScriptError aPageError
1393 * The error received from the server.
1394 */
1395 handlePageError: function WCF_handlePageError(aPageError)
1396 {
1397 let category = Utils.categoryForScriptError(aPageError);
1398 this.outputMessage(category, this.reportPageError, [category, aPageError]);
1399 },
1401 /**
1402 * Handle log messages received from the server. This method outputs the given
1403 * message.
1404 *
1405 * @param object aPacket
1406 * The message packet received from the server.
1407 */
1408 handleLogMessage: function WCF_handleLogMessage(aPacket)
1409 {
1410 if (aPacket.message) {
1411 this.outputMessage(CATEGORY_JS, this._reportLogMessage, [aPacket]);
1412 }
1413 },
1415 /**
1416 * Display log messages received from the server.
1417 *
1418 * @private
1419 * @param object aPacket
1420 * The message packet received from the server.
1421 * @return nsIDOMElement
1422 * The message element to render for the given log message.
1423 */
1424 _reportLogMessage: function WCF__reportLogMessage(aPacket)
1425 {
1426 let msg = aPacket.message;
1427 if (msg.type && msg.type == "longString") {
1428 msg = msg.initial;
1429 }
1430 let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null,
1431 null, null, null, aPacket.timeStamp);
1432 if (WebConsoleUtils.isActorGrip(aPacket.message)) {
1433 node._objectActors = new Set([aPacket.message.actor]);
1434 }
1435 return node;
1436 },
1438 /**
1439 * Log network event.
1440 *
1441 * @param object aActorId
1442 * The network event actor ID to log.
1443 * @return nsIDOMElement|null
1444 * The message element to display in the Web Console output.
1445 */
1446 logNetEvent: function WCF_logNetEvent(aActorId)
1447 {
1448 let networkInfo = this._networkRequests[aActorId];
1449 if (!networkInfo) {
1450 return null;
1451 }
1453 let request = networkInfo.request;
1454 let clipboardText = request.method + " " + request.url;
1455 let severity = SEVERITY_LOG;
1456 let mixedRequest =
1457 WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation);
1458 if (mixedRequest) {
1459 severity = SEVERITY_WARNING;
1460 }
1462 let methodNode = this.document.createElementNS(XHTML_NS, "span");
1463 methodNode.className = "method";
1464 methodNode.textContent = request.method + " ";
1466 let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity,
1467 methodNode, null, null,
1468 clipboardText);
1469 if (networkInfo.private) {
1470 messageNode.setAttribute("private", true);
1471 }
1472 messageNode._connectionId = aActorId;
1473 messageNode.url = request.url;
1475 let body = methodNode.parentNode;
1476 body.setAttribute("aria-haspopup", true);
1478 let displayUrl = request.url;
1479 let pos = displayUrl.indexOf("?");
1480 if (pos > -1) {
1481 displayUrl = displayUrl.substr(0, pos);
1482 }
1484 let urlNode = this.document.createElementNS(XHTML_NS, "a");
1485 urlNode.className = "url";
1486 urlNode.setAttribute("title", request.url);
1487 urlNode.href = request.url;
1488 urlNode.textContent = displayUrl;
1489 urlNode.draggable = false;
1490 body.appendChild(urlNode);
1491 body.appendChild(this.document.createTextNode(" "));
1493 if (mixedRequest) {
1494 messageNode.classList.add("mixed-content");
1495 this.makeMixedContentNode(body);
1496 }
1498 let statusNode = this.document.createElementNS(XHTML_NS, "a");
1499 statusNode.className = "status";
1500 body.appendChild(statusNode);
1502 let onClick = () => {
1503 if (!messageNode._panelOpen) {
1504 this.openNetworkPanel(messageNode, networkInfo);
1505 }
1506 };
1508 this._addMessageLinkCallback(urlNode, onClick);
1509 this._addMessageLinkCallback(statusNode, onClick);
1511 networkInfo.node = messageNode;
1513 this._updateNetMessage(aActorId);
1515 return messageNode;
1516 },
1518 /**
1519 * Create a mixed content warning Node.
1520 *
1521 * @param aLinkNode
1522 * Parent to the requested urlNode.
1523 */
1524 makeMixedContentNode: function WCF_makeMixedContentNode(aLinkNode)
1525 {
1526 let mixedContentWarning = "[" + l10n.getStr("webConsoleMixedContentWarning") + "]";
1528 // Mixed content warning message links to a Learn More page
1529 let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a");
1530 mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE;
1531 mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE;
1532 mixedContentWarningNode.className = "learn-more-link";
1533 mixedContentWarningNode.textContent = mixedContentWarning;
1534 mixedContentWarningNode.draggable = false;
1536 aLinkNode.appendChild(mixedContentWarningNode);
1538 this._addMessageLinkCallback(mixedContentWarningNode, (aEvent) => {
1539 aEvent.stopPropagation();
1540 this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
1541 });
1542 },
1544 /**
1545 * Adds a more info link node to messages based on the nsIScriptError object
1546 * that we need to report to the console
1547 *
1548 * @param aNode
1549 * The node to which we will be adding the more info link node
1550 * @param aScriptError
1551 * The script error object that we are reporting to the console
1552 */
1553 addMoreInfoLink: function WCF_addMoreInfoLink(aNode, aScriptError)
1554 {
1555 let url;
1556 switch (aScriptError.category) {
1557 case "Insecure Password Field":
1558 url = INSECURE_PASSWORDS_LEARN_MORE;
1559 break;
1560 case "Mixed Content Message":
1561 case "Mixed Content Blocker":
1562 url = MIXED_CONTENT_LEARN_MORE;
1563 break;
1564 case "Invalid HSTS Headers":
1565 url = STRICT_TRANSPORT_SECURITY_LEARN_MORE;
1566 break;
1567 default:
1568 // Unknown category. Return without adding more info node.
1569 return;
1570 }
1572 this.addLearnMoreWarningNode(aNode, url);
1573 },
1575 /*
1576 * Appends a clickable warning node to the node passed
1577 * as a parameter to the function. When a user clicks on the appended
1578 * warning node, the browser navigates to the provided url.
1579 *
1580 * @param aNode
1581 * The node to which we will be adding a clickable warning node.
1582 * @param aURL
1583 * The url which points to the page where the user can learn more
1584 * about security issues associated with the specific message that's
1585 * being logged.
1586 */
1587 addLearnMoreWarningNode:
1588 function WCF_addLearnMoreWarningNode(aNode, aURL)
1589 {
1590 let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
1592 let warningNode = this.document.createElementNS(XHTML_NS, "a");
1593 warningNode.title = aURL;
1594 warningNode.href = aURL;
1595 warningNode.draggable = false;
1596 warningNode.textContent = moreInfoLabel;
1597 warningNode.className = "learn-more-link";
1599 this._addMessageLinkCallback(warningNode, (aEvent) => {
1600 aEvent.stopPropagation();
1601 this.owner.openLink(aURL);
1602 });
1604 aNode.appendChild(warningNode);
1605 },
1607 /**
1608 * Log file activity.
1609 *
1610 * @param string aFileURI
1611 * The file URI that was loaded.
1612 * @return nsIDOMElement|undefined
1613 * The message element to display in the Web Console output.
1614 */
1615 logFileActivity: function WCF_logFileActivity(aFileURI)
1616 {
1617 let urlNode = this.document.createElementNS(XHTML_NS, "a");
1618 urlNode.setAttribute("title", aFileURI);
1619 urlNode.className = "url";
1620 urlNode.textContent = aFileURI;
1621 urlNode.draggable = false;
1622 urlNode.href = aFileURI;
1624 let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG,
1625 urlNode, null, null, aFileURI);
1627 this._addMessageLinkCallback(urlNode, () => {
1628 this.owner.viewSource(aFileURI);
1629 });
1631 return outputNode;
1632 },
1634 /**
1635 * Handle the file activity messages coming from the remote Web Console.
1636 *
1637 * @param string aFileURI
1638 * The file URI that was requested.
1639 */
1640 handleFileActivity: function WCF_handleFileActivity(aFileURI)
1641 {
1642 this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [aFileURI]);
1643 },
1645 /**
1646 * Handle the reflow activity messages coming from the remote Web Console.
1647 *
1648 * @param object aMessage
1649 * An object holding information about a reflow batch.
1650 */
1651 logReflowActivity: function WCF_logReflowActivity(aMessage)
1652 {
1653 let {start, end, sourceURL, sourceLine} = aMessage;
1654 let duration = Math.round((end - start) * 100) / 100;
1655 let node = this.document.createElementNS(XHTML_NS, "span");
1656 if (sourceURL) {
1657 node.textContent = l10n.getFormatStr("reflow.messageWithLink", [duration]);
1658 let a = this.document.createElementNS(XHTML_NS, "a");
1659 a.href = "#";
1660 a.draggable = "false";
1661 let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL);
1662 let functionName = aMessage.functionName || l10n.getStr("stacktrace.anonymousFunction");
1663 a.textContent = l10n.getFormatStr("reflow.messageLinkText",
1664 [functionName, filename, sourceLine]);
1665 this._addMessageLinkCallback(a, () => {
1666 this.owner.viewSourceInDebugger(sourceURL, sourceLine);
1667 });
1668 node.appendChild(a);
1669 } else {
1670 node.textContent = l10n.getFormatStr("reflow.messageWithNoLink", [duration]);
1671 }
1672 return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node);
1673 },
1676 handleReflowActivity: function WCF_handleReflowActivity(aMessage)
1677 {
1678 this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [aMessage]);
1679 },
1681 /**
1682 * Inform user that the window.console API has been replaced by a script
1683 * in a content page.
1684 */
1685 logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI()
1686 {
1687 let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
1688 l10n.getStr("ConsoleAPIDisabled"));
1689 this.outputMessage(CATEGORY_JS, node);
1690 },
1692 /**
1693 * Handle the network events coming from the remote Web Console.
1694 *
1695 * @param object aActor
1696 * The NetworkEventActor grip.
1697 */
1698 handleNetworkEvent: function WCF_handleNetworkEvent(aActor)
1699 {
1700 let networkInfo = {
1701 node: null,
1702 actor: aActor.actor,
1703 discardRequestBody: true,
1704 discardResponseBody: true,
1705 startedDateTime: aActor.startedDateTime,
1706 request: {
1707 url: aActor.url,
1708 method: aActor.method,
1709 },
1710 response: {},
1711 timings: {},
1712 updates: [], // track the list of network event updates
1713 private: aActor.private,
1714 };
1716 this._networkRequests[aActor.actor] = networkInfo;
1717 this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor.actor]);
1718 },
1720 /**
1721 * Handle network event updates coming from the server.
1722 *
1723 * @param string aActorId
1724 * The network event actor ID.
1725 * @param string aType
1726 * Update type.
1727 * @param object aPacket
1728 * Update details.
1729 */
1730 handleNetworkEventUpdate:
1731 function WCF_handleNetworkEventUpdate(aActorId, aType, aPacket)
1732 {
1733 let networkInfo = this._networkRequests[aActorId];
1734 if (!networkInfo) {
1735 return;
1736 }
1738 networkInfo.updates.push(aType);
1740 switch (aType) {
1741 case "requestHeaders":
1742 networkInfo.request.headersSize = aPacket.headersSize;
1743 break;
1744 case "requestPostData":
1745 networkInfo.discardRequestBody = aPacket.discardRequestBody;
1746 networkInfo.request.bodySize = aPacket.dataSize;
1747 break;
1748 case "responseStart":
1749 networkInfo.response.httpVersion = aPacket.response.httpVersion;
1750 networkInfo.response.status = aPacket.response.status;
1751 networkInfo.response.statusText = aPacket.response.statusText;
1752 networkInfo.response.headersSize = aPacket.response.headersSize;
1753 networkInfo.discardResponseBody = aPacket.response.discardResponseBody;
1754 break;
1755 case "responseContent":
1756 networkInfo.response.content = {
1757 mimeType: aPacket.mimeType,
1758 };
1759 networkInfo.response.bodySize = aPacket.contentSize;
1760 networkInfo.discardResponseBody = aPacket.discardResponseBody;
1761 break;
1762 case "eventTimings":
1763 networkInfo.totalTime = aPacket.totalTime;
1764 break;
1765 }
1767 if (networkInfo.node && this._updateNetMessage(aActorId)) {
1768 this.emit("messages-updated", new Set([networkInfo.node]));
1769 }
1771 // For unit tests we pass the HTTP activity object to the test callback,
1772 // once requests complete.
1773 if (this.owner.lastFinishedRequestCallback &&
1774 networkInfo.updates.indexOf("responseContent") > -1 &&
1775 networkInfo.updates.indexOf("eventTimings") > -1) {
1776 this.owner.lastFinishedRequestCallback(networkInfo, this);
1777 }
1778 },
1780 /**
1781 * Update an output message to reflect the latest state of a network request,
1782 * given a network event actor ID.
1783 *
1784 * @private
1785 * @param string aActorId
1786 * The network event actor ID for which you want to update the message.
1787 * @return boolean
1788 * |true| if the message node was updated, or |false| otherwise.
1789 */
1790 _updateNetMessage: function WCF__updateNetMessage(aActorId)
1791 {
1792 let networkInfo = this._networkRequests[aActorId];
1793 if (!networkInfo || !networkInfo.node) {
1794 return;
1795 }
1797 let messageNode = networkInfo.node;
1798 let updates = networkInfo.updates;
1799 let hasEventTimings = updates.indexOf("eventTimings") > -1;
1800 let hasResponseStart = updates.indexOf("responseStart") > -1;
1801 let request = networkInfo.request;
1802 let response = networkInfo.response;
1803 let updated = false;
1805 if (hasEventTimings || hasResponseStart) {
1806 let status = [];
1807 if (response.httpVersion && response.status) {
1808 status = [response.httpVersion, response.status, response.statusText];
1809 }
1810 if (hasEventTimings) {
1811 status.push(l10n.getFormatStr("NetworkPanel.durationMS",
1812 [networkInfo.totalTime]));
1813 }
1814 let statusText = "[" + status.join(" ") + "]";
1816 let statusNode = messageNode.getElementsByClassName("status")[0];
1817 statusNode.textContent = statusText;
1819 messageNode.clipboardText = [request.method, request.url, statusText]
1820 .join(" ");
1822 if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE &&
1823 response.status <= MAX_HTTP_ERROR_CODE) {
1824 this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR);
1825 }
1827 updated = true;
1828 }
1830 if (messageNode._netPanel) {
1831 messageNode._netPanel.update();
1832 }
1834 return updated;
1835 },
1837 /**
1838 * Opens a NetworkPanel.
1839 *
1840 * @param nsIDOMNode aNode
1841 * The message node you want the panel to be anchored to.
1842 * @param object aHttpActivity
1843 * The HTTP activity object that holds network request and response
1844 * information. This object is given to the NetworkPanel constructor.
1845 * @return object
1846 * The new NetworkPanel instance.
1847 */
1848 openNetworkPanel: function WCF_openNetworkPanel(aNode, aHttpActivity)
1849 {
1850 let actor = aHttpActivity.actor;
1852 if (actor) {
1853 this.webConsoleClient.getRequestHeaders(actor, function(aResponse) {
1854 if (aResponse.error) {
1855 Cu.reportError("WCF_openNetworkPanel getRequestHeaders:" +
1856 aResponse.error);
1857 return;
1858 }
1860 aHttpActivity.request.headers = aResponse.headers;
1862 this.webConsoleClient.getRequestCookies(actor, onRequestCookies);
1863 }.bind(this));
1864 }
1866 let onRequestCookies = function(aResponse) {
1867 if (aResponse.error) {
1868 Cu.reportError("WCF_openNetworkPanel getRequestCookies:" +
1869 aResponse.error);
1870 return;
1871 }
1873 aHttpActivity.request.cookies = aResponse.cookies;
1875 this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders);
1876 }.bind(this);
1878 let onResponseHeaders = function(aResponse) {
1879 if (aResponse.error) {
1880 Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" +
1881 aResponse.error);
1882 return;
1883 }
1885 aHttpActivity.response.headers = aResponse.headers;
1887 this.webConsoleClient.getResponseCookies(actor, onResponseCookies);
1888 }.bind(this);
1890 let onResponseCookies = function(aResponse) {
1891 if (aResponse.error) {
1892 Cu.reportError("WCF_openNetworkPanel getResponseCookies:" +
1893 aResponse.error);
1894 return;
1895 }
1897 aHttpActivity.response.cookies = aResponse.cookies;
1899 this.webConsoleClient.getRequestPostData(actor, onRequestPostData);
1900 }.bind(this);
1902 let onRequestPostData = function(aResponse) {
1903 if (aResponse.error) {
1904 Cu.reportError("WCF_openNetworkPanel getRequestPostData:" +
1905 aResponse.error);
1906 return;
1907 }
1909 aHttpActivity.request.postData = aResponse.postData;
1910 aHttpActivity.discardRequestBody = aResponse.postDataDiscarded;
1912 this.webConsoleClient.getResponseContent(actor, onResponseContent);
1913 }.bind(this);
1915 let onResponseContent = function(aResponse) {
1916 if (aResponse.error) {
1917 Cu.reportError("WCF_openNetworkPanel getResponseContent:" +
1918 aResponse.error);
1919 return;
1920 }
1922 aHttpActivity.response.content = aResponse.content;
1923 aHttpActivity.discardResponseBody = aResponse.contentDiscarded;
1925 this.webConsoleClient.getEventTimings(actor, onEventTimings);
1926 }.bind(this);
1928 let onEventTimings = function(aResponse) {
1929 if (aResponse.error) {
1930 Cu.reportError("WCF_openNetworkPanel getEventTimings:" +
1931 aResponse.error);
1932 return;
1933 }
1935 aHttpActivity.timings = aResponse.timings;
1937 openPanel();
1938 }.bind(this);
1940 let openPanel = function() {
1941 aNode._netPanel = netPanel;
1943 let panel = netPanel.panel;
1944 panel.openPopup(aNode, "after_pointer", 0, 0, false, false);
1945 panel.sizeTo(450, 500);
1946 panel.setAttribute("hudId", this.hudId);
1948 panel.addEventListener("popuphiding", function WCF_netPanel_onHide() {
1949 panel.removeEventListener("popuphiding", WCF_netPanel_onHide);
1951 aNode._panelOpen = false;
1952 aNode._netPanel = null;
1953 });
1955 aNode._panelOpen = true;
1956 }.bind(this);
1958 let netPanel = new NetworkPanel(this.popupset, aHttpActivity, this);
1959 netPanel.linkNode = aNode;
1961 if (!actor) {
1962 openPanel();
1963 }
1965 return netPanel;
1966 },
1968 /**
1969 * Handler for page location changes.
1970 *
1971 * @param string aURI
1972 * New page location.
1973 * @param string aTitle
1974 * New page title.
1975 */
1976 onLocationChange: function WCF_onLocationChange(aURI, aTitle)
1977 {
1978 this.contentLocation = aURI;
1979 if (this.owner.onLocationChange) {
1980 this.owner.onLocationChange(aURI, aTitle);
1981 }
1982 },
1984 /**
1985 * Handler for the tabNavigated notification.
1986 *
1987 * @param string aEvent
1988 * Event name.
1989 * @param object aPacket
1990 * Notification packet received from the server.
1991 */
1992 handleTabNavigated: function WCF_handleTabNavigated(aEvent, aPacket)
1993 {
1994 if (aEvent == "will-navigate") {
1995 if (this.persistLog) {
1996 let marker = new Messages.NavigationMarker(aPacket.url, Date.now());
1997 this.output.addMessage(marker);
1998 }
1999 else {
2000 this.jsterm.clearOutput();
2001 }
2002 }
2004 if (aPacket.url) {
2005 this.onLocationChange(aPacket.url, aPacket.title);
2006 }
2008 if (aEvent == "navigate" && !aPacket.nativeConsoleAPI) {
2009 this.logWarningAboutReplacedAPI();
2010 }
2011 },
2013 /**
2014 * Output a message node. This filters a node appropriately, then sends it to
2015 * the output, regrouping and pruning output as necessary.
2016 *
2017 * Note: this call is async - the given message node may not be displayed when
2018 * you call this method.
2019 *
2020 * @param integer aCategory
2021 * The category of the message you want to output. See the CATEGORY_*
2022 * constants.
2023 * @param function|nsIDOMElement aMethodOrNode
2024 * The method that creates the message element to send to the output or
2025 * the actual element. If a method is given it will be bound to the HUD
2026 * object and the arguments will be |aArguments|.
2027 * @param array [aArguments]
2028 * If a method is given to output the message element then the method
2029 * will be invoked with the list of arguments given here.
2030 */
2031 outputMessage: function WCF_outputMessage(aCategory, aMethodOrNode, aArguments)
2032 {
2033 if (!this._outputQueue.length) {
2034 // If the queue is empty we consider that now was the last output flush.
2035 // This avoid an immediate output flush when the timer executes.
2036 this._lastOutputFlush = Date.now();
2037 }
2039 this._outputQueue.push([aCategory, aMethodOrNode, aArguments]);
2041 if (!this._outputTimerInitialized) {
2042 this._initOutputTimer();
2043 }
2044 },
2046 /**
2047 * Try to flush the output message queue. This takes the messages in the
2048 * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL.
2049 * Further output is queued to happen later - see OUTPUT_INTERVAL.
2050 *
2051 * @private
2052 */
2053 _flushMessageQueue: function WCF__flushMessageQueue()
2054 {
2055 if (!this._outputTimer) {
2056 return;
2057 }
2059 let timeSinceFlush = Date.now() - this._lastOutputFlush;
2060 if (this._outputQueue.length > MESSAGES_IN_INTERVAL &&
2061 timeSinceFlush < THROTTLE_UPDATES) {
2062 this._initOutputTimer();
2063 return;
2064 }
2066 // Determine how many messages we can display now.
2067 let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL);
2068 if (toDisplay < 1) {
2069 this._outputTimerInitialized = false;
2070 return;
2071 }
2073 // Try to prune the message queue.
2074 let shouldPrune = false;
2075 if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) {
2076 toDisplay = Math.min(this._outputQueue.length, toDisplay);
2077 shouldPrune = true;
2078 }
2080 let batch = this._outputQueue.splice(0, toDisplay);
2081 if (!batch.length) {
2082 this._outputTimerInitialized = false;
2083 return;
2084 }
2086 let outputNode = this.outputNode;
2087 let lastVisibleNode = null;
2088 let scrollNode = outputNode.parentNode;
2089 let scrolledToBottom = Utils.isOutputScrolledToBottom(outputNode);
2090 let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId);
2092 // Output the current batch of messages.
2093 let newMessages = new Set();
2094 let updatedMessages = new Set();
2095 for (let item of batch) {
2096 let result = this._outputMessageFromQueue(hudIdSupportsString, item);
2097 if (result) {
2098 if (result.isRepeated) {
2099 updatedMessages.add(result.isRepeated);
2100 }
2101 else {
2102 newMessages.add(result.node);
2103 }
2104 if (result.visible && result.node == this.outputNode.lastChild) {
2105 lastVisibleNode = result.node;
2106 }
2107 }
2108 }
2110 let oldScrollHeight = 0;
2112 // Prune messages if needed. We do not do this for every flush call to
2113 // improve performance.
2114 let removedNodes = 0;
2115 if (shouldPrune || !this._outputQueue.length) {
2116 oldScrollHeight = scrollNode.scrollHeight;
2118 let categories = Object.keys(this._pruneCategoriesQueue);
2119 categories.forEach(function _pruneOutput(aCategory) {
2120 removedNodes += this.pruneOutputIfNecessary(aCategory);
2121 }, this);
2122 this._pruneCategoriesQueue = {};
2123 }
2125 let isInputOutput = lastVisibleNode &&
2126 (lastVisibleNode.category == CATEGORY_INPUT ||
2127 lastVisibleNode.category == CATEGORY_OUTPUT);
2129 // Scroll to the new node if it is not filtered, and if the output node is
2130 // scrolled at the bottom or if the new node is a jsterm input/output
2131 // message.
2132 if (lastVisibleNode && (scrolledToBottom || isInputOutput)) {
2133 Utils.scrollToVisible(lastVisibleNode);
2134 }
2135 else if (!scrolledToBottom && removedNodes > 0 &&
2136 oldScrollHeight != scrollNode.scrollHeight) {
2137 // If there were pruned messages and if scroll is not at the bottom, then
2138 // we need to adjust the scroll location.
2139 scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight;
2140 }
2142 if (newMessages.size) {
2143 this.emit("messages-added", newMessages);
2144 }
2145 if (updatedMessages.size) {
2146 this.emit("messages-updated", updatedMessages);
2147 }
2149 // If the queue is not empty, schedule another flush.
2150 if (this._outputQueue.length > 0) {
2151 this._initOutputTimer();
2152 }
2153 else {
2154 this._outputTimerInitialized = false;
2155 if (this._flushCallback && this._flushCallback() === false) {
2156 this._flushCallback = null;
2157 }
2158 }
2160 this._lastOutputFlush = Date.now();
2161 },
2163 /**
2164 * Initialize the output timer.
2165 * @private
2166 */
2167 _initOutputTimer: function WCF__initOutputTimer()
2168 {
2169 if (!this._outputTimer) {
2170 return;
2171 }
2173 this._outputTimerInitialized = true;
2174 this._outputTimer.initWithCallback(this._flushMessageQueue,
2175 OUTPUT_INTERVAL,
2176 Ci.nsITimer.TYPE_ONE_SHOT);
2177 },
2179 /**
2180 * Output a message from the queue.
2181 *
2182 * @private
2183 * @param nsISupportsString aHudIdSupportsString
2184 * The HUD ID as an nsISupportsString.
2185 * @param array aItem
2186 * An item from the output queue - this item represents a message.
2187 * @return object
2188 * An object that holds the following properties:
2189 * - node: the DOM element of the message.
2190 * - isRepeated: the DOM element of the original message, if this is
2191 * a repeated message, otherwise null.
2192 * - visible: boolean that tells if the message is visible.
2193 */
2194 _outputMessageFromQueue:
2195 function WCF__outputMessageFromQueue(aHudIdSupportsString, aItem)
2196 {
2197 let [category, methodOrNode, args] = aItem;
2199 let node = typeof methodOrNode == "function" ?
2200 methodOrNode.apply(this, args || []) :
2201 methodOrNode;
2202 if (!node) {
2203 return null;
2204 }
2206 let afterNode = node._outputAfterNode;
2207 if (afterNode) {
2208 delete node._outputAfterNode;
2209 }
2211 let isFiltered = this.filterMessageNode(node);
2213 let isRepeated = this._filterRepeatedMessage(node);
2215 let visible = !isRepeated && !isFiltered;
2216 if (!isRepeated) {
2217 this.outputNode.insertBefore(node,
2218 afterNode ? afterNode.nextSibling : null);
2219 this._pruneCategoriesQueue[node.category] = true;
2221 let nodeID = node.getAttribute("id");
2222 Services.obs.notifyObservers(aHudIdSupportsString,
2223 "web-console-message-created", nodeID);
2225 }
2227 if (node._onOutput) {
2228 node._onOutput();
2229 delete node._onOutput;
2230 }
2232 return {
2233 visible: visible,
2234 node: node,
2235 isRepeated: isRepeated,
2236 };
2237 },
2239 /**
2240 * Prune the queue of messages to display. This avoids displaying messages
2241 * that will be removed at the end of the queue anyway.
2242 * @private
2243 */
2244 _pruneOutputQueue: function WCF__pruneOutputQueue()
2245 {
2246 let nodes = {};
2248 // Group the messages per category.
2249 this._outputQueue.forEach(function(aItem, aIndex) {
2250 let [category] = aItem;
2251 if (!(category in nodes)) {
2252 nodes[category] = [];
2253 }
2254 nodes[category].push(aIndex);
2255 }, this);
2257 let pruned = 0;
2259 // Loop through the categories we found and prune if needed.
2260 for (let category in nodes) {
2261 let limit = Utils.logLimitForCategory(category);
2262 let indexes = nodes[category];
2263 if (indexes.length > limit) {
2264 let n = Math.max(0, indexes.length - limit);
2265 pruned += n;
2266 for (let i = n - 1; i >= 0; i--) {
2267 this._pruneItemFromQueue(this._outputQueue[indexes[i]]);
2268 this._outputQueue.splice(indexes[i], 1);
2269 }
2270 }
2271 }
2273 return pruned;
2274 },
2276 /**
2277 * Prune an item from the output queue.
2278 *
2279 * @private
2280 * @param array aItem
2281 * The item you want to remove from the output queue.
2282 */
2283 _pruneItemFromQueue: function WCF__pruneItemFromQueue(aItem)
2284 {
2285 // TODO: handle object releasing in a more elegant way once all console
2286 // messages use the new API - bug 778766.
2288 let [category, methodOrNode, args] = aItem;
2289 if (typeof methodOrNode != "function" && methodOrNode._objectActors) {
2290 for (let actor of methodOrNode._objectActors) {
2291 this._releaseObject(actor);
2292 }
2293 methodOrNode._objectActors.clear();
2294 }
2296 if (methodOrNode == this.output._flushMessageQueue &&
2297 args[0]._objectActors) {
2298 for (let arg of args) {
2299 if (!arg._objectActors) {
2300 continue;
2301 }
2302 for (let actor of arg._objectActors) {
2303 this._releaseObject(actor);
2304 }
2305 arg._objectActors.clear();
2306 }
2307 }
2309 if (category == CATEGORY_NETWORK) {
2310 let connectionId = null;
2311 if (methodOrNode == this.logNetEvent) {
2312 connectionId = args[0];
2313 }
2314 else if (typeof methodOrNode != "function") {
2315 connectionId = methodOrNode._connectionId;
2316 }
2317 if (connectionId && connectionId in this._networkRequests) {
2318 delete this._networkRequests[connectionId];
2319 this._releaseObject(connectionId);
2320 }
2321 }
2322 else if (category == CATEGORY_WEBDEV &&
2323 methodOrNode == this.logConsoleAPIMessage) {
2324 args[0].arguments.forEach((aValue) => {
2325 if (WebConsoleUtils.isActorGrip(aValue)) {
2326 this._releaseObject(aValue.actor);
2327 }
2328 });
2329 }
2330 else if (category == CATEGORY_JS &&
2331 methodOrNode == this.reportPageError) {
2332 let pageError = args[1];
2333 for (let prop of ["errorMessage", "lineText"]) {
2334 let grip = pageError[prop];
2335 if (WebConsoleUtils.isActorGrip(grip)) {
2336 this._releaseObject(grip.actor);
2337 }
2338 }
2339 }
2340 else if (category == CATEGORY_JS &&
2341 methodOrNode == this._reportLogMessage) {
2342 if (WebConsoleUtils.isActorGrip(args[0].message)) {
2343 this._releaseObject(args[0].message.actor);
2344 }
2345 }
2346 },
2348 /**
2349 * Ensures that the number of message nodes of type aCategory don't exceed that
2350 * category's line limit by removing old messages as needed.
2351 *
2352 * @param integer aCategory
2353 * The category of message nodes to prune if needed.
2354 * @return number
2355 * The number of removed nodes.
2356 */
2357 pruneOutputIfNecessary: function WCF_pruneOutputIfNecessary(aCategory)
2358 {
2359 let logLimit = Utils.logLimitForCategory(aCategory);
2360 let messageNodes = this.outputNode.querySelectorAll(".message[category=" +
2361 CATEGORY_CLASS_FRAGMENTS[aCategory] + "]");
2362 let n = Math.max(0, messageNodes.length - logLimit);
2363 let toRemove = Array.prototype.slice.call(messageNodes, 0, n);
2364 toRemove.forEach(this.removeOutputMessage, this);
2366 return n;
2367 },
2369 /**
2370 * Remove a given message from the output.
2371 *
2372 * @param nsIDOMNode aNode
2373 * The message node you want to remove.
2374 */
2375 removeOutputMessage: function WCF_removeOutputMessage(aNode)
2376 {
2377 if (aNode._messageObject) {
2378 aNode._messageObject.destroy();
2379 }
2381 if (aNode._objectActors) {
2382 for (let actor of aNode._objectActors) {
2383 this._releaseObject(actor);
2384 }
2385 aNode._objectActors.clear();
2386 }
2388 if (aNode.category == CATEGORY_CSS ||
2389 aNode.category == CATEGORY_SECURITY) {
2390 let repeatNode = aNode.getElementsByClassName("message-repeats")[0];
2391 if (repeatNode && repeatNode._uid) {
2392 delete this._repeatNodes[repeatNode._uid];
2393 }
2394 }
2395 else if (aNode._connectionId &&
2396 aNode.category == CATEGORY_NETWORK) {
2397 delete this._networkRequests[aNode._connectionId];
2398 this._releaseObject(aNode._connectionId);
2399 }
2400 else if (aNode.classList.contains("inlined-variables-view")) {
2401 let view = aNode._variablesView;
2402 if (view) {
2403 view.controller.releaseActors();
2404 }
2405 aNode._variablesView = null;
2406 }
2408 if (aNode.parentNode) {
2409 aNode.parentNode.removeChild(aNode);
2410 }
2411 },
2413 /**
2414 * Given a category and message body, creates a DOM node to represent an
2415 * incoming message. The timestamp is automatically added.
2416 *
2417 * @param number aCategory
2418 * The category of the message: one of the CATEGORY_* constants.
2419 * @param number aSeverity
2420 * The severity of the message: one of the SEVERITY_* constants;
2421 * @param string|nsIDOMNode aBody
2422 * The body of the message, either a simple string or a DOM node.
2423 * @param string aSourceURL [optional]
2424 * The URL of the source file that emitted the error.
2425 * @param number aSourceLine [optional]
2426 * The line number on which the error occurred. If zero or omitted,
2427 * there is no line number associated with this message.
2428 * @param string aClipboardText [optional]
2429 * The text that should be copied to the clipboard when this node is
2430 * copied. If omitted, defaults to the body text. If `aBody` is not
2431 * a string, then the clipboard text must be supplied.
2432 * @param number aLevel [optional]
2433 * The level of the console API message.
2434 * @param number aTimeStamp [optional]
2435 * The timestamp to use for this message node. If omitted, the current
2436 * date and time is used.
2437 * @return nsIDOMNode
2438 * The message node: a DIV ready to be inserted into the Web Console
2439 * output node.
2440 */
2441 createMessageNode:
2442 function WCF_createMessageNode(aCategory, aSeverity, aBody, aSourceURL,
2443 aSourceLine, aClipboardText, aLevel, aTimeStamp)
2444 {
2445 if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) {
2446 aClipboardText = aBody.innerText;
2447 }
2449 let indentNode = this.document.createElementNS(XHTML_NS, "span");
2450 indentNode.className = "indent";
2452 // Apply the current group by indenting appropriately.
2453 let indent = this.groupDepth * GROUP_INDENT;
2454 indentNode.style.width = indent + "px";
2456 // Make the icon container, which is a vertical box. Its purpose is to
2457 // ensure that the icon stays anchored at the top of the message even for
2458 // long multi-line messages.
2459 let iconContainer = this.document.createElementNS(XHTML_NS, "span");
2460 iconContainer.className = "icon";
2462 // Create the message body, which contains the actual text of the message.
2463 let bodyNode = this.document.createElementNS(XHTML_NS, "span");
2464 bodyNode.className = "message-body-wrapper message-body devtools-monospace";
2466 // Store the body text, since it is needed later for the variables view.
2467 let body = aBody;
2468 // If a string was supplied for the body, turn it into a DOM node and an
2469 // associated clipboard string now.
2470 aClipboardText = aClipboardText ||
2471 (aBody + (aSourceURL ? " @ " + aSourceURL : "") +
2472 (aSourceLine ? ":" + aSourceLine : ""));
2474 let timestamp = aTimeStamp || Date.now();
2476 // Create the containing node and append all its elements to it.
2477 let node = this.document.createElementNS(XHTML_NS, "div");
2478 node.id = "console-msg-" + gSequenceId();
2479 node.className = "message";
2480 node.clipboardText = aClipboardText;
2481 node.timestamp = timestamp;
2482 this.setMessageType(node, aCategory, aSeverity);
2484 if (aBody instanceof Ci.nsIDOMNode) {
2485 bodyNode.appendChild(aBody);
2486 }
2487 else {
2488 let str = undefined;
2489 if (aLevel == "dir") {
2490 str = VariablesView.getString(aBody.arguments[0]);
2491 }
2492 else {
2493 str = aBody;
2494 }
2496 if (str !== undefined) {
2497 aBody = this.document.createTextNode(str);
2498 bodyNode.appendChild(aBody);
2499 }
2500 }
2502 // Add the message repeats node only when needed.
2503 let repeatNode = null;
2504 if (aCategory != CATEGORY_INPUT &&
2505 aCategory != CATEGORY_OUTPUT &&
2506 aCategory != CATEGORY_NETWORK &&
2507 !(aCategory == CATEGORY_CSS && aSeverity == SEVERITY_LOG)) {
2508 repeatNode = this.document.createElementNS(XHTML_NS, "span");
2509 repeatNode.setAttribute("value", "1");
2510 repeatNode.className = "message-repeats";
2511 repeatNode.textContent = 1;
2512 repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel,
2513 aSourceURL, aSourceLine].join(":");
2514 }
2516 // Create the timestamp.
2517 let timestampNode = this.document.createElementNS(XHTML_NS, "span");
2518 timestampNode.className = "timestamp devtools-monospace";
2520 let timestampString = l10n.timestampString(timestamp);
2521 timestampNode.textContent = timestampString + " ";
2523 // Create the source location (e.g. www.example.com:6) that sits on the
2524 // right side of the message, if applicable.
2525 let locationNode;
2526 if (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) {
2527 locationNode = this.createLocationNode(aSourceURL, aSourceLine);
2528 }
2530 node.appendChild(timestampNode);
2531 node.appendChild(indentNode);
2532 node.appendChild(iconContainer);
2534 // Display the variables view after the message node.
2535 if (aLevel == "dir") {
2536 bodyNode.style.height = (this.window.innerHeight *
2537 CONSOLE_DIR_VIEW_HEIGHT) + "px";
2539 let options = {
2540 objectActor: body.arguments[0],
2541 targetElement: bodyNode,
2542 hideFilterInput: true,
2543 };
2544 this.jsterm.openVariablesView(options).then((aView) => {
2545 node._variablesView = aView;
2546 if (node.classList.contains("hidden-message")) {
2547 node.classList.remove("hidden-message");
2548 }
2549 });
2551 node.classList.add("inlined-variables-view");
2552 }
2554 node.appendChild(bodyNode);
2555 if (repeatNode) {
2556 node.appendChild(repeatNode);
2557 }
2558 if (locationNode) {
2559 node.appendChild(locationNode);
2560 }
2561 node.appendChild(this.document.createTextNode("\n"));
2563 return node;
2564 },
2566 /**
2567 * Creates the anchor that displays the textual location of an incoming
2568 * message.
2569 *
2570 * @param string aSourceURL
2571 * The URL of the source file responsible for the error.
2572 * @param number aSourceLine [optional]
2573 * The line number on which the error occurred. If zero or omitted,
2574 * there is no line number associated with this message.
2575 * @param string aTarget [optional]
2576 * Tells which tool to open the link with, on click. Supported tools:
2577 * jsdebugger, styleeditor, scratchpad.
2578 * @return nsIDOMNode
2579 * The new anchor element, ready to be added to the message node.
2580 */
2581 createLocationNode:
2582 function WCF_createLocationNode(aSourceURL, aSourceLine, aTarget)
2583 {
2584 if (!aSourceURL) {
2585 aSourceURL = "";
2586 }
2587 let locationNode = this.document.createElementNS(XHTML_NS, "a");
2588 let filenameNode = this.document.createElementNS(XHTML_NS, "span");
2590 // Create the text, which consists of an abbreviated version of the URL
2591 // Scratchpad URLs should not be abbreviated.
2592 let filename;
2593 let fullURL;
2594 let isScratchpad = false;
2596 if (/^Scratchpad\/\d+$/.test(aSourceURL)) {
2597 filename = aSourceURL;
2598 fullURL = aSourceURL;
2599 isScratchpad = true;
2600 }
2601 else {
2602 fullURL = aSourceURL.split(" -> ").pop();
2603 filename = WebConsoleUtils.abbreviateSourceURL(fullURL);
2604 }
2606 filenameNode.className = "filename";
2607 filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation"));
2608 locationNode.appendChild(filenameNode);
2610 locationNode.href = isScratchpad || !fullURL ? "#" : fullURL;
2611 locationNode.draggable = false;
2612 if (aTarget) {
2613 locationNode.target = aTarget;
2614 }
2615 locationNode.setAttribute("title", aSourceURL);
2616 locationNode.className = "message-location theme-link devtools-monospace";
2618 // Make the location clickable.
2619 let onClick = () => {
2620 let target = locationNode.target;
2621 if (target == "scratchpad" || isScratchpad) {
2622 this.owner.viewSourceInScratchpad(aSourceURL);
2623 return;
2624 }
2626 let category = locationNode.parentNode.category;
2627 if (target == "styleeditor" || category == CATEGORY_CSS) {
2628 this.owner.viewSourceInStyleEditor(fullURL, aSourceLine);
2629 }
2630 else if (target == "jsdebugger" ||
2631 category == CATEGORY_JS || category == CATEGORY_WEBDEV) {
2632 this.owner.viewSourceInDebugger(fullURL, aSourceLine);
2633 }
2634 else {
2635 this.owner.viewSource(fullURL, aSourceLine);
2636 }
2637 };
2639 if (fullURL) {
2640 this._addMessageLinkCallback(locationNode, onClick);
2641 }
2643 if (aSourceLine) {
2644 let lineNumberNode = this.document.createElementNS(XHTML_NS, "span");
2645 lineNumberNode.className = "line-number";
2646 lineNumberNode.textContent = ":" + aSourceLine;
2647 locationNode.appendChild(lineNumberNode);
2648 locationNode.sourceLine = aSourceLine;
2649 }
2651 return locationNode;
2652 },
2654 /**
2655 * Adjusts the category and severity of the given message.
2656 *
2657 * @param nsIDOMNode aMessageNode
2658 * The message node to alter.
2659 * @param number aCategory
2660 * The category for the message; one of the CATEGORY_ constants.
2661 * @param number aSeverity
2662 * The severity for the message; one of the SEVERITY_ constants.
2663 * @return void
2664 */
2665 setMessageType:
2666 function WCF_setMessageType(aMessageNode, aCategory, aSeverity)
2667 {
2668 aMessageNode.category = aCategory;
2669 aMessageNode.severity = aSeverity;
2670 aMessageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[aCategory]);
2671 aMessageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[aSeverity]);
2672 aMessageNode.setAttribute("filter", MESSAGE_PREFERENCE_KEYS[aCategory][aSeverity]);
2673 },
2675 /**
2676 * Add the mouse event handlers needed to make a link.
2677 *
2678 * @private
2679 * @param nsIDOMNode aNode
2680 * The node for which you want to add the event handlers.
2681 * @param function aCallback
2682 * The function you want to invoke on click.
2683 */
2684 _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback)
2685 {
2686 aNode.addEventListener("mousedown", (aEvent) => {
2687 this._mousedown = true;
2688 this._startX = aEvent.clientX;
2689 this._startY = aEvent.clientY;
2690 }, false);
2692 aNode.addEventListener("click", (aEvent) => {
2693 let mousedown = this._mousedown;
2694 this._mousedown = false;
2696 aEvent.preventDefault();
2698 // Do not allow middle/right-click or 2+ clicks.
2699 if (aEvent.detail != 1 || aEvent.button != 0) {
2700 return;
2701 }
2703 // If this event started with a mousedown event and it ends at a different
2704 // location, we consider this text selection.
2705 if (mousedown &&
2706 (this._startX != aEvent.clientX) &&
2707 (this._startY != aEvent.clientY))
2708 {
2709 this._startX = this._startY = undefined;
2710 return;
2711 }
2713 this._startX = this._startY = undefined;
2715 aCallback.call(this, aEvent);
2716 }, false);
2717 },
2719 _addFocusCallback: function WCF__addFocusCallback(aNode, aCallback)
2720 {
2721 aNode.addEventListener("mousedown", (aEvent) => {
2722 this._mousedown = true;
2723 this._startX = aEvent.clientX;
2724 this._startY = aEvent.clientY;
2725 }, false);
2727 aNode.addEventListener("click", (aEvent) => {
2728 let mousedown = this._mousedown;
2729 this._mousedown = false;
2731 // Do not allow middle/right-click or 2+ clicks.
2732 if (aEvent.detail != 1 || aEvent.button != 0) {
2733 return;
2734 }
2736 // If this event started with a mousedown event and it ends at a different
2737 // location, we consider this text selection.
2738 // Add a fuzz modifier of two pixels in any direction to account for sloppy
2739 // clicking.
2740 if (mousedown &&
2741 (Math.abs(aEvent.clientX - this._startX) >= 2) &&
2742 (Math.abs(aEvent.clientY - this._startY) >= 1))
2743 {
2744 this._startX = this._startY = undefined;
2745 return;
2746 }
2748 this._startX = this._startY = undefined;
2750 aCallback.call(this, aEvent);
2751 }, false);
2752 },
2754 /**
2755 * Handler for the pref-changed event coming from the toolbox.
2756 * Currently this function only handles the timestamps preferences.
2757 *
2758 * @private
2759 * @param object aEvent
2760 * This parameter is a string that holds the event name
2761 * pref-changed in this case.
2762 * @param object aData
2763 * This is the pref-changed data object.
2764 */
2765 _onToolboxPrefChanged: function WCF__onToolboxPrefChanged(aEvent, aData)
2766 {
2767 if (aData.pref == PREF_MESSAGE_TIMESTAMP) {
2768 if (aData.newValue) {
2769 this.outputNode.classList.remove("hideTimestamps");
2770 }
2771 else {
2772 this.outputNode.classList.add("hideTimestamps");
2773 }
2774 }
2775 },
2777 /**
2778 * Copies the selected items to the system clipboard.
2779 *
2780 * @param object aOptions
2781 * - linkOnly:
2782 * An optional flag to copy only URL without timestamp and
2783 * other meta-information. Default is false.
2784 */
2785 copySelectedItems: function WCF_copySelectedItems(aOptions)
2786 {
2787 aOptions = aOptions || { linkOnly: false, contextmenu: false };
2789 // Gather up the selected items and concatenate their clipboard text.
2790 let strings = [];
2792 let children = this.output.getSelectedMessages();
2793 if (!children.length && aOptions.contextmenu) {
2794 children = [this._contextMenuHandler.lastClickedMessage];
2795 }
2797 for (let item of children) {
2798 // Ensure the selected item hasn't been filtered by type or string.
2799 if (!item.classList.contains("filtered-by-type") &&
2800 !item.classList.contains("filtered-by-string")) {
2801 let timestampString = l10n.timestampString(item.timestamp);
2802 if (aOptions.linkOnly) {
2803 strings.push(item.url);
2804 }
2805 else {
2806 strings.push("[" + timestampString + "] " + item.clipboardText);
2807 }
2808 }
2809 }
2811 clipboardHelper.copyString(strings.join("\n"), this.document);
2812 },
2814 /**
2815 * Object properties provider. This function gives you the properties of the
2816 * remote object you want.
2817 *
2818 * @param string aActor
2819 * The object actor ID from which you want the properties.
2820 * @param function aCallback
2821 * Function you want invoked once the properties are received.
2822 */
2823 objectPropertiesProvider:
2824 function WCF_objectPropertiesProvider(aActor, aCallback)
2825 {
2826 this.webConsoleClient.inspectObjectProperties(aActor,
2827 function(aResponse) {
2828 if (aResponse.error) {
2829 Cu.reportError("Failed to retrieve the object properties from the " +
2830 "server. Error: " + aResponse.error);
2831 return;
2832 }
2833 aCallback(aResponse.properties);
2834 });
2835 },
2837 /**
2838 * Release an actor.
2839 *
2840 * @private
2841 * @param string aActor
2842 * The actor ID you want to release.
2843 */
2844 _releaseObject: function WCF__releaseObject(aActor)
2845 {
2846 if (this.proxy) {
2847 this.proxy.releaseActor(aActor);
2848 }
2849 },
2851 /**
2852 * Open the selected item's URL in a new tab.
2853 */
2854 openSelectedItemInTab: function WCF_openSelectedItemInTab()
2855 {
2856 let item = this.output.getSelectedMessages(1)[0] ||
2857 this._contextMenuHandler.lastClickedMessage;
2859 if (!item || !item.url) {
2860 return;
2861 }
2863 this.owner.openLink(item.url);
2864 },
2866 /**
2867 * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks
2868 * when the Web Console is closed.
2869 *
2870 * @return object
2871 * A promise that is resolved when the WebConsoleFrame instance is
2872 * destroyed.
2873 */
2874 destroy: function WCF_destroy()
2875 {
2876 if (this._destroyer) {
2877 return this._destroyer.promise;
2878 }
2880 this._destroyer = promise.defer();
2882 let toolbox = gDevTools.getToolbox(this.owner.target);
2883 if (toolbox) {
2884 toolbox.off("webconsole-selected", this._onPanelSelected);
2885 }
2887 gDevTools.off("pref-changed", this._onToolboxPrefChanged);
2889 this._repeatNodes = {};
2890 this._outputQueue = [];
2891 this._pruneCategoriesQueue = {};
2892 this._networkRequests = {};
2894 if (this._outputTimerInitialized) {
2895 this._outputTimerInitialized = false;
2896 this._outputTimer.cancel();
2897 }
2898 this._outputTimer = null;
2900 if (this.jsterm) {
2901 this.jsterm.destroy();
2902 this.jsterm = null;
2903 }
2904 this.output.destroy();
2905 this.output = null;
2907 if (this._contextMenuHandler) {
2908 this._contextMenuHandler.destroy();
2909 this._contextMenuHandler = null;
2910 }
2912 this._commandController = null;
2914 let onDestroy = function() {
2915 this._destroyer.resolve(null);
2916 }.bind(this);
2918 if (this.proxy) {
2919 this.proxy.disconnect().then(onDestroy);
2920 this.proxy = null;
2921 }
2922 else {
2923 onDestroy();
2924 }
2926 return this._destroyer.promise;
2927 },
2928 };
2931 /**
2932 * @see VariablesView.simpleValueEvalMacro
2933 */
2934 function simpleValueEvalMacro(aItem, aCurrentString)
2935 {
2936 return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self");
2937 };
2940 /**
2941 * @see VariablesView.overrideValueEvalMacro
2942 */
2943 function overrideValueEvalMacro(aItem, aCurrentString)
2944 {
2945 return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self");
2946 };
2949 /**
2950 * @see VariablesView.getterOrSetterEvalMacro
2951 */
2952 function getterOrSetterEvalMacro(aItem, aCurrentString)
2953 {
2954 return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self");
2955 }
2959 /**
2960 * Create a JSTerminal (a JavaScript command line). This is attached to an
2961 * existing HeadsUpDisplay (a Web Console instance). This code is responsible
2962 * with handling command line input, code evaluation and result output.
2963 *
2964 * @constructor
2965 * @param object aWebConsoleFrame
2966 * The WebConsoleFrame object that owns this JSTerm instance.
2967 */
2968 function JSTerm(aWebConsoleFrame)
2969 {
2970 this.hud = aWebConsoleFrame;
2971 this.hudId = this.hud.hudId;
2973 this.lastCompletion = { value: null };
2974 this.history = [];
2976 // Holds the number of entries in history. This value is incremented in
2977 // this.execute().
2978 this.historyIndex = 0; // incremented on this.execute()
2980 // Holds the index of the history entry that the user is currently viewing.
2981 // This is reset to this.history.length when this.execute() is invoked.
2982 this.historyPlaceHolder = 0;
2983 this._objectActorsInVariablesViews = new Map();
2985 this._keyPress = this._keyPress.bind(this);
2986 this._inputEventHandler = this._inputEventHandler.bind(this);
2987 this._focusEventHandler = this._focusEventHandler.bind(this);
2988 this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this);
2989 this._blurEventHandler = this._blurEventHandler.bind(this);
2991 EventEmitter.decorate(this);
2992 }
2994 JSTerm.prototype = {
2995 SELECTED_FRAME: -1,
2997 /**
2998 * Stores the data for the last completion.
2999 * @type object
3000 */
3001 lastCompletion: null,
3003 /**
3004 * Array that caches the user input suggestions received from the server.
3005 * @private
3006 * @type array
3007 */
3008 _autocompleteCache: null,
3010 /**
3011 * The input that caused the last request to the server, whose response is
3012 * cached in the _autocompleteCache array.
3013 * @private
3014 * @type string
3015 */
3016 _autocompleteQuery: null,
3018 /**
3019 * The frameActorId used in the last autocomplete query. Whenever this changes
3020 * the autocomplete cache must be invalidated.
3021 * @private
3022 * @type string
3023 */
3024 _lastFrameActorId: null,
3026 /**
3027 * The Web Console sidebar.
3028 * @see this._createSidebar()
3029 * @see Sidebar.jsm
3030 */
3031 sidebar: null,
3033 /**
3034 * The Variables View instance shown in the sidebar.
3035 * @private
3036 * @type object
3037 */
3038 _variablesView: null,
3040 /**
3041 * Tells if you want the variables view UI updates to be lazy or not. Tests
3042 * disable lazy updates.
3043 *
3044 * @private
3045 * @type boolean
3046 */
3047 _lazyVariablesView: true,
3049 /**
3050 * Holds a map between VariablesView instances and sets of ObjectActor IDs
3051 * that have been retrieved from the server. This allows us to release the
3052 * objects when needed.
3053 *
3054 * @private
3055 * @type Map
3056 */
3057 _objectActorsInVariablesViews: null,
3059 /**
3060 * Last input value.
3061 * @type string
3062 */
3063 lastInputValue: "",
3065 /**
3066 * Tells if the input node changed since the last focus.
3067 *
3068 * @private
3069 * @type boolean
3070 */
3071 _inputChanged: false,
3073 /**
3074 * Tells if the autocomplete popup was navigated since the last open.
3075 *
3076 * @private
3077 * @type boolean
3078 */
3079 _autocompletePopupNavigated: false,
3081 /**
3082 * History of code that was executed.
3083 * @type array
3084 */
3085 history: null,
3086 autocompletePopup: null,
3087 inputNode: null,
3088 completeNode: null,
3090 /**
3091 * Getter for the element that holds the messages we display.
3092 * @type nsIDOMElement
3093 */
3094 get outputNode() this.hud.outputNode,
3096 /**
3097 * Getter for the debugger WebConsoleClient.
3098 * @type object
3099 */
3100 get webConsoleClient() this.hud.webConsoleClient,
3102 COMPLETE_FORWARD: 0,
3103 COMPLETE_BACKWARD: 1,
3104 COMPLETE_HINT_ONLY: 2,
3105 COMPLETE_PAGEUP: 3,
3106 COMPLETE_PAGEDOWN: 4,
3108 /**
3109 * Initialize the JSTerminal UI.
3110 */
3111 init: function JST_init()
3112 {
3113 let autocompleteOptions = {
3114 onSelect: this.onAutocompleteSelect.bind(this),
3115 onClick: this.acceptProposedCompletion.bind(this),
3116 panelId: "webConsole_autocompletePopup",
3117 listBoxId: "webConsole_autocompletePopupListBox",
3118 position: "before_start",
3119 theme: "auto",
3120 direction: "ltr",
3121 autoSelect: true
3122 };
3123 this.autocompletePopup = new AutocompletePopup(this.hud.document,
3124 autocompleteOptions);
3126 let doc = this.hud.document;
3127 let inputContainer = doc.querySelector(".jsterm-input-container");
3128 this.completeNode = doc.querySelector(".jsterm-complete-node");
3129 this.inputNode = doc.querySelector(".jsterm-input-node");
3131 if (this.hud.owner._browserConsole &&
3132 !Services.prefs.getBoolPref("devtools.chrome.enabled")) {
3133 inputContainer.style.display = "none";
3134 }
3135 else {
3136 this.inputNode.addEventListener("keypress", this._keyPress, false);
3137 this.inputNode.addEventListener("input", this._inputEventHandler, false);
3138 this.inputNode.addEventListener("keyup", this._inputEventHandler, false);
3139 this.inputNode.addEventListener("focus", this._focusEventHandler, false);
3140 }
3142 this.hud.window.addEventListener("blur", this._blurEventHandler, false);
3143 this.lastInputValue && this.setInputValue(this.lastInputValue);
3144 },
3146 /**
3147 * The JavaScript evaluation response handler.
3148 *
3149 * @private
3150 * @param object [aAfterMessage]
3151 * Optional message after which the evaluation result will be
3152 * inserted.
3153 * @param function [aCallback]
3154 * Optional function to invoke when the evaluation result is added to
3155 * the output.
3156 * @param object aResponse
3157 * The message received from the server.
3158 */
3159 _executeResultCallback:
3160 function JST__executeResultCallback(aAfterMessage, aCallback, aResponse)
3161 {
3162 if (!this.hud) {
3163 return;
3164 }
3165 if (aResponse.error) {
3166 Cu.reportError("Evaluation error " + aResponse.error + ": " +
3167 aResponse.message);
3168 return;
3169 }
3170 let errorMessage = aResponse.exceptionMessage;
3171 let result = aResponse.result;
3172 let helperResult = aResponse.helperResult;
3173 let helperHasRawOutput = !!(helperResult || {}).rawOutput;
3175 if (helperResult && helperResult.type) {
3176 switch (helperResult.type) {
3177 case "clearOutput":
3178 this.clearOutput();
3179 break;
3180 case "inspectObject":
3181 if (aAfterMessage) {
3182 if (!aAfterMessage._objectActors) {
3183 aAfterMessage._objectActors = new Set();
3184 }
3185 aAfterMessage._objectActors.add(helperResult.object.actor);
3186 }
3187 this.openVariablesView({
3188 label: VariablesView.getString(helperResult.object, { concise: true }),
3189 objectActor: helperResult.object,
3190 });
3191 break;
3192 case "error":
3193 try {
3194 errorMessage = l10n.getStr(helperResult.message);
3195 }
3196 catch (ex) {
3197 errorMessage = helperResult.message;
3198 }
3199 break;
3200 case "help":
3201 this.hud.owner.openLink(HELP_URL);
3202 break;
3203 }
3204 }
3206 // Hide undefined results coming from JSTerm helper functions.
3207 if (!errorMessage && result && typeof result == "object" &&
3208 result.type == "undefined" &&
3209 helperResult && !helperHasRawOutput) {
3210 aCallback && aCallback();
3211 return;
3212 }
3214 let msg = new Messages.JavaScriptEvalOutput(aResponse, errorMessage);
3215 this.hud.output.addMessage(msg);
3217 if (aCallback) {
3218 let oldFlushCallback = this.hud._flushCallback;
3219 this.hud._flushCallback = () => {
3220 aCallback(msg.element);
3221 if (oldFlushCallback) {
3222 oldFlushCallback();
3223 this.hud._flushCallback = oldFlushCallback;
3224 return true;
3225 }
3227 return false;
3228 };
3229 }
3231 msg._afterMessage = aAfterMessage;
3232 msg._objectActors = new Set();
3234 if (WebConsoleUtils.isActorGrip(aResponse.exception)) {
3235 msg._objectActors.add(aResponse.exception.actor);
3236 }
3238 if (WebConsoleUtils.isActorGrip(result)) {
3239 msg._objectActors.add(result.actor);
3240 }
3241 },
3243 /**
3244 * Execute a string. Execution happens asynchronously in the content process.
3245 *
3246 * @param string [aExecuteString]
3247 * The string you want to execute. If this is not provided, the current
3248 * user input is used - taken from |this.inputNode.value|.
3249 * @param function [aCallback]
3250 * Optional function to invoke when the result is displayed.
3251 */
3252 execute: function JST_execute(aExecuteString, aCallback)
3253 {
3254 // attempt to execute the content of the inputNode
3255 aExecuteString = aExecuteString || this.inputNode.value;
3256 if (!aExecuteString) {
3257 return;
3258 }
3260 let message = new Messages.Simple(aExecuteString, {
3261 category: "input",
3262 severity: "log",
3263 });
3264 this.hud.output.addMessage(message);
3265 let onResult = this._executeResultCallback.bind(this, message, aCallback);
3267 let options = { frame: this.SELECTED_FRAME };
3268 this.requestEvaluation(aExecuteString, options).then(onResult, onResult);
3270 // Append a new value in the history of executed code, or overwrite the most
3271 // recent entry. The most recent entry may contain the last edited input
3272 // value that was not evaluated yet.
3273 this.history[this.historyIndex++] = aExecuteString;
3274 this.historyPlaceHolder = this.history.length;
3275 this.setInputValue("");
3276 this.clearCompletion();
3277 },
3279 /**
3280 * Request a JavaScript string evaluation from the server.
3281 *
3282 * @param string aString
3283 * String to execute.
3284 * @param object [aOptions]
3285 * Options for evaluation:
3286 * - bindObjectActor: tells the ObjectActor ID for which you want to do
3287 * the evaluation. The Debugger.Object of the OA will be bound to
3288 * |_self| during evaluation, such that it's usable in the string you
3289 * execute.
3290 * - frame: tells the stackframe depth to evaluate the string in. If
3291 * the jsdebugger is paused, you can pick the stackframe to be used for
3292 * evaluation. Use |this.SELECTED_FRAME| to always pick the
3293 * user-selected stackframe.
3294 * If you do not provide a |frame| the string will be evaluated in the
3295 * global content window.
3296 * @return object
3297 * A promise object that is resolved when the server response is
3298 * received.
3299 */
3300 requestEvaluation: function JST_requestEvaluation(aString, aOptions = {})
3301 {
3302 let deferred = promise.defer();
3304 function onResult(aResponse) {
3305 if (!aResponse.error) {
3306 deferred.resolve(aResponse);
3307 }
3308 else {
3309 deferred.reject(aResponse);
3310 }
3311 }
3313 let frameActor = null;
3314 if ("frame" in aOptions) {
3315 frameActor = this.getFrameActor(aOptions.frame);
3316 }
3318 let evalOptions = {
3319 bindObjectActor: aOptions.bindObjectActor,
3320 frameActor: frameActor,
3321 };
3323 this.webConsoleClient.evaluateJS(aString, onResult, evalOptions);
3324 return deferred.promise;
3325 },
3327 /**
3328 * Retrieve the FrameActor ID given a frame depth.
3329 *
3330 * @param number aFrame
3331 * Frame depth.
3332 * @return string|null
3333 * The FrameActor ID for the given frame depth.
3334 */
3335 getFrameActor: function JST_getFrameActor(aFrame)
3336 {
3337 let state = this.hud.owner.getDebuggerFrames();
3338 if (!state) {
3339 return null;
3340 }
3342 let grip;
3343 if (aFrame == this.SELECTED_FRAME) {
3344 grip = state.frames[state.selected];
3345 }
3346 else {
3347 grip = state.frames[aFrame];
3348 }
3350 return grip ? grip.actor : null;
3351 },
3353 /**
3354 * Opens a new variables view that allows the inspection of the given object.
3355 *
3356 * @param object aOptions
3357 * Options for the variables view:
3358 * - objectActor: grip of the ObjectActor you want to show in the
3359 * variables view.
3360 * - rawObject: the raw object you want to show in the variables view.
3361 * - label: label to display in the variables view for inspected
3362 * object.
3363 * - hideFilterInput: optional boolean, |true| if you want to hide the
3364 * variables view filter input.
3365 * - targetElement: optional nsIDOMElement to append the variables view
3366 * to. An iframe element is used as a container for the view. If this
3367 * option is not used, then the variables view opens in the sidebar.
3368 * - autofocus: optional boolean, |true| if you want to give focus to
3369 * the variables view window after open, |false| otherwise.
3370 * @return object
3371 * A promise object that is resolved when the variables view has
3372 * opened. The new variables view instance is given to the callbacks.
3373 */
3374 openVariablesView: function JST_openVariablesView(aOptions)
3375 {
3376 let onContainerReady = (aWindow) => {
3377 let container = aWindow.document.querySelector("#variables");
3378 let view = this._variablesView;
3379 if (!view || aOptions.targetElement) {
3380 let viewOptions = {
3381 container: container,
3382 hideFilterInput: aOptions.hideFilterInput,
3383 };
3384 view = this._createVariablesView(viewOptions);
3385 if (!aOptions.targetElement) {
3386 this._variablesView = view;
3387 aWindow.addEventListener("keypress", this._onKeypressInVariablesView);
3388 }
3389 }
3390 aOptions.view = view;
3391 this._updateVariablesView(aOptions);
3393 if (!aOptions.targetElement && aOptions.autofocus) {
3394 aWindow.focus();
3395 }
3397 this.emit("variablesview-open", view, aOptions);
3398 return view;
3399 };
3401 let openPromise;
3402 if (aOptions.targetElement) {
3403 let deferred = promise.defer();
3404 openPromise = deferred.promise;
3405 let document = aOptions.targetElement.ownerDocument;
3406 let iframe = document.createElementNS(XHTML_NS, "iframe");
3408 iframe.addEventListener("load", function onIframeLoad(aEvent) {
3409 iframe.removeEventListener("load", onIframeLoad, true);
3410 iframe.style.visibility = "visible";
3411 deferred.resolve(iframe.contentWindow);
3412 }, true);
3414 iframe.flex = 1;
3415 iframe.style.visibility = "hidden";
3416 iframe.setAttribute("src", VARIABLES_VIEW_URL);
3417 aOptions.targetElement.appendChild(iframe);
3418 }
3419 else {
3420 if (!this.sidebar) {
3421 this._createSidebar();
3422 }
3423 openPromise = this._addVariablesViewSidebarTab();
3424 }
3426 return openPromise.then(onContainerReady);
3427 },
3429 /**
3430 * Create the Web Console sidebar.
3431 *
3432 * @see devtools/framework/sidebar.js
3433 * @private
3434 */
3435 _createSidebar: function JST__createSidebar()
3436 {
3437 let tabbox = this.hud.document.querySelector("#webconsole-sidebar");
3438 this.sidebar = new ToolSidebar(tabbox, this, "webconsole");
3439 this.sidebar.show();
3440 },
3442 /**
3443 * Add the variables view tab to the sidebar.
3444 *
3445 * @private
3446 * @return object
3447 * A promise object for the adding of the new tab.
3448 */
3449 _addVariablesViewSidebarTab: function JST__addVariablesViewSidebarTab()
3450 {
3451 let deferred = promise.defer();
3453 let onTabReady = () => {
3454 let window = this.sidebar.getWindowForTab("variablesview");
3455 deferred.resolve(window);
3456 };
3458 let tab = this.sidebar.getTab("variablesview");
3459 if (tab) {
3460 if (this.sidebar.getCurrentTabID() == "variablesview") {
3461 onTabReady();
3462 }
3463 else {
3464 this.sidebar.once("variablesview-selected", onTabReady);
3465 this.sidebar.select("variablesview");
3466 }
3467 }
3468 else {
3469 this.sidebar.once("variablesview-ready", onTabReady);
3470 this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true);
3471 }
3473 return deferred.promise;
3474 },
3476 /**
3477 * The keypress event handler for the Variables View sidebar. Currently this
3478 * is used for removing the sidebar when Escape is pressed.
3479 *
3480 * @private
3481 * @param nsIDOMEvent aEvent
3482 * The keypress DOM event object.
3483 */
3484 _onKeypressInVariablesView: function JST__onKeypressInVariablesView(aEvent)
3485 {
3486 let tag = aEvent.target.nodeName;
3487 if (aEvent.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE || aEvent.shiftKey ||
3488 aEvent.altKey || aEvent.ctrlKey || aEvent.metaKey ||
3489 ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) {
3490 return;
3491 }
3493 this._sidebarDestroy();
3494 this.inputNode.focus();
3495 aEvent.stopPropagation();
3496 },
3498 /**
3499 * Create a variables view instance.
3500 *
3501 * @private
3502 * @param object aOptions
3503 * Options for the new Variables View instance:
3504 * - container: the DOM element where the variables view is inserted.
3505 * - hideFilterInput: boolean, if true the variables filter input is
3506 * hidden.
3507 * @return object
3508 * The new Variables View instance.
3509 */
3510 _createVariablesView: function JST__createVariablesView(aOptions)
3511 {
3512 let view = new VariablesView(aOptions.container);
3513 view.toolbox = gDevTools.getToolbox(this.hud.owner.target);
3514 view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
3515 view.emptyText = l10n.getStr("emptyPropertiesList");
3516 view.searchEnabled = !aOptions.hideFilterInput;
3517 view.lazyEmpty = this._lazyVariablesView;
3519 VariablesViewController.attach(view, {
3520 getEnvironmentClient: aGrip => {
3521 return new EnvironmentClient(this.hud.proxy.client, aGrip);
3522 },
3523 getObjectClient: aGrip => {
3524 return new ObjectClient(this.hud.proxy.client, aGrip);
3525 },
3526 getLongStringClient: aGrip => {
3527 return this.webConsoleClient.longString(aGrip);
3528 },
3529 releaseActor: aActor => {
3530 this.hud._releaseObject(aActor);
3531 },
3532 simpleValueEvalMacro: simpleValueEvalMacro,
3533 overrideValueEvalMacro: overrideValueEvalMacro,
3534 getterOrSetterEvalMacro: getterOrSetterEvalMacro,
3535 });
3537 // Relay events from the VariablesView.
3538 view.on("fetched", (aEvent, aType, aVar) => {
3539 this.emit("variablesview-fetched", aVar);
3540 });
3542 return view;
3543 },
3545 /**
3546 * Update the variables view.
3547 *
3548 * @private
3549 * @param object aOptions
3550 * Options for updating the variables view:
3551 * - view: the view you want to update.
3552 * - objectActor: the grip of the new ObjectActor you want to show in
3553 * the view.
3554 * - rawObject: the new raw object you want to show.
3555 * - label: the new label for the inspected object.
3556 */
3557 _updateVariablesView: function JST__updateVariablesView(aOptions)
3558 {
3559 let view = aOptions.view;
3560 view.empty();
3562 // We need to avoid pruning the object inspection starting point.
3563 // That one is pruned when the console message is removed.
3564 view.controller.releaseActors(aActor => {
3565 return view._consoleLastObjectActor != aActor;
3566 });
3568 if (aOptions.objectActor &&
3569 (!this.hud.owner._browserConsole ||
3570 Services.prefs.getBoolPref("devtools.chrome.enabled"))) {
3571 // Make sure eval works in the correct context.
3572 view.eval = this._variablesViewEvaluate.bind(this, aOptions);
3573 view.switch = this._variablesViewSwitch.bind(this, aOptions);
3574 view.delete = this._variablesViewDelete.bind(this, aOptions);
3575 }
3576 else {
3577 view.eval = null;
3578 view.switch = null;
3579 view.delete = null;
3580 }
3582 let { variable, expanded } = view.controller.setSingleVariable(aOptions);
3583 variable.evaluationMacro = simpleValueEvalMacro;
3585 if (aOptions.objectActor) {
3586 view._consoleLastObjectActor = aOptions.objectActor.actor;
3587 }
3588 else if (aOptions.rawObject) {
3589 view._consoleLastObjectActor = null;
3590 }
3591 else {
3592 throw new Error("Variables View cannot open without giving it an object " +
3593 "display.");
3594 }
3596 expanded.then(() => {
3597 this.emit("variablesview-updated", view, aOptions);
3598 });
3599 },
3601 /**
3602 * The evaluation function used by the variables view when editing a property
3603 * value.
3604 *
3605 * @private
3606 * @param object aOptions
3607 * The options used for |this._updateVariablesView()|.
3608 * @param object aVar
3609 * The Variable object instance for the edited property.
3610 * @param string aValue
3611 * The value the edited property was changed to.
3612 */
3613 _variablesViewEvaluate:
3614 function JST__variablesViewEvaluate(aOptions, aVar, aValue)
3615 {
3616 let updater = this._updateVariablesView.bind(this, aOptions);
3617 let onEval = this._silentEvalCallback.bind(this, updater);
3618 let string = aVar.evaluationMacro(aVar, aValue);
3620 let evalOptions = {
3621 frame: this.SELECTED_FRAME,
3622 bindObjectActor: aOptions.objectActor.actor,
3623 };
3625 this.requestEvaluation(string, evalOptions).then(onEval, onEval);
3626 },
3628 /**
3629 * The property deletion function used by the variables view when a property
3630 * is deleted.
3631 *
3632 * @private
3633 * @param object aOptions
3634 * The options used for |this._updateVariablesView()|.
3635 * @param object aVar
3636 * The Variable object instance for the deleted property.
3637 */
3638 _variablesViewDelete: function JST__variablesViewDelete(aOptions, aVar)
3639 {
3640 let onEval = this._silentEvalCallback.bind(this, null);
3642 let evalOptions = {
3643 frame: this.SELECTED_FRAME,
3644 bindObjectActor: aOptions.objectActor.actor,
3645 };
3647 this.requestEvaluation("delete _self" + aVar.symbolicName, evalOptions)
3648 .then(onEval, onEval);
3649 },
3651 /**
3652 * The property rename function used by the variables view when a property
3653 * is renamed.
3654 *
3655 * @private
3656 * @param object aOptions
3657 * The options used for |this._updateVariablesView()|.
3658 * @param object aVar
3659 * The Variable object instance for the renamed property.
3660 * @param string aNewName
3661 * The new name for the property.
3662 */
3663 _variablesViewSwitch:
3664 function JST__variablesViewSwitch(aOptions, aVar, aNewName)
3665 {
3666 let updater = this._updateVariablesView.bind(this, aOptions);
3667 let onEval = this._silentEvalCallback.bind(this, updater);
3669 let evalOptions = {
3670 frame: this.SELECTED_FRAME,
3671 bindObjectActor: aOptions.objectActor.actor,
3672 };
3674 let newSymbolicName = aVar.ownerView.symbolicName + '["' + aNewName + '"]';
3675 if (newSymbolicName == aVar.symbolicName) {
3676 return;
3677 }
3679 let code = "_self" + newSymbolicName + " = _self" + aVar.symbolicName + ";" +
3680 "delete _self" + aVar.symbolicName;
3682 this.requestEvaluation(code, evalOptions).then(onEval, onEval);
3683 },
3685 /**
3686 * A noop callback for JavaScript evaluation. This method releases any
3687 * result ObjectActors that come from the server for evaluation requests. This
3688 * is used for editing, renaming and deleting properties in the variables
3689 * view.
3690 *
3691 * Exceptions are displayed in the output.
3692 *
3693 * @private
3694 * @param function aCallback
3695 * Function to invoke once the response is received.
3696 * @param object aResponse
3697 * The response packet received from the server.
3698 */
3699 _silentEvalCallback: function JST__silentEvalCallback(aCallback, aResponse)
3700 {
3701 if (aResponse.error) {
3702 Cu.reportError("Web Console evaluation failed. " + aResponse.error + ":" +
3703 aResponse.message);
3705 aCallback && aCallback(aResponse);
3706 return;
3707 }
3709 if (aResponse.exceptionMessage) {
3710 let message = new Messages.Simple(aResponse.exceptionMessage, {
3711 category: "output",
3712 severity: "error",
3713 timestamp: aResponse.timestamp,
3714 });
3715 this.hud.output.addMessage(message);
3716 message._objectActors = new Set();
3717 if (WebConsoleUtils.isActorGrip(aResponse.exception)) {
3718 message._objectActors.add(aResponse.exception.actor);
3719 }
3720 }
3722 let helper = aResponse.helperResult || { type: null };
3723 let helperGrip = null;
3724 if (helper.type == "inspectObject") {
3725 helperGrip = helper.object;
3726 }
3728 let grips = [aResponse.result, helperGrip];
3729 for (let grip of grips) {
3730 if (WebConsoleUtils.isActorGrip(grip)) {
3731 this.hud._releaseObject(grip.actor);
3732 }
3733 }
3735 aCallback && aCallback(aResponse);
3736 },
3739 /**
3740 * Clear the Web Console output.
3741 *
3742 * This method emits the "messages-cleared" notification.
3743 *
3744 * @param boolean aClearStorage
3745 * True if you want to clear the console messages storage associated to
3746 * this Web Console.
3747 */
3748 clearOutput: function JST_clearOutput(aClearStorage)
3749 {
3750 let hud = this.hud;
3751 let outputNode = hud.outputNode;
3752 let node;
3753 while ((node = outputNode.firstChild)) {
3754 hud.removeOutputMessage(node);
3755 }
3757 hud.groupDepth = 0;
3758 hud._outputQueue.forEach(hud._pruneItemFromQueue, hud);
3759 hud._outputQueue = [];
3760 hud._networkRequests = {};
3761 hud._repeatNodes = {};
3763 if (aClearStorage) {
3764 this.webConsoleClient.clearMessagesCache();
3765 }
3767 this.emit("messages-cleared");
3768 },
3770 /**
3771 * Remove all of the private messages from the Web Console output.
3772 *
3773 * This method emits the "private-messages-cleared" notification.
3774 */
3775 clearPrivateMessages: function JST_clearPrivateMessages()
3776 {
3777 let nodes = this.hud.outputNode.querySelectorAll(".message[private]");
3778 for (let node of nodes) {
3779 this.hud.removeOutputMessage(node);
3780 }
3781 this.emit("private-messages-cleared");
3782 },
3784 /**
3785 * Updates the size of the input field (command line) to fit its contents.
3786 *
3787 * @returns void
3788 */
3789 resizeInput: function JST_resizeInput()
3790 {
3791 let inputNode = this.inputNode;
3793 // Reset the height so that scrollHeight will reflect the natural height of
3794 // the contents of the input field.
3795 inputNode.style.height = "auto";
3797 // Now resize the input field to fit its contents.
3798 let scrollHeight = inputNode.inputField.scrollHeight;
3799 if (scrollHeight > 0) {
3800 inputNode.style.height = scrollHeight + "px";
3801 }
3802 },
3804 /**
3805 * Sets the value of the input field (command line), and resizes the field to
3806 * fit its contents. This method is preferred over setting "inputNode.value"
3807 * directly, because it correctly resizes the field.
3808 *
3809 * @param string aNewValue
3810 * The new value to set.
3811 * @returns void
3812 */
3813 setInputValue: function JST_setInputValue(aNewValue)
3814 {
3815 this.inputNode.value = aNewValue;
3816 this.lastInputValue = aNewValue;
3817 this.completeNode.value = "";
3818 this.resizeInput();
3819 this._inputChanged = true;
3820 },
3822 /**
3823 * The inputNode "input" and "keyup" event handler.
3824 * @private
3825 */
3826 _inputEventHandler: function JST__inputEventHandler()
3827 {
3828 if (this.lastInputValue != this.inputNode.value) {
3829 this.resizeInput();
3830 this.complete(this.COMPLETE_HINT_ONLY);
3831 this.lastInputValue = this.inputNode.value;
3832 this._inputChanged = true;
3833 }
3834 },
3836 /**
3837 * The window "blur" event handler.
3838 * @private
3839 */
3840 _blurEventHandler: function JST__blurEventHandler()
3841 {
3842 if (this.autocompletePopup) {
3843 this.clearCompletion();
3844 }
3845 },
3847 /**
3848 * The inputNode "keypress" event handler.
3849 *
3850 * @private
3851 * @param nsIDOMEvent aEvent
3852 */
3853 _keyPress: function JST__keyPress(aEvent)
3854 {
3855 let inputNode = this.inputNode;
3856 let inputUpdated = false;
3858 if (aEvent.ctrlKey) {
3859 switch (aEvent.charCode) {
3860 case 101:
3861 // control-e
3862 if (Services.appinfo.OS == "WINNT") {
3863 break;
3864 }
3865 let lineEndPos = inputNode.value.length;
3866 if (this.hasMultilineInput()) {
3867 // find index of closest newline >= cursor
3868 for (let i = inputNode.selectionEnd; i<lineEndPos; i++) {
3869 if (inputNode.value.charAt(i) == "\r" ||
3870 inputNode.value.charAt(i) == "\n") {
3871 lineEndPos = i;
3872 break;
3873 }
3874 }
3875 }
3876 inputNode.setSelectionRange(lineEndPos, lineEndPos);
3877 aEvent.preventDefault();
3878 this.clearCompletion();
3879 break;
3881 case 110:
3882 // Control-N differs from down arrow: it ignores autocomplete state.
3883 // Note that we preserve the default 'down' navigation within
3884 // multiline text.
3885 if (Services.appinfo.OS == "Darwin" &&
3886 this.canCaretGoNext() &&
3887 this.historyPeruse(HISTORY_FORWARD)) {
3888 aEvent.preventDefault();
3889 // Ctrl-N is also used to focus the Network category button on MacOSX.
3890 // The preventDefault() call doesn't prevent the focus from moving
3891 // away from the input.
3892 inputNode.focus();
3893 }
3894 this.clearCompletion();
3895 break;
3897 case 112:
3898 // Control-P differs from up arrow: it ignores autocomplete state.
3899 // Note that we preserve the default 'up' navigation within
3900 // multiline text.
3901 if (Services.appinfo.OS == "Darwin" &&
3902 this.canCaretGoPrevious() &&
3903 this.historyPeruse(HISTORY_BACK)) {
3904 aEvent.preventDefault();
3905 // Ctrl-P may also be used to focus some category button on MacOSX.
3906 // The preventDefault() call doesn't prevent the focus from moving
3907 // away from the input.
3908 inputNode.focus();
3909 }
3910 this.clearCompletion();
3911 break;
3912 default:
3913 break;
3914 }
3915 return;
3916 }
3917 else if (aEvent.shiftKey &&
3918 aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
3919 // shift return
3920 // TODO: expand the inputNode height by one line
3921 return;
3922 }
3924 switch (aEvent.keyCode) {
3925 case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
3926 if (this.autocompletePopup.isOpen) {
3927 this.clearCompletion();
3928 aEvent.preventDefault();
3929 aEvent.stopPropagation();
3930 }
3931 else if (this.sidebar) {
3932 this._sidebarDestroy();
3933 aEvent.preventDefault();
3934 aEvent.stopPropagation();
3935 }
3936 break;
3938 case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
3939 if (this._autocompletePopupNavigated &&
3940 this.autocompletePopup.isOpen &&
3941 this.autocompletePopup.selectedIndex > -1) {
3942 this.acceptProposedCompletion();
3943 }
3944 else {
3945 this.execute();
3946 this._inputChanged = false;
3947 }
3948 aEvent.preventDefault();
3949 break;
3951 case Ci.nsIDOMKeyEvent.DOM_VK_UP:
3952 if (this.autocompletePopup.isOpen) {
3953 inputUpdated = this.complete(this.COMPLETE_BACKWARD);
3954 if (inputUpdated) {
3955 this._autocompletePopupNavigated = true;
3956 }
3957 }
3958 else if (this.canCaretGoPrevious()) {
3959 inputUpdated = this.historyPeruse(HISTORY_BACK);
3960 }
3961 if (inputUpdated) {
3962 aEvent.preventDefault();
3963 }
3964 break;
3966 case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
3967 if (this.autocompletePopup.isOpen) {
3968 inputUpdated = this.complete(this.COMPLETE_FORWARD);
3969 if (inputUpdated) {
3970 this._autocompletePopupNavigated = true;
3971 }
3972 }
3973 else if (this.canCaretGoNext()) {
3974 inputUpdated = this.historyPeruse(HISTORY_FORWARD);
3975 }
3976 if (inputUpdated) {
3977 aEvent.preventDefault();
3978 }
3979 break;
3981 case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP:
3982 if (this.autocompletePopup.isOpen) {
3983 inputUpdated = this.complete(this.COMPLETE_PAGEUP);
3984 if (inputUpdated) {
3985 this._autocompletePopupNavigated = true;
3986 }
3987 }
3988 else {
3989 this.hud.outputNode.parentNode.scrollTop =
3990 Math.max(0,
3991 this.hud.outputNode.parentNode.scrollTop -
3992 this.hud.outputNode.parentNode.clientHeight
3993 );
3994 }
3995 aEvent.preventDefault();
3996 break;
3998 case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN:
3999 if (this.autocompletePopup.isOpen) {
4000 inputUpdated = this.complete(this.COMPLETE_PAGEDOWN);
4001 if (inputUpdated) {
4002 this._autocompletePopupNavigated = true;
4003 }
4004 }
4005 else {
4006 this.hud.outputNode.parentNode.scrollTop =
4007 Math.min(this.hud.outputNode.parentNode.scrollHeight,
4008 this.hud.outputNode.parentNode.scrollTop +
4009 this.hud.outputNode.parentNode.clientHeight
4010 );
4011 }
4012 aEvent.preventDefault();
4013 break;
4015 case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
4016 case Ci.nsIDOMKeyEvent.DOM_VK_END:
4017 case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
4018 if (this.autocompletePopup.isOpen || this.lastCompletion.value) {
4019 this.clearCompletion();
4020 }
4021 break;
4023 case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: {
4024 let cursorAtTheEnd = this.inputNode.selectionStart ==
4025 this.inputNode.selectionEnd &&
4026 this.inputNode.selectionStart ==
4027 this.inputNode.value.length;
4028 let haveSuggestion = this.autocompletePopup.isOpen ||
4029 this.lastCompletion.value;
4030 let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated;
4031 if (haveSuggestion && useCompletion &&
4032 this.complete(this.COMPLETE_HINT_ONLY) &&
4033 this.lastCompletion.value &&
4034 this.acceptProposedCompletion()) {
4035 aEvent.preventDefault();
4036 }
4037 if (this.autocompletePopup.isOpen) {
4038 this.clearCompletion();
4039 }
4040 break;
4041 }
4042 case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
4043 // Generate a completion and accept the first proposed value.
4044 if (this.complete(this.COMPLETE_HINT_ONLY) &&
4045 this.lastCompletion &&
4046 this.acceptProposedCompletion()) {
4047 aEvent.preventDefault();
4048 }
4049 else if (this._inputChanged) {
4050 this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
4051 aEvent.preventDefault();
4052 }
4053 break;
4054 default:
4055 break;
4056 }
4057 },
4059 /**
4060 * The inputNode "focus" event handler.
4061 * @private
4062 */
4063 _focusEventHandler: function JST__focusEventHandler()
4064 {
4065 this._inputChanged = false;
4066 },
4068 /**
4069 * Go up/down the history stack of input values.
4070 *
4071 * @param number aDirection
4072 * History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
4073 *
4074 * @returns boolean
4075 * True if the input value changed, false otherwise.
4076 */
4077 historyPeruse: function JST_historyPeruse(aDirection)
4078 {
4079 if (!this.history.length) {
4080 return false;
4081 }
4083 // Up Arrow key
4084 if (aDirection == HISTORY_BACK) {
4085 if (this.historyPlaceHolder <= 0) {
4086 return false;
4087 }
4088 let inputVal = this.history[--this.historyPlaceHolder];
4090 // Save the current input value as the latest entry in history, only if
4091 // the user is already at the last entry.
4092 // Note: this code does not store changes to items that are already in
4093 // history.
4094 if (this.historyPlaceHolder+1 == this.historyIndex) {
4095 this.history[this.historyIndex] = this.inputNode.value || "";
4096 }
4098 this.setInputValue(inputVal);
4099 }
4100 // Down Arrow key
4101 else if (aDirection == HISTORY_FORWARD) {
4102 if (this.historyPlaceHolder >= (this.history.length-1)) {
4103 return false;
4104 }
4106 let inputVal = this.history[++this.historyPlaceHolder];
4107 this.setInputValue(inputVal);
4108 }
4109 else {
4110 throw new Error("Invalid argument 0");
4111 }
4113 return true;
4114 },
4116 /**
4117 * Test for multiline input.
4118 *
4119 * @return boolean
4120 * True if CR or LF found in node value; else false.
4121 */
4122 hasMultilineInput: function JST_hasMultilineInput()
4123 {
4124 return /[\r\n]/.test(this.inputNode.value);
4125 },
4127 /**
4128 * Check if the caret is at a location that allows selecting the previous item
4129 * in history when the user presses the Up arrow key.
4130 *
4131 * @return boolean
4132 * True if the caret is at a location that allows selecting the
4133 * previous item in history when the user presses the Up arrow key,
4134 * otherwise false.
4135 */
4136 canCaretGoPrevious: function JST_canCaretGoPrevious()
4137 {
4138 let node = this.inputNode;
4139 if (node.selectionStart != node.selectionEnd) {
4140 return false;
4141 }
4143 let multiline = /[\r\n]/.test(node.value);
4144 return node.selectionStart == 0 ? true :
4145 node.selectionStart == node.value.length && !multiline;
4146 },
4148 /**
4149 * Check if the caret is at a location that allows selecting the next item in
4150 * history when the user presses the Down arrow key.
4151 *
4152 * @return boolean
4153 * True if the caret is at a location that allows selecting the next
4154 * item in history when the user presses the Down arrow key, otherwise
4155 * false.
4156 */
4157 canCaretGoNext: function JST_canCaretGoNext()
4158 {
4159 let node = this.inputNode;
4160 if (node.selectionStart != node.selectionEnd) {
4161 return false;
4162 }
4164 let multiline = /[\r\n]/.test(node.value);
4165 return node.selectionStart == node.value.length ? true :
4166 node.selectionStart == 0 && !multiline;
4167 },
4169 /**
4170 * Completes the current typed text in the inputNode. Completion is performed
4171 * only if the selection/cursor is at the end of the string. If no completion
4172 * is found, the current inputNode value and cursor/selection stay.
4173 *
4174 * @param int aType possible values are
4175 * - this.COMPLETE_FORWARD: If there is more than one possible completion
4176 * and the input value stayed the same compared to the last time this
4177 * function was called, then the next completion of all possible
4178 * completions is used. If the value changed, then the first possible
4179 * completion is used and the selection is set from the current
4180 * cursor position to the end of the completed text.
4181 * If there is only one possible completion, then this completion
4182 * value is used and the cursor is put at the end of the completion.
4183 * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the
4184 * value stayed the same as the last time the function was called,
4185 * then the previous completion of all possible completions is used.
4186 * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the first
4187 * item.
4188 * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select the
4189 * last item.
4190 * - this.COMPLETE_HINT_ONLY: If there is more than one possible
4191 * completion and the input value stayed the same compared to the
4192 * last time this function was called, then the same completion is
4193 * used again. If there is only one possible completion, then
4194 * the inputNode.value is set to this value and the selection is set
4195 * from the current cursor position to the end of the completed text.
4196 * @param function aCallback
4197 * Optional function invoked when the autocomplete properties are
4198 * updated.
4199 * @returns boolean true if there existed a completion for the current input,
4200 * or false otherwise.
4201 */
4202 complete: function JSTF_complete(aType, aCallback)
4203 {
4204 let inputNode = this.inputNode;
4205 let inputValue = inputNode.value;
4206 let frameActor = this.getFrameActor(this.SELECTED_FRAME);
4208 // If the inputNode has no value, then don't try to complete on it.
4209 if (!inputValue) {
4210 this.clearCompletion();
4211 aCallback && aCallback(this);
4212 this.emit("autocomplete-updated");
4213 return false;
4214 }
4216 // Only complete if the selection is empty.
4217 if (inputNode.selectionStart != inputNode.selectionEnd) {
4218 this.clearCompletion();
4219 aCallback && aCallback(this);
4220 this.emit("autocomplete-updated");
4221 return false;
4222 }
4224 // Update the completion results.
4225 if (this.lastCompletion.value != inputValue || frameActor != this._lastFrameActorId) {
4226 this._updateCompletionResult(aType, aCallback);
4227 return false;
4228 }
4230 let popup = this.autocompletePopup;
4231 let accepted = false;
4233 if (aType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
4234 this.acceptProposedCompletion();
4235 accepted = true;
4236 }
4237 else if (aType == this.COMPLETE_BACKWARD) {
4238 popup.selectPreviousItem();
4239 }
4240 else if (aType == this.COMPLETE_FORWARD) {
4241 popup.selectNextItem();
4242 }
4243 else if (aType == this.COMPLETE_PAGEUP) {
4244 popup.selectPreviousPageItem();
4245 }
4246 else if (aType == this.COMPLETE_PAGEDOWN) {
4247 popup.selectNextPageItem();
4248 }
4250 aCallback && aCallback(this);
4251 this.emit("autocomplete-updated");
4252 return accepted || popup.itemCount > 0;
4253 },
4255 /**
4256 * Update the completion result. This operation is performed asynchronously by
4257 * fetching updated results from the content process.
4258 *
4259 * @private
4260 * @param int aType
4261 * Completion type. See this.complete() for details.
4262 * @param function [aCallback]
4263 * Optional, function to invoke when completion results are received.
4264 */
4265 _updateCompletionResult:
4266 function JST__updateCompletionResult(aType, aCallback)
4267 {
4268 let frameActor = this.getFrameActor(this.SELECTED_FRAME);
4269 if (this.lastCompletion.value == this.inputNode.value && frameActor == this._lastFrameActorId) {
4270 return;
4271 }
4273 let requestId = gSequenceId();
4274 let cursor = this.inputNode.selectionStart;
4275 let input = this.inputNode.value.substring(0, cursor);
4276 let cache = this._autocompleteCache;
4278 // If the current input starts with the previous input, then we already
4279 // have a list of suggestions and we just need to filter the cached
4280 // suggestions. When the current input ends with a non-alphanumeric
4281 // character we ask the server again for suggestions.
4283 // Check if last character is non-alphanumeric
4284 if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) {
4285 this._autocompleteQuery = null;
4286 this._autocompleteCache = null;
4287 }
4289 if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) {
4290 let filterBy = input;
4291 // Find the last non-alphanumeric if exists.
4292 let lastNonAlpha = input.match(/[^a-zA-Z0-9][a-zA-Z0-9]*$/);
4293 // If input contains non-alphanumerics, use the part after the last one
4294 // to filter the cache
4295 if (lastNonAlpha) {
4296 filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
4297 }
4299 let newList = cache.sort().filter(function(l) {
4300 return l.startsWith(filterBy);
4301 });
4303 this.lastCompletion = {
4304 requestId: null,
4305 completionType: aType,
4306 value: null,
4307 };
4309 let response = { matches: newList, matchProp: filterBy };
4310 this._receiveAutocompleteProperties(null, aCallback, response);
4311 return;
4312 }
4314 this._lastFrameActorId = frameActor;
4316 this.lastCompletion = {
4317 requestId: requestId,
4318 completionType: aType,
4319 value: null,
4320 };
4322 let callback = this._receiveAutocompleteProperties.bind(this, requestId,
4323 aCallback);
4325 this.webConsoleClient.autocomplete(input, cursor, callback, frameActor);
4326 },
4328 /**
4329 * Handler for the autocompletion results. This method takes
4330 * the completion result received from the server and updates the UI
4331 * accordingly.
4332 *
4333 * @param number aRequestId
4334 * Request ID.
4335 * @param function [aCallback=null]
4336 * Optional, function to invoke when the completion result is received.
4337 * @param object aMessage
4338 * The JSON message which holds the completion results received from
4339 * the content process.
4340 */
4341 _receiveAutocompleteProperties:
4342 function JST__receiveAutocompleteProperties(aRequestId, aCallback, aMessage)
4343 {
4344 let inputNode = this.inputNode;
4345 let inputValue = inputNode.value;
4346 if (this.lastCompletion.value == inputValue ||
4347 aRequestId != this.lastCompletion.requestId) {
4348 return;
4349 }
4350 // Cache whatever came from the server if the last char is alphanumeric or '.'
4351 let cursor = inputNode.selectionStart;
4352 let inputUntilCursor = inputValue.substring(0, cursor);
4354 if (aRequestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) {
4355 this._autocompleteCache = aMessage.matches;
4356 this._autocompleteQuery = inputUntilCursor;
4357 }
4359 let matches = aMessage.matches;
4360 let lastPart = aMessage.matchProp;
4361 if (!matches.length) {
4362 this.clearCompletion();
4363 aCallback && aCallback(this);
4364 this.emit("autocomplete-updated");
4365 return;
4366 }
4368 let items = matches.reverse().map(function(aMatch) {
4369 return { preLabel: lastPart, label: aMatch };
4370 });
4372 let popup = this.autocompletePopup;
4373 popup.setItems(items);
4375 let completionType = this.lastCompletion.completionType;
4376 this.lastCompletion = {
4377 value: inputValue,
4378 matchProp: lastPart,
4379 };
4381 if (items.length > 1 && !popup.isOpen) {
4382 let str = this.inputNode.value.substr(0, this.inputNode.selectionStart);
4383 let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length;
4384 let x = offset * this.hud._inputCharWidth;
4385 popup.openPopup(inputNode, x + this.hud._chevronWidth);
4386 this._autocompletePopupNavigated = false;
4387 }
4388 else if (items.length < 2 && popup.isOpen) {
4389 popup.hidePopup();
4390 this._autocompletePopupNavigated = false;
4391 }
4393 if (items.length == 1) {
4394 popup.selectedIndex = 0;
4395 }
4397 this.onAutocompleteSelect();
4399 if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
4400 this.acceptProposedCompletion();
4401 }
4402 else if (completionType == this.COMPLETE_BACKWARD) {
4403 popup.selectPreviousItem();
4404 }
4405 else if (completionType == this.COMPLETE_FORWARD) {
4406 popup.selectNextItem();
4407 }
4409 aCallback && aCallback(this);
4410 this.emit("autocomplete-updated");
4411 },
4413 onAutocompleteSelect: function JSTF_onAutocompleteSelect()
4414 {
4415 // Render the suggestion only if the cursor is at the end of the input.
4416 if (this.inputNode.selectionStart != this.inputNode.value.length) {
4417 return;
4418 }
4420 let currentItem = this.autocompletePopup.selectedItem;
4421 if (currentItem && this.lastCompletion.value) {
4422 let suffix = currentItem.label.substring(this.lastCompletion.
4423 matchProp.length);
4424 this.updateCompleteNode(suffix);
4425 }
4426 else {
4427 this.updateCompleteNode("");
4428 }
4429 },
4431 /**
4432 * Clear the current completion information and close the autocomplete popup,
4433 * if needed.
4434 */
4435 clearCompletion: function JSTF_clearCompletion()
4436 {
4437 this.autocompletePopup.clearItems();
4438 this.lastCompletion = { value: null };
4439 this.updateCompleteNode("");
4440 if (this.autocompletePopup.isOpen) {
4441 this.autocompletePopup.hidePopup();
4442 this._autocompletePopupNavigated = false;
4443 }
4444 },
4446 /**
4447 * Accept the proposed input completion.
4448 *
4449 * @return boolean
4450 * True if there was a selected completion item and the input value
4451 * was updated, false otherwise.
4452 */
4453 acceptProposedCompletion: function JSTF_acceptProposedCompletion()
4454 {
4455 let updated = false;
4457 let currentItem = this.autocompletePopup.selectedItem;
4458 if (currentItem && this.lastCompletion.value) {
4459 let suffix = currentItem.label.substring(this.lastCompletion.
4460 matchProp.length);
4461 let cursor = this.inputNode.selectionStart;
4462 let value = this.inputNode.value;
4463 this.setInputValue(value.substr(0, cursor) + suffix + value.substr(cursor));
4464 let newCursor = cursor + suffix.length;
4465 this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor;
4466 updated = true;
4467 }
4469 this.clearCompletion();
4471 return updated;
4472 },
4474 /**
4475 * Update the node that displays the currently selected autocomplete proposal.
4476 *
4477 * @param string aSuffix
4478 * The proposed suffix for the inputNode value.
4479 */
4480 updateCompleteNode: function JSTF_updateCompleteNode(aSuffix)
4481 {
4482 // completion prefix = input, with non-control chars replaced by spaces
4483 let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : "";
4484 this.completeNode.value = prefix + aSuffix;
4485 },
4488 /**
4489 * Destroy the sidebar.
4490 * @private
4491 */
4492 _sidebarDestroy: function JST__sidebarDestroy()
4493 {
4494 if (this._variablesView) {
4495 this._variablesView.controller.releaseActors();
4496 this._variablesView = null;
4497 }
4499 if (this.sidebar) {
4500 this.sidebar.hide();
4501 this.sidebar.destroy();
4502 this.sidebar = null;
4503 }
4505 this.emit("sidebar-closed");
4506 },
4508 /**
4509 * Destroy the JSTerm object. Call this method to avoid memory leaks.
4510 */
4511 destroy: function JST_destroy()
4512 {
4513 this._sidebarDestroy();
4515 this.clearCompletion();
4516 this.clearOutput();
4518 this.autocompletePopup.destroy();
4519 this.autocompletePopup = null;
4521 let popup = this.hud.owner.chromeWindow.document
4522 .getElementById("webConsole_autocompletePopup");
4523 if (popup) {
4524 popup.parentNode.removeChild(popup);
4525 }
4527 this.inputNode.removeEventListener("keypress", this._keyPress, false);
4528 this.inputNode.removeEventListener("input", this._inputEventHandler, false);
4529 this.inputNode.removeEventListener("keyup", this._inputEventHandler, false);
4530 this.inputNode.removeEventListener("focus", this._focusEventHandler, false);
4531 this.hud.window.removeEventListener("blur", this._blurEventHandler, false);
4533 this.hud = null;
4534 },
4535 };
4537 /**
4538 * Utils: a collection of globally used functions.
4539 */
4540 var Utils = {
4541 /**
4542 * Scrolls a node so that it's visible in its containing element.
4543 *
4544 * @param nsIDOMNode aNode
4545 * The node to make visible.
4546 * @returns void
4547 */
4548 scrollToVisible: function Utils_scrollToVisible(aNode)
4549 {
4550 aNode.scrollIntoView(false);
4551 },
4553 /**
4554 * Check if the given output node is scrolled to the bottom.
4555 *
4556 * @param nsIDOMNode aOutputNode
4557 * @return boolean
4558 * True if the output node is scrolled to the bottom, or false
4559 * otherwise.
4560 */
4561 isOutputScrolledToBottom: function Utils_isOutputScrolledToBottom(aOutputNode)
4562 {
4563 let lastNodeHeight = aOutputNode.lastChild ?
4564 aOutputNode.lastChild.clientHeight : 0;
4565 let scrollNode = aOutputNode.parentNode;
4566 return scrollNode.scrollTop + scrollNode.clientHeight >=
4567 scrollNode.scrollHeight - lastNodeHeight / 2;
4568 },
4570 /**
4571 * Determine the category of a given nsIScriptError.
4572 *
4573 * @param nsIScriptError aScriptError
4574 * The script error you want to determine the category for.
4575 * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY
4576 * Depending on the script error CATEGORY_JS, CATEGORY_CSS, or
4577 * CATEGORY_SECURITY can be returned.
4578 */
4579 categoryForScriptError: function Utils_categoryForScriptError(aScriptError)
4580 {
4581 let category = aScriptError.category;
4583 if (/^(?:CSS|Layout)\b/.test(category)) {
4584 return CATEGORY_CSS;
4585 }
4587 switch (category) {
4588 case "Mixed Content Blocker":
4589 case "Mixed Content Message":
4590 case "CSP":
4591 case "Invalid HSTS Headers":
4592 case "Insecure Password Field":
4593 case "SSL":
4594 case "CORS":
4595 return CATEGORY_SECURITY;
4597 default:
4598 return CATEGORY_JS;
4599 }
4600 },
4602 /**
4603 * Retrieve the limit of messages for a specific category.
4604 *
4605 * @param number aCategory
4606 * The category of messages you want to retrieve the limit for. See the
4607 * CATEGORY_* constants.
4608 * @return number
4609 * The number of messages allowed for the specific category.
4610 */
4611 logLimitForCategory: function Utils_logLimitForCategory(aCategory)
4612 {
4613 let logLimit = DEFAULT_LOG_LIMIT;
4615 try {
4616 let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory];
4617 logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
4618 logLimit = Math.max(logLimit, 1);
4619 }
4620 catch (e) { }
4622 return logLimit;
4623 },
4624 };
4626 ///////////////////////////////////////////////////////////////////////////////
4627 // CommandController
4628 ///////////////////////////////////////////////////////////////////////////////
4630 /**
4631 * A controller (an instance of nsIController) that makes editing actions
4632 * behave appropriately in the context of the Web Console.
4633 */
4634 function CommandController(aWebConsole)
4635 {
4636 this.owner = aWebConsole;
4637 }
4639 CommandController.prototype = {
4640 /**
4641 * Selects all the text in the HUD output.
4642 */
4643 selectAll: function CommandController_selectAll()
4644 {
4645 this.owner.output.selectAllMessages();
4646 },
4648 /**
4649 * Open the URL of the selected message in a new tab.
4650 */
4651 openURL: function CommandController_openURL()
4652 {
4653 this.owner.openSelectedItemInTab();
4654 },
4656 copyURL: function CommandController_copyURL()
4657 {
4658 this.owner.copySelectedItems({ linkOnly: true, contextmenu: true });
4659 },
4661 supportsCommand: function CommandController_supportsCommand(aCommand)
4662 {
4663 if (!this.owner || !this.owner.output) {
4664 return false;
4665 }
4666 return this.isCommandEnabled(aCommand);
4667 },
4669 isCommandEnabled: function CommandController_isCommandEnabled(aCommand)
4670 {
4671 switch (aCommand) {
4672 case "consoleCmd_openURL":
4673 case "consoleCmd_copyURL": {
4674 // Only enable URL-related actions if node is Net Activity.
4675 let selectedItem = this.owner.output.getSelectedMessages(1)[0] ||
4676 this.owner._contextMenuHandler.lastClickedMessage;
4677 return selectedItem && "url" in selectedItem;
4678 }
4679 case "consoleCmd_clearOutput":
4680 case "cmd_selectAll":
4681 case "cmd_find":
4682 return true;
4683 case "cmd_fontSizeEnlarge":
4684 case "cmd_fontSizeReduce":
4685 case "cmd_fontSizeReset":
4686 case "cmd_close":
4687 return this.owner.owner._browserConsole;
4688 }
4689 return false;
4690 },
4692 doCommand: function CommandController_doCommand(aCommand)
4693 {
4694 switch (aCommand) {
4695 case "consoleCmd_openURL":
4696 this.openURL();
4697 break;
4698 case "consoleCmd_copyURL":
4699 this.copyURL();
4700 break;
4701 case "consoleCmd_clearOutput":
4702 this.owner.jsterm.clearOutput(true);
4703 break;
4704 case "cmd_find":
4705 this.owner.filterBox.focus();
4706 break;
4707 case "cmd_selectAll":
4708 this.selectAll();
4709 break;
4710 case "cmd_fontSizeEnlarge":
4711 this.owner.changeFontSize("+");
4712 break;
4713 case "cmd_fontSizeReduce":
4714 this.owner.changeFontSize("-");
4715 break;
4716 case "cmd_fontSizeReset":
4717 this.owner.changeFontSize("");
4718 break;
4719 case "cmd_close":
4720 this.owner.window.close();
4721 break;
4722 }
4723 }
4724 };
4726 ///////////////////////////////////////////////////////////////////////////////
4727 // Web Console connection proxy
4728 ///////////////////////////////////////////////////////////////////////////////
4730 /**
4731 * The WebConsoleConnectionProxy handles the connection between the Web Console
4732 * and the application we connect to through the remote debug protocol.
4733 *
4734 * @constructor
4735 * @param object aWebConsole
4736 * The Web Console instance that owns this connection proxy.
4737 * @param RemoteTarget aTarget
4738 * The target that the console will connect to.
4739 */
4740 function WebConsoleConnectionProxy(aWebConsole, aTarget)
4741 {
4742 this.owner = aWebConsole;
4743 this.target = aTarget;
4745 this._onPageError = this._onPageError.bind(this);
4746 this._onLogMessage = this._onLogMessage.bind(this);
4747 this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
4748 this._onNetworkEvent = this._onNetworkEvent.bind(this);
4749 this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
4750 this._onFileActivity = this._onFileActivity.bind(this);
4751 this._onReflowActivity = this._onReflowActivity.bind(this);
4752 this._onTabNavigated = this._onTabNavigated.bind(this);
4753 this._onAttachConsole = this._onAttachConsole.bind(this);
4754 this._onCachedMessages = this._onCachedMessages.bind(this);
4755 this._connectionTimeout = this._connectionTimeout.bind(this);
4756 this._onLastPrivateContextExited = this._onLastPrivateContextExited.bind(this);
4757 }
4759 WebConsoleConnectionProxy.prototype = {
4760 /**
4761 * The owning Web Console instance.
4762 *
4763 * @see WebConsoleFrame
4764 * @type object
4765 */
4766 owner: null,
4768 /**
4769 * The target that the console connects to.
4770 * @type RemoteTarget
4771 */
4772 target: null,
4774 /**
4775 * The DebuggerClient object.
4776 *
4777 * @see DebuggerClient
4778 * @type object
4779 */
4780 client: null,
4782 /**
4783 * The WebConsoleClient object.
4784 *
4785 * @see WebConsoleClient
4786 * @type object
4787 */
4788 webConsoleClient: null,
4790 /**
4791 * Tells if the connection is established.
4792 * @type boolean
4793 */
4794 connected: false,
4796 /**
4797 * Timer used for the connection.
4798 * @private
4799 * @type object
4800 */
4801 _connectTimer: null,
4803 _connectDefer: null,
4804 _disconnecter: null,
4806 /**
4807 * The WebConsoleActor ID.
4808 *
4809 * @private
4810 * @type string
4811 */
4812 _consoleActor: null,
4814 /**
4815 * Tells if the window.console object of the remote web page is the native
4816 * object or not.
4817 * @private
4818 * @type boolean
4819 */
4820 _hasNativeConsoleAPI: false,
4822 /**
4823 * Initialize a debugger client and connect it to the debugger server.
4824 *
4825 * @return object
4826 * A promise object that is resolved/rejected based on the success of
4827 * the connection initialization.
4828 */
4829 connect: function WCCP_connect()
4830 {
4831 if (this._connectDefer) {
4832 return this._connectDefer.promise;
4833 }
4835 this._connectDefer = promise.defer();
4837 let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT);
4838 this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
4839 this._connectTimer.initWithCallback(this._connectionTimeout,
4840 timeout, Ci.nsITimer.TYPE_ONE_SHOT);
4842 let connPromise = this._connectDefer.promise;
4843 connPromise.then(function _onSucess() {
4844 this._connectTimer.cancel();
4845 this._connectTimer = null;
4846 }.bind(this), function _onFailure() {
4847 this._connectTimer = null;
4848 }.bind(this));
4850 let client = this.client = this.target.client;
4852 client.addListener("logMessage", this._onLogMessage);
4853 client.addListener("pageError", this._onPageError);
4854 client.addListener("consoleAPICall", this._onConsoleAPICall);
4855 client.addListener("networkEvent", this._onNetworkEvent);
4856 client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
4857 client.addListener("fileActivity", this._onFileActivity);
4858 client.addListener("reflowActivity", this._onReflowActivity);
4859 client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
4860 this.target.on("will-navigate", this._onTabNavigated);
4861 this.target.on("navigate", this._onTabNavigated);
4863 this._consoleActor = this.target.form.consoleActor;
4864 if (!this.target.chrome) {
4865 let tab = this.target.form;
4866 this.owner.onLocationChange(tab.url, tab.title);
4867 }
4868 this._attachConsole();
4870 return connPromise;
4871 },
4873 /**
4874 * Connection timeout handler.
4875 * @private
4876 */
4877 _connectionTimeout: function WCCP__connectionTimeout()
4878 {
4879 let error = {
4880 error: "timeout",
4881 message: l10n.getStr("connectionTimeout"),
4882 };
4884 this._connectDefer.reject(error);
4885 },
4887 /**
4888 * Attach to the Web Console actor.
4889 * @private
4890 */
4891 _attachConsole: function WCCP__attachConsole()
4892 {
4893 let listeners = ["PageError", "ConsoleAPI", "NetworkActivity",
4894 "FileActivity"];
4895 this.client.attachConsole(this._consoleActor, listeners,
4896 this._onAttachConsole);
4897 },
4899 /**
4900 * The "attachConsole" response handler.
4901 *
4902 * @private
4903 * @param object aResponse
4904 * The JSON response object received from the server.
4905 * @param object aWebConsoleClient
4906 * The WebConsoleClient instance for the attached console, for the
4907 * specific tab we work with.
4908 */
4909 _onAttachConsole: function WCCP__onAttachConsole(aResponse, aWebConsoleClient)
4910 {
4911 if (aResponse.error) {
4912 Cu.reportError("attachConsole failed: " + aResponse.error + " " +
4913 aResponse.message);
4914 this._connectDefer.reject(aResponse);
4915 return;
4916 }
4918 this.webConsoleClient = aWebConsoleClient;
4920 this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI;
4922 let msgs = ["PageError", "ConsoleAPI"];
4923 this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
4925 this.owner._updateReflowActivityListener();
4926 },
4928 /**
4929 * The "cachedMessages" response handler.
4930 *
4931 * @private
4932 * @param object aResponse
4933 * The JSON response object received from the server.
4934 */
4935 _onCachedMessages: function WCCP__onCachedMessages(aResponse)
4936 {
4937 if (aResponse.error) {
4938 Cu.reportError("Web Console getCachedMessages error: " + aResponse.error +
4939 " " + aResponse.message);
4940 this._connectDefer.reject(aResponse);
4941 return;
4942 }
4944 if (!this._connectTimer) {
4945 // This happens if the promise is rejected (eg. a timeout), but the
4946 // connection attempt is successful, nonetheless.
4947 Cu.reportError("Web Console getCachedMessages error: invalid state.");
4948 }
4950 this.owner.displayCachedMessages(aResponse.messages);
4952 if (!this._hasNativeConsoleAPI) {
4953 this.owner.logWarningAboutReplacedAPI();
4954 }
4956 this.connected = true;
4957 this._connectDefer.resolve(this);
4958 },
4960 /**
4961 * The "pageError" message type handler. We redirect any page errors to the UI
4962 * for displaying.
4963 *
4964 * @private
4965 * @param string aType
4966 * Message type.
4967 * @param object aPacket
4968 * The message received from the server.
4969 */
4970 _onPageError: function WCCP__onPageError(aType, aPacket)
4971 {
4972 if (this.owner && aPacket.from == this._consoleActor) {
4973 this.owner.handlePageError(aPacket.pageError);
4974 }
4975 },
4977 /**
4978 * The "logMessage" message type handler. We redirect any message to the UI
4979 * for displaying.
4980 *
4981 * @private
4982 * @param string aType
4983 * Message type.
4984 * @param object aPacket
4985 * The message received from the server.
4986 */
4987 _onLogMessage: function WCCP__onLogMessage(aType, aPacket)
4988 {
4989 if (this.owner && aPacket.from == this._consoleActor) {
4990 this.owner.handleLogMessage(aPacket);
4991 }
4992 },
4994 /**
4995 * The "consoleAPICall" message type handler. We redirect any message to
4996 * the UI for displaying.
4997 *
4998 * @private
4999 * @param string aType
5000 * Message type.
5001 * @param object aPacket
5002 * The message received from the server.
5003 */
5004 _onConsoleAPICall: function WCCP__onConsoleAPICall(aType, aPacket)
5005 {
5006 if (this.owner && aPacket.from == this._consoleActor) {
5007 this.owner.handleConsoleAPICall(aPacket.message);
5008 }
5009 },
5011 /**
5012 * The "networkEvent" message type handler. We redirect any message to
5013 * the UI for displaying.
5014 *
5015 * @private
5016 * @param string aType
5017 * Message type.
5018 * @param object aPacket
5019 * The message received from the server.
5020 */
5021 _onNetworkEvent: function WCCP__onNetworkEvent(aType, aPacket)
5022 {
5023 if (this.owner && aPacket.from == this._consoleActor) {
5024 this.owner.handleNetworkEvent(aPacket.eventActor);
5025 }
5026 },
5028 /**
5029 * The "networkEventUpdate" message type handler. We redirect any message to
5030 * the UI for displaying.
5031 *
5032 * @private
5033 * @param string aType
5034 * Message type.
5035 * @param object aPacket
5036 * The message received from the server.
5037 */
5038 _onNetworkEventUpdate: function WCCP__onNetworkEvenUpdatet(aType, aPacket)
5039 {
5040 if (this.owner) {
5041 this.owner.handleNetworkEventUpdate(aPacket.from, aPacket.updateType,
5042 aPacket);
5043 }
5044 },
5046 /**
5047 * The "fileActivity" message type handler. We redirect any message to
5048 * the UI for displaying.
5049 *
5050 * @private
5051 * @param string aType
5052 * Message type.
5053 * @param object aPacket
5054 * The message received from the server.
5055 */
5056 _onFileActivity: function WCCP__onFileActivity(aType, aPacket)
5057 {
5058 if (this.owner && aPacket.from == this._consoleActor) {
5059 this.owner.handleFileActivity(aPacket.uri);
5060 }
5061 },
5063 _onReflowActivity: function WCCP__onReflowActivity(aType, aPacket)
5064 {
5065 if (this.owner && aPacket.from == this._consoleActor) {
5066 this.owner.handleReflowActivity(aPacket);
5067 }
5068 },
5070 /**
5071 * The "lastPrivateContextExited" message type handler. When this message is
5072 * received the Web Console UI is cleared.
5073 *
5074 * @private
5075 * @param string aType
5076 * Message type.
5077 * @param object aPacket
5078 * The message received from the server.
5079 */
5080 _onLastPrivateContextExited:
5081 function WCCP__onLastPrivateContextExited(aType, aPacket)
5082 {
5083 if (this.owner && aPacket.from == this._consoleActor) {
5084 this.owner.jsterm.clearPrivateMessages();
5085 }
5086 },
5088 /**
5089 * The "will-navigate" and "navigate" event handlers. We redirect any message
5090 * to the UI for displaying.
5091 *
5092 * @private
5093 * @param string aEvent
5094 * Event type.
5095 * @param object aPacket
5096 * The message received from the server.
5097 */
5098 _onTabNavigated: function WCCP__onTabNavigated(aEvent, aPacket)
5099 {
5100 if (!this.owner) {
5101 return;
5102 }
5104 this.owner.handleTabNavigated(aEvent, aPacket);
5105 },
5107 /**
5108 * Release an object actor.
5109 *
5110 * @param string aActor
5111 * The actor ID to send the request to.
5112 */
5113 releaseActor: function WCCP_releaseActor(aActor)
5114 {
5115 if (this.client) {
5116 this.client.release(aActor);
5117 }
5118 },
5120 /**
5121 * Disconnect the Web Console from the remote server.
5122 *
5123 * @return object
5124 * A promise object that is resolved when disconnect completes.
5125 */
5126 disconnect: function WCCP_disconnect()
5127 {
5128 if (this._disconnecter) {
5129 return this._disconnecter.promise;
5130 }
5132 this._disconnecter = promise.defer();
5134 if (!this.client) {
5135 this._disconnecter.resolve(null);
5136 return this._disconnecter.promise;
5137 }
5139 this.client.removeListener("logMessage", this._onLogMessage);
5140 this.client.removeListener("pageError", this._onPageError);
5141 this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
5142 this.client.removeListener("networkEvent", this._onNetworkEvent);
5143 this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
5144 this.client.removeListener("fileActivity", this._onFileActivity);
5145 this.client.removeListener("reflowActivity", this._onReflowActivity);
5146 this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
5147 this.target.off("will-navigate", this._onTabNavigated);
5148 this.target.off("navigate", this._onTabNavigated);
5150 this.client = null;
5151 this.webConsoleClient = null;
5152 this.target = null;
5153 this.connected = false;
5154 this.owner = null;
5155 this._disconnecter.resolve(null);
5157 return this._disconnecter.promise;
5158 },
5159 };
5161 function gSequenceId()
5162 {
5163 return gSequenceId.n++;
5164 }
5165 gSequenceId.n = 0;
5167 ///////////////////////////////////////////////////////////////////////////////
5168 // Context Menu
5169 ///////////////////////////////////////////////////////////////////////////////
5171 /*
5172 * ConsoleContextMenu this used to handle the visibility of context menu items.
5173 *
5174 * @constructor
5175 * @param object aOwner
5176 * The WebConsoleFrame instance that owns this object.
5177 */
5178 function ConsoleContextMenu(aOwner)
5179 {
5180 this.owner = aOwner;
5181 this.popup = this.owner.document.getElementById("output-contextmenu");
5182 this.build = this.build.bind(this);
5183 this.popup.addEventListener("popupshowing", this.build);
5184 }
5186 ConsoleContextMenu.prototype = {
5187 lastClickedMessage: null,
5189 /*
5190 * Handle to show/hide context menu item.
5191 */
5192 build: function CCM_build(aEvent)
5193 {
5194 let metadata = this.getSelectionMetadata(aEvent.rangeParent);
5195 for (let element of this.popup.children) {
5196 element.hidden = this.shouldHideMenuItem(element, metadata);
5197 }
5198 },
5200 /*
5201 * Get selection information from the view.
5202 *
5203 * @param nsIDOMElement aClickElement
5204 * The DOM element the user clicked on.
5205 * @return object
5206 * Selection metadata.
5207 */
5208 getSelectionMetadata: function CCM_getSelectionMetadata(aClickElement)
5209 {
5210 let metadata = {
5211 selectionType: "",
5212 selection: new Set(),
5213 };
5214 let selectedItems = this.owner.output.getSelectedMessages();
5215 if (!selectedItems.length) {
5216 let clickedItem = this.owner.output.getMessageForElement(aClickElement);
5217 if (clickedItem) {
5218 this.lastClickedMessage = clickedItem;
5219 selectedItems = [clickedItem];
5220 }
5221 }
5223 metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single";
5225 let selection = metadata.selection;
5226 for (let item of selectedItems) {
5227 switch (item.category) {
5228 case CATEGORY_NETWORK:
5229 selection.add("network");
5230 break;
5231 case CATEGORY_CSS:
5232 selection.add("css");
5233 break;
5234 case CATEGORY_JS:
5235 selection.add("js");
5236 break;
5237 case CATEGORY_WEBDEV:
5238 selection.add("webdev");
5239 break;
5240 }
5241 }
5243 return metadata;
5244 },
5246 /*
5247 * Determine if an item should be hidden.
5248 *
5249 * @param nsIDOMElement aMenuItem
5250 * @param object aMetadata
5251 * @return boolean
5252 * Whether the given item should be hidden or not.
5253 */
5254 shouldHideMenuItem: function CCM_shouldHideMenuItem(aMenuItem, aMetadata)
5255 {
5256 let selectionType = aMenuItem.getAttribute("selectiontype");
5257 if (selectionType && !aMetadata.selectionType == selectionType) {
5258 return true;
5259 }
5261 let selection = aMenuItem.getAttribute("selection");
5262 if (!selection) {
5263 return false;
5264 }
5266 let shouldHide = true;
5267 let itemData = selection.split("|");
5268 for (let type of aMetadata.selection) {
5269 // check whether this menu item should show or not.
5270 if (itemData.indexOf(type) !== -1) {
5271 shouldHide = false;
5272 break;
5273 }
5274 }
5276 return shouldHide;
5277 },
5279 /**
5280 * Destroy the ConsoleContextMenu object instance.
5281 */
5282 destroy: function CCM_destroy()
5283 {
5284 this.popup.removeEventListener("popupshowing", this.build);
5285 this.popup = null;
5286 this.owner = null;
5287 this.lastClickedMessage = null;
5288 },
5289 };