|
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/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 const {Cc, Ci, Cu} = require("chrome"); |
|
10 |
|
11 let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; |
|
12 |
|
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"); |
|
35 |
|
36 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; |
|
37 let l10n = new WebConsoleUtils.l10n(STRINGS_URI); |
|
38 |
|
39 const XHTML_NS = "http://www.w3.org/1999/xhtml"; |
|
40 |
|
41 const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent"; |
|
42 |
|
43 const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords"; |
|
44 |
|
45 const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security"; |
|
46 |
|
47 const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; |
|
48 |
|
49 const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul"; |
|
50 |
|
51 const CONSOLE_DIR_VIEW_HEIGHT = 0.6; |
|
52 |
|
53 const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"]; |
|
54 |
|
55 // The amount of time in milliseconds that we wait before performing a live |
|
56 // search. |
|
57 const SEARCH_DELAY = 200; |
|
58 |
|
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; |
|
63 |
|
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; |
|
73 |
|
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; |
|
80 |
|
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 ]; |
|
91 |
|
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 ]; |
|
99 |
|
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 ]; |
|
115 |
|
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 }; |
|
135 |
|
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; |
|
140 |
|
141 // Constants used for defining the direction of JSTerm input history navigation. |
|
142 const HISTORY_BACK = -1; |
|
143 const HISTORY_FORWARD = 1; |
|
144 |
|
145 // The indent of a console group in pixels. |
|
146 const GROUP_INDENT = 12; |
|
147 |
|
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; |
|
151 |
|
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 |
|
156 |
|
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 |
|
161 |
|
162 // The preference prefix for all of the Web Console filters. |
|
163 const FILTER_PREFS_PREFIX = "devtools.webconsole.filter."; |
|
164 |
|
165 // The minimum font size. |
|
166 const MIN_FONT_SIZE = 10; |
|
167 |
|
168 const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout"; |
|
169 const PREF_PERSISTLOG = "devtools.webconsole.persistlog"; |
|
170 const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; |
|
171 |
|
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; |
|
189 |
|
190 this._repeatNodes = {}; |
|
191 this._outputQueue = []; |
|
192 this._pruneCategoriesQueue = {}; |
|
193 this._networkRequests = {}; |
|
194 this.filterPrefs = {}; |
|
195 |
|
196 this.output = new ConsoleOutput(this); |
|
197 |
|
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); |
|
202 |
|
203 this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
|
204 this._outputTimerInitialized = false; |
|
205 |
|
206 EventEmitter.decorate(this); |
|
207 } |
|
208 exports.WebConsoleFrame = WebConsoleFrame; |
|
209 |
|
210 WebConsoleFrame.prototype = { |
|
211 /** |
|
212 * The WebConsole instance that owns this frame. |
|
213 * @see hudservice.js::WebConsole |
|
214 * @type object |
|
215 */ |
|
216 owner: null, |
|
217 |
|
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, |
|
227 |
|
228 /** |
|
229 * Getter for the xul:popupset that holds any popups we open. |
|
230 * @type nsIDOMElement |
|
231 */ |
|
232 get popupset() this.owner.mainPopupSet, |
|
233 |
|
234 /** |
|
235 * Holds the initialization promise object. |
|
236 * @private |
|
237 * @type object |
|
238 */ |
|
239 _initDefer: null, |
|
240 |
|
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, |
|
248 |
|
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, |
|
257 |
|
258 /** |
|
259 * Message nodes are stored here in a queue for later display. |
|
260 * |
|
261 * @private |
|
262 * @type array |
|
263 */ |
|
264 _outputQueue: null, |
|
265 |
|
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, |
|
273 |
|
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, |
|
282 |
|
283 /** |
|
284 * Timer used for flushing the messages output queue. |
|
285 * |
|
286 * @private |
|
287 * @type nsITimer |
|
288 */ |
|
289 _outputTimer: null, |
|
290 _outputTimerInitialized: null, |
|
291 |
|
292 /** |
|
293 * Store for tracking repeated nodes. |
|
294 * @private |
|
295 * @type object |
|
296 */ |
|
297 _repeatNodes: null, |
|
298 |
|
299 /** |
|
300 * Preferences for filtering messages by type. |
|
301 * @see this._initDefaultFilterPrefs() |
|
302 * @type object |
|
303 */ |
|
304 filterPrefs: null, |
|
305 |
|
306 /** |
|
307 * Prefix used for filter preferences. |
|
308 * @private |
|
309 * @type string |
|
310 */ |
|
311 _filterPrefsPrefix: FILTER_PREFS_PREFIX, |
|
312 |
|
313 /** |
|
314 * The nesting depth of the currently active console group. |
|
315 */ |
|
316 groupDepth: 0, |
|
317 |
|
318 /** |
|
319 * The current target location. |
|
320 * @type string |
|
321 */ |
|
322 contentLocation: "", |
|
323 |
|
324 /** |
|
325 * The JSTerm object that manage the console's input. |
|
326 * @see JSTerm |
|
327 * @type object |
|
328 */ |
|
329 jsterm: null, |
|
330 |
|
331 /** |
|
332 * The element that holds all of the messages we display. |
|
333 * @type nsIDOMElement |
|
334 */ |
|
335 outputNode: null, |
|
336 |
|
337 /** |
|
338 * The ConsoleOutput instance that manages all output. |
|
339 * @type object |
|
340 */ |
|
341 output: null, |
|
342 |
|
343 /** |
|
344 * The input element that allows the user to filter messages by string. |
|
345 * @type nsIDOMElement |
|
346 */ |
|
347 filterBox: null, |
|
348 |
|
349 /** |
|
350 * Getter for the debugger WebConsoleClient. |
|
351 * @type object |
|
352 */ |
|
353 get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null, |
|
354 |
|
355 _destroyer: null, |
|
356 |
|
357 // Used in tests. |
|
358 _saveRequestAndResponseBodies: false, |
|
359 |
|
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, |
|
364 |
|
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 ]; |
|
378 |
|
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 }); |
|
389 |
|
390 return deferred.promise; |
|
391 }, |
|
392 |
|
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 } |
|
405 |
|
406 let deferred = promise.defer(); |
|
407 let newValue = !!aValue; |
|
408 let toSet = { |
|
409 "NetworkMonitor.saveRequestAndResponseBodies": newValue, |
|
410 }; |
|
411 |
|
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 }); |
|
422 |
|
423 return deferred.promise; |
|
424 }, |
|
425 |
|
426 /** |
|
427 * Getter for the persistent logging preference. |
|
428 * @type boolean |
|
429 */ |
|
430 get persistLog() { |
|
431 return Services.prefs.getBoolPref(PREF_PERSISTLOG); |
|
432 }, |
|
433 |
|
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 }, |
|
444 |
|
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 } |
|
458 |
|
459 this._initDefer = promise.defer(); |
|
460 this.proxy = new WebConsoleConnectionProxy(this, this.owner.target); |
|
461 |
|
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 }); |
|
473 |
|
474 return this._initDefer.promise; |
|
475 }, |
|
476 |
|
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; |
|
485 |
|
486 this._initDefaultFilterPrefs(); |
|
487 |
|
488 // Register the controller to handle "select all" properly. |
|
489 this._commandController = new CommandController(this); |
|
490 this.window.controllers.insertControllerAt(0, this._commandController); |
|
491 |
|
492 this._contextMenuHandler = new ConsoleContextMenu(this); |
|
493 |
|
494 let doc = this.document; |
|
495 |
|
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"); |
|
500 |
|
501 this._setFilterTextBoxEvents(); |
|
502 this._initFilterButtons(); |
|
503 |
|
504 let fontSize = this.owner._browserConsole ? |
|
505 Services.prefs.getIntPref("devtools.webconsole.fontSize") : 0; |
|
506 |
|
507 if (fontSize != 0) { |
|
508 fontSize = Math.max(MIN_FONT_SIZE, fontSize); |
|
509 |
|
510 this.outputNode.style.fontSize = fontSize + "px"; |
|
511 this.completeNode.style.fontSize = fontSize + "px"; |
|
512 this.inputNode.style.fontSize = fontSize + "px"; |
|
513 } |
|
514 |
|
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 } |
|
521 |
|
522 // Update the character width and height needed for the popup offset |
|
523 // calculations. |
|
524 this._updateCharSize(); |
|
525 |
|
526 let updateSaveBodiesPrefUI = (aElement) => { |
|
527 this.getSaveRequestAndResponseBodies().then(aValue => { |
|
528 aElement.setAttribute("checked", aValue); |
|
529 this.emit("save-bodies-ui-toggled"); |
|
530 }); |
|
531 } |
|
532 |
|
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 } |
|
540 |
|
541 let saveBodies = doc.getElementById("saveBodies"); |
|
542 saveBodies.addEventListener("command", reverseSaveBodiesPref); |
|
543 saveBodies.disabled = !this.getFilterState("networkinfo") && |
|
544 !this.getFilterState("network"); |
|
545 |
|
546 let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu"); |
|
547 saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref); |
|
548 saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") && |
|
549 !this.getFilterState("network"); |
|
550 |
|
551 saveBodies.parentNode.addEventListener("popupshowing", () => { |
|
552 updateSaveBodiesPrefUI(saveBodies); |
|
553 saveBodies.disabled = !this.getFilterState("networkinfo") && |
|
554 !this.getFilterState("network"); |
|
555 }); |
|
556 |
|
557 saveBodiesContextMenu.parentNode.addEventListener("popupshowing", () => { |
|
558 updateSaveBodiesPrefUI(saveBodiesContextMenu); |
|
559 saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") && |
|
560 !this.getFilterState("network"); |
|
561 }); |
|
562 |
|
563 let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0]; |
|
564 clearButton.addEventListener("command", () => { |
|
565 this.owner._onClearButton(); |
|
566 this.jsterm.clearOutput(true); |
|
567 }); |
|
568 |
|
569 this.jsterm = new JSTerm(this); |
|
570 this.jsterm.init(); |
|
571 |
|
572 let toolbox = gDevTools.getToolbox(this.owner.target); |
|
573 if (toolbox) { |
|
574 toolbox.on("webconsole-selected", this._onPanelSelected); |
|
575 } |
|
576 |
|
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 }); |
|
588 |
|
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 }); |
|
595 |
|
596 // focus input node |
|
597 this.jsterm.inputNode.focus(); |
|
598 }, |
|
599 |
|
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 }, |
|
609 |
|
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 }, |
|
624 |
|
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 }, |
|
646 |
|
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); |
|
655 |
|
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 }; |
|
663 |
|
664 this.filterBox.addEventListener("command", onChange, false); |
|
665 this.filterBox.addEventListener("input", onChange, false); |
|
666 }, |
|
667 |
|
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); |
|
685 |
|
686 let someChecked = false; |
|
687 let severities = aButton.querySelectorAll("menuitem[prefKey]"); |
|
688 Array.forEach(severities, function(aMenuItem) { |
|
689 aMenuItem.addEventListener("command", this._toggleFilter, false); |
|
690 |
|
691 let prefKey = aMenuItem.getAttribute("prefKey"); |
|
692 let checked = this.filterPrefs[prefKey]; |
|
693 aMenuItem.setAttribute("checked", checked); |
|
694 someChecked = someChecked || checked; |
|
695 }, this); |
|
696 |
|
697 aButton.setAttribute("checked", someChecked); |
|
698 }, this); |
|
699 |
|
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 } |
|
707 |
|
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); |
|
712 |
|
713 let logging = this.document.querySelector("toolbarbutton[category=logging]"); |
|
714 logging.removeAttribute("accesskey"); |
|
715 } |
|
716 }, |
|
717 |
|
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", ""); |
|
730 |
|
731 if (this.outputNode.style.fontSize) { |
|
732 fontSize = this.outputNode.style.fontSize.replace("px", ""); |
|
733 } |
|
734 |
|
735 if (aSize == "+" || aSize == "-") { |
|
736 fontSize = parseInt(fontSize, 10); |
|
737 |
|
738 if (aSize == "+") { |
|
739 fontSize += 1; |
|
740 } |
|
741 else { |
|
742 fontSize -= 1; |
|
743 } |
|
744 |
|
745 if (fontSize < MIN_FONT_SIZE) { |
|
746 fontSize = MIN_FONT_SIZE; |
|
747 } |
|
748 |
|
749 Services.prefs.setIntPref("devtools.webconsole.fontSize", fontSize); |
|
750 fontSize = fontSize + "px"; |
|
751 |
|
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 }, |
|
764 |
|
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 }, |
|
791 |
|
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 } |
|
807 |
|
808 switch (tagName) { |
|
809 case "toolbarbutton": { |
|
810 let originalTarget = aEvent.originalTarget; |
|
811 let classes = originalTarget.classList; |
|
812 |
|
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 } |
|
819 |
|
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 } |
|
827 |
|
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); |
|
843 |
|
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); |
|
848 |
|
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 } |
|
857 |
|
858 break; |
|
859 } |
|
860 |
|
861 case "menuitem": { |
|
862 let state = target.getAttribute("checked") !== "true"; |
|
863 target.setAttribute("checked", state); |
|
864 |
|
865 let prefKey = target.getAttribute("prefKey"); |
|
866 this.setFilterState(prefKey, state); |
|
867 |
|
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 } |
|
875 |
|
876 // Adjust the state of the button appropriately. |
|
877 let menuPopup = target.parentNode; |
|
878 |
|
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 }, |
|
895 |
|
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 }, |
|
914 |
|
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 }, |
|
929 |
|
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 }, |
|
940 |
|
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 } |
|
955 |
|
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 }, |
|
962 |
|
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; |
|
980 |
|
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. |
|
984 |
|
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 }, |
|
999 |
|
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; |
|
1009 |
|
1010 for (let i = 0, n = nodes.length; i < n; ++i) { |
|
1011 let node = nodes[i]; |
|
1012 |
|
1013 // hide nodes that match the strings |
|
1014 let text = node.textContent; |
|
1015 |
|
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 }, |
|
1025 |
|
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; |
|
1038 |
|
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 } |
|
1046 |
|
1047 // Filter on the search string. |
|
1048 let search = this.filterBox.value; |
|
1049 let text = aNode.clipboardText; |
|
1050 |
|
1051 // if string matches the filter text |
|
1052 if (!this.stringMatchesFilters(text, search)) { |
|
1053 aNode.classList.add("filtered-by-string"); |
|
1054 isFiltered = true; |
|
1055 } |
|
1056 |
|
1057 if (isFiltered && aNode.classList.contains("inlined-variables-view")) { |
|
1058 aNode.classList.add("hidden-message"); |
|
1059 } |
|
1060 |
|
1061 return isFiltered; |
|
1062 }, |
|
1063 |
|
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 } |
|
1080 |
|
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 }, |
|
1088 |
|
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 } |
|
1105 |
|
1106 let uid = repeatNode._uid; |
|
1107 let dupeNode = null; |
|
1108 |
|
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 } |
|
1124 |
|
1125 let lastRepeatNode = lastMessage.getElementsByClassName("message-repeats")[0]; |
|
1126 if (lastRepeatNode && lastRepeatNode._uid == uid) { |
|
1127 dupeNode = lastMessage; |
|
1128 } |
|
1129 } |
|
1130 |
|
1131 if (dupeNode) { |
|
1132 this.mergeFilteredMessageNode(dupeNode, aNode); |
|
1133 return dupeNode; |
|
1134 } |
|
1135 |
|
1136 return null; |
|
1137 }, |
|
1138 |
|
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 } |
|
1152 |
|
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 }, |
|
1171 |
|
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; |
|
1191 |
|
1192 // Gather the actor IDs. |
|
1193 args.forEach((aValue) => { |
|
1194 if (WebConsoleUtils.isActorGrip(aValue)) { |
|
1195 objectActors.add(aValue.actor); |
|
1196 } |
|
1197 }); |
|
1198 |
|
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 } |
|
1225 |
|
1226 case "group": |
|
1227 case "groupCollapsed": |
|
1228 clipboardText = body = aMessage.groupName; |
|
1229 this.groupDepth++; |
|
1230 break; |
|
1231 |
|
1232 case "groupEnd": |
|
1233 if (this.groupDepth > 0) { |
|
1234 this.groupDepth--; |
|
1235 } |
|
1236 break; |
|
1237 |
|
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 } |
|
1251 |
|
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 } |
|
1262 |
|
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 } |
|
1276 |
|
1277 default: |
|
1278 Cu.reportError("Unknown Console API log level: " + level); |
|
1279 return null; |
|
1280 } |
|
1281 |
|
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 } |
|
1296 |
|
1297 if (level == "groupEnd") { |
|
1298 return null; // no need to continue |
|
1299 } |
|
1300 |
|
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 } |
|
1309 |
|
1310 if (objectActors.size > 0) { |
|
1311 node._objectActors = objectActors; |
|
1312 |
|
1313 if (!node._messageObject) { |
|
1314 let repeatNode = node.getElementsByClassName("message-repeats")[0]; |
|
1315 repeatNode._uid += [...objectActors].join("-"); |
|
1316 } |
|
1317 } |
|
1318 |
|
1319 return node; |
|
1320 }, |
|
1321 |
|
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 }, |
|
1333 |
|
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 } |
|
1350 |
|
1351 let objectActors = new Set(); |
|
1352 |
|
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 } |
|
1360 |
|
1361 let errorMessage = aScriptError.errorMessage; |
|
1362 if (errorMessage.type && errorMessage.type == "longString") { |
|
1363 errorMessage = errorMessage.initial; |
|
1364 } |
|
1365 |
|
1366 let node = this.createMessageNode(aCategory, severity, |
|
1367 errorMessage, |
|
1368 aScriptError.sourceName, |
|
1369 aScriptError.lineNumber, null, null, |
|
1370 aScriptError.timeStamp); |
|
1371 |
|
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); |
|
1376 |
|
1377 if (aScriptError.private) { |
|
1378 node.setAttribute("private", true); |
|
1379 } |
|
1380 |
|
1381 if (objectActors.size > 0) { |
|
1382 node._objectActors = objectActors; |
|
1383 } |
|
1384 |
|
1385 return node; |
|
1386 }, |
|
1387 |
|
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 }, |
|
1400 |
|
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 }, |
|
1414 |
|
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 }, |
|
1437 |
|
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 } |
|
1452 |
|
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 } |
|
1461 |
|
1462 let methodNode = this.document.createElementNS(XHTML_NS, "span"); |
|
1463 methodNode.className = "method"; |
|
1464 methodNode.textContent = request.method + " "; |
|
1465 |
|
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; |
|
1474 |
|
1475 let body = methodNode.parentNode; |
|
1476 body.setAttribute("aria-haspopup", true); |
|
1477 |
|
1478 let displayUrl = request.url; |
|
1479 let pos = displayUrl.indexOf("?"); |
|
1480 if (pos > -1) { |
|
1481 displayUrl = displayUrl.substr(0, pos); |
|
1482 } |
|
1483 |
|
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(" ")); |
|
1492 |
|
1493 if (mixedRequest) { |
|
1494 messageNode.classList.add("mixed-content"); |
|
1495 this.makeMixedContentNode(body); |
|
1496 } |
|
1497 |
|
1498 let statusNode = this.document.createElementNS(XHTML_NS, "a"); |
|
1499 statusNode.className = "status"; |
|
1500 body.appendChild(statusNode); |
|
1501 |
|
1502 let onClick = () => { |
|
1503 if (!messageNode._panelOpen) { |
|
1504 this.openNetworkPanel(messageNode, networkInfo); |
|
1505 } |
|
1506 }; |
|
1507 |
|
1508 this._addMessageLinkCallback(urlNode, onClick); |
|
1509 this._addMessageLinkCallback(statusNode, onClick); |
|
1510 |
|
1511 networkInfo.node = messageNode; |
|
1512 |
|
1513 this._updateNetMessage(aActorId); |
|
1514 |
|
1515 return messageNode; |
|
1516 }, |
|
1517 |
|
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") + "]"; |
|
1527 |
|
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; |
|
1535 |
|
1536 aLinkNode.appendChild(mixedContentWarningNode); |
|
1537 |
|
1538 this._addMessageLinkCallback(mixedContentWarningNode, (aEvent) => { |
|
1539 aEvent.stopPropagation(); |
|
1540 this.owner.openLink(MIXED_CONTENT_LEARN_MORE); |
|
1541 }); |
|
1542 }, |
|
1543 |
|
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 } |
|
1571 |
|
1572 this.addLearnMoreWarningNode(aNode, url); |
|
1573 }, |
|
1574 |
|
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") + "]"; |
|
1591 |
|
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"; |
|
1598 |
|
1599 this._addMessageLinkCallback(warningNode, (aEvent) => { |
|
1600 aEvent.stopPropagation(); |
|
1601 this.owner.openLink(aURL); |
|
1602 }); |
|
1603 |
|
1604 aNode.appendChild(warningNode); |
|
1605 }, |
|
1606 |
|
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; |
|
1623 |
|
1624 let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG, |
|
1625 urlNode, null, null, aFileURI); |
|
1626 |
|
1627 this._addMessageLinkCallback(urlNode, () => { |
|
1628 this.owner.viewSource(aFileURI); |
|
1629 }); |
|
1630 |
|
1631 return outputNode; |
|
1632 }, |
|
1633 |
|
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 }, |
|
1644 |
|
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 }, |
|
1674 |
|
1675 |
|
1676 handleReflowActivity: function WCF_handleReflowActivity(aMessage) |
|
1677 { |
|
1678 this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [aMessage]); |
|
1679 }, |
|
1680 |
|
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 }, |
|
1691 |
|
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 }; |
|
1715 |
|
1716 this._networkRequests[aActor.actor] = networkInfo; |
|
1717 this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor.actor]); |
|
1718 }, |
|
1719 |
|
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 } |
|
1737 |
|
1738 networkInfo.updates.push(aType); |
|
1739 |
|
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 } |
|
1766 |
|
1767 if (networkInfo.node && this._updateNetMessage(aActorId)) { |
|
1768 this.emit("messages-updated", new Set([networkInfo.node])); |
|
1769 } |
|
1770 |
|
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 }, |
|
1779 |
|
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 } |
|
1796 |
|
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; |
|
1804 |
|
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(" ") + "]"; |
|
1815 |
|
1816 let statusNode = messageNode.getElementsByClassName("status")[0]; |
|
1817 statusNode.textContent = statusText; |
|
1818 |
|
1819 messageNode.clipboardText = [request.method, request.url, statusText] |
|
1820 .join(" "); |
|
1821 |
|
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 } |
|
1826 |
|
1827 updated = true; |
|
1828 } |
|
1829 |
|
1830 if (messageNode._netPanel) { |
|
1831 messageNode._netPanel.update(); |
|
1832 } |
|
1833 |
|
1834 return updated; |
|
1835 }, |
|
1836 |
|
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; |
|
1851 |
|
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 } |
|
1859 |
|
1860 aHttpActivity.request.headers = aResponse.headers; |
|
1861 |
|
1862 this.webConsoleClient.getRequestCookies(actor, onRequestCookies); |
|
1863 }.bind(this)); |
|
1864 } |
|
1865 |
|
1866 let onRequestCookies = function(aResponse) { |
|
1867 if (aResponse.error) { |
|
1868 Cu.reportError("WCF_openNetworkPanel getRequestCookies:" + |
|
1869 aResponse.error); |
|
1870 return; |
|
1871 } |
|
1872 |
|
1873 aHttpActivity.request.cookies = aResponse.cookies; |
|
1874 |
|
1875 this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders); |
|
1876 }.bind(this); |
|
1877 |
|
1878 let onResponseHeaders = function(aResponse) { |
|
1879 if (aResponse.error) { |
|
1880 Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" + |
|
1881 aResponse.error); |
|
1882 return; |
|
1883 } |
|
1884 |
|
1885 aHttpActivity.response.headers = aResponse.headers; |
|
1886 |
|
1887 this.webConsoleClient.getResponseCookies(actor, onResponseCookies); |
|
1888 }.bind(this); |
|
1889 |
|
1890 let onResponseCookies = function(aResponse) { |
|
1891 if (aResponse.error) { |
|
1892 Cu.reportError("WCF_openNetworkPanel getResponseCookies:" + |
|
1893 aResponse.error); |
|
1894 return; |
|
1895 } |
|
1896 |
|
1897 aHttpActivity.response.cookies = aResponse.cookies; |
|
1898 |
|
1899 this.webConsoleClient.getRequestPostData(actor, onRequestPostData); |
|
1900 }.bind(this); |
|
1901 |
|
1902 let onRequestPostData = function(aResponse) { |
|
1903 if (aResponse.error) { |
|
1904 Cu.reportError("WCF_openNetworkPanel getRequestPostData:" + |
|
1905 aResponse.error); |
|
1906 return; |
|
1907 } |
|
1908 |
|
1909 aHttpActivity.request.postData = aResponse.postData; |
|
1910 aHttpActivity.discardRequestBody = aResponse.postDataDiscarded; |
|
1911 |
|
1912 this.webConsoleClient.getResponseContent(actor, onResponseContent); |
|
1913 }.bind(this); |
|
1914 |
|
1915 let onResponseContent = function(aResponse) { |
|
1916 if (aResponse.error) { |
|
1917 Cu.reportError("WCF_openNetworkPanel getResponseContent:" + |
|
1918 aResponse.error); |
|
1919 return; |
|
1920 } |
|
1921 |
|
1922 aHttpActivity.response.content = aResponse.content; |
|
1923 aHttpActivity.discardResponseBody = aResponse.contentDiscarded; |
|
1924 |
|
1925 this.webConsoleClient.getEventTimings(actor, onEventTimings); |
|
1926 }.bind(this); |
|
1927 |
|
1928 let onEventTimings = function(aResponse) { |
|
1929 if (aResponse.error) { |
|
1930 Cu.reportError("WCF_openNetworkPanel getEventTimings:" + |
|
1931 aResponse.error); |
|
1932 return; |
|
1933 } |
|
1934 |
|
1935 aHttpActivity.timings = aResponse.timings; |
|
1936 |
|
1937 openPanel(); |
|
1938 }.bind(this); |
|
1939 |
|
1940 let openPanel = function() { |
|
1941 aNode._netPanel = netPanel; |
|
1942 |
|
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); |
|
1947 |
|
1948 panel.addEventListener("popuphiding", function WCF_netPanel_onHide() { |
|
1949 panel.removeEventListener("popuphiding", WCF_netPanel_onHide); |
|
1950 |
|
1951 aNode._panelOpen = false; |
|
1952 aNode._netPanel = null; |
|
1953 }); |
|
1954 |
|
1955 aNode._panelOpen = true; |
|
1956 }.bind(this); |
|
1957 |
|
1958 let netPanel = new NetworkPanel(this.popupset, aHttpActivity, this); |
|
1959 netPanel.linkNode = aNode; |
|
1960 |
|
1961 if (!actor) { |
|
1962 openPanel(); |
|
1963 } |
|
1964 |
|
1965 return netPanel; |
|
1966 }, |
|
1967 |
|
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 }, |
|
1983 |
|
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 } |
|
2003 |
|
2004 if (aPacket.url) { |
|
2005 this.onLocationChange(aPacket.url, aPacket.title); |
|
2006 } |
|
2007 |
|
2008 if (aEvent == "navigate" && !aPacket.nativeConsoleAPI) { |
|
2009 this.logWarningAboutReplacedAPI(); |
|
2010 } |
|
2011 }, |
|
2012 |
|
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 } |
|
2038 |
|
2039 this._outputQueue.push([aCategory, aMethodOrNode, aArguments]); |
|
2040 |
|
2041 if (!this._outputTimerInitialized) { |
|
2042 this._initOutputTimer(); |
|
2043 } |
|
2044 }, |
|
2045 |
|
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 } |
|
2058 |
|
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 } |
|
2065 |
|
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 } |
|
2072 |
|
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 } |
|
2079 |
|
2080 let batch = this._outputQueue.splice(0, toDisplay); |
|
2081 if (!batch.length) { |
|
2082 this._outputTimerInitialized = false; |
|
2083 return; |
|
2084 } |
|
2085 |
|
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); |
|
2091 |
|
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 } |
|
2109 |
|
2110 let oldScrollHeight = 0; |
|
2111 |
|
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; |
|
2117 |
|
2118 let categories = Object.keys(this._pruneCategoriesQueue); |
|
2119 categories.forEach(function _pruneOutput(aCategory) { |
|
2120 removedNodes += this.pruneOutputIfNecessary(aCategory); |
|
2121 }, this); |
|
2122 this._pruneCategoriesQueue = {}; |
|
2123 } |
|
2124 |
|
2125 let isInputOutput = lastVisibleNode && |
|
2126 (lastVisibleNode.category == CATEGORY_INPUT || |
|
2127 lastVisibleNode.category == CATEGORY_OUTPUT); |
|
2128 |
|
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 } |
|
2141 |
|
2142 if (newMessages.size) { |
|
2143 this.emit("messages-added", newMessages); |
|
2144 } |
|
2145 if (updatedMessages.size) { |
|
2146 this.emit("messages-updated", updatedMessages); |
|
2147 } |
|
2148 |
|
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 } |
|
2159 |
|
2160 this._lastOutputFlush = Date.now(); |
|
2161 }, |
|
2162 |
|
2163 /** |
|
2164 * Initialize the output timer. |
|
2165 * @private |
|
2166 */ |
|
2167 _initOutputTimer: function WCF__initOutputTimer() |
|
2168 { |
|
2169 if (!this._outputTimer) { |
|
2170 return; |
|
2171 } |
|
2172 |
|
2173 this._outputTimerInitialized = true; |
|
2174 this._outputTimer.initWithCallback(this._flushMessageQueue, |
|
2175 OUTPUT_INTERVAL, |
|
2176 Ci.nsITimer.TYPE_ONE_SHOT); |
|
2177 }, |
|
2178 |
|
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; |
|
2198 |
|
2199 let node = typeof methodOrNode == "function" ? |
|
2200 methodOrNode.apply(this, args || []) : |
|
2201 methodOrNode; |
|
2202 if (!node) { |
|
2203 return null; |
|
2204 } |
|
2205 |
|
2206 let afterNode = node._outputAfterNode; |
|
2207 if (afterNode) { |
|
2208 delete node._outputAfterNode; |
|
2209 } |
|
2210 |
|
2211 let isFiltered = this.filterMessageNode(node); |
|
2212 |
|
2213 let isRepeated = this._filterRepeatedMessage(node); |
|
2214 |
|
2215 let visible = !isRepeated && !isFiltered; |
|
2216 if (!isRepeated) { |
|
2217 this.outputNode.insertBefore(node, |
|
2218 afterNode ? afterNode.nextSibling : null); |
|
2219 this._pruneCategoriesQueue[node.category] = true; |
|
2220 |
|
2221 let nodeID = node.getAttribute("id"); |
|
2222 Services.obs.notifyObservers(aHudIdSupportsString, |
|
2223 "web-console-message-created", nodeID); |
|
2224 |
|
2225 } |
|
2226 |
|
2227 if (node._onOutput) { |
|
2228 node._onOutput(); |
|
2229 delete node._onOutput; |
|
2230 } |
|
2231 |
|
2232 return { |
|
2233 visible: visible, |
|
2234 node: node, |
|
2235 isRepeated: isRepeated, |
|
2236 }; |
|
2237 }, |
|
2238 |
|
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 = {}; |
|
2247 |
|
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); |
|
2256 |
|
2257 let pruned = 0; |
|
2258 |
|
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 } |
|
2272 |
|
2273 return pruned; |
|
2274 }, |
|
2275 |
|
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. |
|
2287 |
|
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 } |
|
2295 |
|
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 } |
|
2308 |
|
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 }, |
|
2347 |
|
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); |
|
2365 |
|
2366 return n; |
|
2367 }, |
|
2368 |
|
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 } |
|
2380 |
|
2381 if (aNode._objectActors) { |
|
2382 for (let actor of aNode._objectActors) { |
|
2383 this._releaseObject(actor); |
|
2384 } |
|
2385 aNode._objectActors.clear(); |
|
2386 } |
|
2387 |
|
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 } |
|
2407 |
|
2408 if (aNode.parentNode) { |
|
2409 aNode.parentNode.removeChild(aNode); |
|
2410 } |
|
2411 }, |
|
2412 |
|
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 } |
|
2448 |
|
2449 let indentNode = this.document.createElementNS(XHTML_NS, "span"); |
|
2450 indentNode.className = "indent"; |
|
2451 |
|
2452 // Apply the current group by indenting appropriately. |
|
2453 let indent = this.groupDepth * GROUP_INDENT; |
|
2454 indentNode.style.width = indent + "px"; |
|
2455 |
|
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"; |
|
2461 |
|
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"; |
|
2465 |
|
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 : "")); |
|
2473 |
|
2474 let timestamp = aTimeStamp || Date.now(); |
|
2475 |
|
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); |
|
2483 |
|
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 } |
|
2495 |
|
2496 if (str !== undefined) { |
|
2497 aBody = this.document.createTextNode(str); |
|
2498 bodyNode.appendChild(aBody); |
|
2499 } |
|
2500 } |
|
2501 |
|
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 } |
|
2515 |
|
2516 // Create the timestamp. |
|
2517 let timestampNode = this.document.createElementNS(XHTML_NS, "span"); |
|
2518 timestampNode.className = "timestamp devtools-monospace"; |
|
2519 |
|
2520 let timestampString = l10n.timestampString(timestamp); |
|
2521 timestampNode.textContent = timestampString + " "; |
|
2522 |
|
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 } |
|
2529 |
|
2530 node.appendChild(timestampNode); |
|
2531 node.appendChild(indentNode); |
|
2532 node.appendChild(iconContainer); |
|
2533 |
|
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"; |
|
2538 |
|
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 }); |
|
2550 |
|
2551 node.classList.add("inlined-variables-view"); |
|
2552 } |
|
2553 |
|
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")); |
|
2562 |
|
2563 return node; |
|
2564 }, |
|
2565 |
|
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"); |
|
2589 |
|
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; |
|
2595 |
|
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 } |
|
2605 |
|
2606 filenameNode.className = "filename"; |
|
2607 filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation")); |
|
2608 locationNode.appendChild(filenameNode); |
|
2609 |
|
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"; |
|
2617 |
|
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 } |
|
2625 |
|
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 }; |
|
2638 |
|
2639 if (fullURL) { |
|
2640 this._addMessageLinkCallback(locationNode, onClick); |
|
2641 } |
|
2642 |
|
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 } |
|
2650 |
|
2651 return locationNode; |
|
2652 }, |
|
2653 |
|
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 }, |
|
2674 |
|
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); |
|
2691 |
|
2692 aNode.addEventListener("click", (aEvent) => { |
|
2693 let mousedown = this._mousedown; |
|
2694 this._mousedown = false; |
|
2695 |
|
2696 aEvent.preventDefault(); |
|
2697 |
|
2698 // Do not allow middle/right-click or 2+ clicks. |
|
2699 if (aEvent.detail != 1 || aEvent.button != 0) { |
|
2700 return; |
|
2701 } |
|
2702 |
|
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 } |
|
2712 |
|
2713 this._startX = this._startY = undefined; |
|
2714 |
|
2715 aCallback.call(this, aEvent); |
|
2716 }, false); |
|
2717 }, |
|
2718 |
|
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); |
|
2726 |
|
2727 aNode.addEventListener("click", (aEvent) => { |
|
2728 let mousedown = this._mousedown; |
|
2729 this._mousedown = false; |
|
2730 |
|
2731 // Do not allow middle/right-click or 2+ clicks. |
|
2732 if (aEvent.detail != 1 || aEvent.button != 0) { |
|
2733 return; |
|
2734 } |
|
2735 |
|
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 } |
|
2747 |
|
2748 this._startX = this._startY = undefined; |
|
2749 |
|
2750 aCallback.call(this, aEvent); |
|
2751 }, false); |
|
2752 }, |
|
2753 |
|
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 }, |
|
2776 |
|
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 }; |
|
2788 |
|
2789 // Gather up the selected items and concatenate their clipboard text. |
|
2790 let strings = []; |
|
2791 |
|
2792 let children = this.output.getSelectedMessages(); |
|
2793 if (!children.length && aOptions.contextmenu) { |
|
2794 children = [this._contextMenuHandler.lastClickedMessage]; |
|
2795 } |
|
2796 |
|
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 } |
|
2810 |
|
2811 clipboardHelper.copyString(strings.join("\n"), this.document); |
|
2812 }, |
|
2813 |
|
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 }, |
|
2836 |
|
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 }, |
|
2850 |
|
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; |
|
2858 |
|
2859 if (!item || !item.url) { |
|
2860 return; |
|
2861 } |
|
2862 |
|
2863 this.owner.openLink(item.url); |
|
2864 }, |
|
2865 |
|
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 } |
|
2879 |
|
2880 this._destroyer = promise.defer(); |
|
2881 |
|
2882 let toolbox = gDevTools.getToolbox(this.owner.target); |
|
2883 if (toolbox) { |
|
2884 toolbox.off("webconsole-selected", this._onPanelSelected); |
|
2885 } |
|
2886 |
|
2887 gDevTools.off("pref-changed", this._onToolboxPrefChanged); |
|
2888 |
|
2889 this._repeatNodes = {}; |
|
2890 this._outputQueue = []; |
|
2891 this._pruneCategoriesQueue = {}; |
|
2892 this._networkRequests = {}; |
|
2893 |
|
2894 if (this._outputTimerInitialized) { |
|
2895 this._outputTimerInitialized = false; |
|
2896 this._outputTimer.cancel(); |
|
2897 } |
|
2898 this._outputTimer = null; |
|
2899 |
|
2900 if (this.jsterm) { |
|
2901 this.jsterm.destroy(); |
|
2902 this.jsterm = null; |
|
2903 } |
|
2904 this.output.destroy(); |
|
2905 this.output = null; |
|
2906 |
|
2907 if (this._contextMenuHandler) { |
|
2908 this._contextMenuHandler.destroy(); |
|
2909 this._contextMenuHandler = null; |
|
2910 } |
|
2911 |
|
2912 this._commandController = null; |
|
2913 |
|
2914 let onDestroy = function() { |
|
2915 this._destroyer.resolve(null); |
|
2916 }.bind(this); |
|
2917 |
|
2918 if (this.proxy) { |
|
2919 this.proxy.disconnect().then(onDestroy); |
|
2920 this.proxy = null; |
|
2921 } |
|
2922 else { |
|
2923 onDestroy(); |
|
2924 } |
|
2925 |
|
2926 return this._destroyer.promise; |
|
2927 }, |
|
2928 }; |
|
2929 |
|
2930 |
|
2931 /** |
|
2932 * @see VariablesView.simpleValueEvalMacro |
|
2933 */ |
|
2934 function simpleValueEvalMacro(aItem, aCurrentString) |
|
2935 { |
|
2936 return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self"); |
|
2937 }; |
|
2938 |
|
2939 |
|
2940 /** |
|
2941 * @see VariablesView.overrideValueEvalMacro |
|
2942 */ |
|
2943 function overrideValueEvalMacro(aItem, aCurrentString) |
|
2944 { |
|
2945 return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self"); |
|
2946 }; |
|
2947 |
|
2948 |
|
2949 /** |
|
2950 * @see VariablesView.getterOrSetterEvalMacro |
|
2951 */ |
|
2952 function getterOrSetterEvalMacro(aItem, aCurrentString) |
|
2953 { |
|
2954 return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self"); |
|
2955 } |
|
2956 |
|
2957 |
|
2958 |
|
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; |
|
2972 |
|
2973 this.lastCompletion = { value: null }; |
|
2974 this.history = []; |
|
2975 |
|
2976 // Holds the number of entries in history. This value is incremented in |
|
2977 // this.execute(). |
|
2978 this.historyIndex = 0; // incremented on this.execute() |
|
2979 |
|
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(); |
|
2984 |
|
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); |
|
2990 |
|
2991 EventEmitter.decorate(this); |
|
2992 } |
|
2993 |
|
2994 JSTerm.prototype = { |
|
2995 SELECTED_FRAME: -1, |
|
2996 |
|
2997 /** |
|
2998 * Stores the data for the last completion. |
|
2999 * @type object |
|
3000 */ |
|
3001 lastCompletion: null, |
|
3002 |
|
3003 /** |
|
3004 * Array that caches the user input suggestions received from the server. |
|
3005 * @private |
|
3006 * @type array |
|
3007 */ |
|
3008 _autocompleteCache: null, |
|
3009 |
|
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, |
|
3017 |
|
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, |
|
3025 |
|
3026 /** |
|
3027 * The Web Console sidebar. |
|
3028 * @see this._createSidebar() |
|
3029 * @see Sidebar.jsm |
|
3030 */ |
|
3031 sidebar: null, |
|
3032 |
|
3033 /** |
|
3034 * The Variables View instance shown in the sidebar. |
|
3035 * @private |
|
3036 * @type object |
|
3037 */ |
|
3038 _variablesView: null, |
|
3039 |
|
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, |
|
3048 |
|
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, |
|
3058 |
|
3059 /** |
|
3060 * Last input value. |
|
3061 * @type string |
|
3062 */ |
|
3063 lastInputValue: "", |
|
3064 |
|
3065 /** |
|
3066 * Tells if the input node changed since the last focus. |
|
3067 * |
|
3068 * @private |
|
3069 * @type boolean |
|
3070 */ |
|
3071 _inputChanged: false, |
|
3072 |
|
3073 /** |
|
3074 * Tells if the autocomplete popup was navigated since the last open. |
|
3075 * |
|
3076 * @private |
|
3077 * @type boolean |
|
3078 */ |
|
3079 _autocompletePopupNavigated: false, |
|
3080 |
|
3081 /** |
|
3082 * History of code that was executed. |
|
3083 * @type array |
|
3084 */ |
|
3085 history: null, |
|
3086 autocompletePopup: null, |
|
3087 inputNode: null, |
|
3088 completeNode: null, |
|
3089 |
|
3090 /** |
|
3091 * Getter for the element that holds the messages we display. |
|
3092 * @type nsIDOMElement |
|
3093 */ |
|
3094 get outputNode() this.hud.outputNode, |
|
3095 |
|
3096 /** |
|
3097 * Getter for the debugger WebConsoleClient. |
|
3098 * @type object |
|
3099 */ |
|
3100 get webConsoleClient() this.hud.webConsoleClient, |
|
3101 |
|
3102 COMPLETE_FORWARD: 0, |
|
3103 COMPLETE_BACKWARD: 1, |
|
3104 COMPLETE_HINT_ONLY: 2, |
|
3105 COMPLETE_PAGEUP: 3, |
|
3106 COMPLETE_PAGEDOWN: 4, |
|
3107 |
|
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); |
|
3125 |
|
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"); |
|
3130 |
|
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 } |
|
3141 |
|
3142 this.hud.window.addEventListener("blur", this._blurEventHandler, false); |
|
3143 this.lastInputValue && this.setInputValue(this.lastInputValue); |
|
3144 }, |
|
3145 |
|
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; |
|
3174 |
|
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 } |
|
3205 |
|
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 } |
|
3213 |
|
3214 let msg = new Messages.JavaScriptEvalOutput(aResponse, errorMessage); |
|
3215 this.hud.output.addMessage(msg); |
|
3216 |
|
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 } |
|
3226 |
|
3227 return false; |
|
3228 }; |
|
3229 } |
|
3230 |
|
3231 msg._afterMessage = aAfterMessage; |
|
3232 msg._objectActors = new Set(); |
|
3233 |
|
3234 if (WebConsoleUtils.isActorGrip(aResponse.exception)) { |
|
3235 msg._objectActors.add(aResponse.exception.actor); |
|
3236 } |
|
3237 |
|
3238 if (WebConsoleUtils.isActorGrip(result)) { |
|
3239 msg._objectActors.add(result.actor); |
|
3240 } |
|
3241 }, |
|
3242 |
|
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 } |
|
3259 |
|
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); |
|
3266 |
|
3267 let options = { frame: this.SELECTED_FRAME }; |
|
3268 this.requestEvaluation(aExecuteString, options).then(onResult, onResult); |
|
3269 |
|
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 }, |
|
3278 |
|
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(); |
|
3303 |
|
3304 function onResult(aResponse) { |
|
3305 if (!aResponse.error) { |
|
3306 deferred.resolve(aResponse); |
|
3307 } |
|
3308 else { |
|
3309 deferred.reject(aResponse); |
|
3310 } |
|
3311 } |
|
3312 |
|
3313 let frameActor = null; |
|
3314 if ("frame" in aOptions) { |
|
3315 frameActor = this.getFrameActor(aOptions.frame); |
|
3316 } |
|
3317 |
|
3318 let evalOptions = { |
|
3319 bindObjectActor: aOptions.bindObjectActor, |
|
3320 frameActor: frameActor, |
|
3321 }; |
|
3322 |
|
3323 this.webConsoleClient.evaluateJS(aString, onResult, evalOptions); |
|
3324 return deferred.promise; |
|
3325 }, |
|
3326 |
|
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 } |
|
3341 |
|
3342 let grip; |
|
3343 if (aFrame == this.SELECTED_FRAME) { |
|
3344 grip = state.frames[state.selected]; |
|
3345 } |
|
3346 else { |
|
3347 grip = state.frames[aFrame]; |
|
3348 } |
|
3349 |
|
3350 return grip ? grip.actor : null; |
|
3351 }, |
|
3352 |
|
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); |
|
3392 |
|
3393 if (!aOptions.targetElement && aOptions.autofocus) { |
|
3394 aWindow.focus(); |
|
3395 } |
|
3396 |
|
3397 this.emit("variablesview-open", view, aOptions); |
|
3398 return view; |
|
3399 }; |
|
3400 |
|
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"); |
|
3407 |
|
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); |
|
3413 |
|
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 } |
|
3425 |
|
3426 return openPromise.then(onContainerReady); |
|
3427 }, |
|
3428 |
|
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 }, |
|
3441 |
|
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(); |
|
3452 |
|
3453 let onTabReady = () => { |
|
3454 let window = this.sidebar.getWindowForTab("variablesview"); |
|
3455 deferred.resolve(window); |
|
3456 }; |
|
3457 |
|
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 } |
|
3472 |
|
3473 return deferred.promise; |
|
3474 }, |
|
3475 |
|
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 } |
|
3492 |
|
3493 this._sidebarDestroy(); |
|
3494 this.inputNode.focus(); |
|
3495 aEvent.stopPropagation(); |
|
3496 }, |
|
3497 |
|
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; |
|
3518 |
|
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 }); |
|
3536 |
|
3537 // Relay events from the VariablesView. |
|
3538 view.on("fetched", (aEvent, aType, aVar) => { |
|
3539 this.emit("variablesview-fetched", aVar); |
|
3540 }); |
|
3541 |
|
3542 return view; |
|
3543 }, |
|
3544 |
|
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(); |
|
3561 |
|
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 }); |
|
3567 |
|
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 } |
|
3581 |
|
3582 let { variable, expanded } = view.controller.setSingleVariable(aOptions); |
|
3583 variable.evaluationMacro = simpleValueEvalMacro; |
|
3584 |
|
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 } |
|
3595 |
|
3596 expanded.then(() => { |
|
3597 this.emit("variablesview-updated", view, aOptions); |
|
3598 }); |
|
3599 }, |
|
3600 |
|
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); |
|
3619 |
|
3620 let evalOptions = { |
|
3621 frame: this.SELECTED_FRAME, |
|
3622 bindObjectActor: aOptions.objectActor.actor, |
|
3623 }; |
|
3624 |
|
3625 this.requestEvaluation(string, evalOptions).then(onEval, onEval); |
|
3626 }, |
|
3627 |
|
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); |
|
3641 |
|
3642 let evalOptions = { |
|
3643 frame: this.SELECTED_FRAME, |
|
3644 bindObjectActor: aOptions.objectActor.actor, |
|
3645 }; |
|
3646 |
|
3647 this.requestEvaluation("delete _self" + aVar.symbolicName, evalOptions) |
|
3648 .then(onEval, onEval); |
|
3649 }, |
|
3650 |
|
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); |
|
3668 |
|
3669 let evalOptions = { |
|
3670 frame: this.SELECTED_FRAME, |
|
3671 bindObjectActor: aOptions.objectActor.actor, |
|
3672 }; |
|
3673 |
|
3674 let newSymbolicName = aVar.ownerView.symbolicName + '["' + aNewName + '"]'; |
|
3675 if (newSymbolicName == aVar.symbolicName) { |
|
3676 return; |
|
3677 } |
|
3678 |
|
3679 let code = "_self" + newSymbolicName + " = _self" + aVar.symbolicName + ";" + |
|
3680 "delete _self" + aVar.symbolicName; |
|
3681 |
|
3682 this.requestEvaluation(code, evalOptions).then(onEval, onEval); |
|
3683 }, |
|
3684 |
|
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); |
|
3704 |
|
3705 aCallback && aCallback(aResponse); |
|
3706 return; |
|
3707 } |
|
3708 |
|
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 } |
|
3721 |
|
3722 let helper = aResponse.helperResult || { type: null }; |
|
3723 let helperGrip = null; |
|
3724 if (helper.type == "inspectObject") { |
|
3725 helperGrip = helper.object; |
|
3726 } |
|
3727 |
|
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 } |
|
3734 |
|
3735 aCallback && aCallback(aResponse); |
|
3736 }, |
|
3737 |
|
3738 |
|
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 } |
|
3756 |
|
3757 hud.groupDepth = 0; |
|
3758 hud._outputQueue.forEach(hud._pruneItemFromQueue, hud); |
|
3759 hud._outputQueue = []; |
|
3760 hud._networkRequests = {}; |
|
3761 hud._repeatNodes = {}; |
|
3762 |
|
3763 if (aClearStorage) { |
|
3764 this.webConsoleClient.clearMessagesCache(); |
|
3765 } |
|
3766 |
|
3767 this.emit("messages-cleared"); |
|
3768 }, |
|
3769 |
|
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 }, |
|
3783 |
|
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; |
|
3792 |
|
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"; |
|
3796 |
|
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 }, |
|
3803 |
|
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 }, |
|
3821 |
|
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 }, |
|
3835 |
|
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 }, |
|
3846 |
|
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; |
|
3857 |
|
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; |
|
3880 |
|
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; |
|
3896 |
|
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 } |
|
3923 |
|
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; |
|
3937 |
|
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; |
|
3950 |
|
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; |
|
3965 |
|
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; |
|
3980 |
|
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; |
|
3997 |
|
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; |
|
4014 |
|
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; |
|
4022 |
|
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 }, |
|
4058 |
|
4059 /** |
|
4060 * The inputNode "focus" event handler. |
|
4061 * @private |
|
4062 */ |
|
4063 _focusEventHandler: function JST__focusEventHandler() |
|
4064 { |
|
4065 this._inputChanged = false; |
|
4066 }, |
|
4067 |
|
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 } |
|
4082 |
|
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]; |
|
4089 |
|
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 } |
|
4097 |
|
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 } |
|
4105 |
|
4106 let inputVal = this.history[++this.historyPlaceHolder]; |
|
4107 this.setInputValue(inputVal); |
|
4108 } |
|
4109 else { |
|
4110 throw new Error("Invalid argument 0"); |
|
4111 } |
|
4112 |
|
4113 return true; |
|
4114 }, |
|
4115 |
|
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 }, |
|
4126 |
|
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 } |
|
4142 |
|
4143 let multiline = /[\r\n]/.test(node.value); |
|
4144 return node.selectionStart == 0 ? true : |
|
4145 node.selectionStart == node.value.length && !multiline; |
|
4146 }, |
|
4147 |
|
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 } |
|
4163 |
|
4164 let multiline = /[\r\n]/.test(node.value); |
|
4165 return node.selectionStart == node.value.length ? true : |
|
4166 node.selectionStart == 0 && !multiline; |
|
4167 }, |
|
4168 |
|
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); |
|
4207 |
|
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 } |
|
4215 |
|
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 } |
|
4223 |
|
4224 // Update the completion results. |
|
4225 if (this.lastCompletion.value != inputValue || frameActor != this._lastFrameActorId) { |
|
4226 this._updateCompletionResult(aType, aCallback); |
|
4227 return false; |
|
4228 } |
|
4229 |
|
4230 let popup = this.autocompletePopup; |
|
4231 let accepted = false; |
|
4232 |
|
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 } |
|
4249 |
|
4250 aCallback && aCallback(this); |
|
4251 this.emit("autocomplete-updated"); |
|
4252 return accepted || popup.itemCount > 0; |
|
4253 }, |
|
4254 |
|
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 } |
|
4272 |
|
4273 let requestId = gSequenceId(); |
|
4274 let cursor = this.inputNode.selectionStart; |
|
4275 let input = this.inputNode.value.substring(0, cursor); |
|
4276 let cache = this._autocompleteCache; |
|
4277 |
|
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. |
|
4282 |
|
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 } |
|
4288 |
|
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 } |
|
4298 |
|
4299 let newList = cache.sort().filter(function(l) { |
|
4300 return l.startsWith(filterBy); |
|
4301 }); |
|
4302 |
|
4303 this.lastCompletion = { |
|
4304 requestId: null, |
|
4305 completionType: aType, |
|
4306 value: null, |
|
4307 }; |
|
4308 |
|
4309 let response = { matches: newList, matchProp: filterBy }; |
|
4310 this._receiveAutocompleteProperties(null, aCallback, response); |
|
4311 return; |
|
4312 } |
|
4313 |
|
4314 this._lastFrameActorId = frameActor; |
|
4315 |
|
4316 this.lastCompletion = { |
|
4317 requestId: requestId, |
|
4318 completionType: aType, |
|
4319 value: null, |
|
4320 }; |
|
4321 |
|
4322 let callback = this._receiveAutocompleteProperties.bind(this, requestId, |
|
4323 aCallback); |
|
4324 |
|
4325 this.webConsoleClient.autocomplete(input, cursor, callback, frameActor); |
|
4326 }, |
|
4327 |
|
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); |
|
4353 |
|
4354 if (aRequestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) { |
|
4355 this._autocompleteCache = aMessage.matches; |
|
4356 this._autocompleteQuery = inputUntilCursor; |
|
4357 } |
|
4358 |
|
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 } |
|
4367 |
|
4368 let items = matches.reverse().map(function(aMatch) { |
|
4369 return { preLabel: lastPart, label: aMatch }; |
|
4370 }); |
|
4371 |
|
4372 let popup = this.autocompletePopup; |
|
4373 popup.setItems(items); |
|
4374 |
|
4375 let completionType = this.lastCompletion.completionType; |
|
4376 this.lastCompletion = { |
|
4377 value: inputValue, |
|
4378 matchProp: lastPart, |
|
4379 }; |
|
4380 |
|
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 } |
|
4392 |
|
4393 if (items.length == 1) { |
|
4394 popup.selectedIndex = 0; |
|
4395 } |
|
4396 |
|
4397 this.onAutocompleteSelect(); |
|
4398 |
|
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 } |
|
4408 |
|
4409 aCallback && aCallback(this); |
|
4410 this.emit("autocomplete-updated"); |
|
4411 }, |
|
4412 |
|
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 } |
|
4419 |
|
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 }, |
|
4430 |
|
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 }, |
|
4445 |
|
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; |
|
4456 |
|
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 } |
|
4468 |
|
4469 this.clearCompletion(); |
|
4470 |
|
4471 return updated; |
|
4472 }, |
|
4473 |
|
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 }, |
|
4486 |
|
4487 |
|
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 } |
|
4498 |
|
4499 if (this.sidebar) { |
|
4500 this.sidebar.hide(); |
|
4501 this.sidebar.destroy(); |
|
4502 this.sidebar = null; |
|
4503 } |
|
4504 |
|
4505 this.emit("sidebar-closed"); |
|
4506 }, |
|
4507 |
|
4508 /** |
|
4509 * Destroy the JSTerm object. Call this method to avoid memory leaks. |
|
4510 */ |
|
4511 destroy: function JST_destroy() |
|
4512 { |
|
4513 this._sidebarDestroy(); |
|
4514 |
|
4515 this.clearCompletion(); |
|
4516 this.clearOutput(); |
|
4517 |
|
4518 this.autocompletePopup.destroy(); |
|
4519 this.autocompletePopup = null; |
|
4520 |
|
4521 let popup = this.hud.owner.chromeWindow.document |
|
4522 .getElementById("webConsole_autocompletePopup"); |
|
4523 if (popup) { |
|
4524 popup.parentNode.removeChild(popup); |
|
4525 } |
|
4526 |
|
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); |
|
4532 |
|
4533 this.hud = null; |
|
4534 }, |
|
4535 }; |
|
4536 |
|
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 }, |
|
4552 |
|
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 }, |
|
4569 |
|
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; |
|
4582 |
|
4583 if (/^(?:CSS|Layout)\b/.test(category)) { |
|
4584 return CATEGORY_CSS; |
|
4585 } |
|
4586 |
|
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; |
|
4596 |
|
4597 default: |
|
4598 return CATEGORY_JS; |
|
4599 } |
|
4600 }, |
|
4601 |
|
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; |
|
4614 |
|
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) { } |
|
4621 |
|
4622 return logLimit; |
|
4623 }, |
|
4624 }; |
|
4625 |
|
4626 /////////////////////////////////////////////////////////////////////////////// |
|
4627 // CommandController |
|
4628 /////////////////////////////////////////////////////////////////////////////// |
|
4629 |
|
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 } |
|
4638 |
|
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 }, |
|
4647 |
|
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 }, |
|
4655 |
|
4656 copyURL: function CommandController_copyURL() |
|
4657 { |
|
4658 this.owner.copySelectedItems({ linkOnly: true, contextmenu: true }); |
|
4659 }, |
|
4660 |
|
4661 supportsCommand: function CommandController_supportsCommand(aCommand) |
|
4662 { |
|
4663 if (!this.owner || !this.owner.output) { |
|
4664 return false; |
|
4665 } |
|
4666 return this.isCommandEnabled(aCommand); |
|
4667 }, |
|
4668 |
|
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 }, |
|
4691 |
|
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 }; |
|
4725 |
|
4726 /////////////////////////////////////////////////////////////////////////////// |
|
4727 // Web Console connection proxy |
|
4728 /////////////////////////////////////////////////////////////////////////////// |
|
4729 |
|
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; |
|
4744 |
|
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 } |
|
4758 |
|
4759 WebConsoleConnectionProxy.prototype = { |
|
4760 /** |
|
4761 * The owning Web Console instance. |
|
4762 * |
|
4763 * @see WebConsoleFrame |
|
4764 * @type object |
|
4765 */ |
|
4766 owner: null, |
|
4767 |
|
4768 /** |
|
4769 * The target that the console connects to. |
|
4770 * @type RemoteTarget |
|
4771 */ |
|
4772 target: null, |
|
4773 |
|
4774 /** |
|
4775 * The DebuggerClient object. |
|
4776 * |
|
4777 * @see DebuggerClient |
|
4778 * @type object |
|
4779 */ |
|
4780 client: null, |
|
4781 |
|
4782 /** |
|
4783 * The WebConsoleClient object. |
|
4784 * |
|
4785 * @see WebConsoleClient |
|
4786 * @type object |
|
4787 */ |
|
4788 webConsoleClient: null, |
|
4789 |
|
4790 /** |
|
4791 * Tells if the connection is established. |
|
4792 * @type boolean |
|
4793 */ |
|
4794 connected: false, |
|
4795 |
|
4796 /** |
|
4797 * Timer used for the connection. |
|
4798 * @private |
|
4799 * @type object |
|
4800 */ |
|
4801 _connectTimer: null, |
|
4802 |
|
4803 _connectDefer: null, |
|
4804 _disconnecter: null, |
|
4805 |
|
4806 /** |
|
4807 * The WebConsoleActor ID. |
|
4808 * |
|
4809 * @private |
|
4810 * @type string |
|
4811 */ |
|
4812 _consoleActor: null, |
|
4813 |
|
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, |
|
4821 |
|
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 } |
|
4834 |
|
4835 this._connectDefer = promise.defer(); |
|
4836 |
|
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); |
|
4841 |
|
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)); |
|
4849 |
|
4850 let client = this.client = this.target.client; |
|
4851 |
|
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); |
|
4862 |
|
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(); |
|
4869 |
|
4870 return connPromise; |
|
4871 }, |
|
4872 |
|
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 }; |
|
4883 |
|
4884 this._connectDefer.reject(error); |
|
4885 }, |
|
4886 |
|
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 }, |
|
4898 |
|
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 } |
|
4917 |
|
4918 this.webConsoleClient = aWebConsoleClient; |
|
4919 |
|
4920 this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI; |
|
4921 |
|
4922 let msgs = ["PageError", "ConsoleAPI"]; |
|
4923 this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages); |
|
4924 |
|
4925 this.owner._updateReflowActivityListener(); |
|
4926 }, |
|
4927 |
|
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 } |
|
4943 |
|
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 } |
|
4949 |
|
4950 this.owner.displayCachedMessages(aResponse.messages); |
|
4951 |
|
4952 if (!this._hasNativeConsoleAPI) { |
|
4953 this.owner.logWarningAboutReplacedAPI(); |
|
4954 } |
|
4955 |
|
4956 this.connected = true; |
|
4957 this._connectDefer.resolve(this); |
|
4958 }, |
|
4959 |
|
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 }, |
|
4976 |
|
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 }, |
|
4993 |
|
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 }, |
|
5010 |
|
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 }, |
|
5027 |
|
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 }, |
|
5045 |
|
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 }, |
|
5062 |
|
5063 _onReflowActivity: function WCCP__onReflowActivity(aType, aPacket) |
|
5064 { |
|
5065 if (this.owner && aPacket.from == this._consoleActor) { |
|
5066 this.owner.handleReflowActivity(aPacket); |
|
5067 } |
|
5068 }, |
|
5069 |
|
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 }, |
|
5087 |
|
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 } |
|
5103 |
|
5104 this.owner.handleTabNavigated(aEvent, aPacket); |
|
5105 }, |
|
5106 |
|
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 }, |
|
5119 |
|
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 } |
|
5131 |
|
5132 this._disconnecter = promise.defer(); |
|
5133 |
|
5134 if (!this.client) { |
|
5135 this._disconnecter.resolve(null); |
|
5136 return this._disconnecter.promise; |
|
5137 } |
|
5138 |
|
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); |
|
5149 |
|
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); |
|
5156 |
|
5157 return this._disconnecter.promise; |
|
5158 }, |
|
5159 }; |
|
5160 |
|
5161 function gSequenceId() |
|
5162 { |
|
5163 return gSequenceId.n++; |
|
5164 } |
|
5165 gSequenceId.n = 0; |
|
5166 |
|
5167 /////////////////////////////////////////////////////////////////////////////// |
|
5168 // Context Menu |
|
5169 /////////////////////////////////////////////////////////////////////////////// |
|
5170 |
|
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 } |
|
5185 |
|
5186 ConsoleContextMenu.prototype = { |
|
5187 lastClickedMessage: null, |
|
5188 |
|
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 }, |
|
5199 |
|
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 } |
|
5222 |
|
5223 metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single"; |
|
5224 |
|
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 } |
|
5242 |
|
5243 return metadata; |
|
5244 }, |
|
5245 |
|
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 } |
|
5260 |
|
5261 let selection = aMenuItem.getAttribute("selection"); |
|
5262 if (!selection) { |
|
5263 return false; |
|
5264 } |
|
5265 |
|
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 } |
|
5275 |
|
5276 return shouldHide; |
|
5277 }, |
|
5278 |
|
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 }; |
|
5290 |