mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 859059 - Implement "Copy as curl". r=msucan, r=vp
This commit is contained in:
parent
a8094b7d26
commit
d280704ee2
@ -140,6 +140,9 @@ Object.defineProperty(this, "NetworkHelper", {
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
|
||||
"@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Curl",
|
||||
"resource:///modules/devtools/Curl.jsm");
|
||||
|
||||
/**
|
||||
* Object defining the network monitor controller components.
|
||||
*/
|
||||
|
@ -523,6 +523,37 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
|
||||
clipboardHelper.copyString(selected.url, document);
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy a cURL command from the currently selected item.
|
||||
*/
|
||||
copyAsCurl: function() {
|
||||
let selected = this.selectedItem.attachment;
|
||||
Task.spawn(function*() {
|
||||
// Create a sanitized object for the Curl command generator.
|
||||
let data = {
|
||||
url: selected.url,
|
||||
method: selected.method,
|
||||
headers: [],
|
||||
httpVersion: selected.httpVersion,
|
||||
postDataText: null
|
||||
};
|
||||
|
||||
// Fetch header values.
|
||||
for (let { name, value } of selected.requestHeaders.headers) {
|
||||
let text = yield gNetwork.getString(value);
|
||||
data.headers.push({ name: name, value: text });
|
||||
}
|
||||
|
||||
// Fetch the request payload.
|
||||
if (selected.requestPostData) {
|
||||
let postData = selected.requestPostData.postData.text;
|
||||
data.postDataText = yield gNetwork.getString(postData);
|
||||
}
|
||||
|
||||
clipboardHelper.copyString(Curl.generateCommand(data), document);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy image as data uri.
|
||||
*/
|
||||
@ -1560,6 +1591,9 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
|
||||
let copyUrlElement = $("#request-menu-context-copy-url");
|
||||
copyUrlElement.hidden = !selectedItem;
|
||||
|
||||
let copyAsCurlElement = $("#request-menu-context-copy-as-curl");
|
||||
copyAsCurlElement.hidden = !selectedItem || !selectedItem.attachment.responseContent;
|
||||
|
||||
let copyImageAsDataUriElement = $("#request-menu-context-copy-image-as-data-uri");
|
||||
copyImageAsDataUriElement.hidden = !selectedItem ||
|
||||
!selectedItem.attachment.responseContent ||
|
||||
|
@ -29,6 +29,9 @@
|
||||
<menuitem id="request-menu-context-copy-url"
|
||||
label="&netmonitorUI.context.copyUrl;"
|
||||
accesskey="&netmonitorUI.context.copyUrl.accesskey;"/>
|
||||
<menuitem id="request-menu-context-copy-as-curl"
|
||||
label="&netmonitorUI.context.copyAsCurl;"
|
||||
oncommand="NetMonitorView.RequestsMenu.copyAsCurl();"/>
|
||||
<menuitem id="request-menu-context-copy-image-as-data-uri"
|
||||
label="&netmonitorUI.context.copyImageAsDataUri;"
|
||||
accesskey="&netmonitorUI.context.copyImageAsDataUri.accesskey;"/>
|
||||
|
@ -21,6 +21,8 @@ support-files =
|
||||
html_sorting-test-page.html
|
||||
html_statistics-test-page.html
|
||||
html_status-codes-test-page.html
|
||||
html_copy-as-curl.html
|
||||
html_curl-utils.html
|
||||
sjs_content-type-test-server.sjs
|
||||
sjs_simple-test-server.sjs
|
||||
sjs_sorting-test-server.sjs
|
||||
@ -39,8 +41,10 @@ support-files =
|
||||
[browser_net_clear.js]
|
||||
[browser_net_complex-params.js]
|
||||
[browser_net_content-type.js]
|
||||
[browser_net_curl-utils.js]
|
||||
[browser_net_copy_image_as_data_uri.js]
|
||||
[browser_net_copy_url.js]
|
||||
[browser_net_copy_as_curl.js]
|
||||
[browser_net_cyrillic-01.js]
|
||||
[browser_net_cyrillic-02.js]
|
||||
[browser_net_filter-01.js]
|
||||
|
72
browser/devtools/netmonitor/test/browser_net_copy_as_curl.js
Normal file
72
browser/devtools/netmonitor/test/browser_net_copy_as_curl.js
Normal file
@ -0,0 +1,72 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests if Copy as cURL works.
|
||||
*/
|
||||
|
||||
function test() {
|
||||
initNetMonitor(CURL_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
const EXPECTED_POSIX_RESULT = [
|
||||
"curl",
|
||||
"'" + SIMPLE_SJS + "'",
|
||||
"-H 'Host: example.com'",
|
||||
"-H 'User-Agent: " + aDebuggee.navigator.userAgent + "'",
|
||||
"-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'",
|
||||
"-H 'Accept-Language: " + aDebuggee.navigator.language + "'",
|
||||
"-H 'Accept-Encoding: gzip, deflate'",
|
||||
"-H 'X-Custom-Header-1: Custom value'",
|
||||
"-H 'X-Custom-Header-2: 8.8.8.8'",
|
||||
"-H 'X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT'",
|
||||
"-H 'Referer: " + CURL_URL + "'",
|
||||
"-H 'Connection: keep-alive'"
|
||||
].join(" ");
|
||||
|
||||
const EXPECTED_WIN_RESULT = [
|
||||
'curl',
|
||||
'"' + SIMPLE_SJS + '"',
|
||||
'-H "Host: example.com"',
|
||||
'-H "User-Agent: ' + aDebuggee.navigator.userAgent + '"',
|
||||
'-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"',
|
||||
'-H "Accept-Language: ' + aDebuggee.navigator.language + '"',
|
||||
'-H "Accept-Encoding: gzip, deflate"',
|
||||
'-H "X-Custom-Header-1: Custom value"',
|
||||
'-H "X-Custom-Header-2: 8.8.8.8"',
|
||||
'-H "X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"',
|
||||
'-H "Referer: ' + CURL_URL + '"',
|
||||
'-H "Connection: keep-alive"'
|
||||
].join(" ");
|
||||
|
||||
const EXPECTED_RESULT = Services.appinfo.OS == "WINNT" ?
|
||||
EXPECTED_WIN_RESULT : EXPECTED_POSIX_RESULT;
|
||||
|
||||
let { NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
||||
waitForNetworkEvents(aMonitor, 1).then(() => {
|
||||
let requestItem = RequestsMenu.getItemAtIndex(0);
|
||||
RequestsMenu.selectedItem = requestItem;
|
||||
|
||||
waitForClipboard(EXPECTED_RESULT, function setup() {
|
||||
RequestsMenu.copyAsCurl();
|
||||
}, function onSuccess() {
|
||||
ok(true, "Clipboard contains a cURL command for the currently selected item's url.");
|
||||
cleanUp();
|
||||
}, function onFailure() {
|
||||
ok(false, "Creating a cURL command for the currently selected item was unsuccessful.");
|
||||
cleanUp();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
aDebuggee.performRequest(SIMPLE_SJS);
|
||||
|
||||
function cleanUp(){
|
||||
teardown(aMonitor).then(finish);
|
||||
}
|
||||
});
|
||||
}
|
232
browser/devtools/netmonitor/test/browser_net_curl-utils.js
Normal file
232
browser/devtools/netmonitor/test/browser_net_curl-utils.js
Normal file
@ -0,0 +1,232 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests Curl Utils functionality.
|
||||
*/
|
||||
|
||||
function test() {
|
||||
initNetMonitor(CURL_UTILS_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
let { NetMonitorView, gNetwork } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
||||
waitForNetworkEvents(aMonitor, 1, 3).then(() => {
|
||||
let requests = {
|
||||
get: RequestsMenu.getItemAtIndex(0),
|
||||
post: RequestsMenu.getItemAtIndex(1),
|
||||
multipart: RequestsMenu.getItemAtIndex(2),
|
||||
multipartForm: RequestsMenu.getItemAtIndex(3)
|
||||
};
|
||||
|
||||
Task.spawn(function*() {
|
||||
yield createCurlData(requests.get.attachment, gNetwork).then((aData) => {
|
||||
test_findHeader(aData);
|
||||
});
|
||||
|
||||
yield createCurlData(requests.post.attachment, gNetwork).then((aData) => {
|
||||
test_isUrlEncodedRequest(aData);
|
||||
test_writePostDataTextParams(aData);
|
||||
});
|
||||
|
||||
yield createCurlData(requests.multipart.attachment, gNetwork).then((aData) => {
|
||||
test_isMultipartRequest(aData);
|
||||
test_getMultipartBoundary(aData);
|
||||
test_removeBinaryDataFromMultipartText(aData);
|
||||
});
|
||||
|
||||
yield createCurlData(requests.multipartForm.attachment, gNetwork).then((aData) => {
|
||||
test_getHeadersFromMultipartText(aData);
|
||||
});
|
||||
|
||||
if (Services.appinfo.OS != "WINNT") {
|
||||
test_escapeStringPosix();
|
||||
} else {
|
||||
test_escapeStringWin();
|
||||
}
|
||||
|
||||
teardown(aMonitor).then(finish);
|
||||
});
|
||||
});
|
||||
|
||||
aDebuggee.performRequests(SIMPLE_SJS);
|
||||
});
|
||||
}
|
||||
|
||||
function test_isUrlEncodedRequest(aData) {
|
||||
let isUrlEncoded = CurlUtils.isUrlEncodedRequest(aData);
|
||||
ok(isUrlEncoded, "Should return true for url encoded requests.");
|
||||
}
|
||||
|
||||
function test_isMultipartRequest(aData) {
|
||||
let isMultipart = CurlUtils.isMultipartRequest(aData);
|
||||
ok(isMultipart, "Should return true for multipart/form-data requests.");
|
||||
}
|
||||
|
||||
function test_findHeader(aData) {
|
||||
let headers = aData.headers;
|
||||
let hostName = CurlUtils.findHeader(headers, "Host");
|
||||
let requestedWithLowerCased = CurlUtils.findHeader(headers, "x-requested-with");
|
||||
let doesNotExist = CurlUtils.findHeader(headers, "X-Does-Not-Exist");
|
||||
|
||||
is(hostName, "example.com",
|
||||
"Header with name 'Host' should be found in the request array.");
|
||||
is(requestedWithLowerCased, "XMLHttpRequest",
|
||||
"The search should be case insensitive.");
|
||||
is(doesNotExist, null,
|
||||
"Should return null when a header is not found.");
|
||||
}
|
||||
|
||||
function test_writePostDataTextParams(aData) {
|
||||
let params = CurlUtils.writePostDataTextParams(aData.postDataText);
|
||||
is(params, "param1=value1¶m2=value2¶m3=value3",
|
||||
"Should return a serialized representation of the request parameters");
|
||||
}
|
||||
|
||||
function test_getMultipartBoundary(aData) {
|
||||
let boundary = CurlUtils.getMultipartBoundary(aData);
|
||||
ok(/-{3,}\w+/.test(boundary),
|
||||
"A boundary string should be found in a multipart request.");
|
||||
}
|
||||
|
||||
function test_removeBinaryDataFromMultipartText(aData) {
|
||||
let generatedBoundary = CurlUtils.getMultipartBoundary(aData);
|
||||
let text = aData.postDataText;
|
||||
let binaryRemoved =
|
||||
CurlUtils.removeBinaryDataFromMultipartText(text, generatedBoundary);
|
||||
let boundary = "--" + generatedBoundary;
|
||||
|
||||
const EXPECTED_POSIX_RESULT = [
|
||||
"$'",
|
||||
boundary,
|
||||
"\\r\\n\\r\\n",
|
||||
"Content-Disposition: form-data; name=\"param1\"",
|
||||
"\\r\\n\\r\\n",
|
||||
"value1",
|
||||
"\\r\\n",
|
||||
boundary,
|
||||
"\\r\\n\\r\\n",
|
||||
"Content-Disposition: form-data; name=\"file\"; filename=\"filename.png\"",
|
||||
"\\r\\n",
|
||||
"Content-Type: image/png",
|
||||
"\\r\\n\\r\\n",
|
||||
generatedBoundary,
|
||||
"--\\r\\n",
|
||||
"'"
|
||||
].join("");
|
||||
|
||||
const EXPECTED_WIN_RESULT = [
|
||||
'"' + boundary + '"^',
|
||||
'\u000d\u000A\u000d\u000A',
|
||||
'"Content-Disposition: form-data; name=""param1"""^',
|
||||
'\u000d\u000A\u000d\u000A',
|
||||
'"value1"^',
|
||||
'\u000d\u000A',
|
||||
'"' + boundary + '"^',
|
||||
'\u000d\u000A\u000d\u000A',
|
||||
'"Content-Disposition: form-data; name=""file""; filename=""filename.png"""^',
|
||||
'\u000d\u000A',
|
||||
'"Content-Type: image/png"^',
|
||||
'\u000d\u000A\u000d\u000A',
|
||||
'"' + generatedBoundary + '--"^',
|
||||
'\u000d\u000A',
|
||||
'""'
|
||||
].join("");
|
||||
|
||||
if (Services.appinfo.OS != "WINNT") {
|
||||
is(CurlUtils.escapeStringPosix(binaryRemoved), EXPECTED_POSIX_RESULT,
|
||||
"The mulitpart request payload should not contain binary data.");
|
||||
} else {
|
||||
is(CurlUtils.escapeStringWin(binaryRemoved), EXPECTED_WIN_RESULT,
|
||||
"WinNT: The mulitpart request payload should not contain binary data.");
|
||||
}
|
||||
}
|
||||
|
||||
function test_getHeadersFromMultipartText(aData) {
|
||||
let headers = CurlUtils.getHeadersFromMultipartText(aData.postDataText);
|
||||
|
||||
ok(Array.isArray(headers),
|
||||
"Should return an array.");
|
||||
ok(headers.length > 0,
|
||||
"There should exist at least one request header.");
|
||||
is(headers[0].name, "Content-Type",
|
||||
"The first header name should be 'Content-Type'.");
|
||||
}
|
||||
|
||||
function test_escapeStringPosix() {
|
||||
let surroundedWithQuotes = "A simple string";
|
||||
is(CurlUtils.escapeStringPosix(surroundedWithQuotes), "'A simple string'",
|
||||
"The string should be surrounded with single quotes.");
|
||||
|
||||
let singleQuotes = "It's unusual to put crickets in your coffee.";
|
||||
is(CurlUtils.escapeStringPosix(singleQuotes),
|
||||
"$'It\\'s unusual to put crickets in your coffee.'",
|
||||
"Single quotes should be escaped.");
|
||||
|
||||
let newLines = "Line 1\r\nLine 2\u000d\u000ALine3";
|
||||
is(CurlUtils.escapeStringPosix(newLines), "$'Line 1\\r\\nLine 2\\r\\nLine3'",
|
||||
"Newlines should be escaped.");
|
||||
|
||||
let controlChars = "\u0007 \u0009 \u000C \u001B";
|
||||
is(CurlUtils.escapeStringPosix(controlChars), "$'\\x07 \\x09 \\x0c \\x1b'",
|
||||
"Control characters should be escaped.");
|
||||
|
||||
let extendedAsciiChars = "æ ø ü ß ö é";
|
||||
is(CurlUtils.escapeStringPosix(extendedAsciiChars),
|
||||
"$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'",
|
||||
"Character codes outside of the decimal range 32 - 126 should be escaped.");
|
||||
}
|
||||
|
||||
function test_escapeStringWin() {
|
||||
let surroundedWithDoubleQuotes = "A simple string";
|
||||
is(CurlUtils.escapeStringWin(surroundedWithDoubleQuotes), '"A simple string"',
|
||||
"The string should be surrounded with double quotes.");
|
||||
|
||||
let doubleQuotes = "Quote: \"Time is an illusion. Lunchtime doubly so.\"";
|
||||
is(CurlUtils.escapeStringWin(doubleQuotes),
|
||||
'"Quote: ""Time is an illusion. Lunchtime doubly so."""',
|
||||
"Double quotes should be escaped.");
|
||||
|
||||
let percentSigns = "%AppData%";
|
||||
is(CurlUtils.escapeStringWin(percentSigns), '""%"AppData"%""',
|
||||
"Percent signs should be escaped.");
|
||||
|
||||
let backslashes = "\\A simple string\\";
|
||||
is(CurlUtils.escapeStringWin(backslashes), '"\\\\A simple string\\\\"',
|
||||
"Backslashes should be escaped.");
|
||||
|
||||
let newLines = "line1\r\nline2\r\nline3";
|
||||
is(CurlUtils.escapeStringWin(newLines),
|
||||
'"line1"^\u000d\u000A"line2"^\u000d\u000A"line3"',
|
||||
"Newlines should be escaped.");
|
||||
}
|
||||
|
||||
function createCurlData(aSelected, aNetwork) {
|
||||
return Task.spawn(function*() {
|
||||
// Create a sanitized object for the Curl command generator.
|
||||
let data = {
|
||||
url: aSelected.url,
|
||||
method: aSelected.method,
|
||||
headers: [],
|
||||
httpVersion: aSelected.httpVersion,
|
||||
postDataText: null
|
||||
};
|
||||
|
||||
// Fetch header values.
|
||||
for (let { name, value } of aSelected.requestHeaders.headers) {
|
||||
let text = yield aNetwork.getString(value);
|
||||
data.headers.push({ name: name, value: text });
|
||||
}
|
||||
|
||||
// Fetch the request payload.
|
||||
if (aSelected.requestPostData) {
|
||||
let postData = aSelected.requestPostData.postData.text;
|
||||
data.postDataText = yield aNetwork.getString(postData);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
@ -9,6 +9,7 @@ let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
|
||||
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
let { CurlUtils } = Cu.import("resource:///modules/devtools/Curl.jsm", {});
|
||||
let TargetFactory = devtools.TargetFactory;
|
||||
let Toolbox = devtools.Toolbox;
|
||||
|
||||
@ -34,6 +35,8 @@ const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
|
||||
const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
|
||||
const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html";
|
||||
const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
|
||||
const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html";
|
||||
const CURL_UTILS_URL = EXAMPLE_URL + "html_curl-utils.html";
|
||||
|
||||
const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
|
||||
const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
|
||||
|
27
browser/devtools/netmonitor/test/html_copy-as-curl.html
Normal file
27
browser/devtools/netmonitor/test/html_copy-as-curl.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Network Monitor test page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>Performing a GET request</p>
|
||||
|
||||
<script type="text/javascript">
|
||||
function performRequest(aUrl) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", aUrl, true);
|
||||
xhr.setRequestHeader("Accept-Language", window.navigator.language);
|
||||
xhr.setRequestHeader("X-Custom-Header-1", "Custom value");
|
||||
xhr.setRequestHeader("X-Custom-Header-2", "8.8.8.8");
|
||||
xhr.setRequestHeader("X-Custom-Header-3", "Mon, 3 Mar 2014 11:11:11 GMT");
|
||||
xhr.send(null);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
99
browser/devtools/netmonitor/test/html_curl-utils.html
Normal file
99
browser/devtools/netmonitor/test/html_curl-utils.html
Normal file
@ -0,0 +1,99 @@
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Network Monitor test page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>Performing requests</p>
|
||||
|
||||
<p>
|
||||
<canvas width="100" height="100"></canvas>
|
||||
</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<form method="post" action="#" enctype="multipart/form-data" target="target" id="post-form">
|
||||
<input type="text" name="param1" value="value1"/>
|
||||
<input type="text" name="param2" value="value2"/>
|
||||
<input type="text" name="param3" value="value3"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<iframe name="target"></iframe>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
function ajaxGet(aUrl, aCallback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", aUrl + "?param1=value1¶m2=value2¶m3=value3", true);
|
||||
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||||
xhr.onload = function() {
|
||||
aCallback();
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function ajaxPost(aUrl, aCallback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", aUrl, true);
|
||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||||
xhr.onload = function() {
|
||||
aCallback();
|
||||
};
|
||||
var params = "param1=value1¶m2=value2¶m3=value3";
|
||||
xhr.send(params);
|
||||
}
|
||||
|
||||
function ajaxMultipart(aUrl, aCallback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", aUrl, true);
|
||||
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||||
xhr.onload = function() {
|
||||
aCallback();
|
||||
};
|
||||
|
||||
getCanvasElem().toBlob((blob) => {
|
||||
var formData = new FormData();
|
||||
formData.append("param1", "value1");
|
||||
formData.append("file", blob, "filename.png");
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
var form = document.querySelector("#post-form");
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function getCanvasElem() {
|
||||
return document.querySelector("canvas");
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
var canvas = getCanvasElem();
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.fillRect(0,0,100,100);
|
||||
ctx.clearRect(20,20,60,60);
|
||||
ctx.strokeRect(25,25,50,50);
|
||||
}
|
||||
|
||||
function performRequests(aUrl) {
|
||||
ajaxGet(aUrl, () => {
|
||||
ajaxPost(aUrl, () => {
|
||||
ajaxMultipart(aUrl, () => {
|
||||
submitForm();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initCanvas();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
396
browser/devtools/shared/Curl.jsm
Normal file
396
browser/devtools/shared/Curl.jsm
Normal file
@ -0,0 +1,396 @@
|
||||
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
|
||||
|
||||
/*
|
||||
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
|
||||
* Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
|
||||
* Copyright (C) 2011 Google Inc. All rights reserved.
|
||||
* Copyright (C) 2009 Mozilla Foundation. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
|
||||
* its contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Curl", "CurlUtils"];
|
||||
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const DEFAULT_HTTP_VERSION = "HTTP/1.1";
|
||||
|
||||
this.Curl = {
|
||||
/**
|
||||
* Generates a cURL command string which can be used from the command line etc.
|
||||
*
|
||||
* @param object aData
|
||||
* Datasource to create the command from.
|
||||
* The object must contain the following properties:
|
||||
* - url:string, the URL of the request.
|
||||
* - method:string, the request method upper cased. HEAD / GET / POST etc.
|
||||
* - headers:array, an array of request headers {name:x, value:x} tuples.
|
||||
* - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1"
|
||||
* - postDataText:string, optional - the request payload.
|
||||
*
|
||||
* @return string
|
||||
* A cURL command.
|
||||
*/
|
||||
generateCommand: function(aData) {
|
||||
let utils = CurlUtils;
|
||||
|
||||
let command = ["curl"];
|
||||
let ignoredHeaders = new Set();
|
||||
|
||||
// The cURL command is expected to run on the same platform that Firefox runs
|
||||
// (it may be different from the inspected page platform).
|
||||
let escapeString = Services.appinfo.OS == "WINNT" ?
|
||||
utils.escapeStringWin : utils.escapeStringPosix;
|
||||
|
||||
// Add URL.
|
||||
command.push(escapeString(aData.url));
|
||||
|
||||
let postDataText = null;
|
||||
let multipartRequest = utils.isMultipartRequest(aData);
|
||||
|
||||
// Create post data.
|
||||
let data = [];
|
||||
if (utils.isUrlEncodedRequest(aData) || aData.method == "PUT") {
|
||||
postDataText = aData.postDataText;
|
||||
data.push("--data");
|
||||
data.push(escapeString(utils.writePostDataTextParams(postDataText)));
|
||||
ignoredHeaders.add("Content-Length");
|
||||
} else if (multipartRequest) {
|
||||
postDataText = aData.postDataText;
|
||||
data.push("--data-binary");
|
||||
let boundary = utils.getMultipartBoundary(aData);
|
||||
let text = utils.removeBinaryDataFromMultipartText(postDataText, boundary);
|
||||
data.push(escapeString(text));
|
||||
ignoredHeaders.add("Content-Length");
|
||||
}
|
||||
|
||||
// Add method.
|
||||
// For GET and POST requests this is not necessary as GET is the
|
||||
// default. If --data or --binary is added POST is the default.
|
||||
if (!(aData.method == "GET" || aData.method == "POST")) {
|
||||
command.push("-X");
|
||||
command.push(aData.method);
|
||||
}
|
||||
|
||||
// Add -I (HEAD)
|
||||
// For servers that supports HEAD.
|
||||
// This will fetch the header of a document only.
|
||||
if (aData.method == "HEAD") {
|
||||
command.push("-I");
|
||||
}
|
||||
|
||||
// Add http version.
|
||||
if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) {
|
||||
command.push("--" + aData.httpVersion.split("/")[1]);
|
||||
}
|
||||
|
||||
// Add request headers.
|
||||
let headers = aData.headers;
|
||||
if (multipartRequest) {
|
||||
let multipartHeaders = utils.getHeadersFromMultipartText(postDataText);
|
||||
headers = headers.concat(multipartHeaders);
|
||||
}
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
let header = headers[i];
|
||||
if (ignoredHeaders.has(header.name)) {
|
||||
continue;
|
||||
}
|
||||
command.push("-H");
|
||||
command.push(escapeString(header.name + ": " + header.value));
|
||||
}
|
||||
|
||||
// Add post data.
|
||||
command = command.concat(data);
|
||||
|
||||
return command.join(" ");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility functions for the Curl command generator.
|
||||
*/
|
||||
this.CurlUtils = {
|
||||
/**
|
||||
* Check if the request is an URL encoded request.
|
||||
*
|
||||
* @param object aData
|
||||
* The data source. See the description in the Curl object.
|
||||
* @return boolean
|
||||
* True if the request is URL encoded, false otherwise.
|
||||
*/
|
||||
isUrlEncodedRequest: function(aData) {
|
||||
let postDataText = aData.postDataText;
|
||||
if (!postDataText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
postDataText = postDataText.toLowerCase();
|
||||
if (postDataText.contains("content-type: application/x-www-form-urlencoded")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let contentType = this.findHeader(aData.headers, "content-type");
|
||||
|
||||
return (contentType &&
|
||||
contentType.toLowerCase().contains("application/x-www-form-urlencoded"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the request is a multipart request.
|
||||
*
|
||||
* @param object aData
|
||||
* The data source.
|
||||
* @return boolean
|
||||
* True if the request is multipart reqeust, false otherwise.
|
||||
*/
|
||||
isMultipartRequest: function(aData) {
|
||||
let postDataText = aData.postDataText;
|
||||
if (!postDataText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
postDataText = postDataText.toLowerCase();
|
||||
if (postDataText.contains("content-type: multipart/form-data")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let contentType = this.findHeader(aData.headers, "content-type");
|
||||
|
||||
return (contentType &&
|
||||
contentType.toLowerCase().contains("multipart/form-data;"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Write out paramters from post data text.
|
||||
*
|
||||
* @param object aPostDataText
|
||||
* Post data text.
|
||||
* @return string
|
||||
* Post data parameters.
|
||||
*/
|
||||
writePostDataTextParams: function(aPostDataText) {
|
||||
let lines = aPostDataText.split("\r\n");
|
||||
return lines[lines.length - 1];
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds the header with the given name in the headers array.
|
||||
*
|
||||
* @param array aHeaders
|
||||
* Array of headers info {name:x, value:x}.
|
||||
* @param string aName
|
||||
* The header name to find.
|
||||
* @return string
|
||||
* The found header value or null if not found.
|
||||
*/
|
||||
findHeader: function(aHeaders, aName) {
|
||||
if (!aHeaders) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let name = aName.toLowerCase();
|
||||
for (let header of aHeaders) {
|
||||
if (name == header.name.toLowerCase()) {
|
||||
return header.value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the boundary string for a multipart request.
|
||||
*
|
||||
* @param string aData
|
||||
* The data source. See the description in the Curl object.
|
||||
* @return string
|
||||
* The boundary string for the request.
|
||||
*/
|
||||
getMultipartBoundary: function(aData) {
|
||||
let boundaryRe = /\bboundary=(-{3,}\w+)/i;
|
||||
|
||||
// Get the boundary string from the Content-Type request header.
|
||||
let contentType = this.findHeader(aData.headers, "Content-Type");
|
||||
if (boundaryRe.test(contentType)) {
|
||||
return contentType.match(boundaryRe)[1];
|
||||
}
|
||||
// Temporary workaround. As of 2014-03-11 the requestHeaders array does not
|
||||
// always contain the Content-Type header for mulitpart requests. See bug 978144.
|
||||
// Find the header from the request payload.
|
||||
let boundaryString = aData.postDataText.match(boundaryRe)[1];
|
||||
if (boundaryString) {
|
||||
return boundaryString;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the binary data from mulitpart text.
|
||||
*
|
||||
* @param string aMultipartText
|
||||
* Multipart form data text.
|
||||
* @param string aBoundary
|
||||
* The boundary string.
|
||||
* @return string
|
||||
* The mulitpart text without the binary data.
|
||||
*/
|
||||
removeBinaryDataFromMultipartText: function(aMultipartText, aBoundary) {
|
||||
let result = "";
|
||||
let boundary = "--" + aBoundary;
|
||||
let parts = aMultipartText.split(boundary);
|
||||
for (let part of parts) {
|
||||
// Each part is expected to have a content disposition line.
|
||||
let contentDispositionLine = part.trimLeft().split("\r\n")[0];
|
||||
if (!contentDispositionLine) {
|
||||
continue;
|
||||
}
|
||||
contentDispositionLine = contentDispositionLine.toLowerCase();
|
||||
if (contentDispositionLine.contains("content-disposition: form-data")) {
|
||||
if (contentDispositionLine.contains("filename=")) {
|
||||
// The header lines and the binary blob is separated by 2 CRLF's.
|
||||
// Add only the headers to the result.
|
||||
let headers = part.split("\r\n\r\n")[0];
|
||||
result += boundary + "\r\n" + headers + "\r\n\r\n";
|
||||
}
|
||||
else {
|
||||
result += boundary + "\r\n" + part;
|
||||
}
|
||||
}
|
||||
}
|
||||
result += aBoundary + "--\r\n";
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the headers from a multipart post data text.
|
||||
*
|
||||
* @param string aMultipartText
|
||||
* Multipart post text.
|
||||
* @return array
|
||||
* An array of header objects {name:x, value:x}
|
||||
*/
|
||||
getHeadersFromMultipartText: function(aMultipartText) {
|
||||
let headers = [];
|
||||
if (!aMultipartText || aMultipartText.startsWith("---")) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Get the header section.
|
||||
let index = aMultipartText.indexOf("\r\n\r\n");
|
||||
if (index == -1) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Parse the header lines.
|
||||
let headersText = aMultipartText.substring(0, index);
|
||||
let headerLines = headersText.split("\r\n");
|
||||
let lastHeaderName = null;
|
||||
|
||||
for (let line of headerLines) {
|
||||
// Create a header for each line in fields that spans across multiple lines.
|
||||
// Subsquent lines always begins with at least one space or tab character.
|
||||
// (rfc2616)
|
||||
if (lastHeaderName && /^\s+/.test(line)) {
|
||||
headers.push({ name: lastHeaderName, value: line.trim() });
|
||||
continue;
|
||||
}
|
||||
|
||||
let indexOfColon = line.indexOf(":");
|
||||
if (indexOfColon == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let header = [line.slice(0, indexOfColon), line.slice(indexOfColon + 1)];
|
||||
if (header.length != 2) {
|
||||
continue;
|
||||
}
|
||||
lastHeaderName = header[0].trim();
|
||||
headers.push({ name: lastHeaderName, value: header[1].trim() });
|
||||
}
|
||||
|
||||
return headers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape util function for POSIX oriented operating systems.
|
||||
* Credit: Google DevTools
|
||||
*/
|
||||
escapeStringPosix: function(str) {
|
||||
function escapeCharacter(x) {
|
||||
let code = x.charCodeAt(0);
|
||||
if (code < 256) {
|
||||
// Add leading zero when needed to not care about the next character.
|
||||
return code < 16 ? "\\x0" + code.toString(16) : "\\x" + code.toString(16);
|
||||
}
|
||||
code = code.toString(16);
|
||||
return "\\u" + ("0000" + code).substr(code.length, 4);
|
||||
}
|
||||
|
||||
if (/[^\x20-\x7E]|\'/.test(str)) {
|
||||
// Use ANSI-C quoting syntax.
|
||||
return "$\'" + str.replace(/\\/g, "\\\\")
|
||||
.replace(/\'/g, "\\\'")
|
||||
.replace(/\n/g, "\\n")
|
||||
.replace(/\r/g, "\\r")
|
||||
.replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
|
||||
} else {
|
||||
// Use single quote syntax.
|
||||
return "'" + str + "'";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape util function for Windows systems.
|
||||
* Credit: Google DevTools
|
||||
*/
|
||||
escapeStringWin: function(str) {
|
||||
/* Replace quote by double quote (but not by \") because it is
|
||||
recognized by both cmd.exe and MS Crt arguments parser.
|
||||
|
||||
Replace % by "%" because it could be expanded to an environment
|
||||
variable value. So %% becomes "%""%". Even if an env variable ""
|
||||
(2 doublequotes) is declared, the cmd.exe will not
|
||||
substitute it with its value.
|
||||
|
||||
Replace each backslash with double backslash to make sure
|
||||
MS Crt arguments parser won't collapse them.
|
||||
|
||||
Replace new line outside of quotes since cmd.exe doesn't let
|
||||
to do it inside.
|
||||
*/
|
||||
return "\"" + str.replace(/"/g, "\"\"")
|
||||
.replace(/%/g, "\"%\"")
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/[\r\n]+/g, "\"^$&\"") + "\"";
|
||||
}
|
||||
};
|
@ -202,6 +202,12 @@
|
||||
- on the context menu that copies the selected request's url -->
|
||||
<!ENTITY netmonitorUI.context.copyUrl "Copy URL">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.context.copyAsCurl): This is the label displayed
|
||||
- on the context menu that copies the selected request as a cURL command.
|
||||
- The capitalization is part of the official name and should be used throughout all languages.
|
||||
- http://en.wikipedia.org/wiki/CURL -->
|
||||
<!ENTITY netmonitorUI.context.copyAsCurl "Copy as cURL">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.context.copyUrl.accesskey): This is the access key
|
||||
- for the Copy URL menu item displayed in the context menu for a request -->
|
||||
<!ENTITY netmonitorUI.context.copyUrl.accesskey "C">
|
||||
|
Loading…
Reference in New Issue
Block a user