summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/netmonitor-view.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/netmonitor/netmonitor-view.js')
-rw-r--r--devtools/client/netmonitor/netmonitor-view.js1230
1 files changed, 1230 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/netmonitor-view.js b/devtools/client/netmonitor/netmonitor-view.js
new file mode 100644
index 000000000..68470f7a9
--- /dev/null
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -0,0 +1,1230 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+/* import-globals-from ./netmonitor-controller.js */
+/* globals Prefs, gNetwork, setInterval, setTimeout, clearInterval, clearTimeout, btoa */
+/* exported $, $all */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function () {
+ return require("devtools/shared/webconsole/network-helper");
+});
+
+/* eslint-disable mozilla/reject-some-requires */
+const {VariablesView} = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
+/* eslint-disable mozilla/reject-some-requires */
+const {VariablesViewController} = require("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+const {ToolSidebar} = require("devtools/client/framework/sidebar");
+const {testing: isTesting} = require("devtools/shared/flags");
+const {ViewHelpers, Heritage} = require("devtools/client/shared/widgets/view-helpers");
+const {Filters} = require("./filter-predicates");
+const {getFormDataSections,
+ formDataURI,
+ getUriHostPort} = require("./request-utils");
+const {L10N} = require("./l10n");
+const {RequestsMenuView} = require("./requests-menu-view");
+const {CustomRequestView} = require("./custom-request-view");
+const {ToolbarView} = require("./toolbar-view");
+const {configureStore} = require("./store");
+const {PerformanceStatisticsView} = require("./performance-statistics-view");
+
+// Initialize the global redux variables
+var gStore = configureStore();
+
+// ms
+const WDA_DEFAULT_VERIFY_INTERVAL = 50;
+
+// Use longer timeout during testing as the tests need this process to succeed
+// and two seconds is quite short on slow debug builds. The timeout here should
+// be at least equal to the general mochitest timeout of 45 seconds so that this
+// never gets hit during testing.
+// ms
+const WDA_DEFAULT_GIVE_UP_TIMEOUT = isTesting ? 45000 : 2000;
+
+// 100 KB in bytes
+const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400;
+const HEADERS_SIZE_DECIMALS = 3;
+const CONTENT_MIME_TYPE_MAPPINGS = {
+ "/ecmascript": Editor.modes.js,
+ "/javascript": Editor.modes.js,
+ "/x-javascript": Editor.modes.js,
+ "/html": Editor.modes.html,
+ "/xhtml": Editor.modes.html,
+ "/xml": Editor.modes.html,
+ "/atom": Editor.modes.html,
+ "/soap": Editor.modes.html,
+ "/vnd.mpeg.dash.mpd": Editor.modes.html,
+ "/rdf": Editor.modes.css,
+ "/rss": Editor.modes.css,
+ "/css": Editor.modes.css
+};
+
+const DEFAULT_EDITOR_CONFIG = {
+ mode: Editor.modes.text,
+ readOnly: true,
+ lineNumbers: true
+};
+const GENERIC_VARIABLES_VIEW_SETTINGS = {
+ lazyEmpty: true,
+ // ms
+ lazyEmptyDelay: 10,
+ searchEnabled: true,
+ editableValueTooltip: "",
+ editableNameTooltip: "",
+ preventDisableOnChange: true,
+ preventDescriptorModifiers: true,
+ eval: () => {}
+};
+
+/**
+ * Object defining the network monitor view components.
+ */
+var NetMonitorView = {
+ /**
+ * Initializes the network monitor view.
+ */
+ initialize: function () {
+ this._initializePanes();
+
+ this.Toolbar.initialize(gStore);
+ this.RequestsMenu.initialize(gStore);
+ this.NetworkDetails.initialize();
+ this.CustomRequest.initialize();
+ this.PerformanceStatistics.initialize(gStore);
+ },
+
+ /**
+ * Destroys the network monitor view.
+ */
+ destroy: function () {
+ this._isDestroyed = true;
+ this.Toolbar.destroy();
+ this.RequestsMenu.destroy();
+ this.NetworkDetails.destroy();
+ this.CustomRequest.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._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
+ this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight);
+ this.toggleDetailsPane({ visible: false });
+
+ // Disable the performance statistics mode.
+ if (!Prefs.statistics) {
+ $("#request-menu-context-perf").hidden = true;
+ $("#notice-perf-message").hidden = true;
+ $("#requests-menu-network-summary-button").hidden = true;
+ }
+ },
+
+ /**
+ * Destroys the UI for all the displayed panes.
+ */
+ _destroyPanes: Task.async(function* () {
+ dumpn("Destroying the NetMonitorView panes");
+
+ Prefs.networkDetailsWidth = this._detailsPane.getAttribute("width");
+ Prefs.networkDetailsHeight = this._detailsPane.getAttribute("height");
+
+ this._detailsPane = null;
+
+ for (let p of this._editorPromises.values()) {
+ let editor = yield p;
+ editor.destroy();
+ }
+ }),
+
+ /**
+ * Gets the visibility state of the network details pane.
+ * @return boolean
+ */
+ get detailsPaneHidden() {
+ return this._detailsPane.classList.contains("pane-collapsed");
+ },
+
+ /**
+ * Sets the network details pane hidden or visible.
+ *
+ * @param object flags
+ * 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 tabIndex [optional]
+ * The index of the intended selected tab in the details pane.
+ */
+ toggleDetailsPane: function (flags, tabIndex) {
+ ViewHelpers.togglePane(flags, this._detailsPane);
+
+ if (flags.visible) {
+ this._body.classList.remove("pane-collapsed");
+ gStore.dispatch(Actions.showSidebar(true));
+ } else {
+ this._body.classList.add("pane-collapsed");
+ gStore.dispatch(Actions.showSidebar(false));
+ }
+
+ if (tabIndex !== undefined) {
+ $("#event-details-pane").selectedIndex = tabIndex;
+ }
+ },
+
+ /**
+ * Gets the current mode for this tool.
+ * @return string (e.g, "network-inspector-view" or "network-statistics-view")
+ */
+ get currentFrontendMode() {
+ // The getter may be called from a timeout after the panel is destroyed.
+ if (!this._body.selectedPanel) {
+ return null;
+ }
+ return this._body.selectedPanel.id;
+ },
+
+ /**
+ * Toggles between the frontend view modes ("Inspector" vs. "Statistics").
+ */
+ toggleFrontendMode: function () {
+ if (this.currentFrontendMode != "network-inspector-view") {
+ this.showNetworkInspectorView();
+ } else {
+ this.showNetworkStatisticsView();
+ }
+ },
+
+ /**
+ * Switches to the "Inspector" frontend view mode.
+ */
+ showNetworkInspectorView: function () {
+ this._body.selectedPanel = $("#network-inspector-view");
+ this.RequestsMenu._flushWaterfallViews(true);
+ },
+
+ /**
+ * Switches to the "Statistics" frontend view mode.
+ */
+ showNetworkStatisticsView: function () {
+ this._body.selectedPanel = $("#network-statistics-view");
+
+ let controller = NetMonitorController;
+ let requestsView = this.RequestsMenu;
+ let statisticsView = this.PerformanceStatistics;
+
+ Task.spawn(function* () {
+ statisticsView.displayPlaceholderCharts();
+ yield controller.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+
+ try {
+ // • The response headers and status code are required for determining
+ // whether a response is "fresh" (cacheable).
+ // • The response content size and request total time are necessary for
+ // populating the statistics view.
+ // • The response mime type is used for categorization.
+ yield whenDataAvailable(requestsView, [
+ "responseHeaders", "status", "contentSize", "mimeType", "totalTime"
+ ]);
+ } catch (ex) {
+ // Timed out while waiting for data. Continue with what we have.
+ console.error(ex);
+ }
+
+ statisticsView.createPrimedCacheChart(requestsView.items);
+ statisticsView.createEmptyCacheChart(requestsView.items);
+ });
+ },
+
+ reloadPage: function () {
+ NetMonitorController.triggerActivity(
+ ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT);
+ },
+
+ /**
+ * Lazily initializes and returns a promise for a Editor instance.
+ *
+ * @param string id
+ * The id of the editor placeholder node.
+ * @return object
+ * A promise that is resolved when the editor is available.
+ */
+ editor: function (id) {
+ dumpn("Getting a NetMonitorView editor: " + id);
+
+ if (this._editorPromises.has(id)) {
+ return this._editorPromises.get(id);
+ }
+
+ let deferred = promise.defer();
+ this._editorPromises.set(id, deferred.promise);
+
+ // Initialize the source editor and store the newly created instance
+ // in the ether of a resolved promise's value.
+ let editor = new Editor(DEFAULT_EDITOR_CONFIG);
+ editor.appendTo($(id)).then(() => deferred.resolve(editor));
+
+ return deferred.promise;
+ },
+
+ _body: null,
+ _detailsPane: null,
+ _editorPromises: new Map()
+};
+
+/**
+ * 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 visibleFlag
+ * Specifies the intended visibility.
+ */
+ toggle: function (visibleFlag) {
+ NetMonitorView.toggleDetailsPane({ visible: visibleFlag });
+ NetMonitorView.RequestsMenu._flushWaterfallViews(true);
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ * @return object
+ * Returns a promise that resolves upon population of the subview.
+ */
+ populate: Task.async(function* (data) {
+ let isCustom = data.isCustom;
+ let view = isCustom ?
+ NetMonitorView.CustomRequest :
+ NetMonitorView.NetworkDetails;
+
+ yield view.populate(data);
+ $("#details-pane").selectedIndex = isCustom ? 0 : 1;
+
+ window.emit(EVENTS.SIDEBAR_POPULATED);
+ })
+};
+
+/**
+ * Functions handling the requests details view.
+ */
+function NetworkDetailsView() {
+ dumpn("NetworkDetailsView was instantiated");
+
+ // The ToolSidebar requires the panel object to be able to emit events.
+ EventEmitter.decorate(this);
+
+ this._onTabSelect = this._onTabSelect.bind(this);
+}
+
+NetworkDetailsView.prototype = {
+ /**
+ * An object containing the state of tabs.
+ */
+ _viewState: {
+ // if updating[tab] is true a task is currently updating the given tab.
+ updating: [],
+ // if dirty[tab] is true, the tab needs to be repopulated once current
+ // update task finishes
+ dirty: [],
+ // the most recently received attachment data for the request
+ latestData: null,
+ },
+
+ /**
+ * Initialization function, called when the network monitor is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the NetworkDetailsView");
+
+ this.widget = $("#event-details-pane");
+ this.sidebar = new ToolSidebar(this.widget, this, "netmonitor", {
+ disableTelemetry: true,
+ showAllTabsMenu: true
+ });
+
+ 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, {
+ onlyEnumVisible: true,
+ searchPlaceholder: L10N.getStr("jsonFilterText")
+ }));
+ VariablesViewController.attach(this._json);
+
+ this._paramsQueryString = L10N.getStr("paramsQueryString");
+ this._paramsFormData = L10N.getStr("paramsFormData");
+ this._paramsPostPayload = L10N.getStr("paramsPostPayload");
+ this._requestHeaders = L10N.getStr("requestHeaders");
+ this._requestHeadersFromUpload = L10N.getStr("requestHeadersFromUpload");
+ 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");
+ this.sidebar.destroy();
+ $("tabpanels", this.widget).removeEventListener("select",
+ this._onTabSelect);
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ * @return object
+ * Returns a promise that resolves upon population the view.
+ */
+ populate: function (data) {
+ $("#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;
+ $("#raw-headers").hidden = true;
+ $("#response-content-image-box").hidden = true;
+
+ let isHtml = Filters.html(data);
+
+ // Show the "Preview" tabpanel only for plain HTML responses.
+ this.sidebar.toggleTab(isHtml, "preview-tab");
+
+ // Show the "Security" tab only for requests that
+ // 1) are https (state != insecure)
+ // 2) come from a target that provides security information.
+ let hasSecurityInfo = data.securityState &&
+ data.securityState !== "insecure";
+ this.sidebar.toggleTab(hasSecurityInfo, "security-tab");
+
+ // Switch to the "Headers" tabpanel if the "Preview" previously selected
+ // and this is not an HTML response or "Security" was selected but this
+ // request has no security information.
+
+ if (!isHtml && this.widget.selectedPanel === $("#preview-tabpanel") ||
+ !hasSecurityInfo && this.widget.selectedPanel ===
+ $("#security-tabpanel")) {
+ this.widget.selectedIndex = 0;
+ }
+
+ this._headers.empty();
+ this._cookies.empty();
+ this._params.empty();
+ this._json.empty();
+
+ this._dataSrc = { src: data, populated: [] };
+ this._onTabSelect();
+ window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+
+ return promise.resolve();
+ },
+
+ /**
+ * Listener handling the tab selection event.
+ */
+ _onTabSelect: function () {
+ let { src, populated } = this._dataSrc || {};
+ let tab = this.widget.selectedIndex;
+ let view = this;
+
+ // Make sure the data source is valid and don't populate the same tab twice.
+ if (!src || populated[tab]) {
+ return;
+ }
+
+ let viewState = this._viewState;
+ if (viewState.updating[tab]) {
+ // A task is currently updating this tab. If we started another update
+ // task now it would result in a duplicated content as described in bugs
+ // 997065 and 984687. As there's no way to stop the current task mark the
+ // tab dirty and refresh the panel once the current task finishes.
+ viewState.dirty[tab] = true;
+ viewState.latestData = src;
+ return;
+ }
+
+ Task.spawn(function* () {
+ viewState.updating[tab] = true;
+ switch (tab) {
+ // "Headers"
+ case 0:
+ yield view._setSummary(src);
+ yield view._setResponseHeaders(src.responseHeaders);
+ yield view._setRequestHeaders(
+ src.requestHeaders,
+ src.requestHeadersFromUploadStream);
+ break;
+ // "Cookies"
+ case 1:
+ yield view._setResponseCookies(src.responseCookies);
+ yield view._setRequestCookies(src.requestCookies);
+ break;
+ // "Params"
+ case 2:
+ yield view._setRequestGetParams(src.url);
+ yield view._setRequestPostParams(
+ src.requestHeaders,
+ src.requestHeadersFromUploadStream,
+ src.requestPostData);
+ break;
+ // "Response"
+ case 3:
+ yield view._setResponseBody(src.url, src.responseContent);
+ break;
+ // "Timings"
+ case 4:
+ yield view._setTimingsInformation(src.eventTimings);
+ break;
+ // "Security"
+ case 5:
+ yield view._setSecurityInfo(src.securityInfo, src.url);
+ break;
+ // "Preview"
+ case 6:
+ yield view._setHtmlPreview(src.responseContent);
+ break;
+ }
+ viewState.updating[tab] = false;
+ }).then(() => {
+ if (tab == this.widget.selectedIndex) {
+ if (viewState.dirty[tab]) {
+ // The request information was updated while the task was running.
+ viewState.dirty[tab] = false;
+ view.populate(viewState.latestData);
+ } else {
+ // Tab is selected but not dirty. We're done here.
+ populated[tab] = true;
+ window.emit(EVENTS.TAB_UPDATED);
+
+ if (NetMonitorController.isConnected()) {
+ NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
+ }
+ }
+ } else if (viewState.dirty[tab]) {
+ // Tab is dirty but no longer selected. Don't refresh it now, it'll be
+ // done if the tab is shown again.
+ viewState.dirty[tab] = false;
+ }
+ }, e => console.error(e));
+ },
+
+ /**
+ * Sets the network request summary shown in this view.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ */
+ _setSummary: function (data) {
+ if (data.url) {
+ let unicodeUrl = NetworkHelper.convertToUnicode(unescape(data.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 (data.method) {
+ $("#headers-summary-method-value").setAttribute("value", data.method);
+ $("#headers-summary-method").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-method").setAttribute("hidden", "true");
+ }
+
+ if (data.remoteAddress) {
+ let address = data.remoteAddress;
+ if (address.indexOf(":") != -1) {
+ address = `[${address}]`;
+ }
+ if (data.remotePort) {
+ address += `:${data.remotePort}`;
+ }
+ $("#headers-summary-address-value").setAttribute("value", address);
+ $("#headers-summary-address-value").setAttribute("tooltiptext", address);
+ $("#headers-summary-address").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-address").setAttribute("hidden", "true");
+ }
+
+ if (data.status) {
+ // "code" attribute is only used by css to determine the icon color
+ let code;
+ if (data.fromCache) {
+ code = "cached";
+ } else if (data.fromServiceWorker) {
+ code = "service worker";
+ } else {
+ code = data.status;
+ }
+ $("#headers-summary-status-circle").setAttribute("code", code);
+ $("#headers-summary-status-value").setAttribute("value",
+ data.status + " " + data.statusText);
+ $("#headers-summary-status").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-status").setAttribute("hidden", "true");
+ }
+
+ if (data.httpVersion) {
+ $("#headers-summary-version-value").setAttribute("value",
+ data.httpVersion);
+ $("#headers-summary-version").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-version").setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Sets the network request headers shown in this view.
+ *
+ * @param object headers
+ * The "requestHeaders" message received from the server.
+ * @param object uploadHeaders
+ * The "requestHeadersFromUploadStream" inferred from the POST payload.
+ * @return object
+ * A promise that resolves when request headers are set.
+ */
+ _setRequestHeaders: Task.async(function* (headers, uploadHeaders) {
+ if (headers && headers.headers.length) {
+ yield this._addHeaders(this._requestHeaders, headers);
+ }
+ if (uploadHeaders && uploadHeaders.headers.length) {
+ yield this._addHeaders(this._requestHeadersFromUpload, uploadHeaders);
+ }
+ }),
+
+ /**
+ * Sets the network response headers shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that resolves when response headers are set.
+ */
+ _setResponseHeaders: Task.async(function* (response) {
+ if (response && response.headers.length) {
+ response.headers.sort((a, b) => a.name > b.name);
+ yield this._addHeaders(this._responseHeaders, response);
+ }
+ }),
+
+ /**
+ * Populates the headers container in this view with the specified data.
+ *
+ * @param string name
+ * The type of headers to populate (request or response).
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that resolves when headers are added.
+ */
+ _addHeaders: Task.async(function* (name, response) {
+ let kb = response.headersSize / 1024;
+ let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
+ let text = L10N.getFormatStr("networkMenu.sizeKB", size);
+
+ let headersScope = this._headers.addScope(name + " (" + text + ")");
+ headersScope.expanded = true;
+
+ for (let header of response.headers) {
+ let headerVar = headersScope.addItem(header.name, {}, {relaxed: true});
+ let headerValue = yield gNetwork.getString(header.value);
+ headerVar.setGrip(headerValue);
+ }
+ }),
+
+ /**
+ * Sets the network request cookies shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the request cookies are set.
+ */
+ _setRequestCookies: Task.async(function* (response) {
+ if (response && response.cookies.length) {
+ response.cookies.sort((a, b) => a.name > b.name);
+ yield this._addCookies(this._requestCookies, response);
+ }
+ }),
+
+ /**
+ * Sets the network response cookies shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the response cookies are set.
+ */
+ _setResponseCookies: Task.async(function* (response) {
+ if (response && response.cookies.length) {
+ yield this._addCookies(this._responseCookies, response);
+ }
+ }),
+
+ /**
+ * Populates the cookies container in this view with the specified data.
+ *
+ * @param string name
+ * The type of cookies to populate (request or response).
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * Returns a promise that resolves upon the adding of cookies.
+ */
+ _addCookies: Task.async(function* (name, response) {
+ let cookiesScope = this._cookies.addScope(name);
+ cookiesScope.expanded = true;
+
+ for (let cookie of response.cookies) {
+ let cookieVar = cookiesScope.addItem(cookie.name, {}, {relaxed: true});
+ let cookieValue = yield gNetwork.getString(cookie.value);
+ cookieVar.setGrip(cookieValue);
+
+ // 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 url
+ * The request's url.
+ */
+ _setRequestGetParams: function (url) {
+ let query = NetworkHelper.nsIURL(url).query;
+ if (query) {
+ this._addParams(this._paramsQueryString, query);
+ }
+ },
+
+ /**
+ * Sets the network request post params shown in this view.
+ *
+ * @param object headers
+ * The "requestHeaders" message received from the server.
+ * @param object uploadHeaders
+ * The "requestHeadersFromUploadStream" inferred from the POST payload.
+ * @param object postData
+ * The "requestPostData" message received from the server.
+ * @return object
+ * A promise that is resolved when the request post params are set.
+ */
+ _setRequestPostParams: Task.async(function* (headers, uploadHeaders,
+ postData) {
+ if (!headers || !uploadHeaders || !postData) {
+ return;
+ }
+
+ let formDataSections = yield getFormDataSections(
+ headers,
+ uploadHeaders,
+ postData,
+ gNetwork.getString.bind(gNetwork));
+
+ this._params.onlyEnumVisible = false;
+
+ // Handle urlencoded form data sections (e.g. "?foo=bar&baz=42").
+ if (formDataSections.length > 0) {
+ formDataSections.forEach(section => {
+ this._addParams(this._paramsFormData, section);
+ });
+ } else {
+ // Handle JSON and actual forms ("multipart/form-data" content type).
+ let postDataLongString = postData.postData.text;
+ let text = yield gNetwork.getString(postDataLongString);
+ let jsonVal = null;
+ try {
+ jsonVal = JSON.parse(text);
+ } catch (ex) { // eslint-disable-line
+ }
+
+ if (jsonVal) {
+ this._params.onlyEnumVisible = true;
+ let jsonScopeName = L10N.getStr("jsonScopeName");
+ let jsonScope = this._params.addScope(jsonScopeName);
+ jsonScope.expanded = true;
+ let jsonItem = jsonScope.addItem(undefined, { enumerable: true });
+ jsonItem.populate(jsonVal, { sorted: true });
+ } 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;
+ let editor = yield NetMonitorView.editor("#request-post-data-textarea");
+ editor.setMode(Editor.modes.text);
+ editor.setText(text);
+ }
+ }
+
+ window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ }),
+
+ /**
+ * Populates the params container in this view with the specified data.
+ *
+ * @param string name
+ * The type of params to populate (get or post).
+ * @param string queryString
+ * A query string of params (e.g. "?foo=bar&baz=42").
+ */
+ _addParams: function (name, queryString) {
+ let paramsArray = NetworkHelper.parseQueryString(queryString);
+ if (!paramsArray) {
+ return;
+ }
+ let paramsScope = this._params.addScope(name);
+ paramsScope.expanded = true;
+
+ for (let param of paramsArray) {
+ let paramVar = paramsScope.addItem(param.name, {}, {relaxed: true});
+ paramVar.setGrip(param.value);
+ }
+ },
+
+ /**
+ * Sets the network response body shown in this view.
+ *
+ * @param string url
+ * The request's url.
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the response body is set.
+ */
+ _setResponseBody: Task.async(function* (url, response) {
+ if (!response) {
+ return;
+ }
+ let { mimeType, text, encoding } = response.content;
+ let responseBody = yield gNetwork.getString(text);
+
+ // 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".
+ // Additionally, we also directly parse the response text content to
+ // verify whether it's json or not, to handle responses incorrectly
+ // labeled as text/plain instead.
+ let jsonMimeType, jsonObject, jsonObjectParseError;
+ try {
+ jsonMimeType = /\bjson/.test(mimeType);
+ jsonObject = JSON.parse(responseBody);
+ } catch (e) {
+ jsonObjectParseError = e;
+ }
+ if (jsonMimeType || jsonObject) {
+ // Extract the actual json substring in case this might be a "JSONP".
+ // This regex basically parses a function call and captures the
+ // function name and arguments in two separate groups.
+ let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
+ let [_, callbackPadding, jsonpString] = // eslint-disable-line
+ responseBody.match(jsonpRegex) || [];
+
+ // Make sure this is a valid JSON object first. If so, nicely display
+ // the parsing results in a variables view. Otherwise, simply show
+ // the contents as plain text.
+ if (callbackPadding && jsonpString) {
+ try {
+ jsonObject = JSON.parse(jsonpString);
+ } catch (e) {
+ jsonObjectParseError = e;
+ }
+ }
+
+ // Valid JSON or JSONP.
+ if (jsonObject) {
+ $("#response-content-json-box").hidden = false;
+ let jsonScopeName = callbackPadding
+ ? L10N.getFormatStr("jsonpScopeName", callbackPadding)
+ : L10N.getStr("jsonScopeName");
+
+ let jsonVar = { label: jsonScopeName, rawObject: jsonObject };
+ yield this._json.controller.setSingleVariable(jsonVar).expanded;
+ } else {
+ // Malformed JSON.
+ $("#response-content-textarea-box").hidden = false;
+ let infoHeader = $("#response-content-info-header");
+ infoHeader.setAttribute("value", jsonObjectParseError);
+ infoHeader.setAttribute("tooltiptext", jsonObjectParseError);
+ infoHeader.hidden = false;
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ editor.setMode(Editor.modes.js);
+ editor.setText(responseBody);
+ }
+ } else if (mimeType.includes("image/")) {
+ // Handle images.
+ $("#response-content-image-box").setAttribute("align", "center");
+ $("#response-content-image-box").setAttribute("pack", "center");
+ $("#response-content-image-box").hidden = false;
+ $("#response-content-image").src = formDataURI(mimeType, encoding, responseBody);
+
+ // Immediately display additional information about the image:
+ // file name, mime type and encoding.
+ $("#response-content-image-name-value").setAttribute("value",
+ NetworkHelper.nsIURL(url).fileName);
+ $("#response-content-image-mime-value").setAttribute("value", mimeType);
+
+ // 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) + " \u00D7 " + (height - 2);
+ $("#response-content-image-dimensions-value").setAttribute("value",
+ dimensions);
+ };
+ } else {
+ $("#response-content-textarea-box").hidden = false;
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ editor.setMode(Editor.modes.text);
+ editor.setText(responseBody);
+
+ // Maybe set a more appropriate mode in the Source Editor if possible,
+ // but avoid doing this for very large files.
+ if (responseBody.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
+ let mapping = Object.keys(CONTENT_MIME_TYPE_MAPPINGS).find(key => {
+ return mimeType.includes(key);
+ });
+
+ if (mapping) {
+ editor.setMode(CONTENT_MIME_TYPE_MAPPINGS[mapping]);
+ }
+ }
+ }
+
+ window.emit(EVENTS.RESPONSE_BODY_DISPLAYED);
+ }),
+
+ /**
+ * Sets the timings information shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _setTimingsInformation: function (response) {
+ if (!response) {
+ return;
+ }
+ let { blocked, dns, connect, send, wait, receive } = response.timings;
+
+ let tabboxWidth = $("#details-pane").getAttribute("width");
+
+ // Other nodes also take some space.
+ let availableWidth = tabboxWidth / 2;
+ let scale = (response.totalTime > 0 ?
+ Math.max(availableWidth / response.totalTime, 0) :
+ 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)";
+ },
+
+ /**
+ * Sets the preview for HTML responses shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the html preview is rendered.
+ */
+ _setHtmlPreview: Task.async(function* (response) {
+ if (!response) {
+ return promise.resolve();
+ }
+ let { text } = response.content;
+ let responseBody = yield gNetwork.getString(text);
+
+ // Always disable JS when previewing HTML responses.
+ let iframe = $("#response-preview");
+ iframe.contentDocument.docShell.allowJavascript = false;
+ iframe.contentDocument.documentElement.innerHTML = responseBody;
+
+ window.emit(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED);
+ return undefined;
+ }),
+
+ /**
+ * Sets the security information shown in this view.
+ *
+ * @param object securityInfo
+ * The data received from server
+ * @param string url
+ * The URL of this request
+ * @return object
+ * A promise that is resolved when the security info is rendered.
+ */
+ _setSecurityInfo: Task.async(function* (securityInfo, url) {
+ if (!securityInfo) {
+ // We don't have security info. This could mean one of two things:
+ // 1) This connection is not secure and this tab is not visible and thus
+ // we shouldn't be here.
+ // 2) We have already received securityState and the tab is visible BUT
+ // the rest of the information is still on its way. Once it arrives
+ // this method is called again.
+ return;
+ }
+
+ /**
+ * A helper that sets value and tooltiptext attributes of an element to
+ * specified value.
+ *
+ * @param string selector
+ * A selector for the element.
+ * @param string value
+ * The value to set. If this evaluates to false a placeholder string
+ * <Not Available> is used instead.
+ */
+ function setValue(selector, value) {
+ let label = $(selector);
+ if (!value) {
+ label.setAttribute("value", L10N.getStr(
+ "netmonitor.security.notAvailable"));
+ label.setAttribute("tooltiptext", label.getAttribute("value"));
+ } else {
+ label.setAttribute("value", value);
+ label.setAttribute("tooltiptext", value);
+ }
+ }
+
+ let errorbox = $("#security-error");
+ let infobox = $("#security-information");
+
+ if (securityInfo.state === "secure" || securityInfo.state === "weak") {
+ infobox.hidden = false;
+ errorbox.hidden = true;
+
+ // Warning icons
+ let cipher = $("#security-warning-cipher");
+
+ if (securityInfo.state === "weak") {
+ cipher.hidden = securityInfo.weaknessReasons.indexOf("cipher") === -1;
+ } else {
+ cipher.hidden = true;
+ }
+
+ let enabledLabel = L10N.getStr("netmonitor.security.enabled");
+ let disabledLabel = L10N.getStr("netmonitor.security.disabled");
+
+ // Connection parameters
+ setValue("#security-protocol-version-value",
+ securityInfo.protocolVersion);
+ setValue("#security-ciphersuite-value", securityInfo.cipherSuite);
+
+ // Host header
+ let domain = getUriHostPort(url);
+ let hostHeader = L10N.getFormatStr("netmonitor.security.hostHeader",
+ domain);
+ setValue("#security-info-host-header", hostHeader);
+
+ // Parameters related to the domain
+ setValue("#security-http-strict-transport-security-value",
+ securityInfo.hsts ? enabledLabel : disabledLabel);
+
+ setValue("#security-public-key-pinning-value",
+ securityInfo.hpkp ? enabledLabel : disabledLabel);
+
+ // Certificate parameters
+ let cert = securityInfo.cert;
+ setValue("#security-cert-subject-cn", cert.subject.commonName);
+ setValue("#security-cert-subject-o", cert.subject.organization);
+ setValue("#security-cert-subject-ou", cert.subject.organizationalUnit);
+
+ setValue("#security-cert-issuer-cn", cert.issuer.commonName);
+ setValue("#security-cert-issuer-o", cert.issuer.organization);
+ setValue("#security-cert-issuer-ou", cert.issuer.organizationalUnit);
+
+ setValue("#security-cert-validity-begins", cert.validity.start);
+ setValue("#security-cert-validity-expires", cert.validity.end);
+
+ setValue("#security-cert-sha1-fingerprint", cert.fingerprint.sha1);
+ setValue("#security-cert-sha256-fingerprint", cert.fingerprint.sha256);
+ } else {
+ infobox.hidden = true;
+ errorbox.hidden = false;
+
+ // Strip any HTML from the message.
+ let plain = new DOMParser().parseFromString(securityInfo.errorMessage,
+ "text/html");
+ setValue("#security-error-message", plain.body.textContent);
+ }
+ }),
+
+ _dataSrc: null,
+ _headers: null,
+ _cookies: null,
+ _params: null,
+ _json: null,
+ _paramsQueryString: "",
+ _paramsFormData: "",
+ _paramsPostPayload: "",
+ _requestHeaders: "",
+ _responseHeaders: "",
+ _requestCookies: "",
+ _responseCookies: ""
+};
+
+/**
+ * DOM query helper.
+ * TODO: Move it into "dom-utils.js" module and "require" it when needed.
+ */
+var $ = (selector, target = document) => target.querySelector(selector);
+var $all = (selector, target = document) => target.querySelectorAll(selector);
+
+/**
+ * Makes sure certain properties are available on all objects in a data store.
+ *
+ * @param array dataStore
+ * The request view object from which to fetch the item list.
+ * @param array mandatoryFields
+ * A list of strings representing properties of objects in dataStore.
+ * @return object
+ * A promise resolved when all objects in dataStore contain the
+ * properties defined in mandatoryFields.
+ */
+function whenDataAvailable(requestsView, mandatoryFields) {
+ let deferred = promise.defer();
+
+ let interval = setInterval(() => {
+ const { attachments } = requestsView;
+ if (attachments.length > 0 && attachments.every(item => {
+ return mandatoryFields.every(field => field in item);
+ })) {
+ clearInterval(interval);
+ clearTimeout(timer);
+ deferred.resolve();
+ }
+ }, WDA_DEFAULT_VERIFY_INTERVAL);
+
+ let timer = setTimeout(() => {
+ clearInterval(interval);
+ deferred.reject(new Error("Timed out while waiting for data"));
+ }, WDA_DEFAULT_GIVE_UP_TIMEOUT);
+
+ return deferred.promise;
+}
+
+/**
+ * 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();
+NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();