browser/devtools/netmonitor/netmonitor-view.js

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

mercurial