gecko/browser/devtools/netmonitor/netmonitor-view.js
Felix Crux d01314ff24 Bug 905226 - Identify custom JSON MIME-types as text. r=vporof
MIME types that can't be identified as textual are base64 encoded before
being shown in the Network Monitor tab of the developer tools. This text
detection wasn't identifying "custom" JSON MIME-types like
"application/x-bigcorp-api-json" as being JSON, and consequently they were
shown in a rather unhelpful base64 form.

This patch extends and liberalizes the isTextMimeType method to identify
these forms correctly.
2013-09-20 10:20:44 -04:00

2169 lines
70 KiB
JavaScript

/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const HTML_NS = "http://www.w3.org/1999/xhtml";
const EPSILON = 0.001;
const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400; // 100 KB in bytes
const RESIZE_REFRESH_RATE = 50; // ms
const REQUESTS_REFRESH_RATE = 50; // ms
const REQUESTS_HEADERS_SAFE_BOUNDS = 30; // px
const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // px
const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
const DEFAULT_HTTP_VERSION = "HTTP/1.1";
const HEADERS_SIZE_DECIMALS = 3;
const CONTENT_SIZE_DECIMALS = 2;
const CONTENT_MIME_TYPE_ABBREVIATIONS = {
"ecmascript": "js",
"javascript": "js",
"x-javascript": "js"
};
const CONTENT_MIME_TYPE_MAPPINGS = {
"/ecmascript": SourceEditor.MODES.JAVASCRIPT,
"/javascript": SourceEditor.MODES.JAVASCRIPT,
"/x-javascript": SourceEditor.MODES.JAVASCRIPT,
"/html": SourceEditor.MODES.HTML,
"/xhtml": SourceEditor.MODES.HTML,
"/xml": SourceEditor.MODES.HTML,
"/atom": SourceEditor.MODES.HTML,
"/soap": SourceEditor.MODES.HTML,
"/rdf": SourceEditor.MODES.HTML,
"/rss": SourceEditor.MODES.HTML,
"/css": SourceEditor.MODES.CSS
};
const DEFAULT_EDITOR_CONFIG = {
mode: SourceEditor.MODES.TEXT,
readOnly: true,
showLineNumbers: true
};
const GENERIC_VARIABLES_VIEW_SETTINGS = {
lazyEmpty: true,
lazyEmptyDelay: 10, // ms
searchEnabled: true,
editableValueTooltip: "",
editableNameTooltip: "",
preventDisableOnChage: true,
preventDescriptorModifiers: true,
eval: () => {},
switch: () => {}
};
/**
* Object defining the network monitor view components.
*/
let NetMonitorView = {
/**
* Initializes the network monitor view.
*/
initialize: function() {
this._initializePanes();
this.Toolbar.initialize();
this.RequestsMenu.initialize();
this.NetworkDetails.initialize();
},
/**
* Destroys the network monitor view.
*/
destroy: function() {
this.Toolbar.destroy();
this.RequestsMenu.destroy();
this.NetworkDetails.destroy();
this._destroyPanes();
},
/**
* Initializes the UI for all the displayed panes.
*/
_initializePanes: function() {
dumpn("Initializing the NetMonitorView panes");
this._body = $("#body");
this._detailsPane = $("#details-pane");
this._detailsPaneToggleButton = $("#details-pane-toggle");
this._collapsePaneString = L10N.getStr("collapseDetailsPane");
this._expandPaneString = L10N.getStr("expandDetailsPane");
this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight);
this.toggleDetailsPane({ visible: false });
},
/**
* Destroys the UI for all the displayed panes.
*/
_destroyPanes: function() {
dumpn("Destroying the NetMonitorView panes");
Prefs.networkDetailsWidth = this._detailsPane.getAttribute("width");
Prefs.networkDetailsHeight = this._detailsPane.getAttribute("height");
this._detailsPane = null;
this._detailsPaneToggleButton = null;
},
/**
* Gets the visibility state of the network details pane.
* @return boolean
*/
get detailsPaneHidden()
this._detailsPane.hasAttribute("pane-collapsed"),
/**
* Sets the network details pane hidden or visible.
*
* @param object aFlags
* An object containing some of the following properties:
* - visible: true if the pane should be shown, false to hide
* - animated: true to display an animation on toggle
* - delayed: true to wait a few cycles before toggle
* - callback: a function to invoke when the toggle finishes
* @param number aTabIndex [optional]
* The index of the intended selected tab in the details pane.
*/
toggleDetailsPane: function(aFlags, aTabIndex) {
let pane = this._detailsPane;
let button = this._detailsPaneToggleButton;
ViewHelpers.togglePane(aFlags, pane);
if (aFlags.visible) {
this._body.removeAttribute("pane-collapsed");
button.removeAttribute("pane-collapsed");
button.setAttribute("tooltiptext", this._collapsePaneString);
} else {
this._body.setAttribute("pane-collapsed", "");
button.setAttribute("pane-collapsed", "");
button.setAttribute("tooltiptext", this._expandPaneString);
}
if (aTabIndex !== undefined) {
$("#event-details-pane").selectedIndex = aTabIndex;
}
},
/**
* Lazily initializes and returns a promise for a SourceEditor instance.
*
* @param string aId
* The id of the editor placeholder node.
* @return object
* A promise that is resolved when the editor is available.
*/
editor: function(aId) {
dumpn("Getting a NetMonitorView editor: " + aId);
if (this._editorPromises.has(aId)) {
return this._editorPromises.get(aId);
}
let deferred = promise.defer();
this._editorPromises.set(aId, deferred.promise);
// Initialize the source editor and store the newly created instance
// in the ether of a resolved promise's value.
new SourceEditor().init($(aId), DEFAULT_EDITOR_CONFIG, deferred.resolve);
return deferred.promise;
},
_body: null,
_detailsPane: null,
_detailsPaneToggleButton: null,
_collapsePaneString: "",
_expandPaneString: "",
_editorPromises: new Map()
};
/**
* Functions handling the toolbar view: expand/collapse button etc.
*/
function ToolbarView() {
dumpn("ToolbarView was instantiated");
this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this);
}
ToolbarView.prototype = {
/**
* Initialization function, called when the debugger is started.
*/
initialize: function() {
dumpn("Initializing the ToolbarView");
this._detailsPaneToggleButton = $("#details-pane-toggle");
this._detailsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false);
},
/**
* Destruction function, called when the debugger is closed.
*/
destroy: function() {
dumpn("Destroying the ToolbarView");
this._detailsPaneToggleButton.removeEventListener("mousedown", this._onTogglePanesPressed, false);
},
/**
* Listener handling the toggle button click event.
*/
_onTogglePanesPressed: function() {
let requestsMenu = NetMonitorView.RequestsMenu;
let selectedIndex = requestsMenu.selectedIndex;
// Make sure there's a selection if the button is pressed, to avoid
// showing an empty network details pane.
if (selectedIndex == -1 && requestsMenu.itemCount) {
requestsMenu.selectedIndex = 0;
} else {
requestsMenu.selectedIndex = -1;
}
},
_detailsPaneToggleButton: null
};
/**
* Functions handling the requests menu (containing details about each request,
* like status, method, file, domain, as well as a waterfall representing
* timing imformation).
*/
function RequestsMenuView() {
dumpn("RequestsMenuView was instantiated");
this._flushRequests = this._flushRequests.bind(this);
this._onSelect = this._onSelect.bind(this);
this._onResize = this._onResize.bind(this);
this._byFile = this._byFile.bind(this);
this._byDomain = this._byDomain.bind(this);
this._byType = this._byType.bind(this);
}
RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
/**
* Initialization function, called when the network monitor is started.
*/
initialize: function() {
dumpn("Initializing the RequestsMenuView");
this.widget = new SideMenuWidget($("#requests-menu-contents"), false);
this._splitter = $('#splitter');
this._summary = $("#request-menu-network-summary");
this.allowFocusOnRightClick = true;
this.widget.maintainSelectionVisible = false;
this.widget.autoscrollWithAppendedItems = true;
this.widget.addEventListener("select", this._onSelect, false);
this._splitter.addEventListener("mousemove", this._onResize, false);
window.addEventListener("resize", this._onResize, false);
},
/**
* Destruction function, called when the network monitor is closed.
*/
destroy: function() {
dumpn("Destroying the SourcesView");
this.widget.removeEventListener("select", this._onSelect, false);
this._splitter.removeEventListener("mousemove", this._onResize, false);
window.removeEventListener("resize", this._onResize, false);
},
/**
* Resets this container (removes all the networking information).
*/
reset: function() {
this.empty();
this._firstRequestStartedMillis = -1;
this._lastRequestEndedMillis = -1;
},
/**
* Specifies if this view may be updated lazily.
*/
lazyUpdate: true,
/**
* Adds a network request to this container.
*
* @param string aId
* An identifier coming from the network monitor controller.
* @param string aStartedDateTime
* A string representation of when the request was started, which
* can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
* @param string aMethod
* Specifies the request method (e.g. "GET", "POST", etc.)
* @param string aUrl
* Specifies the request's url.
* @param boolean aIsXHR
* True if this request was initiated via XHR.
*/
addRequest: function(aId, aStartedDateTime, aMethod, aUrl, aIsXHR) {
// Convert the received date/time string to a unix timestamp.
let unixTime = Date.parse(aStartedDateTime);
// Create the element node for the network request item.
let menuView = this._createMenuView(aMethod, aUrl);
// Remember the first and last event boundaries.
this._registerFirstRequestStart(unixTime);
this._registerLastRequestEnd(unixTime);
// Append a network request item to this container.
let requestItem = this.push([menuView, aId, ""], {
attachment: {
startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
startedMillis: unixTime,
method: aMethod,
url: aUrl,
isXHR: aIsXHR
}
});
$("#details-pane-toggle").disabled = false;
$("#requests-menu-empty-notice").hidden = true;
this.refreshSummary();
this.refreshZebra();
if (aId == this._preferredItemId) {
this.selectedItem = requestItem;
}
},
/**
* Create a new custom request form populated with the data from
* the currently selected request.
*/
cloneSelectedRequest: function() {
let selected = this.selectedItem.attachment;
// Create the element node for the network request item.
let menuView = this._createMenuView(selected.method, selected.url);
let newItem = this.push([menuView,, ""], {
attachment: Object.create(selected, {
isCustom: { value: true }
})
});
// Immediately switch to new request pane.
this.selectedItem = newItem;
},
/**
* Opens selected item in a new tab.
*/
openRequestInTab: function() {
let win = Services.wm.getMostRecentWindow("navigator:browser");
let selected = this.selectedItem.attachment;
win.openUILinkIn(selected.url, "tab", { relatedToCurrent: true });
},
/**
* Copy the request url from the currently selected item.
*/
copyUrl: function() {
let selected = this.selectedItem.attachment;
clipboardHelper.copyString(selected.url, document);
},
/**
* Send a new HTTP request using the data in the custom request form.
*/
sendCustomRequest: function() {
let selected = this.selectedItem.attachment;
let data = Object.create(selected, {
headers: { value: selected.requestHeaders.headers }
});
if (selected.requestPostData) {
data.body = selected.requestPostData.postData.text;
}
NetMonitorController.webConsoleClient.sendHTTPRequest(data, aResponse => {
let id = aResponse.eventActor.actor;
this._preferredItemId = id;
});
this.closeCustomRequest();
},
/**
* Remove the currently selected custom request.
*/
closeCustomRequest: function() {
this.remove(this.selectedItem);
NetMonitorView.Sidebar.toggle(false);
},
/**
* Filters all network requests in this container by a specified type.
*
* @param string aType
* Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
* or "flash".
*/
filterOn: function(aType = "all") {
let target = $("#requests-menu-filter-" + aType + "-button");
let buttons = document.querySelectorAll(".requests-menu-footer-button");
for (let button of buttons) {
if (button != target) {
button.removeAttribute("checked");
} else {
button.setAttribute("checked", "true");
}
}
// Filter on whatever was requested.
switch (aType) {
case "all":
this.filterContents(() => true);
break;
case "html":
this.filterContents(this._onHtml);
break;
case "css":
this.filterContents(this._onCss);
break;
case "js":
this.filterContents(this._onJs);
break;
case "xhr":
this.filterContents(this._onXhr);
break;
case "fonts":
this.filterContents(this._onFonts);
break;
case "images":
this.filterContents(this._onImages);
break;
case "media":
this.filterContents(this._onMedia);
break;
case "flash":
this.filterContents(this._onFlash);
break;
}
this.refreshSummary();
this.refreshZebra();
},
/**
* Sorts all network requests in this container by a specified detail.
*
* @param string aType
* Either "status", "method", "file", "domain", "type", "size" or
* "waterfall".
*/
sortBy: function(aType = "waterfall") {
let target = $("#requests-menu-" + aType + "-button");
let headers = document.querySelectorAll(".requests-menu-header-button");
for (let header of headers) {
if (header != target) {
header.removeAttribute("sorted");
header.removeAttribute("tooltiptext");
}
}
let direction = "";
if (target) {
if (target.getAttribute("sorted") == "ascending") {
target.setAttribute("sorted", direction = "descending");
target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedDesc"));
} else {
target.setAttribute("sorted", direction = "ascending");
target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedAsc"));
}
}
// Sort by whatever was requested.
switch (aType) {
case "status":
if (direction == "ascending") {
this.sortContents(this._byStatus);
} else {
this.sortContents((a, b) => !this._byStatus(a, b));
}
break;
case "method":
if (direction == "ascending") {
this.sortContents(this._byMethod);
} else {
this.sortContents((a, b) => !this._byMethod(a, b));
}
break;
case "file":
if (direction == "ascending") {
this.sortContents(this._byFile);
} else {
this.sortContents((a, b) => !this._byFile(a, b));
}
break;
case "domain":
if (direction == "ascending") {
this.sortContents(this._byDomain);
} else {
this.sortContents((a, b) => !this._byDomain(a, b));
}
break;
case "type":
if (direction == "ascending") {
this.sortContents(this._byType);
} else {
this.sortContents((a, b) => !this._byType(a, b));
}
break;
case "size":
if (direction == "ascending") {
this.sortContents(this._bySize);
} else {
this.sortContents((a, b) => !this._bySize(a, b));
}
break;
case "waterfall":
if (direction == "ascending") {
this.sortContents(this._byTiming);
} else {
this.sortContents((a, b) => !this._byTiming(a, b));
}
break;
}
this.refreshSummary();
this.refreshZebra();
},
/**
* Predicates used when filtering items.
*
* @param object aItem
* The filtered item.
* @return boolean
* True if the item should be visible, false otherwise.
*/
_onHtml: function({ attachment: { mimeType } })
mimeType && mimeType.contains("/html"),
_onCss: function({ attachment: { mimeType } })
mimeType && mimeType.contains("/css"),
_onJs: function({ attachment: { mimeType } })
mimeType && (
mimeType.contains("/ecmascript") ||
mimeType.contains("/javascript") ||
mimeType.contains("/x-javascript")),
_onXhr: function({ attachment: { isXHR } })
isXHR,
_onFonts: function({ attachment: { url, mimeType } }) // Fonts are a mess.
(mimeType && (
mimeType.contains("font/") ||
mimeType.contains("/font"))) ||
url.contains(".eot") ||
url.contains(".ttf") ||
url.contains(".otf") ||
url.contains(".woff"),
_onImages: function({ attachment: { mimeType } })
mimeType && mimeType.contains("image/"),
_onMedia: function({ attachment: { mimeType } }) // Not including images.
mimeType && (
mimeType.contains("audio/") ||
mimeType.contains("video/") ||
mimeType.contains("model/")),
_onFlash: function({ attachment: { url, mimeType } }) // Flash is a mess.
(mimeType && (
mimeType.contains("/x-flv") ||
mimeType.contains("/x-shockwave-flash"))) ||
url.contains(".swf") ||
url.contains(".flv"),
/**
* Predicates used when sorting items.
*
* @param object aFirst
* The first item used in the comparison.
* @param object aSecond
* The second item used in the comparison.
* @return number
* -1 to sort aFirst to a lower index than aSecond
* 0 to leave aFirst and aSecond unchanged with respect to each other
* 1 to sort aSecond to a lower index than aFirst
*/
_byTiming: function({ attachment: first }, { attachment: second })
first.startedMillis > second.startedMillis,
_byStatus: function({ attachment: first }, { attachment: second })
first.status == second.status
? first.startedMillis > second.startedMillis
: first.status > second.status,
_byMethod: function({ attachment: first }, { attachment: second })
first.method == second.method
? first.startedMillis > second.startedMillis
: first.method > second.method,
_byFile: function({ attachment: first }, { attachment: second }) {
let firstUrl = this._getUriNameWithQuery(first.url).toLowerCase();
let secondUrl = this._getUriNameWithQuery(second.url).toLowerCase();
return firstUrl == secondUrl
? first.startedMillis > second.startedMillis
: firstUrl > secondUrl;
},
_byDomain: function({ attachment: first }, { attachment: second }) {
let firstDomain = this._getUriHostPort(first.url).toLowerCase();
let secondDomain = this._getUriHostPort(second.url).toLowerCase();
return firstDomain == secondDomain
? first.startedMillis > second.startedMillis
: firstDomain > secondDomain;
},
_byType: function({ attachment: first }, { attachment: second }) {
let firstType = this._getAbbreviatedMimeType(first.mimeType).toLowerCase();
let secondType = this._getAbbreviatedMimeType(second.mimeType).toLowerCase();
return firstType == secondType
? first.startedMillis > second.startedMillis
: firstType > secondType;
},
_bySize: function({ attachment: first }, { attachment: second })
first.contentSize > second.contentSize,
/**
* Refreshes the status displayed in this container's footer, providing
* concise information about all requests.
*/
refreshSummary: function() {
let visibleItems = this.visibleItems;
let visibleRequestsCount = visibleItems.length;
if (!visibleRequestsCount) {
this._summary.setAttribute("value", L10N.getStr("networkMenu.empty"));
return;
}
let totalBytes = this._getTotalBytesOfRequests(visibleItems);
let totalMillis =
this._getNewestRequest(visibleItems).attachment.endedMillis -
this._getOldestRequest(visibleItems).attachment.startedMillis;
// https://developer.mozilla.org/en-US/docs/Localization_and_Plurals
let str = PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary"));
this._summary.setAttribute("value", str
.replace("#1", visibleRequestsCount)
.replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2))
.replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2))
);
},
/**
* Adds odd/even attributes to all the visible items in this container.
*/
refreshZebra: function() {
let visibleItems = this.visibleItems;
for (let i = 0, len = visibleItems.length; i < len; i++) {
let requestItem = visibleItems[i];
let requestTarget = requestItem.target;
if (i % 2 == 0) {
requestTarget.setAttribute("even", "");
requestTarget.removeAttribute("odd");
} else {
requestTarget.setAttribute("odd", "");
requestTarget.removeAttribute("even");
}
}
},
/**
* Schedules adding additional information to a network request.
*
* @param string aId
* An identifier coming from the network monitor controller.
* @param object aData
* An object containing several { key: value } tuples of network info.
* Supported keys are "httpVersion", "status", "statusText" etc.
*/
updateRequest: function(aId, aData) {
// Prevent interference from zombie updates received after target closed.
if (NetMonitorView._isDestroyed) {
return;
}
this._updateQueue.push([aId, aData]);
// Lazy updating is disabled in some tests.
if (!this.lazyUpdate) {
return void this._flushRequests();
}
// Allow requests to settle down first.
setNamedTimeout(
"update-requests", REQUESTS_REFRESH_RATE, () => this._flushRequests());
},
/**
* Starts adding all queued additional information about network requests.
*/
_flushRequests: function() {
// For each queued additional information packet, get the corresponding
// request item in the view and update it based on the specified data.
for (let [id, data] of this._updateQueue) {
let requestItem = this.getItemByValue(id);
if (!requestItem) {
// Packet corresponds to a dead request item, target navigated.
continue;
}
// Each information packet may contain several { key: value } tuples of
// network info, so update the view based on each one.
for (let key in data) {
let value = data[key];
if (value === undefined) {
// The information in the packet is empty, it can be safely ignored.
continue;
}
switch (key) {
case "requestHeaders":
requestItem.attachment.requestHeaders = value;
break;
case "requestCookies":
requestItem.attachment.requestCookies = value;
break;
case "requestPostData":
requestItem.attachment.requestPostData = value;
break;
case "responseHeaders":
requestItem.attachment.responseHeaders = value;
break;
case "responseCookies":
requestItem.attachment.responseCookies = value;
break;
case "httpVersion":
requestItem.attachment.httpVersion = value;
break;
case "status":
requestItem.attachment.status = value;
this.updateMenuView(requestItem, key, value);
break;
case "statusText":
requestItem.attachment.statusText = value;
this.updateMenuView(requestItem, key,
requestItem.attachment.status + " " +
requestItem.attachment.statusText);
break;
case "headersSize":
requestItem.attachment.headersSize = value;
break;
case "contentSize":
requestItem.attachment.contentSize = value;
this.updateMenuView(requestItem, key, value);
break;
case "mimeType":
requestItem.attachment.mimeType = value;
this.updateMenuView(requestItem, key, value);
break;
case "responseContent":
requestItem.attachment.responseContent = value;
break;
case "totalTime":
requestItem.attachment.totalTime = value;
requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value;
this.updateMenuView(requestItem, key, value);
this._registerLastRequestEnd(requestItem.attachment.endedMillis);
break;
case "eventTimings":
requestItem.attachment.eventTimings = value;
this._createWaterfallView(requestItem, value.timings);
break;
}
}
// This update may have additional information about a request which
// isn't shown yet in the network details pane.
let selectedItem = this.selectedItem;
if (selectedItem && selectedItem.value == id) {
NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
}
}
// We're done flushing all the requests, clear the update queue.
this._updateQueue = [];
// Make sure all the requests are sorted and filtered.
// Freshly added requests may not yet contain all the information required
// for sorting and filtering predicates, so this is done each time the
// network requests table is flushed (don't worry, events are drained first
// so this doesn't happen once per network event update).
this.sortContents();
this.filterContents();
this.refreshSummary();
this.refreshZebra();
},
/**
* Customization function for creating an item's UI.
*
* @param string aMethod
* Specifies the request method (e.g. "GET", "POST", etc.)
* @param string aUrl
* Specifies the request's url.
* @return nsIDOMNode
* The network request view.
*/
_createMenuView: function(aMethod, aUrl) {
let template = $("#requests-menu-item-template");
let fragment = document.createDocumentFragment();
this.updateMenuView(template, 'method', aMethod);
this.updateMenuView(template, 'url', aUrl);
let waterfall = $(".requests-menu-waterfall", template);
waterfall.style.backgroundImage = this._cachedWaterfallBackground;
// Flatten the DOM by removing one redundant box (the template container).
for (let node of template.childNodes) {
fragment.appendChild(node.cloneNode(true));
}
return fragment;
},
/**
* Updates the information displayed in a network request item view.
*
* @param object aItem
* The network request item in this container.
* @param string aKey
* The type of information that is to be updated.
* @param any aValue
* The new value to be shown.
*/
updateMenuView: function(aItem, aKey, aValue) {
let target = aItem.target || aItem;
switch (aKey) {
case "method": {
let node = $(".requests-menu-method", target);
node.setAttribute("value", aValue);
break;
}
case "url": {
let uri;
try {
uri = nsIURL(aValue);
} catch(e) {
break; // User input may not make a well-formed url yet.
}
let nameWithQuery = this._getUriNameWithQuery(uri);
let hostPort = this._getUriHostPort(uri);
let node = $(".requests-menu-file", target);
node.setAttribute("value", nameWithQuery);
node.setAttribute("tooltiptext", nameWithQuery);
let domain = $(".requests-menu-domain", target);
domain.setAttribute("value", hostPort);
domain.setAttribute("tooltiptext", hostPort);
break;
}
case "status": {
let node = $(".requests-menu-status", target);
node.setAttribute("code", aValue);
break;
}
case "statusText": {
let node = $(".requests-menu-status-and-method", target);
node.setAttribute("tooltiptext", aValue);
break;
}
case "contentSize": {
let kb = aValue / 1024;
let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
let node = $(".requests-menu-size", target);
let text = L10N.getFormatStr("networkMenu.sizeKB", size);
node.setAttribute("value", text);
node.setAttribute("tooltiptext", text);
break;
}
case "mimeType": {
let type = this._getAbbreviatedMimeType(aValue);
let node = $(".requests-menu-type", target);
let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
node.setAttribute("value", text);
node.setAttribute("tooltiptext", aValue);
break;
}
case "totalTime": {
let node = $(".requests-menu-timings-total", target);
let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer
node.setAttribute("value", text);
node.setAttribute("tooltiptext", text);
break;
}
}
},
/**
* Creates a waterfall representing timing information in a network request item view.
*
* @param object aItem
* The network request item in this container.
* @param object aTimings
* An object containing timing information.
*/
_createWaterfallView: function(aItem, aTimings) {
let { target, attachment } = aItem;
let sections = ["dns", "connect", "send", "wait", "receive"];
// Skipping "blocked" because it doesn't work yet.
let timingsNode = $(".requests-menu-timings", target);
let startCapNode = $(".requests-menu-timings-cap.start", timingsNode);
let endCapNode = $(".requests-menu-timings-cap.end", timingsNode);
let firstBox;
// Add a set of boxes representing timing information.
for (let key of sections) {
let width = aTimings[key];
// Don't render anything if it surely won't be visible.
// One millisecond == one unscaled pixel.
if (width > 0) {
let timingBox = document.createElement("hbox");
timingBox.className = "requests-menu-timings-box " + key;
timingBox.setAttribute("width", width);
timingsNode.insertBefore(timingBox, endCapNode);
// Make the start cap inherit the aspect of the first timing box.
if (!firstBox) {
firstBox = timingBox;
startCapNode.classList.add(key);
}
// Same goes for the end cap, inherit the aspect of the last timing box.
endCapNode.classList.add(key);
}
}
// Since at least one timing box should've been rendered, unhide the
// start and end timing cap nodes.
startCapNode.hidden = false;
endCapNode.hidden = false;
// Rescale all the waterfalls so that everything is visible at once.
this._flushWaterfallViews();
},
/**
* Rescales and redraws all the waterfall views in this container.
*
* @param boolean aReset
* True if this container's width was changed.
*/
_flushWaterfallViews: function(aReset) {
// To avoid expensive operations like getBoundingClientRect() and
// rebuilding the waterfall background each time a new request comes in,
// stuff is cached. However, in certain scenarios like when the window
// is resized, this needs to be invalidated.
if (aReset) {
this._cachedWaterfallWidth = 0;
this._hideOverflowingColumns();
}
// Determine the scaling to be applied to all the waterfalls so that
// everything is visible at once. One millisecond == one unscaled pixel.
let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
let longestWidth = this._lastRequestEndedMillis - this._firstRequestStartedMillis;
let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1);
// Redraw and set the canvas background for each waterfall view.
this._showWaterfallDivisionLabels(scale);
this._drawWaterfallBackground(scale);
this._flushWaterfallBackgrounds();
// Apply CSS transforms to each waterfall in this container totalTime
// accurately translate and resize as needed.
for (let { target, attachment } in this) {
let timingsNode = $(".requests-menu-timings", target);
let startCapNode = $(".requests-menu-timings-cap.start", target);
let endCapNode = $(".requests-menu-timings-cap.end", target);
let totalNode = $(".requests-menu-timings-total", target);
let direction = window.isRTL ? -1 : 1;
// Render the timing information at a specific horizontal translation
// based on the delta to the first monitored event network.
let translateX = "translateX(" + (direction * attachment.startedDeltaMillis) + "px)";
// Based on the total time passed until the last request, rescale
// all the waterfalls to a reasonable size.
let scaleX = "scaleX(" + scale + ")";
// Certain nodes should not be scaled, even if they're children of
// another scaled node. In this case, apply a reversed transformation.
let revScaleX = "scaleX(" + (1 / scale) + ")";
timingsNode.style.transform = scaleX + " " + translateX;
startCapNode.style.transform = revScaleX + " translateX(" + (direction * 0.5) + "px)";
endCapNode.style.transform = revScaleX + " translateX(" + (direction * -0.5) + "px)";
totalNode.style.transform = revScaleX;
}
},
/**
* Creates the labels displayed on the waterfall header in this container.
*
* @param number aScale
* The current waterfall scale.
*/
_showWaterfallDivisionLabels: function(aScale) {
let container = $("#requests-menu-waterfall-button");
let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
// Nuke all existing labels.
while (container.hasChildNodes()) {
container.firstChild.remove();
}
// Build new millisecond tick labels...
let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
let optimalTickIntervalFound = false;
while (!optimalTickIntervalFound) {
// Ignore any divisions that would end up being too close to each other.
let scaledStep = aScale * timingStep;
if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
timingStep <<= 1;
continue;
}
optimalTickIntervalFound = true;
// Insert one label for each division on the current scale.
let fragment = document.createDocumentFragment();
let direction = window.isRTL ? -1 : 1;
for (let x = 0; x < availableWidth; x += scaledStep) {
let divisionMS = (x / aScale).toFixed(0);
let translateX = "translateX(" + ((direction * x) | 0) + "px)";
let node = document.createElement("label");
let text = L10N.getFormatStr("networkMenu.divisionMS", divisionMS);
node.className = "plain requests-menu-timings-division";
node.style.transform = translateX;
node.setAttribute("value", text);
fragment.appendChild(node);
}
container.appendChild(fragment);
}
},
/**
* Creates the background displayed on each waterfall view in this container.
*
* @param number aScale
* The current waterfall scale.
*/
_drawWaterfallBackground: function(aScale) {
if (!this._canvas || !this._ctx) {
this._canvas = document.createElementNS(HTML_NS, "canvas");
this._ctx = this._canvas.getContext("2d");
}
let canvas = this._canvas;
let ctx = this._ctx;
// Nuke the context.
let canvasWidth = canvas.width = this._waterfallWidth;
let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
// Start over.
let imageData = ctx.createImageData(canvasWidth, canvasHeight);
let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length);
let buf8 = new Uint8ClampedArray(buf);
let data32 = new Uint32Array(buf);
// Build new millisecond tick lines...
let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE;
let [r, g, b] = REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
let optimalTickIntervalFound = false;
while (!optimalTickIntervalFound) {
// Ignore any divisions that would end up being too close to each other.
let scaledStep = aScale * timingStep;
if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) {
timingStep <<= 1;
continue;
}
optimalTickIntervalFound = true;
// Insert one pixel for each division on each scale.
for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
let increment = scaledStep * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) {
let position = (window.isRTL ? canvasWidth - x : x) | 0;
data32[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
}
}
// Flush the image data and cache the waterfall background.
pixelArray.set(buf8);
ctx.putImageData(imageData, 0, 0);
this._cachedWaterfallBackground = "url(" + canvas.toDataURL() + ")";
},
/**
* Reapplies the current waterfall background on all request items.
*/
_flushWaterfallBackgrounds: function() {
for (let { target } in this) {
let waterfallNode = $(".requests-menu-waterfall", target);
waterfallNode.style.backgroundImage = this._cachedWaterfallBackground;
}
},
/**
* Hides the overflowing columns in the requests table.
*/
_hideOverflowingColumns: function() {
if (window.isRTL) {
return;
}
let table = $("#network-table");
let toolbar = $("#requests-menu-toolbar");
let columns = [
["#requests-menu-waterfall-header-box", "waterfall-overflows"],
["#requests-menu-size-header-box", "size-overflows"],
["#requests-menu-type-header-box", "type-overflows"],
["#requests-menu-domain-header-box", "domain-overflows"]
];
// Flush headers.
columns.forEach(([, attribute]) => table.removeAttribute(attribute));
let availableWidth = toolbar.getBoundingClientRect().width;
// Hide the columns.
columns.forEach(([id, attribute]) => {
let bounds = $(id).getBoundingClientRect();
if (bounds.right > availableWidth - REQUESTS_HEADERS_SAFE_BOUNDS) {
table.setAttribute(attribute, "");
}
});
},
/**
* The selection listener for this container.
*/
_onSelect: function({ detail: item }) {
if (item) {
NetMonitorView.Sidebar.populate(item.attachment);
NetMonitorView.Sidebar.toggle(true);
} else {
NetMonitorView.Sidebar.toggle(false);
}
},
/**
* The resize listener for this container's window.
*/
_onResize: function(e) {
// Allow requests to settle down first.
setNamedTimeout(
"resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
},
/**
* Handle the context menu opening. Hide items if no request is selected.
*/
_onContextShowing: function() {
let resendElement = $("#request-menu-context-resend");
resendElement.hidden = !this.selectedItem || this.selectedItem.attachment.isCustom;
let copyUrlElement = $("#request-menu-context-copy-url");
copyUrlElement.hidden = !this.selectedItem;
},
/**
* Checks if the specified unix time is the first one to be known of,
* and saves it if so.
*
* @param number aUnixTime
* The milliseconds to check and save.
*/
_registerFirstRequestStart: function(aUnixTime) {
if (this._firstRequestStartedMillis == -1) {
this._firstRequestStartedMillis = aUnixTime;
}
},
/**
* Checks if the specified unix time is the last one to be known of,
* and saves it if so.
*
* @param number aUnixTime
* The milliseconds to check and save.
*/
_registerLastRequestEnd: function(aUnixTime) {
if (this._lastRequestEndedMillis < aUnixTime) {
this._lastRequestEndedMillis = aUnixTime;
}
},
/**
* Helpers for getting details about an nsIURL.
*
* @param nsIURL | string aUrl
* @return string
*/
_getUriNameWithQuery: function(aUrl) {
if (!(aUrl instanceof Ci.nsIURL)) {
aUrl = nsIURL(aUrl);
}
let name = NetworkHelper.convertToUnicode(unescape(aUrl.fileName)) || "/";
let query = NetworkHelper.convertToUnicode(unescape(aUrl.query));
return name + (query ? "?" + query : "");
},
_getUriHostPort: function(aUrl) {
if (!(aUrl instanceof Ci.nsIURL)) {
aUrl = nsIURL(aUrl);
}
return NetworkHelper.convertToUnicode(unescape(aUrl.hostPort));
},
/**
* Helper for getting an abbreviated string for a mime type.
*
* @param string aMimeType
* @return string
*/
_getAbbreviatedMimeType: function(aMimeType) {
if (!aMimeType) {
return "";
}
return (aMimeType.split(";")[0].split("/")[1] || "").split("+")[0];
},
/**
* Gets the total number of bytes representing the cumulated content size of
* a set of requests. Returns 0 for an empty set.
*
* @param array aItemsArray
* @return number
*/
_getTotalBytesOfRequests: function(aItemsArray) {
if (!aItemsArray.length) {
return 0;
}
return aItemsArray.reduce((prev, curr) => prev + curr.attachment.contentSize || 0, 0);
},
/**
* Gets the oldest (first performed) request in a set. Returns null for an
* empty set.
*
* @param array aItemsArray
* @return object
*/
_getOldestRequest: function(aItemsArray) {
if (!aItemsArray.length) {
return null;
}
return aItemsArray.reduce((prev, curr) =>
prev.attachment.startedMillis < curr.attachment.startedMillis ? prev : curr);
},
/**
* Gets the newest (latest performed) request in a set. Returns null for an
* empty set.
*
* @param array aItemsArray
* @return object
*/
_getNewestRequest: function(aItemsArray) {
if (!aItemsArray.length) {
return null;
}
return aItemsArray.reduce((prev, curr) =>
prev.attachment.startedMillis > curr.attachment.startedMillis ? prev : curr);
},
/**
* Gets the available waterfall width in this container.
* @return number
*/
get _waterfallWidth() {
if (this._cachedWaterfallWidth == 0) {
let container = $("#requests-menu-toolbar");
let waterfall = $("#requests-menu-waterfall-header-box");
let containerBounds = container.getBoundingClientRect();
let waterfallBounds = waterfall.getBoundingClientRect();
if (!window.isRTL) {
this._cachedWaterfallWidth = containerBounds.width - waterfallBounds.left;
} else {
this._cachedWaterfallWidth = waterfallBounds.right;
}
}
return this._cachedWaterfallWidth;
},
_splitter: null,
_summary: null,
_canvas: null,
_ctx: null,
_cachedWaterfallWidth: 0,
_cachedWaterfallBackground: "",
_firstRequestStartedMillis: -1,
_lastRequestEndedMillis: -1,
_updateQueue: [],
_updateTimeout: null,
_resizeTimeout: null
});
/**
* Functions handling the sidebar details view.
*/
function SidebarView() {
dumpn("SidebarView was instantiated");
}
SidebarView.prototype = {
/**
* Sets this view hidden or visible. It's visible by default.
*
* @param boolean aVisibleFlag
* Specifies the intended visibility.
*/
toggle: function(aVisibleFlag) {
NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag });
NetMonitorView.RequestsMenu._flushWaterfallViews(true);
},
/**
* Populates this view with the specified data.
*
* @param object aData
* The data source (this should be the attachment of a request item).
*/
populate: function(aData) {
if (aData.isCustom) {
NetMonitorView.CustomRequest.populate(aData);
$("#details-pane").selectedIndex = 0;
} else {
NetMonitorView.NetworkDetails.populate(aData);
$("#details-pane").selectedIndex = 1;
}
},
/**
* Hides this container.
*/
reset: function() {
this.toggle(false);
}
}
/**
* Functions handling the custom request view.
*/
function CustomRequestView() {
dumpn("CustomRequestView was instantiated");
}
CustomRequestView.prototype = {
/**
* Populates this view with the specified data.
*
* @param object aData
* The data source (this should be the attachment of a request item).
*/
populate: function(aData) {
$("#custom-url-value").value = aData.url;
$("#custom-method-value").value = aData.method;
$("#custom-headers-value").value =
writeHeaderText(aData.requestHeaders.headers);
if (aData.requestPostData) {
let body = aData.requestPostData.postData.text;
gNetwork.getString(body).then(aString => {
$("#custom-postdata-value").value = aString;
});
}
this.updateCustomQuery(aData.url);
},
/**
* Handle user input in the custom request form.
*
* @param object aField
* the field that the user updated.
*/
onUpdate: function(aField) {
let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
let field = aField;
let value;
switch(aField) {
case 'method':
value = $("#custom-method-value").value.trim();
selectedItem.attachment.method = value;
break;
case 'url':
value = $("#custom-url-value").value;
this.updateCustomQuery(value);
selectedItem.attachment.url = value;
break;
case 'query':
let query = $("#custom-query-value").value;
this.updateCustomUrl(query);
field = 'url';
value = $("#custom-url-value").value
selectedItem.attachment.url = value;
break;
case 'body':
value = $("#custom-postdata-value").value;
selectedItem.attachment.requestPostData = {
postData: {
text: value
}
};
break;
case 'headers':
let headersText = $("#custom-headers-value").value;
value = parseHeaderText(headersText);
selectedItem.attachment.requestHeaders = {
headers: value
};
break;
}
NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value);
},
/**
* Update the query string field based on the url.
*
* @param object aUrl
* url to extract query string from.
*/
updateCustomQuery: function(aUrl) {
let paramsArray = parseQueryString(nsIURL(aUrl).query);
if (!paramsArray) {
$("#custom-query").hidden = true;
return;
}
$("#custom-query").hidden = false;
$("#custom-query-value").value = writeQueryText(paramsArray);
},
/**
* Update the url based on the query string field.
*
* @param object aQueryText
* contents of the query string field.
*/
updateCustomUrl: function(aQueryText) {
let params = parseQueryText(aQueryText);
let queryString = writeQueryString(params);
let url = $("#custom-url-value").value;
let oldQuery = nsIURL(url).query;
let path = url.replace(oldQuery, queryString);
$("#custom-url-value").value = path;
}
}
/**
* Functions handling the requests details view.
*/
function NetworkDetailsView() {
dumpn("NetworkDetailsView was instantiated");
this._onTabSelect = this._onTabSelect.bind(this);
};
NetworkDetailsView.prototype = {
/**
* Initialization function, called when the network monitor is started.
*/
initialize: function() {
dumpn("Initializing the NetworkDetailsView");
this.widget = $("#event-details-pane");
this._headers = new VariablesView($("#all-headers"),
Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
emptyText: L10N.getStr("headersEmptyText"),
searchPlaceholder: L10N.getStr("headersFilterText")
}));
this._cookies = new VariablesView($("#all-cookies"),
Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
emptyText: L10N.getStr("cookiesEmptyText"),
searchPlaceholder: L10N.getStr("cookiesFilterText")
}));
this._params = new VariablesView($("#request-params"),
Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
emptyText: L10N.getStr("paramsEmptyText"),
searchPlaceholder: L10N.getStr("paramsFilterText")
}));
this._json = new VariablesView($("#response-content-json"),
Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
searchPlaceholder: L10N.getStr("jsonFilterText")
}));
this._paramsQueryString = L10N.getStr("paramsQueryString");
this._paramsFormData = L10N.getStr("paramsFormData");
this._paramsPostPayload = L10N.getStr("paramsPostPayload");
this._requestHeaders = L10N.getStr("requestHeaders");
this._responseHeaders = L10N.getStr("responseHeaders");
this._requestCookies = L10N.getStr("requestCookies");
this._responseCookies = L10N.getStr("responseCookies");
$("tabpanels", this.widget).addEventListener("select", this._onTabSelect);
},
/**
* Destruction function, called when the network monitor is closed.
*/
destroy: function() {
dumpn("Destroying the NetworkDetailsView");
},
/**
* Resets this container (removes all the networking information).
*/
reset: function() {
this._dataSrc = null;
},
/**
* Populates this view with the specified data.
*
* @param object aData
* The data source (this should be the attachment of a request item).
*/
populate: function(aData) {
$("#request-params-box").setAttribute("flex", "1");
$("#request-params-box").hidden = false;
$("#request-post-data-textarea-box").hidden = true;
$("#response-content-info-header").hidden = true;
$("#response-content-json-box").hidden = true;
$("#response-content-textarea-box").hidden = true;
$("#response-content-image-box").hidden = true;
this._headers.empty();
this._cookies.empty();
this._params.empty();
this._json.empty();
this._dataSrc = { src: aData, populated: [] };
this._onTabSelect();
},
/**
* Listener handling the tab selection event.
*/
_onTabSelect: function() {
let { src, populated } = this._dataSrc || {};
let tab = this.widget.selectedIndex;
// Make sure the data source is valid and don't populate the same tab twice.
if (!src || populated[tab]) {
return;
}
switch (tab) {
case 0: // "Headers"
this._setSummary(src);
this._setResponseHeaders(src.responseHeaders);
this._setRequestHeaders(src.requestHeaders);
break;
case 1: // "Cookies"
this._setResponseCookies(src.responseCookies);
this._setRequestCookies(src.requestCookies);
break;
case 2: // "Params"
this._setRequestGetParams(src.url);
this._setRequestPostParams(src.requestHeaders, src.requestPostData);
break;
case 3: // "Response"
this._setResponseBody(src.url, src.responseContent);
break;
case 4: // "Timings"
this._setTimingsInformation(src.eventTimings);
break;
}
populated[tab] = true;
},
/**
* Sets the network request summary shown in this view.
*
* @param object aData
* The data source (this should be the attachment of a request item).
*/
_setSummary: function(aData) {
if (aData.url) {
let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aData.url));
$("#headers-summary-url-value").setAttribute("value", unicodeUrl);
$("#headers-summary-url-value").setAttribute("tooltiptext", unicodeUrl);
$("#headers-summary-url").removeAttribute("hidden");
} else {
$("#headers-summary-url").setAttribute("hidden", "true");
}
if (aData.method) {
$("#headers-summary-method-value").setAttribute("value", aData.method);
$("#headers-summary-method").removeAttribute("hidden");
} else {
$("#headers-summary-method").setAttribute("hidden", "true");
}
if (aData.status) {
$("#headers-summary-status-circle").setAttribute("code", aData.status);
$("#headers-summary-status-value").setAttribute("value", aData.status + " " + aData.statusText);
$("#headers-summary-status").removeAttribute("hidden");
} else {
$("#headers-summary-status").setAttribute("hidden", "true");
}
if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) {
$("#headers-summary-version-value").setAttribute("value", aData.httpVersion);
$("#headers-summary-version").removeAttribute("hidden");
} else {
$("#headers-summary-version").setAttribute("hidden", "true");
}
},
/**
* Sets the network request headers shown in this view.
*
* @param object aResponse
* The message received from the server.
*/
_setRequestHeaders: function(aResponse) {
if (aResponse && aResponse.headers.length) {
this._addHeaders(this._requestHeaders, aResponse);
}
},
/**
* Sets the network response headers shown in this view.
*
* @param object aResponse
* The message received from the server.
*/
_setResponseHeaders: function(aResponse) {
if (aResponse && aResponse.headers.length) {
aResponse.headers.sort((a, b) => a.name > b.name);
this._addHeaders(this._responseHeaders, aResponse);
}
},
/**
* Populates the headers container in this view with the specified data.
*
* @param string aName
* The type of headers to populate (request or response).
* @param object aResponse
* The message received from the server.
*/
_addHeaders: function(aName, aResponse) {
let kb = aResponse.headersSize / 1024;
let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
let text = L10N.getFormatStr("networkMenu.sizeKB", size);
let headersScope = this._headers.addScope(aName + " (" + text + ")");
headersScope.expanded = true;
for (let header of aResponse.headers) {
let headerVar = headersScope.addItem(header.name, {}, true);
gNetwork.getString(header.value).then(aString => headerVar.setGrip(aString));
}
},
/**
* Sets the network request cookies shown in this view.
*
* @param object aResponse
* The message received from the server.
*/
_setRequestCookies: function(aResponse) {
if (aResponse && aResponse.cookies.length) {
aResponse.cookies.sort((a, b) => a.name > b.name);
this._addCookies(this._requestCookies, aResponse);
}
},
/**
* Sets the network response cookies shown in this view.
*
* @param object aResponse
* The message received from the server.
*/
_setResponseCookies: function(aResponse) {
if (aResponse && aResponse.cookies.length) {
this._addCookies(this._responseCookies, aResponse);
}
},
/**
* Populates the cookies container in this view with the specified data.
*
* @param string aName
* The type of cookies to populate (request or response).
* @param object aResponse
* The message received from the server.
*/
_addCookies: function(aName, aResponse) {
let cookiesScope = this._cookies.addScope(aName);
cookiesScope.expanded = true;
for (let cookie of aResponse.cookies) {
let cookieVar = cookiesScope.addItem(cookie.name, {}, true);
gNetwork.getString(cookie.value).then(aString => cookieVar.setGrip(aString));
// By default the cookie name and value are shown. If this is the only
// information available, then nothing else is to be displayed.
let cookieProps = Object.keys(cookie);
if (cookieProps.length == 2) {
continue;
}
// Display any other information other than the cookie name and value
// which may be available.
let rawObject = Object.create(null);
let otherProps = cookieProps.filter(e => e != "name" && e != "value");
for (let prop of otherProps) {
rawObject[prop] = cookie[prop];
}
cookieVar.populate(rawObject);
cookieVar.twisty = true;
cookieVar.expanded = true;
}
},
/**
* Sets the network request get params shown in this view.
*
* @param string aUrl
* The request's url.
*/
_setRequestGetParams: function(aUrl) {
let query = nsIURL(aUrl).query;
if (query) {
this._addParams(this._paramsQueryString, query);
}
},
/**
* Sets the network request post params shown in this view.
*
* @param object aHeadersResponse
* The "requestHeaders" message received from the server.
* @param object aPostDataResponse
* The "requestPostData" message received from the server.
*/
_setRequestPostParams: function(aHeadersResponse, aPostDataResponse) {
if (!aHeadersResponse || !aPostDataResponse) {
return;
}
gNetwork.getString(aPostDataResponse.postData.text).then(aString => {
// Handle query strings (poor man's forms, e.g. "?foo=bar&baz=42").
let cType = aHeadersResponse.headers.filter(({ name }) => name == "Content-Type")[0];
let cString = cType ? cType.value : "";
if (cString.contains("x-www-form-urlencoded") ||
aString.contains("x-www-form-urlencoded")) {
let formDataGroups = aString.split(/\r\n|\n|\r/);
for (let group of formDataGroups) {
this._addParams(this._paramsFormData, group);
}
}
// Handle actual forms ("multipart/form-data" content type).
else {
// This is really awkward, but hey, it works. Let's show an empty
// scope in the params view and place the source editor containing
// the raw post data directly underneath.
$("#request-params-box").removeAttribute("flex");
let paramsScope = this._params.addScope(this._paramsPostPayload);
paramsScope.expanded = true;
paramsScope.locked = true;
$("#request-post-data-textarea-box").hidden = false;
NetMonitorView.editor("#request-post-data-textarea").then(aEditor => {
aEditor.setText(aString);
});
}
window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
});
},
/**
* Populates the params container in this view with the specified data.
*
* @param string aName
* The type of params to populate (get or post).
* @param string aQueryString
* A query string of params (e.g. "?foo=bar&baz=42").
*/
_addParams: function(aName, aQueryString) {
let paramsArray = parseQueryString(aQueryString);
if (!paramsArray) {
return;
}
let paramsScope = this._params.addScope(aName);
paramsScope.expanded = true;
for (let param of paramsArray) {
let headerVar = paramsScope.addItem(param.name, {}, true);
headerVar.setGrip(param.value);
}
},
/**
* Sets the network response body shown in this view.
*
* @param string aUrl
* The request's url.
* @param object aResponse
* The message received from the server.
*/
_setResponseBody: function(aUrl, aResponse) {
if (!aResponse) {
return;
}
let { mimeType, text, encoding } = aResponse.content;
gNetwork.getString(text).then(aString => {
// Handle json, which we tentatively identify by checking the MIME type
// for "json" after any word boundary. This works for the standard
// "application/json", and also for custom types like "x-bigcorp-json".
// This should be marginally more reliable than just looking for "json".
if (/\bjson/.test(mimeType)) {
let jsonpRegex = /^[a-zA-Z0-9_$]+\(|\)$/g; // JSONP with callback.
let sanitizedJSON = aString.replace(jsonpRegex, "");
let callbackPadding = aString.match(jsonpRegex);
// Make sure this is an valid JSON object first. If so, nicely display
// the parsing results in a variables view. Otherwise, simply show
// the contents as plain text.
try {
var jsonObject = JSON.parse(sanitizedJSON);
} catch (e) {
var parsingError = e;
}
// Valid JSON.
if (jsonObject) {
$("#response-content-json-box").hidden = false;
let jsonScopeName = callbackPadding
? L10N.getFormatStr("jsonpScopeName", callbackPadding[0].slice(0, -1))
: L10N.getStr("jsonScopeName");
let jsonScope = this._json.addScope(jsonScopeName);
jsonScope.addItem().populate(jsonObject, { expanded: true });
jsonScope.expanded = true;
}
// Malformed JSON.
else {
$("#response-content-textarea-box").hidden = false;
NetMonitorView.editor("#response-content-textarea").then(aEditor => {
aEditor.setMode(SourceEditor.MODES.JAVASCRIPT);
aEditor.setText(aString);
});
let infoHeader = $("#response-content-info-header");
infoHeader.setAttribute("value", parsingError);
infoHeader.setAttribute("tooltiptext", parsingError);
infoHeader.hidden = false;
}
}
// Handle images.
else if (mimeType.contains("image/")) {
$("#response-content-image-box").setAttribute("align", "center");
$("#response-content-image-box").setAttribute("pack", "center");
$("#response-content-image-box").hidden = false;
$("#response-content-image").src =
"data:" + mimeType + ";" + encoding + "," + aString;
// Immediately display additional information about the image:
// file name, mime type and encoding.
$("#response-content-image-name-value").setAttribute("value", nsIURL(aUrl).fileName);
$("#response-content-image-mime-value").setAttribute("value", mimeType);
$("#response-content-image-encoding-value").setAttribute("value", encoding);
// Wait for the image to load in order to display the width and height.
$("#response-content-image").onload = e => {
// XUL images are majestic so they don't bother storing their dimensions
// in width and height attributes like the rest of the folk. Hack around
// this by getting the bounding client rect and subtracting the margins.
let { width, height } = e.target.getBoundingClientRect();
let dimensions = (width - 2) + " x " + (height - 2);
$("#response-content-image-dimensions-value").setAttribute("value", dimensions);
};
}
// Handle anything else.
else {
$("#response-content-textarea-box").hidden = false;
NetMonitorView.editor("#response-content-textarea").then(aEditor => {
aEditor.setMode(SourceEditor.MODES.TEXT);
aEditor.setText(aString);
// Maybe set a more appropriate mode in the Source Editor if possible,
// but avoid doing this for very large files.
if (aString.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
for (let key in CONTENT_MIME_TYPE_MAPPINGS) {
if (mimeType.contains(key)) {
aEditor.setMode(CONTENT_MIME_TYPE_MAPPINGS[key]);
break;
}
}
}
});
}
window.emit(EVENTS.RESPONSE_BODY_DISPLAYED);
});
},
/**
* Sets the timings information shown in this view.
*
* @param object aResponse
* The message received from the server.
*/
_setTimingsInformation: function(aResponse) {
if (!aResponse) {
return;
}
let { blocked, dns, connect, send, wait, receive } = aResponse.timings;
let tabboxWidth = $("#details-pane").getAttribute("width");
let availableWidth = tabboxWidth / 2; // Other nodes also take some space.
let scale = Math.max(availableWidth / aResponse.totalTime, 0);
$("#timings-summary-blocked .requests-menu-timings-box")
.setAttribute("width", blocked * scale);
$("#timings-summary-blocked .requests-menu-timings-total")
.setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", blocked));
$("#timings-summary-dns .requests-menu-timings-box")
.setAttribute("width", dns * scale);
$("#timings-summary-dns .requests-menu-timings-total")
.setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", dns));
$("#timings-summary-connect .requests-menu-timings-box")
.setAttribute("width", connect * scale);
$("#timings-summary-connect .requests-menu-timings-total")
.setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", connect));
$("#timings-summary-send .requests-menu-timings-box")
.setAttribute("width", send * scale);
$("#timings-summary-send .requests-menu-timings-total")
.setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", send));
$("#timings-summary-wait .requests-menu-timings-box")
.setAttribute("width", wait * scale);
$("#timings-summary-wait .requests-menu-timings-total")
.setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", wait));
$("#timings-summary-receive .requests-menu-timings-box")
.setAttribute("width", receive * scale);
$("#timings-summary-receive .requests-menu-timings-total")
.setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", receive));
$("#timings-summary-dns .requests-menu-timings-box")
.style.transform = "translateX(" + (scale * blocked) + "px)";
$("#timings-summary-connect .requests-menu-timings-box")
.style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
$("#timings-summary-send .requests-menu-timings-box")
.style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)";
$("#timings-summary-wait .requests-menu-timings-box")
.style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
$("#timings-summary-receive .requests-menu-timings-box")
.style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)";
$("#timings-summary-dns .requests-menu-timings-total")
.style.transform = "translateX(" + (scale * blocked) + "px)";
$("#timings-summary-connect .requests-menu-timings-total")
.style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
$("#timings-summary-send .requests-menu-timings-total")
.style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)";
$("#timings-summary-wait .requests-menu-timings-total")
.style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
$("#timings-summary-receive .requests-menu-timings-total")
.style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)";
},
_dataSrc: null,
_headers: null,
_cookies: null,
_params: null,
_json: null,
_paramsQueryString: "",
_paramsFormData: "",
_paramsPostPayload: "",
_requestHeaders: "",
_responseHeaders: "",
_requestCookies: "",
_responseCookies: ""
};
/**
* DOM query helper.
*/
function $(aSelector, aTarget = document) aTarget.querySelector(aSelector);
/**
* Helper for getting an nsIURL instance out of a string.
*/
function nsIURL(aUrl, aStore = nsIURL.store) {
if (aStore.has(aUrl)) {
return aStore.get(aUrl);
}
let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
aStore.set(aUrl, uri);
return uri;
}
nsIURL.store = new Map();
/**
* Parse a url's query string into its components
*
* @param string aQueryString
* The query part of a url
* @return array
* Array of query params {name, value}
*/
function parseQueryString(aQueryString) {
// Make sure there's at least one param available.
if (!aQueryString || !aQueryString.contains("=")) {
return;
}
// Turn the params string into an array containing { name: value } tuples.
let paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map(e =>
let (param = e.split("=")) {
name: NetworkHelper.convertToUnicode(unescape(param[0])),
value: NetworkHelper.convertToUnicode(unescape(param[1]))
});
return paramsArray;
}
/**
* Parse text representation of HTTP headers.
*
* @param string aText
* Text of headers
* @return array
* Array of headers info {name, value}
*/
function parseHeaderText(aText) {
return parseRequestText(aText, ":");
}
/**
* Parse readable text list of a query string.
*
* @param string aText
* Text of query string represetation
* @return array
* Array of query params {name, value}
*/
function parseQueryText(aText) {
return parseRequestText(aText, "=");
}
/**
* Parse a text representation of a name:value list with
* the given name:value divider character.
*
* @param string aText
* Text of list
* @return array
* Array of headers info {name, value}
*/
function parseRequestText(aText, aDivider) {
let regex = new RegExp("(.+?)\\" + aDivider + "\\s*(.+)");
let pairs = [];
for (let line of aText.split("\n")) {
let matches;
if (matches = regex.exec(line)) {
let [, name, value] = matches;
pairs.push({name: name, value: value});
}
}
return pairs;
}
/**
* Write out a list of headers into a chunk of text
*
* @param array aHeaders
* Array of headers info {name, value}
* @return string aText
* List of headers in text format
*/
function writeHeaderText(aHeaders) {
return [(name + ": " + value) for ({name, value} of aHeaders)].join("\n");
}
/**
* Write out a list of query params into a chunk of text
*
* @param array aParams
* Array of query params {name, value}
* @return string
* List of query params in text format
*/
function writeQueryText(aParams) {
return [(name + "=" + value) for ({name, value} of aParams)].join("\n");
}
/**
* Write out a list of query params into a query string
*
* @param array aParams
* Array of query params {name, value}
* @return string
* Query string that can be appended to a url.
*/
function writeQueryString(aParams) {
return [(name + "=" + value) for ({name, value} of aParams)].join("&");
}
/**
* Preliminary setup for the NetMonitorView object.
*/
NetMonitorView.Toolbar = new ToolbarView();
NetMonitorView.RequestsMenu = new RequestsMenuView();
NetMonitorView.Sidebar = new SidebarView();
NetMonitorView.CustomRequest = new CustomRequestView();
NetMonitorView.NetworkDetails = new NetworkDetailsView();