1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/netmonitor/netmonitor-view.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,2932 @@ 1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 +"use strict"; 1.10 + 1.11 +const HTML_NS = "http://www.w3.org/1999/xhtml"; 1.12 +const EPSILON = 0.001; 1.13 +const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400; // 100 KB in bytes 1.14 +const RESIZE_REFRESH_RATE = 50; // ms 1.15 +const REQUESTS_REFRESH_RATE = 50; // ms 1.16 +const REQUESTS_HEADERS_SAFE_BOUNDS = 30; // px 1.17 +const REQUESTS_TOOLTIP_POSITION = "topcenter bottomleft"; 1.18 +const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px 1.19 +const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // px 1.20 +const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms 1.21 +const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px 1.22 +const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms 1.23 +const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3; 1.24 +const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px 1.25 +const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144]; 1.26 +const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte 1.27 +const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte 1.28 +const DEFAULT_HTTP_VERSION = "HTTP/1.1"; 1.29 +const REQUEST_TIME_DECIMALS = 2; 1.30 +const HEADERS_SIZE_DECIMALS = 3; 1.31 +const CONTENT_SIZE_DECIMALS = 2; 1.32 +const CONTENT_MIME_TYPE_ABBREVIATIONS = { 1.33 + "ecmascript": "js", 1.34 + "javascript": "js", 1.35 + "x-javascript": "js" 1.36 +}; 1.37 +const CONTENT_MIME_TYPE_MAPPINGS = { 1.38 + "/ecmascript": Editor.modes.js, 1.39 + "/javascript": Editor.modes.js, 1.40 + "/x-javascript": Editor.modes.js, 1.41 + "/html": Editor.modes.html, 1.42 + "/xhtml": Editor.modes.html, 1.43 + "/xml": Editor.modes.html, 1.44 + "/atom": Editor.modes.html, 1.45 + "/soap": Editor.modes.html, 1.46 + "/rdf": Editor.modes.css, 1.47 + "/rss": Editor.modes.css, 1.48 + "/css": Editor.modes.css 1.49 +}; 1.50 +const DEFAULT_EDITOR_CONFIG = { 1.51 + mode: Editor.modes.text, 1.52 + readOnly: true, 1.53 + lineNumbers: true 1.54 +}; 1.55 +const GENERIC_VARIABLES_VIEW_SETTINGS = { 1.56 + lazyEmpty: true, 1.57 + lazyEmptyDelay: 10, // ms 1.58 + searchEnabled: true, 1.59 + editableValueTooltip: "", 1.60 + editableNameTooltip: "", 1.61 + preventDisableOnChange: true, 1.62 + preventDescriptorModifiers: true, 1.63 + eval: () => {} 1.64 +}; 1.65 +const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px 1.66 + 1.67 +/** 1.68 + * Object defining the network monitor view components. 1.69 + */ 1.70 +let NetMonitorView = { 1.71 + /** 1.72 + * Initializes the network monitor view. 1.73 + */ 1.74 + initialize: function() { 1.75 + this._initializePanes(); 1.76 + 1.77 + this.Toolbar.initialize(); 1.78 + this.RequestsMenu.initialize(); 1.79 + this.NetworkDetails.initialize(); 1.80 + this.CustomRequest.initialize(); 1.81 + }, 1.82 + 1.83 + /** 1.84 + * Destroys the network monitor view. 1.85 + */ 1.86 + destroy: function() { 1.87 + this.Toolbar.destroy(); 1.88 + this.RequestsMenu.destroy(); 1.89 + this.NetworkDetails.destroy(); 1.90 + this.CustomRequest.destroy(); 1.91 + 1.92 + this._destroyPanes(); 1.93 + }, 1.94 + 1.95 + /** 1.96 + * Initializes the UI for all the displayed panes. 1.97 + */ 1.98 + _initializePanes: function() { 1.99 + dumpn("Initializing the NetMonitorView panes"); 1.100 + 1.101 + this._body = $("#body"); 1.102 + this._detailsPane = $("#details-pane"); 1.103 + this._detailsPaneToggleButton = $("#details-pane-toggle"); 1.104 + 1.105 + this._collapsePaneString = L10N.getStr("collapseDetailsPane"); 1.106 + this._expandPaneString = L10N.getStr("expandDetailsPane"); 1.107 + 1.108 + this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth); 1.109 + this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight); 1.110 + this.toggleDetailsPane({ visible: false }); 1.111 + 1.112 + // Disable the performance statistics mode. 1.113 + if (!Prefs.statistics) { 1.114 + $("#request-menu-context-perf").hidden = true; 1.115 + $("#notice-perf-message").hidden = true; 1.116 + $("#requests-menu-network-summary-button").hidden = true; 1.117 + $("#requests-menu-network-summary-label").hidden = true; 1.118 + } 1.119 + }, 1.120 + 1.121 + /** 1.122 + * Destroys the UI for all the displayed panes. 1.123 + */ 1.124 + _destroyPanes: function() { 1.125 + dumpn("Destroying the NetMonitorView panes"); 1.126 + 1.127 + Prefs.networkDetailsWidth = this._detailsPane.getAttribute("width"); 1.128 + Prefs.networkDetailsHeight = this._detailsPane.getAttribute("height"); 1.129 + 1.130 + this._detailsPane = null; 1.131 + this._detailsPaneToggleButton = null; 1.132 + }, 1.133 + 1.134 + /** 1.135 + * Gets the visibility state of the network details pane. 1.136 + * @return boolean 1.137 + */ 1.138 + get detailsPaneHidden() { 1.139 + return this._detailsPane.hasAttribute("pane-collapsed"); 1.140 + }, 1.141 + 1.142 + /** 1.143 + * Sets the network details pane hidden or visible. 1.144 + * 1.145 + * @param object aFlags 1.146 + * An object containing some of the following properties: 1.147 + * - visible: true if the pane should be shown, false to hide 1.148 + * - animated: true to display an animation on toggle 1.149 + * - delayed: true to wait a few cycles before toggle 1.150 + * - callback: a function to invoke when the toggle finishes 1.151 + * @param number aTabIndex [optional] 1.152 + * The index of the intended selected tab in the details pane. 1.153 + */ 1.154 + toggleDetailsPane: function(aFlags, aTabIndex) { 1.155 + let pane = this._detailsPane; 1.156 + let button = this._detailsPaneToggleButton; 1.157 + 1.158 + ViewHelpers.togglePane(aFlags, pane); 1.159 + 1.160 + if (aFlags.visible) { 1.161 + this._body.removeAttribute("pane-collapsed"); 1.162 + button.removeAttribute("pane-collapsed"); 1.163 + button.setAttribute("tooltiptext", this._collapsePaneString); 1.164 + } else { 1.165 + this._body.setAttribute("pane-collapsed", ""); 1.166 + button.setAttribute("pane-collapsed", ""); 1.167 + button.setAttribute("tooltiptext", this._expandPaneString); 1.168 + } 1.169 + 1.170 + if (aTabIndex !== undefined) { 1.171 + $("#event-details-pane").selectedIndex = aTabIndex; 1.172 + } 1.173 + }, 1.174 + 1.175 + /** 1.176 + * Gets the current mode for this tool. 1.177 + * @return string (e.g, "network-inspector-view" or "network-statistics-view") 1.178 + */ 1.179 + get currentFrontendMode() { 1.180 + return this._body.selectedPanel.id; 1.181 + }, 1.182 + 1.183 + /** 1.184 + * Toggles between the frontend view modes ("Inspector" vs. "Statistics"). 1.185 + */ 1.186 + toggleFrontendMode: function() { 1.187 + if (this.currentFrontendMode != "network-inspector-view") { 1.188 + this.showNetworkInspectorView(); 1.189 + } else { 1.190 + this.showNetworkStatisticsView(); 1.191 + } 1.192 + }, 1.193 + 1.194 + /** 1.195 + * Switches to the "Inspector" frontend view mode. 1.196 + */ 1.197 + showNetworkInspectorView: function() { 1.198 + this._body.selectedPanel = $("#network-inspector-view"); 1.199 + this.RequestsMenu._flushWaterfallViews(true); 1.200 + }, 1.201 + 1.202 + /** 1.203 + * Switches to the "Statistics" frontend view mode. 1.204 + */ 1.205 + showNetworkStatisticsView: function() { 1.206 + this._body.selectedPanel = $("#network-statistics-view"); 1.207 + 1.208 + let controller = NetMonitorController; 1.209 + let requestsView = this.RequestsMenu; 1.210 + let statisticsView = this.PerformanceStatistics; 1.211 + 1.212 + Task.spawn(function*() { 1.213 + statisticsView.displayPlaceholderCharts(); 1.214 + yield controller.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED); 1.215 + 1.216 + try { 1.217 + // • The response headers and status code are required for determining 1.218 + // whether a response is "fresh" (cacheable). 1.219 + // • The response content size and request total time are necessary for 1.220 + // populating the statistics view. 1.221 + // • The response mime type is used for categorization. 1.222 + yield whenDataAvailable(requestsView.attachments, [ 1.223 + "responseHeaders", "status", "contentSize", "mimeType", "totalTime" 1.224 + ]); 1.225 + } catch (ex) { 1.226 + // Timed out while waiting for data. Continue with what we have. 1.227 + DevToolsUtils.reportException("showNetworkStatisticsView", ex); 1.228 + } 1.229 + 1.230 + statisticsView.createPrimedCacheChart(requestsView.items); 1.231 + statisticsView.createEmptyCacheChart(requestsView.items); 1.232 + }); 1.233 + }, 1.234 + 1.235 + /** 1.236 + * Lazily initializes and returns a promise for a Editor instance. 1.237 + * 1.238 + * @param string aId 1.239 + * The id of the editor placeholder node. 1.240 + * @return object 1.241 + * A promise that is resolved when the editor is available. 1.242 + */ 1.243 + editor: function(aId) { 1.244 + dumpn("Getting a NetMonitorView editor: " + aId); 1.245 + 1.246 + if (this._editorPromises.has(aId)) { 1.247 + return this._editorPromises.get(aId); 1.248 + } 1.249 + 1.250 + let deferred = promise.defer(); 1.251 + this._editorPromises.set(aId, deferred.promise); 1.252 + 1.253 + // Initialize the source editor and store the newly created instance 1.254 + // in the ether of a resolved promise's value. 1.255 + let editor = new Editor(DEFAULT_EDITOR_CONFIG); 1.256 + editor.appendTo($(aId)).then(() => deferred.resolve(editor)); 1.257 + 1.258 + return deferred.promise; 1.259 + }, 1.260 + 1.261 + _body: null, 1.262 + _detailsPane: null, 1.263 + _detailsPaneToggleButton: null, 1.264 + _collapsePaneString: "", 1.265 + _expandPaneString: "", 1.266 + _editorPromises: new Map() 1.267 +}; 1.268 + 1.269 +/** 1.270 + * Functions handling the toolbar view: expand/collapse button etc. 1.271 + */ 1.272 +function ToolbarView() { 1.273 + dumpn("ToolbarView was instantiated"); 1.274 + 1.275 + this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this); 1.276 +} 1.277 + 1.278 +ToolbarView.prototype = { 1.279 + /** 1.280 + * Initialization function, called when the debugger is started. 1.281 + */ 1.282 + initialize: function() { 1.283 + dumpn("Initializing the ToolbarView"); 1.284 + 1.285 + this._detailsPaneToggleButton = $("#details-pane-toggle"); 1.286 + this._detailsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false); 1.287 + }, 1.288 + 1.289 + /** 1.290 + * Destruction function, called when the debugger is closed. 1.291 + */ 1.292 + destroy: function() { 1.293 + dumpn("Destroying the ToolbarView"); 1.294 + 1.295 + this._detailsPaneToggleButton.removeEventListener("mousedown", this._onTogglePanesPressed, false); 1.296 + }, 1.297 + 1.298 + /** 1.299 + * Listener handling the toggle button click event. 1.300 + */ 1.301 + _onTogglePanesPressed: function() { 1.302 + let requestsMenu = NetMonitorView.RequestsMenu; 1.303 + let selectedIndex = requestsMenu.selectedIndex; 1.304 + 1.305 + // Make sure there's a selection if the button is pressed, to avoid 1.306 + // showing an empty network details pane. 1.307 + if (selectedIndex == -1 && requestsMenu.itemCount) { 1.308 + requestsMenu.selectedIndex = 0; 1.309 + } else { 1.310 + requestsMenu.selectedIndex = -1; 1.311 + } 1.312 + }, 1.313 + 1.314 + _detailsPaneToggleButton: null 1.315 +}; 1.316 + 1.317 +/** 1.318 + * Functions handling the requests menu (containing details about each request, 1.319 + * like status, method, file, domain, as well as a waterfall representing 1.320 + * timing imformation). 1.321 + */ 1.322 +function RequestsMenuView() { 1.323 + dumpn("RequestsMenuView was instantiated"); 1.324 + 1.325 + this._flushRequests = this._flushRequests.bind(this); 1.326 + this._onHover = this._onHover.bind(this); 1.327 + this._onSelect = this._onSelect.bind(this); 1.328 + this._onSwap = this._onSwap.bind(this); 1.329 + this._onResize = this._onResize.bind(this); 1.330 + this._byFile = this._byFile.bind(this); 1.331 + this._byDomain = this._byDomain.bind(this); 1.332 + this._byType = this._byType.bind(this); 1.333 +} 1.334 + 1.335 +RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { 1.336 + /** 1.337 + * Initialization function, called when the network monitor is started. 1.338 + */ 1.339 + initialize: function() { 1.340 + dumpn("Initializing the RequestsMenuView"); 1.341 + 1.342 + this.widget = new SideMenuWidget($("#requests-menu-contents")); 1.343 + this._splitter = $("#network-inspector-view-splitter"); 1.344 + this._summary = $("#requests-menu-network-summary-label"); 1.345 + this._summary.setAttribute("value", L10N.getStr("networkMenu.empty")); 1.346 + 1.347 + Prefs.filters.forEach(type => this.filterOn(type)); 1.348 + this.sortContents(this._byTiming); 1.349 + 1.350 + this.allowFocusOnRightClick = true; 1.351 + this.maintainSelectionVisible = true; 1.352 + this.widget.autoscrollWithAppendedItems = true; 1.353 + 1.354 + this.widget.addEventListener("select", this._onSelect, false); 1.355 + this.widget.addEventListener("swap", this._onSwap, false); 1.356 + this._splitter.addEventListener("mousemove", this._onResize, false); 1.357 + window.addEventListener("resize", this._onResize, false); 1.358 + 1.359 + this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this)); 1.360 + this.requestsMenuFilterEvent = getKeyWithEvent(this.filterOn.bind(this)); 1.361 + this.reqeustsMenuClearEvent = this.clear.bind(this); 1.362 + this._onContextShowing = this._onContextShowing.bind(this); 1.363 + this._onContextNewTabCommand = this.openRequestInTab.bind(this); 1.364 + this._onContextCopyUrlCommand = this.copyUrl.bind(this); 1.365 + this._onContextCopyImageAsDataUriCommand = this.copyImageAsDataUri.bind(this); 1.366 + this._onContextResendCommand = this.cloneSelectedRequest.bind(this); 1.367 + this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode(); 1.368 + 1.369 + this.sendCustomRequestEvent = this.sendCustomRequest.bind(this); 1.370 + this.closeCustomRequestEvent = this.closeCustomRequest.bind(this); 1.371 + this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this); 1.372 + 1.373 + $("#toolbar-labels").addEventListener("click", this.requestsMenuSortEvent, false); 1.374 + $("#requests-menu-footer").addEventListener("click", this.requestsMenuFilterEvent, false); 1.375 + $("#requests-menu-clear-button").addEventListener("click", this.reqeustsMenuClearEvent, false); 1.376 + $("#network-request-popup").addEventListener("popupshowing", this._onContextShowing, false); 1.377 + $("#request-menu-context-newtab").addEventListener("command", this._onContextNewTabCommand, false); 1.378 + $("#request-menu-context-copy-url").addEventListener("command", this._onContextCopyUrlCommand, false); 1.379 + $("#request-menu-context-copy-image-as-data-uri").addEventListener("command", this._onContextCopyImageAsDataUriCommand, false); 1.380 + 1.381 + window.once("connected", this._onConnect.bind(this)); 1.382 + }, 1.383 + 1.384 + _onConnect: function() { 1.385 + if (NetMonitorController.supportsCustomRequest) { 1.386 + $("#request-menu-context-resend").addEventListener("command", this._onContextResendCommand, false); 1.387 + $("#custom-request-send-button").addEventListener("click", this.sendCustomRequestEvent, false); 1.388 + $("#custom-request-close-button").addEventListener("click", this.closeCustomRequestEvent, false); 1.389 + $("#headers-summary-resend").addEventListener("click", this.cloneSelectedRequestEvent, false); 1.390 + } else { 1.391 + $("#request-menu-context-resend").hidden = true; 1.392 + $("#headers-summary-resend").hidden = true; 1.393 + } 1.394 + 1.395 + if (NetMonitorController.supportsPerfStats) { 1.396 + $("#request-menu-context-perf").addEventListener("command", this._onContextPerfCommand, false); 1.397 + $("#requests-menu-perf-notice-button").addEventListener("command", this._onContextPerfCommand, false); 1.398 + $("#requests-menu-network-summary-button").addEventListener("command", this._onContextPerfCommand, false); 1.399 + $("#requests-menu-network-summary-label").addEventListener("click", this._onContextPerfCommand, false); 1.400 + $("#network-statistics-back-button").addEventListener("command", this._onContextPerfCommand, false); 1.401 + } else { 1.402 + $("#notice-perf-message").hidden = true; 1.403 + $("#request-menu-context-perf").hidden = true; 1.404 + $("#requests-menu-network-summary-button").hidden = true; 1.405 + $("#requests-menu-network-summary-label").hidden = true; 1.406 + } 1.407 + }, 1.408 + 1.409 + /** 1.410 + * Destruction function, called when the network monitor is closed. 1.411 + */ 1.412 + destroy: function() { 1.413 + dumpn("Destroying the SourcesView"); 1.414 + 1.415 + Prefs.filters = this._activeFilters; 1.416 + 1.417 + this.widget.removeEventListener("select", this._onSelect, false); 1.418 + this.widget.removeEventListener("swap", this._onSwap, false); 1.419 + this._splitter.removeEventListener("mousemove", this._onResize, false); 1.420 + window.removeEventListener("resize", this._onResize, false); 1.421 + 1.422 + $("#toolbar-labels").removeEventListener("click", this.requestsMenuSortEvent, false); 1.423 + $("#requests-menu-footer").removeEventListener("click", this.requestsMenuFilterEvent, false); 1.424 + $("#requests-menu-clear-button").removeEventListener("click", this.reqeustsMenuClearEvent, false); 1.425 + $("#network-request-popup").removeEventListener("popupshowing", this._onContextShowing, false); 1.426 + $("#request-menu-context-newtab").removeEventListener("command", this._onContextNewTabCommand, false); 1.427 + $("#request-menu-context-copy-url").removeEventListener("command", this._onContextCopyUrlCommand, false); 1.428 + $("#request-menu-context-copy-image-as-data-uri").removeEventListener("command", this._onContextCopyImageAsDataUriCommand, false); 1.429 + $("#request-menu-context-resend").removeEventListener("command", this._onContextResendCommand, false); 1.430 + $("#request-menu-context-perf").removeEventListener("command", this._onContextPerfCommand, false); 1.431 + 1.432 + $("#requests-menu-perf-notice-button").removeEventListener("command", this._onContextPerfCommand, false); 1.433 + $("#requests-menu-network-summary-button").removeEventListener("command", this._onContextPerfCommand, false); 1.434 + $("#requests-menu-network-summary-label").removeEventListener("click", this._onContextPerfCommand, false); 1.435 + $("#network-statistics-back-button").removeEventListener("command", this._onContextPerfCommand, false); 1.436 + 1.437 + $("#custom-request-send-button").removeEventListener("click", this.sendCustomRequestEvent, false); 1.438 + $("#custom-request-close-button").removeEventListener("click", this.closeCustomRequestEvent, false); 1.439 + $("#headers-summary-resend").removeEventListener("click", this.cloneSelectedRequestEvent, false); 1.440 + }, 1.441 + 1.442 + /** 1.443 + * Resets this container (removes all the networking information). 1.444 + */ 1.445 + reset: function() { 1.446 + this.empty(); 1.447 + this._firstRequestStartedMillis = -1; 1.448 + this._lastRequestEndedMillis = -1; 1.449 + }, 1.450 + 1.451 + /** 1.452 + * Specifies if this view may be updated lazily. 1.453 + */ 1.454 + lazyUpdate: true, 1.455 + 1.456 + /** 1.457 + * Adds a network request to this container. 1.458 + * 1.459 + * @param string aId 1.460 + * An identifier coming from the network monitor controller. 1.461 + * @param string aStartedDateTime 1.462 + * A string representation of when the request was started, which 1.463 + * can be parsed by Date (for example "2012-09-17T19:50:03.699Z"). 1.464 + * @param string aMethod 1.465 + * Specifies the request method (e.g. "GET", "POST", etc.) 1.466 + * @param string aUrl 1.467 + * Specifies the request's url. 1.468 + * @param boolean aIsXHR 1.469 + * True if this request was initiated via XHR. 1.470 + */ 1.471 + addRequest: function(aId, aStartedDateTime, aMethod, aUrl, aIsXHR) { 1.472 + // Convert the received date/time string to a unix timestamp. 1.473 + let unixTime = Date.parse(aStartedDateTime); 1.474 + 1.475 + // Create the element node for the network request item. 1.476 + let menuView = this._createMenuView(aMethod, aUrl); 1.477 + 1.478 + // Remember the first and last event boundaries. 1.479 + this._registerFirstRequestStart(unixTime); 1.480 + this._registerLastRequestEnd(unixTime); 1.481 + 1.482 + // Append a network request item to this container. 1.483 + let requestItem = this.push([menuView, aId], { 1.484 + attachment: { 1.485 + startedDeltaMillis: unixTime - this._firstRequestStartedMillis, 1.486 + startedMillis: unixTime, 1.487 + method: aMethod, 1.488 + url: aUrl, 1.489 + isXHR: aIsXHR 1.490 + } 1.491 + }); 1.492 + 1.493 + // Create a tooltip for the newly appended network request item. 1.494 + let requestTooltip = requestItem.attachment.tooltip = new Tooltip(document, { 1.495 + closeOnEvents: [{ 1.496 + emitter: $("#requests-menu-contents"), 1.497 + event: "scroll", 1.498 + useCapture: true 1.499 + }] 1.500 + }); 1.501 + 1.502 + $("#details-pane-toggle").disabled = false; 1.503 + $("#requests-menu-empty-notice").hidden = true; 1.504 + 1.505 + this.refreshSummary(); 1.506 + this.refreshZebra(); 1.507 + this.refreshTooltip(requestItem); 1.508 + 1.509 + if (aId == this._preferredItemId) { 1.510 + this.selectedItem = requestItem; 1.511 + } 1.512 + }, 1.513 + 1.514 + /** 1.515 + * Opens selected item in a new tab. 1.516 + */ 1.517 + openRequestInTab: function() { 1.518 + let win = Services.wm.getMostRecentWindow("navigator:browser"); 1.519 + let selected = this.selectedItem.attachment; 1.520 + win.openUILinkIn(selected.url, "tab", { relatedToCurrent: true }); 1.521 + }, 1.522 + 1.523 + /** 1.524 + * Copy the request url from the currently selected item. 1.525 + */ 1.526 + copyUrl: function() { 1.527 + let selected = this.selectedItem.attachment; 1.528 + clipboardHelper.copyString(selected.url, document); 1.529 + }, 1.530 + 1.531 + /** 1.532 + * Copy a cURL command from the currently selected item. 1.533 + */ 1.534 + copyAsCurl: function() { 1.535 + let selected = this.selectedItem.attachment; 1.536 + 1.537 + Task.spawn(function*() { 1.538 + // Create a sanitized object for the Curl command generator. 1.539 + let data = { 1.540 + url: selected.url, 1.541 + method: selected.method, 1.542 + headers: [], 1.543 + httpVersion: selected.httpVersion, 1.544 + postDataText: null 1.545 + }; 1.546 + 1.547 + // Fetch header values. 1.548 + for (let { name, value } of selected.requestHeaders.headers) { 1.549 + let text = yield gNetwork.getString(value); 1.550 + data.headers.push({ name: name, value: text }); 1.551 + } 1.552 + 1.553 + // Fetch the request payload. 1.554 + if (selected.requestPostData) { 1.555 + let postData = selected.requestPostData.postData.text; 1.556 + data.postDataText = yield gNetwork.getString(postData); 1.557 + } 1.558 + 1.559 + clipboardHelper.copyString(Curl.generateCommand(data), document); 1.560 + }); 1.561 + }, 1.562 + 1.563 + /** 1.564 + * Copy image as data uri. 1.565 + */ 1.566 + copyImageAsDataUri: function() { 1.567 + let selected = this.selectedItem.attachment; 1.568 + let { mimeType, text, encoding } = selected.responseContent.content; 1.569 + 1.570 + gNetwork.getString(text).then(aString => { 1.571 + let data = "data:" + mimeType + ";" + encoding + "," + aString; 1.572 + clipboardHelper.copyString(data, document); 1.573 + }); 1.574 + }, 1.575 + 1.576 + /** 1.577 + * Create a new custom request form populated with the data from 1.578 + * the currently selected request. 1.579 + */ 1.580 + cloneSelectedRequest: function() { 1.581 + let selected = this.selectedItem.attachment; 1.582 + 1.583 + // Create the element node for the network request item. 1.584 + let menuView = this._createMenuView(selected.method, selected.url); 1.585 + 1.586 + // Append a network request item to this container. 1.587 + let newItem = this.push([menuView], { 1.588 + attachment: Object.create(selected, { 1.589 + isCustom: { value: true } 1.590 + }) 1.591 + }); 1.592 + 1.593 + // Immediately switch to new request pane. 1.594 + this.selectedItem = newItem; 1.595 + }, 1.596 + 1.597 + /** 1.598 + * Send a new HTTP request using the data in the custom request form. 1.599 + */ 1.600 + sendCustomRequest: function() { 1.601 + let selected = this.selectedItem.attachment; 1.602 + 1.603 + let data = { 1.604 + url: selected.url, 1.605 + method: selected.method, 1.606 + httpVersion: selected.httpVersion, 1.607 + }; 1.608 + if (selected.requestHeaders) { 1.609 + data.headers = selected.requestHeaders.headers; 1.610 + } 1.611 + if (selected.requestPostData) { 1.612 + data.body = selected.requestPostData.postData.text; 1.613 + } 1.614 + 1.615 + NetMonitorController.webConsoleClient.sendHTTPRequest(data, aResponse => { 1.616 + let id = aResponse.eventActor.actor; 1.617 + this._preferredItemId = id; 1.618 + }); 1.619 + 1.620 + this.closeCustomRequest(); 1.621 + }, 1.622 + 1.623 + /** 1.624 + * Remove the currently selected custom request. 1.625 + */ 1.626 + closeCustomRequest: function() { 1.627 + this.remove(this.selectedItem); 1.628 + NetMonitorView.Sidebar.toggle(false); 1.629 + }, 1.630 + 1.631 + /** 1.632 + * Filters all network requests in this container by a specified type. 1.633 + * 1.634 + * @param string aType 1.635 + * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media" 1.636 + * "flash" or "other". 1.637 + */ 1.638 + filterOn: function(aType = "all") { 1.639 + if (aType === "all") { 1.640 + // The filter "all" is special as it doesn't toggle. 1.641 + // - If some filters are selected and 'all' is clicked, the previously 1.642 + // selected filters will be disabled and 'all' is the only active one. 1.643 + // - If 'all' is already selected, do nothing. 1.644 + if (this._activeFilters.indexOf("all") !== -1) { 1.645 + return; 1.646 + } 1.647 + 1.648 + // Uncheck all other filters and select 'all'. Must create a copy as 1.649 + // _disableFilter removes the filters from the list while it's being 1.650 + // iterated. 'all' will be enabled automatically by _disableFilter once 1.651 + // the last filter is disabled. 1.652 + this._activeFilters.slice().forEach(this._disableFilter, this); 1.653 + } 1.654 + else if (this._activeFilters.indexOf(aType) === -1) { 1.655 + this._enableFilter(aType); 1.656 + } 1.657 + else { 1.658 + this._disableFilter(aType); 1.659 + } 1.660 + 1.661 + this.filterContents(this._filterPredicate); 1.662 + this.refreshSummary(); 1.663 + this.refreshZebra(); 1.664 + }, 1.665 + 1.666 + /** 1.667 + * Same as `filterOn`, except that it only allows a single type exclusively. 1.668 + * 1.669 + * @param string aType 1.670 + * @see RequestsMenuView.prototype.fitlerOn 1.671 + */ 1.672 + filterOnlyOn: function(aType = "all") { 1.673 + this._activeFilters.slice().forEach(this._disableFilter, this); 1.674 + this.filterOn(aType); 1.675 + }, 1.676 + 1.677 + /** 1.678 + * Disables the given filter, its button and toggles 'all' on if the filter to 1.679 + * be disabled is the last one active. 1.680 + * 1.681 + * @param string aType 1.682 + * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media" 1.683 + * "flash" or "other". 1.684 + */ 1.685 + _disableFilter: function (aType) { 1.686 + // Remove the filter from list of active filters. 1.687 + this._activeFilters.splice(this._activeFilters.indexOf(aType), 1); 1.688 + 1.689 + // Remove the checked status from the filter. 1.690 + let target = $("#requests-menu-filter-" + aType + "-button"); 1.691 + target.removeAttribute("checked"); 1.692 + 1.693 + // Check if the filter disabled was the last one. If so, toggle all on. 1.694 + if (this._activeFilters.length === 0) { 1.695 + this._enableFilter("all"); 1.696 + } 1.697 + }, 1.698 + 1.699 + /** 1.700 + * Enables the given filter, its button and toggles 'all' off if the filter to 1.701 + * be enabled is the first one active. 1.702 + * 1.703 + * @param string aType 1.704 + * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media" 1.705 + * "flash" or "other". 1.706 + */ 1.707 + _enableFilter: function (aType) { 1.708 + // Make sure this is a valid filter type. 1.709 + if (Object.keys(this._allFilterPredicates).indexOf(aType) == -1) { 1.710 + return; 1.711 + } 1.712 + 1.713 + // Add the filter to the list of active filters. 1.714 + this._activeFilters.push(aType); 1.715 + 1.716 + // Add the checked status to the filter button. 1.717 + let target = $("#requests-menu-filter-" + aType + "-button"); 1.718 + target.setAttribute("checked", true); 1.719 + 1.720 + // Check if 'all' was selected before. If so, disable it. 1.721 + if (aType !== "all" && this._activeFilters.indexOf("all") !== -1) { 1.722 + this._disableFilter("all"); 1.723 + } 1.724 + }, 1.725 + 1.726 + /** 1.727 + * Returns a predicate that can be used to test if a request matches any of 1.728 + * the active filters. 1.729 + */ 1.730 + get _filterPredicate() { 1.731 + let filterPredicates = this._allFilterPredicates; 1.732 + 1.733 + if (this._activeFilters.length === 1) { 1.734 + // The simplest case: only one filter active. 1.735 + return filterPredicates[this._activeFilters[0]].bind(this); 1.736 + } else { 1.737 + // Multiple filters active. 1.738 + return requestItem => { 1.739 + return this._activeFilters.some(filterName => { 1.740 + return filterPredicates[filterName].call(this, requestItem); 1.741 + }); 1.742 + }; 1.743 + } 1.744 + }, 1.745 + 1.746 + /** 1.747 + * Returns an object with all the filter predicates as [key: function] pairs. 1.748 + */ 1.749 + get _allFilterPredicates() ({ 1.750 + all: () => true, 1.751 + html: this.isHtml, 1.752 + css: this.isCss, 1.753 + js: this.isJs, 1.754 + xhr: this.isXHR, 1.755 + fonts: this.isFont, 1.756 + images: this.isImage, 1.757 + media: this.isMedia, 1.758 + flash: this.isFlash, 1.759 + other: this.isOther 1.760 + }), 1.761 + 1.762 + /** 1.763 + * Sorts all network requests in this container by a specified detail. 1.764 + * 1.765 + * @param string aType 1.766 + * Either "status", "method", "file", "domain", "type", "size" or 1.767 + * "waterfall". 1.768 + */ 1.769 + sortBy: function(aType = "waterfall") { 1.770 + let target = $("#requests-menu-" + aType + "-button"); 1.771 + let headers = document.querySelectorAll(".requests-menu-header-button"); 1.772 + 1.773 + for (let header of headers) { 1.774 + if (header != target) { 1.775 + header.removeAttribute("sorted"); 1.776 + header.removeAttribute("tooltiptext"); 1.777 + } 1.778 + } 1.779 + 1.780 + let direction = ""; 1.781 + if (target) { 1.782 + if (target.getAttribute("sorted") == "ascending") { 1.783 + target.setAttribute("sorted", direction = "descending"); 1.784 + target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedDesc")); 1.785 + } else { 1.786 + target.setAttribute("sorted", direction = "ascending"); 1.787 + target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedAsc")); 1.788 + } 1.789 + } 1.790 + 1.791 + // Sort by whatever was requested. 1.792 + switch (aType) { 1.793 + case "status": 1.794 + if (direction == "ascending") { 1.795 + this.sortContents(this._byStatus); 1.796 + } else { 1.797 + this.sortContents((a, b) => !this._byStatus(a, b)); 1.798 + } 1.799 + break; 1.800 + case "method": 1.801 + if (direction == "ascending") { 1.802 + this.sortContents(this._byMethod); 1.803 + } else { 1.804 + this.sortContents((a, b) => !this._byMethod(a, b)); 1.805 + } 1.806 + break; 1.807 + case "file": 1.808 + if (direction == "ascending") { 1.809 + this.sortContents(this._byFile); 1.810 + } else { 1.811 + this.sortContents((a, b) => !this._byFile(a, b)); 1.812 + } 1.813 + break; 1.814 + case "domain": 1.815 + if (direction == "ascending") { 1.816 + this.sortContents(this._byDomain); 1.817 + } else { 1.818 + this.sortContents((a, b) => !this._byDomain(a, b)); 1.819 + } 1.820 + break; 1.821 + case "type": 1.822 + if (direction == "ascending") { 1.823 + this.sortContents(this._byType); 1.824 + } else { 1.825 + this.sortContents((a, b) => !this._byType(a, b)); 1.826 + } 1.827 + break; 1.828 + case "size": 1.829 + if (direction == "ascending") { 1.830 + this.sortContents(this._bySize); 1.831 + } else { 1.832 + this.sortContents((a, b) => !this._bySize(a, b)); 1.833 + } 1.834 + break; 1.835 + case "waterfall": 1.836 + if (direction == "ascending") { 1.837 + this.sortContents(this._byTiming); 1.838 + } else { 1.839 + this.sortContents((a, b) => !this._byTiming(a, b)); 1.840 + } 1.841 + break; 1.842 + } 1.843 + 1.844 + this.refreshSummary(); 1.845 + this.refreshZebra(); 1.846 + }, 1.847 + 1.848 + /** 1.849 + * Removes all network requests and closes the sidebar if open. 1.850 + */ 1.851 + clear: function() { 1.852 + NetMonitorView.Sidebar.toggle(false); 1.853 + $("#details-pane-toggle").disabled = true; 1.854 + 1.855 + this.empty(); 1.856 + this.refreshSummary(); 1.857 + }, 1.858 + 1.859 + /** 1.860 + * Predicates used when filtering items. 1.861 + * 1.862 + * @param object aItem 1.863 + * The filtered item. 1.864 + * @return boolean 1.865 + * True if the item should be visible, false otherwise. 1.866 + */ 1.867 + isHtml: function({ attachment: { mimeType } }) 1.868 + mimeType && mimeType.contains("/html"), 1.869 + 1.870 + isCss: function({ attachment: { mimeType } }) 1.871 + mimeType && mimeType.contains("/css"), 1.872 + 1.873 + isJs: function({ attachment: { mimeType } }) 1.874 + mimeType && ( 1.875 + mimeType.contains("/ecmascript") || 1.876 + mimeType.contains("/javascript") || 1.877 + mimeType.contains("/x-javascript")), 1.878 + 1.879 + isXHR: function({ attachment: { isXHR } }) 1.880 + isXHR, 1.881 + 1.882 + isFont: function({ attachment: { url, mimeType } }) // Fonts are a mess. 1.883 + (mimeType && ( 1.884 + mimeType.contains("font/") || 1.885 + mimeType.contains("/font"))) || 1.886 + url.contains(".eot") || 1.887 + url.contains(".ttf") || 1.888 + url.contains(".otf") || 1.889 + url.contains(".woff"), 1.890 + 1.891 + isImage: function({ attachment: { mimeType } }) 1.892 + mimeType && mimeType.contains("image/"), 1.893 + 1.894 + isMedia: function({ attachment: { mimeType } }) // Not including images. 1.895 + mimeType && ( 1.896 + mimeType.contains("audio/") || 1.897 + mimeType.contains("video/") || 1.898 + mimeType.contains("model/")), 1.899 + 1.900 + isFlash: function({ attachment: { url, mimeType } }) // Flash is a mess. 1.901 + (mimeType && ( 1.902 + mimeType.contains("/x-flv") || 1.903 + mimeType.contains("/x-shockwave-flash"))) || 1.904 + url.contains(".swf") || 1.905 + url.contains(".flv"), 1.906 + 1.907 + isOther: function(e) 1.908 + !this.isHtml(e) && !this.isCss(e) && !this.isJs(e) && !this.isXHR(e) && 1.909 + !this.isFont(e) && !this.isImage(e) && !this.isMedia(e) && !this.isFlash(e), 1.910 + 1.911 + /** 1.912 + * Predicates used when sorting items. 1.913 + * 1.914 + * @param object aFirst 1.915 + * The first item used in the comparison. 1.916 + * @param object aSecond 1.917 + * The second item used in the comparison. 1.918 + * @return number 1.919 + * -1 to sort aFirst to a lower index than aSecond 1.920 + * 0 to leave aFirst and aSecond unchanged with respect to each other 1.921 + * 1 to sort aSecond to a lower index than aFirst 1.922 + */ 1.923 + _byTiming: function({ attachment: first }, { attachment: second }) 1.924 + first.startedMillis > second.startedMillis, 1.925 + 1.926 + _byStatus: function({ attachment: first }, { attachment: second }) 1.927 + first.status == second.status 1.928 + ? first.startedMillis > second.startedMillis 1.929 + : first.status > second.status, 1.930 + 1.931 + _byMethod: function({ attachment: first }, { attachment: second }) 1.932 + first.method == second.method 1.933 + ? first.startedMillis > second.startedMillis 1.934 + : first.method > second.method, 1.935 + 1.936 + _byFile: function({ attachment: first }, { attachment: second }) { 1.937 + let firstUrl = this._getUriNameWithQuery(first.url).toLowerCase(); 1.938 + let secondUrl = this._getUriNameWithQuery(second.url).toLowerCase(); 1.939 + return firstUrl == secondUrl 1.940 + ? first.startedMillis > second.startedMillis 1.941 + : firstUrl > secondUrl; 1.942 + }, 1.943 + 1.944 + _byDomain: function({ attachment: first }, { attachment: second }) { 1.945 + let firstDomain = this._getUriHostPort(first.url).toLowerCase(); 1.946 + let secondDomain = this._getUriHostPort(second.url).toLowerCase(); 1.947 + return firstDomain == secondDomain 1.948 + ? first.startedMillis > second.startedMillis 1.949 + : firstDomain > secondDomain; 1.950 + }, 1.951 + 1.952 + _byType: function({ attachment: first }, { attachment: second }) { 1.953 + let firstType = this._getAbbreviatedMimeType(first.mimeType).toLowerCase(); 1.954 + let secondType = this._getAbbreviatedMimeType(second.mimeType).toLowerCase(); 1.955 + return firstType == secondType 1.956 + ? first.startedMillis > second.startedMillis 1.957 + : firstType > secondType; 1.958 + }, 1.959 + 1.960 + _bySize: function({ attachment: first }, { attachment: second }) 1.961 + first.contentSize > second.contentSize, 1.962 + 1.963 + /** 1.964 + * Refreshes the status displayed in this container's footer, providing 1.965 + * concise information about all requests. 1.966 + */ 1.967 + refreshSummary: function() { 1.968 + let visibleItems = this.visibleItems; 1.969 + let visibleRequestsCount = visibleItems.length; 1.970 + if (!visibleRequestsCount) { 1.971 + this._summary.setAttribute("value", L10N.getStr("networkMenu.empty")); 1.972 + return; 1.973 + } 1.974 + 1.975 + let totalBytes = this._getTotalBytesOfRequests(visibleItems); 1.976 + let totalMillis = 1.977 + this._getNewestRequest(visibleItems).attachment.endedMillis - 1.978 + this._getOldestRequest(visibleItems).attachment.startedMillis; 1.979 + 1.980 + // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals 1.981 + let str = PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary")); 1.982 + this._summary.setAttribute("value", str 1.983 + .replace("#1", visibleRequestsCount) 1.984 + .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, CONTENT_SIZE_DECIMALS)) 1.985 + .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, REQUEST_TIME_DECIMALS)) 1.986 + ); 1.987 + }, 1.988 + 1.989 + /** 1.990 + * Adds odd/even attributes to all the visible items in this container. 1.991 + */ 1.992 + refreshZebra: function() { 1.993 + let visibleItems = this.visibleItems; 1.994 + 1.995 + for (let i = 0, len = visibleItems.length; i < len; i++) { 1.996 + let requestItem = visibleItems[i]; 1.997 + let requestTarget = requestItem.target; 1.998 + 1.999 + if (i % 2 == 0) { 1.1000 + requestTarget.setAttribute("even", ""); 1.1001 + requestTarget.removeAttribute("odd"); 1.1002 + } else { 1.1003 + requestTarget.setAttribute("odd", ""); 1.1004 + requestTarget.removeAttribute("even"); 1.1005 + } 1.1006 + } 1.1007 + }, 1.1008 + 1.1009 + /** 1.1010 + * Refreshes the toggling anchor for the specified item's tooltip. 1.1011 + * 1.1012 + * @param object aItem 1.1013 + * The network request item in this container. 1.1014 + */ 1.1015 + refreshTooltip: function(aItem) { 1.1016 + let tooltip = aItem.attachment.tooltip; 1.1017 + tooltip.hide(); 1.1018 + tooltip.startTogglingOnHover(aItem.target, this._onHover); 1.1019 + tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION; 1.1020 + }, 1.1021 + 1.1022 + /** 1.1023 + * Schedules adding additional information to a network request. 1.1024 + * 1.1025 + * @param string aId 1.1026 + * An identifier coming from the network monitor controller. 1.1027 + * @param object aData 1.1028 + * An object containing several { key: value } tuples of network info. 1.1029 + * Supported keys are "httpVersion", "status", "statusText" etc. 1.1030 + */ 1.1031 + updateRequest: function(aId, aData) { 1.1032 + // Prevent interference from zombie updates received after target closed. 1.1033 + if (NetMonitorView._isDestroyed) { 1.1034 + return; 1.1035 + } 1.1036 + this._updateQueue.push([aId, aData]); 1.1037 + 1.1038 + // Lazy updating is disabled in some tests. 1.1039 + if (!this.lazyUpdate) { 1.1040 + return void this._flushRequests(); 1.1041 + } 1.1042 + // Allow requests to settle down first. 1.1043 + setNamedTimeout( 1.1044 + "update-requests", REQUESTS_REFRESH_RATE, () => this._flushRequests()); 1.1045 + }, 1.1046 + 1.1047 + /** 1.1048 + * Starts adding all queued additional information about network requests. 1.1049 + */ 1.1050 + _flushRequests: function() { 1.1051 + // For each queued additional information packet, get the corresponding 1.1052 + // request item in the view and update it based on the specified data. 1.1053 + for (let [id, data] of this._updateQueue) { 1.1054 + let requestItem = this.getItemByValue(id); 1.1055 + if (!requestItem) { 1.1056 + // Packet corresponds to a dead request item, target navigated. 1.1057 + continue; 1.1058 + } 1.1059 + 1.1060 + // Each information packet may contain several { key: value } tuples of 1.1061 + // network info, so update the view based on each one. 1.1062 + for (let key in data) { 1.1063 + let value = data[key]; 1.1064 + if (value === undefined) { 1.1065 + // The information in the packet is empty, it can be safely ignored. 1.1066 + continue; 1.1067 + } 1.1068 + 1.1069 + switch (key) { 1.1070 + case "requestHeaders": 1.1071 + requestItem.attachment.requestHeaders = value; 1.1072 + break; 1.1073 + case "requestCookies": 1.1074 + requestItem.attachment.requestCookies = value; 1.1075 + break; 1.1076 + case "requestPostData": 1.1077 + // Search the POST data upload stream for request headers and add 1.1078 + // them to a separate store, different from the classic headers. 1.1079 + // XXX: Be really careful here! We're creating a function inside 1.1080 + // a loop, so remember the actual request item we want to modify. 1.1081 + let currentItem = requestItem; 1.1082 + let currentStore = { headers: [], headersSize: 0 }; 1.1083 + 1.1084 + Task.spawn(function*() { 1.1085 + let postData = yield gNetwork.getString(value.postData.text); 1.1086 + let payloadHeaders = CurlUtils.getHeadersFromMultipartText(postData); 1.1087 + 1.1088 + currentStore.headers = payloadHeaders; 1.1089 + currentStore.headersSize = payloadHeaders.reduce( 1.1090 + (acc, { name, value }) => acc + name.length + value.length + 2, 0); 1.1091 + 1.1092 + // The `getString` promise is async, so we need to refresh the 1.1093 + // information displayed in the network details pane again here. 1.1094 + refreshNetworkDetailsPaneIfNecessary(currentItem); 1.1095 + }); 1.1096 + 1.1097 + requestItem.attachment.requestPostData = value; 1.1098 + requestItem.attachment.requestHeadersFromUploadStream = currentStore; 1.1099 + break; 1.1100 + case "responseHeaders": 1.1101 + requestItem.attachment.responseHeaders = value; 1.1102 + break; 1.1103 + case "responseCookies": 1.1104 + requestItem.attachment.responseCookies = value; 1.1105 + break; 1.1106 + case "httpVersion": 1.1107 + requestItem.attachment.httpVersion = value; 1.1108 + break; 1.1109 + case "status": 1.1110 + requestItem.attachment.status = value; 1.1111 + this.updateMenuView(requestItem, key, value); 1.1112 + break; 1.1113 + case "statusText": 1.1114 + requestItem.attachment.statusText = value; 1.1115 + this.updateMenuView(requestItem, key, 1.1116 + requestItem.attachment.status + " " + 1.1117 + requestItem.attachment.statusText); 1.1118 + break; 1.1119 + case "headersSize": 1.1120 + requestItem.attachment.headersSize = value; 1.1121 + break; 1.1122 + case "contentSize": 1.1123 + requestItem.attachment.contentSize = value; 1.1124 + this.updateMenuView(requestItem, key, value); 1.1125 + break; 1.1126 + case "mimeType": 1.1127 + requestItem.attachment.mimeType = value; 1.1128 + this.updateMenuView(requestItem, key, value); 1.1129 + break; 1.1130 + case "responseContent": 1.1131 + // If there's no mime type available when the response content 1.1132 + // is received, assume text/plain as a fallback. 1.1133 + if (!requestItem.attachment.mimeType) { 1.1134 + requestItem.attachment.mimeType = "text/plain"; 1.1135 + this.updateMenuView(requestItem, "mimeType", "text/plain"); 1.1136 + } 1.1137 + requestItem.attachment.responseContent = value; 1.1138 + this.updateMenuView(requestItem, key, value); 1.1139 + break; 1.1140 + case "totalTime": 1.1141 + requestItem.attachment.totalTime = value; 1.1142 + requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value; 1.1143 + this.updateMenuView(requestItem, key, value); 1.1144 + this._registerLastRequestEnd(requestItem.attachment.endedMillis); 1.1145 + break; 1.1146 + case "eventTimings": 1.1147 + requestItem.attachment.eventTimings = value; 1.1148 + this._createWaterfallView(requestItem, value.timings); 1.1149 + break; 1.1150 + } 1.1151 + } 1.1152 + refreshNetworkDetailsPaneIfNecessary(requestItem); 1.1153 + } 1.1154 + 1.1155 + /** 1.1156 + * Refreshes the information displayed in the sidebar, in case this update 1.1157 + * may have additional information about a request which isn't shown yet 1.1158 + * in the network details pane. 1.1159 + * 1.1160 + * @param object aRequestItem 1.1161 + * The item to repopulate the sidebar with in case it's selected in 1.1162 + * this requests menu. 1.1163 + */ 1.1164 + function refreshNetworkDetailsPaneIfNecessary(aRequestItem) { 1.1165 + let selectedItem = NetMonitorView.RequestsMenu.selectedItem; 1.1166 + if (selectedItem == aRequestItem) { 1.1167 + NetMonitorView.NetworkDetails.populate(selectedItem.attachment); 1.1168 + } 1.1169 + } 1.1170 + 1.1171 + // We're done flushing all the requests, clear the update queue. 1.1172 + this._updateQueue = []; 1.1173 + 1.1174 + // Make sure all the requests are sorted and filtered. 1.1175 + // Freshly added requests may not yet contain all the information required 1.1176 + // for sorting and filtering predicates, so this is done each time the 1.1177 + // network requests table is flushed (don't worry, events are drained first 1.1178 + // so this doesn't happen once per network event update). 1.1179 + this.sortContents(); 1.1180 + this.filterContents(); 1.1181 + this.refreshSummary(); 1.1182 + this.refreshZebra(); 1.1183 + 1.1184 + // Rescale all the waterfalls so that everything is visible at once. 1.1185 + this._flushWaterfallViews(); 1.1186 + }, 1.1187 + 1.1188 + /** 1.1189 + * Customization function for creating an item's UI. 1.1190 + * 1.1191 + * @param string aMethod 1.1192 + * Specifies the request method (e.g. "GET", "POST", etc.) 1.1193 + * @param string aUrl 1.1194 + * Specifies the request's url. 1.1195 + * @return nsIDOMNode 1.1196 + * The network request view. 1.1197 + */ 1.1198 + _createMenuView: function(aMethod, aUrl) { 1.1199 + let template = $("#requests-menu-item-template"); 1.1200 + let fragment = document.createDocumentFragment(); 1.1201 + 1.1202 + this.updateMenuView(template, 'method', aMethod); 1.1203 + this.updateMenuView(template, 'url', aUrl); 1.1204 + 1.1205 + let waterfall = $(".requests-menu-waterfall", template); 1.1206 + waterfall.style.backgroundImage = this._cachedWaterfallBackground; 1.1207 + 1.1208 + // Flatten the DOM by removing one redundant box (the template container). 1.1209 + for (let node of template.childNodes) { 1.1210 + fragment.appendChild(node.cloneNode(true)); 1.1211 + } 1.1212 + 1.1213 + return fragment; 1.1214 + }, 1.1215 + 1.1216 + /** 1.1217 + * Updates the information displayed in a network request item view. 1.1218 + * 1.1219 + * @param object aItem 1.1220 + * The network request item in this container. 1.1221 + * @param string aKey 1.1222 + * The type of information that is to be updated. 1.1223 + * @param any aValue 1.1224 + * The new value to be shown. 1.1225 + * @return object 1.1226 + * A promise that is resolved once the information is displayed. 1.1227 + */ 1.1228 + updateMenuView: Task.async(function*(aItem, aKey, aValue) { 1.1229 + let target = aItem.target || aItem; 1.1230 + 1.1231 + switch (aKey) { 1.1232 + case "method": { 1.1233 + let node = $(".requests-menu-method", target); 1.1234 + node.setAttribute("value", aValue); 1.1235 + break; 1.1236 + } 1.1237 + case "url": { 1.1238 + let uri; 1.1239 + try { 1.1240 + uri = nsIURL(aValue); 1.1241 + } catch(e) { 1.1242 + break; // User input may not make a well-formed url yet. 1.1243 + } 1.1244 + let nameWithQuery = this._getUriNameWithQuery(uri); 1.1245 + let hostPort = this._getUriHostPort(uri); 1.1246 + 1.1247 + let file = $(".requests-menu-file", target); 1.1248 + file.setAttribute("value", nameWithQuery); 1.1249 + file.setAttribute("tooltiptext", nameWithQuery); 1.1250 + 1.1251 + let domain = $(".requests-menu-domain", target); 1.1252 + domain.setAttribute("value", hostPort); 1.1253 + domain.setAttribute("tooltiptext", hostPort); 1.1254 + break; 1.1255 + } 1.1256 + case "status": { 1.1257 + let node = $(".requests-menu-status", target); 1.1258 + let codeNode = $(".requests-menu-status-code", target); 1.1259 + codeNode.setAttribute("value", aValue); 1.1260 + node.setAttribute("code", aValue); 1.1261 + break; 1.1262 + } 1.1263 + case "statusText": { 1.1264 + let node = $(".requests-menu-status-and-method", target); 1.1265 + node.setAttribute("tooltiptext", aValue); 1.1266 + break; 1.1267 + } 1.1268 + case "contentSize": { 1.1269 + let kb = aValue / 1024; 1.1270 + let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS); 1.1271 + let node = $(".requests-menu-size", target); 1.1272 + let text = L10N.getFormatStr("networkMenu.sizeKB", size); 1.1273 + node.setAttribute("value", text); 1.1274 + node.setAttribute("tooltiptext", text); 1.1275 + break; 1.1276 + } 1.1277 + case "mimeType": { 1.1278 + let type = this._getAbbreviatedMimeType(aValue); 1.1279 + let node = $(".requests-menu-type", target); 1.1280 + let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type; 1.1281 + node.setAttribute("value", text); 1.1282 + node.setAttribute("tooltiptext", aValue); 1.1283 + break; 1.1284 + } 1.1285 + case "responseContent": { 1.1286 + let { mimeType } = aItem.attachment; 1.1287 + let { text, encoding } = aValue.content; 1.1288 + 1.1289 + if (mimeType.contains("image/")) { 1.1290 + let responseBody = yield gNetwork.getString(text); 1.1291 + let node = $(".requests-menu-icon", aItem.target); 1.1292 + node.src = "data:" + mimeType + ";" + encoding + "," + responseBody; 1.1293 + node.setAttribute("type", "thumbnail"); 1.1294 + node.removeAttribute("hidden"); 1.1295 + 1.1296 + window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED); 1.1297 + } 1.1298 + break; 1.1299 + } 1.1300 + case "totalTime": { 1.1301 + let node = $(".requests-menu-timings-total", target); 1.1302 + let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer 1.1303 + node.setAttribute("value", text); 1.1304 + node.setAttribute("tooltiptext", text); 1.1305 + break; 1.1306 + } 1.1307 + } 1.1308 + }), 1.1309 + 1.1310 + /** 1.1311 + * Creates a waterfall representing timing information in a network request item view. 1.1312 + * 1.1313 + * @param object aItem 1.1314 + * The network request item in this container. 1.1315 + * @param object aTimings 1.1316 + * An object containing timing information. 1.1317 + */ 1.1318 + _createWaterfallView: function(aItem, aTimings) { 1.1319 + let { target, attachment } = aItem; 1.1320 + let sections = ["dns", "connect", "send", "wait", "receive"]; 1.1321 + // Skipping "blocked" because it doesn't work yet. 1.1322 + 1.1323 + let timingsNode = $(".requests-menu-timings", target); 1.1324 + let timingsTotal = $(".requests-menu-timings-total", timingsNode); 1.1325 + 1.1326 + // Add a set of boxes representing timing information. 1.1327 + for (let key of sections) { 1.1328 + let width = aTimings[key]; 1.1329 + 1.1330 + // Don't render anything if it surely won't be visible. 1.1331 + // One millisecond == one unscaled pixel. 1.1332 + if (width > 0) { 1.1333 + let timingBox = document.createElement("hbox"); 1.1334 + timingBox.className = "requests-menu-timings-box " + key; 1.1335 + timingBox.setAttribute("width", width); 1.1336 + timingsNode.insertBefore(timingBox, timingsTotal); 1.1337 + } 1.1338 + } 1.1339 + }, 1.1340 + 1.1341 + /** 1.1342 + * Rescales and redraws all the waterfall views in this container. 1.1343 + * 1.1344 + * @param boolean aReset 1.1345 + * True if this container's width was changed. 1.1346 + */ 1.1347 + _flushWaterfallViews: function(aReset) { 1.1348 + // Don't paint things while the waterfall view isn't even visible, 1.1349 + // or there are no items added to this container. 1.1350 + if (NetMonitorView.currentFrontendMode != "network-inspector-view" || !this.itemCount) { 1.1351 + return; 1.1352 + } 1.1353 + 1.1354 + // To avoid expensive operations like getBoundingClientRect() and 1.1355 + // rebuilding the waterfall background each time a new request comes in, 1.1356 + // stuff is cached. However, in certain scenarios like when the window 1.1357 + // is resized, this needs to be invalidated. 1.1358 + if (aReset) { 1.1359 + this._cachedWaterfallWidth = 0; 1.1360 + } 1.1361 + 1.1362 + // Determine the scaling to be applied to all the waterfalls so that 1.1363 + // everything is visible at once. One millisecond == one unscaled pixel. 1.1364 + let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS; 1.1365 + let longestWidth = this._lastRequestEndedMillis - this._firstRequestStartedMillis; 1.1366 + let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1); 1.1367 + 1.1368 + // Redraw and set the canvas background for each waterfall view. 1.1369 + this._showWaterfallDivisionLabels(scale); 1.1370 + this._drawWaterfallBackground(scale); 1.1371 + this._flushWaterfallBackgrounds(); 1.1372 + 1.1373 + // Apply CSS transforms to each waterfall in this container totalTime 1.1374 + // accurately translate and resize as needed. 1.1375 + for (let { target, attachment } of this) { 1.1376 + let timingsNode = $(".requests-menu-timings", target); 1.1377 + let totalNode = $(".requests-menu-timings-total", target); 1.1378 + let direction = window.isRTL ? -1 : 1; 1.1379 + 1.1380 + // Render the timing information at a specific horizontal translation 1.1381 + // based on the delta to the first monitored event network. 1.1382 + let translateX = "translateX(" + (direction * attachment.startedDeltaMillis) + "px)"; 1.1383 + 1.1384 + // Based on the total time passed until the last request, rescale 1.1385 + // all the waterfalls to a reasonable size. 1.1386 + let scaleX = "scaleX(" + scale + ")"; 1.1387 + 1.1388 + // Certain nodes should not be scaled, even if they're children of 1.1389 + // another scaled node. In this case, apply a reversed transformation. 1.1390 + let revScaleX = "scaleX(" + (1 / scale) + ")"; 1.1391 + 1.1392 + timingsNode.style.transform = scaleX + " " + translateX; 1.1393 + totalNode.style.transform = revScaleX; 1.1394 + } 1.1395 + }, 1.1396 + 1.1397 + /** 1.1398 + * Creates the labels displayed on the waterfall header in this container. 1.1399 + * 1.1400 + * @param number aScale 1.1401 + * The current waterfall scale. 1.1402 + */ 1.1403 + _showWaterfallDivisionLabels: function(aScale) { 1.1404 + let container = $("#requests-menu-waterfall-button"); 1.1405 + let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS; 1.1406 + 1.1407 + // Nuke all existing labels. 1.1408 + while (container.hasChildNodes()) { 1.1409 + container.firstChild.remove(); 1.1410 + } 1.1411 + 1.1412 + // Build new millisecond tick labels... 1.1413 + let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE; 1.1414 + let optimalTickIntervalFound = false; 1.1415 + 1.1416 + while (!optimalTickIntervalFound) { 1.1417 + // Ignore any divisions that would end up being too close to each other. 1.1418 + let scaledStep = aScale * timingStep; 1.1419 + if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) { 1.1420 + timingStep <<= 1; 1.1421 + continue; 1.1422 + } 1.1423 + optimalTickIntervalFound = true; 1.1424 + 1.1425 + // Insert one label for each division on the current scale. 1.1426 + let fragment = document.createDocumentFragment(); 1.1427 + let direction = window.isRTL ? -1 : 1; 1.1428 + 1.1429 + for (let x = 0; x < availableWidth; x += scaledStep) { 1.1430 + let translateX = "translateX(" + ((direction * x) | 0) + "px)"; 1.1431 + let millisecondTime = x / aScale; 1.1432 + 1.1433 + let normalizedTime = millisecondTime; 1.1434 + let divisionScale = "millisecond"; 1.1435 + 1.1436 + // If the division is greater than 1 minute. 1.1437 + if (normalizedTime > 60000) { 1.1438 + normalizedTime /= 60000; 1.1439 + divisionScale = "minute"; 1.1440 + } 1.1441 + // If the division is greater than 1 second. 1.1442 + else if (normalizedTime > 1000) { 1.1443 + normalizedTime /= 1000; 1.1444 + divisionScale = "second"; 1.1445 + } 1.1446 + 1.1447 + // Showing too many decimals is bad UX. 1.1448 + if (divisionScale == "millisecond") { 1.1449 + normalizedTime |= 0; 1.1450 + } else { 1.1451 + normalizedTime = L10N.numberWithDecimals(normalizedTime, REQUEST_TIME_DECIMALS); 1.1452 + } 1.1453 + 1.1454 + let node = document.createElement("label"); 1.1455 + let text = L10N.getFormatStr("networkMenu." + divisionScale, normalizedTime); 1.1456 + node.className = "plain requests-menu-timings-division"; 1.1457 + node.setAttribute("division-scale", divisionScale); 1.1458 + node.style.transform = translateX; 1.1459 + 1.1460 + node.setAttribute("value", text); 1.1461 + fragment.appendChild(node); 1.1462 + } 1.1463 + container.appendChild(fragment); 1.1464 + } 1.1465 + }, 1.1466 + 1.1467 + /** 1.1468 + * Creates the background displayed on each waterfall view in this container. 1.1469 + * 1.1470 + * @param number aScale 1.1471 + * The current waterfall scale. 1.1472 + */ 1.1473 + _drawWaterfallBackground: function(aScale) { 1.1474 + if (!this._canvas || !this._ctx) { 1.1475 + this._canvas = document.createElementNS(HTML_NS, "canvas"); 1.1476 + this._ctx = this._canvas.getContext("2d"); 1.1477 + } 1.1478 + let canvas = this._canvas; 1.1479 + let ctx = this._ctx; 1.1480 + 1.1481 + // Nuke the context. 1.1482 + let canvasWidth = canvas.width = this._waterfallWidth; 1.1483 + let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis. 1.1484 + 1.1485 + // Start over. 1.1486 + let imageData = ctx.createImageData(canvasWidth, canvasHeight); 1.1487 + let pixelArray = imageData.data; 1.1488 + 1.1489 + let buf = new ArrayBuffer(pixelArray.length); 1.1490 + let buf8 = new Uint8ClampedArray(buf); 1.1491 + let data32 = new Uint32Array(buf); 1.1492 + 1.1493 + // Build new millisecond tick lines... 1.1494 + let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE; 1.1495 + let [r, g, b] = REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB; 1.1496 + let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN; 1.1497 + let optimalTickIntervalFound = false; 1.1498 + 1.1499 + while (!optimalTickIntervalFound) { 1.1500 + // Ignore any divisions that would end up being too close to each other. 1.1501 + let scaledStep = aScale * timingStep; 1.1502 + if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) { 1.1503 + timingStep <<= 1; 1.1504 + continue; 1.1505 + } 1.1506 + optimalTickIntervalFound = true; 1.1507 + 1.1508 + // Insert one pixel for each division on each scale. 1.1509 + for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) { 1.1510 + let increment = scaledStep * Math.pow(2, i); 1.1511 + for (let x = 0; x < canvasWidth; x += increment) { 1.1512 + let position = (window.isRTL ? canvasWidth - x : x) | 0; 1.1513 + data32[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r; 1.1514 + } 1.1515 + alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD; 1.1516 + } 1.1517 + } 1.1518 + 1.1519 + // Flush the image data and cache the waterfall background. 1.1520 + pixelArray.set(buf8); 1.1521 + ctx.putImageData(imageData, 0, 0); 1.1522 + this._cachedWaterfallBackground = "url(" + canvas.toDataURL() + ")"; 1.1523 + }, 1.1524 + 1.1525 + /** 1.1526 + * Reapplies the current waterfall background on all request items. 1.1527 + */ 1.1528 + _flushWaterfallBackgrounds: function() { 1.1529 + for (let { target } of this) { 1.1530 + let waterfallNode = $(".requests-menu-waterfall", target); 1.1531 + waterfallNode.style.backgroundImage = this._cachedWaterfallBackground; 1.1532 + } 1.1533 + }, 1.1534 + 1.1535 + /** 1.1536 + * The selection listener for this container. 1.1537 + */ 1.1538 + _onSelect: function({ detail: item }) { 1.1539 + if (item) { 1.1540 + NetMonitorView.Sidebar.populate(item.attachment); 1.1541 + NetMonitorView.Sidebar.toggle(true); 1.1542 + } else { 1.1543 + NetMonitorView.Sidebar.toggle(false); 1.1544 + } 1.1545 + }, 1.1546 + 1.1547 + /** 1.1548 + * The swap listener for this container. 1.1549 + * Called when two items switch places, when the contents are sorted. 1.1550 + */ 1.1551 + _onSwap: function({ detail: [firstItem, secondItem] }) { 1.1552 + // Sorting will create new anchor nodes for all the swapped request items 1.1553 + // in this container, so it's necessary to refresh the Tooltip instances. 1.1554 + this.refreshTooltip(firstItem); 1.1555 + this.refreshTooltip(secondItem); 1.1556 + }, 1.1557 + 1.1558 + /** 1.1559 + * The predicate used when deciding whether a popup should be shown 1.1560 + * over a request item or not. 1.1561 + * 1.1562 + * @param nsIDOMNode aTarget 1.1563 + * The element node currently being hovered. 1.1564 + * @param object aTooltip 1.1565 + * The current tooltip instance. 1.1566 + */ 1.1567 + _onHover: function(aTarget, aTooltip) { 1.1568 + let requestItem = this.getItemForElement(aTarget); 1.1569 + if (!requestItem || !requestItem.attachment.responseContent) { 1.1570 + return; 1.1571 + } 1.1572 + 1.1573 + let hovered = requestItem.attachment; 1.1574 + let { url } = hovered; 1.1575 + let { mimeType, text, encoding } = hovered.responseContent.content; 1.1576 + 1.1577 + if (mimeType && mimeType.contains("image/") && ( 1.1578 + aTarget.classList.contains("requests-menu-icon") || 1.1579 + aTarget.classList.contains("requests-menu-file"))) 1.1580 + { 1.1581 + return gNetwork.getString(text).then(aString => { 1.1582 + let anchor = $(".requests-menu-icon", requestItem.target); 1.1583 + let src = "data:" + mimeType + ";" + encoding + "," + aString; 1.1584 + aTooltip.setImageContent(src, { maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM }); 1.1585 + return anchor; 1.1586 + }); 1.1587 + } 1.1588 + }, 1.1589 + 1.1590 + /** 1.1591 + * The resize listener for this container's window. 1.1592 + */ 1.1593 + _onResize: function(e) { 1.1594 + // Allow requests to settle down first. 1.1595 + setNamedTimeout( 1.1596 + "resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true)); 1.1597 + }, 1.1598 + 1.1599 + /** 1.1600 + * Handle the context menu opening. Hide items if no request is selected. 1.1601 + */ 1.1602 + _onContextShowing: function() { 1.1603 + let selectedItem = this.selectedItem; 1.1604 + 1.1605 + let resendElement = $("#request-menu-context-resend"); 1.1606 + resendElement.hidden = !NetMonitorController.supportsCustomRequest || 1.1607 + !selectedItem || selectedItem.attachment.isCustom; 1.1608 + 1.1609 + let copyUrlElement = $("#request-menu-context-copy-url"); 1.1610 + copyUrlElement.hidden = !selectedItem; 1.1611 + 1.1612 + let copyAsCurlElement = $("#request-menu-context-copy-as-curl"); 1.1613 + copyAsCurlElement.hidden = !selectedItem || !selectedItem.attachment.responseContent; 1.1614 + 1.1615 + let copyImageAsDataUriElement = $("#request-menu-context-copy-image-as-data-uri"); 1.1616 + copyImageAsDataUriElement.hidden = !selectedItem || 1.1617 + !selectedItem.attachment.responseContent || 1.1618 + !selectedItem.attachment.responseContent.content.mimeType.contains("image/"); 1.1619 + 1.1620 + let newTabElement = $("#request-menu-context-newtab"); 1.1621 + newTabElement.hidden = !selectedItem; 1.1622 + }, 1.1623 + 1.1624 + /** 1.1625 + * Checks if the specified unix time is the first one to be known of, 1.1626 + * and saves it if so. 1.1627 + * 1.1628 + * @param number aUnixTime 1.1629 + * The milliseconds to check and save. 1.1630 + */ 1.1631 + _registerFirstRequestStart: function(aUnixTime) { 1.1632 + if (this._firstRequestStartedMillis == -1) { 1.1633 + this._firstRequestStartedMillis = aUnixTime; 1.1634 + } 1.1635 + }, 1.1636 + 1.1637 + /** 1.1638 + * Checks if the specified unix time is the last one to be known of, 1.1639 + * and saves it if so. 1.1640 + * 1.1641 + * @param number aUnixTime 1.1642 + * The milliseconds to check and save. 1.1643 + */ 1.1644 + _registerLastRequestEnd: function(aUnixTime) { 1.1645 + if (this._lastRequestEndedMillis < aUnixTime) { 1.1646 + this._lastRequestEndedMillis = aUnixTime; 1.1647 + } 1.1648 + }, 1.1649 + 1.1650 + /** 1.1651 + * Helpers for getting details about an nsIURL. 1.1652 + * 1.1653 + * @param nsIURL | string aUrl 1.1654 + * @return string 1.1655 + */ 1.1656 + _getUriNameWithQuery: function(aUrl) { 1.1657 + if (!(aUrl instanceof Ci.nsIURL)) { 1.1658 + aUrl = nsIURL(aUrl); 1.1659 + } 1.1660 + let name = NetworkHelper.convertToUnicode(unescape(aUrl.fileName)) || "/"; 1.1661 + let query = NetworkHelper.convertToUnicode(unescape(aUrl.query)); 1.1662 + return name + (query ? "?" + query : ""); 1.1663 + }, 1.1664 + _getUriHostPort: function(aUrl) { 1.1665 + if (!(aUrl instanceof Ci.nsIURL)) { 1.1666 + aUrl = nsIURL(aUrl); 1.1667 + } 1.1668 + return NetworkHelper.convertToUnicode(unescape(aUrl.hostPort)); 1.1669 + }, 1.1670 + 1.1671 + /** 1.1672 + * Helper for getting an abbreviated string for a mime type. 1.1673 + * 1.1674 + * @param string aMimeType 1.1675 + * @return string 1.1676 + */ 1.1677 + _getAbbreviatedMimeType: function(aMimeType) { 1.1678 + if (!aMimeType) { 1.1679 + return ""; 1.1680 + } 1.1681 + return (aMimeType.split(";")[0].split("/")[1] || "").split("+")[0]; 1.1682 + }, 1.1683 + 1.1684 + /** 1.1685 + * Gets the total number of bytes representing the cumulated content size of 1.1686 + * a set of requests. Returns 0 for an empty set. 1.1687 + * 1.1688 + * @param array aItemsArray 1.1689 + * @return number 1.1690 + */ 1.1691 + _getTotalBytesOfRequests: function(aItemsArray) { 1.1692 + if (!aItemsArray.length) { 1.1693 + return 0; 1.1694 + } 1.1695 + return aItemsArray.reduce((prev, curr) => prev + curr.attachment.contentSize || 0, 0); 1.1696 + }, 1.1697 + 1.1698 + /** 1.1699 + * Gets the oldest (first performed) request in a set. Returns null for an 1.1700 + * empty set. 1.1701 + * 1.1702 + * @param array aItemsArray 1.1703 + * @return object 1.1704 + */ 1.1705 + _getOldestRequest: function(aItemsArray) { 1.1706 + if (!aItemsArray.length) { 1.1707 + return null; 1.1708 + } 1.1709 + return aItemsArray.reduce((prev, curr) => 1.1710 + prev.attachment.startedMillis < curr.attachment.startedMillis ? prev : curr); 1.1711 + }, 1.1712 + 1.1713 + /** 1.1714 + * Gets the newest (latest performed) request in a set. Returns null for an 1.1715 + * empty set. 1.1716 + * 1.1717 + * @param array aItemsArray 1.1718 + * @return object 1.1719 + */ 1.1720 + _getNewestRequest: function(aItemsArray) { 1.1721 + if (!aItemsArray.length) { 1.1722 + return null; 1.1723 + } 1.1724 + return aItemsArray.reduce((prev, curr) => 1.1725 + prev.attachment.startedMillis > curr.attachment.startedMillis ? prev : curr); 1.1726 + }, 1.1727 + 1.1728 + /** 1.1729 + * Gets the available waterfall width in this container. 1.1730 + * @return number 1.1731 + */ 1.1732 + get _waterfallWidth() { 1.1733 + if (this._cachedWaterfallWidth == 0) { 1.1734 + let container = $("#requests-menu-toolbar"); 1.1735 + let waterfall = $("#requests-menu-waterfall-header-box"); 1.1736 + let containerBounds = container.getBoundingClientRect(); 1.1737 + let waterfallBounds = waterfall.getBoundingClientRect(); 1.1738 + if (!window.isRTL) { 1.1739 + this._cachedWaterfallWidth = containerBounds.width - waterfallBounds.left; 1.1740 + } else { 1.1741 + this._cachedWaterfallWidth = waterfallBounds.right; 1.1742 + } 1.1743 + } 1.1744 + return this._cachedWaterfallWidth; 1.1745 + }, 1.1746 + 1.1747 + _splitter: null, 1.1748 + _summary: null, 1.1749 + _canvas: null, 1.1750 + _ctx: null, 1.1751 + _cachedWaterfallWidth: 0, 1.1752 + _cachedWaterfallBackground: "", 1.1753 + _firstRequestStartedMillis: -1, 1.1754 + _lastRequestEndedMillis: -1, 1.1755 + _updateQueue: [], 1.1756 + _updateTimeout: null, 1.1757 + _resizeTimeout: null, 1.1758 + _activeFilters: ["all"] 1.1759 +}); 1.1760 + 1.1761 +/** 1.1762 + * Functions handling the sidebar details view. 1.1763 + */ 1.1764 +function SidebarView() { 1.1765 + dumpn("SidebarView was instantiated"); 1.1766 +} 1.1767 + 1.1768 +SidebarView.prototype = { 1.1769 + /** 1.1770 + * Sets this view hidden or visible. It's visible by default. 1.1771 + * 1.1772 + * @param boolean aVisibleFlag 1.1773 + * Specifies the intended visibility. 1.1774 + */ 1.1775 + toggle: function(aVisibleFlag) { 1.1776 + NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag }); 1.1777 + NetMonitorView.RequestsMenu._flushWaterfallViews(true); 1.1778 + }, 1.1779 + 1.1780 + /** 1.1781 + * Populates this view with the specified data. 1.1782 + * 1.1783 + * @param object aData 1.1784 + * The data source (this should be the attachment of a request item). 1.1785 + * @return object 1.1786 + * Returns a promise that resolves upon population of the subview. 1.1787 + */ 1.1788 + populate: Task.async(function*(aData) { 1.1789 + let isCustom = aData.isCustom; 1.1790 + let view = isCustom ? 1.1791 + NetMonitorView.CustomRequest : 1.1792 + NetMonitorView.NetworkDetails; 1.1793 + 1.1794 + yield view.populate(aData); 1.1795 + $("#details-pane").selectedIndex = isCustom ? 0 : 1; 1.1796 + 1.1797 + window.emit(EVENTS.SIDEBAR_POPULATED); 1.1798 + }) 1.1799 +} 1.1800 + 1.1801 +/** 1.1802 + * Functions handling the custom request view. 1.1803 + */ 1.1804 +function CustomRequestView() { 1.1805 + dumpn("CustomRequestView was instantiated"); 1.1806 +} 1.1807 + 1.1808 +CustomRequestView.prototype = { 1.1809 + /** 1.1810 + * Initialization function, called when the network monitor is started. 1.1811 + */ 1.1812 + initialize: function() { 1.1813 + dumpn("Initializing the CustomRequestView"); 1.1814 + 1.1815 + this.updateCustomRequestEvent = getKeyWithEvent(this.onUpdate.bind(this)); 1.1816 + $("#custom-pane").addEventListener("input", this.updateCustomRequestEvent, false); 1.1817 + }, 1.1818 + 1.1819 + /** 1.1820 + * Destruction function, called when the network monitor is closed. 1.1821 + */ 1.1822 + destroy: function() { 1.1823 + dumpn("Destroying the CustomRequestView"); 1.1824 + 1.1825 + $("#custom-pane").removeEventListener("input", this.updateCustomRequestEvent, false); 1.1826 + }, 1.1827 + 1.1828 + /** 1.1829 + * Populates this view with the specified data. 1.1830 + * 1.1831 + * @param object aData 1.1832 + * The data source (this should be the attachment of a request item). 1.1833 + * @return object 1.1834 + * Returns a promise that resolves upon population the view. 1.1835 + */ 1.1836 + populate: Task.async(function*(aData) { 1.1837 + $("#custom-url-value").value = aData.url; 1.1838 + $("#custom-method-value").value = aData.method; 1.1839 + this.updateCustomQuery(aData.url); 1.1840 + 1.1841 + if (aData.requestHeaders) { 1.1842 + let headers = aData.requestHeaders.headers; 1.1843 + $("#custom-headers-value").value = writeHeaderText(headers); 1.1844 + } 1.1845 + if (aData.requestPostData) { 1.1846 + let postData = aData.requestPostData.postData.text; 1.1847 + $("#custom-postdata-value").value = yield gNetwork.getString(postData); 1.1848 + } 1.1849 + 1.1850 + window.emit(EVENTS.CUSTOMREQUESTVIEW_POPULATED); 1.1851 + }), 1.1852 + 1.1853 + /** 1.1854 + * Handle user input in the custom request form. 1.1855 + * 1.1856 + * @param object aField 1.1857 + * the field that the user updated. 1.1858 + */ 1.1859 + onUpdate: function(aField) { 1.1860 + let selectedItem = NetMonitorView.RequestsMenu.selectedItem; 1.1861 + let field = aField; 1.1862 + let value; 1.1863 + 1.1864 + switch(aField) { 1.1865 + case 'method': 1.1866 + value = $("#custom-method-value").value.trim(); 1.1867 + selectedItem.attachment.method = value; 1.1868 + break; 1.1869 + case 'url': 1.1870 + value = $("#custom-url-value").value; 1.1871 + this.updateCustomQuery(value); 1.1872 + selectedItem.attachment.url = value; 1.1873 + break; 1.1874 + case 'query': 1.1875 + let query = $("#custom-query-value").value; 1.1876 + this.updateCustomUrl(query); 1.1877 + field = 'url'; 1.1878 + value = $("#custom-url-value").value 1.1879 + selectedItem.attachment.url = value; 1.1880 + break; 1.1881 + case 'body': 1.1882 + value = $("#custom-postdata-value").value; 1.1883 + selectedItem.attachment.requestPostData = { postData: { text: value } }; 1.1884 + break; 1.1885 + case 'headers': 1.1886 + let headersText = $("#custom-headers-value").value; 1.1887 + value = parseHeadersText(headersText); 1.1888 + selectedItem.attachment.requestHeaders = { headers: value }; 1.1889 + break; 1.1890 + } 1.1891 + 1.1892 + NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value); 1.1893 + }, 1.1894 + 1.1895 + /** 1.1896 + * Update the query string field based on the url. 1.1897 + * 1.1898 + * @param object aUrl 1.1899 + * The URL to extract query string from. 1.1900 + */ 1.1901 + updateCustomQuery: function(aUrl) { 1.1902 + let paramsArray = parseQueryString(nsIURL(aUrl).query); 1.1903 + if (!paramsArray) { 1.1904 + $("#custom-query").hidden = true; 1.1905 + return; 1.1906 + } 1.1907 + $("#custom-query").hidden = false; 1.1908 + $("#custom-query-value").value = writeQueryText(paramsArray); 1.1909 + }, 1.1910 + 1.1911 + /** 1.1912 + * Update the url based on the query string field. 1.1913 + * 1.1914 + * @param object aQueryText 1.1915 + * The contents of the query string field. 1.1916 + */ 1.1917 + updateCustomUrl: function(aQueryText) { 1.1918 + let params = parseQueryText(aQueryText); 1.1919 + let queryString = writeQueryString(params); 1.1920 + 1.1921 + let url = $("#custom-url-value").value; 1.1922 + let oldQuery = nsIURL(url).query; 1.1923 + let path = url.replace(oldQuery, queryString); 1.1924 + 1.1925 + $("#custom-url-value").value = path; 1.1926 + } 1.1927 +} 1.1928 + 1.1929 +/** 1.1930 + * Functions handling the requests details view. 1.1931 + */ 1.1932 +function NetworkDetailsView() { 1.1933 + dumpn("NetworkDetailsView was instantiated"); 1.1934 + 1.1935 + this._onTabSelect = this._onTabSelect.bind(this); 1.1936 +}; 1.1937 + 1.1938 +NetworkDetailsView.prototype = { 1.1939 + /** 1.1940 + * Initialization function, called when the network monitor is started. 1.1941 + */ 1.1942 + initialize: function() { 1.1943 + dumpn("Initializing the NetworkDetailsView"); 1.1944 + 1.1945 + this.widget = $("#event-details-pane"); 1.1946 + 1.1947 + this._headers = new VariablesView($("#all-headers"), 1.1948 + Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, { 1.1949 + emptyText: L10N.getStr("headersEmptyText"), 1.1950 + searchPlaceholder: L10N.getStr("headersFilterText") 1.1951 + })); 1.1952 + this._cookies = new VariablesView($("#all-cookies"), 1.1953 + Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, { 1.1954 + emptyText: L10N.getStr("cookiesEmptyText"), 1.1955 + searchPlaceholder: L10N.getStr("cookiesFilterText") 1.1956 + })); 1.1957 + this._params = new VariablesView($("#request-params"), 1.1958 + Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, { 1.1959 + emptyText: L10N.getStr("paramsEmptyText"), 1.1960 + searchPlaceholder: L10N.getStr("paramsFilterText") 1.1961 + })); 1.1962 + this._json = new VariablesView($("#response-content-json"), 1.1963 + Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, { 1.1964 + onlyEnumVisible: true, 1.1965 + searchPlaceholder: L10N.getStr("jsonFilterText") 1.1966 + })); 1.1967 + VariablesViewController.attach(this._json); 1.1968 + 1.1969 + this._paramsQueryString = L10N.getStr("paramsQueryString"); 1.1970 + this._paramsFormData = L10N.getStr("paramsFormData"); 1.1971 + this._paramsPostPayload = L10N.getStr("paramsPostPayload"); 1.1972 + this._requestHeaders = L10N.getStr("requestHeaders"); 1.1973 + this._requestHeadersFromUpload = L10N.getStr("requestHeadersFromUpload"); 1.1974 + this._responseHeaders = L10N.getStr("responseHeaders"); 1.1975 + this._requestCookies = L10N.getStr("requestCookies"); 1.1976 + this._responseCookies = L10N.getStr("responseCookies"); 1.1977 + 1.1978 + $("tabpanels", this.widget).addEventListener("select", this._onTabSelect); 1.1979 + }, 1.1980 + 1.1981 + /** 1.1982 + * Destruction function, called when the network monitor is closed. 1.1983 + */ 1.1984 + destroy: function() { 1.1985 + dumpn("Destroying the NetworkDetailsView"); 1.1986 + }, 1.1987 + 1.1988 + /** 1.1989 + * Populates this view with the specified data. 1.1990 + * 1.1991 + * @param object aData 1.1992 + * The data source (this should be the attachment of a request item). 1.1993 + * @return object 1.1994 + * Returns a promise that resolves upon population the view. 1.1995 + */ 1.1996 + populate: function(aData) { 1.1997 + $("#request-params-box").setAttribute("flex", "1"); 1.1998 + $("#request-params-box").hidden = false; 1.1999 + $("#request-post-data-textarea-box").hidden = true; 1.2000 + $("#response-content-info-header").hidden = true; 1.2001 + $("#response-content-json-box").hidden = true; 1.2002 + $("#response-content-textarea-box").hidden = true; 1.2003 + $("#response-content-image-box").hidden = true; 1.2004 + 1.2005 + let isHtml = RequestsMenuView.prototype.isHtml({ attachment: aData }); 1.2006 + 1.2007 + // Show the "Preview" tabpanel only for plain HTML responses. 1.2008 + $("#preview-tab").hidden = !isHtml; 1.2009 + $("#preview-tabpanel").hidden = !isHtml; 1.2010 + 1.2011 + // Switch to the "Headers" tabpanel if the "Preview" previously selected 1.2012 + // and this is not an HTML response. 1.2013 + if (!isHtml && this.widget.selectedIndex == 5) { 1.2014 + this.widget.selectedIndex = 0; 1.2015 + } 1.2016 + 1.2017 + this._headers.empty(); 1.2018 + this._cookies.empty(); 1.2019 + this._params.empty(); 1.2020 + this._json.empty(); 1.2021 + 1.2022 + this._dataSrc = { src: aData, populated: [] }; 1.2023 + this._onTabSelect(); 1.2024 + window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED); 1.2025 + 1.2026 + return promise.resolve(); 1.2027 + }, 1.2028 + 1.2029 + /** 1.2030 + * Listener handling the tab selection event. 1.2031 + */ 1.2032 + _onTabSelect: function() { 1.2033 + let { src, populated } = this._dataSrc || {}; 1.2034 + let tab = this.widget.selectedIndex; 1.2035 + let view = this; 1.2036 + 1.2037 + // Make sure the data source is valid and don't populate the same tab twice. 1.2038 + if (!src || populated[tab]) { 1.2039 + return; 1.2040 + } 1.2041 + 1.2042 + Task.spawn(function*() { 1.2043 + switch (tab) { 1.2044 + case 0: // "Headers" 1.2045 + yield view._setSummary(src); 1.2046 + yield view._setResponseHeaders(src.responseHeaders); 1.2047 + yield view._setRequestHeaders( 1.2048 + src.requestHeaders, 1.2049 + src.requestHeadersFromUploadStream); 1.2050 + break; 1.2051 + case 1: // "Cookies" 1.2052 + yield view._setResponseCookies(src.responseCookies); 1.2053 + yield view._setRequestCookies(src.requestCookies); 1.2054 + break; 1.2055 + case 2: // "Params" 1.2056 + yield view._setRequestGetParams(src.url); 1.2057 + yield view._setRequestPostParams( 1.2058 + src.requestHeaders, 1.2059 + src.requestHeadersFromUploadStream, 1.2060 + src.requestPostData); 1.2061 + break; 1.2062 + case 3: // "Response" 1.2063 + yield view._setResponseBody(src.url, src.responseContent); 1.2064 + break; 1.2065 + case 4: // "Timings" 1.2066 + yield view._setTimingsInformation(src.eventTimings); 1.2067 + break; 1.2068 + case 5: // "Preview" 1.2069 + yield view._setHtmlPreview(src.responseContent); 1.2070 + break; 1.2071 + } 1.2072 + populated[tab] = true; 1.2073 + window.emit(EVENTS.TAB_UPDATED); 1.2074 + NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible(); 1.2075 + }); 1.2076 + }, 1.2077 + 1.2078 + /** 1.2079 + * Sets the network request summary shown in this view. 1.2080 + * 1.2081 + * @param object aData 1.2082 + * The data source (this should be the attachment of a request item). 1.2083 + */ 1.2084 + _setSummary: function(aData) { 1.2085 + if (aData.url) { 1.2086 + let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aData.url)); 1.2087 + $("#headers-summary-url-value").setAttribute("value", unicodeUrl); 1.2088 + $("#headers-summary-url-value").setAttribute("tooltiptext", unicodeUrl); 1.2089 + $("#headers-summary-url").removeAttribute("hidden"); 1.2090 + } else { 1.2091 + $("#headers-summary-url").setAttribute("hidden", "true"); 1.2092 + } 1.2093 + 1.2094 + if (aData.method) { 1.2095 + $("#headers-summary-method-value").setAttribute("value", aData.method); 1.2096 + $("#headers-summary-method").removeAttribute("hidden"); 1.2097 + } else { 1.2098 + $("#headers-summary-method").setAttribute("hidden", "true"); 1.2099 + } 1.2100 + 1.2101 + if (aData.status) { 1.2102 + $("#headers-summary-status-circle").setAttribute("code", aData.status); 1.2103 + $("#headers-summary-status-value").setAttribute("value", aData.status + " " + aData.statusText); 1.2104 + $("#headers-summary-status").removeAttribute("hidden"); 1.2105 + } else { 1.2106 + $("#headers-summary-status").setAttribute("hidden", "true"); 1.2107 + } 1.2108 + 1.2109 + if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) { 1.2110 + $("#headers-summary-version-value").setAttribute("value", aData.httpVersion); 1.2111 + $("#headers-summary-version").removeAttribute("hidden"); 1.2112 + } else { 1.2113 + $("#headers-summary-version").setAttribute("hidden", "true"); 1.2114 + } 1.2115 + }, 1.2116 + 1.2117 + /** 1.2118 + * Sets the network request headers shown in this view. 1.2119 + * 1.2120 + * @param object aHeadersResponse 1.2121 + * The "requestHeaders" message received from the server. 1.2122 + * @param object aHeadersFromUploadStream 1.2123 + * The "requestHeadersFromUploadStream" inferred from the POST payload. 1.2124 + * @return object 1.2125 + * A promise that resolves when request headers are set. 1.2126 + */ 1.2127 + _setRequestHeaders: Task.async(function*(aHeadersResponse, aHeadersFromUploadStream) { 1.2128 + if (aHeadersResponse && aHeadersResponse.headers.length) { 1.2129 + yield this._addHeaders(this._requestHeaders, aHeadersResponse); 1.2130 + } 1.2131 + if (aHeadersFromUploadStream && aHeadersFromUploadStream.headers.length) { 1.2132 + yield this._addHeaders(this._requestHeadersFromUpload, aHeadersFromUploadStream); 1.2133 + } 1.2134 + }), 1.2135 + 1.2136 + /** 1.2137 + * Sets the network response headers shown in this view. 1.2138 + * 1.2139 + * @param object aResponse 1.2140 + * The message received from the server. 1.2141 + * @return object 1.2142 + * A promise that resolves when response headers are set. 1.2143 + */ 1.2144 + _setResponseHeaders: Task.async(function*(aResponse) { 1.2145 + if (aResponse && aResponse.headers.length) { 1.2146 + aResponse.headers.sort((a, b) => a.name > b.name); 1.2147 + yield this._addHeaders(this._responseHeaders, aResponse); 1.2148 + } 1.2149 + }), 1.2150 + 1.2151 + /** 1.2152 + * Populates the headers container in this view with the specified data. 1.2153 + * 1.2154 + * @param string aName 1.2155 + * The type of headers to populate (request or response). 1.2156 + * @param object aResponse 1.2157 + * The message received from the server. 1.2158 + * @return object 1.2159 + * A promise that resolves when headers are added. 1.2160 + */ 1.2161 + _addHeaders: Task.async(function*(aName, aResponse) { 1.2162 + let kb = aResponse.headersSize / 1024; 1.2163 + let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS); 1.2164 + let text = L10N.getFormatStr("networkMenu.sizeKB", size); 1.2165 + 1.2166 + let headersScope = this._headers.addScope(aName + " (" + text + ")"); 1.2167 + headersScope.expanded = true; 1.2168 + 1.2169 + for (let header of aResponse.headers) { 1.2170 + let headerVar = headersScope.addItem(header.name, {}, true); 1.2171 + let headerValue = yield gNetwork.getString(header.value); 1.2172 + headerVar.setGrip(headerValue); 1.2173 + } 1.2174 + }), 1.2175 + 1.2176 + /** 1.2177 + * Sets the network request cookies shown in this view. 1.2178 + * 1.2179 + * @param object aResponse 1.2180 + * The message received from the server. 1.2181 + * @return object 1.2182 + * A promise that is resolved when the request cookies are set. 1.2183 + */ 1.2184 + _setRequestCookies: Task.async(function*(aResponse) { 1.2185 + if (aResponse && aResponse.cookies.length) { 1.2186 + aResponse.cookies.sort((a, b) => a.name > b.name); 1.2187 + yield this._addCookies(this._requestCookies, aResponse); 1.2188 + } 1.2189 + }), 1.2190 + 1.2191 + /** 1.2192 + * Sets the network response cookies shown in this view. 1.2193 + * 1.2194 + * @param object aResponse 1.2195 + * The message received from the server. 1.2196 + * @return object 1.2197 + * A promise that is resolved when the response cookies are set. 1.2198 + */ 1.2199 + _setResponseCookies: Task.async(function*(aResponse) { 1.2200 + if (aResponse && aResponse.cookies.length) { 1.2201 + yield this._addCookies(this._responseCookies, aResponse); 1.2202 + } 1.2203 + }), 1.2204 + 1.2205 + /** 1.2206 + * Populates the cookies container in this view with the specified data. 1.2207 + * 1.2208 + * @param string aName 1.2209 + * The type of cookies to populate (request or response). 1.2210 + * @param object aResponse 1.2211 + * The message received from the server. 1.2212 + * @return object 1.2213 + * Returns a promise that resolves upon the adding of cookies. 1.2214 + */ 1.2215 + _addCookies: Task.async(function*(aName, aResponse) { 1.2216 + let cookiesScope = this._cookies.addScope(aName); 1.2217 + cookiesScope.expanded = true; 1.2218 + 1.2219 + for (let cookie of aResponse.cookies) { 1.2220 + let cookieVar = cookiesScope.addItem(cookie.name, {}, true); 1.2221 + let cookieValue = yield gNetwork.getString(cookie.value); 1.2222 + cookieVar.setGrip(cookieValue); 1.2223 + 1.2224 + // By default the cookie name and value are shown. If this is the only 1.2225 + // information available, then nothing else is to be displayed. 1.2226 + let cookieProps = Object.keys(cookie); 1.2227 + if (cookieProps.length == 2) { 1.2228 + return; 1.2229 + } 1.2230 + 1.2231 + // Display any other information other than the cookie name and value 1.2232 + // which may be available. 1.2233 + let rawObject = Object.create(null); 1.2234 + let otherProps = cookieProps.filter(e => e != "name" && e != "value"); 1.2235 + for (let prop of otherProps) { 1.2236 + rawObject[prop] = cookie[prop]; 1.2237 + } 1.2238 + cookieVar.populate(rawObject); 1.2239 + cookieVar.twisty = true; 1.2240 + cookieVar.expanded = true; 1.2241 + } 1.2242 + }), 1.2243 + 1.2244 + /** 1.2245 + * Sets the network request get params shown in this view. 1.2246 + * 1.2247 + * @param string aUrl 1.2248 + * The request's url. 1.2249 + */ 1.2250 + _setRequestGetParams: function(aUrl) { 1.2251 + let query = nsIURL(aUrl).query; 1.2252 + if (query) { 1.2253 + this._addParams(this._paramsQueryString, query); 1.2254 + } 1.2255 + }, 1.2256 + 1.2257 + /** 1.2258 + * Sets the network request post params shown in this view. 1.2259 + * 1.2260 + * @param object aHeadersResponse 1.2261 + * The "requestHeaders" message received from the server. 1.2262 + * @param object aHeadersFromUploadStream 1.2263 + * The "requestHeadersFromUploadStream" inferred from the POST payload. 1.2264 + * @param object aPostDataResponse 1.2265 + * The "requestPostData" message received from the server. 1.2266 + * @return object 1.2267 + * A promise that is resolved when the request post params are set. 1.2268 + */ 1.2269 + _setRequestPostParams: Task.async(function*(aHeadersResponse, aHeadersFromUploadStream, aPostDataResponse) { 1.2270 + if (!aHeadersResponse || !aHeadersFromUploadStream || !aPostDataResponse) { 1.2271 + return; 1.2272 + } 1.2273 + 1.2274 + let { headers: requestHeaders } = aHeadersResponse; 1.2275 + let { headers: payloadHeaders } = aHeadersFromUploadStream; 1.2276 + let allHeaders = [...payloadHeaders, ...requestHeaders]; 1.2277 + 1.2278 + let contentTypeHeader = allHeaders.find(e => e.name.toLowerCase() == "content-type"); 1.2279 + let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : ""; 1.2280 + let postDataLongString = aPostDataResponse.postData.text; 1.2281 + 1.2282 + let postData = yield gNetwork.getString(postDataLongString); 1.2283 + let contentType = yield gNetwork.getString(contentTypeLongString); 1.2284 + 1.2285 + // Handle query strings (e.g. "?foo=bar&baz=42"). 1.2286 + if (contentType.contains("x-www-form-urlencoded")) { 1.2287 + for (let section of postData.split(/\r\n|\r|\n/)) { 1.2288 + // Before displaying it, make sure this section of the POST data 1.2289 + // isn't a line containing upload stream headers. 1.2290 + if (payloadHeaders.every(header => !section.startsWith(header.name))) { 1.2291 + this._addParams(this._paramsFormData, section); 1.2292 + } 1.2293 + } 1.2294 + } 1.2295 + // Handle actual forms ("multipart/form-data" content type). 1.2296 + else { 1.2297 + // This is really awkward, but hey, it works. Let's show an empty 1.2298 + // scope in the params view and place the source editor containing 1.2299 + // the raw post data directly underneath. 1.2300 + $("#request-params-box").removeAttribute("flex"); 1.2301 + let paramsScope = this._params.addScope(this._paramsPostPayload); 1.2302 + paramsScope.expanded = true; 1.2303 + paramsScope.locked = true; 1.2304 + 1.2305 + $("#request-post-data-textarea-box").hidden = false; 1.2306 + let editor = yield NetMonitorView.editor("#request-post-data-textarea"); 1.2307 + // Most POST bodies are usually JSON, so they can be neatly 1.2308 + // syntax highlighted as JS. Otheriwse, fall back to plain text. 1.2309 + try { 1.2310 + JSON.parse(postData); 1.2311 + editor.setMode(Editor.modes.js); 1.2312 + } catch (e) { 1.2313 + editor.setMode(Editor.modes.text); 1.2314 + } finally { 1.2315 + editor.setText(postData); 1.2316 + } 1.2317 + } 1.2318 + 1.2319 + window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED); 1.2320 + }), 1.2321 + 1.2322 + /** 1.2323 + * Populates the params container in this view with the specified data. 1.2324 + * 1.2325 + * @param string aName 1.2326 + * The type of params to populate (get or post). 1.2327 + * @param string aQueryString 1.2328 + * A query string of params (e.g. "?foo=bar&baz=42"). 1.2329 + */ 1.2330 + _addParams: function(aName, aQueryString) { 1.2331 + let paramsArray = parseQueryString(aQueryString); 1.2332 + if (!paramsArray) { 1.2333 + return; 1.2334 + } 1.2335 + let paramsScope = this._params.addScope(aName); 1.2336 + paramsScope.expanded = true; 1.2337 + 1.2338 + for (let param of paramsArray) { 1.2339 + let paramVar = paramsScope.addItem(param.name, {}, true); 1.2340 + paramVar.setGrip(param.value); 1.2341 + } 1.2342 + }, 1.2343 + 1.2344 + /** 1.2345 + * Sets the network response body shown in this view. 1.2346 + * 1.2347 + * @param string aUrl 1.2348 + * The request's url. 1.2349 + * @param object aResponse 1.2350 + * The message received from the server. 1.2351 + * @return object 1.2352 + * A promise that is resolved when the response body is set. 1.2353 + */ 1.2354 + _setResponseBody: Task.async(function*(aUrl, aResponse) { 1.2355 + if (!aResponse) { 1.2356 + return; 1.2357 + } 1.2358 + let { mimeType, text, encoding } = aResponse.content; 1.2359 + let responseBody = yield gNetwork.getString(text); 1.2360 + 1.2361 + // Handle json, which we tentatively identify by checking the MIME type 1.2362 + // for "json" after any word boundary. This works for the standard 1.2363 + // "application/json", and also for custom types like "x-bigcorp-json". 1.2364 + // Additionally, we also directly parse the response text content to 1.2365 + // verify whether it's json or not, to handle responses incorrectly 1.2366 + // labeled as text/plain instead. 1.2367 + let jsonMimeType, jsonObject, jsonObjectParseError; 1.2368 + try { 1.2369 + jsonMimeType = /\bjson/.test(mimeType); 1.2370 + jsonObject = JSON.parse(responseBody); 1.2371 + } catch (e) { 1.2372 + jsonObjectParseError = e; 1.2373 + } 1.2374 + if (jsonMimeType || jsonObject) { 1.2375 + // Extract the actual json substring in case this might be a "JSONP". 1.2376 + // This regex basically parses a function call and captures the 1.2377 + // function name and arguments in two separate groups. 1.2378 + let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/; 1.2379 + let [_, callbackPadding, jsonpString] = responseBody.match(jsonpRegex) || []; 1.2380 + 1.2381 + // Make sure this is a valid JSON object first. If so, nicely display 1.2382 + // the parsing results in a variables view. Otherwise, simply show 1.2383 + // the contents as plain text. 1.2384 + if (callbackPadding && jsonpString) { 1.2385 + try { 1.2386 + jsonObject = JSON.parse(jsonpString); 1.2387 + } catch (e) { 1.2388 + jsonObjectParseError = e; 1.2389 + } 1.2390 + } 1.2391 + 1.2392 + // Valid JSON or JSONP. 1.2393 + if (jsonObject) { 1.2394 + $("#response-content-json-box").hidden = false; 1.2395 + let jsonScopeName = callbackPadding 1.2396 + ? L10N.getFormatStr("jsonpScopeName", callbackPadding) 1.2397 + : L10N.getStr("jsonScopeName"); 1.2398 + 1.2399 + let jsonVar = { label: jsonScopeName, rawObject: jsonObject }; 1.2400 + yield this._json.controller.setSingleVariable(jsonVar).expanded; 1.2401 + } 1.2402 + // Malformed JSON. 1.2403 + else { 1.2404 + $("#response-content-textarea-box").hidden = false; 1.2405 + let infoHeader = $("#response-content-info-header"); 1.2406 + infoHeader.setAttribute("value", jsonObjectParseError); 1.2407 + infoHeader.setAttribute("tooltiptext", jsonObjectParseError); 1.2408 + infoHeader.hidden = false; 1.2409 + 1.2410 + let editor = yield NetMonitorView.editor("#response-content-textarea"); 1.2411 + editor.setMode(Editor.modes.js); 1.2412 + editor.setText(responseBody); 1.2413 + } 1.2414 + } 1.2415 + // Handle images. 1.2416 + else if (mimeType.contains("image/")) { 1.2417 + $("#response-content-image-box").setAttribute("align", "center"); 1.2418 + $("#response-content-image-box").setAttribute("pack", "center"); 1.2419 + $("#response-content-image-box").hidden = false; 1.2420 + $("#response-content-image").src = 1.2421 + "data:" + mimeType + ";" + encoding + "," + responseBody; 1.2422 + 1.2423 + // Immediately display additional information about the image: 1.2424 + // file name, mime type and encoding. 1.2425 + $("#response-content-image-name-value").setAttribute("value", nsIURL(aUrl).fileName); 1.2426 + $("#response-content-image-mime-value").setAttribute("value", mimeType); 1.2427 + $("#response-content-image-encoding-value").setAttribute("value", encoding); 1.2428 + 1.2429 + // Wait for the image to load in order to display the width and height. 1.2430 + $("#response-content-image").onload = e => { 1.2431 + // XUL images are majestic so they don't bother storing their dimensions 1.2432 + // in width and height attributes like the rest of the folk. Hack around 1.2433 + // this by getting the bounding client rect and subtracting the margins. 1.2434 + let { width, height } = e.target.getBoundingClientRect(); 1.2435 + let dimensions = (width - 2) + " x " + (height - 2); 1.2436 + $("#response-content-image-dimensions-value").setAttribute("value", dimensions); 1.2437 + }; 1.2438 + } 1.2439 + // Handle anything else. 1.2440 + else { 1.2441 + $("#response-content-textarea-box").hidden = false; 1.2442 + let editor = yield NetMonitorView.editor("#response-content-textarea"); 1.2443 + editor.setMode(Editor.modes.text); 1.2444 + editor.setText(responseBody); 1.2445 + 1.2446 + // Maybe set a more appropriate mode in the Source Editor if possible, 1.2447 + // but avoid doing this for very large files. 1.2448 + if (responseBody.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) { 1.2449 + let mapping = Object.keys(CONTENT_MIME_TYPE_MAPPINGS).find(key => mimeType.contains(key)); 1.2450 + if (mapping) { 1.2451 + editor.setMode(CONTENT_MIME_TYPE_MAPPINGS[mapping]); 1.2452 + } 1.2453 + } 1.2454 + } 1.2455 + 1.2456 + window.emit(EVENTS.RESPONSE_BODY_DISPLAYED); 1.2457 + }), 1.2458 + 1.2459 + /** 1.2460 + * Sets the timings information shown in this view. 1.2461 + * 1.2462 + * @param object aResponse 1.2463 + * The message received from the server. 1.2464 + */ 1.2465 + _setTimingsInformation: function(aResponse) { 1.2466 + if (!aResponse) { 1.2467 + return; 1.2468 + } 1.2469 + let { blocked, dns, connect, send, wait, receive } = aResponse.timings; 1.2470 + 1.2471 + let tabboxWidth = $("#details-pane").getAttribute("width"); 1.2472 + let availableWidth = tabboxWidth / 2; // Other nodes also take some space. 1.2473 + let scale = Math.max(availableWidth / aResponse.totalTime, 0); 1.2474 + 1.2475 + $("#timings-summary-blocked .requests-menu-timings-box") 1.2476 + .setAttribute("width", blocked * scale); 1.2477 + $("#timings-summary-blocked .requests-menu-timings-total") 1.2478 + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", blocked)); 1.2479 + 1.2480 + $("#timings-summary-dns .requests-menu-timings-box") 1.2481 + .setAttribute("width", dns * scale); 1.2482 + $("#timings-summary-dns .requests-menu-timings-total") 1.2483 + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", dns)); 1.2484 + 1.2485 + $("#timings-summary-connect .requests-menu-timings-box") 1.2486 + .setAttribute("width", connect * scale); 1.2487 + $("#timings-summary-connect .requests-menu-timings-total") 1.2488 + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", connect)); 1.2489 + 1.2490 + $("#timings-summary-send .requests-menu-timings-box") 1.2491 + .setAttribute("width", send * scale); 1.2492 + $("#timings-summary-send .requests-menu-timings-total") 1.2493 + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", send)); 1.2494 + 1.2495 + $("#timings-summary-wait .requests-menu-timings-box") 1.2496 + .setAttribute("width", wait * scale); 1.2497 + $("#timings-summary-wait .requests-menu-timings-total") 1.2498 + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", wait)); 1.2499 + 1.2500 + $("#timings-summary-receive .requests-menu-timings-box") 1.2501 + .setAttribute("width", receive * scale); 1.2502 + $("#timings-summary-receive .requests-menu-timings-total") 1.2503 + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", receive)); 1.2504 + 1.2505 + $("#timings-summary-dns .requests-menu-timings-box") 1.2506 + .style.transform = "translateX(" + (scale * blocked) + "px)"; 1.2507 + $("#timings-summary-connect .requests-menu-timings-box") 1.2508 + .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)"; 1.2509 + $("#timings-summary-send .requests-menu-timings-box") 1.2510 + .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)"; 1.2511 + $("#timings-summary-wait .requests-menu-timings-box") 1.2512 + .style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)"; 1.2513 + $("#timings-summary-receive .requests-menu-timings-box") 1.2514 + .style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)"; 1.2515 + 1.2516 + $("#timings-summary-dns .requests-menu-timings-total") 1.2517 + .style.transform = "translateX(" + (scale * blocked) + "px)"; 1.2518 + $("#timings-summary-connect .requests-menu-timings-total") 1.2519 + .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)"; 1.2520 + $("#timings-summary-send .requests-menu-timings-total") 1.2521 + .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)"; 1.2522 + $("#timings-summary-wait .requests-menu-timings-total") 1.2523 + .style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)"; 1.2524 + $("#timings-summary-receive .requests-menu-timings-total") 1.2525 + .style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)"; 1.2526 + }, 1.2527 + 1.2528 + /** 1.2529 + * Sets the preview for HTML responses shown in this view. 1.2530 + * 1.2531 + * @param object aResponse 1.2532 + * The message received from the server. 1.2533 + * @return object 1.2534 + * A promise that is resolved when the html preview is rendered. 1.2535 + */ 1.2536 + _setHtmlPreview: Task.async(function*(aResponse) { 1.2537 + if (!aResponse) { 1.2538 + return promise.resolve(); 1.2539 + } 1.2540 + let { text } = aResponse.content; 1.2541 + let responseBody = yield gNetwork.getString(text); 1.2542 + 1.2543 + // Always disable JS when previewing HTML responses. 1.2544 + let iframe = $("#response-preview"); 1.2545 + iframe.contentDocument.docShell.allowJavascript = false; 1.2546 + iframe.contentDocument.documentElement.innerHTML = responseBody; 1.2547 + 1.2548 + window.emit(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED); 1.2549 + }), 1.2550 + 1.2551 + _dataSrc: null, 1.2552 + _headers: null, 1.2553 + _cookies: null, 1.2554 + _params: null, 1.2555 + _json: null, 1.2556 + _paramsQueryString: "", 1.2557 + _paramsFormData: "", 1.2558 + _paramsPostPayload: "", 1.2559 + _requestHeaders: "", 1.2560 + _responseHeaders: "", 1.2561 + _requestCookies: "", 1.2562 + _responseCookies: "" 1.2563 +}; 1.2564 + 1.2565 +/** 1.2566 + * Functions handling the performance statistics view. 1.2567 + */ 1.2568 +function PerformanceStatisticsView() { 1.2569 +} 1.2570 + 1.2571 +PerformanceStatisticsView.prototype = { 1.2572 + /** 1.2573 + * Initializes and displays empty charts in this container. 1.2574 + */ 1.2575 + displayPlaceholderCharts: function() { 1.2576 + this._createChart({ 1.2577 + id: "#primed-cache-chart", 1.2578 + title: "charts.cacheEnabled" 1.2579 + }); 1.2580 + this._createChart({ 1.2581 + id: "#empty-cache-chart", 1.2582 + title: "charts.cacheDisabled" 1.2583 + }); 1.2584 + window.emit(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED); 1.2585 + }, 1.2586 + 1.2587 + /** 1.2588 + * Populates and displays the primed cache chart in this container. 1.2589 + * 1.2590 + * @param array aItems 1.2591 + * @see this._sanitizeChartDataSource 1.2592 + */ 1.2593 + createPrimedCacheChart: function(aItems) { 1.2594 + this._createChart({ 1.2595 + id: "#primed-cache-chart", 1.2596 + title: "charts.cacheEnabled", 1.2597 + data: this._sanitizeChartDataSource(aItems), 1.2598 + strings: this._commonChartStrings, 1.2599 + totals: this._commonChartTotals, 1.2600 + sorted: true 1.2601 + }); 1.2602 + window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED); 1.2603 + }, 1.2604 + 1.2605 + /** 1.2606 + * Populates and displays the empty cache chart in this container. 1.2607 + * 1.2608 + * @param array aItems 1.2609 + * @see this._sanitizeChartDataSource 1.2610 + */ 1.2611 + createEmptyCacheChart: function(aItems) { 1.2612 + this._createChart({ 1.2613 + id: "#empty-cache-chart", 1.2614 + title: "charts.cacheDisabled", 1.2615 + data: this._sanitizeChartDataSource(aItems, true), 1.2616 + strings: this._commonChartStrings, 1.2617 + totals: this._commonChartTotals, 1.2618 + sorted: true 1.2619 + }); 1.2620 + window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED); 1.2621 + }, 1.2622 + 1.2623 + /** 1.2624 + * Common stringifier predicates used for items and totals in both the 1.2625 + * "primed" and "empty" cache charts. 1.2626 + */ 1.2627 + _commonChartStrings: { 1.2628 + size: value => { 1.2629 + let string = L10N.numberWithDecimals(value / 1024, CONTENT_SIZE_DECIMALS); 1.2630 + return L10N.getFormatStr("charts.sizeKB", string); 1.2631 + }, 1.2632 + time: value => { 1.2633 + let string = L10N.numberWithDecimals(value / 1000, REQUEST_TIME_DECIMALS); 1.2634 + return L10N.getFormatStr("charts.totalS", string); 1.2635 + } 1.2636 + }, 1.2637 + _commonChartTotals: { 1.2638 + size: total => { 1.2639 + let string = L10N.numberWithDecimals(total / 1024, CONTENT_SIZE_DECIMALS); 1.2640 + return L10N.getFormatStr("charts.totalSize", string); 1.2641 + }, 1.2642 + time: total => { 1.2643 + let seconds = total / 1000; 1.2644 + let string = L10N.numberWithDecimals(seconds, REQUEST_TIME_DECIMALS); 1.2645 + return PluralForm.get(seconds, L10N.getStr("charts.totalSeconds")).replace("#1", string); 1.2646 + }, 1.2647 + cached: total => { 1.2648 + return L10N.getFormatStr("charts.totalCached", total); 1.2649 + }, 1.2650 + count: total => { 1.2651 + return L10N.getFormatStr("charts.totalCount", total); 1.2652 + } 1.2653 + }, 1.2654 + 1.2655 + /** 1.2656 + * Adds a specific chart to this container. 1.2657 + * 1.2658 + * @param object 1.2659 + * An object containing all or some the following properties: 1.2660 + * - id: either "#primed-cache-chart" or "#empty-cache-chart" 1.2661 + * - title/data/strings/totals/sorted: @see Chart.jsm for details 1.2662 + */ 1.2663 + _createChart: function({ id, title, data, strings, totals, sorted }) { 1.2664 + let container = $(id); 1.2665 + 1.2666 + // Nuke all existing charts of the specified type. 1.2667 + while (container.hasChildNodes()) { 1.2668 + container.firstChild.remove(); 1.2669 + } 1.2670 + 1.2671 + // Create a new chart. 1.2672 + let chart = Chart.PieTable(document, { 1.2673 + diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER, 1.2674 + title: L10N.getStr(title), 1.2675 + data: data, 1.2676 + strings: strings, 1.2677 + totals: totals, 1.2678 + sorted: sorted 1.2679 + }); 1.2680 + 1.2681 + chart.on("click", (_, item) => { 1.2682 + NetMonitorView.RequestsMenu.filterOnlyOn(item.label); 1.2683 + NetMonitorView.showNetworkInspectorView(); 1.2684 + }); 1.2685 + 1.2686 + container.appendChild(chart.node); 1.2687 + }, 1.2688 + 1.2689 + /** 1.2690 + * Sanitizes the data source used for creating charts, to follow the 1.2691 + * data format spec defined in Chart.jsm. 1.2692 + * 1.2693 + * @param array aItems 1.2694 + * A collection of request items used as the data source for the chart. 1.2695 + * @param boolean aEmptyCache 1.2696 + * True if the cache is considered enabled, false for disabled. 1.2697 + */ 1.2698 + _sanitizeChartDataSource: function(aItems, aEmptyCache) { 1.2699 + let data = [ 1.2700 + "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "other" 1.2701 + ].map(e => ({ 1.2702 + cached: 0, 1.2703 + count: 0, 1.2704 + label: e, 1.2705 + size: 0, 1.2706 + time: 0 1.2707 + })); 1.2708 + 1.2709 + for (let requestItem of aItems) { 1.2710 + let details = requestItem.attachment; 1.2711 + let type; 1.2712 + 1.2713 + if (RequestsMenuView.prototype.isHtml(requestItem)) { 1.2714 + type = 0; // "html" 1.2715 + } else if (RequestsMenuView.prototype.isCss(requestItem)) { 1.2716 + type = 1; // "css" 1.2717 + } else if (RequestsMenuView.prototype.isJs(requestItem)) { 1.2718 + type = 2; // "js" 1.2719 + } else if (RequestsMenuView.prototype.isFont(requestItem)) { 1.2720 + type = 4; // "fonts" 1.2721 + } else if (RequestsMenuView.prototype.isImage(requestItem)) { 1.2722 + type = 5; // "images" 1.2723 + } else if (RequestsMenuView.prototype.isMedia(requestItem)) { 1.2724 + type = 6; // "media" 1.2725 + } else if (RequestsMenuView.prototype.isFlash(requestItem)) { 1.2726 + type = 7; // "flash" 1.2727 + } else if (RequestsMenuView.prototype.isXHR(requestItem)) { 1.2728 + // Verify XHR last, to categorize other mime types in their own blobs. 1.2729 + type = 3; // "xhr" 1.2730 + } else { 1.2731 + type = 8; // "other" 1.2732 + } 1.2733 + 1.2734 + if (aEmptyCache || !responseIsFresh(details)) { 1.2735 + data[type].time += details.totalTime || 0; 1.2736 + data[type].size += details.contentSize || 0; 1.2737 + } else { 1.2738 + data[type].cached++; 1.2739 + } 1.2740 + data[type].count++; 1.2741 + } 1.2742 + 1.2743 + return data.filter(e => e.count > 0); 1.2744 + }, 1.2745 +}; 1.2746 + 1.2747 +/** 1.2748 + * DOM query helper. 1.2749 + */ 1.2750 +function $(aSelector, aTarget = document) aTarget.querySelector(aSelector); 1.2751 +function $all(aSelector, aTarget = document) aTarget.querySelectorAll(aSelector); 1.2752 + 1.2753 +/** 1.2754 + * Helper for getting an nsIURL instance out of a string. 1.2755 + */ 1.2756 +function nsIURL(aUrl, aStore = nsIURL.store) { 1.2757 + if (aStore.has(aUrl)) { 1.2758 + return aStore.get(aUrl); 1.2759 + } 1.2760 + let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); 1.2761 + aStore.set(aUrl, uri); 1.2762 + return uri; 1.2763 +} 1.2764 +nsIURL.store = new Map(); 1.2765 + 1.2766 +/** 1.2767 + * Parse a url's query string into its components 1.2768 + * 1.2769 + * @param string aQueryString 1.2770 + * The query part of a url 1.2771 + * @return array 1.2772 + * Array of query params {name, value} 1.2773 + */ 1.2774 +function parseQueryString(aQueryString) { 1.2775 + // Make sure there's at least one param available. 1.2776 + // Be careful here, params don't necessarily need to have values, so 1.2777 + // no need to verify the existence of a "=". 1.2778 + if (!aQueryString) { 1.2779 + return; 1.2780 + } 1.2781 + // Turn the params string into an array containing { name: value } tuples. 1.2782 + let paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map(e => 1.2783 + let (param = e.split("=")) { 1.2784 + name: param[0] ? NetworkHelper.convertToUnicode(unescape(param[0])) : "", 1.2785 + value: param[1] ? NetworkHelper.convertToUnicode(unescape(param[1])) : "" 1.2786 + }); 1.2787 + return paramsArray; 1.2788 +} 1.2789 + 1.2790 +/** 1.2791 + * Parse text representation of multiple HTTP headers. 1.2792 + * 1.2793 + * @param string aText 1.2794 + * Text of headers 1.2795 + * @return array 1.2796 + * Array of headers info {name, value} 1.2797 + */ 1.2798 +function parseHeadersText(aText) { 1.2799 + return parseRequestText(aText, "\\S+?", ":"); 1.2800 +} 1.2801 + 1.2802 +/** 1.2803 + * Parse readable text list of a query string. 1.2804 + * 1.2805 + * @param string aText 1.2806 + * Text of query string represetation 1.2807 + * @return array 1.2808 + * Array of query params {name, value} 1.2809 + */ 1.2810 +function parseQueryText(aText) { 1.2811 + return parseRequestText(aText, ".+?", "="); 1.2812 +} 1.2813 + 1.2814 +/** 1.2815 + * Parse a text representation of a name[divider]value list with 1.2816 + * the given name regex and divider character. 1.2817 + * 1.2818 + * @param string aText 1.2819 + * Text of list 1.2820 + * @return array 1.2821 + * Array of headers info {name, value} 1.2822 + */ 1.2823 +function parseRequestText(aText, aName, aDivider) { 1.2824 + let regex = new RegExp("(" + aName + ")\\" + aDivider + "\\s*(.+)"); 1.2825 + let pairs = []; 1.2826 + for (let line of aText.split("\n")) { 1.2827 + let matches; 1.2828 + if (matches = regex.exec(line)) { 1.2829 + let [, name, value] = matches; 1.2830 + pairs.push({name: name, value: value}); 1.2831 + } 1.2832 + } 1.2833 + return pairs; 1.2834 +} 1.2835 + 1.2836 +/** 1.2837 + * Write out a list of headers into a chunk of text 1.2838 + * 1.2839 + * @param array aHeaders 1.2840 + * Array of headers info {name, value} 1.2841 + * @return string aText 1.2842 + * List of headers in text format 1.2843 + */ 1.2844 +function writeHeaderText(aHeaders) { 1.2845 + return [(name + ": " + value) for ({name, value} of aHeaders)].join("\n"); 1.2846 +} 1.2847 + 1.2848 +/** 1.2849 + * Write out a list of query params into a chunk of text 1.2850 + * 1.2851 + * @param array aParams 1.2852 + * Array of query params {name, value} 1.2853 + * @return string 1.2854 + * List of query params in text format 1.2855 + */ 1.2856 +function writeQueryText(aParams) { 1.2857 + return [(name + "=" + value) for ({name, value} of aParams)].join("\n"); 1.2858 +} 1.2859 + 1.2860 +/** 1.2861 + * Write out a list of query params into a query string 1.2862 + * 1.2863 + * @param array aParams 1.2864 + * Array of query params {name, value} 1.2865 + * @return string 1.2866 + * Query string that can be appended to a url. 1.2867 + */ 1.2868 +function writeQueryString(aParams) { 1.2869 + return [(name + "=" + value) for ({name, value} of aParams)].join("&"); 1.2870 +} 1.2871 + 1.2872 +/** 1.2873 + * Checks if the "Expiration Calculations" defined in section 13.2.4 of the 1.2874 + * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers. 1.2875 + * 1.2876 + * @param object 1.2877 + * An object containing the { responseHeaders, status } properties. 1.2878 + * @return boolean 1.2879 + * True if the response is fresh and loaded from cache. 1.2880 + */ 1.2881 +function responseIsFresh({ responseHeaders, status }) { 1.2882 + // Check for a "304 Not Modified" status and response headers availability. 1.2883 + if (status != 304 || !responseHeaders) { 1.2884 + return false; 1.2885 + } 1.2886 + 1.2887 + let list = responseHeaders.headers; 1.2888 + let cacheControl = list.filter(e => e.name.toLowerCase() == "cache-control")[0]; 1.2889 + let expires = list.filter(e => e.name.toLowerCase() == "expires")[0]; 1.2890 + 1.2891 + // Check the "Cache-Control" header for a maximum age value. 1.2892 + if (cacheControl) { 1.2893 + let maxAgeMatch = 1.2894 + cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) || 1.2895 + cacheControl.value.match(/max-age\s*=\s*(\d+)/); 1.2896 + 1.2897 + if (maxAgeMatch && maxAgeMatch.pop() > 0) { 1.2898 + return true; 1.2899 + } 1.2900 + } 1.2901 + 1.2902 + // Check the "Expires" header for a valid date. 1.2903 + if (expires && Date.parse(expires.value)) { 1.2904 + return true; 1.2905 + } 1.2906 + 1.2907 + return false; 1.2908 +} 1.2909 + 1.2910 +/** 1.2911 + * 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. 1.2912 + * 1.2913 + * @param function callback 1.2914 + * Function to execute execute when data-key is present in event.target. 1.2915 + * @return function 1.2916 + * Wrapped function with the target data-key as the first argument. 1.2917 + */ 1.2918 +function getKeyWithEvent(callback) { 1.2919 + return function(event) { 1.2920 + var key = event.target.getAttribute("data-key"); 1.2921 + if (key) { 1.2922 + callback.call(null, key); 1.2923 + } 1.2924 + }; 1.2925 +} 1.2926 + 1.2927 +/** 1.2928 + * Preliminary setup for the NetMonitorView object. 1.2929 + */ 1.2930 +NetMonitorView.Toolbar = new ToolbarView(); 1.2931 +NetMonitorView.RequestsMenu = new RequestsMenuView(); 1.2932 +NetMonitorView.Sidebar = new SidebarView(); 1.2933 +NetMonitorView.CustomRequest = new CustomRequestView(); 1.2934 +NetMonitorView.NetworkDetails = new NetworkDetailsView(); 1.2935 +NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();