Skip to content
Permalink
Browse files
Web Inspector: ER: Copy as fetch
https://bugs.webkit.org/show_bug.cgi?id=241216

Reviewed by Patrick Angle.

* Source/WebInspectorUI/UserInterface/Models/Resource.js:
(WI.Resource.prototype.generateFetchCode): Added.

* Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js:
(WI.appendContextMenuItemsForSourceCode):

* Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js:

* LayoutTests/http/tests/inspector/network/copy-as-fetch.html: Added.
* LayoutTests/http/tests/inspector/network/copy-as-fetch-expected.txt: Added.

Canonical link: https://commits.webkit.org/251226@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@295135 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
dcrousso committed Jun 2, 2022
1 parent 3532f29 commit f607038576930e1a2284585fb4d8fa149703baa3
Showing 5 changed files with 282 additions and 0 deletions.
@@ -0,0 +1,111 @@
Tests that "Copy as fetch" returns valid JS.


== Running test suite: WI.Resource.prototype.generateFetchCode
-- Running test case: WI.Resource.prototype.generateFetchCode.SimpleURL
fetch("http://127.0.0.1:8000/inspector/network/resources/url?query=true", {
"cache": "default",
"credentials": "omit",
"headers": {
"Accept": "*/*",
"User-Agent": <filtered>
},
"method": "GET",
"mode": "cors",
"redirect": "follow",
"referrer": "http://127.0.0.1:8000/inspector/network/copy-as-fetch.html"
})

-- Running test case: WI.Resource.prototype.generateFetchCode.SpecialURL
fetch("http://127.0.0.1:8000/inspector/network/resources/url'with$special%7B1..20%7Dchars[]%20.html", {
"cache": "default",
"credentials": "omit",
"headers": {
"Accept": "*/*",
"User-Agent": <filtered>
},
"method": "GET",
"mode": "cors",
"redirect": "follow",
"referrer": "http://127.0.0.1:8000/inspector/network/copy-as-fetch.html"
})

-- Running test case: WI.Resource.prototype.generateFetchCode.SpecialHeaders
fetch("http://127.0.0.1:8000/inspector/network/resources/url", {
"cache": "default",
"credentials": "omit",
"headers": {
"Accept": "*/*",
"User-Agent": <filtered>,
"X-Custom1": "test1",
"X-Custom2'%": "'Test''1\\'2",
"X-Custom3": "'${PWD}"
},
"method": "GET",
"mode": "cors",
"redirect": "follow",
"referrer": "http://127.0.0.1:8000/inspector/network/copy-as-fetch.html"
})

-- Running test case: WI.Resource.prototype.generateFetchCode.URLUTF8
fetch("http://127.0.0.1:8000/inspector/network/resources/url?utf8=%F0%9F%91%8D", {
"cache": "default",
"credentials": "omit",
"headers": {
"Accept": "*/*",
"User-Agent": <filtered>
},
"method": "GET",
"mode": "cors",
"redirect": "follow",
"referrer": "http://127.0.0.1:8000/inspector/network/copy-as-fetch.html"
})

-- Running test case: WI.Resource.prototype.generateFetchCode.POSTRequestURLEncodedData
fetch("http://127.0.0.1:8000/inspector/network/resources/url", {
"body": "lorem=ipsum&$dolor='sit'&amet={1..20}",
"cache": "default",
"credentials": "omit",
"headers": {
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": <filtered>
},
"method": "POST",
"mode": "cors",
"redirect": "follow",
"referrer": "http://127.0.0.1:8000/inspector/network/copy-as-fetch.html"
})

-- Running test case: WI.Resource.prototype.generateFetchCode.POSTRequestURLUTF8
fetch("http://127.0.0.1:8000/inspector/network/resources/url?utf8=%F0%9F%91%8D", {
"body": "🌨=⛄️",
"cache": "default",
"credentials": "omit",
"headers": {
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": <filtered>
},
"method": "POST",
"mode": "cors",
"redirect": "follow",
"referrer": "http://127.0.0.1:8000/inspector/network/copy-as-fetch.html"
})

-- Running test case: WI.Resource.prototype.generateFetchCode.PUTRequestWithJSON
fetch("http://127.0.0.1:8000/inspector/network/resources/url", {
"body": "{\"update\":\"now\"}",
"cache": "default",
"credentials": "omit",
"headers": {
"Accept": "*/*",
"Content-Type": "application/json",
"User-Agent": <filtered>
},
"method": "PUT",
"mode": "cors",
"redirect": "follow",
"referrer": "http://127.0.0.1:8000/inspector/network/copy-as-fetch.html"
})

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="../resources/inspector-test.js"></script>
<script>
function createSimpleGETRequest()
{
let request = new XMLHttpRequest;
request.open("GET", "resources/url?query=true", true);
request.send();
}

function createGETRequestWithSpecialURL()
{
let request = new XMLHttpRequest;
request.open("GET", "resources/url'with$special{1..20}chars[] .html", true);
request.send();
}

function createGETRequestWithSpecialCharsInHeaders()
{
let request = new XMLHttpRequest;
request.open("GET", "resources/url", true);
request.setRequestHeader("X-Custom1", "test1");
request.setRequestHeader("X-Custom2'%", "\'Test'\'1\\'2");
request.setRequestHeader("X-Custom3", "\'${PWD}");
request.send();
}

function createGETRequestWithUTF8()
{
let request = new XMLHttpRequest;
request.open("GET", "resources/url?utf8=👍", true);
request.send();
}

