michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const HTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: const EPSILON = 0.001; michael@0: const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400; // 100 KB in bytes michael@0: const RESIZE_REFRESH_RATE = 50; // ms michael@0: const REQUESTS_REFRESH_RATE = 50; // ms michael@0: const REQUESTS_HEADERS_SAFE_BOUNDS = 30; // px michael@0: const REQUESTS_TOOLTIP_POSITION = "topcenter bottomleft"; michael@0: const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px michael@0: const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // px michael@0: const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms michael@0: const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px michael@0: const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms michael@0: const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3; michael@0: const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px michael@0: const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144]; michael@0: const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte michael@0: const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte michael@0: const DEFAULT_HTTP_VERSION = "HTTP/1.1"; michael@0: const REQUEST_TIME_DECIMALS = 2; michael@0: const HEADERS_SIZE_DECIMALS = 3; michael@0: const CONTENT_SIZE_DECIMALS = 2; michael@0: const CONTENT_MIME_TYPE_ABBREVIATIONS = { michael@0: "ecmascript": "js", michael@0: "javascript": "js", michael@0: "x-javascript": "js" michael@0: }; michael@0: const CONTENT_MIME_TYPE_MAPPINGS = { michael@0: "/ecmascript": Editor.modes.js, michael@0: "/javascript": Editor.modes.js, michael@0: "/x-javascript": Editor.modes.js, michael@0: "/html": Editor.modes.html, michael@0: "/xhtml": Editor.modes.html, michael@0: "/xml": Editor.modes.html, michael@0: "/atom": Editor.modes.html, michael@0: "/soap": Editor.modes.html, michael@0: "/rdf": Editor.modes.css, michael@0: "/rss": Editor.modes.css, michael@0: "/css": Editor.modes.css michael@0: }; michael@0: const DEFAULT_EDITOR_CONFIG = { michael@0: mode: Editor.modes.text, michael@0: readOnly: true, michael@0: lineNumbers: true michael@0: }; michael@0: const GENERIC_VARIABLES_VIEW_SETTINGS = { michael@0: lazyEmpty: true, michael@0: lazyEmptyDelay: 10, // ms michael@0: searchEnabled: true, michael@0: editableValueTooltip: "", michael@0: editableNameTooltip: "", michael@0: preventDisableOnChange: true, michael@0: preventDescriptorModifiers: true, michael@0: eval: () => {} michael@0: }; michael@0: const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px michael@0: michael@0: /** michael@0: * Object defining the network monitor view components. michael@0: */ michael@0: let NetMonitorView = { michael@0: /** michael@0: * Initializes the network monitor view. michael@0: */ michael@0: initialize: function() { michael@0: this._initializePanes(); michael@0: michael@0: this.Toolbar.initialize(); michael@0: this.RequestsMenu.initialize(); michael@0: this.NetworkDetails.initialize(); michael@0: this.CustomRequest.initialize(); michael@0: }, michael@0: michael@0: /** michael@0: * Destroys the network monitor view. michael@0: */ michael@0: destroy: function() { michael@0: this.Toolbar.destroy(); michael@0: this.RequestsMenu.destroy(); michael@0: this.NetworkDetails.destroy(); michael@0: this.CustomRequest.destroy(); michael@0: michael@0: this._destroyPanes(); michael@0: }, michael@0: michael@0: /** michael@0: * Initializes the UI for all the displayed panes. michael@0: */ michael@0: _initializePanes: function() { michael@0: dumpn("Initializing the NetMonitorView panes"); michael@0: michael@0: this._body = $("#body"); michael@0: this._detailsPane = $("#details-pane"); michael@0: this._detailsPaneToggleButton = $("#details-pane-toggle"); michael@0: michael@0: this._collapsePaneString = L10N.getStr("collapseDetailsPane"); michael@0: this._expandPaneString = L10N.getStr("expandDetailsPane"); michael@0: michael@0: this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth); michael@0: this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight); michael@0: this.toggleDetailsPane({ visible: false }); michael@0: michael@0: // Disable the performance statistics mode. michael@0: if (!Prefs.statistics) { michael@0: $("#request-menu-context-perf").hidden = true; michael@0: $("#notice-perf-message").hidden = true; michael@0: $("#requests-menu-network-summary-button").hidden = true; michael@0: $("#requests-menu-network-summary-label").hidden = true; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Destroys the UI for all the displayed panes. michael@0: */ michael@0: _destroyPanes: function() { michael@0: dumpn("Destroying the NetMonitorView panes"); michael@0: michael@0: Prefs.networkDetailsWidth = this._detailsPane.getAttribute("width"); michael@0: Prefs.networkDetailsHeight = this._detailsPane.getAttribute("height"); michael@0: michael@0: this._detailsPane = null; michael@0: this._detailsPaneToggleButton = null; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the visibility state of the network details pane. michael@0: * @return boolean michael@0: */ michael@0: get detailsPaneHidden() { michael@0: return this._detailsPane.hasAttribute("pane-collapsed"); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the network details pane hidden or visible. michael@0: * michael@0: * @param object aFlags michael@0: * An object containing some of the following properties: michael@0: * - visible: true if the pane should be shown, false to hide michael@0: * - animated: true to display an animation on toggle michael@0: * - delayed: true to wait a few cycles before toggle michael@0: * - callback: a function to invoke when the toggle finishes michael@0: * @param number aTabIndex [optional] michael@0: * The index of the intended selected tab in the details pane. michael@0: */ michael@0: toggleDetailsPane: function(aFlags, aTabIndex) { michael@0: let pane = this._detailsPane; michael@0: let button = this._detailsPaneToggleButton; michael@0: michael@0: ViewHelpers.togglePane(aFlags, pane); michael@0: michael@0: if (aFlags.visible) { michael@0: this._body.removeAttribute("pane-collapsed"); michael@0: button.removeAttribute("pane-collapsed"); michael@0: button.setAttribute("tooltiptext", this._collapsePaneString); michael@0: } else { michael@0: this._body.setAttribute("pane-collapsed", ""); michael@0: button.setAttribute("pane-collapsed", ""); michael@0: button.setAttribute("tooltiptext", this._expandPaneString); michael@0: } michael@0: michael@0: if (aTabIndex !== undefined) { michael@0: $("#event-details-pane").selectedIndex = aTabIndex; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Gets the current mode for this tool. michael@0: * @return string (e.g, "network-inspector-view" or "network-statistics-view") michael@0: */ michael@0: get currentFrontendMode() { michael@0: return this._body.selectedPanel.id; michael@0: }, michael@0: michael@0: /** michael@0: * Toggles between the frontend view modes ("Inspector" vs. "Statistics"). michael@0: */ michael@0: toggleFrontendMode: function() { michael@0: if (this.currentFrontendMode != "network-inspector-view") { michael@0: this.showNetworkInspectorView(); michael@0: } else { michael@0: this.showNetworkStatisticsView(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Switches to the "Inspector" frontend view mode. michael@0: */ michael@0: showNetworkInspectorView: function() { michael@0: this._body.selectedPanel = $("#network-inspector-view"); michael@0: this.RequestsMenu._flushWaterfallViews(true); michael@0: }, michael@0: michael@0: /** michael@0: * Switches to the "Statistics" frontend view mode. michael@0: */ michael@0: showNetworkStatisticsView: function() { michael@0: this._body.selectedPanel = $("#network-statistics-view"); michael@0: michael@0: let controller = NetMonitorController; michael@0: let requestsView = this.RequestsMenu; michael@0: let statisticsView = this.PerformanceStatistics; michael@0: michael@0: Task.spawn(function*() { michael@0: statisticsView.displayPlaceholderCharts(); michael@0: yield controller.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED); michael@0: michael@0: try { michael@0: // • The response headers and status code are required for determining michael@0: // whether a response is "fresh" (cacheable). michael@0: // • The response content size and request total time are necessary for michael@0: // populating the statistics view. michael@0: // • The response mime type is used for categorization. michael@0: yield whenDataAvailable(requestsView.attachments, [ michael@0: "responseHeaders", "status", "contentSize", "mimeType", "totalTime" michael@0: ]); michael@0: } catch (ex) { michael@0: // Timed out while waiting for data. Continue with what we have. michael@0: DevToolsUtils.reportException("showNetworkStatisticsView", ex); michael@0: } michael@0: michael@0: statisticsView.createPrimedCacheChart(requestsView.items); michael@0: statisticsView.createEmptyCacheChart(requestsView.items); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Lazily initializes and returns a promise for a Editor instance. michael@0: * michael@0: * @param string aId michael@0: * The id of the editor placeholder node. michael@0: * @return object michael@0: * A promise that is resolved when the editor is available. michael@0: */ michael@0: editor: function(aId) { michael@0: dumpn("Getting a NetMonitorView editor: " + aId); michael@0: michael@0: if (this._editorPromises.has(aId)) { michael@0: return this._editorPromises.get(aId); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: this._editorPromises.set(aId, deferred.promise); michael@0: michael@0: // Initialize the source editor and store the newly created instance michael@0: // in the ether of a resolved promise's value. michael@0: let editor = new Editor(DEFAULT_EDITOR_CONFIG); michael@0: editor.appendTo($(aId)).then(() => deferred.resolve(editor)); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _body: null, michael@0: _detailsPane: null, michael@0: _detailsPaneToggleButton: null, michael@0: _collapsePaneString: "", michael@0: _expandPaneString: "", michael@0: _editorPromises: new Map() michael@0: }; michael@0: michael@0: /** michael@0: * Functions handling the toolbar view: expand/collapse button etc. michael@0: */ michael@0: function ToolbarView() { michael@0: dumpn("ToolbarView was instantiated"); michael@0: michael@0: this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this); michael@0: } michael@0: michael@0: ToolbarView.prototype = { michael@0: /** michael@0: * Initialization function, called when the debugger is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the ToolbarView"); michael@0: michael@0: this._detailsPaneToggleButton = $("#details-pane-toggle"); michael@0: this._detailsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the debugger is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the ToolbarView"); michael@0: michael@0: this._detailsPaneToggleButton.removeEventListener("mousedown", this._onTogglePanesPressed, false); michael@0: }, michael@0: michael@0: /** michael@0: * Listener handling the toggle button click event. michael@0: */ michael@0: _onTogglePanesPressed: function() { michael@0: let requestsMenu = NetMonitorView.RequestsMenu; michael@0: let selectedIndex = requestsMenu.selectedIndex; michael@0: michael@0: // Make sure there's a selection if the button is pressed, to avoid michael@0: // showing an empty network details pane. michael@0: if (selectedIndex == -1 && requestsMenu.itemCount) { michael@0: requestsMenu.selectedIndex = 0; michael@0: } else { michael@0: requestsMenu.selectedIndex = -1; michael@0: } michael@0: }, michael@0: michael@0: _detailsPaneToggleButton: null michael@0: }; michael@0: michael@0: /** michael@0: * Functions handling the requests menu (containing details about each request, michael@0: * like status, method, file, domain, as well as a waterfall representing michael@0: * timing imformation). michael@0: */ michael@0: function RequestsMenuView() { michael@0: dumpn("RequestsMenuView was instantiated"); michael@0: michael@0: this._flushRequests = this._flushRequests.bind(this); michael@0: this._onHover = this._onHover.bind(this); michael@0: this._onSelect = this._onSelect.bind(this); michael@0: this._onSwap = this._onSwap.bind(this); michael@0: this._onResize = this._onResize.bind(this); michael@0: this._byFile = this._byFile.bind(this); michael@0: this._byDomain = this._byDomain.bind(this); michael@0: this._byType = this._byType.bind(this); michael@0: } michael@0: michael@0: RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { michael@0: /** michael@0: * Initialization function, called when the network monitor is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the RequestsMenuView"); michael@0: michael@0: this.widget = new SideMenuWidget($("#requests-menu-contents")); michael@0: this._splitter = $("#network-inspector-view-splitter"); michael@0: this._summary = $("#requests-menu-network-summary-label"); michael@0: this._summary.setAttribute("value", L10N.getStr("networkMenu.empty")); michael@0: michael@0: Prefs.filters.forEach(type => this.filterOn(type)); michael@0: this.sortContents(this._byTiming); michael@0: michael@0: this.allowFocusOnRightClick = true; michael@0: this.maintainSelectionVisible = true; michael@0: this.widget.autoscrollWithAppendedItems = true; michael@0: michael@0: this.widget.addEventListener("select", this._onSelect, false); michael@0: this.widget.addEventListener("swap", this._onSwap, false); michael@0: this._splitter.addEventListener("mousemove", this._onResize, false); michael@0: window.addEventListener("resize", this._onResize, false); michael@0: michael@0: this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this)); michael@0: this.requestsMenuFilterEvent = getKeyWithEvent(this.filterOn.bind(this)); michael@0: this.reqeustsMenuClearEvent = this.clear.bind(this); michael@0: this._onContextShowing = this._onContextShowing.bind(this); michael@0: this._onContextNewTabCommand = this.openRequestInTab.bind(this); michael@0: this._onContextCopyUrlCommand = this.copyUrl.bind(this); michael@0: this._onContextCopyImageAsDataUriCommand = this.copyImageAsDataUri.bind(this); michael@0: this._onContextResendCommand = this.cloneSelectedRequest.bind(this); michael@0: this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode(); michael@0: michael@0: this.sendCustomRequestEvent = this.sendCustomRequest.bind(this); michael@0: this.closeCustomRequestEvent = this.closeCustomRequest.bind(this); michael@0: this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this); michael@0: michael@0: $("#toolbar-labels").addEventListener("click", this.requestsMenuSortEvent, false); michael@0: $("#requests-menu-footer").addEventListener("click", this.requestsMenuFilterEvent, false); michael@0: $("#requests-menu-clear-button").addEventListener("click", this.reqeustsMenuClearEvent, false); michael@0: $("#network-request-popup").addEventListener("popupshowing", this._onContextShowing, false); michael@0: $("#request-menu-context-newtab").addEventListener("command", this._onContextNewTabCommand, false); michael@0: $("#request-menu-context-copy-url").addEventListener("command", this._onContextCopyUrlCommand, false); michael@0: $("#request-menu-context-copy-image-as-data-uri").addEventListener("command", this._onContextCopyImageAsDataUriCommand, false); michael@0: michael@0: window.once("connected", this._onConnect.bind(this)); michael@0: }, michael@0: michael@0: _onConnect: function() { michael@0: if (NetMonitorController.supportsCustomRequest) { michael@0: $("#request-menu-context-resend").addEventListener("command", this._onContextResendCommand, false); michael@0: $("#custom-request-send-button").addEventListener("click", this.sendCustomRequestEvent, false); michael@0: $("#custom-request-close-button").addEventListener("click", this.closeCustomRequestEvent, false); michael@0: $("#headers-summary-resend").addEventListener("click", this.cloneSelectedRequestEvent, false); michael@0: } else { michael@0: $("#request-menu-context-resend").hidden = true; michael@0: $("#headers-summary-resend").hidden = true; michael@0: } michael@0: michael@0: if (NetMonitorController.supportsPerfStats) { michael@0: $("#request-menu-context-perf").addEventListener("command", this._onContextPerfCommand, false); michael@0: $("#requests-menu-perf-notice-button").addEventListener("command", this._onContextPerfCommand, false); michael@0: $("#requests-menu-network-summary-button").addEventListener("command", this._onContextPerfCommand, false); michael@0: $("#requests-menu-network-summary-label").addEventListener("click", this._onContextPerfCommand, false); michael@0: $("#network-statistics-back-button").addEventListener("command", this._onContextPerfCommand, false); michael@0: } else { michael@0: $("#notice-perf-message").hidden = true; michael@0: $("#request-menu-context-perf").hidden = true; michael@0: $("#requests-menu-network-summary-button").hidden = true; michael@0: $("#requests-menu-network-summary-label").hidden = true; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the network monitor is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the SourcesView"); michael@0: michael@0: Prefs.filters = this._activeFilters; michael@0: michael@0: this.widget.removeEventListener("select", this._onSelect, false); michael@0: this.widget.removeEventListener("swap", this._onSwap, false); michael@0: this._splitter.removeEventListener("mousemove", this._onResize, false); michael@0: window.removeEventListener("resize", this._onResize, false); michael@0: michael@0: $("#toolbar-labels").removeEventListener("click", this.requestsMenuSortEvent, false); michael@0: $("#requests-menu-footer").removeEventListener("click", this.requestsMenuFilterEvent, false); michael@0: $("#requests-menu-clear-button").removeEventListener("click", this.reqeustsMenuClearEvent, false); michael@0: $("#network-request-popup").removeEventListener("popupshowing", this._onContextShowing, false); michael@0: $("#request-menu-context-newtab").removeEventListener("command", this._onContextNewTabCommand, false); michael@0: $("#request-menu-context-copy-url").removeEventListener("command", this._onContextCopyUrlCommand, false); michael@0: $("#request-menu-context-copy-image-as-data-uri").removeEventListener("command", this._onContextCopyImageAsDataUriCommand, false); michael@0: $("#request-menu-context-resend").removeEventListener("command", this._onContextResendCommand, false); michael@0: $("#request-menu-context-perf").removeEventListener("command", this._onContextPerfCommand, false); michael@0: michael@0: $("#requests-menu-perf-notice-button").removeEventListener("command", this._onContextPerfCommand, false); michael@0: $("#requests-menu-network-summary-button").removeEventListener("command", this._onContextPerfCommand, false); michael@0: $("#requests-menu-network-summary-label").removeEventListener("click", this._onContextPerfCommand, false); michael@0: $("#network-statistics-back-button").removeEventListener("command", this._onContextPerfCommand, false); michael@0: michael@0: $("#custom-request-send-button").removeEventListener("click", this.sendCustomRequestEvent, false); michael@0: $("#custom-request-close-button").removeEventListener("click", this.closeCustomRequestEvent, false); michael@0: $("#headers-summary-resend").removeEventListener("click", this.cloneSelectedRequestEvent, false); michael@0: }, michael@0: michael@0: /** michael@0: * Resets this container (removes all the networking information). michael@0: */ michael@0: reset: function() { michael@0: this.empty(); michael@0: this._firstRequestStartedMillis = -1; michael@0: this._lastRequestEndedMillis = -1; michael@0: }, michael@0: michael@0: /** michael@0: * Specifies if this view may be updated lazily. michael@0: */ michael@0: lazyUpdate: true, michael@0: michael@0: /** michael@0: * Adds a network request to this container. michael@0: * michael@0: * @param string aId michael@0: * An identifier coming from the network monitor controller. michael@0: * @param string aStartedDateTime michael@0: * A string representation of when the request was started, which michael@0: * can be parsed by Date (for example "2012-09-17T19:50:03.699Z"). michael@0: * @param string aMethod michael@0: * Specifies the request method (e.g. "GET", "POST", etc.) michael@0: * @param string aUrl michael@0: * Specifies the request's url. michael@0: * @param boolean aIsXHR michael@0: * True if this request was initiated via XHR. michael@0: */ michael@0: addRequest: function(aId, aStartedDateTime, aMethod, aUrl, aIsXHR) { michael@0: // Convert the received date/time string to a unix timestamp. michael@0: let unixTime = Date.parse(aStartedDateTime); michael@0: michael@0: // Create the element node for the network request item. michael@0: let menuView = this._createMenuView(aMethod, aUrl); michael@0: michael@0: // Remember the first and last event boundaries. michael@0: this._registerFirstRequestStart(unixTime); michael@0: this._registerLastRequestEnd(unixTime); michael@0: michael@0: // Append a network request item to this container. michael@0: let requestItem = this.push([menuView, aId], { michael@0: attachment: { michael@0: startedDeltaMillis: unixTime - this._firstRequestStartedMillis, michael@0: startedMillis: unixTime, michael@0: method: aMethod, michael@0: url: aUrl, michael@0: isXHR: aIsXHR michael@0: } michael@0: }); michael@0: michael@0: // Create a tooltip for the newly appended network request item. michael@0: let requestTooltip = requestItem.attachment.tooltip = new Tooltip(document, { michael@0: closeOnEvents: [{ michael@0: emitter: $("#requests-menu-contents"), michael@0: event: "scroll", michael@0: useCapture: true michael@0: }] michael@0: }); michael@0: michael@0: $("#details-pane-toggle").disabled = false; michael@0: $("#requests-menu-empty-notice").hidden = true; michael@0: michael@0: this.refreshSummary(); michael@0: this.refreshZebra(); michael@0: this.refreshTooltip(requestItem); michael@0: michael@0: if (aId == this._preferredItemId) { michael@0: this.selectedItem = requestItem; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Opens selected item in a new tab. michael@0: */ michael@0: openRequestInTab: function() { michael@0: let win = Services.wm.getMostRecentWindow("navigator:browser"); michael@0: let selected = this.selectedItem.attachment; michael@0: win.openUILinkIn(selected.url, "tab", { relatedToCurrent: true }); michael@0: }, michael@0: michael@0: /** michael@0: * Copy the request url from the currently selected item. michael@0: */ michael@0: copyUrl: function() { michael@0: let selected = this.selectedItem.attachment; michael@0: clipboardHelper.copyString(selected.url, document); michael@0: }, michael@0: michael@0: /** michael@0: * Copy a cURL command from the currently selected item. michael@0: */ michael@0: copyAsCurl: function() { michael@0: let selected = this.selectedItem.attachment; michael@0: michael@0: Task.spawn(function*() { michael@0: // Create a sanitized object for the Curl command generator. michael@0: let data = { michael@0: url: selected.url, michael@0: method: selected.method, michael@0: headers: [], michael@0: httpVersion: selected.httpVersion, michael@0: postDataText: null michael@0: }; michael@0: michael@0: // Fetch header values. michael@0: for (let { name, value } of selected.requestHeaders.headers) { michael@0: let text = yield gNetwork.getString(value); michael@0: data.headers.push({ name: name, value: text }); michael@0: } michael@0: michael@0: // Fetch the request payload. michael@0: if (selected.requestPostData) { michael@0: let postData = selected.requestPostData.postData.text; michael@0: data.postDataText = yield gNetwork.getString(postData); michael@0: } michael@0: michael@0: clipboardHelper.copyString(Curl.generateCommand(data), document); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Copy image as data uri. michael@0: */ michael@0: copyImageAsDataUri: function() { michael@0: let selected = this.selectedItem.attachment; michael@0: let { mimeType, text, encoding } = selected.responseContent.content; michael@0: michael@0: gNetwork.getString(text).then(aString => { michael@0: let data = "data:" + mimeType + ";" + encoding + "," + aString; michael@0: clipboardHelper.copyString(data, document); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Create a new custom request form populated with the data from michael@0: * the currently selected request. michael@0: */ michael@0: cloneSelectedRequest: function() { michael@0: let selected = this.selectedItem.attachment; michael@0: michael@0: // Create the element node for the network request item. michael@0: let menuView = this._createMenuView(selected.method, selected.url); michael@0: michael@0: // Append a network request item to this container. michael@0: let newItem = this.push([menuView], { michael@0: attachment: Object.create(selected, { michael@0: isCustom: { value: true } michael@0: }) michael@0: }); michael@0: michael@0: // Immediately switch to new request pane. michael@0: this.selectedItem = newItem; michael@0: }, michael@0: michael@0: /** michael@0: * Send a new HTTP request using the data in the custom request form. michael@0: */ michael@0: sendCustomRequest: function() { michael@0: let selected = this.selectedItem.attachment; michael@0: michael@0: let data = { michael@0: url: selected.url, michael@0: method: selected.method, michael@0: httpVersion: selected.httpVersion, michael@0: }; michael@0: if (selected.requestHeaders) { michael@0: data.headers = selected.requestHeaders.headers; michael@0: } michael@0: if (selected.requestPostData) { michael@0: data.body = selected.requestPostData.postData.text; michael@0: } michael@0: michael@0: NetMonitorController.webConsoleClient.sendHTTPRequest(data, aResponse => { michael@0: let id = aResponse.eventActor.actor; michael@0: this._preferredItemId = id; michael@0: }); michael@0: michael@0: this.closeCustomRequest(); michael@0: }, michael@0: michael@0: /** michael@0: * Remove the currently selected custom request. michael@0: */ michael@0: closeCustomRequest: function() { michael@0: this.remove(this.selectedItem); michael@0: NetMonitorView.Sidebar.toggle(false); michael@0: }, michael@0: michael@0: /** michael@0: * Filters all network requests in this container by a specified type. michael@0: * michael@0: * @param string aType michael@0: * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media" michael@0: * "flash" or "other". michael@0: */ michael@0: filterOn: function(aType = "all") { michael@0: if (aType === "all") { michael@0: // The filter "all" is special as it doesn't toggle. michael@0: // - If some filters are selected and 'all' is clicked, the previously michael@0: // selected filters will be disabled and 'all' is the only active one. michael@0: // - If 'all' is already selected, do nothing. michael@0: if (this._activeFilters.indexOf("all") !== -1) { michael@0: return; michael@0: } michael@0: michael@0: // Uncheck all other filters and select 'all'. Must create a copy as michael@0: // _disableFilter removes the filters from the list while it's being michael@0: // iterated. 'all' will be enabled automatically by _disableFilter once michael@0: // the last filter is disabled. michael@0: this._activeFilters.slice().forEach(this._disableFilter, this); michael@0: } michael@0: else if (this._activeFilters.indexOf(aType) === -1) { michael@0: this._enableFilter(aType); michael@0: } michael@0: else { michael@0: this._disableFilter(aType); michael@0: } michael@0: michael@0: this.filterContents(this._filterPredicate); michael@0: this.refreshSummary(); michael@0: this.refreshZebra(); michael@0: }, michael@0: michael@0: /** michael@0: * Same as `filterOn`, except that it only allows a single type exclusively. michael@0: * michael@0: * @param string aType michael@0: * @see RequestsMenuView.prototype.fitlerOn michael@0: */ michael@0: filterOnlyOn: function(aType = "all") { michael@0: this._activeFilters.slice().forEach(this._disableFilter, this); michael@0: this.filterOn(aType); michael@0: }, michael@0: michael@0: /** michael@0: * Disables the given filter, its button and toggles 'all' on if the filter to michael@0: * be disabled is the last one active. michael@0: * michael@0: * @param string aType michael@0: * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media" michael@0: * "flash" or "other". michael@0: */ michael@0: _disableFilter: function (aType) { michael@0: // Remove the filter from list of active filters. michael@0: this._activeFilters.splice(this._activeFilters.indexOf(aType), 1); michael@0: michael@0: // Remove the checked status from the filter. michael@0: let target = $("#requests-menu-filter-" + aType + "-button"); michael@0: target.removeAttribute("checked"); michael@0: michael@0: // Check if the filter disabled was the last one. If so, toggle all on. michael@0: if (this._activeFilters.length === 0) { michael@0: this._enableFilter("all"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Enables the given filter, its button and toggles 'all' off if the filter to michael@0: * be enabled is the first one active. michael@0: * michael@0: * @param string aType michael@0: * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media" michael@0: * "flash" or "other". michael@0: */ michael@0: _enableFilter: function (aType) { michael@0: // Make sure this is a valid filter type. michael@0: if (Object.keys(this._allFilterPredicates).indexOf(aType) == -1) { michael@0: return; michael@0: } michael@0: michael@0: // Add the filter to the list of active filters. michael@0: this._activeFilters.push(aType); michael@0: michael@0: // Add the checked status to the filter button. michael@0: let target = $("#requests-menu-filter-" + aType + "-button"); michael@0: target.setAttribute("checked", true); michael@0: michael@0: // Check if 'all' was selected before. If so, disable it. michael@0: if (aType !== "all" && this._activeFilters.indexOf("all") !== -1) { michael@0: this._disableFilter("all"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns a predicate that can be used to test if a request matches any of michael@0: * the active filters. michael@0: */ michael@0: get _filterPredicate() { michael@0: let filterPredicates = this._allFilterPredicates; michael@0: michael@0: if (this._activeFilters.length === 1) { michael@0: // The simplest case: only one filter active. michael@0: return filterPredicates[this._activeFilters[0]].bind(this); michael@0: } else { michael@0: // Multiple filters active. michael@0: return requestItem => { michael@0: return this._activeFilters.some(filterName => { michael@0: return filterPredicates[filterName].call(this, requestItem); michael@0: }); michael@0: }; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns an object with all the filter predicates as [key: function] pairs. michael@0: */ michael@0: get _allFilterPredicates() ({ michael@0: all: () => true, michael@0: html: this.isHtml, michael@0: css: this.isCss, michael@0: js: this.isJs, michael@0: xhr: this.isXHR, michael@0: fonts: this.isFont, michael@0: images: this.isImage, michael@0: media: this.isMedia, michael@0: flash: this.isFlash, michael@0: other: this.isOther michael@0: }), michael@0: michael@0: /** michael@0: * Sorts all network requests in this container by a specified detail. michael@0: * michael@0: * @param string aType michael@0: * Either "status", "method", "file", "domain", "type", "size" or michael@0: * "waterfall". michael@0: */ michael@0: sortBy: function(aType = "waterfall") { michael@0: let target = $("#requests-menu-" + aType + "-button"); michael@0: let headers = document.querySelectorAll(".requests-menu-header-button"); michael@0: michael@0: for (let header of headers) { michael@0: if (header != target) { michael@0: header.removeAttribute("sorted"); michael@0: header.removeAttribute("tooltiptext"); michael@0: } michael@0: } michael@0: michael@0: let direction = ""; michael@0: if (target) { michael@0: if (target.getAttribute("sorted") == "ascending") { michael@0: target.setAttribute("sorted", direction = "descending"); michael@0: target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedDesc")); michael@0: } else { michael@0: target.setAttribute("sorted", direction = "ascending"); michael@0: target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedAsc")); michael@0: } michael@0: } michael@0: michael@0: // Sort by whatever was requested. michael@0: switch (aType) { michael@0: case "status": michael@0: if (direction == "ascending") { michael@0: this.sortContents(this._byStatus); michael@0: } else { michael@0: this.sortContents((a, b) => !this._byStatus(a, b)); michael@0: } michael@0: break; michael@0: case "method": michael@0: if (direction == "ascending") { michael@0: this.sortContents(this._byMethod); michael@0: } else { michael@0: this.sortContents((a, b) => !this._byMethod(a, b)); michael@0: } michael@0: break; michael@0: case "file": michael@0: if (direction == "ascending") { michael@0: this.sortContents(this._byFile); michael@0: } else { michael@0: this.sortContents((a, b) => !this._byFile(a, b)); michael@0: } michael@0: break; michael@0: case "domain": michael@0: if (direction == "ascending") { michael@0: this.sortContents(this._byDomain); michael@0: } else { michael@0: this.sortContents((a, b) => !this._byDomain(a, b)); michael@0: } michael@0: break; michael@0: case "type": michael@0: if (direction == "ascending") { michael@0: this.sortContents(this._byType); michael@0: } else { michael@0: this.sortContents((a, b) => !this._byType(a, b)); michael@0: } michael@0: break; michael@0: case "size": michael@0: if (direction == "ascending") { michael@0: this.sortContents(this._bySize); michael@0: } else { michael@0: this.sortContents((a, b) => !this._bySize(a, b)); michael@0: } michael@0: break; michael@0: case "waterfall": michael@0: if (direction == "ascending") { michael@0: this.sortContents(this._byTiming); michael@0: } else { michael@0: this.sortContents((a, b) => !this._byTiming(a, b)); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: this.refreshSummary(); michael@0: this.refreshZebra(); michael@0: }, michael@0: michael@0: /** michael@0: * Removes all network requests and closes the sidebar if open. michael@0: */ michael@0: clear: function() { michael@0: NetMonitorView.Sidebar.toggle(false); michael@0: $("#details-pane-toggle").disabled = true; michael@0: michael@0: this.empty(); michael@0: this.refreshSummary(); michael@0: }, michael@0: michael@0: /** michael@0: * Predicates used when filtering items. michael@0: * michael@0: * @param object aItem michael@0: * The filtered item. michael@0: * @return boolean michael@0: * True if the item should be visible, false otherwise. michael@0: */ michael@0: isHtml: function({ attachment: { mimeType } }) michael@0: mimeType && mimeType.contains("/html"), michael@0: michael@0: isCss: function({ attachment: { mimeType } }) michael@0: mimeType && mimeType.contains("/css"), michael@0: michael@0: isJs: function({ attachment: { mimeType } }) michael@0: mimeType && ( michael@0: mimeType.contains("/ecmascript") || michael@0: mimeType.contains("/javascript") || michael@0: mimeType.contains("/x-javascript")), michael@0: michael@0: isXHR: function({ attachment: { isXHR } }) michael@0: isXHR, michael@0: michael@0: isFont: function({ attachment: { url, mimeType } }) // Fonts are a mess. michael@0: (mimeType && ( michael@0: mimeType.contains("font/") || michael@0: mimeType.contains("/font"))) || michael@0: url.contains(".eot") || michael@0: url.contains(".ttf") || michael@0: url.contains(".otf") || michael@0: url.contains(".woff"), michael@0: michael@0: isImage: function({ attachment: { mimeType } }) michael@0: mimeType && mimeType.contains("image/"), michael@0: michael@0: isMedia: function({ attachment: { mimeType } }) // Not including images. michael@0: mimeType && ( michael@0: mimeType.contains("audio/") || michael@0: mimeType.contains("video/") || michael@0: mimeType.contains("model/")), michael@0: michael@0: isFlash: function({ attachment: { url, mimeType } }) // Flash is a mess. michael@0: (mimeType && ( michael@0: mimeType.contains("/x-flv") || michael@0: mimeType.contains("/x-shockwave-flash"))) || michael@0: url.contains(".swf") || michael@0: url.contains(".flv"), michael@0: michael@0: isOther: function(e) michael@0: !this.isHtml(e) && !this.isCss(e) && !this.isJs(e) && !this.isXHR(e) && michael@0: !this.isFont(e) && !this.isImage(e) && !this.isMedia(e) && !this.isFlash(e), michael@0: michael@0: /** michael@0: * Predicates used when sorting items. michael@0: * michael@0: * @param object aFirst michael@0: * The first item used in the comparison. michael@0: * @param object aSecond michael@0: * The second item used in the comparison. michael@0: * @return number michael@0: * -1 to sort aFirst to a lower index than aSecond michael@0: * 0 to leave aFirst and aSecond unchanged with respect to each other michael@0: * 1 to sort aSecond to a lower index than aFirst michael@0: */ michael@0: _byTiming: function({ attachment: first }, { attachment: second }) michael@0: first.startedMillis > second.startedMillis, michael@0: michael@0: _byStatus: function({ attachment: first }, { attachment: second }) michael@0: first.status == second.status michael@0: ? first.startedMillis > second.startedMillis michael@0: : first.status > second.status, michael@0: michael@0: _byMethod: function({ attachment: first }, { attachment: second }) michael@0: first.method == second.method michael@0: ? first.startedMillis > second.startedMillis michael@0: : first.method > second.method, michael@0: michael@0: _byFile: function({ attachment: first }, { attachment: second }) { michael@0: let firstUrl = this._getUriNameWithQuery(first.url).toLowerCase(); michael@0: let secondUrl = this._getUriNameWithQuery(second.url).toLowerCase(); michael@0: return firstUrl == secondUrl michael@0: ? first.startedMillis > second.startedMillis michael@0: : firstUrl > secondUrl; michael@0: }, michael@0: michael@0: _byDomain: function({ attachment: first }, { attachment: second }) { michael@0: let firstDomain = this._getUriHostPort(first.url).toLowerCase(); michael@0: let secondDomain = this._getUriHostPort(second.url).toLowerCase(); michael@0: return firstDomain == secondDomain michael@0: ? first.startedMillis > second.startedMillis michael@0: : firstDomain > secondDomain; michael@0: }, michael@0: michael@0: _byType: function({ attachment: first }, { attachment: second }) { michael@0: let firstType = this._getAbbreviatedMimeType(first.mimeType).toLowerCase(); michael@0: let secondType = this._getAbbreviatedMimeType(second.mimeType).toLowerCase(); michael@0: return firstType == secondType michael@0: ? first.startedMillis > second.startedMillis michael@0: : firstType > secondType; michael@0: }, michael@0: michael@0: _bySize: function({ attachment: first }, { attachment: second }) michael@0: first.contentSize > second.contentSize, michael@0: michael@0: /** michael@0: * Refreshes the status displayed in this container's footer, providing michael@0: * concise information about all requests. michael@0: */ michael@0: refreshSummary: function() { michael@0: let visibleItems = this.visibleItems; michael@0: let visibleRequestsCount = visibleItems.length; michael@0: if (!visibleRequestsCount) { michael@0: this._summary.setAttribute("value", L10N.getStr("networkMenu.empty")); michael@0: return; michael@0: } michael@0: michael@0: let totalBytes = this._getTotalBytesOfRequests(visibleItems); michael@0: let totalMillis = michael@0: this._getNewestRequest(visibleItems).attachment.endedMillis - michael@0: this._getOldestRequest(visibleItems).attachment.startedMillis; michael@0: michael@0: // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals michael@0: let str = PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary")); michael@0: this._summary.setAttribute("value", str michael@0: .replace("#1", visibleRequestsCount) michael@0: .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, CONTENT_SIZE_DECIMALS)) michael@0: .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, REQUEST_TIME_DECIMALS)) michael@0: ); michael@0: }, michael@0: michael@0: /** michael@0: * Adds odd/even attributes to all the visible items in this container. michael@0: */ michael@0: refreshZebra: function() { michael@0: let visibleItems = this.visibleItems; michael@0: michael@0: for (let i = 0, len = visibleItems.length; i < len; i++) { michael@0: let requestItem = visibleItems[i]; michael@0: let requestTarget = requestItem.target; michael@0: michael@0: if (i % 2 == 0) { michael@0: requestTarget.setAttribute("even", ""); michael@0: requestTarget.removeAttribute("odd"); michael@0: } else { michael@0: requestTarget.setAttribute("odd", ""); michael@0: requestTarget.removeAttribute("even"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Refreshes the toggling anchor for the specified item's tooltip. michael@0: * michael@0: * @param object aItem michael@0: * The network request item in this container. michael@0: */ michael@0: refreshTooltip: function(aItem) { michael@0: let tooltip = aItem.attachment.tooltip; michael@0: tooltip.hide(); michael@0: tooltip.startTogglingOnHover(aItem.target, this._onHover); michael@0: tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION; michael@0: }, michael@0: michael@0: /** michael@0: * Schedules adding additional information to a network request. michael@0: * michael@0: * @param string aId michael@0: * An identifier coming from the network monitor controller. michael@0: * @param object aData michael@0: * An object containing several { key: value } tuples of network info. michael@0: * Supported keys are "httpVersion", "status", "statusText" etc. michael@0: */ michael@0: updateRequest: function(aId, aData) { michael@0: // Prevent interference from zombie updates received after target closed. michael@0: if (NetMonitorView._isDestroyed) { michael@0: return; michael@0: } michael@0: this._updateQueue.push([aId, aData]); michael@0: michael@0: // Lazy updating is disabled in some tests. michael@0: if (!this.lazyUpdate) { michael@0: return void this._flushRequests(); michael@0: } michael@0: // Allow requests to settle down first. michael@0: setNamedTimeout( michael@0: "update-requests", REQUESTS_REFRESH_RATE, () => this._flushRequests()); michael@0: }, michael@0: michael@0: /** michael@0: * Starts adding all queued additional information about network requests. michael@0: */ michael@0: _flushRequests: function() { michael@0: // For each queued additional information packet, get the corresponding michael@0: // request item in the view and update it based on the specified data. michael@0: for (let [id, data] of this._updateQueue) { michael@0: let requestItem = this.getItemByValue(id); michael@0: if (!requestItem) { michael@0: // Packet corresponds to a dead request item, target navigated. michael@0: continue; michael@0: } michael@0: michael@0: // Each information packet may contain several { key: value } tuples of michael@0: // network info, so update the view based on each one. michael@0: for (let key in data) { michael@0: let value = data[key]; michael@0: if (value === undefined) { michael@0: // The information in the packet is empty, it can be safely ignored. michael@0: continue; michael@0: } michael@0: michael@0: switch (key) { michael@0: case "requestHeaders": michael@0: requestItem.attachment.requestHeaders = value; michael@0: break; michael@0: case "requestCookies": michael@0: requestItem.attachment.requestCookies = value; michael@0: break; michael@0: case "requestPostData": michael@0: // Search the POST data upload stream for request headers and add michael@0: // them to a separate store, different from the classic headers. michael@0: // XXX: Be really careful here! We're creating a function inside michael@0: // a loop, so remember the actual request item we want to modify. michael@0: let currentItem = requestItem; michael@0: let currentStore = { headers: [], headersSize: 0 }; michael@0: michael@0: Task.spawn(function*() { michael@0: let postData = yield gNetwork.getString(value.postData.text); michael@0: let payloadHeaders = CurlUtils.getHeadersFromMultipartText(postData); michael@0: michael@0: currentStore.headers = payloadHeaders; michael@0: currentStore.headersSize = payloadHeaders.reduce( michael@0: (acc, { name, value }) => acc + name.length + value.length + 2, 0); michael@0: michael@0: // The `getString` promise is async, so we need to refresh the michael@0: // information displayed in the network details pane again here. michael@0: refreshNetworkDetailsPaneIfNecessary(currentItem); michael@0: }); michael@0: michael@0: requestItem.attachment.requestPostData = value; michael@0: requestItem.attachment.requestHeadersFromUploadStream = currentStore; michael@0: break; michael@0: case "responseHeaders": michael@0: requestItem.attachment.responseHeaders = value; michael@0: break; michael@0: case "responseCookies": michael@0: requestItem.attachment.responseCookies = value; michael@0: break; michael@0: case "httpVersion": michael@0: requestItem.attachment.httpVersion = value; michael@0: break; michael@0: case "status": michael@0: requestItem.attachment.status = value; michael@0: this.updateMenuView(requestItem, key, value); michael@0: break; michael@0: case "statusText": michael@0: requestItem.attachment.statusText = value; michael@0: this.updateMenuView(requestItem, key, michael@0: requestItem.attachment.status + " " + michael@0: requestItem.attachment.statusText); michael@0: break; michael@0: case "headersSize": michael@0: requestItem.attachment.headersSize = value; michael@0: break; michael@0: case "contentSize": michael@0: requestItem.attachment.contentSize = value; michael@0: this.updateMenuView(requestItem, key, value); michael@0: break; michael@0: case "mimeType": michael@0: requestItem.attachment.mimeType = value; michael@0: this.updateMenuView(requestItem, key, value); michael@0: break; michael@0: case "responseContent": michael@0: // If there's no mime type available when the response content michael@0: // is received, assume text/plain as a fallback. michael@0: if (!requestItem.attachment.mimeType) { michael@0: requestItem.attachment.mimeType = "text/plain"; michael@0: this.updateMenuView(requestItem, "mimeType", "text/plain"); michael@0: } michael@0: requestItem.attachment.responseContent = value; michael@0: this.updateMenuView(requestItem, key, value); michael@0: break; michael@0: case "totalTime": michael@0: requestItem.attachment.totalTime = value; michael@0: requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value; michael@0: this.updateMenuView(requestItem, key, value); michael@0: this._registerLastRequestEnd(requestItem.attachment.endedMillis); michael@0: break; michael@0: case "eventTimings": michael@0: requestItem.attachment.eventTimings = value; michael@0: this._createWaterfallView(requestItem, value.timings); michael@0: break; michael@0: } michael@0: } michael@0: refreshNetworkDetailsPaneIfNecessary(requestItem); michael@0: } michael@0: michael@0: /** michael@0: * Refreshes the information displayed in the sidebar, in case this update michael@0: * may have additional information about a request which isn't shown yet michael@0: * in the network details pane. michael@0: * michael@0: * @param object aRequestItem michael@0: * The item to repopulate the sidebar with in case it's selected in michael@0: * this requests menu. michael@0: */ michael@0: function refreshNetworkDetailsPaneIfNecessary(aRequestItem) { michael@0: let selectedItem = NetMonitorView.RequestsMenu.selectedItem; michael@0: if (selectedItem == aRequestItem) { michael@0: NetMonitorView.NetworkDetails.populate(selectedItem.attachment); michael@0: } michael@0: } michael@0: michael@0: // We're done flushing all the requests, clear the update queue. michael@0: this._updateQueue = []; michael@0: michael@0: // Make sure all the requests are sorted and filtered. michael@0: // Freshly added requests may not yet contain all the information required michael@0: // for sorting and filtering predicates, so this is done each time the michael@0: // network requests table is flushed (don't worry, events are drained first michael@0: // so this doesn't happen once per network event update). michael@0: this.sortContents(); michael@0: this.filterContents(); michael@0: this.refreshSummary(); michael@0: this.refreshZebra(); michael@0: michael@0: // Rescale all the waterfalls so that everything is visible at once. michael@0: this._flushWaterfallViews(); michael@0: }, michael@0: michael@0: /** michael@0: * Customization function for creating an item's UI. michael@0: * michael@0: * @param string aMethod michael@0: * Specifies the request method (e.g. "GET", "POST", etc.) michael@0: * @param string aUrl michael@0: * Specifies the request's url. michael@0: * @return nsIDOMNode michael@0: * The network request view. michael@0: */ michael@0: _createMenuView: function(aMethod, aUrl) { michael@0: let template = $("#requests-menu-item-template"); michael@0: let fragment = document.createDocumentFragment(); michael@0: michael@0: this.updateMenuView(template, 'method', aMethod); michael@0: this.updateMenuView(template, 'url', aUrl); michael@0: michael@0: let waterfall = $(".requests-menu-waterfall", template); michael@0: waterfall.style.backgroundImage = this._cachedWaterfallBackground; michael@0: michael@0: // Flatten the DOM by removing one redundant box (the template container). michael@0: for (let node of template.childNodes) { michael@0: fragment.appendChild(node.cloneNode(true)); michael@0: } michael@0: michael@0: return fragment; michael@0: }, michael@0: michael@0: /** michael@0: * Updates the information displayed in a network request item view. michael@0: * michael@0: * @param object aItem michael@0: * The network request item in this container. michael@0: * @param string aKey michael@0: * The type of information that is to be updated. michael@0: * @param any aValue michael@0: * The new value to be shown. michael@0: * @return object michael@0: * A promise that is resolved once the information is displayed. michael@0: */ michael@0: updateMenuView: Task.async(function*(aItem, aKey, aValue) { michael@0: let target = aItem.target || aItem; michael@0: michael@0: switch (aKey) { michael@0: case "method": { michael@0: let node = $(".requests-menu-method", target); michael@0: node.setAttribute("value", aValue); michael@0: break; michael@0: } michael@0: case "url": { michael@0: let uri; michael@0: try { michael@0: uri = nsIURL(aValue); michael@0: } catch(e) { michael@0: break; // User input may not make a well-formed url yet. michael@0: } michael@0: let nameWithQuery = this._getUriNameWithQuery(uri); michael@0: let hostPort = this._getUriHostPort(uri); michael@0: michael@0: let file = $(".requests-menu-file", target); michael@0: file.setAttribute("value", nameWithQuery); michael@0: file.setAttribute("tooltiptext", nameWithQuery); michael@0: michael@0: let domain = $(".requests-menu-domain", target); michael@0: domain.setAttribute("value", hostPort); michael@0: domain.setAttribute("tooltiptext", hostPort); michael@0: break; michael@0: } michael@0: case "status": { michael@0: let node = $(".requests-menu-status", target); michael@0: let codeNode = $(".requests-menu-status-code", target); michael@0: codeNode.setAttribute("value", aValue); michael@0: node.setAttribute("code", aValue); michael@0: break; michael@0: } michael@0: case "statusText": { michael@0: let node = $(".requests-menu-status-and-method", target); michael@0: node.setAttribute("tooltiptext", aValue); michael@0: break; michael@0: } michael@0: case "contentSize": { michael@0: let kb = aValue / 1024; michael@0: let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS); michael@0: let node = $(".requests-menu-size", target); michael@0: let text = L10N.getFormatStr("networkMenu.sizeKB", size); michael@0: node.setAttribute("value", text); michael@0: node.setAttribute("tooltiptext", text); michael@0: break; michael@0: } michael@0: case "mimeType": { michael@0: let type = this._getAbbreviatedMimeType(aValue); michael@0: let node = $(".requests-menu-type", target); michael@0: let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type; michael@0: node.setAttribute("value", text); michael@0: node.setAttribute("tooltiptext", aValue); michael@0: break; michael@0: } michael@0: case "responseContent": { michael@0: let { mimeType } = aItem.attachment; michael@0: let { text, encoding } = aValue.content; michael@0: michael@0: if (mimeType.contains("image/")) { michael@0: let responseBody = yield gNetwork.getString(text); michael@0: let node = $(".requests-menu-icon", aItem.target); michael@0: node.src = "data:" + mimeType + ";" + encoding + "," + responseBody; michael@0: node.setAttribute("type", "thumbnail"); michael@0: node.removeAttribute("hidden"); michael@0: michael@0: window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED); michael@0: } michael@0: break; michael@0: } michael@0: case "totalTime": { michael@0: let node = $(".requests-menu-timings-total", target); michael@0: let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer michael@0: node.setAttribute("value", text); michael@0: node.setAttribute("tooltiptext", text); michael@0: break; michael@0: } michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Creates a waterfall representing timing information in a network request item view. michael@0: * michael@0: * @param object aItem michael@0: * The network request item in this container. michael@0: * @param object aTimings michael@0: * An object containing timing information. michael@0: */ michael@0: _createWaterfallView: function(aItem, aTimings) { michael@0: let { target, attachment } = aItem; michael@0: let sections = ["dns", "connect", "send", "wait", "receive"]; michael@0: // Skipping "blocked" because it doesn't work yet. michael@0: michael@0: let timingsNode = $(".requests-menu-timings", target); michael@0: let timingsTotal = $(".requests-menu-timings-total", timingsNode); michael@0: michael@0: // Add a set of boxes representing timing information. michael@0: for (let key of sections) { michael@0: let width = aTimings[key]; michael@0: michael@0: // Don't render anything if it surely won't be visible. michael@0: // One millisecond == one unscaled pixel. michael@0: if (width > 0) { michael@0: let timingBox = document.createElement("hbox"); michael@0: timingBox.className = "requests-menu-timings-box " + key; michael@0: timingBox.setAttribute("width", width); michael@0: timingsNode.insertBefore(timingBox, timingsTotal); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Rescales and redraws all the waterfall views in this container. michael@0: * michael@0: * @param boolean aReset michael@0: * True if this container's width was changed. michael@0: */ michael@0: _flushWaterfallViews: function(aReset) { michael@0: // Don't paint things while the waterfall view isn't even visible, michael@0: // or there are no items added to this container. michael@0: if (NetMonitorView.currentFrontendMode != "network-inspector-view" || !this.itemCount) { michael@0: return; michael@0: } michael@0: michael@0: // To avoid expensive operations like getBoundingClientRect() and michael@0: // rebuilding the waterfall background each time a new request comes in, michael@0: // stuff is cached. However, in certain scenarios like when the window michael@0: // is resized, this needs to be invalidated. michael@0: if (aReset) { michael@0: this._cachedWaterfallWidth = 0; michael@0: } michael@0: michael@0: // Determine the scaling to be applied to all the waterfalls so that michael@0: // everything is visible at once. One millisecond == one unscaled pixel. michael@0: let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS; michael@0: let longestWidth = this._lastRequestEndedMillis - this._firstRequestStartedMillis; michael@0: let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1); michael@0: michael@0: // Redraw and set the canvas background for each waterfall view. michael@0: this._showWaterfallDivisionLabels(scale); michael@0: this._drawWaterfallBackground(scale); michael@0: this._flushWaterfallBackgrounds(); michael@0: michael@0: // Apply CSS transforms to each waterfall in this container totalTime michael@0: // accurately translate and resize as needed. michael@0: for (let { target, attachment } of this) { michael@0: let timingsNode = $(".requests-menu-timings", target); michael@0: let totalNode = $(".requests-menu-timings-total", target); michael@0: let direction = window.isRTL ? -1 : 1; michael@0: michael@0: // Render the timing information at a specific horizontal translation michael@0: // based on the delta to the first monitored event network. michael@0: let translateX = "translateX(" + (direction * attachment.startedDeltaMillis) + "px)"; michael@0: michael@0: // Based on the total time passed until the last request, rescale michael@0: // all the waterfalls to a reasonable size. michael@0: let scaleX = "scaleX(" + scale + ")"; michael@0: michael@0: // Certain nodes should not be scaled, even if they're children of michael@0: // another scaled node. In this case, apply a reversed transformation. michael@0: let revScaleX = "scaleX(" + (1 / scale) + ")"; michael@0: michael@0: timingsNode.style.transform = scaleX + " " + translateX; michael@0: totalNode.style.transform = revScaleX; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Creates the labels displayed on the waterfall header in this container. michael@0: * michael@0: * @param number aScale michael@0: * The current waterfall scale. michael@0: */ michael@0: _showWaterfallDivisionLabels: function(aScale) { michael@0: let container = $("#requests-menu-waterfall-button"); michael@0: let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS; michael@0: michael@0: // Nuke all existing labels. michael@0: while (container.hasChildNodes()) { michael@0: container.firstChild.remove(); michael@0: } michael@0: michael@0: // Build new millisecond tick labels... michael@0: let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE; michael@0: let optimalTickIntervalFound = false; michael@0: michael@0: while (!optimalTickIntervalFound) { michael@0: // Ignore any divisions that would end up being too close to each other. michael@0: let scaledStep = aScale * timingStep; michael@0: if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) { michael@0: timingStep <<= 1; michael@0: continue; michael@0: } michael@0: optimalTickIntervalFound = true; michael@0: michael@0: // Insert one label for each division on the current scale. michael@0: let fragment = document.createDocumentFragment(); michael@0: let direction = window.isRTL ? -1 : 1; michael@0: michael@0: for (let x = 0; x < availableWidth; x += scaledStep) { michael@0: let translateX = "translateX(" + ((direction * x) | 0) + "px)"; michael@0: let millisecondTime = x / aScale; michael@0: michael@0: let normalizedTime = millisecondTime; michael@0: let divisionScale = "millisecond"; michael@0: michael@0: // If the division is greater than 1 minute. michael@0: if (normalizedTime > 60000) { michael@0: normalizedTime /= 60000; michael@0: divisionScale = "minute"; michael@0: } michael@0: // If the division is greater than 1 second. michael@0: else if (normalizedTime > 1000) { michael@0: normalizedTime /= 1000; michael@0: divisionScale = "second"; michael@0: } michael@0: michael@0: // Showing too many decimals is bad UX. michael@0: if (divisionScale == "millisecond") { michael@0: normalizedTime |= 0; michael@0: } else { michael@0: normalizedTime = L10N.numberWithDecimals(normalizedTime, REQUEST_TIME_DECIMALS); michael@0: } michael@0: michael@0: let node = document.createElement("label"); michael@0: let text = L10N.getFormatStr("networkMenu." + divisionScale, normalizedTime); michael@0: node.className = "plain requests-menu-timings-division"; michael@0: node.setAttribute("division-scale", divisionScale); michael@0: node.style.transform = translateX; michael@0: michael@0: node.setAttribute("value", text); michael@0: fragment.appendChild(node); michael@0: } michael@0: container.appendChild(fragment); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Creates the background displayed on each waterfall view in this container. michael@0: * michael@0: * @param number aScale michael@0: * The current waterfall scale. michael@0: */ michael@0: _drawWaterfallBackground: function(aScale) { michael@0: if (!this._canvas || !this._ctx) { michael@0: this._canvas = document.createElementNS(HTML_NS, "canvas"); michael@0: this._ctx = this._canvas.getContext("2d"); michael@0: } michael@0: let canvas = this._canvas; michael@0: let ctx = this._ctx; michael@0: michael@0: // Nuke the context. michael@0: let canvasWidth = canvas.width = this._waterfallWidth; michael@0: let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis. michael@0: michael@0: // Start over. michael@0: let imageData = ctx.createImageData(canvasWidth, canvasHeight); michael@0: let pixelArray = imageData.data; michael@0: michael@0: let buf = new ArrayBuffer(pixelArray.length); michael@0: let buf8 = new Uint8ClampedArray(buf); michael@0: let data32 = new Uint32Array(buf); michael@0: michael@0: // Build new millisecond tick lines... michael@0: let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE; michael@0: let [r, g, b] = REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB; michael@0: let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN; michael@0: let optimalTickIntervalFound = false; michael@0: michael@0: while (!optimalTickIntervalFound) { michael@0: // Ignore any divisions that would end up being too close to each other. michael@0: let scaledStep = aScale * timingStep; michael@0: if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) { michael@0: timingStep <<= 1; michael@0: continue; michael@0: } michael@0: optimalTickIntervalFound = true; michael@0: michael@0: // Insert one pixel for each division on each scale. michael@0: for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) { michael@0: let increment = scaledStep * Math.pow(2, i); michael@0: for (let x = 0; x < canvasWidth; x += increment) { michael@0: let position = (window.isRTL ? canvasWidth - x : x) | 0; michael@0: data32[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r; michael@0: } michael@0: alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD; michael@0: } michael@0: } michael@0: michael@0: // Flush the image data and cache the waterfall background. michael@0: pixelArray.set(buf8); michael@0: ctx.putImageData(imageData, 0, 0); michael@0: this._cachedWaterfallBackground = "url(" + canvas.toDataURL() + ")"; michael@0: }, michael@0: michael@0: /** michael@0: * Reapplies the current waterfall background on all request items. michael@0: */ michael@0: _flushWaterfallBackgrounds: function() { michael@0: for (let { target } of this) { michael@0: let waterfallNode = $(".requests-menu-waterfall", target); michael@0: waterfallNode.style.backgroundImage = this._cachedWaterfallBackground; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The selection listener for this container. michael@0: */ michael@0: _onSelect: function({ detail: item }) { michael@0: if (item) { michael@0: NetMonitorView.Sidebar.populate(item.attachment); michael@0: NetMonitorView.Sidebar.toggle(true); michael@0: } else { michael@0: NetMonitorView.Sidebar.toggle(false); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The swap listener for this container. michael@0: * Called when two items switch places, when the contents are sorted. michael@0: */ michael@0: _onSwap: function({ detail: [firstItem, secondItem] }) { michael@0: // Sorting will create new anchor nodes for all the swapped request items michael@0: // in this container, so it's necessary to refresh the Tooltip instances. michael@0: this.refreshTooltip(firstItem); michael@0: this.refreshTooltip(secondItem); michael@0: }, michael@0: michael@0: /** michael@0: * The predicate used when deciding whether a popup should be shown michael@0: * over a request item or not. michael@0: * michael@0: * @param nsIDOMNode aTarget michael@0: * The element node currently being hovered. michael@0: * @param object aTooltip michael@0: * The current tooltip instance. michael@0: */ michael@0: _onHover: function(aTarget, aTooltip) { michael@0: let requestItem = this.getItemForElement(aTarget); michael@0: if (!requestItem || !requestItem.attachment.responseContent) { michael@0: return; michael@0: } michael@0: michael@0: let hovered = requestItem.attachment; michael@0: let { url } = hovered; michael@0: let { mimeType, text, encoding } = hovered.responseContent.content; michael@0: michael@0: if (mimeType && mimeType.contains("image/") && ( michael@0: aTarget.classList.contains("requests-menu-icon") || michael@0: aTarget.classList.contains("requests-menu-file"))) michael@0: { michael@0: return gNetwork.getString(text).then(aString => { michael@0: let anchor = $(".requests-menu-icon", requestItem.target); michael@0: let src = "data:" + mimeType + ";" + encoding + "," + aString; michael@0: aTooltip.setImageContent(src, { maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM }); michael@0: return anchor; michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The resize listener for this container's window. michael@0: */ michael@0: _onResize: function(e) { michael@0: // Allow requests to settle down first. michael@0: setNamedTimeout( michael@0: "resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true)); michael@0: }, michael@0: michael@0: /** michael@0: * Handle the context menu opening. Hide items if no request is selected. michael@0: */ michael@0: _onContextShowing: function() { michael@0: let selectedItem = this.selectedItem; michael@0: michael@0: let resendElement = $("#request-menu-context-resend"); michael@0: resendElement.hidden = !NetMonitorController.supportsCustomRequest || michael@0: !selectedItem || selectedItem.attachment.isCustom; michael@0: michael@0: let copyUrlElement = $("#request-menu-context-copy-url"); michael@0: copyUrlElement.hidden = !selectedItem; michael@0: michael@0: let copyAsCurlElement = $("#request-menu-context-copy-as-curl"); michael@0: copyAsCurlElement.hidden = !selectedItem || !selectedItem.attachment.responseContent; michael@0: michael@0: let copyImageAsDataUriElement = $("#request-menu-context-copy-image-as-data-uri"); michael@0: copyImageAsDataUriElement.hidden = !selectedItem || michael@0: !selectedItem.attachment.responseContent || michael@0: !selectedItem.attachment.responseContent.content.mimeType.contains("image/"); michael@0: michael@0: let newTabElement = $("#request-menu-context-newtab"); michael@0: newTabElement.hidden = !selectedItem; michael@0: }, michael@0: michael@0: /** michael@0: * Checks if the specified unix time is the first one to be known of, michael@0: * and saves it if so. michael@0: * michael@0: * @param number aUnixTime michael@0: * The milliseconds to check and save. michael@0: */ michael@0: _registerFirstRequestStart: function(aUnixTime) { michael@0: if (this._firstRequestStartedMillis == -1) { michael@0: this._firstRequestStartedMillis = aUnixTime; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Checks if the specified unix time is the last one to be known of, michael@0: * and saves it if so. michael@0: * michael@0: * @param number aUnixTime michael@0: * The milliseconds to check and save. michael@0: */ michael@0: _registerLastRequestEnd: function(aUnixTime) { michael@0: if (this._lastRequestEndedMillis < aUnixTime) { michael@0: this._lastRequestEndedMillis = aUnixTime; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Helpers for getting details about an nsIURL. michael@0: * michael@0: * @param nsIURL | string aUrl michael@0: * @return string michael@0: */ michael@0: _getUriNameWithQuery: function(aUrl) { michael@0: if (!(aUrl instanceof Ci.nsIURL)) { michael@0: aUrl = nsIURL(aUrl); michael@0: } michael@0: let name = NetworkHelper.convertToUnicode(unescape(aUrl.fileName)) || "/"; michael@0: let query = NetworkHelper.convertToUnicode(unescape(aUrl.query)); michael@0: return name + (query ? "?" + query : ""); michael@0: }, michael@0: _getUriHostPort: function(aUrl) { michael@0: if (!(aUrl instanceof Ci.nsIURL)) { michael@0: aUrl = nsIURL(aUrl); michael@0: } michael@0: return NetworkHelper.convertToUnicode(unescape(aUrl.hostPort)); michael@0: }, michael@0: michael@0: /** michael@0: * Helper for getting an abbreviated string for a mime type. michael@0: * michael@0: * @param string aMimeType michael@0: * @return string michael@0: */ michael@0: _getAbbreviatedMimeType: function(aMimeType) { michael@0: if (!aMimeType) { michael@0: return ""; michael@0: } michael@0: return (aMimeType.split(";")[0].split("/")[1] || "").split("+")[0]; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the total number of bytes representing the cumulated content size of michael@0: * a set of requests. Returns 0 for an empty set. michael@0: * michael@0: * @param array aItemsArray michael@0: * @return number michael@0: */ michael@0: _getTotalBytesOfRequests: function(aItemsArray) { michael@0: if (!aItemsArray.length) { michael@0: return 0; michael@0: } michael@0: return aItemsArray.reduce((prev, curr) => prev + curr.attachment.contentSize || 0, 0); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the oldest (first performed) request in a set. Returns null for an michael@0: * empty set. michael@0: * michael@0: * @param array aItemsArray michael@0: * @return object michael@0: */ michael@0: _getOldestRequest: function(aItemsArray) { michael@0: if (!aItemsArray.length) { michael@0: return null; michael@0: } michael@0: return aItemsArray.reduce((prev, curr) => michael@0: prev.attachment.startedMillis < curr.attachment.startedMillis ? prev : curr); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the newest (latest performed) request in a set. Returns null for an michael@0: * empty set. michael@0: * michael@0: * @param array aItemsArray michael@0: * @return object michael@0: */ michael@0: _getNewestRequest: function(aItemsArray) { michael@0: if (!aItemsArray.length) { michael@0: return null; michael@0: } michael@0: return aItemsArray.reduce((prev, curr) => michael@0: prev.attachment.startedMillis > curr.attachment.startedMillis ? prev : curr); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the available waterfall width in this container. michael@0: * @return number michael@0: */ michael@0: get _waterfallWidth() { michael@0: if (this._cachedWaterfallWidth == 0) { michael@0: let container = $("#requests-menu-toolbar"); michael@0: let waterfall = $("#requests-menu-waterfall-header-box"); michael@0: let containerBounds = container.getBoundingClientRect(); michael@0: let waterfallBounds = waterfall.getBoundingClientRect(); michael@0: if (!window.isRTL) { michael@0: this._cachedWaterfallWidth = containerBounds.width - waterfallBounds.left; michael@0: } else { michael@0: this._cachedWaterfallWidth = waterfallBounds.right; michael@0: } michael@0: } michael@0: return this._cachedWaterfallWidth; michael@0: }, michael@0: michael@0: _splitter: null, michael@0: _summary: null, michael@0: _canvas: null, michael@0: _ctx: null, michael@0: _cachedWaterfallWidth: 0, michael@0: _cachedWaterfallBackground: "", michael@0: _firstRequestStartedMillis: -1, michael@0: _lastRequestEndedMillis: -1, michael@0: _updateQueue: [], michael@0: _updateTimeout: null, michael@0: _resizeTimeout: null, michael@0: _activeFilters: ["all"] michael@0: }); michael@0: michael@0: /** michael@0: * Functions handling the sidebar details view. michael@0: */ michael@0: function SidebarView() { michael@0: dumpn("SidebarView was instantiated"); michael@0: } michael@0: michael@0: SidebarView.prototype = { michael@0: /** michael@0: * Sets this view hidden or visible. It's visible by default. michael@0: * michael@0: * @param boolean aVisibleFlag michael@0: * Specifies the intended visibility. michael@0: */ michael@0: toggle: function(aVisibleFlag) { michael@0: NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag }); michael@0: NetMonitorView.RequestsMenu._flushWaterfallViews(true); michael@0: }, michael@0: michael@0: /** michael@0: * Populates this view with the specified data. michael@0: * michael@0: * @param object aData michael@0: * The data source (this should be the attachment of a request item). michael@0: * @return object michael@0: * Returns a promise that resolves upon population of the subview. michael@0: */ michael@0: populate: Task.async(function*(aData) { michael@0: let isCustom = aData.isCustom; michael@0: let view = isCustom ? michael@0: NetMonitorView.CustomRequest : michael@0: NetMonitorView.NetworkDetails; michael@0: michael@0: yield view.populate(aData); michael@0: $("#details-pane").selectedIndex = isCustom ? 0 : 1; michael@0: michael@0: window.emit(EVENTS.SIDEBAR_POPULATED); michael@0: }) michael@0: } michael@0: michael@0: /** michael@0: * Functions handling the custom request view. michael@0: */ michael@0: function CustomRequestView() { michael@0: dumpn("CustomRequestView was instantiated"); michael@0: } michael@0: michael@0: CustomRequestView.prototype = { michael@0: /** michael@0: * Initialization function, called when the network monitor is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the CustomRequestView"); michael@0: michael@0: this.updateCustomRequestEvent = getKeyWithEvent(this.onUpdate.bind(this)); michael@0: $("#custom-pane").addEventListener("input", this.updateCustomRequestEvent, false); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the network monitor is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the CustomRequestView"); michael@0: michael@0: $("#custom-pane").removeEventListener("input", this.updateCustomRequestEvent, false); michael@0: }, michael@0: michael@0: /** michael@0: * Populates this view with the specified data. michael@0: * michael@0: * @param object aData michael@0: * The data source (this should be the attachment of a request item). michael@0: * @return object michael@0: * Returns a promise that resolves upon population the view. michael@0: */ michael@0: populate: Task.async(function*(aData) { michael@0: $("#custom-url-value").value = aData.url; michael@0: $("#custom-method-value").value = aData.method; michael@0: this.updateCustomQuery(aData.url); michael@0: michael@0: if (aData.requestHeaders) { michael@0: let headers = aData.requestHeaders.headers; michael@0: $("#custom-headers-value").value = writeHeaderText(headers); michael@0: } michael@0: if (aData.requestPostData) { michael@0: let postData = aData.requestPostData.postData.text; michael@0: $("#custom-postdata-value").value = yield gNetwork.getString(postData); michael@0: } michael@0: michael@0: window.emit(EVENTS.CUSTOMREQUESTVIEW_POPULATED); michael@0: }), michael@0: michael@0: /** michael@0: * Handle user input in the custom request form. michael@0: * michael@0: * @param object aField michael@0: * the field that the user updated. michael@0: */ michael@0: onUpdate: function(aField) { michael@0: let selectedItem = NetMonitorView.RequestsMenu.selectedItem; michael@0: let field = aField; michael@0: let value; michael@0: michael@0: switch(aField) { michael@0: case 'method': michael@0: value = $("#custom-method-value").value.trim(); michael@0: selectedItem.attachment.method = value; michael@0: break; michael@0: case 'url': michael@0: value = $("#custom-url-value").value; michael@0: this.updateCustomQuery(value); michael@0: selectedItem.attachment.url = value; michael@0: break; michael@0: case 'query': michael@0: let query = $("#custom-query-value").value; michael@0: this.updateCustomUrl(query); michael@0: field = 'url'; michael@0: value = $("#custom-url-value").value michael@0: selectedItem.attachment.url = value; michael@0: break; michael@0: case 'body': michael@0: value = $("#custom-postdata-value").value; michael@0: selectedItem.attachment.requestPostData = { postData: { text: value } }; michael@0: break; michael@0: case 'headers': michael@0: let headersText = $("#custom-headers-value").value; michael@0: value = parseHeadersText(headersText); michael@0: selectedItem.attachment.requestHeaders = { headers: value }; michael@0: break; michael@0: } michael@0: michael@0: NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value); michael@0: }, michael@0: michael@0: /** michael@0: * Update the query string field based on the url. michael@0: * michael@0: * @param object aUrl michael@0: * The URL to extract query string from. michael@0: */ michael@0: updateCustomQuery: function(aUrl) { michael@0: let paramsArray = parseQueryString(nsIURL(aUrl).query); michael@0: if (!paramsArray) { michael@0: $("#custom-query").hidden = true; michael@0: return; michael@0: } michael@0: $("#custom-query").hidden = false; michael@0: $("#custom-query-value").value = writeQueryText(paramsArray); michael@0: }, michael@0: michael@0: /** michael@0: * Update the url based on the query string field. michael@0: * michael@0: * @param object aQueryText michael@0: * The contents of the query string field. michael@0: */ michael@0: updateCustomUrl: function(aQueryText) { michael@0: let params = parseQueryText(aQueryText); michael@0: let queryString = writeQueryString(params); michael@0: michael@0: let url = $("#custom-url-value").value; michael@0: let oldQuery = nsIURL(url).query; michael@0: let path = url.replace(oldQuery, queryString); michael@0: michael@0: $("#custom-url-value").value = path; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Functions handling the requests details view. michael@0: */ michael@0: function NetworkDetailsView() { michael@0: dumpn("NetworkDetailsView was instantiated"); michael@0: michael@0: this._onTabSelect = this._onTabSelect.bind(this); michael@0: }; michael@0: michael@0: NetworkDetailsView.prototype = { michael@0: /** michael@0: * Initialization function, called when the network monitor is started. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the NetworkDetailsView"); michael@0: michael@0: this.widget = $("#event-details-pane"); michael@0: michael@0: this._headers = new VariablesView($("#all-headers"), michael@0: Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, { michael@0: emptyText: L10N.getStr("headersEmptyText"), michael@0: searchPlaceholder: L10N.getStr("headersFilterText") michael@0: })); michael@0: this._cookies = new VariablesView($("#all-cookies"), michael@0: Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, { michael@0: emptyText: L10N.getStr("cookiesEmptyText"), michael@0: searchPlaceholder: L10N.getStr("cookiesFilterText") michael@0: })); michael@0: this._params = new VariablesView($("#request-params"), michael@0: Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, { michael@0: emptyText: L10N.getStr("paramsEmptyText"), michael@0: searchPlaceholder: L10N.getStr("paramsFilterText") michael@0: })); michael@0: this._json = new VariablesView($("#response-content-json"), michael@0: Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, { michael@0: onlyEnumVisible: true, michael@0: searchPlaceholder: L10N.getStr("jsonFilterText") michael@0: })); michael@0: VariablesViewController.attach(this._json); michael@0: michael@0: this._paramsQueryString = L10N.getStr("paramsQueryString"); michael@0: this._paramsFormData = L10N.getStr("paramsFormData"); michael@0: this._paramsPostPayload = L10N.getStr("paramsPostPayload"); michael@0: this._requestHeaders = L10N.getStr("requestHeaders"); michael@0: this._requestHeadersFromUpload = L10N.getStr("requestHeadersFromUpload"); michael@0: this._responseHeaders = L10N.getStr("responseHeaders"); michael@0: this._requestCookies = L10N.getStr("requestCookies"); michael@0: this._responseCookies = L10N.getStr("responseCookies"); michael@0: michael@0: $("tabpanels", this.widget).addEventListener("select", this._onTabSelect); michael@0: }, michael@0: michael@0: /** michael@0: * Destruction function, called when the network monitor is closed. michael@0: */ michael@0: destroy: function() { michael@0: dumpn("Destroying the NetworkDetailsView"); michael@0: }, michael@0: michael@0: /** michael@0: * Populates this view with the specified data. michael@0: * michael@0: * @param object aData michael@0: * The data source (this should be the attachment of a request item). michael@0: * @return object michael@0: * Returns a promise that resolves upon population the view. michael@0: */ michael@0: populate: function(aData) { michael@0: $("#request-params-box").setAttribute("flex", "1"); michael@0: $("#request-params-box").hidden = false; michael@0: $("#request-post-data-textarea-box").hidden = true; michael@0: $("#response-content-info-header").hidden = true; michael@0: $("#response-content-json-box").hidden = true; michael@0: $("#response-content-textarea-box").hidden = true; michael@0: $("#response-content-image-box").hidden = true; michael@0: michael@0: let isHtml = RequestsMenuView.prototype.isHtml({ attachment: aData }); michael@0: michael@0: // Show the "Preview" tabpanel only for plain HTML responses. michael@0: $("#preview-tab").hidden = !isHtml; michael@0: $("#preview-tabpanel").hidden = !isHtml; michael@0: michael@0: // Switch to the "Headers" tabpanel if the "Preview" previously selected michael@0: // and this is not an HTML response. michael@0: if (!isHtml && this.widget.selectedIndex == 5) { michael@0: this.widget.selectedIndex = 0; michael@0: } michael@0: michael@0: this._headers.empty(); michael@0: this._cookies.empty(); michael@0: this._params.empty(); michael@0: this._json.empty(); michael@0: michael@0: this._dataSrc = { src: aData, populated: [] }; michael@0: this._onTabSelect(); michael@0: window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED); michael@0: michael@0: return promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Listener handling the tab selection event. michael@0: */ michael@0: _onTabSelect: function() { michael@0: let { src, populated } = this._dataSrc || {}; michael@0: let tab = this.widget.selectedIndex; michael@0: let view = this; michael@0: michael@0: // Make sure the data source is valid and don't populate the same tab twice. michael@0: if (!src || populated[tab]) { michael@0: return; michael@0: } michael@0: michael@0: Task.spawn(function*() { michael@0: switch (tab) { michael@0: case 0: // "Headers" michael@0: yield view._setSummary(src); michael@0: yield view._setResponseHeaders(src.responseHeaders); michael@0: yield view._setRequestHeaders( michael@0: src.requestHeaders, michael@0: src.requestHeadersFromUploadStream); michael@0: break; michael@0: case 1: // "Cookies" michael@0: yield view._setResponseCookies(src.responseCookies); michael@0: yield view._setRequestCookies(src.requestCookies); michael@0: break; michael@0: case 2: // "Params" michael@0: yield view._setRequestGetParams(src.url); michael@0: yield view._setRequestPostParams( michael@0: src.requestHeaders, michael@0: src.requestHeadersFromUploadStream, michael@0: src.requestPostData); michael@0: break; michael@0: case 3: // "Response" michael@0: yield view._setResponseBody(src.url, src.responseContent); michael@0: break; michael@0: case 4: // "Timings" michael@0: yield view._setTimingsInformation(src.eventTimings); michael@0: break; michael@0: case 5: // "Preview" michael@0: yield view._setHtmlPreview(src.responseContent); michael@0: break; michael@0: } michael@0: populated[tab] = true; michael@0: window.emit(EVENTS.TAB_UPDATED); michael@0: NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible(); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the network request summary shown in this view. michael@0: * michael@0: * @param object aData michael@0: * The data source (this should be the attachment of a request item). michael@0: */ michael@0: _setSummary: function(aData) { michael@0: if (aData.url) { michael@0: let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aData.url)); michael@0: $("#headers-summary-url-value").setAttribute("value", unicodeUrl); michael@0: $("#headers-summary-url-value").setAttribute("tooltiptext", unicodeUrl); michael@0: $("#headers-summary-url").removeAttribute("hidden"); michael@0: } else { michael@0: $("#headers-summary-url").setAttribute("hidden", "true"); michael@0: } michael@0: michael@0: if (aData.method) { michael@0: $("#headers-summary-method-value").setAttribute("value", aData.method); michael@0: $("#headers-summary-method").removeAttribute("hidden"); michael@0: } else { michael@0: $("#headers-summary-method").setAttribute("hidden", "true"); michael@0: } michael@0: michael@0: if (aData.status) { michael@0: $("#headers-summary-status-circle").setAttribute("code", aData.status); michael@0: $("#headers-summary-status-value").setAttribute("value", aData.status + " " + aData.statusText); michael@0: $("#headers-summary-status").removeAttribute("hidden"); michael@0: } else { michael@0: $("#headers-summary-status").setAttribute("hidden", "true"); michael@0: } michael@0: michael@0: if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) { michael@0: $("#headers-summary-version-value").setAttribute("value", aData.httpVersion); michael@0: $("#headers-summary-version").removeAttribute("hidden"); michael@0: } else { michael@0: $("#headers-summary-version").setAttribute("hidden", "true"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sets the network request headers shown in this view. michael@0: * michael@0: * @param object aHeadersResponse michael@0: * The "requestHeaders" message received from the server. michael@0: * @param object aHeadersFromUploadStream michael@0: * The "requestHeadersFromUploadStream" inferred from the POST payload. michael@0: * @return object michael@0: * A promise that resolves when request headers are set. michael@0: */ michael@0: _setRequestHeaders: Task.async(function*(aHeadersResponse, aHeadersFromUploadStream) { michael@0: if (aHeadersResponse && aHeadersResponse.headers.length) { michael@0: yield this._addHeaders(this._requestHeaders, aHeadersResponse); michael@0: } michael@0: if (aHeadersFromUploadStream && aHeadersFromUploadStream.headers.length) { michael@0: yield this._addHeaders(this._requestHeadersFromUpload, aHeadersFromUploadStream); michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Sets the network response headers shown in this view. michael@0: * michael@0: * @param object aResponse michael@0: * The message received from the server. michael@0: * @return object michael@0: * A promise that resolves when response headers are set. michael@0: */ michael@0: _setResponseHeaders: Task.async(function*(aResponse) { michael@0: if (aResponse && aResponse.headers.length) { michael@0: aResponse.headers.sort((a, b) => a.name > b.name); michael@0: yield this._addHeaders(this._responseHeaders, aResponse); michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Populates the headers container in this view with the specified data. michael@0: * michael@0: * @param string aName michael@0: * The type of headers to populate (request or response). michael@0: * @param object aResponse michael@0: * The message received from the server. michael@0: * @return object michael@0: * A promise that resolves when headers are added. michael@0: */ michael@0: _addHeaders: Task.async(function*(aName, aResponse) { michael@0: let kb = aResponse.headersSize / 1024; michael@0: let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS); michael@0: let text = L10N.getFormatStr("networkMenu.sizeKB", size); michael@0: michael@0: let headersScope = this._headers.addScope(aName + " (" + text + ")"); michael@0: headersScope.expanded = true; michael@0: michael@0: for (let header of aResponse.headers) { michael@0: let headerVar = headersScope.addItem(header.name, {}, true); michael@0: let headerValue = yield gNetwork.getString(header.value); michael@0: headerVar.setGrip(headerValue); michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Sets the network request cookies shown in this view. michael@0: * michael@0: * @param object aResponse michael@0: * The message received from the server. michael@0: * @return object michael@0: * A promise that is resolved when the request cookies are set. michael@0: */ michael@0: _setRequestCookies: Task.async(function*(aResponse) { michael@0: if (aResponse && aResponse.cookies.length) { michael@0: aResponse.cookies.sort((a, b) => a.name > b.name); michael@0: yield this._addCookies(this._requestCookies, aResponse); michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Sets the network response cookies shown in this view. michael@0: * michael@0: * @param object aResponse michael@0: * The message received from the server. michael@0: * @return object michael@0: * A promise that is resolved when the response cookies are set. michael@0: */ michael@0: _setResponseCookies: Task.async(function*(aResponse) { michael@0: if (aResponse && aResponse.cookies.length) { michael@0: yield this._addCookies(this._responseCookies, aResponse); michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Populates the cookies container in this view with the specified data. michael@0: * michael@0: * @param string aName michael@0: * The type of cookies to populate (request or response). michael@0: * @param object aResponse michael@0: * The message received from the server. michael@0: * @return object michael@0: * Returns a promise that resolves upon the adding of cookies. michael@0: */ michael@0: _addCookies: Task.async(function*(aName, aResponse) { michael@0: let cookiesScope = this._cookies.addScope(aName); michael@0: cookiesScope.expanded = true; michael@0: michael@0: for (let cookie of aResponse.cookies) { michael@0: let cookieVar = cookiesScope.addItem(cookie.name, {}, true); michael@0: let cookieValue = yield gNetwork.getString(cookie.value); michael@0: cookieVar.setGrip(cookieValue); michael@0: michael@0: // By default the cookie name and value are shown. If this is the only michael@0: // information available, then nothing else is to be displayed. michael@0: let cookieProps = Object.keys(cookie); michael@0: if (cookieProps.length == 2) { michael@0: return; michael@0: } michael@0: michael@0: // Display any other information other than the cookie name and value michael@0: // which may be available. michael@0: let rawObject = Object.create(null); michael@0: let otherProps = cookieProps.filter(e => e != "name" && e != "value"); michael@0: for (let prop of otherProps) { michael@0: rawObject[prop] = cookie[prop]; michael@0: } michael@0: cookieVar.populate(rawObject); michael@0: cookieVar.twisty = true; michael@0: cookieVar.expanded = true; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Sets the network request get params shown in this view. michael@0: * michael@0: * @param string aUrl michael@0: * The request's url. michael@0: */ michael@0: _setRequestGetParams: function(aUrl) { michael@0: let query = nsIURL(aUrl).query; michael@0: if (query) { michael@0: this._addParams(this._paramsQueryString, query); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sets the network request post params shown in this view. michael@0: * michael@0: * @param object aHeadersResponse michael@0: * The "requestHeaders" message received from the server. michael@0: * @param object aHeadersFromUploadStream michael@0: * The "requestHeadersFromUploadStream" inferred from the POST payload. michael@0: * @param object aPostDataResponse michael@0: * The "requestPostData" message received from the server. michael@0: * @return object michael@0: * A promise that is resolved when the request post params are set. michael@0: */ michael@0: _setRequestPostParams: Task.async(function*(aHeadersResponse, aHeadersFromUploadStream, aPostDataResponse) { michael@0: if (!aHeadersResponse || !aHeadersFromUploadStream || !aPostDataResponse) { michael@0: return; michael@0: } michael@0: michael@0: let { headers: requestHeaders } = aHeadersResponse; michael@0: let { headers: payloadHeaders } = aHeadersFromUploadStream; michael@0: let allHeaders = [...payloadHeaders, ...requestHeaders]; michael@0: michael@0: let contentTypeHeader = allHeaders.find(e => e.name.toLowerCase() == "content-type"); michael@0: let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : ""; michael@0: let postDataLongString = aPostDataResponse.postData.text; michael@0: michael@0: let postData = yield gNetwork.getString(postDataLongString); michael@0: let contentType = yield gNetwork.getString(contentTypeLongString); michael@0: michael@0: // Handle query strings (e.g. "?foo=bar&baz=42"). michael@0: if (contentType.contains("x-www-form-urlencoded")) { michael@0: for (let section of postData.split(/\r\n|\r|\n/)) { michael@0: // Before displaying it, make sure this section of the POST data michael@0: // isn't a line containing upload stream headers. michael@0: if (payloadHeaders.every(header => !section.startsWith(header.name))) { michael@0: this._addParams(this._paramsFormData, section); michael@0: } michael@0: } michael@0: } michael@0: // Handle actual forms ("multipart/form-data" content type). michael@0: else { michael@0: // This is really awkward, but hey, it works. Let's show an empty michael@0: // scope in the params view and place the source editor containing michael@0: // the raw post data directly underneath. michael@0: $("#request-params-box").removeAttribute("flex"); michael@0: let paramsScope = this._params.addScope(this._paramsPostPayload); michael@0: paramsScope.expanded = true; michael@0: paramsScope.locked = true; michael@0: michael@0: $("#request-post-data-textarea-box").hidden = false; michael@0: let editor = yield NetMonitorView.editor("#request-post-data-textarea"); michael@0: // Most POST bodies are usually JSON, so they can be neatly michael@0: // syntax highlighted as JS. Otheriwse, fall back to plain text. michael@0: try { michael@0: JSON.parse(postData); michael@0: editor.setMode(Editor.modes.js); michael@0: } catch (e) { michael@0: editor.setMode(Editor.modes.text); michael@0: } finally { michael@0: editor.setText(postData); michael@0: } michael@0: } michael@0: michael@0: window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED); michael@0: }), michael@0: michael@0: /** michael@0: * Populates the params container in this view with the specified data. michael@0: * michael@0: * @param string aName michael@0: * The type of params to populate (get or post). michael@0: * @param string aQueryString michael@0: * A query string of params (e.g. "?foo=bar&baz=42"). michael@0: */ michael@0: _addParams: function(aName, aQueryString) { michael@0: let paramsArray = parseQueryString(aQueryString); michael@0: if (!paramsArray) { michael@0: return; michael@0: } michael@0: let paramsScope = this._params.addScope(aName); michael@0: paramsScope.expanded = true; michael@0: michael@0: for (let param of paramsArray) { michael@0: let paramVar = paramsScope.addItem(param.name, {}, true); michael@0: paramVar.setGrip(param.value); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sets the network response body shown in this view. michael@0: * michael@0: * @param string aUrl michael@0: * The request's url. michael@0: * @param object aResponse michael@0: * The message received from the server. michael@0: * @return object michael@0: * A promise that is resolved when the response body is set. michael@0: */ michael@0: _setResponseBody: Task.async(function*(aUrl, aResponse) { michael@0: if (!aResponse) { michael@0: return; michael@0: } michael@0: let { mimeType, text, encoding } = aResponse.content; michael@0: let responseBody = yield gNetwork.getString(text); michael@0: michael@0: // Handle json, which we tentatively identify by checking the MIME type michael@0: // for "json" after any word boundary. This works for the standard michael@0: // "application/json", and also for custom types like "x-bigcorp-json". michael@0: // Additionally, we also directly parse the response text content to michael@0: // verify whether it's json or not, to handle responses incorrectly michael@0: // labeled as text/plain instead. michael@0: let jsonMimeType, jsonObject, jsonObjectParseError; michael@0: try { michael@0: jsonMimeType = /\bjson/.test(mimeType); michael@0: jsonObject = JSON.parse(responseBody); michael@0: } catch (e) { michael@0: jsonObjectParseError = e; michael@0: } michael@0: if (jsonMimeType || jsonObject) { michael@0: // Extract the actual json substring in case this might be a "JSONP". michael@0: // This regex basically parses a function call and captures the michael@0: // function name and arguments in two separate groups. michael@0: let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/; michael@0: let [_, callbackPadding, jsonpString] = responseBody.match(jsonpRegex) || []; michael@0: michael@0: // Make sure this is a valid JSON object first. If so, nicely display michael@0: // the parsing results in a variables view. Otherwise, simply show michael@0: // the contents as plain text. michael@0: if (callbackPadding && jsonpString) { michael@0: try { michael@0: jsonObject = JSON.parse(jsonpString); michael@0: } catch (e) { michael@0: jsonObjectParseError = e; michael@0: } michael@0: } michael@0: michael@0: // Valid JSON or JSONP. michael@0: if (jsonObject) { michael@0: $("#response-content-json-box").hidden = false; michael@0: let jsonScopeName = callbackPadding michael@0: ? L10N.getFormatStr("jsonpScopeName", callbackPadding) michael@0: : L10N.getStr("jsonScopeName"); michael@0: michael@0: let jsonVar = { label: jsonScopeName, rawObject: jsonObject }; michael@0: yield this._json.controller.setSingleVariable(jsonVar).expanded; michael@0: } michael@0: // Malformed JSON. michael@0: else { michael@0: $("#response-content-textarea-box").hidden = false; michael@0: let infoHeader = $("#response-content-info-header"); michael@0: infoHeader.setAttribute("value", jsonObjectParseError); michael@0: infoHeader.setAttribute("tooltiptext", jsonObjectParseError); michael@0: infoHeader.hidden = false; michael@0: michael@0: let editor = yield NetMonitorView.editor("#response-content-textarea"); michael@0: editor.setMode(Editor.modes.js); michael@0: editor.setText(responseBody); michael@0: } michael@0: } michael@0: // Handle images. michael@0: else if (mimeType.contains("image/")) { michael@0: $("#response-content-image-box").setAttribute("align", "center"); michael@0: $("#response-content-image-box").setAttribute("pack", "center"); michael@0: $("#response-content-image-box").hidden = false; michael@0: $("#response-content-image").src = michael@0: "data:" + mimeType + ";" + encoding + "," + responseBody; michael@0: michael@0: // Immediately display additional information about the image: michael@0: // file name, mime type and encoding. michael@0: $("#response-content-image-name-value").setAttribute("value", nsIURL(aUrl).fileName); michael@0: $("#response-content-image-mime-value").setAttribute("value", mimeType); michael@0: $("#response-content-image-encoding-value").setAttribute("value", encoding); michael@0: michael@0: // Wait for the image to load in order to display the width and height. michael@0: $("#response-content-image").onload = e => { michael@0: // XUL images are majestic so they don't bother storing their dimensions michael@0: // in width and height attributes like the rest of the folk. Hack around michael@0: // this by getting the bounding client rect and subtracting the margins. michael@0: let { width, height } = e.target.getBoundingClientRect(); michael@0: let dimensions = (width - 2) + " x " + (height - 2); michael@0: $("#response-content-image-dimensions-value").setAttribute("value", dimensions); michael@0: }; michael@0: } michael@0: // Handle anything else. michael@0: else { michael@0: $("#response-content-textarea-box").hidden = false; michael@0: let editor = yield NetMonitorView.editor("#response-content-textarea"); michael@0: editor.setMode(Editor.modes.text); michael@0: editor.setText(responseBody); michael@0: michael@0: // Maybe set a more appropriate mode in the Source Editor if possible, michael@0: // but avoid doing this for very large files. michael@0: if (responseBody.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) { michael@0: let mapping = Object.keys(CONTENT_MIME_TYPE_MAPPINGS).find(key => mimeType.contains(key)); michael@0: if (mapping) { michael@0: editor.setMode(CONTENT_MIME_TYPE_MAPPINGS[mapping]); michael@0: } michael@0: } michael@0: } michael@0: michael@0: window.emit(EVENTS.RESPONSE_BODY_DISPLAYED); michael@0: }), michael@0: michael@0: /** michael@0: * Sets the timings information shown in this view. michael@0: * michael@0: * @param object aResponse michael@0: * The message received from the server. michael@0: */ michael@0: _setTimingsInformation: function(aResponse) { michael@0: if (!aResponse) { michael@0: return; michael@0: } michael@0: let { blocked, dns, connect, send, wait, receive } = aResponse.timings; michael@0: michael@0: let tabboxWidth = $("#details-pane").getAttribute("width"); michael@0: let availableWidth = tabboxWidth / 2; // Other nodes also take some space. michael@0: let scale = Math.max(availableWidth / aResponse.totalTime, 0); michael@0: michael@0: $("#timings-summary-blocked .requests-menu-timings-box") michael@0: .setAttribute("width", blocked * scale); michael@0: $("#timings-summary-blocked .requests-menu-timings-total") michael@0: .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", blocked)); michael@0: michael@0: $("#timings-summary-dns .requests-menu-timings-box") michael@0: .setAttribute("width", dns * scale); michael@0: $("#timings-summary-dns .requests-menu-timings-total") michael@0: .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", dns)); michael@0: michael@0: $("#timings-summary-connect .requests-menu-timings-box") michael@0: .setAttribute("width", connect * scale); michael@0: $("#timings-summary-connect .requests-menu-timings-total") michael@0: .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", connect)); michael@0: michael@0: $("#timings-summary-send .requests-menu-timings-box") michael@0: .setAttribute("width", send * scale); michael@0: $("#timings-summary-send .requests-menu-timings-total") michael@0: .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", send)); michael@0: michael@0: $("#timings-summary-wait .requests-menu-timings-box") michael@0: .setAttribute("width", wait * scale); michael@0: $("#timings-summary-wait .requests-menu-timings-total") michael@0: .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", wait)); michael@0: michael@0: $("#timings-summary-receive .requests-menu-timings-box") michael@0: .setAttribute("width", receive * scale); michael@0: $("#timings-summary-receive .requests-menu-timings-total") michael@0: .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", receive)); michael@0: michael@0: $("#timings-summary-dns .requests-menu-timings-box") michael@0: .style.transform = "translateX(" + (scale * blocked) + "px)"; michael@0: $("#timings-summary-connect .requests-menu-timings-box") michael@0: .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)"; michael@0: $("#timings-summary-send .requests-menu-timings-box") michael@0: .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)"; michael@0: $("#timings-summary-wait .requests-menu-timings-box") michael@0: .style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)"; michael@0: $("#timings-summary-receive .requests-menu-timings-box") michael@0: .style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)"; michael@0: michael@0: $("#timings-summary-dns .requests-menu-timings-total") michael@0: .style.transform = "translateX(" + (scale * blocked) + "px)"; michael@0: $("#timings-summary-connect .requests-menu-timings-total") michael@0: .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)"; michael@0: $("#timings-summary-send .requests-menu-timings-total") michael@0: .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)"; michael@0: $("#timings-summary-wait .requests-menu-timings-total") michael@0: .style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)"; michael@0: $("#timings-summary-receive .requests-menu-timings-total") michael@0: .style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)"; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the preview for HTML responses shown in this view. michael@0: * michael@0: * @param object aResponse michael@0: * The message received from the server. michael@0: * @return object michael@0: * A promise that is resolved when the html preview is rendered. michael@0: */ michael@0: _setHtmlPreview: Task.async(function*(aResponse) { michael@0: if (!aResponse) { michael@0: return promise.resolve(); michael@0: } michael@0: let { text } = aResponse.content; michael@0: let responseBody = yield gNetwork.getString(text); michael@0: michael@0: // Always disable JS when previewing HTML responses. michael@0: let iframe = $("#response-preview"); michael@0: iframe.contentDocument.docShell.allowJavascript = false; michael@0: iframe.contentDocument.documentElement.innerHTML = responseBody; michael@0: michael@0: window.emit(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED); michael@0: }), michael@0: michael@0: _dataSrc: null, michael@0: _headers: null, michael@0: _cookies: null, michael@0: _params: null, michael@0: _json: null, michael@0: _paramsQueryString: "", michael@0: _paramsFormData: "", michael@0: _paramsPostPayload: "", michael@0: _requestHeaders: "", michael@0: _responseHeaders: "", michael@0: _requestCookies: "", michael@0: _responseCookies: "" michael@0: }; michael@0: michael@0: /** michael@0: * Functions handling the performance statistics view. michael@0: */ michael@0: function PerformanceStatisticsView() { michael@0: } michael@0: michael@0: PerformanceStatisticsView.prototype = { michael@0: /** michael@0: * Initializes and displays empty charts in this container. michael@0: */ michael@0: displayPlaceholderCharts: function() { michael@0: this._createChart({ michael@0: id: "#primed-cache-chart", michael@0: title: "charts.cacheEnabled" michael@0: }); michael@0: this._createChart({ michael@0: id: "#empty-cache-chart", michael@0: title: "charts.cacheDisabled" michael@0: }); michael@0: window.emit(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED); michael@0: }, michael@0: michael@0: /** michael@0: * Populates and displays the primed cache chart in this container. michael@0: * michael@0: * @param array aItems michael@0: * @see this._sanitizeChartDataSource michael@0: */ michael@0: createPrimedCacheChart: function(aItems) { michael@0: this._createChart({ michael@0: id: "#primed-cache-chart", michael@0: title: "charts.cacheEnabled", michael@0: data: this._sanitizeChartDataSource(aItems), michael@0: strings: this._commonChartStrings, michael@0: totals: this._commonChartTotals, michael@0: sorted: true michael@0: }); michael@0: window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED); michael@0: }, michael@0: michael@0: /** michael@0: * Populates and displays the empty cache chart in this container. michael@0: * michael@0: * @param array aItems michael@0: * @see this._sanitizeChartDataSource michael@0: */ michael@0: createEmptyCacheChart: function(aItems) { michael@0: this._createChart({ michael@0: id: "#empty-cache-chart", michael@0: title: "charts.cacheDisabled", michael@0: data: this._sanitizeChartDataSource(aItems, true), michael@0: strings: this._commonChartStrings, michael@0: totals: this._commonChartTotals, michael@0: sorted: true michael@0: }); michael@0: window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED); michael@0: }, michael@0: michael@0: /** michael@0: * Common stringifier predicates used for items and totals in both the michael@0: * "primed" and "empty" cache charts. michael@0: */ michael@0: _commonChartStrings: { michael@0: size: value => { michael@0: let string = L10N.numberWithDecimals(value / 1024, CONTENT_SIZE_DECIMALS); michael@0: return L10N.getFormatStr("charts.sizeKB", string); michael@0: }, michael@0: time: value => { michael@0: let string = L10N.numberWithDecimals(value / 1000, REQUEST_TIME_DECIMALS); michael@0: return L10N.getFormatStr("charts.totalS", string); michael@0: } michael@0: }, michael@0: _commonChartTotals: { michael@0: size: total => { michael@0: let string = L10N.numberWithDecimals(total / 1024, CONTENT_SIZE_DECIMALS); michael@0: return L10N.getFormatStr("charts.totalSize", string); michael@0: }, michael@0: time: total => { michael@0: let seconds = total / 1000; michael@0: let string = L10N.numberWithDecimals(seconds, REQUEST_TIME_DECIMALS); michael@0: return PluralForm.get(seconds, L10N.getStr("charts.totalSeconds")).replace("#1", string); michael@0: }, michael@0: cached: total => { michael@0: return L10N.getFormatStr("charts.totalCached", total); michael@0: }, michael@0: count: total => { michael@0: return L10N.getFormatStr("charts.totalCount", total); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Adds a specific chart to this container. michael@0: * michael@0: * @param object michael@0: * An object containing all or some the following properties: michael@0: * - id: either "#primed-cache-chart" or "#empty-cache-chart" michael@0: * - title/data/strings/totals/sorted: @see Chart.jsm for details michael@0: */ michael@0: _createChart: function({ id, title, data, strings, totals, sorted }) { michael@0: let container = $(id); michael@0: michael@0: // Nuke all existing charts of the specified type. michael@0: while (container.hasChildNodes()) { michael@0: container.firstChild.remove(); michael@0: } michael@0: michael@0: // Create a new chart. michael@0: let chart = Chart.PieTable(document, { michael@0: diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER, michael@0: title: L10N.getStr(title), michael@0: data: data, michael@0: strings: strings, michael@0: totals: totals, michael@0: sorted: sorted michael@0: }); michael@0: michael@0: chart.on("click", (_, item) => { michael@0: NetMonitorView.RequestsMenu.filterOnlyOn(item.label); michael@0: NetMonitorView.showNetworkInspectorView(); michael@0: }); michael@0: michael@0: container.appendChild(chart.node); michael@0: }, michael@0: michael@0: /** michael@0: * Sanitizes the data source used for creating charts, to follow the michael@0: * data format spec defined in Chart.jsm. michael@0: * michael@0: * @param array aItems michael@0: * A collection of request items used as the data source for the chart. michael@0: * @param boolean aEmptyCache michael@0: * True if the cache is considered enabled, false for disabled. michael@0: */ michael@0: _sanitizeChartDataSource: function(aItems, aEmptyCache) { michael@0: let data = [ michael@0: "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "other" michael@0: ].map(e => ({ michael@0: cached: 0, michael@0: count: 0, michael@0: label: e, michael@0: size: 0, michael@0: time: 0 michael@0: })); michael@0: michael@0: for (let requestItem of aItems) { michael@0: let details = requestItem.attachment; michael@0: let type; michael@0: michael@0: if (RequestsMenuView.prototype.isHtml(requestItem)) { michael@0: type = 0; // "html" michael@0: } else if (RequestsMenuView.prototype.isCss(requestItem)) { michael@0: type = 1; // "css" michael@0: } else if (RequestsMenuView.prototype.isJs(requestItem)) { michael@0: type = 2; // "js" michael@0: } else if (RequestsMenuView.prototype.isFont(requestItem)) { michael@0: type = 4; // "fonts" michael@0: } else if (RequestsMenuView.prototype.isImage(requestItem)) { michael@0: type = 5; // "images" michael@0: } else if (RequestsMenuView.prototype.isMedia(requestItem)) { michael@0: type = 6; // "media" michael@0: } else if (RequestsMenuView.prototype.isFlash(requestItem)) { michael@0: type = 7; // "flash" michael@0: } else if (RequestsMenuView.prototype.isXHR(requestItem)) { michael@0: // Verify XHR last, to categorize other mime types in their own blobs. michael@0: type = 3; // "xhr" michael@0: } else { michael@0: type = 8; // "other" michael@0: } michael@0: michael@0: if (aEmptyCache || !responseIsFresh(details)) { michael@0: data[type].time += details.totalTime || 0; michael@0: data[type].size += details.contentSize || 0; michael@0: } else { michael@0: data[type].cached++; michael@0: } michael@0: data[type].count++; michael@0: } michael@0: michael@0: return data.filter(e => e.count > 0); michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * DOM query helper. michael@0: */ michael@0: function $(aSelector, aTarget = document) aTarget.querySelector(aSelector); michael@0: function $all(aSelector, aTarget = document) aTarget.querySelectorAll(aSelector); michael@0: michael@0: /** michael@0: * Helper for getting an nsIURL instance out of a string. michael@0: */ michael@0: function nsIURL(aUrl, aStore = nsIURL.store) { michael@0: if (aStore.has(aUrl)) { michael@0: return aStore.get(aUrl); michael@0: } michael@0: let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); michael@0: aStore.set(aUrl, uri); michael@0: return uri; michael@0: } michael@0: nsIURL.store = new Map(); michael@0: michael@0: /** michael@0: * Parse a url's query string into its components michael@0: * michael@0: * @param string aQueryString michael@0: * The query part of a url michael@0: * @return array michael@0: * Array of query params {name, value} michael@0: */ michael@0: function parseQueryString(aQueryString) { michael@0: // Make sure there's at least one param available. michael@0: // Be careful here, params don't necessarily need to have values, so michael@0: // no need to verify the existence of a "=". michael@0: if (!aQueryString) { michael@0: return; michael@0: } michael@0: // Turn the params string into an array containing { name: value } tuples. michael@0: let paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map(e => michael@0: let (param = e.split("=")) { michael@0: name: param[0] ? NetworkHelper.convertToUnicode(unescape(param[0])) : "", michael@0: value: param[1] ? NetworkHelper.convertToUnicode(unescape(param[1])) : "" michael@0: }); michael@0: return paramsArray; michael@0: } michael@0: michael@0: /** michael@0: * Parse text representation of multiple HTTP headers. michael@0: * michael@0: * @param string aText michael@0: * Text of headers michael@0: * @return array michael@0: * Array of headers info {name, value} michael@0: */ michael@0: function parseHeadersText(aText) { michael@0: return parseRequestText(aText, "\\S+?", ":"); michael@0: } michael@0: michael@0: /** michael@0: * Parse readable text list of a query string. michael@0: * michael@0: * @param string aText michael@0: * Text of query string represetation michael@0: * @return array michael@0: * Array of query params {name, value} michael@0: */ michael@0: function parseQueryText(aText) { michael@0: return parseRequestText(aText, ".+?", "="); michael@0: } michael@0: michael@0: /** michael@0: * Parse a text representation of a name[divider]value list with michael@0: * the given name regex and divider character. michael@0: * michael@0: * @param string aText michael@0: * Text of list michael@0: * @return array michael@0: * Array of headers info {name, value} michael@0: */ michael@0: function parseRequestText(aText, aName, aDivider) { michael@0: let regex = new RegExp("(" + aName + ")\\" + aDivider + "\\s*(.+)"); michael@0: let pairs = []; michael@0: for (let line of aText.split("\n")) { michael@0: let matches; michael@0: if (matches = regex.exec(line)) { michael@0: let [, name, value] = matches; michael@0: pairs.push({name: name, value: value}); michael@0: } michael@0: } michael@0: return pairs; michael@0: } michael@0: michael@0: /** michael@0: * Write out a list of headers into a chunk of text michael@0: * michael@0: * @param array aHeaders michael@0: * Array of headers info {name, value} michael@0: * @return string aText michael@0: * List of headers in text format michael@0: */ michael@0: function writeHeaderText(aHeaders) { michael@0: return [(name + ": " + value) for ({name, value} of aHeaders)].join("\n"); michael@0: } michael@0: michael@0: /** michael@0: * Write out a list of query params into a chunk of text michael@0: * michael@0: * @param array aParams michael@0: * Array of query params {name, value} michael@0: * @return string michael@0: * List of query params in text format michael@0: */ michael@0: function writeQueryText(aParams) { michael@0: return [(name + "=" + value) for ({name, value} of aParams)].join("\n"); michael@0: } michael@0: michael@0: /** michael@0: * Write out a list of query params into a query string michael@0: * michael@0: * @param array aParams michael@0: * Array of query params {name, value} michael@0: * @return string michael@0: * Query string that can be appended to a url. michael@0: */ michael@0: function writeQueryString(aParams) { michael@0: return [(name + "=" + value) for ({name, value} of aParams)].join("&"); michael@0: } michael@0: michael@0: /** michael@0: * Checks if the "Expiration Calculations" defined in section 13.2.4 of the michael@0: * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers. michael@0: * michael@0: * @param object michael@0: * An object containing the { responseHeaders, status } properties. michael@0: * @return boolean michael@0: * True if the response is fresh and loaded from cache. michael@0: */ michael@0: function responseIsFresh({ responseHeaders, status }) { michael@0: // Check for a "304 Not Modified" status and response headers availability. michael@0: if (status != 304 || !responseHeaders) { michael@0: return false; michael@0: } michael@0: michael@0: let list = responseHeaders.headers; michael@0: let cacheControl = list.filter(e => e.name.toLowerCase() == "cache-control")[0]; michael@0: let expires = list.filter(e => e.name.toLowerCase() == "expires")[0]; michael@0: michael@0: // Check the "Cache-Control" header for a maximum age value. michael@0: if (cacheControl) { michael@0: let maxAgeMatch = michael@0: cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) || michael@0: cacheControl.value.match(/max-age\s*=\s*(\d+)/); michael@0: michael@0: if (maxAgeMatch && maxAgeMatch.pop() > 0) { michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: // Check the "Expires" header for a valid date. michael@0: if (expires && Date.parse(expires.value)) { michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * 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: * michael@0: * @param function callback michael@0: * Function to execute execute when data-key is present in event.target. michael@0: * @return function michael@0: * Wrapped function with the target data-key as the first argument. michael@0: */ michael@0: function getKeyWithEvent(callback) { michael@0: return function(event) { michael@0: var key = event.target.getAttribute("data-key"); michael@0: if (key) { michael@0: callback.call(null, key); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Preliminary setup for the NetMonitorView object. michael@0: */ michael@0: NetMonitorView.Toolbar = new ToolbarView(); michael@0: NetMonitorView.RequestsMenu = new RequestsMenuView(); michael@0: NetMonitorView.Sidebar = new SidebarView(); michael@0: NetMonitorView.CustomRequest = new CustomRequestView(); michael@0: NetMonitorView.NetworkDetails = new NetworkDetailsView(); michael@0: NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();