/* -*- 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; } }, /** * 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 * 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();