function createPOSTRequestWithURLEncodedData()
{
let request = new XMLHttpRequest;
request.open("POST", "resources/url", true);
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.send("lorem=ipsum&$dolor='sit'&amet={1..20}");
}

function createPOSTRequestWithUTF8()
{
let request = new XMLHttpRequest;
request.open("POST", "resources/url?utf8=👍", true);
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.send("🌨=⛄️");
}

function createPUTRequestWithJSON()
{
let request = new XMLHttpRequest;
request.open("PUT", "resources/url", true);
request.setRequestHeader("Content-Type", "application/json");
request.send("{\"update\":\"now\"}");
}

function test()
{
let suite = InspectorTest.createAsyncSuite("WI.Resource.prototype.generateFetchCode");

[
{name: "SimpleURL", expression: `createSimpleGETRequest()`},
{name: "SpecialURL", expression: `createGETRequestWithSpecialURL()`},
{name: "SpecialHeaders", expression: `createGETRequestWithSpecialCharsInHeaders()`},
{name: "URLUTF8", expression: `createGETRequestWithUTF8()`},
{name: "POSTRequestURLEncodedData", expression: `createPOSTRequestWithURLEncodedData()`},
{name: "POSTRequestURLUTF8", expression: `createPOSTRequestWithUTF8()`},
{name: "PUTRequestWithJSON", expression: `createPUTRequestWithJSON()`},
].forEach(({name, expression}) => {
suite.addTestCase({
name: suite.name + "." + name,
async test() {
let [resourceWasAddedEvent] = await Promise.all([
WI.Frame.awaitEvent(WI.Frame.Event.ResourceWasAdded),
InspectorTest.evaluateInPage(expression),
]);

let resource = resourceWasAddedEvent.data.resource;
let fetchCode = resource.generateFetchCode();
fetchCode = fetchCode.replace(/("User-Agent": )(".+?")(,?\n)/, "$1<filtered>$3");
InspectorTest.log(fetchCode);
},
});
});

suite.runTestCasesAndFinish();
}
</script>
</head>
<body onload="runTest()">
<p>Tests that "Copy as fetch" returns valid JS.</p>
</body>
</html>
@@ -416,6 +416,8 @@ localizedStrings["Copy Rule"] = "Copy Rule";
localizedStrings["Copy Selected"] = "Copy Selected";
localizedStrings["Copy Table"] = "Copy Table";
localizedStrings["Copy as cURL"] = "Copy as cURL";
/* Copy the URL, method, headers, etc. of the given network request in the format of a JS fetch expression. */
localizedStrings["Copy as fetch"] = "Copy as fetch";
localizedStrings["Could not capture screenshot"] = "Could not capture screenshot";
localizedStrings["Could not fetch properties. Object may no longer exist."] = "Could not fetch properties. Object may no longer exist.";
localizedStrings["Count"] = "Count";
@@ -1138,6 +1138,73 @@ WI.Resource = class Resource extends WI.SourceCode
this.dispatchEventToListeners(WI.Resource.Event.RequestDataDidChange);
}

generateFetchCode()
{
let options = {};

if (this.requestData)
options.body = this.requestData;

options.cache = "default";
options.credentials = (this.requestCookies.length || this._requestHeaders.valueForCaseInsensitiveKey("Authorization")) ? "include" : "omit";

// https://fetch.spec.whatwg.org/#forbidden-header-name
const forbiddenHeaders = new Set([
"accept-charset",
"accept-encoding",
"access-control-request-headers",
"access-control-request-method",
"connection",
"content-length",
"cookie",
"cookie2",
"date",
"dnt",
"expect",
"host",
"keep-alive",
"origin",
"referer",
"te",
"trailer",
"transfer-encoding",
"upgrade",
"via",
]);
let headers = Object.entries(this.requestHeaders)
.filter((header) => {
let key = header[0].toLowerCase();
if (forbiddenHeaders.has(key))
return false;
if (key.startsWith("proxy-") || key.startsWith("sec-"))
return false;
return true;
})
.sort((a, b) => a[0].extendedLocaleCompare(b[0]))
.reduce((accumulator, current) => {
accumulator[current[0]] = current[1];
return accumulator;
}, {});
if (!isEmptyObject(headers))
options.headers = headers;

// FIXME: <https://webkit.org/b/241217> Web Inspector: include `integrity` in "Copy as fetch"

if (this.requestMethod)
options.method = this.requestMethod;

options.mode = "cors";
options.redirect = "follow";

let referrer = this.requestHeaders.valueForCaseInsensitiveKey("Referer");
if (referrer)
options.referrer = referrer;

// FIXME: <https://webkit.org/b/241218> Web Inspector: include `referrerPolicy` in "Copy as fetch"

return `fetch(${JSON.stringify(this.url)}, ${JSON.stringify(options, null, WI.indentString())})`;
}

generateCURLCommand()
{
function escapeStringPosix(str) {
@@ -175,6 +175,10 @@ WI.appendContextMenuItemsForSourceCode = function(contextMenu, sourceCodeOrLocat

if (sourceCode instanceof WI.Resource && !sourceCode.localResourceOverride) {
if (sourceCode.urlComponents.scheme !== "data") {
contextMenu.appendItem(WI.UIString("Copy as fetch", "Copy the URL, method, headers, etc. of the given network request in the format of a JS fetch expression."), () => {
InspectorFrontendHost.copyText(sourceCode.generateFetchCode());
});

contextMenu.appendItem(WI.UIString("Copy as cURL"), () => {
InspectorFrontendHost.copyText(sourceCode.generateCURLCommand());
});

0 comments on commit f607038

Please sign in to comment.