browser/devtools/netmonitor/netmonitor-view.js

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

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

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

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

mercurial