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 @@
+
+
@@ -103,7 +113,7 @@
-
diff --git a/browser/devtools/netmonitor/test/Makefile.in b/browser/devtools/netmonitor/test/Makefile.in
index 0fcd2e895ad..6d238a9bda9 100644
--- a/browser/devtools/netmonitor/test/Makefile.in
+++ b/browser/devtools/netmonitor/test/Makefile.in
@@ -42,6 +42,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_net_accessibility-01.js \
browser_net_accessibility-02.js \
browser_net_footer-summary.js \
+ browser_net_resend.js \
head.js \
$(NULL)
diff --git a/browser/devtools/netmonitor/test/browser_net_post-data-02.js b/browser/devtools/netmonitor/test/browser_net_post-data-02.js
index 305f31b7185..c60550af560 100644
--- a/browser/devtools/netmonitor/test/browser_net_post-data-02.js
+++ b/browser/devtools/netmonitor/test/browser_net_post-data-02.js
@@ -20,8 +20,8 @@ function test() {
NetMonitorView.toggleDetailsPane({ visible: true }, 2)
RequestsMenu.selectedIndex = 0;
- let tab = document.querySelectorAll("#details-pane tab")[2];
- let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+ let tab = document.querySelectorAll("#event-details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#event-details-pane tabpanel")[2];
is(tab.getAttribute("selected"), "true",
"The params tab in the network details pane should be selected.");
diff --git a/browser/devtools/netmonitor/test/browser_net_resend.js b/browser/devtools/netmonitor/test/browser_net_resend.js
new file mode 100644
index 00000000000..2d9b52acd9b
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_resend.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gPanelWin;
+let gPanelDoc;
+
+const ADD_QUERY = "t1=t2";
+const ADD_HEADER = "Test-header: true";
+const ADD_POSTDATA = "t3=t4";
+
+/**
+ * Tests if resending a request works.
+ */
+
+function test() {
+ initNetMonitor(POST_DATA_URL).then(([aTab, aDebuggee, aMonitor]) => {
+ info("Starting test... ");
+
+ gPanelWin = aMonitor.panelWin;
+ gPanelDoc = gPanelWin.document;
+
+ let { NetMonitorView } = gPanelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(aMonitor, 0, 2).then(() => {
+ let origItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = origItem;
+
+ // add a new custom request cloned from selected request
+ RequestsMenu.cloneSelectedRequest();
+ testCustomForm(origItem.attachment);
+
+ let customItem = RequestsMenu.selectedItem;
+ testCustomItem(customItem, origItem);
+
+ // edit the custom request
+ editCustomForm(() => {
+ testCustomItemChanged(customItem, origItem);
+
+ waitForNetworkEvents(aMonitor, 0, 1).then(() => {
+ let sentItem = RequestsMenu.selectedItem;
+ testSentRequest(sentItem.attachment, origItem.attachment);
+ finishUp(aMonitor);
+ });
+ // send the new request
+ RequestsMenu.sendCustomRequest();
+ });
+ });
+
+ aDebuggee.performRequests();
+ });
+}
+
+function testCustomItem(aItem, aOrigItem) {
+ let method = aItem.target.querySelector(".requests-menu-method").value;
+ let origMethod = aOrigItem.target.querySelector(".requests-menu-method").value;
+ is(method, origMethod, "menu item is showing the same method as original request");
+
+ let file = aItem.target.querySelector(".requests-menu-file").value;
+ let origFile = aOrigItem.target.querySelector(".requests-menu-file").value;
+ is(file, origFile, "menu item is showing the same file name as original request");
+
+ let domain = aItem.target.querySelector(".requests-menu-domain").value;
+ let origDomain = aOrigItem.target.querySelector(".requests-menu-domain").value;
+ is(domain, origDomain, "menu item is showing the same domain as original request");
+}
+
+function testCustomItemChanged(aItem, aOrigItem) {
+ let file = aItem.target.querySelector(".requests-menu-file").value;
+ let expectedFile = aOrigItem.target.querySelector(".requests-menu-file").value + "&" + ADD_QUERY;
+
+ is(file, expectedFile, "menu item is updated to reflect url entered in form");
+}
+
+/*
+ * Test that the New Request form was populated correctly
+ */
+function testCustomForm(aData) {
+ is(gPanelDoc.getElementById("custom-method-value").value, aData.method,
+ "new request form showing correct method");
+
+ is(gPanelDoc.getElementById("custom-url-value").value, aData.url,
+ "new request form showing correct url");
+
+ let query = gPanelDoc.getElementById("custom-query-value");
+ is(query.value, "foo=bar\nbaz=42\ntype=urlencoded",
+ "new request form showing correct query string");
+
+ let headers = gPanelDoc.getElementById("custom-headers-value").value.split("\n");
+ for (let {name, value} of aData.requestHeaders.headers) {
+ ok(headers.indexOf(name + ": " + value) >= 0, "form contains header from request");
+ }
+
+ let postData = gPanelDoc.getElementById("custom-postdata-value");
+ is(postData.value, aData.requestPostData.postData.text,
+ "new request form showing correct post data");
+}
+
+/*
+ * Add some params and headers to the request form
+ */
+function editCustomForm(callback) {
+ gPanelWin.focus();
+
+ let query = gPanelDoc.getElementById("custom-query-value");
+ query.addEventListener("focus", function onFocus() {
+ query.removeEventListener("focus", onFocus, false);
+
+ // add params to url query string field
+ type(["VK_RETURN"]);
+ type(ADD_QUERY);
+
+ let headers = gPanelDoc.getElementById("custom-headers-value");
+ headers.addEventListener("focus", function onFocus() {
+ headers.removeEventListener("focus", onFocus, false);
+
+ // add a header
+ type(["VK_RETURN"]);
+ type(ADD_HEADER);
+
+ let postData = gPanelDoc.getElementById("custom-postdata-value");
+ postData.addEventListener("focus", function onFocus() {
+ postData.removeEventListener("focus", onFocus, false);
+
+ // add to POST data
+ type(ADD_POSTDATA);
+ callback();
+ }, false);
+ postData.focus();
+ }, false);
+ headers.focus();
+ }, false);
+ query.focus();
+}
+
+/*
+ * Make sure newly created event matches expected request
+ */
+function testSentRequest(aData, aOrigData) {
+ is(aData.method, aOrigData.method, "correct method in sent request");
+ is(aData.url, aOrigData.url + "&" + ADD_QUERY, "correct url in sent request");
+
+ let hasHeader = aData.requestHeaders.headers.some((header) => {
+ return (header.name + ": " + header.value) == ADD_HEADER;
+ })
+ ok(hasHeader, "new header added to sent request");
+
+ is(aData.requestPostData.postData.text,
+ aOrigData.requestPostData.postData.text + ADD_POSTDATA,
+ "post data added to sent request");
+}
+
+
+function type(aString) {
+ for (let ch of aString) {
+ EventUtils.synthesizeKey(ch, {}, gPanelWin);
+ }
+}
+
+function finishUp(aMonitor) {
+ gPanelWin = null;
+ gPanelDoc = null;
+
+ teardown(aMonitor).then(finish);
+}
diff --git a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
index bdf28c90c9b..79654af2a53 100644
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
@@ -168,3 +168,36 @@
- in the network details timings tab identifying the amount of time spent
- in a "receive" state. -->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/themes/linux/devtools/netmonitor.css b/browser/themes/linux/devtools/netmonitor.css
index a93106ca32e..59a2854009c 100644
--- a/browser/themes/linux/devtools/netmonitor.css
+++ b/browser/themes/linux/devtools/netmonitor.css
@@ -383,6 +383,11 @@ box.requests-menu-status[code^="5"] {
padding-top: 2px;
}
+#headers-summary-resend {
+ margin: 0 6px;
+ min-height: 20px;
+}
+
/* Response tabpanel */
#response-content-info-header {
@@ -422,6 +427,20 @@ box.requests-menu-status[code^="5"] {
transition: transform 0.2s ease-out;
}
+/* Custom request form */
+
+#custom-pane {
+ padding: 0.6em 0.5em;
+}
+
+.custom-header {
+ font-size: 1.1em;
+}
+
+.custom-section {
+ margin-top: 0.5em;
+}
+
/* Footer */
#requests-menu-footer {
diff --git a/browser/themes/osx/devtools/netmonitor.css b/browser/themes/osx/devtools/netmonitor.css
index 906ae13feea..ad31a365888 100644
--- a/browser/themes/osx/devtools/netmonitor.css
+++ b/browser/themes/osx/devtools/netmonitor.css
@@ -383,6 +383,11 @@ box.requests-menu-status[code^="5"] {
padding-top: 2px;
}
+#headers-summary-resend {
+ margin: 0 6px;
+ min-height: 20px;
+}
+
/* Response tabpanel */
#response-content-info-header {
@@ -422,6 +427,20 @@ box.requests-menu-status[code^="5"] {
transition: transform 0.2s ease-out;
}
+/* Custom request form */
+
+#custom-pane {
+ padding: 0.6em 0.5em;
+}
+
+.custom-header {
+ font-size: 1.1em;
+}
+
+.custom-section {
+ margin-top: 0.5em;
+}
+
/* Footer */
#requests-menu-footer {
diff --git a/browser/themes/windows/devtools/netmonitor.css b/browser/themes/windows/devtools/netmonitor.css
index ee72aa607a4..1dc29e0676d 100644
--- a/browser/themes/windows/devtools/netmonitor.css
+++ b/browser/themes/windows/devtools/netmonitor.css
@@ -383,6 +383,11 @@ box.requests-menu-status[code^="5"] {
padding-top: 2px;
}
+#headers-summary-resend {
+ margin: 0 6px;
+ min-height: 20px;
+}
+
/* Response tabpanel */
#response-content-info-header {
@@ -422,6 +427,20 @@ box.requests-menu-status[code^="5"] {
transition: transform 0.2s ease-out;
}
+/* Custom request form */
+
+#custom-pane {
+ padding: 0.6em 0.5em;
+}
+
+.custom-header {
+ font-size: 1.1em;
+}
+
+.custom-section {
+ margin-top: 0.5em;
+}
+
/* Footer */
#requests-menu-footer {
diff --git a/toolkit/devtools/server/actors/webconsole.js b/toolkit/devtools/server/actors/webconsole.js
index 82a16a4e492..3f64d30eeb4 100644
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -89,6 +89,7 @@ function WebConsoleActor(aConnection, aParentActor)
this._protoChains = new Map();
this._dbgGlobals = new Map();
+ this._netEvents = new Map();
this._getDebuggerGlobal(this.window);
this._onObserverNotification = this._onObserverNotification.bind(this);
@@ -147,6 +148,15 @@ WebConsoleActor.prototype =
*/
_dbgGlobals: null,
+ /**
+ * Holds a map between nsIChannel objects and NetworkEventActors for requests
+ * created with sendHTTPRequest.
+ *
+ * @private
+ * @type Map
+ */
+ _netEvents: null,
+
/**
* Object that holds the JSTerm API, the helper functions, for the default
* window object.
@@ -252,6 +262,8 @@ WebConsoleActor.prototype =
"last-pb-context-exited");
}
this._actorPool = null;
+
+ this._netEvents.clear();
this._protoChains.clear();
this._dbgGlobals.clear();
this._jstermHelpers = null;
@@ -1000,10 +1012,10 @@ WebConsoleActor.prototype =
* A new NetworkEventActor is returned. This is used for tracking the
* network request and response.
*/
- onNetworkEvent: function WCA_onNetworkEvent(aEvent)
+ onNetworkEvent: function WCA_onNetworkEvent(aEvent, aChannel)
{
- let actor = new NetworkEventActor(aEvent, this);
- this._actorPool.addActor(actor);
+ let actor = this.getNetworkEventActor(aChannel);
+ actor.init(aEvent);
let packet = {
from: this.actorID,
@@ -1016,6 +1028,57 @@ WebConsoleActor.prototype =
return actor;
},
+ /**
+ * Get the NetworkEventActor for a nsIChannel, if it exists,
+ * otherwise create a new one.
+ *
+ * @param object aChannel
+ * The channel for the network event.
+ */
+ getNetworkEventActor: function WCA_getNetworkEventActor(aChannel) {
+ let actor = this._netEvents.get(aChannel);
+ if (actor) {
+ // delete from map as we should only need to do this check once
+ this._netEvents.delete(aChannel);
+ actor.channel = null;
+ return actor;
+ }
+
+ actor = new NetworkEventActor(aChannel, this);
+ this._actorPool.addActor(actor);
+ return actor;
+ },
+
+ /**
+ * Send a new HTTP request from the target's window.
+ *
+ * @param object aMessage
+ * Object with 'request' - the HTTP request details.
+ */
+ onSendHTTPRequest: function WCA_onSendHTTPRequest(aMessage)
+ {
+ let details = aMessage.request;
+
+ // send request from target's window
+ let request = new this._window.XMLHttpRequest();
+ request.open(details.method, details.url, true);
+
+ for (let {name, value} of details.headers) {
+ request.setRequestHeader(name, value);
+ }
+ request.send(details.body);
+
+ let actor = this.getNetworkEventActor(request.channel);
+
+ // map channel to actor so we can associate future events with it
+ this._netEvents.set(request.channel, actor);
+
+ return {
+ from: this.actorID,
+ eventActor: actor.grip()
+ };
+ },
+
/**
* Handler for file activity. This method sends the file request information
* to the remote Web Console client.
@@ -1113,7 +1176,7 @@ WebConsoleActor.prototype =
});
break;
}
- },
+ }
};
WebConsoleActor.prototype.requestTypes =
@@ -1125,32 +1188,31 @@ WebConsoleActor.prototype.requestTypes =
autocomplete: WebConsoleActor.prototype.onAutocomplete,
clearMessagesCache: WebConsoleActor.prototype.onClearMessagesCache,
setPreferences: WebConsoleActor.prototype.onSetPreferences,
+ sendHTTPRequest: WebConsoleActor.prototype.onSendHTTPRequest
};
/**
* Creates an actor for a network event.
*
* @constructor
- * @param object aNetworkEvent
- * The network event you want to use the actor for.
+ * @param object aChannel
+ * The nsIChannel associated with this event.
* @param object aWebConsoleActor
* The parent WebConsoleActor instance for this object.
*/
-function NetworkEventActor(aNetworkEvent, aWebConsoleActor)
+function NetworkEventActor(aChannel, aWebConsoleActor)
{
this.parent = aWebConsoleActor;
this.conn = this.parent.conn;
-
- this._startedDateTime = aNetworkEvent.startedDateTime;
- this._isXHR = aNetworkEvent.isXHR;
+ this.channel = aChannel;
this._request = {
- method: aNetworkEvent.method,
- url: aNetworkEvent.url,
- httpVersion: aNetworkEvent.httpVersion,
+ method: null,
+ url: null,
+ httpVersion: null,
headers: [],
cookies: [],
- headersSize: aNetworkEvent.headersSize,
+ headersSize: null,
postData: {},
};
@@ -1164,10 +1226,6 @@ function NetworkEventActor(aNetworkEvent, aWebConsoleActor)
// Keep track of LongStringActors owned by this NetworkEventActor.
this._longStringActors = new Set();
-
- this._discardRequestBody = aNetworkEvent.discardRequestBody;
- this._discardResponseBody = aNetworkEvent.discardResponseBody;
- this._private = aNetworkEvent.private;
}
NetworkEventActor.prototype =
@@ -1206,6 +1264,10 @@ NetworkEventActor.prototype =
}
}
this._longStringActors = new Set();
+
+ if (this.channel) {
+ this.parent._netEvents.delete(this.channel);
+ }
this.parent.releaseActor(this);
},
@@ -1218,6 +1280,27 @@ NetworkEventActor.prototype =
return {};
},
+ /**
+ * Set the properties of this actor based on it's corresponding
+ * network event.
+ *
+ * @param object aNetworkEvent
+ * The network event associated with this actor.
+ */
+ init: function NEA_init(aNetworkEvent)
+ {
+ this._startedDateTime = aNetworkEvent.startedDateTime;
+ this._isXHR = aNetworkEvent.isXHR;
+
+ for (let prop of ['method', 'url', 'httpVersion', 'headersSize']) {
+ this._request[prop] = aNetworkEvent[prop];
+ }
+
+ this._discardRequestBody = aNetworkEvent.discardRequestBody;
+ this._discardResponseBody = aNetworkEvent.discardResponseBody;
+ this._private = aNetworkEvent.private;
+ },
+
/**
* The "getRequestHeaders" packet type handler.
*
@@ -1545,4 +1628,3 @@ NetworkEventActor.prototype.requestTypes =
DebuggerServer.addTabActor(WebConsoleActor, "consoleActor");
DebuggerServer.addGlobalActor(WebConsoleActor, "consoleActor");
-
diff --git a/toolkit/devtools/webconsole/WebConsoleClient.jsm b/toolkit/devtools/webconsole/WebConsoleClient.jsm
index 9c7ced146bc..8f87661e227 100644
--- a/toolkit/devtools/webconsole/WebConsoleClient.jsm
+++ b/toolkit/devtools/webconsole/WebConsoleClient.jsm
@@ -284,6 +284,23 @@ WebConsoleClient.prototype = {
this._client.request(packet, aOnResponse);
},
+ /**
+ * Send a HTTP request with the given data.
+ *
+ * @param string aData
+ * The details of the HTTP request.
+ * @param function aOnResponse
+ * The function invoked when the response is received.
+ */
+ sendHTTPRequest: function WCC_sendHTTPRequest(aData, aOnResponse) {
+ let packet = {
+ to: this._actor,
+ type: "sendHTTPRequest",
+ request: aData
+ };
+ this._client.request(packet, aOnResponse);
+ },
+
/**
* Start the given Web Console listeners.
*
diff --git a/toolkit/devtools/webconsole/WebConsoleUtils.jsm b/toolkit/devtools/webconsole/WebConsoleUtils.jsm
index 3d8d8e553bb..3f560a0572d 100644
--- a/toolkit/devtools/webconsole/WebConsoleUtils.jsm
+++ b/toolkit/devtools/webconsole/WebConsoleUtils.jsm
@@ -1748,10 +1748,10 @@ NetworkResponseListener.prototype = {
* window is given, all browser network requests are logged.
* @param object aOwner
* The network monitor owner. This object needs to hold:
- * - onNetworkEvent(aRequestInfo). This method is invoked once for every
- * new network request and it is given one arguments: the initial network
- * request information. onNetworkEvent() must return an object which
- * holds several add*() methods which are used to add further network
+ * - onNetworkEvent(aRequestInfo, aChannel). This method is invoked once for
+ * every new network request and it is given two arguments: the initial network
+ * request information, and the channel. onNetworkEvent() must return an object
+ * which holds several add*() methods which are used to add further network
* request/response information.
* - saveRequestAndResponseBodies property which tells if you want to log
* request and response bodies.
@@ -2052,7 +2052,7 @@ NetworkMonitor.prototype = {
cookies = NetworkHelper.parseCookieHeader(cookieHeader);
}
- httpActivity.owner = this.owner.onNetworkEvent(event);
+ httpActivity.owner = this.owner.onNetworkEvent(event, aChannel);
this._setupResponseListener(httpActivity);