1240 lines
43 KiB
JavaScript
1240 lines
43 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* 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;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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.openSidebar(true));
|
|
} else {
|
|
this._body.classList.add("pane-collapsed");
|
|
gStore.dispatch(Actions.openSidebar(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", "transferredSize", "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, ssl, 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-ssl .requests-menu-timings-box")
|
|
.setAttribute("width", ssl * scale);
|
|
$("#timings-summary-ssl .requests-menu-timings-total")
|
|
.setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", ssl));
|
|
|
|
$("#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-ssl .requests-menu-timings-box")
|
|
.style.transform = "translateX(" + (scale * blocked) + "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-ssl .requests-menu-timings-total")
|
|
.style.transform = "translateX(" + (scale * blocked) + "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);
|
|
setValue("#security-keagroup-value", securityInfo.keaGroupName);
|
|
setValue("#security-signaturescheme-value", securityInfo.signatureSchemeName);
|
|
|
|
// 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();
|