diff --git a/browser/devtools/netmonitor/netmonitor-controller.js b/browser/devtools/netmonitor/netmonitor-controller.js index 72e7a8a7056..1da4336b5c5 100644 --- a/browser/devtools/netmonitor/netmonitor-controller.js +++ b/browser/devtools/netmonitor/netmonitor-controller.js @@ -250,6 +250,7 @@ TargetEventsHandler.prototype = { case "will-navigate": { // Reset UI. NetMonitorView.RequestsMenu.reset(); + NetMonitorView.Sidebar.reset(); NetMonitorView.NetworkDetails.reset(); // Reset global helpers cache. diff --git a/browser/devtools/netmonitor/netmonitor-view.js b/browser/devtools/netmonitor/netmonitor-view.js index b8437dc6b57..afdd1e59fc4 100644 --- a/browser/devtools/netmonitor/netmonitor-view.js +++ b/browser/devtools/netmonitor/netmonitor-view.js @@ -165,7 +165,7 @@ let NetMonitorView = { } if (aTabIndex !== undefined) { - $("#details-pane").selectedIndex = aTabIndex; + $("#event-details-pane").selectedIndex = aTabIndex; } }, @@ -351,8 +351,63 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { this.refreshSummary(); this.refreshZebra(); + + if (aId == this._preferredItemId) { + this.selectedItem = requestItem; + } }, + /** + * Create a new custom request form populated with the data from + * the currently selected request. + */ + cloneSelectedRequest: function() { + let selected = this.selectedItem.attachment; + + // Create the element node for the network request item. + let menuView = this._createMenuView(selected.method, selected.url); + + let newItem = this.push([menuView], { + attachment: Object.create(selected, { + isCustom: { value: true } + }) + }); + + // Immediately switch to new request pane. + this.selectedItem = newItem; + }, + + /** + * Send a new HTTP request using the data in the custom request form. + */ + sendCustomRequest: function() { + let selected = this.selectedItem.attachment; + + let data = Object.create(selected, { + headers: { value: selected.requestHeaders.headers } + }); + + if (selected.requestPostData) { + data.body = selected.requestPostData.postData.text; + } + + NetMonitorController.webConsoleClient.sendHTTPRequest(data, (response) => { + let id = response.eventActor.actor; + this._preferredItemId = id; + }); + + this.closeCustomRequest(); + }, + + /** + * Remove the currently selected custom request. + */ + closeCustomRequest: function() { + this.remove(this.selectedItem); + + NetMonitorView.Sidebar.toggle(false); + }, + /** * Filters all network requests in this container by a specified type. * @@ -690,11 +745,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { break; case "status": requestItem.attachment.status = value; - this._updateMenuView(requestItem, key, value); + this.updateMenuView(requestItem, key, value); break; case "statusText": requestItem.attachment.statusText = value; - this._updateMenuView(requestItem, key, + this.updateMenuView(requestItem, key, requestItem.attachment.status + " " + requestItem.attachment.statusText); break; @@ -703,11 +758,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { break; case "contentSize": requestItem.attachment.contentSize = value; - this._updateMenuView(requestItem, key, value); + this.updateMenuView(requestItem, key, value); break; case "mimeType": requestItem.attachment.mimeType = value; - this._updateMenuView(requestItem, key, value); + this.updateMenuView(requestItem, key, value); break; case "responseContent": requestItem.attachment.responseContent = value; @@ -715,7 +770,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { case "totalTime": requestItem.attachment.totalTime = value; requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value; - this._updateMenuView(requestItem, key, value); + this.updateMenuView(requestItem, key, value); this._registerLastRequestEnd(requestItem.attachment.endedMillis); break; case "eventTimings": @@ -757,23 +812,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { * The network request view. */ _createMenuView: function(aMethod, aUrl) { - let uri = nsIURL(aUrl); - let nameWithQuery = this._getUriNameWithQuery(uri); - let hostPort = this._getUriHostPort(uri); - let template = $("#requests-menu-item-template"); let fragment = document.createDocumentFragment(); - let method = $(".requests-menu-method", template); - method.setAttribute("value", aMethod); - - let file = $(".requests-menu-file", template); - file.setAttribute("value", nameWithQuery); - file.setAttribute("tooltiptext", nameWithQuery); - - let domain = $(".requests-menu-domain", template); - domain.setAttribute("value", hostPort); - domain.setAttribute("tooltiptext", hostPort); + this.updateMenuView(template, 'method', aMethod); + this.updateMenuView(template, 'url', aUrl); let waterfall = $(".requests-menu-waterfall", template); waterfall.style.backgroundImage = this._cachedWaterfallBackground; @@ -796,22 +839,48 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { * @param any aValue * The new value to be shown. */ - _updateMenuView: function(aItem, aKey, aValue) { + updateMenuView: function(aItem, aKey, aValue) { + let target = aItem.target || aItem; + switch (aKey) { + case "method": { + let node = $(".requests-menu-method", target); + node.setAttribute("value", aValue); + break; + } + case "url": { + let uri; + try { + uri = nsIURL(aValue); + } catch(e) { + break; // User input may not make a well-formed url yet. + } + let nameWithQuery = this._getUriNameWithQuery(uri); + let hostPort = this._getUriHostPort(uri); + + let node = $(".requests-menu-file", target); + node.setAttribute("value", nameWithQuery); + node.setAttribute("tooltiptext", nameWithQuery); + + let domain = $(".requests-menu-domain", target); + domain.setAttribute("value", hostPort); + domain.setAttribute("tooltiptext", hostPort); + break; + } case "status": { - let node = $(".requests-menu-status", aItem.target); + let node = $(".requests-menu-status", target); node.setAttribute("code", aValue); break; } case "statusText": { - let node = $(".requests-menu-status-and-method", aItem.target); + let node = $(".requests-menu-status-and-method", target); node.setAttribute("tooltiptext", aValue); break; } case "contentSize": { let kb = aValue / 1024; let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS); - let node = $(".requests-menu-size", aItem.target); + let node = $(".requests-menu-size", target); let text = L10N.getFormatStr("networkMenu.sizeKB", size); node.setAttribute("value", text); node.setAttribute("tooltiptext", text); @@ -819,14 +888,14 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { } case "mimeType": { let type = this._getAbbreviatedMimeType(aValue); - let node = $(".requests-menu-type", aItem.target); + let node = $(".requests-menu-type", target); let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type; node.setAttribute("value", text); node.setAttribute("tooltiptext", aValue); break; } case "totalTime": { - let node = $(".requests-menu-timings-total", aItem.target); + let node = $(".requests-menu-timings-total", target); let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer node.setAttribute("value", text); node.setAttribute("tooltiptext", text); @@ -1089,10 +1158,10 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { */ _onSelect: function({ detail: item }) { if (item) { - NetMonitorView.NetworkDetails.populate(item.attachment); - NetMonitorView.NetworkDetails.toggle(true); + NetMonitorView.Sidebar.populate(item.attachment); + NetMonitorView.Sidebar.toggle(true); } else { - NetMonitorView.NetworkDetails.toggle(false); + NetMonitorView.Sidebar.toggle(false); } }, @@ -1104,6 +1173,14 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { drain("resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true)); }, + /** + * Handle the context menu opening. Hide items if no request is selected. + */ + _onContextShowing: function() { + let element = $("#request-menu-context-resend"); + element.hidden = !this.selectedItem || this.selectedItem.attachment.isCustom; + }, + /** * Checks if the specified unix time is the first one to be known of, * and saves it if so. @@ -1239,6 +1316,162 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { _resizeTimeout: null }); +/** + * Functions handling the sidebar details view. + */ +function SidebarView() { + dumpn("SidebarView was instantiated"); +} + +SidebarView.prototype = { + /** + * Sets this view hidden or visible. It's visible by default. + * + * @param boolean aVisibleFlag + * Specifies the intended visibility. + */ + toggle: function(aVisibleFlag) { + NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag }); + NetMonitorView.RequestsMenu._flushWaterfallViews(true); + }, + + /** + * Populates this view with the specified data. + * + * @param object aData + * The data source (this should be the attachment of a request item). + */ + populate: function(aData) { + if (aData.isCustom) { + NetMonitorView.CustomRequest.populate(aData); + $("#details-pane").selectedIndex = 0; + } else { + NetMonitorView.NetworkDetails.populate(aData); + $("#details-pane").selectedIndex = 1; + } + }, + + /** + * Hides this container. + */ + reset: function() { + this.toggle(false); + } +} + +/** + * Functions handling the custom request view. + */ +function CustomRequestView() { + dumpn("CustomRequestView was instantiated"); +} + +CustomRequestView.prototype = { + /** + * Populates this view with the specified data. + * + * @param object aData + * The data source (this should be the attachment of a request item). + */ + populate: function(aData) { + $("#custom-url-value").value = aData.url; + $("#custom-method-value").value = aData.method; + $("#custom-headers-value").value = + writeHeaderText(aData.requestHeaders.headers); + + if (aData.requestPostData) { + let body = aData.requestPostData.postData.text; + + gNetwork.getString(body).then((aString) => { + $("#custom-postdata-value").value = aString; + }); + } + + this.updateCustomQuery(aData.url); + }, + + /** + * Handle user input in the custom request form. + * + * @param object aField + * the field that the user updated. + */ + onUpdate: function(aField) { + let selectedItem = NetMonitorView.RequestsMenu.selectedItem; + let field = aField; + let value; + + switch(aField) { + case 'method': + value = $("#custom-method-value").value.trim(); + selectedItem.attachment.method = value; + break; + case 'url': + value = $("#custom-url-value").value; + this.updateCustomQuery(value); + selectedItem.attachment.url = value; + break; + case 'query': + let query = $("#custom-query-value").value; + this.updateCustomUrl(query); + field = 'url'; + value = $("#custom-url-value").value + selectedItem.attachment.url = value; + break; + case 'body': + value = $("#custom-postdata-value").value; + selectedItem.attachment.requestPostData = { + postData: { + text: value + } + }; + break; + case 'headers': + let headersText = $("#custom-headers-value").value; + value = parseHeaderText(headersText); + selectedItem.attachment.requestHeaders = { + headers: value + }; + break; + } + + NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value); + }, + + /** + * Update the query string field based on the url. + * + * @param object aUrl + * url to extract query string from. + */ + updateCustomQuery: function(aUrl) { + let paramsArray = parseQueryString(nsIURL(aUrl).query); + if (!paramsArray) { + $("#custom-query").hidden = true; + return; + } + $("#custom-query").hidden = false; + $("#custom-query-value").value = writeQueryText(paramsArray); + }, + + /** + * Update the url based on the query string field. + * + * @param object aQueryText + * contents of the query string field. + */ + updateCustomUrl: function(aQueryText) { + let params = parseQueryText(aQueryText); + let queryString = writeQueryString(params); + + let url = $("#custom-url-value").value; + let oldQuery = nsIURL(url).query; + let path = url.replace(oldQuery, queryString); + + $("#custom-url-value").value = path; + } +} + /** * Functions handling the requests details view. */ @@ -1253,9 +1486,9 @@ NetworkDetailsView.prototype = { * Initialization function, called when the network monitor is started. */ initialize: function() { - dumpn("Initializing the RequestsMenuView"); + dumpn("Initializing the NetworkDetailsView"); - this.widget = $("#details-pane"); + this.widget = $("#event-details-pane"); this._headers = new VariablesView($("#all-headers"), Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, { @@ -1292,25 +1525,13 @@ NetworkDetailsView.prototype = { * Destruction function, called when the network monitor is closed. */ destroy: function() { - dumpn("Destroying the SourcesView"); + dumpn("Destroying the NetworkDetailsView"); }, /** - * Sets this view hidden or visible. It's visible by default. - * - * @param boolean aVisibleFlag - * Specifies the intended visibility. - */ - toggle: function(aVisibleFlag) { - NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag }); - NetMonitorView.RequestsMenu._flushWaterfallViews(true); - }, - - /** - * Hides and resets this container (removes all the networking information). + * Resets this container (removes all the networking information). */ reset: function() { - this.toggle(false); this._dataSrc = null; }, @@ -1581,21 +1802,14 @@ NetworkDetailsView.prototype = { * * @param string aName * The type of params to populate (get or post). - * @param string aParams + * @param string aQueryString * A query string of params (e.g. "?foo=bar&baz=42"). */ - _addParams: function(aName, aParams) { - // Make sure there's at least one param available. - if (!aParams || !aParams.contains("=")) { + _addParams: function(aName, aQueryString) { + let paramsArray = parseQueryString(aQueryString); + if (!paramsArray) { return; } - // Turn the params string into an array containing { name: value } tuples. - let paramsArray = aParams.replace(/^[?&]/, "").split("&").map((e) => - let (param = e.split("=")) { - name: NetworkHelper.convertToUnicode(unescape(param[0])), - value: NetworkHelper.convertToUnicode(unescape(param[1])) - }); - let paramsScope = this._params.addScope(aName); paramsScope.expanded = true; @@ -1807,6 +2021,110 @@ function nsIURL(aUrl, aStore = nsIURL.store) { } nsIURL.store = new Map(); +/** + * Parse a url's query string into its components + * + * @param string aQueryString + * The query part of a url + * @return array + * Array of query params {name, value} + */ +function parseQueryString(aQueryString) { + // Make sure there's at least one param available. + if (!aQueryString || !aQueryString.contains("=")) { + return; + } + // Turn the params string into an array containing { name: value } tuples. + let paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map((e) => + let (param = e.split("=")) { + name: NetworkHelper.convertToUnicode(unescape(param[0])), + value: NetworkHelper.convertToUnicode(unescape(param[1])) + }); + return paramsArray; +} + +/** + * Parse text representation of HTTP headers. + * + * @param string aText + * Text of headers + * @return array + * Array of headers info {name, value} + */ +function parseHeaderText(aText) { + return parseRequestText(aText, ":"); +} + +/** + * Parse readable text list of a query string. + * + * @param string aText + * Text of query string represetation + * @return array + * Array of query params {name, value} + */ +function parseQueryText(aText) { + return parseRequestText(aText, "="); +} + +/** + * Parse a text representation of a name:value list with + * the given name:value divider character. + * + * @param string aText + * Text of list + * @return array + * Array of headers info {name, value} + */ +function parseRequestText(aText, aDivider) { + let regex = new RegExp("(.+?)\\" + aDivider + "\\s*(.+)"); + let pairs = []; + for (let line of aText.split("\n")) { + let matches; + if (matches = regex.exec(line)) { + let [, name, value] = matches; + pairs.push({name: name, value: value}); + } + } + return pairs; +} + +/** + * Write out a list of headers into a chunk of text + * + * @param array aHeaders + * Array of headers info {name, value} + * @return string aText + * List of headers in text format + */ +function writeHeaderText(aHeaders) { + return [(name + ": " + value) for ({name, value} of aHeaders)].join("\n"); +} + +/** + * Write out a list of query params into a chunk of text + * + * @param array aParams + * Array of query params {name, value} + * @return string + * List of query params in text format + */ +function writeQueryText(aParams) { + return [(name + "=" + value) for ({name, value} of aParams)].join("\n"); +} + +/** + * Write out a list of query params into a query string + * + * @param array aParams + * Array of query params {name, value} + * @return string + * Query string that can be appended to a url. + */ +function writeQueryString(aParams) { + return [(name + "=" + value) for ({name, value} of aParams)].join("&"); +} + /** * Helper for draining a rapid succession of events and invoking a callback * once everything settles down. @@ -1822,4 +2140,6 @@ drain.store = new Map(); */ NetMonitorView.Toolbar = new ToolbarView(); NetMonitorView.RequestsMenu = new RequestsMenuView(); +NetMonitorView.Sidebar = new SidebarView(); +NetMonitorView.CustomRequest = new CustomRequestView(); NetMonitorView.NetworkDetails = new NetworkDetailsView(); diff --git a/browser/devtools/netmonitor/netmonitor.css b/browser/devtools/netmonitor/netmonitor.css index cae41f8aa2e..ce087d7dca1 100644 --- a/browser/devtools/netmonitor/netmonitor.css +++ b/browser/devtools/netmonitor/netmonitor.css @@ -15,6 +15,10 @@ overflow: auto; } +#custom-pane { + overflow: auto; +} + #timings-summary-blocked { display: none; /* This doesn't work yet. */ } diff --git a/browser/devtools/netmonitor/netmonitor.xul b/browser/devtools/netmonitor/netmonitor.xul index 06f4f1e09ec..48165c5376f 100644 --- a/browser/devtools/netmonitor/netmonitor.xul +++ b/browser/devtools/netmonitor/netmonitor.xul @@ -17,6 +17,16 @@