mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
2154 lines
69 KiB
JavaScript
2154 lines
69 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._summary = $("#request-menu-network-summary");
|
|
|
|
this.allowFocusOnRightClick = true;
|
|
this.widget.maintainSelectionVisible = false;
|
|
this.widget.autoscrollWithAppendedItems = true;
|
|
|
|
this.widget.addEventListener("select", this._onSelect, 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);
|
|
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,
|
|
|
|
_byMethod: function({ attachment: first }, { attachment: second })
|
|
first.method > second.method,
|
|
|
|
_byFile: function({ attachment: first }, { attachment: second })
|
|
this._getUriNameWithQuery(first.url).toLowerCase() >
|
|
this._getUriNameWithQuery(second.url).toLowerCase(),
|
|
|
|
_byDomain: function({ attachment: first }, { attachment: second })
|
|
this._getUriHostPort(first.url).toLowerCase() >
|
|
this._getUriHostPort(second.url).toLowerCase(),
|
|
|
|
_byType: function({ attachment: first }, { attachment: second })
|
|
this._getAbbreviatedMimeType(first.mimeType).toLowerCase() >
|
|
this._getAbbreviatedMimeType(second.mimeType).toLowerCase(),
|
|
|
|
_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.orderedVisibleItems;
|
|
|
|
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.
|
|
drain("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.
|
|
drain("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;
|
|
},
|
|
|
|
_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.
|
|
if (mimeType.contains("/json")) {
|
|
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("&");
|
|
}
|
|
|
|
/**
|
|
* Helper for draining a rapid succession of events and invoking a callback
|
|
* once everything settles down.
|
|
*/
|
|
function drain(aId, aWait, aCallback, aStore = drain.store) {
|
|
window.clearTimeout(aStore.get(aId));
|
|
aStore.set(aId, window.setTimeout(aCallback, aWait));
|
|
}
|
|
drain.store = new Map();
|
|
|
|
/**
|
|
* 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();
|