browser/devtools/netmonitor/netmonitor-view.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial