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