browser/devtools/netmonitor/netmonitor-view.js

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:ebe00eec7105
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 "use strict";
7
8 const HTML_NS = "http://www.w3.org/1999/xhtml";
9 const EPSILON = 0.001;
10 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400; // 100 KB in bytes
11 const RESIZE_REFRESH_RATE = 50; // ms
12 const REQUESTS_REFRESH_RATE = 50; // ms
13 const REQUESTS_HEADERS_SAFE_BOUNDS = 30; // px
14 const REQUESTS_TOOLTIP_POSITION = "topcenter bottomleft";
15 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px
16 const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // px
17 const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
18 const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
19 const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
20 const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
21 const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
22 const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
23 const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
24 const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
25 const DEFAULT_HTTP_VERSION = "HTTP/1.1";
26 const REQUEST_TIME_DECIMALS = 2;
27 const HEADERS_SIZE_DECIMALS = 3;
28 const CONTENT_SIZE_DECIMALS = 2;
29 const CONTENT_MIME_TYPE_ABBREVIATIONS = {
30 "ecmascript": "js",
31 "javascript": "js",
32 "x-javascript": "js"
33 };
34 const CONTENT_MIME_TYPE_MAPPINGS = {
35 "/ecmascript": Editor.modes.js,
36 "/javascript": Editor.modes.js,
37 "/x-javascript": Editor.modes.js,
38 "/html": Editor.modes.html,
39 "/xhtml": Editor.modes.html,
40 "/xml": Editor.modes.html,
41 "/atom": Editor.modes.html,
42 "/soap": Editor.modes.html,
43 "/rdf": Editor.modes.css,
44 "/rss": Editor.modes.css,
45 "/css": Editor.modes.css
46 };
47 const DEFAULT_EDITOR_CONFIG = {
48 mode: Editor.modes.text,
49 readOnly: true,
50 lineNumbers: true
51 };
52 const GENERIC_VARIABLES_VIEW_SETTINGS = {
53 lazyEmpty: true,
54 lazyEmptyDelay: 10, // ms
55 searchEnabled: true,
56 editableValueTooltip: "",
57 editableNameTooltip: "",
58 preventDisableOnChange: true,
59 preventDescriptorModifiers: true,
60 eval: () => {}
61 };
62 const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px
63
64 /**
65 * Object defining the network monitor view components.
66 */
67 let NetMonitorView = {
68 /**
69 * Initializes the network monitor view.
70 */
71 initialize: function() {
72 this._initializePanes();
73
74 this.Toolbar.initialize();
75 this.RequestsMenu.initialize();
76 this.NetworkDetails.initialize();
77 this.CustomRequest.initialize();
78 },
79
80 /**
81 * Destroys the network monitor view.
82 */
83 destroy: function() {
84 this.Toolbar.destroy();
85 this.RequestsMenu.destroy();
86 this.NetworkDetails.destroy();
87 this.CustomRequest.destroy();
88
89 this._destroyPanes();
90 },
91
92 /**
93 * Initializes the UI for all the displayed panes.
94 */
95 _initializePanes: function() {
96 dumpn("Initializing the NetMonitorView panes");
97
98 this._body = $("#body");
99 this._detailsPane = $("#details-pane");
100 this._detailsPaneToggleButton = $("#details-pane-toggle");
101
102 this._collapsePaneString = L10N.getStr("collapseDetailsPane");
103 this._expandPaneString = L10N.getStr("expandDetailsPane");
104
105 this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
106 this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight);
107 this.toggleDetailsPane({ visible: false });
108
109 // Disable the performance statistics mode.
110 if (!Prefs.statistics) {
111 $("#request-menu-context-perf").hidden = true;
112 $("#notice-perf-message").hidden = true;
113 $("#requests-menu-network-summary-button").hidden = true;
114 $("#requests-menu-network-summary-label").hidden = true;
115 }
116 },
117
118 /**
119 * Destroys the UI for all the displayed panes.
120 */
121 _destroyPanes: function() {
122 dumpn("Destroying the NetMonitorView panes");
123
124 Prefs.networkDetailsWidth = this._detailsPane.getAttribute("width");
125 Prefs.networkDetailsHeight = this._detailsPane.getAttribute("height");
126
127 this._detailsPane = null;
128 this._detailsPaneToggleButton = null;
129 },
130
131 /**
132 * Gets the visibility state of the network details pane.
133 * @return boolean
134 */
135 get detailsPaneHidden() {
136 return this._detailsPane.hasAttribute("pane-collapsed");
137 },
138
139 /**
140 * Sets the network details pane hidden or visible.
141 *
142 * @param object aFlags
143 * An object containing some of the following properties:
144 * - visible: true if the pane should be shown, false to hide
145 * - animated: true to display an animation on toggle
146 * - delayed: true to wait a few cycles before toggle
147 * - callback: a function to invoke when the toggle finishes
148 * @param number aTabIndex [optional]
149 * The index of the intended selected tab in the details pane.
150 */
151 toggleDetailsPane: function(aFlags, aTabIndex) {
152 let pane = this._detailsPane;
153 let button = this._detailsPaneToggleButton;
154
155 ViewHelpers.togglePane(aFlags, pane);
156
157 if (aFlags.visible) {
158 this._body.removeAttribute("pane-collapsed");
159 button.removeAttribute("pane-collapsed");
160 button.setAttribute("tooltiptext", this._collapsePaneString);
161 } else {
162 this._body.setAttribute("pane-collapsed", "");
163 button.setAttribute("pane-collapsed", "");
164 button.setAttribute("tooltiptext", this._expandPaneString);
165 }
166
167 if (aTabIndex !== undefined) {
168 $("#event-details-pane").selectedIndex = aTabIndex;
169 }
170 },
171
172 /**
173 * Gets the current mode for this tool.
174 * @return string (e.g, "network-inspector-view" or "network-statistics-view")
175 */
176 get currentFrontendMode() {
177 return this._body.selectedPanel.id;
178 },
179
180 /**
181 * Toggles between the frontend view modes ("Inspector" vs. "Statistics").
182 */
183 toggleFrontendMode: function() {
184 if (this.currentFrontendMode != "network-inspector-view") {
185 this.showNetworkInspectorView();
186 } else {
187 this.showNetworkStatisticsView();
188 }
189 },
190
191 /**
192 * Switches to the "Inspector" frontend view mode.
193 */
194 showNetworkInspectorView: function() {
195 this._body.selectedPanel = $("#network-inspector-view");
196 this.RequestsMenu._flushWaterfallViews(true);
197 },
198
199 /**
200 * Switches to the "Statistics" frontend view mode.
201 */
202 showNetworkStatisticsView: function() {
203 this._body.selectedPanel = $("#network-statistics-view");
204
205 let controller = NetMonitorController;
206 let requestsView = this.RequestsMenu;
207 let statisticsView = this.PerformanceStatistics;
208
209 Task.spawn(function*() {
210 statisticsView.displayPlaceholderCharts();
211 yield controller.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
212
213 try {
214 // • The response headers and status code are required for determining
215 // whether a response is "fresh" (cacheable).
216 // • The response content size and request total time are necessary for
217 // populating the statistics view.
218 // • The response mime type is used for categorization.
219 yield whenDataAvailable(requestsView.attachments, [
220 "responseHeaders", "status", "contentSize", "mimeType", "totalTime"
221 ]);
222 } catch (ex) {
223 // Timed out while waiting for data. Continue with what we have.
224 DevToolsUtils.reportException("showNetworkStatisticsView", ex);
225 }
226
227 statisticsView.createPrimedCacheChart(requestsView.items);
228 statisticsView.createEmptyCacheChart(requestsView.items);
229 });
230 },
231
232 /**
233 * Lazily initializes and returns a promise for a Editor instance.
234 *
235 * @param string aId
236 * The id of the editor placeholder node.
237 * @return object
238 * A promise that is resolved when the editor is available.
239 */
240 editor: function(aId) {
241 dumpn("Getting a NetMonitorView editor: " + aId);
242
243 if (this._editorPromises.has(aId)) {
244 return this._editorPromises.get(aId);
245 }
246
247 let deferred = promise.defer();
248 this._editorPromises.set(aId, deferred.promise);
249
250 // Initialize the source editor and store the newly created instance
251 // in the ether of a resolved promise's value.
252 let editor = new Editor(DEFAULT_EDITOR_CONFIG);
253 editor.appendTo($(aId)).then(() => deferred.resolve(editor));
254
255 return deferred.promise;
256 },
257
258 _body: null,
259 _detailsPane: null,
260 _detailsPaneToggleButton: null,
261 _collapsePaneString: "",
262 _expandPaneString: "",
263 _editorPromises: new Map()
264 };
265
266 /**
267 * Functions handling the toolbar view: expand/collapse button etc.
268 */
269 function ToolbarView() {
270 dumpn("ToolbarView was instantiated");
271
272 this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this);
273 }
274
275 ToolbarView.prototype = {
276 /**
277 * Initialization function, called when the debugger is started.
278 */
279 initialize: function() {
280 dumpn("Initializing the ToolbarView");
281
282 this._detailsPaneToggleButton = $("#details-pane-toggle");
283 this._detailsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false);
284 },
285
286 /**
287 * Destruction function, called when the debugger is closed.
288 */
289 destroy: function() {
290 dumpn("Destroying the ToolbarView");
291
292 this._detailsPaneToggleButton.removeEventListener("mousedown", this._onTogglePanesPressed, false);
293 },
294
295 /**
296 * Listener handling the toggle button click event.
297 */
298 _onTogglePanesPressed: function() {
299 let requestsMenu = NetMonitorView.RequestsMenu;
300 let selectedIndex = requestsMenu.selectedIndex;
301
302 // Make sure there's a selection if the button is pressed, to avoid
303 // showing an empty network details pane.
304 if (selectedIndex == -1 && requestsMenu.itemCount) {
305 requestsMenu.selectedIndex = 0;
306 } else {
307 requestsMenu.selectedIndex = -1;
308 }
309 },
310
311 _detailsPaneToggleButton: null
312 };
313
314 /**
315 * Functions handling the requests menu (containing details about each request,
316 * like status, method, file, domain, as well as a waterfall representing
317 * timing imformation).
318 */
319 function RequestsMenuView() {
320 dumpn("RequestsMenuView was instantiated");
321
322 this._flushRequests = this._flushRequests.bind(this);
323 this._onHover = this._onHover.bind(this);
324 this._onSelect = this._onSelect.bind(this);
325 this._onSwap = this._onSwap.bind(this);
326 this._onResize = this._onResize.bind(this);
327 this._byFile = this._byFile.bind(this);
328 this._byDomain = this._byDomain.bind(this);
329 this._byType = this._byType.bind(this);
330 }
331
332 RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
333 /**
334 * Initialization function, called when the network monitor is started.
335 */
336 initialize: function() {
337 dumpn("Initializing the RequestsMenuView");
338
339 this.widget = new SideMenuWidget($("#requests-menu-contents"));
340 this._splitter = $("#network-inspector-view-splitter");
341 this._summary = $("#requests-menu-network-summary-label");
342 this._summary.setAttribute("value", L10N.getStr("networkMenu.empty"));
343
344 Prefs.filters.forEach(type => this.filterOn(type));
345 this.sortContents(this._byTiming);
346
347 this.allowFocusOnRightClick = true;
348 this.maintainSelectionVisible = true;
349 this.widget.autoscrollWithAppendedItems = true;
350
351 this.widget.addEventListener("select", this._onSelect, false);
352 this.widget.addEventListener("swap", this._onSwap, false);
353 this._splitter.addEventListener("mousemove", this._onResize, false);
354 window.addEventListener("resize", this._onResize, false);
355
356 this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this));
357 this.requestsMenuFilterEvent = getKeyWithEvent(this.filterOn.bind(this));
358 this.reqeustsMenuClearEvent = this.clear.bind(this);
359 this._onContextShowing = this._onContextShowing.bind(this);
360 this._onContextNewTabCommand = this.openRequestInTab.bind(this);
361 this._onContextCopyUrlCommand = this.copyUrl.bind(this);
362 this._onContextCopyImageAsDataUriCommand = this.copyImageAsDataUri.bind(this);
363 this._onContextResendCommand = this.cloneSelectedRequest.bind(this);
364 this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode();
365
366 this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
367 this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
368 this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this);
369
370 $("#toolbar-labels").addEventListener("click", this.requestsMenuSortEvent, false);
371 $("#requests-menu-footer").addEventListener("click", this.requestsMenuFilterEvent, false);
372 $("#requests-menu-clear-button").addEventListener("click", this.reqeustsMenuClearEvent, false);
373 $("#network-request-popup").addEventListener("popupshowing", this._onContextShowing, false);
374 $("#request-menu-context-newtab").addEventListener("command", this._onContextNewTabCommand, false);
375 $("#request-menu-context-copy-url").addEventListener("command", this._onContextCopyUrlCommand, false);
376 $("#request-menu-context-copy-image-as-data-uri").addEventListener("command", this._onContextCopyImageAsDataUriCommand, false);
377
378 window.once("connected", this._onConnect.bind(this));
379 },
380
381 _onConnect: function() {
382 if (NetMonitorController.supportsCustomRequest) {
383 $("#request-menu-context-resend").addEventListener("command", this._onContextResendCommand, false);
384 $("#custom-request-send-button").addEventListener("click", this.sendCustomRequestEvent, false);
385 $("#custom-request-close-button").addEventListener("click", this.closeCustomRequestEvent, false);
386 $("#headers-summary-resend").addEventListener("click", this.cloneSelectedRequestEvent, false);
387 } else {
388 $("#request-menu-context-resend").hidden = true;
389 $("#headers-summary-resend").hidden = true;
390 }
391
392 if (NetMonitorController.supportsPerfStats) {
393 $("#request-menu-context-perf").addEventListener("command", this._onContextPerfCommand, false);
394 $("#requests-menu-perf-notice-button").addEventListener("command", this._onContextPerfCommand, false);
395 $("#requests-menu-network-summary-button").addEventListener("command", this._onContextPerfCommand, false);
396 $("#requests-menu-network-summary-label").addEventListener("click", this._onContextPerfCommand, false);
397 $("#network-statistics-back-button").addEventListener("command", this._onContextPerfCommand, false);
398 } else {
399 $("#notice-perf-message").hidden = true;
400 $("#request-menu-context-perf").hidden = true;
401 $("#requests-menu-network-summary-button").hidden = true;
402 $("#requests-menu-network-summary-label").hidden = true;
403 }
404 },
405
406 /**
407 * Destruction function, called when the network monitor is closed.
408 */
409 destroy: function() {
410 dumpn("Destroying the SourcesView");
411
412 Prefs.filters = this._activeFilters;
413
414 this.widget.removeEventListener("select", this._onSelect, false);
415 this.widget.removeEventListener("swap", this._onSwap, false);
416 this._splitter.removeEventListener("mousemove", this._onResize, false);
417 window.removeEventListener("resize", this._onResize, false);
418
419 $("#toolbar-labels").removeEventListener("click", this.requestsMenuSortEvent, false);
420 $("#requests-menu-footer").removeEventListener("click", this.requestsMenuFilterEvent, false);
421 $("#requests-menu-clear-button").removeEventListener("click", this.reqeustsMenuClearEvent, false);
422 $("#network-request-popup").removeEventListener("popupshowing", this._onContextShowing, false);
423 $("#request-menu-context-newtab").removeEventListener("command", this._onContextNewTabCommand, false);
424 $("#request-menu-context-copy-url").removeEventListener("command", this._onContextCopyUrlCommand, false);
425 $("#request-menu-context-copy-image-as-data-uri").removeEventListener("command", this._onContextCopyImageAsDataUriCommand, false);
426 $("#request-menu-context-resend").removeEventListener("command", this._onContextResendCommand, false);
427 $("#request-menu-context-perf").removeEventListener("command", this._onContextPerfCommand, false);
428
429 $("#requests-menu-perf-notice-button").removeEventListener("command", this._onContextPerfCommand, false);
430 $("#requests-menu-network-summary-button").removeEventListener("command", this._onContextPerfCommand, false);
431 $("#requests-menu-network-summary-label").removeEventListener("click", this._onContextPerfCommand, false);
432 $("#network-statistics-back-button").removeEventListener("command", this._onContextPerfCommand, false);
433
434 $("#custom-request-send-button").removeEventListener("click", this.sendCustomRequestEvent, false);
435 $("#custom-request-close-button").removeEventListener("click", this.closeCustomRequestEvent, false);
436 $("#headers-summary-resend").removeEventListener("click", this.cloneSelectedRequestEvent, false);
437 },
438
439 /**
440 * Resets this container (removes all the networking information).
441 */
442 reset: function() {
443 this.empty();
444 this._firstRequestStartedMillis = -1;
445 this._lastRequestEndedMillis = -1;
446 },
447
448 /**
449 * Specifies if this view may be updated lazily.
450 */
451 lazyUpdate: true,
452
453 /**
454 * Adds a network request to this container.
455 *
456 * @param string aId
457 * An identifier coming from the network monitor controller.
458 * @param string aStartedDateTime
459 * A string representation of when the request was started, which
460 * can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
461 * @param string aMethod
462 * Specifies the request method (e.g. "GET", "POST", etc.)
463 * @param string aUrl
464 * Specifies the request's url.
465 * @param boolean aIsXHR
466 * True if this request was initiated via XHR.
467 */
468 addRequest: function(aId, aStartedDateTime, aMethod, aUrl, aIsXHR) {
469 // Convert the received date/time string to a unix timestamp.
470 let unixTime = Date.parse(aStartedDateTime);
471
472 // Create the element node for the network request item.
473 let menuView = this._createMenuView(aMethod, aUrl);
474
475 // Remember the first and last event boundaries.
476 this._registerFirstRequestStart(unixTime);
477 this._registerLastRequestEnd(unixTime);
478
479 // Append a network request item to this container.
480 let requestItem = this.push([menuView, aId], {
481 attachment: {
482 startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
483 startedMillis: unixTime,
484 method: aMethod,
485 url: aUrl,
486 isXHR: aIsXHR
487 }
488 });
489
490 // Create a tooltip for the newly appended network request item.
491 let requestTooltip = requestItem.attachment.tooltip = new Tooltip(document, {
492 closeOnEvents: [{
493 emitter: $("#requests-menu-contents"),
494 event: "scroll",
495 useCapture: true
496 }]
497 });
498
499 $("#details-pane-toggle").disabled = false;
500 $("#requests-menu-empty-notice").hidden = true;
501
502 this.refreshSummary();
503 this.refreshZebra();
504 this.refreshTooltip(requestItem);
505
506 if (aId == this._preferredItemId) {
507 this.selectedItem = requestItem;
508 }
509 },
510
511 /**
512 * Opens selected item in a new tab.
513 */
514 openRequestInTab: function() {
515 let win = Services.wm.getMostRecentWindow("navigator:browser");
516 let selected = this.selectedItem.attachment;
517 win.openUILinkIn(selected.url, "tab", { relatedToCurrent: true });
518 },
519
520 /**
521 * Copy the request url from the currently selected item.
522 */
523 copyUrl: function() {
524 let selected = this.selectedItem.attachment;
525 clipboardHelper.copyString(selected.url, document);
526 },
527
528 /**
529 * Copy a cURL command from the currently selected item.
530 */
531 copyAsCurl: function() {
532 let selected = this.selectedItem.attachment;
533
534 Task.spawn(function*() {
535 // Create a sanitized object for the Curl command generator.
536 let data = {
537 url: selected.url,
538 method: selected.method,
539 headers: [],
540 httpVersion: selected.httpVersion,
541 postDataText: null
542 };
543
544 // Fetch header values.
545 for (let { name, value } of selected.requestHeaders.headers) {
546 let text = yield gNetwork.getString(value);
547 data.headers.push({ name: name, value: text });
548 }
549
550 // Fetch the request payload.
551 if (selected.requestPostData) {
552 let postData = selected.requestPostData.postData.text;
553 data.postDataText = yield gNetwork.getString(postData);
554 }
555
556 clipboardHelper.copyString(Curl.generateCommand(data), document);
557 });
558 },
559
560 /**
561 * Copy image as data uri.
562 */
563 copyImageAsDataUri: function() {
564 let selected = this.selectedItem.attachment;
565 let { mimeType, text, encoding } = selected.responseContent.content;
566
567 gNetwork.getString(text).then(aString => {
568 let data = "data:" + mimeType + ";" + encoding + "," + aString;
569 clipboardHelper.copyString(data, document);
570 });
571 },
572
573 /**
574 * Create a new custom request form populated with the data from
575 * the currently selected request.
576 */
577 cloneSelectedRequest: function() {
578 let selected = this.selectedItem.attachment;
579
580 // Create the element node for the network request item.
581 let menuView = this._createMenuView(selected.method, selected.url);
582
583 // Append a network request item to this container.
584 let newItem = this.push([menuView], {
585 attachment: Object.create(selected, {
586 isCustom: { value: true }
587 })
588 });
589
590 // Immediately switch to new request pane.
591 this.selectedItem = newItem;
592 },
593
594 /**
595 * Send a new HTTP request using the data in the custom request form.
596 */
597 sendCustomRequest: function() {
598 let selected = this.selectedItem.attachment;
599
600 let data = {
601 url: selected.url,
602 method: selected.method,
603 httpVersion: selected.httpVersion,
604 };
605 if (selected.requestHeaders) {
606 data.headers = selected.requestHeaders.headers;
607 }
608 if (selected.requestPostData) {
609 data.body = selected.requestPostData.postData.text;
610 }
611
612 NetMonitorController.webConsoleClient.sendHTTPRequest(data, aResponse => {
613 let id = aResponse.eventActor.actor;
614 this._preferredItemId = id;
615 });
616
617 this.closeCustomRequest();
618 },
619
620 /**
621 * Remove the currently selected custom request.
622 */
623 closeCustomRequest: function() {
624 this.remove(this.selectedItem);
625 NetMonitorView.Sidebar.toggle(false);
626 },
627
628 /**
629 * Filters all network requests in this container by a specified type.
630 *
631 * @param string aType
632 * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
633 * "flash" or "other".
634 */
635 filterOn: function(aType = "all") {
636 if (aType === "all") {
637 // The filter "all" is special as it doesn't toggle.
638 // - If some filters are selected and 'all' is clicked, the previously
639 // selected filters will be disabled and 'all' is the only active one.
640 // - If 'all' is already selected, do nothing.
641 if (this._activeFilters.indexOf("all") !== -1) {
642 return;
643 }
644
645 // Uncheck all other filters and select 'all'. Must create a copy as
646 // _disableFilter removes the filters from the list while it's being
647 // iterated. 'all' will be enabled automatically by _disableFilter once
648 // the last filter is disabled.
649 this._activeFilters.slice().forEach(this._disableFilter, this);
650 }
651 else if (this._activeFilters.indexOf(aType) === -1) {
652 this._enableFilter(aType);
653 }
654 else {
655 this._disableFilter(aType);
656 }
657
658 this.filterContents(this._filterPredicate);
659 this.refreshSummary();
660 this.refreshZebra();
661 },
662
663 /**
664 * Same as `filterOn`, except that it only allows a single type exclusively.
665 *
666 * @param string aType
667 * @see RequestsMenuView.prototype.fitlerOn
668 */
669 filterOnlyOn: function(aType = "all") {
670 this._activeFilters.slice().forEach(this._disableFilter, this);
671 this.filterOn(aType);
672 },
673
674 /**
675 * Disables the given filter, its button and toggles 'all' on if the filter to
676 * be disabled is the last one active.
677 *
678 * @param string aType
679 * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
680 * "flash" or "other".
681 */
682 _disableFilter: function (aType) {
683 // Remove the filter from list of active filters.
684 this._activeFilters.splice(this._activeFilters.indexOf(aType), 1);
685
686 // Remove the checked status from the filter.
687 let target = $("#requests-menu-filter-" + aType + "-button");
688 target.removeAttribute("checked");
689
690 // Check if the filter disabled was the last one. If so, toggle all on.
691 if (this._activeFilters.length === 0) {
692 this._enableFilter("all");
693 }
694 },
695
696 /**
697 * Enables the given filter, its button and toggles 'all' off if the filter to
698 * be enabled is the first one active.
699 *
700 * @param string aType
701 * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
702 * "flash" or "other".
703 */
704 _enableFilter: function (aType) {
705 // Make sure this is a valid filter type.
706 if (Object.keys(this._allFilterPredicates).indexOf(aType) == -1) {
707 return;
708 }
709
710 // Add the filter to the list of active filters.
711 this._activeFilters.push(aType);
712
713 // Add the checked status to the filter button.
714 let target = $("#requests-menu-filter-" + aType + "-button");
715 target.setAttribute("checked", true);
716
717 // Check if 'all' was selected before. If so, disable it.
718 if (aType !== "all" && this._activeFilters.indexOf("all") !== -1) {
719 this._disableFilter("all");
720 }
721 },
722
723 /**
724 * Returns a predicate that can be used to test if a request matches any of
725 * the active filters.
726 */
727 get _filterPredicate() {
728 let filterPredicates = this._allFilterPredicates;
729
730 if (this._activeFilters.length === 1) {
731 // The simplest case: only one filter active.
732 return filterPredicates[this._activeFilters[0]].bind(this);
733 } else {
734 // Multiple filters active.
735 return requestItem => {
736 return this._activeFilters.some(filterName => {
737 return filterPredicates[filterName].call(this, requestItem);
738 });
739 };
740 }
741 },
742
743 /**
744 * Returns an object with all the filter predicates as [key: function] pairs.
745 */
746 get _allFilterPredicates() ({
747 all: () => true,
748 html: this.isHtml,
749 css: this.isCss,
750 js: this.isJs,
751 xhr: this.isXHR,
752 fonts: this.isFont,
753 images: this.isImage,
754 media: this.isMedia,
755 flash: this.isFlash,
756 other: this.isOther
757 }),
758
759 /**
760 * Sorts all network requests in this container by a specified detail.
761 *
762 * @param string aType
763 * Either "status", "method", "file", "domain", "type", "size" or
764 * "waterfall".
765 */
766 sortBy: function(aType = "waterfall") {
767 let target = $("#requests-menu-" + aType + "-button");
768 let headers = document.querySelectorAll(".requests-menu-header-button");
769
770 for (let header of headers) {
771 if (header != target) {
772 header.removeAttribute("sorted");
773 header.removeAttribute("tooltiptext");
774 }
775 }
776
777 let direction = "";
778 if (target) {
779 if (target.getAttribute("sorted") == "ascending") {
780 target.setAttribute("sorted", direction = "descending");
781 target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedDesc"));
782 } else {
783 target.setAttribute("sorted", direction = "ascending");
784 target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedAsc"));
785 }
786 }
787
788 // Sort by whatever was requested.
789 switch (aType) {
790 case "status":
791 if (direction == "ascending") {
792 this.sortContents(this._byStatus);
793 } else {
794 this.sortContents((a, b) => !this._byStatus(a, b));
795 }
796 break;
797 case "method":
798 if (direction == "ascending") {
799 this.sortContents(this._byMethod);
800 } else {
801 this.sortContents((a, b) => !this._byMethod(a, b));
802 }
803 break;
804 case "file":
805 if (direction == "ascending") {
806 this.sortContents(this._byFile);
807 } else {
808 this.sortContents((a, b) => !this._byFile(a, b));
809 }
810 break;
811 case "domain":
812 if (direction == "ascending") {
813 this.sortContents(this._byDomain);
814 } else {
815 this.sortContents((a, b) => !this._byDomain(a, b));
816 }
817 break;
818 case "type":
819 if (direction == "ascending") {
820 this.sortContents(this._byType);
821 } else {
822 this.sortContents((a, b) => !this._byType(a, b));
823 }
824 break;
825 case "size":
826 if (direction == "ascending") {
827 this.sortContents(this._bySize);
828 } else {
829 this.sortContents((a, b) => !this._bySize(a, b));
830 }
831 break;
832 case "waterfall":
833 if (direction == "ascending") {
834 this.sortContents(this._byTiming);
835 } else {
836 this.sortContents((a, b) => !this._byTiming(a, b));
837 }
838 break;
839 }
840
841 this.refreshSummary();
842 this.refreshZebra();
843 },
844
845 /**
846 * Removes all network requests and closes the sidebar if open.
847 */
848 clear: function() {
849 NetMonitorView.Sidebar.toggle(false);
850 $("#details-pane-toggle").disabled = true;
851
852 this.empty();
853 this.refreshSummary();
854 },
855
856 /**
857 * Predicates used when filtering items.
858 *
859 * @param object aItem
860 * The filtered item.
861 * @return boolean
862 * True if the item should be visible, false otherwise.
863 */
864 isHtml: function({ attachment: { mimeType } })
865 mimeType && mimeType.contains("/html"),
866
867 isCss: function({ attachment: { mimeType } })
868 mimeType && mimeType.contains("/css"),
869
870 isJs: function({ attachment: { mimeType } })
871 mimeType && (
872 mimeType.contains("/ecmascript") ||
873 mimeType.contains("/javascript") ||
874 mimeType.contains("/x-javascript")),
875
876 isXHR: function({ attachment: { isXHR } })
877 isXHR,
878
879 isFont: function({ attachment: { url, mimeType } }) // Fonts are a mess.
880 (mimeType && (
881 mimeType.contains("font/") ||
882 mimeType.contains("/font"))) ||
883 url.contains(".eot") ||
884 url.contains(".ttf") ||
885 url.contains(".otf") ||
886 url.contains(".woff"),
887
888 isImage: function({ attachment: { mimeType } })
889 mimeType && mimeType.contains("image/"),
890
891 isMedia: function({ attachment: { mimeType } }) // Not including images.
892 mimeType && (
893 mimeType.contains("audio/") ||
894 mimeType.contains("video/") ||
895 mimeType.contains("model/")),
896
897 isFlash: function({ attachment: { url, mimeType } }) // Flash is a mess.
898 (mimeType && (
899 mimeType.contains("/x-flv") ||
900 mimeType.contains("/x-shockwave-flash"))) ||
901 url.contains(".swf") ||
902 url.contains(".flv"),
903
904 isOther: function(e)
905 !this.isHtml(e) && !this.isCss(e) && !this.isJs(e) && !this.isXHR(e) &&
906 !this.isFont(e) && !this.isImage(e) && !this.isMedia(e) && !this.isFlash(e),
907
908 /**
909 * Predicates used when sorting items.
910 *
911 * @param object aFirst
912 * The first item used in the comparison.
913 * @param object aSecond
914 * The second item used in the comparison.
915 * @return number
916 * -1 to sort aFirst to a lower index than aSecond
917 * 0 to leave aFirst and aSecond unchanged with respect to each other
918 * 1 to sort aSecond to a lower index than aFirst
919 */
920 _byTiming: function({ attachment: first }, { attachment: second })
921 first.startedMillis > second.startedMillis,
922
923 _byStatus: function({ attachment: first }, { attachment: second })
924 first.status == second.status
925 ? first.startedMillis > second.startedMillis
926 : first.status > second.status,
927
928 _byMethod: function({ attachment: first }, { attachment: second })
929 first.method == second.method
930 ? first.startedMillis > second.startedMillis
931 : first.method > second.method,
932
933 _byFile: function({ attachment: first }, { attachment: second }) {
934 let firstUrl = this._getUriNameWithQuery(first.url).toLowerCase();
935 let secondUrl = this._getUriNameWithQuery(second.url).toLowerCase();
936 return firstUrl == secondUrl
937 ? first.startedMillis > second.startedMillis
938 : firstUrl > secondUrl;
939 },
940
941 _byDomain: function({ attachment: first }, { attachment: second }) {
942 let firstDomain = this._getUriHostPort(first.url).toLowerCase();
943 let secondDomain = this._getUriHostPort(second.url).toLowerCase();
944 return firstDomain == secondDomain
945 ? first.startedMillis > second.startedMillis
946 : firstDomain > secondDomain;
947 },
948
949 _byType: function({ attachment: first }, { attachment: second }) {
950 let firstType = this._getAbbreviatedMimeType(first.mimeType).toLowerCase();
951 let secondType = this._getAbbreviatedMimeType(second.mimeType).toLowerCase();
952 return firstType == secondType
953 ? first.startedMillis > second.startedMillis
954 : firstType > secondType;
955 },
956
957 _bySize: function({ attachment: first }, { attachment: second })
958 first.contentSize > second.contentSize,
959
960 /**
961 * Refreshes the status displayed in this container's footer, providing
962 * concise information about all requests.
963 */
964 refreshSummary: function() {
965 let visibleItems = this.visibleItems;
966 let visibleRequestsCount = visibleItems.length;
967 if (!visibleRequestsCount) {
968 this._summary.setAttribute("value", L10N.getStr("networkMenu.empty"));
969 return;
970 }
971
972 let totalBytes = this._getTotalBytesOfRequests(visibleItems);
973 let totalMillis =
974 this._getNewestRequest(visibleItems).attachment.endedMillis -
975 this._getOldestRequest(visibleItems).attachment.startedMillis;
976
977 // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals
978 let str = PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary"));
979 this._summary.setAttribute("value", str
980 .replace("#1", visibleRequestsCount)
981 .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, CONTENT_SIZE_DECIMALS))
982 .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, REQUEST_TIME_DECIMALS))
983 );
984 },
985
986 /**
987 * Adds odd/even attributes to all the visible items in this container.
988 */
989 refreshZebra: function() {
990 let visibleItems = this.visibleItems;
991
992 for (let i = 0, len = visibleItems.length; i < len; i++) {
993 let requestItem = visibleItems[i];
994 let requestTarget = requestItem.target;
995
996 if (i % 2 == 0) {
997 requestTarget.setAttribute("even", "");
998 requestTarget.removeAttribute("odd");
999 } else {
1000 requestTarget.setAttribute("odd", "");
1001 requestTarget.removeAttribute("even");
1002 }
1003 }
1004 },
1005
1006 /**
1007 * Refreshes the toggling anchor for the specified item's tooltip.
1008 *
1009 * @param object aItem
1010 * The network request item in this container.
1011 */
1012 refreshTooltip: function(aItem) {
1013 let tooltip = aItem.attachment.tooltip;
1014 tooltip.hide();
1015 tooltip.startTogglingOnHover(aItem.target, this._onHover);
1016 tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
1017 },
1018
1019 /**
1020 * Schedules adding additional information to a network request.
1021 *
1022 * @param string aId
1023 * An identifier coming from the network monitor controller.
1024 * @param object aData
1025 * An object containing several { key: value } tuples of network info.
1026 * Supported keys are "httpVersion", "status", "statusText" etc.
1027 */
1028 updateRequest: function(aId, aData) {
1029 // Prevent interference from zombie updates received after target closed.
1030 if (NetMonitorView._isDestroyed) {
1031 return;
1032 }
1033 this._updateQueue.push([aId, aData]);
1034
1035 // Lazy updating is disabled in some tests.
1036 if (!this.lazyUpdate) {
1037 return void this._flushRequests();
1038 }
1039 // Allow requests to settle down first.
1040 setNamedTimeout(
1041 "update-requests", REQUESTS_REFRESH_RATE, () => this._flushRequests());
1042 },
1043
1044 /**
1045 * Starts adding all queued additional information about network requests.
1046 */
1047 _flushRequests: function() {
1048 // For each queued additional information packet, get the corresponding
1049 // request item in the view and update it based on the specified data.
1050 for (let [id, data] of this._updateQueue) {
1051 let requestItem = this.getItemByValue(id);
1052 if (!requestItem) {
1053 // Packet corresponds to a dead request item, target navigated.
1054 continue;
1055 }
1056
1057 // Each information packet may contain several { key: value } tuples of
1058 // network info, so update the view based on each one.
1059 for (let key in data) {
1060 let value = data[key];
1061 if (value === undefined) {
1062 // The information in the packet is empty, it can be safely ignored.
1063 continue;
1064 }
1065
1066 switch (key) {
1067 case "requestHeaders":
1068 requestItem.attachment.requestHeaders = value;
1069 break;
1070 case "requestCookies":
1071 requestItem.attachment.requestCookies = value;
1072 break;
1073 case "requestPostData":
1074 // Search the POST data upload stream for request headers and add
1075 // them to a separate store, different from the classic headers.
1076 // XXX: Be really careful here! We're creating a function inside
1077 // a loop, so remember the actual request item we want to modify.
1078 let currentItem = requestItem;
1079 let currentStore = { headers: [], headersSize: 0 };
1080
1081 Task.spawn(function*() {
1082 let postData = yield gNetwork.getString(value.postData.text);
1083 let payloadHeaders = CurlUtils.getHeadersFromMultipartText(postData);
1084
1085 currentStore.headers = payloadHeaders;
1086 currentStore.headersSize = payloadHeaders.reduce(
1087 (acc, { name, value }) => acc + name.length + value.length + 2, 0);
1088
1089 // The `getString` promise is async, so we need to refresh the
1090 // information displayed in the network details pane again here.
1091 refreshNetworkDetailsPaneIfNecessary(currentItem);
1092 });
1093
1094 requestItem.attachment.requestPostData = value;
1095 requestItem.attachment.requestHeadersFromUploadStream = currentStore;
1096 break;
1097 case "responseHeaders":
1098 requestItem.attachment.responseHeaders = value;
1099 break;
1100 case "responseCookies":
1101 requestItem.attachment.responseCookies = value;
1102 break;
1103 case "httpVersion":
1104 requestItem.attachment.httpVersion = value;
1105 break;
1106 case "status":
1107 requestItem.attachment.status = value;
1108 this.updateMenuView(requestItem, key, value);
1109 break;
1110 case "statusText":
1111 requestItem.attachment.statusText = value;
1112 this.updateMenuView(requestItem, key,
1113 requestItem.attachment.status + " " +
1114 requestItem.attachment.statusText);
1115 break;
1116 case "headersSize":
1117 requestItem.attachment.headersSize = value;
1118 break;
1119 case "contentSize":
1120 requestItem.attachment.contentSize = value;
1121 this.updateMenuView(requestItem, key, value);
1122 break;
1123 case "mimeType":
1124 requestItem.attachment.mimeType = value;
1125 this.updateMenuView(requestItem, key, value);
1126 break;
1127 case "responseContent":
1128 // If there's no mime type available when the response content
1129 // is received, assume text/plain as a fallback.
1130 if (!requestItem.attachment.mimeType) {
1131 requestItem.attachment.mimeType = "text/plain";
1132 this.updateMenuView(requestItem, "mimeType", "text/plain");
1133 }
1134 requestItem.attachment.responseContent = value;
1135 this.updateMenuView(requestItem, key, value);
1136 break;
1137 case "totalTime":
1138 requestItem.attachment.totalTime = value;
1139 requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value;
1140 this.updateMenuView(requestItem, key, value);
1141 this._registerLastRequestEnd(requestItem.attachment.endedMillis);
1142 break;
1143 case "eventTimings":
1144 requestItem.attachment.eventTimings = value;
1145 this._createWaterfallView(requestItem, value.timings);
1146 break;
1147 }
1148 }
1149 refreshNetworkDetailsPaneIfNecessary(requestItem);
1150 }
1151
1152 /**
1153 * Refreshes the information displayed in the sidebar, in case this update
1154 * may have additional information about a request which isn't shown yet
1155 * in the network details pane.
1156 *
1157 * @param object aRequestItem
1158 * The item to repopulate the sidebar with in case it's selected in
1159 * this requests menu.
1160 */
1161 function refreshNetworkDetailsPaneIfNecessary(aRequestItem) {
1162 let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
1163 if (selectedItem == aRequestItem) {
1164 NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
1165 }
1166 }
1167
1168 // We're done flushing all the requests, clear the update queue.
1169 this._updateQueue = [];
1170
1171 // Make sure all the requests are sorted and filtered.
1172 // Freshly added requests may not yet contain all the information required
1173 // for sorting and filtering predicates, so this is done each time the
1174 // network requests table is flushed (don't worry, events are drained first
1175 // so this doesn't happen once per network event update).
1176 this.sortContents();
1177 this.filterContents();
1178 this.refreshSummary();
1179 this.refreshZebra();
1180
1181 // Rescale all the waterfalls so that everything is visible at once.
1182 this._flushWaterfallViews();
1183 },
1184
1185 /**
1186 * Customization function for creating an item's UI.
1187 *
1188 * @param string aMethod
1189 * Specifies the request method (e.g. "GET", "POST", etc.)
1190 * @param string aUrl
1191 * Specifies the request's url.
1192 * @return nsIDOMNode
1193 * The network request view.
1194 */
1195 _createMenuView: function(aMethod, aUrl) {
1196 let template = $("#requests-menu-item-template");
1197 let fragment = document.createDocumentFragment();
1198
1199 this.updateMenuView(template, 'method', aMethod);
1200 this.updateMenuView(template, 'url', aUrl);
1201
1202 let waterfall = $(".requests-menu-waterfall", template);
1203 waterfall.style.backgroundImage = this._cachedWaterfallBackground;
1204
1205 // Flatten the DOM by removing one redundant box (the template container).
1206 for (let node of template.childNodes) {
1207 fragment.appendChild(node.cloneNode(true));
1208 }
1209
1210 return fragment;
1211 },
1212
1213 /**
1214 * Updates the information displayed in a network request item view.
1215 *
1216 * @param object aItem
1217 * The network request item in this container.
1218 * @param string aKey
1219 * The type of information that is to be updated.
1220 * @param any aValue
1221 * The new value to be shown.
1222 * @return object
1223 * A promise that is resolved once the information is displayed.
1224 */
1225 updateMenuView: Task.async(function*(aItem, aKey, aValue) {
1226 let target = aItem.target || aItem;
1227
1228 switch (aKey) {
1229 case "method": {
1230 let node = $(".requests-menu-method", target);
1231 node.setAttribute("value", aValue);
1232 break;
1233 }
1234 case "url": {
1235 let uri;
1236 try {
1237 uri = nsIURL(aValue);
1238 } catch(e) {
1239 break; // User input may not make a well-formed url yet.
1240 }
1241 let nameWithQuery = this._getUriNameWithQuery(uri);
1242 let hostPort = this._getUriHostPort(uri);
1243
1244 let file = $(".requests-menu-file", target);
1245 file.setAttribute("value", nameWithQuery);
1246 file.setAttribute("tooltiptext", nameWithQuery);
1247
1248 let domain = $(".requests-menu-domain", target);
1249 domain.setAttribute("value", hostPort);
1250 domain.setAttribute("tooltiptext", hostPort);
1251 break;
1252 }
1253 case "status": {
1254 let node = $(".requests-menu-status", target);
1255 let codeNode = $(".requests-menu-status-code", target);
1256 codeNode.setAttribute("value", aValue);
1257 node.setAttribute("code", aValue);
1258 break;
1259 }
1260 case "statusText": {
1261 let node = $(".requests-menu-status-and-method", target);
1262 node.setAttribute("tooltiptext", aValue);
1263 break;
1264 }
1265 case "contentSize": {
1266 let kb = aValue / 1024;
1267 let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
1268 let node = $(".requests-menu-size", target);
1269 let text = L10N.getFormatStr("networkMenu.sizeKB", size);
1270 node.setAttribute("value", text);
1271 node.setAttribute("tooltiptext", text);
1272 break;
1273 }
1274 case "mimeType": {
1275 let type = this._getAbbreviatedMimeType(aValue);
1276 let node = $(".requests-menu-type", target);
1277 let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
1278 node.setAttribute("value", text);
1279 node.setAttribute("tooltiptext", aValue);
1280 break;
1281 }
1282 case "responseContent": {
1283 let { mimeType } = aItem.attachment;
1284 let { text, encoding } = aValue.content;
1285
1286 if (mimeType.contains("image/")) {
1287 let responseBody = yield gNetwork.getString(text);
1288 let node = $(".requests-menu-icon", aItem.target);
1289 node.src = "data:" + mimeType + ";" + encoding + "," + responseBody;
1290 node.setAttribute("type", "thumbnail");
1291 node.removeAttribute("hidden");
1292
1293 window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
1294 }
1295 break;
1296 }
1297 case "totalTime": {
1298 let node = $(".requests-menu-timings-total", target);
1299 let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer
1300 node.setAttribute("value", text);
1301 node.setAttribute("tooltiptext", text);
1302 break;
1303 }
1304 }
1305 }),
1306
1307 /**
1308 * Creates a waterfall representing timing information in a network request item view.
1309 *
1310 * @param object aItem
1311 * The network request item in this container.
1312 * @param object aTimings
1313 * An object containing timing information.
1314 */
1315 _createWaterfallView: function(aItem, aTimings) {
1316 let { target, attachment } = aItem;
1317 let sections = ["dns", "connect", "send", "wait", "receive"];
1318 // Skipping "blocked" because it doesn't work yet.
1319
1320 let timingsNode = $(".requests-menu-timings", target);
1321 let timingsTotal = $(".requests-menu-timings-total", timingsNode);
1322
1323 // Add a set of boxes representing timing information.
1324 for (let key of sections) {
1325 let width = aTimings[key];
1326
1327 // Don't render anything if it surely won't be visible.
1328 // One millisecond == one unscaled pixel.
1329 if (width > 0) {
1330 let timingBox = document.createElement("hbox");
1331 timingBox.className = "requests-menu-timings-box " + key;
1332 timingBox.setAttribute("width", width);
1333 timingsNode.insertBefore(timingBox, timingsTotal);
1334 }
1335 }
1336 },
1337
1338 /**
1339 * Rescales and redraws all the waterfall views in this container.
1340 *
1341 * @param boolean aReset
1342 * True if this container's width was changed.
1343 */
1344 _flushWaterfallViews: function(aReset) {
1345 // Don't paint things while the waterfall view isn't even visible,
1346 // or there are no items added to this container.
1347 if (NetMonitorView.currentFrontendMode != "network-inspector-view" || !this.itemCount) {
1348 return;
1349 }
1350
1351 // To avoid expensive operations like getBoundingClientRect() and
1352 // rebuilding the waterfall background each time a new request comes in,
1353 // stuff is cached. However, in certain scenarios like when the window
1354 // is resized, this needs to be invalidated.
1355 if (aReset) {
1356 this._cachedWaterfallWidth = 0;
1357 }
1358
1359 // Determine the scaling to be applied to all the waterfalls so that
1360 // everything is visible at once. One millisecond == one unscaled pixel.
1361 let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
1362 let longestWidth = this._lastRequestEndedMillis - this._firstRequestStartedMillis;
1363 let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1);
1364
1365 // Redraw and set the canvas background for each waterfall view.
1366 this._showWaterfallDivisionLabels(scale);
1367 this._drawWaterfallBackground(scale);
1368 this._flushWaterfallBackgrounds();
1369
1370 // Apply CSS transforms to each waterfall in this container totalTime
1371 // accurately translate and resize as needed.
1372 for (let { target, attachment } of this) {
1373 let timingsNode = $(".requests-menu-timings", target);
1374 let totalNode = $(".requests-menu-timings-total", target);
1375 let direction = window.isRTL ? -1 : 1;
1376
1377 // Render the timing information at a specific horizontal translation
1378 // based on the delta to the first monitored event network.
1379 let translateX = "translateX(" + (direction * attachment.startedDeltaMillis) + "px)";
1380
1381 // Based on the total time passed until the last request, rescale
1382 // all the waterfalls to a reasonable size.
1383 let scaleX = "scaleX(" + scale + ")";
1384
1385 // Certain nodes should not be scaled, even if they're children of
1386 // another scaled node. In this case, apply a reversed transformation.
1387 let revScaleX = "scaleX(" + (1 / scale) + ")";
1388
1389 timingsNode.style.transform = scaleX + " " + translateX;
1390 totalNode.style.transform = revScaleX;
1391 }
1392 },
1393
1394 /**
1395 * Creates the labels displayed on the waterfall header in this container.
1396 *
1397 * @param number aScale
1398 * The current waterfall scale.
1399 */
1400 _showWaterfallDivisionLabels: function(aScale) {
1401 let container = $("#requests-menu-waterfall-button");
1402 let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
1403
1404 // Nuke all existing labels.
1405 while (container.hasChildNodes()) {
1406 container.firstChild.remove();
1407 }
1408
1409 // Build new millisecond tick labels...
1410 let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
1411 let optimalTickIntervalFound = false;
1412
1413 while (!optimalTickIntervalFound) {
1414 // Ignore any divisions that would end up being too close to each other.
1415 let scaledStep = aScale * timingStep;
1416 if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
1417 timingStep <<= 1;
1418 continue;
1419 }
1420 optimalTickIntervalFound = true;
1421
1422 // Insert one label for each division on the current scale.
1423 let fragment = document.createDocumentFragment();
1424 let direction = window.isRTL ? -1 : 1;
1425
1426 for (let x = 0; x < availableWidth; x += scaledStep) {
1427 let translateX = "translateX(" + ((direction * x) | 0) + "px)";
1428 let millisecondTime = x / aScale;
1429
1430 let normalizedTime = millisecondTime;
1431 let divisionScale = "millisecond";
1432
1433 // If the division is greater than 1 minute.
1434 if (normalizedTime > 60000) {
1435 normalizedTime /= 60000;
1436 divisionScale = "minute";
1437 }
1438 // If the division is greater than 1 second.
1439 else if (normalizedTime > 1000) {
1440 normalizedTime /= 1000;
1441 divisionScale = "second";
1442 }
1443
1444 // Showing too many decimals is bad UX.
1445 if (divisionScale == "millisecond") {
1446 normalizedTime |= 0;
1447 } else {
1448 normalizedTime = L10N.numberWithDecimals(normalizedTime, REQUEST_TIME_DECIMALS);
1449 }
1450
1451 let node = document.createElement("label");
1452 let text = L10N.getFormatStr("networkMenu." + divisionScale, normalizedTime);
1453 node.className = "plain requests-menu-timings-division";
1454 node.setAttribute("division-scale", divisionScale);
1455 node.style.transform = translateX;
1456
1457 node.setAttribute("value", text);
1458 fragment.appendChild(node);
1459 }
1460 container.appendChild(fragment);
1461 }
1462 },
1463
1464 /**
1465 * Creates the background displayed on each waterfall view in this container.
1466 *
1467 * @param number aScale
1468 * The current waterfall scale.
1469 */
1470 _drawWaterfallBackground: function(aScale) {
1471 if (!this._canvas || !this._ctx) {
1472 this._canvas = document.createElementNS(HTML_NS, "canvas");
1473 this._ctx = this._canvas.getContext("2d");
1474 }
1475 let canvas = this._canvas;
1476 let ctx = this._ctx;
1477
1478 // Nuke the context.
1479 let canvasWidth = canvas.width = this._waterfallWidth;
1480 let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
1481
1482 // Start over.
1483 let imageData = ctx.createImageData(canvasWidth, canvasHeight);
1484 let pixelArray = imageData.data;
1485
1486 let buf = new ArrayBuffer(pixelArray.length);
1487 let buf8 = new Uint8ClampedArray(buf);
1488 let data32 = new Uint32Array(buf);
1489
1490 // Build new millisecond tick lines...
1491 let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE;
1492 let [r, g, b] = REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
1493 let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
1494 let optimalTickIntervalFound = false;
1495
1496 while (!optimalTickIntervalFound) {
1497 // Ignore any divisions that would end up being too close to each other.
1498 let scaledStep = aScale * timingStep;
1499 if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) {
1500 timingStep <<= 1;
1501 continue;
1502 }
1503 optimalTickIntervalFound = true;
1504
1505 // Insert one pixel for each division on each scale.
1506 for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
1507 let increment = scaledStep * Math.pow(2, i);
1508 for (let x = 0; x < canvasWidth; x += increment) {
1509 let position = (window.isRTL ? canvasWidth - x : x) | 0;
1510 data32[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
1511 }
1512 alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
1513 }
1514 }
1515
1516 // Flush the image data and cache the waterfall background.
1517 pixelArray.set(buf8);
1518 ctx.putImageData(imageData, 0, 0);
1519 this._cachedWaterfallBackground = "url(" + canvas.toDataURL() + ")";
1520 },
1521
1522 /**
1523 * Reapplies the current waterfall background on all request items.
1524 */
1525 _flushWaterfallBackgrounds: function() {
1526 for (let { target } of this) {
1527 let waterfallNode = $(".requests-menu-waterfall", target);
1528 waterfallNode.style.backgroundImage = this._cachedWaterfallBackground;
1529 }
1530 },
1531
1532 /**
1533 * The selection listener for this container.
1534 */
1535 _onSelect: function({ detail: item }) {
1536 if (item) {
1537 NetMonitorView.Sidebar.populate(item.attachment);
1538 NetMonitorView.Sidebar.toggle(true);
1539 } else {
1540 NetMonitorView.Sidebar.toggle(false);
1541 }
1542 },
1543
1544 /**
1545 * The swap listener for this container.
1546 * Called when two items switch places, when the contents are sorted.
1547 */
1548 _onSwap: function({ detail: [firstItem, secondItem] }) {
1549 // Sorting will create new anchor nodes for all the swapped request items
1550 // in this container, so it's necessary to refresh the Tooltip instances.
1551 this.refreshTooltip(firstItem);
1552 this.refreshTooltip(secondItem);
1553 },
1554
1555 /**
1556 * The predicate used when deciding whether a popup should be shown
1557 * over a request item or not.
1558 *
1559 * @param nsIDOMNode aTarget
1560 * The element node currently being hovered.
1561 * @param object aTooltip
1562 * The current tooltip instance.
1563 */
1564 _onHover: function(aTarget, aTooltip) {
1565 let requestItem = this.getItemForElement(aTarget);
1566 if (!requestItem || !requestItem.attachment.responseContent) {
1567 return;
1568 }
1569
1570 let hovered = requestItem.attachment;
1571 let { url } = hovered;
1572 let { mimeType, text, encoding } = hovered.responseContent.content;
1573
1574 if (mimeType && mimeType.contains("image/") && (
1575 aTarget.classList.contains("requests-menu-icon") ||
1576 aTarget.classList.contains("requests-menu-file")))
1577 {
1578 return gNetwork.getString(text).then(aString => {
1579 let anchor = $(".requests-menu-icon", requestItem.target);
1580 let src = "data:" + mimeType + ";" + encoding + "," + aString;
1581 aTooltip.setImageContent(src, { maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM });
1582 return anchor;
1583 });
1584 }
1585 },
1586
1587 /**
1588 * The resize listener for this container's window.
1589 */
1590 _onResize: function(e) {
1591 // Allow requests to settle down first.
1592 setNamedTimeout(
1593 "resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
1594 },
1595
1596 /**
1597 * Handle the context menu opening. Hide items if no request is selected.
1598 */
1599 _onContextShowing: function() {
1600 let selectedItem = this.selectedItem;
1601
1602 let resendElement = $("#request-menu-context-resend");
1603 resendElement.hidden = !NetMonitorController.supportsCustomRequest ||
1604 !selectedItem || selectedItem.attachment.isCustom;
1605
1606 let copyUrlElement = $("#request-menu-context-copy-url");
1607 copyUrlElement.hidden = !selectedItem;
1608
1609 let copyAsCurlElement = $("#request-menu-context-copy-as-curl");
1610 copyAsCurlElement.hidden = !selectedItem || !selectedItem.attachment.responseContent;
1611
1612 let copyImageAsDataUriElement = $("#request-menu-context-copy-image-as-data-uri");
1613 copyImageAsDataUriElement.hidden = !selectedItem ||
1614 !selectedItem.attachment.responseContent ||
1615 !selectedItem.attachment.responseContent.content.mimeType.contains("image/");
1616
1617 let newTabElement = $("#request-menu-context-newtab");
1618 newTabElement.hidden = !selectedItem;
1619 },
1620
1621 /**
1622 * Checks if the specified unix time is the first one to be known of,
1623 * and saves it if so.
1624 *
1625 * @param number aUnixTime
1626 * The milliseconds to check and save.
1627 */
1628 _registerFirstRequestStart: function(aUnixTime) {
1629 if (this._firstRequestStartedMillis == -1) {
1630 this._firstRequestStartedMillis = aUnixTime;
1631 }
1632 },
1633
1634 /**
1635 * Checks if the specified unix time is the last one to be known of,
1636 * and saves it if so.
1637 *
1638 * @param number aUnixTime
1639 * The milliseconds to check and save.
1640 */
1641 _registerLastRequestEnd: function(aUnixTime) {
1642 if (this._lastRequestEndedMillis < aUnixTime) {
1643 this._lastRequestEndedMillis = aUnixTime;
1644 }
1645 },
1646
1647 /**
1648 * Helpers for getting details about an nsIURL.
1649 *
1650 * @param nsIURL | string aUrl
1651 * @return string
1652 */
1653 _getUriNameWithQuery: function(aUrl) {
1654 if (!(aUrl instanceof Ci.nsIURL)) {
1655 aUrl = nsIURL(aUrl);
1656 }
1657 let name = NetworkHelper.convertToUnicode(unescape(aUrl.fileName)) || "/";
1658 let query = NetworkHelper.convertToUnicode(unescape(aUrl.query));
1659 return name + (query ? "?" + query : "");
1660 },
1661 _getUriHostPort: function(aUrl) {
1662 if (!(aUrl instanceof Ci.nsIURL)) {
1663 aUrl = nsIURL(aUrl);
1664 }
1665 return NetworkHelper.convertToUnicode(unescape(aUrl.hostPort));
1666 },
1667
1668 /**
1669 * Helper for getting an abbreviated string for a mime type.
1670 *
1671 * @param string aMimeType
1672 * @return string
1673 */
1674 _getAbbreviatedMimeType: function(aMimeType) {
1675 if (!aMimeType) {
1676 return "";
1677 }
1678 return (aMimeType.split(";")[0].split("/")[1] || "").split("+")[0];
1679 },
1680
1681 /**
1682 * Gets the total number of bytes representing the cumulated content size of
1683 * a set of requests. Returns 0 for an empty set.
1684 *
1685 * @param array aItemsArray
1686 * @return number
1687 */
1688 _getTotalBytesOfRequests: function(aItemsArray) {
1689 if (!aItemsArray.length) {
1690 return 0;
1691 }
1692 return aItemsArray.reduce((prev, curr) => prev + curr.attachment.contentSize || 0, 0);
1693 },
1694
1695 /**
1696 * Gets the oldest (first performed) request in a set. Returns null for an
1697 * empty set.
1698 *
1699 * @param array aItemsArray
1700 * @return object
1701 */
1702 _getOldestRequest: function(aItemsArray) {
1703 if (!aItemsArray.length) {
1704 return null;
1705 }
1706 return aItemsArray.reduce((prev, curr) =>
1707 prev.attachment.startedMillis < curr.attachment.startedMillis ? prev : curr);
1708 },
1709
1710 /**
1711 * Gets the newest (latest performed) request in a set. Returns null for an
1712 * empty set.
1713 *
1714 * @param array aItemsArray
1715 * @return object
1716 */
1717 _getNewestRequest: function(aItemsArray) {
1718 if (!aItemsArray.length) {
1719 return null;
1720 }
1721 return aItemsArray.reduce((prev, curr) =>
1722 prev.attachment.startedMillis > curr.attachment.startedMillis ? prev : curr);
1723 },
1724
1725 /**
1726 * Gets the available waterfall width in this container.
1727 * @return number
1728 */
1729 get _waterfallWidth() {
1730 if (this._cachedWaterfallWidth == 0) {
1731 let container = $("#requests-menu-toolbar");
1732 let waterfall = $("#requests-menu-waterfall-header-box");
1733 let containerBounds = container.getBoundingClientRect();
1734 let waterfallBounds = waterfall.getBoundingClientRect();
1735 if (!window.isRTL) {
1736 this._cachedWaterfallWidth = containerBounds.width - waterfallBounds.left;
1737 } else {
1738 this._cachedWaterfallWidth = waterfallBounds.right;
1739 }
1740 }
1741 return this._cachedWaterfallWidth;
1742 },
1743
1744 _splitter: null,
1745 _summary: null,
1746 _canvas: null,
1747 _ctx: null,
1748 _cachedWaterfallWidth: 0,
1749 _cachedWaterfallBackground: "",
1750 _firstRequestStartedMillis: -1,
1751 _lastRequestEndedMillis: -1,
1752 _updateQueue: [],
1753 _updateTimeout: null,
1754 _resizeTimeout: null,
1755 _activeFilters: ["all"]
1756 });
1757
1758 /**
1759 * Functions handling the sidebar details view.
1760 */
1761 function SidebarView() {
1762 dumpn("SidebarView was instantiated");
1763 }
1764
1765 SidebarView.prototype = {
1766 /**
1767 * Sets this view hidden or visible. It's visible by default.
1768 *
1769 * @param boolean aVisibleFlag
1770 * Specifies the intended visibility.
1771 */
1772 toggle: function(aVisibleFlag) {
1773 NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag });
1774 NetMonitorView.RequestsMenu._flushWaterfallViews(true);
1775 },
1776
1777 /**
1778 * Populates this view with the specified data.
1779 *
1780 * @param object aData
1781 * The data source (this should be the attachment of a request item).
1782 * @return object
1783 * Returns a promise that resolves upon population of the subview.
1784 */
1785 populate: Task.async(function*(aData) {
1786 let isCustom = aData.isCustom;
1787 let view = isCustom ?
1788 NetMonitorView.CustomRequest :
1789 NetMonitorView.NetworkDetails;
1790
1791 yield view.populate(aData);
1792 $("#details-pane").selectedIndex = isCustom ? 0 : 1;
1793
1794 window.emit(EVENTS.SIDEBAR_POPULATED);
1795 })
1796 }
1797
1798 /**
1799 * Functions handling the custom request view.
1800 */
1801 function CustomRequestView() {
1802 dumpn("CustomRequestView was instantiated");
1803 }
1804
1805 CustomRequestView.prototype = {
1806 /**
1807 * Initialization function, called when the network monitor is started.
1808 */
1809 initialize: function() {
1810 dumpn("Initializing the CustomRequestView");
1811
1812 this.updateCustomRequestEvent = getKeyWithEvent(this.onUpdate.bind(this));
1813 $("#custom-pane").addEventListener("input", this.updateCustomRequestEvent, false);
1814 },
1815
1816 /**
1817 * Destruction function, called when the network monitor is closed.
1818 */
1819 destroy: function() {
1820 dumpn("Destroying the CustomRequestView");
1821
1822 $("#custom-pane").removeEventListener("input", this.updateCustomRequestEvent, false);
1823 },
1824
1825 /**
1826 * Populates this view with the specified data.
1827 *
1828 * @param object aData
1829 * The data source (this should be the attachment of a request item).
1830 * @return object
1831 * Returns a promise that resolves upon population the view.
1832 */
1833 populate: Task.async(function*(aData) {
1834 $("#custom-url-value").value = aData.url;
1835 $("#custom-method-value").value = aData.method;
1836 this.updateCustomQuery(aData.url);
1837
1838 if (aData.requestHeaders) {
1839 let headers = aData.requestHeaders.headers;
1840 $("#custom-headers-value").value = writeHeaderText(headers);
1841 }
1842 if (aData.requestPostData) {
1843 let postData = aData.requestPostData.postData.text;
1844 $("#custom-postdata-value").value = yield gNetwork.getString(postData);
1845 }
1846
1847 window.emit(EVENTS.CUSTOMREQUESTVIEW_POPULATED);
1848 }),
1849
1850 /**
1851 * Handle user input in the custom request form.
1852 *
1853 * @param object aField
1854 * the field that the user updated.
1855 */
1856 onUpdate: function(aField) {
1857 let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
1858 let field = aField;
1859 let value;
1860
1861 switch(aField) {
1862 case 'method':
1863 value = $("#custom-method-value").value.trim();
1864 selectedItem.attachment.method = value;
1865 break;
1866 case 'url':
1867 value = $("#custom-url-value").value;
1868 this.updateCustomQuery(value);
1869 selectedItem.attachment.url = value;
1870 break;
1871 case 'query':
1872 let query = $("#custom-query-value").value;
1873 this.updateCustomUrl(query);
1874 field = 'url';
1875 value = $("#custom-url-value").value
1876 selectedItem.attachment.url = value;
1877 break;
1878 case 'body':
1879 value = $("#custom-postdata-value").value;
1880 selectedItem.attachment.requestPostData = { postData: { text: value } };
1881 break;
1882 case 'headers':
1883 let headersText = $("#custom-headers-value").value;
1884 value = parseHeadersText(headersText);
1885 selectedItem.attachment.requestHeaders = { headers: value };
1886 break;
1887 }
1888
1889 NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value);
1890 },
1891
1892 /**
1893 * Update the query string field based on the url.
1894 *
1895 * @param object aUrl
1896 * The URL to extract query string from.
1897 */
1898 updateCustomQuery: function(aUrl) {
1899 let paramsArray = parseQueryString(nsIURL(aUrl).query);
1900 if (!paramsArray) {
1901 $("#custom-query").hidden = true;
1902 return;
1903 }
1904 $("#custom-query").hidden = false;
1905 $("#custom-query-value").value = writeQueryText(paramsArray);
1906 },
1907
1908 /**
1909 * Update the url based on the query string field.
1910 *
1911 * @param object aQueryText
1912 * The contents of the query string field.
1913 */
1914 updateCustomUrl: function(aQueryText) {
1915 let params = parseQueryText(aQueryText);
1916 let queryString = writeQueryString(params);
1917
1918 let url = $("#custom-url-value").value;
1919 let oldQuery = nsIURL(url).query;
1920 let path = url.replace(oldQuery, queryString);
1921
1922 $("#custom-url-value").value = path;
1923 }
1924 }
1925
1926 /**
1927 * Functions handling the requests details view.
1928 */
1929 function NetworkDetailsView() {
1930 dumpn("NetworkDetailsView was instantiated");
1931
1932 this._onTabSelect = this._onTabSelect.bind(this);
1933 };
1934
1935 NetworkDetailsView.prototype = {
1936 /**
1937 * Initialization function, called when the network monitor is started.
1938 */
1939 initialize: function() {
1940 dumpn("Initializing the NetworkDetailsView");
1941
1942 this.widget = $("#event-details-pane");
1943
1944 this._headers = new VariablesView($("#all-headers"),
1945 Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
1946 emptyText: L10N.getStr("headersEmptyText"),
1947 searchPlaceholder: L10N.getStr("headersFilterText")
1948 }));
1949 this._cookies = new VariablesView($("#all-cookies"),
1950 Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
1951 emptyText: L10N.getStr("cookiesEmptyText"),
1952 searchPlaceholder: L10N.getStr("cookiesFilterText")
1953 }));
1954 this._params = new VariablesView($("#request-params"),
1955 Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
1956 emptyText: L10N.getStr("paramsEmptyText"),
1957 searchPlaceholder: L10N.getStr("paramsFilterText")
1958 }));
1959 this._json = new VariablesView($("#response-content-json"),
1960 Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
1961 onlyEnumVisible: true,
1962 searchPlaceholder: L10N.getStr("jsonFilterText")
1963 }));
1964 VariablesViewController.attach(this._json);
1965
1966 this._paramsQueryString = L10N.getStr("paramsQueryString");
1967 this._paramsFormData = L10N.getStr("paramsFormData");
1968 this._paramsPostPayload = L10N.getStr("paramsPostPayload");
1969 this._requestHeaders = L10N.getStr("requestHeaders");
1970 this._requestHeadersFromUpload = L10N.getStr("requestHeadersFromUpload");
1971 this._responseHeaders = L10N.getStr("responseHeaders");
1972 this._requestCookies = L10N.getStr("requestCookies");
1973 this._responseCookies = L10N.getStr("responseCookies");
1974
1975 $("tabpanels", this.widget).addEventListener("select", this._onTabSelect);
1976 },
1977
1978 /**
1979 * Destruction function, called when the network monitor is closed.
1980 */
1981 destroy: function() {
1982 dumpn("Destroying the NetworkDetailsView");
1983 },
1984
1985 /**
1986 * Populates this view with the specified data.
1987 *
1988 * @param object aData
1989 * The data source (this should be the attachment of a request item).
1990 * @return object
1991 * Returns a promise that resolves upon population the view.
1992 */
1993 populate: function(aData) {
1994 $("#request-params-box").setAttribute("flex", "1");
1995 $("#request-params-box").hidden = false;
1996 $("#request-post-data-textarea-box").hidden = true;
1997 $("#response-content-info-header").hidden = true;
1998 $("#response-content-json-box").hidden = true;
1999 $("#response-content-textarea-box").hidden = true;
2000 $("#response-content-image-box").hidden = true;
2001
2002 let isHtml = RequestsMenuView.prototype.isHtml({ attachment: aData });
2003
2004 // Show the "Preview" tabpanel only for plain HTML responses.
2005 $("#preview-tab").hidden = !isHtml;
2006 $("#preview-tabpanel").hidden = !isHtml;
2007
2008 // Switch to the "Headers" tabpanel if the "Preview" previously selected
2009 // and this is not an HTML response.
2010 if (!isHtml && this.widget.selectedIndex == 5) {
2011 this.widget.selectedIndex = 0;
2012 }
2013
2014 this._headers.empty();
2015 this._cookies.empty();
2016 this._params.empty();
2017 this._json.empty();
2018
2019 this._dataSrc = { src: aData, populated: [] };
2020 this._onTabSelect();
2021 window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED);
2022
2023 return promise.resolve();
2024 },
2025
2026 /**
2027 * Listener handling the tab selection event.
2028 */
2029 _onTabSelect: function() {
2030 let { src, populated } = this._dataSrc || {};
2031 let tab = this.widget.selectedIndex;
2032 let view = this;
2033
2034 // Make sure the data source is valid and don't populate the same tab twice.
2035 if (!src || populated[tab]) {
2036 return;
2037 }
2038
2039 Task.spawn(function*() {
2040 switch (tab) {
2041 case 0: // "Headers"
2042 yield view._setSummary(src);
2043 yield view._setResponseHeaders(src.responseHeaders);
2044 yield view._setRequestHeaders(
2045 src.requestHeaders,
2046 src.requestHeadersFromUploadStream);
2047 break;
2048 case 1: // "Cookies"
2049 yield view._setResponseCookies(src.responseCookies);
2050 yield view._setRequestCookies(src.requestCookies);
2051 break;
2052 case 2: // "Params"
2053 yield view._setRequestGetParams(src.url);
2054 yield view._setRequestPostParams(
2055 src.requestHeaders,
2056 src.requestHeadersFromUploadStream,
2057 src.requestPostData);
2058 break;
2059 case 3: // "Response"
2060 yield view._setResponseBody(src.url, src.responseContent);
2061 break;
2062 case 4: // "Timings"
2063 yield view._setTimingsInformation(src.eventTimings);
2064 break;
2065 case 5: // "Preview"
2066 yield view._setHtmlPreview(src.responseContent);
2067 break;
2068 }
2069 populated[tab] = true;
2070 window.emit(EVENTS.TAB_UPDATED);
2071 NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
2072 });
2073 },
2074
2075 /**
2076 * Sets the network request summary shown in this view.
2077 *
2078 * @param object aData
2079 * The data source (this should be the attachment of a request item).
2080 */
2081 _setSummary: function(aData) {
2082 if (aData.url) {
2083 let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aData.url));
2084 $("#headers-summary-url-value").setAttribute("value", unicodeUrl);
2085 $("#headers-summary-url-value").setAttribute("tooltiptext", unicodeUrl);
2086 $("#headers-summary-url").removeAttribute("hidden");
2087 } else {
2088 $("#headers-summary-url").setAttribute("hidden", "true");
2089 }
2090
2091 if (aData.method) {
2092 $("#headers-summary-method-value").setAttribute("value", aData.method);
2093 $("#headers-summary-method").removeAttribute("hidden");
2094 } else {
2095 $("#headers-summary-method").setAttribute("hidden", "true");
2096 }
2097
2098 if (aData.status) {
2099 $("#headers-summary-status-circle").setAttribute("code", aData.status);
2100 $("#headers-summary-status-value").setAttribute("value", aData.status + " " + aData.statusText);
2101 $("#headers-summary-status").removeAttribute("hidden");
2102 } else {
2103 $("#headers-summary-status").setAttribute("hidden", "true");
2104 }
2105
2106 if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) {
2107 $("#headers-summary-version-value").setAttribute("value", aData.httpVersion);
2108 $("#headers-summary-version").removeAttribute("hidden");
2109 } else {
2110 $("#headers-summary-version").setAttribute("hidden", "true");
2111 }
2112 },
2113
2114 /**
2115 * Sets the network request headers shown in this view.
2116 *
2117 * @param object aHeadersResponse
2118 * The "requestHeaders" message received from the server.
2119 * @param object aHeadersFromUploadStream
2120 * The "requestHeadersFromUploadStream" inferred from the POST payload.
2121 * @return object
2122 * A promise that resolves when request headers are set.
2123 */
2124 _setRequestHeaders: Task.async(function*(aHeadersResponse, aHeadersFromUploadStream) {
2125 if (aHeadersResponse && aHeadersResponse.headers.length) {
2126 yield this._addHeaders(this._requestHeaders, aHeadersResponse);
2127 }
2128 if (aHeadersFromUploadStream && aHeadersFromUploadStream.headers.length) {
2129 yield this._addHeaders(this._requestHeadersFromUpload, aHeadersFromUploadStream);
2130 }
2131 }),
2132
2133 /**
2134 * Sets the network response headers shown in this view.
2135 *
2136 * @param object aResponse
2137 * The message received from the server.
2138 * @return object
2139 * A promise that resolves when response headers are set.
2140 */
2141 _setResponseHeaders: Task.async(function*(aResponse) {
2142 if (aResponse && aResponse.headers.length) {
2143 aResponse.headers.sort((a, b) => a.name > b.name);
2144 yield this._addHeaders(this._responseHeaders, aResponse);
2145 }
2146 }),
2147
2148 /**
2149 * Populates the headers container in this view with the specified data.
2150 *
2151 * @param string aName
2152 * The type of headers to populate (request or response).
2153 * @param object aResponse
2154 * The message received from the server.
2155 * @return object
2156 * A promise that resolves when headers are added.
2157 */
2158 _addHeaders: Task.async(function*(aName, aResponse) {
2159 let kb = aResponse.headersSize / 1024;
2160 let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
2161 let text = L10N.getFormatStr("networkMenu.sizeKB", size);
2162
2163 let headersScope = this._headers.addScope(aName + " (" + text + ")");
2164 headersScope.expanded = true;
2165
2166 for (let header of aResponse.headers) {
2167 let headerVar = headersScope.addItem(header.name, {}, true);
2168 let headerValue = yield gNetwork.getString(header.value);
2169 headerVar.setGrip(headerValue);
2170 }
2171 }),
2172
2173 /**
2174 * Sets the network request cookies shown in this view.
2175 *
2176 * @param object aResponse
2177 * The message received from the server.
2178 * @return object
2179 * A promise that is resolved when the request cookies are set.
2180 */
2181 _setRequestCookies: Task.async(function*(aResponse) {
2182 if (aResponse && aResponse.cookies.length) {
2183 aResponse.cookies.sort((a, b) => a.name > b.name);
2184 yield this._addCookies(this._requestCookies, aResponse);
2185 }
2186 }),
2187
2188 /**
2189 * Sets the network response cookies shown in this view.
2190 *
2191 * @param object aResponse
2192 * The message received from the server.
2193 * @return object
2194 * A promise that is resolved when the response cookies are set.
2195 */
2196 _setResponseCookies: Task.async(function*(aResponse) {
2197 if (aResponse && aResponse.cookies.length) {
2198 yield this._addCookies(this._responseCookies, aResponse);
2199 }
2200 }),
2201
2202 /**
2203 * Populates the cookies container in this view with the specified data.
2204 *
2205 * @param string aName
2206 * The type of cookies to populate (request or response).
2207 * @param object aResponse
2208 * The message received from the server.
2209 * @return object
2210 * Returns a promise that resolves upon the adding of cookies.
2211 */
2212 _addCookies: Task.async(function*(aName, aResponse) {
2213 let cookiesScope = this._cookies.addScope(aName);
2214 cookiesScope.expanded = true;
2215
2216 for (let cookie of aResponse.cookies) {
2217 let cookieVar = cookiesScope.addItem(cookie.name, {}, true);
2218 let cookieValue = yield gNetwork.getString(cookie.value);
2219 cookieVar.setGrip(cookieValue);
2220
2221 // By default the cookie name and value are shown. If this is the only
2222 // information available, then nothing else is to be displayed.
2223 let cookieProps = Object.keys(cookie);
2224 if (cookieProps.length == 2) {
2225 return;
2226 }
2227
2228 // Display any other information other than the cookie name and value
2229 // which may be available.
2230 let rawObject = Object.create(null);
2231 let otherProps = cookieProps.filter(e => e != "name" && e != "value");
2232 for (let prop of otherProps) {
2233 rawObject[prop] = cookie[prop];
2234 }
2235 cookieVar.populate(rawObject);
2236 cookieVar.twisty = true;
2237 cookieVar.expanded = true;
2238 }
2239 }),
2240
2241 /**
2242 * Sets the network request get params shown in this view.
2243 *
2244 * @param string aUrl
2245 * The request's url.
2246 */
2247 _setRequestGetParams: function(aUrl) {
2248 let query = nsIURL(aUrl).query;
2249 if (query) {
2250 this._addParams(this._paramsQueryString, query);
2251 }
2252 },
2253
2254 /**
2255 * Sets the network request post params shown in this view.
2256 *
2257 * @param object aHeadersResponse
2258 * The "requestHeaders" message received from the server.
2259 * @param object aHeadersFromUploadStream
2260 * The "requestHeadersFromUploadStream" inferred from the POST payload.
2261 * @param object aPostDataResponse
2262 * The "requestPostData" message received from the server.
2263 * @return object
2264 * A promise that is resolved when the request post params are set.
2265 */
2266 _setRequestPostParams: Task.async(function*(aHeadersResponse, aHeadersFromUploadStream, aPostDataResponse) {
2267 if (!aHeadersResponse || !aHeadersFromUploadStream || !aPostDataResponse) {
2268 return;
2269 }
2270
2271 let { headers: requestHeaders } = aHeadersResponse;
2272 let { headers: payloadHeaders } = aHeadersFromUploadStream;
2273 let allHeaders = [...payloadHeaders, ...requestHeaders];
2274
2275 let contentTypeHeader = allHeaders.find(e => e.name.toLowerCase() == "content-type");
2276 let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : "";
2277 let postDataLongString = aPostDataResponse.postData.text;
2278
2279 let postData = yield gNetwork.getString(postDataLongString);
2280 let contentType = yield gNetwork.getString(contentTypeLongString);
2281
2282 // Handle query strings (e.g. "?foo=bar&baz=42").
2283 if (contentType.contains("x-www-form-urlencoded")) {
2284 for (let section of postData.split(/\r\n|\r|\n/)) {
2285 // Before displaying it, make sure this section of the POST data
2286 // isn't a line containing upload stream headers.
2287 if (payloadHeaders.every(header => !section.startsWith(header.name))) {
2288 this._addParams(this._paramsFormData, section);
2289 }
2290 }
2291 }
2292 // Handle actual forms ("multipart/form-data" content type).
2293 else {
2294 // This is really awkward, but hey, it works. Let's show an empty
2295 // scope in the params view and place the source editor containing
2296 // the raw post data directly underneath.
2297 $("#request-params-box").removeAttribute("flex");
2298 let paramsScope = this._params.addScope(this._paramsPostPayload);
2299 paramsScope.expanded = true;
2300 paramsScope.locked = true;
2301
2302 $("#request-post-data-textarea-box").hidden = false;
2303 let editor = yield NetMonitorView.editor("#request-post-data-textarea");
2304 // Most POST bodies are usually JSON, so they can be neatly
2305 // syntax highlighted as JS. Otheriwse, fall back to plain text.
2306 try {
2307 JSON.parse(postData);
2308 editor.setMode(Editor.modes.js);
2309 } catch (e) {
2310 editor.setMode(Editor.modes.text);
2311 } finally {
2312 editor.setText(postData);
2313 }
2314 }
2315
2316 window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
2317 }),
2318
2319 /**
2320 * Populates the params container in this view with the specified data.
2321 *
2322 * @param string aName
2323 * The type of params to populate (get or post).
2324 * @param string aQueryString
2325 * A query string of params (e.g. "?foo=bar&baz=42").
2326 */
2327 _addParams: function(aName, aQueryString) {
2328 let paramsArray = parseQueryString(aQueryString);
2329 if (!paramsArray) {
2330 return;
2331 }
2332 let paramsScope = this._params.addScope(aName);
2333 paramsScope.expanded = true;
2334
2335 for (let param of paramsArray) {
2336 let paramVar = paramsScope.addItem(param.name, {}, true);
2337 paramVar.setGrip(param.value);
2338 }
2339 },
2340
2341 /**
2342 * Sets the network response body shown in this view.
2343 *
2344 * @param string aUrl
2345 * The request's url.
2346 * @param object aResponse
2347 * The message received from the server.
2348 * @return object
2349 * A promise that is resolved when the response body is set.
2350 */
2351 _setResponseBody: Task.async(function*(aUrl, aResponse) {
2352 if (!aResponse) {
2353 return;
2354 }
2355 let { mimeType, text, encoding } = aResponse.content;
2356 let responseBody = yield gNetwork.getString(text);
2357
2358 // Handle json, which we tentatively identify by checking the MIME type
2359 // for "json" after any word boundary. This works for the standard
2360 // "application/json", and also for custom types like "x-bigcorp-json".
2361 // Additionally, we also directly parse the response text content to
2362 // verify whether it's json or not, to handle responses incorrectly
2363 // labeled as text/plain instead.
2364 let jsonMimeType, jsonObject, jsonObjectParseError;
2365 try {
2366 jsonMimeType = /\bjson/.test(mimeType);
2367 jsonObject = JSON.parse(responseBody);
2368 } catch (e) {
2369 jsonObjectParseError = e;
2370 }
2371 if (jsonMimeType || jsonObject) {
2372 // Extract the actual json substring in case this might be a "JSONP".
2373 // This regex basically parses a function call and captures the
2374 // function name and arguments in two separate groups.
2375 let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
2376 let [_, callbackPadding, jsonpString] = responseBody.match(jsonpRegex) || [];
2377
2378 // Make sure this is a valid JSON object first. If so, nicely display
2379 // the parsing results in a variables view. Otherwise, simply show
2380 // the contents as plain text.
2381 if (callbackPadding && jsonpString) {
2382 try {
2383 jsonObject = JSON.parse(jsonpString);
2384 } catch (e) {
2385 jsonObjectParseError = e;
2386 }
2387 }
2388
2389 // Valid JSON or JSONP.
2390 if (jsonObject) {
2391 $("#response-content-json-box").hidden = false;
2392 let jsonScopeName = callbackPadding
2393 ? L10N.getFormatStr("jsonpScopeName", callbackPadding)
2394 : L10N.getStr("jsonScopeName");
2395
2396 let jsonVar = { label: jsonScopeName, rawObject: jsonObject };
2397 yield this._json.controller.setSingleVariable(jsonVar).expanded;
2398 }
2399 // Malformed JSON.
2400 else {
2401 $("#response-content-textarea-box").hidden = false;
2402 let infoHeader = $("#response-content-info-header");
2403 infoHeader.setAttribute("value", jsonObjectParseError);
2404 infoHeader.setAttribute("tooltiptext", jsonObjectParseError);
2405 infoHeader.hidden = false;
2406
2407 let editor = yield NetMonitorView.editor("#response-content-textarea");
2408 editor.setMode(Editor.modes.js);
2409 editor.setText(responseBody);
2410 }
2411 }
2412 // Handle images.
2413 else if (mimeType.contains("image/")) {
2414 $("#response-content-image-box").setAttribute("align", "center");
2415 $("#response-content-image-box").setAttribute("pack", "center");
2416 $("#response-content-image-box").hidden = false;
2417 $("#response-content-image").src =
2418 "data:" + mimeType + ";" + encoding + "," + responseBody;
2419
2420 // Immediately display additional information about the image:
2421 // file name, mime type and encoding.
2422 $("#response-content-image-name-value").setAttribute("value", nsIURL(aUrl).fileName);
2423 $("#response-content-image-mime-value").setAttribute("value", mimeType);
2424 $("#response-content-image-encoding-value").setAttribute("value", encoding);
2425
2426 // Wait for the image to load in order to display the width and height.
2427 $("#response-content-image").onload = e => {
2428 // XUL images are majestic so they don't bother storing their dimensions
2429 // in width and height attributes like the rest of the folk. Hack around
2430 // this by getting the bounding client rect and subtracting the margins.
2431 let { width, height } = e.target.getBoundingClientRect();
2432 let dimensions = (width - 2) + " x " + (height - 2);
2433 $("#response-content-image-dimensions-value").setAttribute("value", dimensions);
2434 };
2435 }
2436 // Handle anything else.
2437 else {
2438 $("#response-content-textarea-box").hidden = false;
2439 let editor = yield NetMonitorView.editor("#response-content-textarea");
2440 editor.setMode(Editor.modes.text);
2441 editor.setText(responseBody);
2442
2443 // Maybe set a more appropriate mode in the Source Editor if possible,
2444 // but avoid doing this for very large files.
2445 if (responseBody.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
2446 let mapping = Object.keys(CONTENT_MIME_TYPE_MAPPINGS).find(key => mimeType.contains(key));
2447 if (mapping) {
2448 editor.setMode(CONTENT_MIME_TYPE_MAPPINGS[mapping]);
2449 }
2450 }
2451 }
2452
2453 window.emit(EVENTS.RESPONSE_BODY_DISPLAYED);
2454 }),
2455
2456 /**
2457 * Sets the timings information shown in this view.
2458 *
2459 * @param object aResponse
2460 * The message received from the server.
2461 */
2462 _setTimingsInformation: function(aResponse) {
2463 if (!aResponse) {
2464 return;
2465 }
2466 let { blocked, dns, connect, send, wait, receive } = aResponse.timings;
2467
2468 let tabboxWidth = $("#details-pane").getAttribute("width");
2469 let availableWidth = tabboxWidth / 2; // Other nodes also take some space.
2470 let scale = Math.max(availableWidth / aResponse.totalTime, 0);
2471
2472 $("#timings-summary-blocked .requests-menu-timings-box")
2473 .setAttribute("width", blocked * scale);
2474 $("#timings-summary-blocked .requests-menu-timings-total")
2475 .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", blocked));
2476
2477 $("#timings-summary-dns .requests-menu-timings-box")
2478 .setAttribute("width", dns * scale);
2479 $("#timings-summary-dns .requests-menu-timings-total")
2480 .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", dns));
2481
2482 $("#timings-summary-connect .requests-menu-timings-box")
2483 .setAttribute("width", connect * scale);
2484 $("#timings-summary-connect .requests-menu-timings-total")
2485 .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", connect));
2486
2487 $("#timings-summary-send .requests-menu-timings-box")
2488 .setAttribute("width", send * scale);
2489 $("#timings-summary-send .requests-menu-timings-total")
2490 .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", send));
2491
2492 $("#timings-summary-wait .requests-menu-timings-box")
2493 .setAttribute("width", wait * scale);
2494 $("#timings-summary-wait .requests-menu-timings-total")
2495 .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", wait));
2496
2497 $("#timings-summary-receive .requests-menu-timings-box")
2498 .setAttribute("width", receive * scale);
2499 $("#timings-summary-receive .requests-menu-timings-total")
2500 .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", receive));
2501
2502 $("#timings-summary-dns .requests-menu-timings-box")
2503 .style.transform = "translateX(" + (scale * blocked) + "px)";
2504 $("#timings-summary-connect .requests-menu-timings-box")
2505 .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
2506 $("#timings-summary-send .requests-menu-timings-box")
2507 .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)";
2508 $("#timings-summary-wait .requests-menu-timings-box")
2509 .style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
2510 $("#timings-summary-receive .requests-menu-timings-box")
2511 .style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)";
2512
2513 $("#timings-summary-dns .requests-menu-timings-total")
2514 .style.transform = "translateX(" + (scale * blocked) + "px)";
2515 $("#timings-summary-connect .requests-menu-timings-total")
2516 .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
2517 $("#timings-summary-send .requests-menu-timings-total")
2518 .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)";
2519 $("#timings-summary-wait .requests-menu-timings-total")
2520 .style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
2521 $("#timings-summary-receive .requests-menu-timings-total")
2522 .style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)";
2523 },
2524
2525 /**
2526 * Sets the preview for HTML responses shown in this view.
2527 *
2528 * @param object aResponse
2529 * The message received from the server.
2530 * @return object
2531 * A promise that is resolved when the html preview is rendered.
2532 */
2533 _setHtmlPreview: Task.async(function*(aResponse) {
2534 if (!aResponse) {
2535 return promise.resolve();
2536 }
2537 let { text } = aResponse.content;
2538 let responseBody = yield gNetwork.getString(text);
2539
2540 // Always disable JS when previewing HTML responses.
2541 let iframe = $("#response-preview");
2542 iframe.contentDocument.docShell.allowJavascript = false;
2543 iframe.contentDocument.documentElement.innerHTML = responseBody;
2544
2545 window.emit(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED);
2546 }),
2547
2548 _dataSrc: null,
2549 _headers: null,
2550 _cookies: null,
2551 _params: null,
2552 _json: null,
2553 _paramsQueryString: "",
2554 _paramsFormData: "",
2555 _paramsPostPayload: "",
2556 _requestHeaders: "",
2557 _responseHeaders: "",
2558 _requestCookies: "",
2559 _responseCookies: ""
2560 };
2561
2562 /**
2563 * Functions handling the performance statistics view.
2564 */
2565 function PerformanceStatisticsView() {
2566 }
2567
2568 PerformanceStatisticsView.prototype = {
2569 /**
2570 * Initializes and displays empty charts in this container.
2571 */
2572 displayPlaceholderCharts: function() {
2573 this._createChart({
2574 id: "#primed-cache-chart",
2575 title: "charts.cacheEnabled"
2576 });
2577 this._createChart({
2578 id: "#empty-cache-chart",
2579 title: "charts.cacheDisabled"
2580 });
2581 window.emit(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
2582 },
2583
2584 /**
2585 * Populates and displays the primed cache chart in this container.
2586 *
2587 * @param array aItems
2588 * @see this._sanitizeChartDataSource
2589 */
2590 createPrimedCacheChart: function(aItems) {
2591 this._createChart({
2592 id: "#primed-cache-chart",
2593 title: "charts.cacheEnabled",
2594 data: this._sanitizeChartDataSource(aItems),
2595 strings: this._commonChartStrings,
2596 totals: this._commonChartTotals,
2597 sorted: true
2598 });
2599 window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED);
2600 },
2601
2602 /**
2603 * Populates and displays the empty cache chart in this container.
2604 *
2605 * @param array aItems
2606 * @see this._sanitizeChartDataSource
2607 */
2608 createEmptyCacheChart: function(aItems) {
2609 this._createChart({
2610 id: "#empty-cache-chart",
2611 title: "charts.cacheDisabled",
2612 data: this._sanitizeChartDataSource(aItems, true),
2613 strings: this._commonChartStrings,
2614 totals: this._commonChartTotals,
2615 sorted: true
2616 });
2617 window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED);
2618 },
2619
2620 /**
2621 * Common stringifier predicates used for items and totals in both the
2622 * "primed" and "empty" cache charts.
2623 */
2624 _commonChartStrings: {
2625 size: value => {
2626 let string = L10N.numberWithDecimals(value / 1024, CONTENT_SIZE_DECIMALS);
2627 return L10N.getFormatStr("charts.sizeKB", string);
2628 },
2629 time: value => {
2630 let string = L10N.numberWithDecimals(value / 1000, REQUEST_TIME_DECIMALS);
2631 return L10N.getFormatStr("charts.totalS", string);
2632 }
2633 },
2634 _commonChartTotals: {
2635 size: total => {
2636 let string = L10N.numberWithDecimals(total / 1024, CONTENT_SIZE_DECIMALS);
2637 return L10N.getFormatStr("charts.totalSize", string);
2638 },
2639 time: total => {
2640 let seconds = total / 1000;
2641 let string = L10N.numberWithDecimals(seconds, REQUEST_TIME_DECIMALS);
2642 return PluralForm.get(seconds, L10N.getStr("charts.totalSeconds")).replace("#1", string);
2643 },
2644 cached: total => {
2645 return L10N.getFormatStr("charts.totalCached", total);
2646 },
2647 count: total => {
2648 return L10N.getFormatStr("charts.totalCount", total);
2649 }
2650 },
2651
2652 /**
2653 * Adds a specific chart to this container.
2654 *
2655 * @param object
2656 * An object containing all or some the following properties:
2657 * - id: either "#primed-cache-chart" or "#empty-cache-chart"
2658 * - title/data/strings/totals/sorted: @see Chart.jsm for details
2659 */
2660 _createChart: function({ id, title, data, strings, totals, sorted }) {
2661 let container = $(id);
2662
2663 // Nuke all existing charts of the specified type.
2664 while (container.hasChildNodes()) {
2665 container.firstChild.remove();
2666 }
2667
2668 // Create a new chart.
2669 let chart = Chart.PieTable(document, {
2670 diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
2671 title: L10N.getStr(title),
2672 data: data,
2673 strings: strings,
2674 totals: totals,
2675 sorted: sorted
2676 });
2677
2678 chart.on("click", (_, item) => {
2679 NetMonitorView.RequestsMenu.filterOnlyOn(item.label);
2680 NetMonitorView.showNetworkInspectorView();
2681 });
2682
2683 container.appendChild(chart.node);
2684 },
2685
2686 /**
2687 * Sanitizes the data source used for creating charts, to follow the
2688 * data format spec defined in Chart.jsm.
2689 *
2690 * @param array aItems
2691 * A collection of request items used as the data source for the chart.
2692 * @param boolean aEmptyCache
2693 * True if the cache is considered enabled, false for disabled.
2694 */
2695 _sanitizeChartDataSource: function(aItems, aEmptyCache) {
2696 let data = [
2697 "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "other"
2698 ].map(e => ({
2699 cached: 0,
2700 count: 0,
2701 label: e,
2702 size: 0,
2703 time: 0
2704 }));
2705
2706 for (let requestItem of aItems) {
2707 let details = requestItem.attachment;
2708 let type;
2709
2710 if (RequestsMenuView.prototype.isHtml(requestItem)) {
2711 type = 0; // "html"
2712 } else if (RequestsMenuView.prototype.isCss(requestItem)) {
2713 type = 1; // "css"
2714 } else if (RequestsMenuView.prototype.isJs(requestItem)) {
2715 type = 2; // "js"
2716 } else if (RequestsMenuView.prototype.isFont(requestItem)) {
2717 type = 4; // "fonts"
2718 } else if (RequestsMenuView.prototype.isImage(requestItem)) {
2719 type = 5; // "images"
2720 } else if (RequestsMenuView.prototype.isMedia(requestItem)) {
2721 type = 6; // "media"
2722 } else if (RequestsMenuView.prototype.isFlash(requestItem)) {
2723 type = 7; // "flash"
2724 } else if (RequestsMenuView.prototype.isXHR(requestItem)) {
2725 // Verify XHR last, to categorize other mime types in their own blobs.
2726 type = 3; // "xhr"
2727 } else {
2728 type = 8; // "other"
2729 }
2730
2731 if (aEmptyCache || !responseIsFresh(details)) {
2732 data[type].time += details.totalTime || 0;
2733 data[type].size += details.contentSize || 0;
2734 } else {
2735 data[type].cached++;
2736 }
2737 data[type].count++;
2738 }
2739
2740 return data.filter(e => e.count > 0);
2741 },
2742 };
2743
2744 /**
2745 * DOM query helper.
2746 */
2747 function $(aSelector, aTarget = document) aTarget.querySelector(aSelector);
2748 function $all(aSelector, aTarget = document) aTarget.querySelectorAll(aSelector);
2749
2750 /**
2751 * Helper for getting an nsIURL instance out of a string.
2752 */
2753 function nsIURL(aUrl, aStore = nsIURL.store) {
2754 if (aStore.has(aUrl)) {
2755 return aStore.get(aUrl);
2756 }
2757 let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
2758 aStore.set(aUrl, uri);
2759 return uri;
2760 }
2761 nsIURL.store = new Map();
2762
2763 /**
2764 * Parse a url's query string into its components
2765 *
2766 * @param string aQueryString
2767 * The query part of a url
2768 * @return array
2769 * Array of query params {name, value}
2770 */
2771 function parseQueryString(aQueryString) {
2772 // Make sure there's at least one param available.
2773 // Be careful here, params don't necessarily need to have values, so
2774 // no need to verify the existence of a "=".
2775 if (!aQueryString) {
2776 return;
2777 }
2778 // Turn the params string into an array containing { name: value } tuples.
2779 let paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map(e =>
2780 let (param = e.split("=")) {
2781 name: param[0] ? NetworkHelper.convertToUnicode(unescape(param[0])) : "",
2782 value: param[1] ? NetworkHelper.convertToUnicode(unescape(param[1])) : ""
2783 });
2784 return paramsArray;
2785 }
2786
2787 /**
2788 * Parse text representation of multiple HTTP headers.
2789 *
2790 * @param string aText
2791 * Text of headers
2792 * @return array
2793 * Array of headers info {name, value}
2794 */
2795 function parseHeadersText(aText) {
2796 return parseRequestText(aText, "\\S+?", ":");
2797 }
2798
2799 /**
2800 * Parse readable text list of a query string.
2801 *
2802 * @param string aText
2803 * Text of query string represetation
2804 * @return array
2805 * Array of query params {name, value}
2806 */
2807 function parseQueryText(aText) {
2808 return parseRequestText(aText, ".+?", "=");
2809 }
2810
2811 /**
2812 * Parse a text representation of a name[divider]value list with
2813 * the given name regex and divider character.
2814 *
2815 * @param string aText
2816 * Text of list
2817 * @return array
2818 * Array of headers info {name, value}
2819 */
2820 function parseRequestText(aText, aName, aDivider) {
2821 let regex = new RegExp("(" + aName + ")\\" + aDivider + "\\s*(.+)");
2822 let pairs = [];
2823 for (let line of aText.split("\n")) {
2824 let matches;
2825 if (matches = regex.exec(line)) {
2826 let [, name, value] = matches;
2827 pairs.push({name: name, value: value});
2828 }
2829 }
2830 return pairs;
2831 }
2832
2833 /**
2834 * Write out a list of headers into a chunk of text
2835 *
2836 * @param array aHeaders
2837 * Array of headers info {name, value}
2838 * @return string aText
2839 * List of headers in text format
2840 */
2841 function writeHeaderText(aHeaders) {
2842 return [(name + ": " + value) for ({name, value} of aHeaders)].join("\n");
2843 }
2844
2845 /**
2846 * Write out a list of query params into a chunk of text
2847 *
2848 * @param array aParams
2849 * Array of query params {name, value}
2850 * @return string
2851 * List of query params in text format
2852 */
2853 function writeQueryText(aParams) {
2854 return [(name + "=" + value) for ({name, value} of aParams)].join("\n");
2855 }
2856
2857 /**
2858 * Write out a list of query params into a query string
2859 *
2860 * @param array aParams
2861 * Array of query params {name, value}
2862 * @return string
2863 * Query string that can be appended to a url.
2864 */
2865 function writeQueryString(aParams) {
2866 return [(name + "=" + value) for ({name, value} of aParams)].join("&");
2867 }
2868
2869 /**
2870 * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
2871 * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
2872 *
2873 * @param object
2874 * An object containing the { responseHeaders, status } properties.
2875 * @return boolean
2876 * True if the response is fresh and loaded from cache.
2877 */
2878 function responseIsFresh({ responseHeaders, status }) {
2879 // Check for a "304 Not Modified" status and response headers availability.
2880 if (status != 304 || !responseHeaders) {
2881 return false;
2882 }
2883
2884 let list = responseHeaders.headers;
2885 let cacheControl = list.filter(e => e.name.toLowerCase() == "cache-control")[0];
2886 let expires = list.filter(e => e.name.toLowerCase() == "expires")[0];
2887
2888 // Check the "Cache-Control" header for a maximum age value.
2889 if (cacheControl) {
2890 let maxAgeMatch =
2891 cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
2892 cacheControl.value.match(/max-age\s*=\s*(\d+)/);
2893
2894 if (maxAgeMatch && maxAgeMatch.pop() > 0) {
2895 return true;
2896 }
2897 }
2898
2899 // Check the "Expires" header for a valid date.
2900 if (expires && Date.parse(expires.value)) {
2901 return true;
2902 }
2903
2904 return false;
2905 }
2906
2907 /**
2908 * Helper method to get a wrapped function which can be bound to as an event listener directly and is executed only when data-key is present in event.target.
2909 *
2910 * @param function callback
2911 * Function to execute execute when data-key is present in event.target.
2912 * @return function
2913 * Wrapped function with the target data-key as the first argument.
2914 */
2915 function getKeyWithEvent(callback) {
2916 return function(event) {
2917 var key = event.target.getAttribute("data-key");
2918 if (key) {
2919 callback.call(null, key);
2920 }
2921 };
2922 }
2923
2924 /**
2925 * Preliminary setup for the NetMonitorView object.
2926 */
2927 NetMonitorView.Toolbar = new ToolbarView();
2928 NetMonitorView.RequestsMenu = new RequestsMenuView();
2929 NetMonitorView.Sidebar = new SidebarView();
2930 NetMonitorView.CustomRequest = new CustomRequestView();
2931 NetMonitorView.NetworkDetails = new NetworkDetailsView();
2932 NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();

mercurial