From 3df6267373160bfa56872d29f9b17a7727ca647b Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Thu, 3 Dec 2020 13:02:59 +0100 Subject: [PATCH 01/13] Add test cases for urlencoded serialization of filenames --- url/urlencoded-filenames.html | 130 ++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 url/urlencoded-filenames.html diff --git a/url/urlencoded-filenames.html b/url/urlencoded-filenames.html new file mode 100644 index 00000000000000..2e220ff0c91f44 --- /dev/null +++ b/url/urlencoded-filenames.html @@ -0,0 +1,130 @@ + + +Test urlencoding of filenames + + + + From cf5f2264dcc03b52ef6f3a3ca3e235a2ad70cd72 Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Thu, 3 Dec 2020 13:31:42 +0100 Subject: [PATCH 02/13] Make it a .window.js test --- url/urlencoded-filenames.html | 130 ----------------------------- url/urlencoded-filenames.window.js | 122 +++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 130 deletions(-) delete mode 100644 url/urlencoded-filenames.html create mode 100644 url/urlencoded-filenames.window.js diff --git a/url/urlencoded-filenames.html b/url/urlencoded-filenames.html deleted file mode 100644 index 2e220ff0c91f44..00000000000000 --- a/url/urlencoded-filenames.html +++ /dev/null @@ -1,130 +0,0 @@ - - -Test urlencoding of filenames - - - - diff --git a/url/urlencoded-filenames.window.js b/url/urlencoded-filenames.window.js new file mode 100644 index 00000000000000..45cb73c8e6e388 --- /dev/null +++ b/url/urlencoded-filenames.window.js @@ -0,0 +1,122 @@ +form({ + filename: "basic-test.txt", + expectedFilename: "basic-test.txt", + description: "Basic test", +}); + +form({ + filename: "a\0b", + expectedFilename: "a%00b", + description: "Controls: 0x00", +}); + +form({ + filename: "a\nb", + expectedFilename: "a%0Ab", + description: "Newlines: \\n", +}); + +form({ + filename: "a\rb", + expectedFilename: "a%0Db", + description: "Newlines: \\r", +}); + +form({ + filename: "a\n\rb", + expectedFilename: "a%0A%0Db", + description: "Newlines: \\n\\r", +}); + +form({ + filename: "a\r\nb", + expectedFilename: "a%0D%0Ab", + description: "Newlines: \\r\\n", +}); + +form({ + filename: 'a"b', + expectedFilename: "a%22b", + description: "Special punctuation: double quote", +}); + +form({ + filename: "a'b", + expectedFilename: "a%27b", + description: "Special punctuation: single quote", +}); + +form({ + filename: "a\\b", + expectedFilename: "a%5Cb", + description: "Special punctuation: backslash", +}); + +form({ + filename: "ábc", + expectedFilename: "%C3%A1bc", + description: "Non-ASCII", +}); + +form({ + filename: "a\uFFFDb", + expectedFilename: "a%26%2365533%3Bb", + formEncoding: "windows-1252", + description: "Character not in encoding", +}); + +function form({ + filename, + expectedFilename, + formEncoding = "utf-8", + description, +}) { + promise_test(async (testCase) => { + if (document.readyState !== "complete") { + await new Promise((resolve) => addEventListener("load", resolve)); + } + + const formTargetFrame = Object.assign(document.createElement("iframe"), { + name: "formtargetframe", + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + + const form = Object.assign(document.createElement("form"), { + acceptCharset: formEncoding, + // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py + // to work around WebKit not percent-encoding \x00 (which causes the + // response to be detected as a binary file and served as a download). + // The output should not change if the urlencoded serializer is correct. + action: "/FileAPI/file/resources/echo-content-escaped.py", + method: "POST", + enctype: "application/x-www-form-urlencoded", + target: formTargetFrame.name, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + const fileInput = Object.assign(document.createElement("input"), { + type: "file", + name: "file", + }); + form.append(fileInput); + + await new Promise((resolve) => { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(new File([], filename, { type: "text/plain" })); + fileInput.files = dataTransfer.files; + + form.submit(); + formTargetFrame.onload = resolve; + }); + + const urlencoded = formTargetFrame.contentDocument.body.textContent; + const expected = `file=${expectedFilename}`; + assert_equals(urlencoded, expected); + }, `Test urlencoding of filenames: ${description}`); +} From be06777f61d038a191dce25b525d2f3cf4d043f1 Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Thu, 3 Dec 2020 15:08:57 +0100 Subject: [PATCH 03/13] Add readme in html/semantics/forms/form-submission-0 --- html/semantics/forms/form-submission-0/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/semantics/forms/form-submission-0/README.md diff --git a/html/semantics/forms/form-submission-0/README.md b/html/semantics/forms/form-submission-0/README.md new file mode 100644 index 00000000000000..19eae0d51778d2 --- /dev/null +++ b/html/semantics/forms/form-submission-0/README.md @@ -0,0 +1,5 @@ +Form submissions involving files are also tested in: + +- `/FileAPI/file/send-file*` (for `multipart/form-data`) +- `/url/urlencoded-filenames.window.js` (for + `application/x-www-form-urlencoded`) From f7c8d61223c4cefc9904d109f36d24e8946efc36 Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Fri, 4 Dec 2020 12:01:57 +0100 Subject: [PATCH 04/13] Update the tests to mandate newline normalization --- url/urlencoded-filenames.window.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/url/urlencoded-filenames.window.js b/url/urlencoded-filenames.window.js index 45cb73c8e6e388..31eb11ff850b32 100644 --- a/url/urlencoded-filenames.window.js +++ b/url/urlencoded-filenames.window.js @@ -12,26 +12,26 @@ form({ form({ filename: "a\nb", - expectedFilename: "a%0Ab", - description: "Newlines: \\n", + expectedFilename: "a%0D%0Ab", + description: "Newlines: \\n becomes \\r\\n", }); form({ filename: "a\rb", - expectedFilename: "a%0Db", - description: "Newlines: \\r", + expectedFilename: "a%0D%0Ab", + description: "Newlines: \\r becomes \\r\\n", }); form({ filename: "a\n\rb", - expectedFilename: "a%0A%0Db", - description: "Newlines: \\n\\r", + expectedFilename: "a%0D%0A%0D%0Ab", + description: "Newlines: \\n\\r becomes \\r\\n\\r\\n", }); form({ filename: "a\r\nb", expectedFilename: "a%0D%0Ab", - description: "Newlines: \\r\\n", + description: "Newlines: \\r\\n stays unchanged", }); form({ From 700daa59c9da6a72ca02fb7804ed0f735eaa5624 Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Fri, 4 Dec 2020 18:16:28 +0100 Subject: [PATCH 05/13] Add tests for text/plain form encoding. --- .../form-submission-0/text-plain.window.js | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 html/semantics/forms/form-submission-0/text-plain.window.js diff --git a/html/semantics/forms/form-submission-0/text-plain.window.js b/html/semantics/forms/form-submission-0/text-plain.window.js new file mode 100644 index 00000000000000..6dbd270cb73824 --- /dev/null +++ b/html/semantics/forms/form-submission-0/text-plain.window.js @@ -0,0 +1,130 @@ +form({ + filename: "basic-test.txt", + expectedFilename: "basic-test.txt", + description: "Basic test", +}); + +form({ + filename: "a\0b", + expectedFilename: "a\0b", + description: "Controls: 0x00", +}); + +form({ + filename: "a\nb", + expectedFilename: "a\r\nb", + description: "Newlines: \\n becomes \\r\\n", +}); + +form({ + filename: "a\rb", + expectedFilename: "a\r\nb", + description: "Newlines: \\r becomes \\r\\n", +}); + +form({ + filename: "a\n\rb", + expectedFilename: "a\r\n\r\nb", + description: "Newlines: \\n\\r becomes \\r\\n\\r\\n", +}); + +form({ + filename: "a\r\nb", + expectedFilename: "a\r\nb", + description: "Newlines: \\r\\n stays unchanged", +}); + +form({ + filename: 'a"b', + expectedFilename: 'a"b', + description: "Special punctuation: double quote", +}); + +form({ + filename: "a'b", + expectedFilename: "a'b", + description: "Special punctuation: single quote", +}); + +form({ + filename: "a\\b", + expectedFilename: "a\\b", + description: "Special punctuation: backslash", +}); + +form({ + filename: "ábc", + expectedFilename: "\xC3\xA1bc", + description: "Non-ASCII", +}); + +form({ + filename: "a\uFFFDb", + expectedFilename: "a�b", + formEncoding: "windows-1252", + description: "Character not in encoding", +}); + +function form({ + filename, + expectedFilename, + formEncoding = "utf-8", + description, +}) { + promise_test(async (testCase) => { + if (document.readyState !== "complete") { + await new Promise((resolve) => addEventListener("load", resolve)); + } + + const formTargetFrame = Object.assign(document.createElement("iframe"), { + name: "formtargetframe", + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + + const form = Object.assign(document.createElement("form"), { + acceptCharset: formEncoding, + action: "/FileAPI/file/resources/echo-content-escaped.py", + method: "POST", + enctype: "text/plain", + target: formTargetFrame.name, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + const fileInput = Object.assign(document.createElement("input"), { + type: "file", + name: "file", + }); + form.append(fileInput); + + const textPlain = await new Promise((resolve) => { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(new File([], filename, { type: "text/plain" })); + fileInput.files = dataTransfer.files; + + form.submit(); + formTargetFrame.onload = () => { + // Undo the newline normalization caused by loading the server's output + // as an iframe, and decode the echo-content-escaped.py escapes to reach + // an isomorphic-encoding of the bytes sent to the server. + resolve( + formTargetFrame.contentDocument.body.textContent + .replace(/\n/g, "\r\n") + .replace( + /\\x[0-9a-f]{2}/gi, + (esc) => String.fromCodePoint(parseInt(esc.substring(2), 16)), + ) + .replace(/\\\\/g, "\\"), + ); + }; + }); + + const expected = `file=${expectedFilename}\r\n`; + assert_equals(textPlain, expected); + }, `Test urlencoding of filenames: ${description}`); +} From 1cef8b1e5550d20ce1cfd1d3f6f785377f7a6e58 Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Mon, 14 Dec 2020 19:19:41 +0100 Subject: [PATCH 06/13] text-plain.window.js isn't testing urlencoded --- html/semantics/forms/form-submission-0/text-plain.window.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/semantics/forms/form-submission-0/text-plain.window.js b/html/semantics/forms/form-submission-0/text-plain.window.js index 6dbd270cb73824..418ad2c68c2870 100644 --- a/html/semantics/forms/form-submission-0/text-plain.window.js +++ b/html/semantics/forms/form-submission-0/text-plain.window.js @@ -126,5 +126,5 @@ function form({ const expected = `file=${expectedFilename}\r\n`; assert_equals(textPlain, expected); - }, `Test urlencoding of filenames: ${description}`); + }, `Test the encoding of filenames in text/plain forms: ${description}`); } From 3b4a7df745c1a6cd0829cf3c6ae28c50ba43becc Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Thu, 14 Jan 2021 13:43:20 +0100 Subject: [PATCH 07/13] Modify tests for urlencoded and text/plain --- .../forms/form-submission-0/README.md | 2 - .../form-submission-0/text-plain.window.js | 307 +++++++++++++---- .../form-submission-0/urlencoded2.window.js | 321 ++++++++++++++++++ url/urlencoded-filenames.window.js | 122 ------- url/urlsearchparams-stringifier.any.js | 10 + 5 files changed, 579 insertions(+), 183 deletions(-) create mode 100644 html/semantics/forms/form-submission-0/urlencoded2.window.js delete mode 100644 url/urlencoded-filenames.window.js diff --git a/html/semantics/forms/form-submission-0/README.md b/html/semantics/forms/form-submission-0/README.md index 19eae0d51778d2..8dcbddf820bc0f 100644 --- a/html/semantics/forms/form-submission-0/README.md +++ b/html/semantics/forms/form-submission-0/README.md @@ -1,5 +1,3 @@ Form submissions involving files are also tested in: - `/FileAPI/file/send-file*` (for `multipart/form-data`) -- `/url/urlencoded-filenames.window.js` (for - `application/x-www-form-urlencoded`) diff --git a/html/semantics/forms/form-submission-0/text-plain.window.js b/html/semantics/forms/form-submission-0/text-plain.window.js index 418ad2c68c2870..37c2d6e0f38211 100644 --- a/html/semantics/forms/form-submission-0/text-plain.window.js +++ b/html/semantics/forms/form-submission-0/text-plain.window.js @@ -1,76 +1,223 @@ form({ - filename: "basic-test.txt", - expectedFilename: "basic-test.txt", + name: "basic", + value: "test", + expected: "basic=test\r\n", description: "Basic test", }); form({ - filename: "a\0b", - expectedFilename: "a\0b", - description: "Controls: 0x00", + name: "basic", + value: new File([], "file-test.txt"), + expected: "basic=file-test.txt\r\n", + description: "Basic File test", }); form({ - filename: "a\nb", - expectedFilename: "a\r\nb", - description: "Newlines: \\n becomes \\r\\n", + name: "a\0b", + value: "c", + expected: "a\0b=c\r\n", + description: "0x00 in name", }); form({ - filename: "a\rb", - expectedFilename: "a\r\nb", - description: "Newlines: \\r becomes \\r\\n", + name: "a", + value: "b\0c", + expected: "a=b\0c\r\n", + description: "0x00 in value", }); form({ - filename: "a\n\rb", - expectedFilename: "a\r\n\r\nb", - description: "Newlines: \\n\\r becomes \\r\\n\\r\\n", + name: "a", + value: new File([], "b\0c"), + expected: "a=b\0c\r\n", + description: "0x00 in filename", }); form({ - filename: "a\r\nb", - expectedFilename: "a\r\nb", - description: "Newlines: \\r\\n stays unchanged", + name: "a\nb", + value: "c", + expected: "a\r\nb=c\r\n", + description: "\\n in name", }); form({ - filename: 'a"b', - expectedFilename: 'a"b', - description: "Special punctuation: double quote", + name: "a\rb", + value: "c", + expected: "a\r\nb=c\r\n", + description: "\\r in name", }); form({ - filename: "a'b", - expectedFilename: "a'b", - description: "Special punctuation: single quote", + name: "a\r\nb", + value: "c", + expected: "a\r\nb=c\r\n", + description: "\\r\\n in name", }); form({ - filename: "a\\b", - expectedFilename: "a\\b", - description: "Special punctuation: backslash", + name: "a\n\rb", + value: "c", + expected: "a\r\n\r\nb=c\r\n", + description: "\\n\\r in name", }); form({ - filename: "ábc", - expectedFilename: "\xC3\xA1bc", - description: "Non-ASCII", + name: "a", + value: "b\nc", + expected: "a=b\r\nc\r\n", + description: "\\n in value", }); form({ - filename: "a\uFFFDb", - expectedFilename: "a�b", + name: "a", + value: "b\rc", + expected: "a=b\r\nc\r\n", + description: "\\r in value", +}); + +form({ + name: "a", + value: "b\r\nc", + expected: "a=b\r\nc\r\n", + description: "\\r\\n in value", +}); + +form({ + name: "a", + value: "b\n\rc", + expected: "a=b\r\n\r\nc\r\n", + description: "\\n\\r in value", +}); + +form({ + name: "a", + value: new File([], "b\nc"), + expected: "a=b\r\nc\r\n", + description: "\\n in filename", +}); + +form({ + name: "a", + value: new File([], "b\rc"), + expected: "a=b\r\nc\r\n", + description: "\\r in filename", +}); + +form({ + name: "a", + value: new File([], "b\r\nc"), + expected: "a=b\r\nc\r\n", + description: "\\r\\n in filename", +}); + +form({ + name: "a", + value: new File([], "b\n\rc"), + expected: "a=b\r\n\r\nc\r\n", + description: "\\n\\r in filename", +}); + +form({ + name: 'a"b', + value: "c", + expected: 'a"b=c\r\n', + description: "double quote in name", +}); + +form({ + name: "a", + value: 'b"c', + expected: 'a=b"c\r\n', + description: "double quote in value", +}); + +form({ + name: "a", + value: new File([], 'b"c'), + expected: 'a=b"c\r\n', + description: "double quote in filename", +}); + +form({ + name: "a'b", + value: "c", + expected: "a'b=c\r\n", + description: "single quote in name", +}); + +form({ + name: "a", + value: "b'c", + expected: "a=b'c\r\n", + description: "single quote in value", +}); + +form({ + name: "a", + value: new File([], "b'c"), + expected: "a=b'c\r\n", + description: "single quote in filename", +}); + +form({ + name: "a\\b", + value: "c", + expected: "a\\b=c\r\n", + description: "backslash in name", +}); + +form({ + name: "a", + value: "b\\c", + expected: "a=b\\c\r\n", + description: "backslash in value", +}); + +form({ + name: "a", + value: new File([], "b\\c"), + expected: "a=b\\c\r\n", + description: "backslash in filename", +}); + +form({ + name: "áb", + value: "ç", + expected: "\xC3\xA1b=\xC3\xA7\r\n", + description: "non-ASCII in name and value", +}); + +form({ + name: "a", + value: new File([], "ə.txt"), + expected: "a=\xC9\x99.txt\r\n", + description: "non-ASCII in filename", +}); + +form({ + name: "aəb", + value: "c\uFFFDd", + formEncoding: "windows-1252", + expected: "aəb=c�d\r\n", + description: "characters not in encoding in name and value", +}); + +form({ + name: "á", + value: new File([], "💩"), formEncoding: "windows-1252", - description: "Character not in encoding", + expected: "\xE1=💩\r\n", + description: "character not in encoding in filename", }); function form({ - filename, - expectedFilename, + name, + value, + expected, formEncoding = "utf-8", description, }) { + // Normal form promise_test(async (testCase) => { if (document.readyState !== "complete") { await new Promise((resolve) => addEventListener("load", resolve)); @@ -86,6 +233,9 @@ function form({ const form = Object.assign(document.createElement("form"), { acceptCharset: formEncoding, + // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py + // because we're doing tests with \x00, which can cause the response to be + // detected as a binary file and served as a download). action: "/FileAPI/file/resources/echo-content-escaped.py", method: "POST", enctype: "text/plain", @@ -96,35 +246,74 @@ function form({ document.body.removeChild(form); }); - const fileInput = Object.assign(document.createElement("input"), { - type: "file", - name: "file", + const input = document.createElement("input"); + input.name = name; + if (value instanceof File) { + input.type = "file"; + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(value); + input.files = dataTransfer.files; + } else { + input.type = "hidden"; + input.value = value; + } + form.append(input); + + await new Promise((resolve) => { + form.submit(); + formTargetFrame.onload = resolve; }); - form.append(fileInput); - const textPlain = await new Promise((resolve) => { - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(new File([], filename, { type: "text/plain" })); - fileInput.files = dataTransfer.files; + const urlencoded = formTargetFrame.contentDocument.body.textContent; + assert_equals(unescape(urlencoded), expected); + }, `text/plain: ${description} (normal form)`); + + // formdata event + promise_test(async (testCase) => { + if (document.readyState !== "complete") { + await new Promise((resolve) => addEventListener("load", resolve)); + } + + const formTargetFrame = Object.assign(document.createElement("iframe"), { + name: "formtargetframe", + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + const form = Object.assign(document.createElement("form"), { + acceptCharset: formEncoding, + // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py + // because we're doing tests with \x00, which can cause the response to be + // detected as a binary file and served as a download). + action: "/FileAPI/file/resources/echo-content-escaped.py", + method: "POST", + enctype: "text/plain", + target: formTargetFrame.name, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + form.addEventListener("formdata", (evt) => { + evt.formData.append(name, value); + }); + + await new Promise((resolve) => { form.submit(); - formTargetFrame.onload = () => { - // Undo the newline normalization caused by loading the server's output - // as an iframe, and decode the echo-content-escaped.py escapes to reach - // an isomorphic-encoding of the bytes sent to the server. - resolve( - formTargetFrame.contentDocument.body.textContent - .replace(/\n/g, "\r\n") - .replace( - /\\x[0-9a-f]{2}/gi, - (esc) => String.fromCodePoint(parseInt(esc.substring(2), 16)), - ) - .replace(/\\\\/g, "\\"), - ); - }; + formTargetFrame.onload = resolve; }); - const expected = `file=${expectedFilename}\r\n`; - assert_equals(textPlain, expected); - }, `Test the encoding of filenames in text/plain forms: ${description}`); + const urlencoded = formTargetFrame.contentDocument.body.textContent; + assert_equals(unescape(urlencoded), expected); + }, `text/plain: ${description} (formdata event)`); +} + +function unescape(str) { + return str.replace(/\r\n?|\n/g, "\r\n").replace( + /\\x[0-9A-Fa-f]{2}/g, + (escape) => String.fromCodePoint(parseInt(escape.substring(2), 16)), + ).replace(/\\\\/g, "\\"); } diff --git a/html/semantics/forms/form-submission-0/urlencoded2.window.js b/html/semantics/forms/form-submission-0/urlencoded2.window.js new file mode 100644 index 00000000000000..ee0ad67180b5f2 --- /dev/null +++ b/html/semantics/forms/form-submission-0/urlencoded2.window.js @@ -0,0 +1,321 @@ +form({ + name: "basic", + value: "test", + expected: "basic=test", + description: "Basic test", +}); + +form({ + name: "basic", + value: new File([], "file-test.txt"), + expected: "basic=file-test.txt", + description: "Basic File test", +}); + +form({ + name: "a\0b", + value: "c", + expected: "a%00b=c", + description: "0x00 in name", +}); + +form({ + name: "a", + value: "b\0c", + expected: "a=b%00c", + description: "0x00 in value", +}); + +form({ + name: "a", + value: new File([], "b\0c"), + expected: "a=b%00c", + description: "0x00 in filename", +}); + +form({ + name: "a\nb", + value: "c", + expected: "a%0D%0Ab=c", + description: "\\n in name", +}); + +form({ + name: "a\rb", + value: "c", + expected: "a%0D%0Ab=c", + description: "\\r in name", +}); + +form({ + name: "a\r\nb", + value: "c", + expected: "a%0D%0Ab=c", + description: "\\r\\n in name", +}); + +form({ + name: "a\n\rb", + value: "c", + expected: "a%0D%0A%0D%0Ab=c", + description: "\\n\\r in name", +}); + +form({ + name: "a", + value: "b\nc", + expected: "a=b%0D%0Ac", + description: "\\n in value", +}); + +form({ + name: "a", + value: "b\rc", + expected: "a=b%0D%0Ac", + description: "\\r in value", +}); + +form({ + name: "a", + value: "b\r\nc", + expected: "a=b%0D%0Ac", + description: "\\r\\n in value", +}); + +form({ + name: "a", + value: "b\n\rc", + expected: "a=b%0D%0A%0D%0Ac", + description: "\\n\\r in value", +}); + +form({ + name: "a", + value: new File([], "b\nc"), + expected: "a=b%0D%0Ac", + description: "\\n in filename", +}); + +form({ + name: "a", + value: new File([], "b\rc"), + expected: "a=b%0D%0Ac", + description: "\\r in filename", +}); + +form({ + name: "a", + value: new File([], "b\r\nc"), + expected: "a=b%0D%0Ac", + description: "\\r\\n in filename", +}); + +form({ + name: "a", + value: new File([], "b\n\rc"), + expected: "a=b%0D%0A%0D%0Ac", + description: "\\n\\r in filename", +}); + +form({ + name: 'a"b', + value: "c", + expected: "a%22b=c", + description: "double quote in name", +}); + +form({ + name: "a", + value: 'b"c', + expected: "a=b%22c", + description: "double quote in value", +}); + +form({ + name: "a", + value: new File([], 'b"c'), + expected: "a=b%22c", + description: "double quote in filename", +}); + +form({ + name: "a'b", + value: "c", + expected: "a%27b=c", + description: "single quote in name", +}); + +form({ + name: "a", + value: "b'c", + expected: "a=b%27c", + description: "single quote in value", +}); + +form({ + name: "a", + value: new File([], "b'c"), + expected: "a=b%27c", + description: "single quote in filename", +}); + +form({ + name: "a\\b", + value: "c", + expected: "a%5Cb=c", + description: "backslash in name", +}); + +form({ + name: "a", + value: "b\\c", + expected: "a=b%5Cc", + description: "backslash in value", +}); + +form({ + name: "a", + value: new File([], "b\\c"), + expected: "a=b%5Cc", + description: "backslash in filename", +}); + +form({ + name: "áb", + value: "ç", + expected: "%C3%A1b=%C3%A7", + description: "non-ASCII in name and value", +}); + +form({ + name: "a", + value: new File([], "ə.txt"), + expected: "a=%C9%99.txt", + description: "non-ASCII in filename", +}); + +form({ + name: "aəb", + value: "c\uFFFDd", + formEncoding: "windows-1252", + expected: "a%26%23601%3Bb=c%26%2365533%3Bd", + description: "characters not in encoding in name and value", +}); + +form({ + name: "á", + value: new File([], "💩"), + formEncoding: "windows-1252", + expected: "%E1=%26%23128169%3B", + description: "character not in encoding in filename", +}); + +function form({ + name, + value, + expected, + formEncoding = "utf-8", + description, +}) { + // Normal form + promise_test(async (testCase) => { + if (document.readyState !== "complete") { + await new Promise((resolve) => addEventListener("load", resolve)); + } + + const formTargetFrame = Object.assign(document.createElement("iframe"), { + name: "formtargetframe", + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + + const form = Object.assign(document.createElement("form"), { + acceptCharset: formEncoding, + // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py + // to work around WebKit not percent-encoding \x00 (which causes the + // response to be detected as a binary file and served as a download). + // The output should not change if the urlencoded serializer is correct. + action: "/FileAPI/file/resources/echo-content-escaped.py", + method: "POST", + enctype: "application/x-www-form-urlencoded", + target: formTargetFrame.name, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + const input = document.createElement("input"); + input.name = name; + if (value instanceof File) { + input.type = "file"; + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(value); + input.files = dataTransfer.files; + } else { + input.type = "hidden"; + input.value = value; + } + form.append(input); + + await new Promise((resolve) => { + form.submit(); + formTargetFrame.onload = resolve; + }); + + const urlencoded = formTargetFrame.contentDocument.body.textContent; + assert_equals(unescape(urlencoded), expected); + }, `application/x-www-form-urlencoded: ${description} (normal form)`); + + // formdata event + promise_test(async (testCase) => { + if (document.readyState !== "complete") { + await new Promise((resolve) => addEventListener("load", resolve)); + } + + const formTargetFrame = Object.assign(document.createElement("iframe"), { + name: "formtargetframe", + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + + const form = Object.assign(document.createElement("form"), { + acceptCharset: formEncoding, + // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py + // to work around WebKit not percent-encoding \x00 (which causes the + // response to be detected as a binary file and served as a download). + // The output should not change if the urlencoded serializer is correct. + action: "/FileAPI/file/resources/echo-content-escaped.py", + method: "POST", + enctype: "application/x-www-form-urlencoded", + target: formTargetFrame.name, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + form.addEventListener("formdata", (evt) => { + evt.formData.append(name, value); + }); + + await new Promise((resolve) => { + form.submit(); + formTargetFrame.onload = resolve; + }); + + const urlencoded = formTargetFrame.contentDocument.body.textContent; + assert_equals(unescape(urlencoded), expected); + }, `application/x-www-form-urlencoded: ${description} (formdata event)`); +} + +function unescape(str) { + return str.replace(/\r\n?|\n/g, "\r\n").replace( + /\\x[0-9A-Fa-f]{2}/g, + (escape) => String.fromCodePoint(parseInt(escape.substring(2), 16)), + ).replace(/\\\\/g, "\\"); +} diff --git a/url/urlencoded-filenames.window.js b/url/urlencoded-filenames.window.js deleted file mode 100644 index 31eb11ff850b32..00000000000000 --- a/url/urlencoded-filenames.window.js +++ /dev/null @@ -1,122 +0,0 @@ -form({ - filename: "basic-test.txt", - expectedFilename: "basic-test.txt", - description: "Basic test", -}); - -form({ - filename: "a\0b", - expectedFilename: "a%00b", - description: "Controls: 0x00", -}); - -form({ - filename: "a\nb", - expectedFilename: "a%0D%0Ab", - description: "Newlines: \\n becomes \\r\\n", -}); - -form({ - filename: "a\rb", - expectedFilename: "a%0D%0Ab", - description: "Newlines: \\r becomes \\r\\n", -}); - -form({ - filename: "a\n\rb", - expectedFilename: "a%0D%0A%0D%0Ab", - description: "Newlines: \\n\\r becomes \\r\\n\\r\\n", -}); - -form({ - filename: "a\r\nb", - expectedFilename: "a%0D%0Ab", - description: "Newlines: \\r\\n stays unchanged", -}); - -form({ - filename: 'a"b', - expectedFilename: "a%22b", - description: "Special punctuation: double quote", -}); - -form({ - filename: "a'b", - expectedFilename: "a%27b", - description: "Special punctuation: single quote", -}); - -form({ - filename: "a\\b", - expectedFilename: "a%5Cb", - description: "Special punctuation: backslash", -}); - -form({ - filename: "ábc", - expectedFilename: "%C3%A1bc", - description: "Non-ASCII", -}); - -form({ - filename: "a\uFFFDb", - expectedFilename: "a%26%2365533%3Bb", - formEncoding: "windows-1252", - description: "Character not in encoding", -}); - -function form({ - filename, - expectedFilename, - formEncoding = "utf-8", - description, -}) { - promise_test(async (testCase) => { - if (document.readyState !== "complete") { - await new Promise((resolve) => addEventListener("load", resolve)); - } - - const formTargetFrame = Object.assign(document.createElement("iframe"), { - name: "formtargetframe", - }); - document.body.append(formTargetFrame); - testCase.add_cleanup(() => { - document.body.removeChild(formTargetFrame); - }); - - const form = Object.assign(document.createElement("form"), { - acceptCharset: formEncoding, - // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py - // to work around WebKit not percent-encoding \x00 (which causes the - // response to be detected as a binary file and served as a download). - // The output should not change if the urlencoded serializer is correct. - action: "/FileAPI/file/resources/echo-content-escaped.py", - method: "POST", - enctype: "application/x-www-form-urlencoded", - target: formTargetFrame.name, - }); - document.body.append(form); - testCase.add_cleanup(() => { - document.body.removeChild(form); - }); - - const fileInput = Object.assign(document.createElement("input"), { - type: "file", - name: "file", - }); - form.append(fileInput); - - await new Promise((resolve) => { - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(new File([], filename, { type: "text/plain" })); - fileInput.files = dataTransfer.files; - - form.submit(); - formTargetFrame.onload = resolve; - }); - - const urlencoded = formTargetFrame.contentDocument.body.textContent; - const expected = `file=${expectedFilename}`; - assert_equals(urlencoded, expected); - }, `Test urlencoding of filenames: ${description}`); -} diff --git a/url/urlsearchparams-stringifier.any.js b/url/urlsearchparams-stringifier.any.js index bc7bedd533f58d..6187db64b1747d 100644 --- a/url/urlsearchparams-stringifier.any.js +++ b/url/urlsearchparams-stringifier.any.js @@ -133,3 +133,13 @@ test(() => { assert_equals(url.toString(), 'http://www.example.com/?a=b%2Cc&x=y'); assert_equals(params.toString(), 'a=b%2Cc&x=y'); }, 'URLSearchParams connected to URL'); + +test(() => { + const url = new URL('http://www.example.com/'); + const params = url.searchParams; + + params.append('a\nb', 'c\rd'); + params.append('e\n\rf', 'g\r\nh'); + + assert_equals(params.toString(), "a%0Ab=c%0Dd&e%0A%0Df=g%0D%0Ah"); +}, 'URLSearchParams must not do newline normalization'); From 4493bdce75f5b0d194cddccba52d3c995d50102e Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Thu, 14 Jan 2021 15:53:06 +0100 Subject: [PATCH 08/13] Add changes to the FACE tests. --- .../ElementInternals-setFormValue.html | 408 +++++++++++++++--- 1 file changed, 345 insertions(+), 63 deletions(-) diff --git a/custom-elements/form-associated/ElementInternals-setFormValue.html b/custom-elements/form-associated/ElementInternals-setFormValue.html index 66ff8841a6966c..8c7f9b6768a113 100644 --- a/custom-elements/form-associated/ElementInternals-setFormValue.html +++ b/custom-elements/form-associated/ElementInternals-setFormValue.html @@ -1,4 +1,5 @@ +
@@ -30,32 +31,85 @@ customElements.define('my-control', MyControl); const $ = document.querySelector.bind(document); -function submitPromise(t) { +function submitPromise(t, extractFromIframe) { + if (!extractFromIframe) { + extractFromIframe = (iframe) => iframe.contentWindow.location.search; + } return new Promise((resolve, reject) => { const iframe = $('iframe'); - iframe.onload = () => resolve(iframe.contentWindow.location.search); + iframe.onload = () => resolve(extractFromIframe(iframe)); iframe.onerror = () => reject(new Error('iframe onerror fired')); $('form').submit(); }); } -function testSerializedEntry({name, expectedName, value, expectedValue, description}) { - promise_test(async t => { - $('#container').innerHTML = '
' + - '' + - '
' + - ''; - if (name !== undefined) { - $('my-control').setAttribute("name", name); - } - if (Array.isArray(value)) { - $('my-control').setValues(value); - } else { - $('my-control').value = value; - } - const query = await submitPromise(t); - assert_equals(query, `?${expectedName}=${expectedValue}`); - }, description); +function testSerializedEntry({name, value, expected, description}) { + // urlencoded + { + const {name: expectedName, value: expectedValue} = expected.urlencoded; + promise_test(async t => { + $('#container').innerHTML = '
' + + '' + + '
' + + ''; + if (name !== undefined) { + $('my-control').setAttribute("name", name); + } + if (Array.isArray(value)) { + $('my-control').setValues(value); + } else { + $('my-control').value = value; + } + const query = await submitPromise(t); + assert_equals(query, `?${expectedName}=${expectedValue}`); + }, `${description} (urlencoded)`); + } + + // formdata + { + const {name: expectedName, filename: expectedFilename, value: expectedValue} = expected.formdata; + promise_test(async t => { + $('#container').innerHTML = + '
' + + '' + + '
' + + ''; + if (name !== undefined) { + $('my-control').setAttribute("name", name); + } + if (Array.isArray(value)) { + $('my-control').setValues(value); + } else { + $('my-control').value = value; + } + const escaped = await submitPromise(t, iframe => iframe.contentDocument.body.textContent); + const formdata = escaped + .replace(/\r\n?|\n/g, "\r\n") + .replace( + /\\x[0-9A-Fa-f]{2}/g, + escape => String.fromCodePoint(parseInt(escape.substring(2), 16)) + ); + const boundary = formdata.split("\r\n")[0]; + const expected = [ + boundary, + ...(() => { + if (expectedFilename === undefined) { + return [`Content-Disposition: form-data; name="${expectedName}"`]; + } else { + return [ + `Content-Disposition: form-data; name="${expectedName}"; filename="${expectedFilename}"`, + "Content-Type: text/plain" + ]; + } + })(), + "", + expectedValue, + boundary + "--", + "" + ].join("\r\n"); + assert_equals(formdata, expected); + }, `${description} (formdata)`); + } } promise_test(t => { @@ -145,152 +199,380 @@ testSerializedEntry({ name: 'a\nb', value: 'c', - expectedName: 'a%0D%0Ab', - expectedValue: 'c', + expected: { + urlencoded: { + name: 'a%0D%0Ab', + value: 'c' + }, + formdata: { + name: 'a%0D%0Ab', + value: 'c' + } + }, description: 'Newline normalization - \\n in name' }); testSerializedEntry({ name: 'a\rb', value: 'c', - expectedName: 'a%0D%0Ab', - expectedValue: 'c', + expected: { + urlencoded: { + name: 'a%0D%0Ab', + value: 'c' + }, + formdata: { + name: 'a%0D%0Ab', + value: 'c' + } + }, description: 'Newline normalization - \\r in name' }); testSerializedEntry({ name: 'a\r\nb', value: 'c', - expectedName: 'a%0D%0Ab', - expectedValue: 'c', + expected: { + urlencoded: { + name: 'a%0D%0Ab', + value: 'c' + }, + formdata: { + name: 'a%0D%0Ab', + value: 'c' + } + }, description: 'Newline normalization - \\r\\n in name' }); testSerializedEntry({ name: 'a\n\rb', value: 'c', - expectedName: 'a%0D%0A%0D%0Ab', - expectedValue: 'c', + expected: { + urlencoded: { + name: 'a%0D%0A%0D%0Ab', + value: 'c' + }, + formdata: { + name: 'a%0D%0A%0D%0Ab', + value: 'c' + } + }, description: 'Newline normalization - \\n\\r in name' }); testSerializedEntry({ name: 'a', value: 'b\nc', - expectedName: 'a', - expectedValue: 'b%0D%0Ac', + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + value: 'b\r\nc' + } + }, description: 'Newline normalization - \\n in value' }); testSerializedEntry({ name: 'a', value: 'b\rc', - expectedName: 'a', - expectedValue: 'b%0D%0Ac', + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + value: 'b\r\nc' + } + }, description: 'Newline normalization - \\r in value' }); testSerializedEntry({ name: 'a', value: 'b\r\nc', - expectedName: 'a', - expectedValue: 'b%0D%0Ac', + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + value: 'b\r\nc' + } + }, description: 'Newline normalization - \\r\\n in value' }); testSerializedEntry({ name: 'a', value: 'b\n\rc', - expectedName: 'a', - expectedValue: 'b%0D%0A%0D%0Ac', + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0A%0D%0Ac' + }, + formdata: { + name: 'a', + value: 'b\r\n\r\nc' + } + }, description: 'Newline normalization - \\n\\r in value' }); testSerializedEntry({ name: 'a', - value: new File([], "b\nc"), - expectedName: 'a', - expectedValue: 'b%0D%0Ac', + value: new File([], "b\nc", {type: "text/plain"}), + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + filename: 'b%0Ac', + value: '' + } + }, description: 'Newline normalization - \\n in filename' }); testSerializedEntry({ name: 'a', - value: new File([], "b\rc"), - expectedName: 'a', - expectedValue: 'b%0D%0Ac', + value: new File([], "b\rc", {type: "text/plain"}), + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + filename: 'b%0Dc', + value: '' + } + }, description: 'Newline normalization - \\r in filename' }); testSerializedEntry({ name: 'a', - value: new File([], "b\r\nc"), - expectedName: 'a', - expectedValue: 'b%0D%0Ac', + value: new File([], "b\r\nc", {type: "text/plain"}), + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + filename: 'b%0D%0Ac', + value: '' + } + }, description: 'Newline normalization - \\r\\n in filename' }); testSerializedEntry({ name: 'a', - value: new File([], "b\n\rc"), - expectedName: 'a', - expectedValue: 'b%0D%0A%0D%0Ac', + value: new File([], "b\n\rc", {type: "text/plain"}), + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0A%0D%0Ac' + }, + formdata: { + name: 'a', + filename: 'b%0A%0Dc', + value: '' + } + }, description: 'Newline normalization - \\n\\r in filename' }); testSerializedEntry({ value: [['a\nb', 'c']], - expectedName: 'a%0Ab', - expectedValue: 'c', + expected: { + urlencoded: { + name: 'a%0D%0Ab', + value: 'c' + }, + formdata: { + name: 'a%0D%0Ab', + value: 'c' + } + }, description: 'Newline normalization - \\n in FormData name' }); testSerializedEntry({ value: [['a\rb', 'c']], - expectedName: 'a%0Db', - expectedValue: 'c', + expected: { + urlencoded: { + name: 'a%0D%0Ab', + value: 'c' + }, + formdata: { + name: 'a%0D%0Ab', + value: 'c' + } + }, description: 'Newline normalization - \\r in FormData name' }); testSerializedEntry({ value: [['a\r\nb', 'c']], - expectedName: 'a%0D%0Ab', - expectedValue: 'c', + expected: { + urlencoded: { + name: 'a%0D%0Ab', + value: 'c' + }, + formdata: { + name: 'a%0D%0Ab', + value: 'c' + } + }, description: 'Newline normalization - \\r\\n in FormData name' }); testSerializedEntry({ value: [['a\n\rb', 'c']], - expectedName: 'a%0A%0Db', - expectedValue: 'c', + expected: { + urlencoded: { + name: 'a%0D%0A%0D%0Ab', + value: 'c' + }, + formdata: { + name: 'a%0D%0A%0D%0Ab', + value: 'c' + } + }, description: 'Newline normalization - \\n\\r in FormData name' }); testSerializedEntry({ value: [['a', 'b\nc']], - expectedName: 'a', - expectedValue: 'b%0Ac', + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + value: 'b\r\nc' + } + }, description: 'Newline normalization - \\n in FormData value' }); testSerializedEntry({ value: [['a', 'b\rc']], - expectedName: 'a', - expectedValue: 'b%0Dc', + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + value: 'b\r\nc' + } + }, description: 'Newline normalization - \\r in FormData value' }); testSerializedEntry({ value: [['a', 'b\r\nc']], - expectedName: 'a', - expectedValue: 'b%0D%0Ac', + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + value: 'b\r\nc' + } + }, description: 'Newline normalization - \\r\\n in FormData value' }); testSerializedEntry({ value: [['a', 'b\n\rc']], - expectedName: 'a', - expectedValue: 'b%0A%0Dc', + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0A%0D%0Ac' + }, + formdata: { + name: 'a', + value: 'b\r\n\r\nc' + } + }, description: 'Newline normalization - \\n\\r in FormData value' }); + +testSerializedEntry({ + value: [['a', new File([], 'b\nc', {type: "text/plain"})]], + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + filename: 'b%0Ac', + value: '' + } + }, + description: 'Newline normalization - \\n in FormData filename' +}); + +testSerializedEntry({ + value: [['a', new File([], 'b\rc', {type: "text/plain"})]], + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + filename: 'b%0Dc', + value: '' + } + }, + description: 'Newline normalization - \\r in FormData filename' +}); + +testSerializedEntry({ + value: [['a', new File([], 'b\r\nc', {type: "text/plain"})]], + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0Ac' + }, + formdata: { + name: 'a', + filename: 'b%0D%0Ac', + value: '' + } + }, + description: 'Newline normalization - \\r\\n in FormData filename' +}); + +testSerializedEntry({ + value: [['a', new File([], 'b\n\rc', {type: "text/plain"})]], + expected: { + urlencoded: { + name: 'a', + value: 'b%0D%0A%0D%0Ac' + }, + formdata: { + name: 'a', + filename: 'b%0A%0Dc', + value: '' + } + }, + description: 'Newline normalization - \\n\\r in FormData filename' +}); From 7d8607f60bc25716ce1b445bd0a6a92fd61d2cab Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Thu, 14 Jan 2021 16:02:38 +0100 Subject: [PATCH 09/13] fix lint --- .../form-associated/ElementInternals-setFormValue.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom-elements/form-associated/ElementInternals-setFormValue.html b/custom-elements/form-associated/ElementInternals-setFormValue.html index 8c7f9b6768a113..38a2590beb48dc 100644 --- a/custom-elements/form-associated/ElementInternals-setFormValue.html +++ b/custom-elements/form-associated/ElementInternals-setFormValue.html @@ -64,7 +64,7 @@ assert_equals(query, `?${expectedName}=${expectedValue}`); }, `${description} (urlencoded)`); } - + // formdata { const {name: expectedName, filename: expectedFilename, value: expectedValue} = expected.formdata; From 5cb5f11fd89476134eb5649ea58471dfcb3694ce Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Thu, 14 Jan 2021 16:12:55 +0100 Subject: [PATCH 10/13] Change the FormData tests. --- ...ls.tentative.html => send-file-formdata-controls.html} | 6 +----- ...tentative.html => send-file-formdata-punctuation.html} | 6 +----- FileAPI/support/send-file-formdata-helper.js | 8 +++++--- 3 files changed, 7 insertions(+), 13 deletions(-) rename FileAPI/file/{send-file-formdata-controls.tentative.html => send-file-formdata-controls.html} (92%) rename FileAPI/file/{send-file-formdata-punctuation.tentative.html => send-file-formdata-punctuation.html} (95%) diff --git a/FileAPI/file/send-file-formdata-controls.tentative.html b/FileAPI/file/send-file-formdata-controls.html similarity index 92% rename from FileAPI/file/send-file-formdata-controls.tentative.html rename to FileAPI/file/send-file-formdata-controls.html index 4259741b63ef31..2982b13be70722 100644 --- a/FileAPI/file/send-file-formdata-controls.tentative.html +++ b/FileAPI/file/send-file-formdata-controls.html @@ -1,10 +1,6 @@ -FormData: Upload files named using controls (tentative) - +FormData: Upload files named using controls -FormData: Upload files named using punctuation (tentative) - +FormData: Upload files named using punctuation Date: Fri, 15 Jan 2021 11:59:29 +0100 Subject: [PATCH 11/13] Add some missing tests related to the formdata event and multipart/form-data. --- .../forms/form-submission-0/README.md | 3 - .../constructing-form-data-set.html | 11 + .../multipart-formdata.window.js | 444 ++++++++++++++++++ .../form-submission-0/text-plain.window.js | 8 +- .../form-submission-0/urlencoded2.window.js | 8 +- 5 files changed, 463 insertions(+), 11 deletions(-) delete mode 100644 html/semantics/forms/form-submission-0/README.md create mode 100644 html/semantics/forms/form-submission-0/multipart-formdata.window.js diff --git a/html/semantics/forms/form-submission-0/README.md b/html/semantics/forms/form-submission-0/README.md deleted file mode 100644 index 8dcbddf820bc0f..00000000000000 --- a/html/semantics/forms/form-submission-0/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Form submissions involving files are also tested in: - -- `/FileAPI/file/send-file*` (for `multipart/form-data`) diff --git a/html/semantics/forms/form-submission-0/constructing-form-data-set.html b/html/semantics/forms/form-submission-0/constructing-form-data-set.html index b27f1996d99cb8..a6c5e91554d28f 100644 --- a/html/semantics/forms/form-submission-0/constructing-form-data-set.html +++ b/html/semantics/forms/form-submission-0/constructing-form-data-set.html @@ -114,6 +114,17 @@ form.submit(); }); +test(() => { + const form = populateForm(''); + form.addEventListener('formdata', e => { + e.formData.append('a\nb', 'c\rd'); + }); + const formData = new FormData(form); + const [name, value] = [...formData][0]; + assert_equals(name, 'a\nb'); + assert_equals(value, 'c\rd'); +}, 'Entries added to the "formdata" IDL attribute shouldn\'t be newline normalized in the resulting FormData'); + test(() => { let form = populateForm(''); let formDataInEvent = null; diff --git a/html/semantics/forms/form-submission-0/multipart-formdata.window.js b/html/semantics/forms/form-submission-0/multipart-formdata.window.js new file mode 100644 index 00000000000000..9c5b3ef8d4c91c --- /dev/null +++ b/html/semantics/forms/form-submission-0/multipart-formdata.window.js @@ -0,0 +1,444 @@ +// Form submissions in multipart/form-data are also tested in +// /FileAPI/file/send-file* + +form({ + name: "basic", + value: "test", + expected: { + name: "basic", + value: "test", + }, + description: "Basic test", +}); + +form({ + name: "basic", + value: new File([], "file-test.txt", { type: "text/plain" }), + expected: { + name: "basic", + filename: "file-test.txt", + value: "", + }, + description: "Basic File test", +}); + +form({ + name: "a\0b", + value: "c", + expected: { + name: "a\0b", + value: "c", + }, + description: "0x00 in name", +}); + +form({ + name: "a", + value: "b\0c", + expected: { + name: "a", + value: "b\0c", + }, + description: "0x00 in value", +}); + +form({ + name: "a", + value: new File([], "b\0c", { type: "text/plain" }), + expected: { + name: "a", + filename: "b\0c", + value: "", + }, + description: "0x00 in filename", +}); + +form({ + name: "a\nb", + value: "c", + expected: { + name: "a%0D%0Ab", + value: "c", + }, + description: "\\n in name", +}); + +form({ + name: "a\rb", + value: "c", + expected: { + name: "a%0D%0Ab", + value: "c", + }, + description: "\\r in name", +}); + +form({ + name: "a\r\nb", + value: "c", + expected: { + name: "a%0D%0Ab", + value: "c", + }, + description: "\\r\\n in name", +}); + +form({ + name: "a\n\rb", + value: "c", + expected: { + name: "a%0D%0A%0D%0Ab", + value: "c", + }, + description: "\\n\\r in name", +}); + +form({ + name: "a", + value: "b\nc", + expected: { + name: "a", + value: "b\r\nc", + }, + description: "\\n in value", +}); + +form({ + name: "a", + value: "b\rc", + expected: { + name: "a", + value: "b\r\nc", + }, + description: "\\r in value", +}); + +form({ + name: "a", + value: "b\r\nc", + expected: { + name: "a", + value: "b\r\nc", + }, + description: "\\r\\n in value", +}); + +form({ + name: "a", + value: "b\n\rc", + expected: { + name: "a", + value: "b\r\n\r\nc", + }, + description: "\\n\\r in value", +}); + +form({ + name: "a", + value: new File([], "b\nc", { type: "text/plain" }), + expected: { + name: "a", + filename: "b%0Ac", + value: "", + }, + description: "\\n in filename", +}); + +form({ + name: "a", + value: new File([], "b\rc", { type: "text/plain" }), + expected: { + name: "a", + filename: "b%0Dc", + value: "", + }, + description: "\\r in filename", +}); + +form({ + name: "a", + value: new File([], "b\r\nc", { type: "text/plain" }), + expected: { + name: "a", + filename: "b%0D%0Ac", + value: "", + }, + description: "\\r\\n in filename", +}); + +form({ + name: "a", + value: new File([], "b\n\rc", { type: "text/plain" }), + expected: { + name: "a", + filename: "b%0A%0Dc", + value: "", + }, + description: "\\n\\r in filename", +}); + +form({ + name: 'a"b', + value: "c", + expected: { + name: "a%22b", + value: "c", + }, + description: "double quote in name", +}); + +form({ + name: "a", + value: 'b"c', + expected: { + name: "a", + value: 'b"c', + }, + description: "double quote in value", +}); + +form({ + name: "a", + value: new File([], 'b"c', { type: "text/plain" }), + expected: { + name: "a", + filename: "b%22c", + value: "", + }, + description: "double quote in filename", +}); + +form({ + name: "a'b", + value: "c", + expected: { + name: "a'b", + value: "c", + }, + description: "single quote in name", +}); + +form({ + name: "a", + value: "b'c", + expected: { + name: "a", + value: "b'c", + }, + description: "single quote in value", +}); + +form({ + name: "a", + value: new File([], "b'c", { type: "text/plain" }), + expected: { + name: "a", + filename: "b'c", + value: "", + }, + description: "single quote in filename", +}); + +form({ + name: "a\\b", + value: "c", + expected: { + name: "a\\b", + value: "c", + }, + description: "backslash in name", +}); + +form({ + name: "a", + value: "b\\c", + expected: { + name: "a", + value: "b\\c", + }, + description: "backslash in value", +}); + +form({ + name: "a", + value: new File([], "b\\c", { type: "text/plain" }), + expected: { + name: "a", + filename: "b\\c", + value: "", + }, + description: "backslash in filename", +}); + +form({ + name: "áb", + value: "ç", + expected: { + name: "\xC3\xA1b", + value: "\xC3\xA7", + }, + description: "non-ASCII in name and value", +}); + +form({ + name: "a", + value: new File([], "ə.txt", { type: "text/plain" }), + expected: { + name: "a", + filename: "\xC9\x99.txt", + value: "", + }, + description: "non-ASCII in filename", +}); + +form({ + name: "aəb", + value: "c\uFFFDd", + formEncoding: "windows-1252", + expected: { + name: "aəb", + value: "c�d", + }, + description: "characters not in encoding in name and value", +}); + +form({ + name: "á", + value: new File([], "💩", { type: "text/plain" }), + formEncoding: "windows-1252", + expected: { + name: "\xE1", + filename: "💩", + value: "", + }, + description: "character not in encoding in filename", +}); + +function form({ name, value, expected, formEncoding = "utf-8", description }) { + // Normal form + promise_test(async (testCase) => { + if (document.readyState !== "complete") { + await new Promise((resolve) => addEventListener("load", resolve)); + } + + const formTargetFrame = Object.assign(document.createElement("iframe"), { + name: "formtargetframe", + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + + const form = Object.assign(document.createElement("form"), { + acceptCharset: formEncoding, + // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py + // because we're doing tests with \x00, which can cause the response to be + // detected as a binary file and served as a download). + action: "/FileAPI/file/resources/echo-content-escaped.py", + method: "POST", + enctype: "multipart/form-data", + target: formTargetFrame.name, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + const input = document.createElement("input"); + input.name = name; + if (value instanceof File) { + input.type = "file"; + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(value); + input.files = dataTransfer.files; + } else { + input.type = "hidden"; + input.value = value; + } + form.append(input); + + await new Promise((resolve) => { + form.submit(); + formTargetFrame.onload = resolve; + }); + + const serialized = unescape( + formTargetFrame.contentDocument.body.textContent, + ); + const boundary = serialized.split("\r\n")[0]; + assert_equals(serialized, expectedPayload(expected, boundary)); + }, `multipart/form-data: ${description} (normal form)`); + + // formdata event + promise_test(async (testCase) => { + if (document.readyState !== "complete") { + await new Promise((resolve) => addEventListener("load", resolve)); + } + + const formTargetFrame = Object.assign(document.createElement("iframe"), { + name: "formtargetframe", + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + + const form = Object.assign(document.createElement("form"), { + acceptCharset: formEncoding, + // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py + // because we're doing tests with \x00, which can cause the response to be + // detected as a binary file and served as a download). + action: "/FileAPI/file/resources/echo-content-escaped.py", + method: "POST", + enctype: "multipart/form-data", + target: formTargetFrame.name, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + form.addEventListener("formdata", (evt) => { + evt.formData.append(name, value); + }); + + await new Promise((resolve) => { + form.submit(); + formTargetFrame.onload = resolve; + }); + + const serialized = unescape( + formTargetFrame.contentDocument.body.textContent, + ); + const boundary = serialized.split("\r\n")[0]; + assert_equals(serialized, expectedPayload(expected, boundary)); + }, `multipart/form-data: ${description} (formdata event)`); +} + +function unescape(str) { + return str.replace(/\r\n?|\n/g, "\r\n").replace( + /\\x[0-9A-Fa-f]{2}/g, + (escape) => String.fromCodePoint(parseInt(escape.substring(2), 16)), + ).replace(/\\\\/g, "\\"); +} + +function expectedPayload({ name, filename, value }, boundary) { + let headers; + if (filename === undefined) { + headers = [`Content-Disposition: form-data; name="${name}"`]; + } else { + headers = [ + `Content-Disposition: form-data; name="${name}"; filename="${filename}"`, + "Content-Type: text/plain", + ]; + } + + return [ + boundary, + ...headers, + "", + value, + boundary + "--", + "", + ].join("\r\n"); +} diff --git a/html/semantics/forms/form-submission-0/text-plain.window.js b/html/semantics/forms/form-submission-0/text-plain.window.js index 37c2d6e0f38211..b68bec8cda9511 100644 --- a/html/semantics/forms/form-submission-0/text-plain.window.js +++ b/html/semantics/forms/form-submission-0/text-plain.window.js @@ -264,8 +264,8 @@ function form({ formTargetFrame.onload = resolve; }); - const urlencoded = formTargetFrame.contentDocument.body.textContent; - assert_equals(unescape(urlencoded), expected); + const serialized = formTargetFrame.contentDocument.body.textContent; + assert_equals(unescape(serialized), expected); }, `text/plain: ${description} (normal form)`); // formdata event @@ -306,8 +306,8 @@ function form({ formTargetFrame.onload = resolve; }); - const urlencoded = formTargetFrame.contentDocument.body.textContent; - assert_equals(unescape(urlencoded), expected); + const serialized = formTargetFrame.contentDocument.body.textContent; + assert_equals(unescape(serialized), expected); }, `text/plain: ${description} (formdata event)`); } diff --git a/html/semantics/forms/form-submission-0/urlencoded2.window.js b/html/semantics/forms/form-submission-0/urlencoded2.window.js index ee0ad67180b5f2..0c20b2c2e4e2bd 100644 --- a/html/semantics/forms/form-submission-0/urlencoded2.window.js +++ b/html/semantics/forms/form-submission-0/urlencoded2.window.js @@ -265,8 +265,8 @@ function form({ formTargetFrame.onload = resolve; }); - const urlencoded = formTargetFrame.contentDocument.body.textContent; - assert_equals(unescape(urlencoded), expected); + const serialized = formTargetFrame.contentDocument.body.textContent; + assert_equals(unescape(serialized), expected); }, `application/x-www-form-urlencoded: ${description} (normal form)`); // formdata event @@ -308,8 +308,8 @@ function form({ formTargetFrame.onload = resolve; }); - const urlencoded = formTargetFrame.contentDocument.body.textContent; - assert_equals(unescape(urlencoded), expected); + const serialized = formTargetFrame.contentDocument.body.textContent; + assert_equals(unescape(serialized), expected); }, `application/x-www-form-urlencoded: ${description} (formdata event)`); } From fd2f907c2e1a97b8ca35cc69eb17c0d4cf32c9f0 Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Fri, 9 Apr 2021 12:23:54 +0200 Subject: [PATCH 12/13] Move shared code to a separate file. --- .../form-submission-0/enctypes-helper.js | 125 ++++++++++++++ .../multipart-formdata.window.js | 160 ++++-------------- .../form-submission-0/text-plain.window.js | 115 +------------ .../form-submission-0/urlencoded2.window.js | 117 +------------ 4 files changed, 170 insertions(+), 347 deletions(-) create mode 100644 html/semantics/forms/form-submission-0/enctypes-helper.js diff --git a/html/semantics/forms/form-submission-0/enctypes-helper.js b/html/semantics/forms/form-submission-0/enctypes-helper.js new file mode 100644 index 00000000000000..c22e50230089e4 --- /dev/null +++ b/html/semantics/forms/form-submission-0/enctypes-helper.js @@ -0,0 +1,125 @@ +(() => { + // Using echo-content-escaped.py rather than + // /fetch/api/resources/echo-content.py to work around WebKit not + // percent-encoding \x00, which causes the response to be detected as + // a binary file and served as a download. + const ACTION_URL = "/FileAPI/file/resources/echo-content-escaped.py"; + + const IFRAME_NAME = "formtargetframe"; + + // Undoes the escapes from /fetch/api/resources/echo-content.py + function unescape(str) { + return str + .replace(/\r\n?|\n/g, "\r\n") + .replace( + /\\x[0-9A-Fa-f]{2}/g, + (escape) => String.fromCodePoint(parseInt(escape.substring(2), 16)), + ) + .replace(/\\\\/g, "\\"); + } + + async function formSubmissionTest({ + name, + value, + expectedBuilder, + enctype, + formEncoding, + testFormData = false, + testCase, + }) { + if (document.readyState !== "complete") { + await new Promise((resolve) => addEventListener("load", resolve)); + } + + const formTargetFrame = Object.assign(document.createElement("iframe"), { + name: IFRAME_NAME, + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + + const form = Object.assign(document.createElement("form"), { + acceptCharset: formEncoding, + action: ACTION_URL, + method: "POST", + enctype, + target: IFRAME_NAME, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + if (!testFormData) { + const input = document.createElement("input"); + input.name = name; + if (value instanceof File) { + input.type = "file"; + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(value); + input.files = dataTransfer.files; + } else { + input.type = "hidden"; + input.value = value; + } + form.append(input); + } else { + form.addEventListener("formdata", (evt) => { + evt.formData.append(name, value); + }); + } + + await new Promise((resolve) => { + form.submit(); + formTargetFrame.onload = resolve; + }); + + const serialized = unescape( + formTargetFrame.contentDocument.body.textContent, + ); + const expected = expectedBuilder(serialized); + assert_equals(serialized, expected); + } + + window.formSubmissionTemplate = (enctype, expectedBuilder) => { + function form({ + name, + value, + expected, + formEncoding = "utf-8", + description, + }) { + const commonParams = { + name, + value, + expectedBuilder: expectedBuilder.bind(null, expected), + enctype, + formEncoding, + }; + + // Normal form + promise_test( + (testCase) => + formSubmissionTest({ + ...commonParams, + testCase, + }), + `${enctype}: ${description} (normal form)`, + ); + + // formdata event + promise_test( + (testCase) => + formSubmissionTest({ + ...commonParams, + testFormData: true, + testCase, + }), + `${enctype}: ${description} (formdata event)`, + ); + } + + return form; + }; +})(); diff --git a/html/semantics/forms/form-submission-0/multipart-formdata.window.js b/html/semantics/forms/form-submission-0/multipart-formdata.window.js index 9c5b3ef8d4c91c..bed3d06a2076b8 100644 --- a/html/semantics/forms/form-submission-0/multipart-formdata.window.js +++ b/html/semantics/forms/form-submission-0/multipart-formdata.window.js @@ -1,6 +1,37 @@ +// META: script=enctypes-helper.js + // Form submissions in multipart/form-data are also tested in // /FileAPI/file/send-file* +function expectedPayload({ name, filename, value }, boundary) { + let headers; + if (filename === undefined) { + headers = [`Content-Disposition: form-data; name="${name}"`]; + } else { + headers = [ + `Content-Disposition: form-data; name="${name}"; filename="${filename}"`, + "Content-Type: text/plain", + ]; + } + + return [ + boundary, + ...headers, + "", + value, + boundary + "--", + "", + ].join("\r\n"); +} + +const form = formSubmissionTemplate( + "multipart/form-data", + (expected, serialized) => { + const boundary = serialized.split("\r\n")[0]; + return expectedPayload(expected, boundary); + }, +); + form({ name: "basic", value: "test", @@ -313,132 +344,3 @@ form({ }, description: "character not in encoding in filename", }); - -function form({ name, value, expected, formEncoding = "utf-8", description }) { - // Normal form - promise_test(async (testCase) => { - if (document.readyState !== "complete") { - await new Promise((resolve) => addEventListener("load", resolve)); - } - - const formTargetFrame = Object.assign(document.createElement("iframe"), { - name: "formtargetframe", - }); - document.body.append(formTargetFrame); - testCase.add_cleanup(() => { - document.body.removeChild(formTargetFrame); - }); - - const form = Object.assign(document.createElement("form"), { - acceptCharset: formEncoding, - // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py - // because we're doing tests with \x00, which can cause the response to be - // detected as a binary file and served as a download). - action: "/FileAPI/file/resources/echo-content-escaped.py", - method: "POST", - enctype: "multipart/form-data", - target: formTargetFrame.name, - }); - document.body.append(form); - testCase.add_cleanup(() => { - document.body.removeChild(form); - }); - - const input = document.createElement("input"); - input.name = name; - if (value instanceof File) { - input.type = "file"; - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(value); - input.files = dataTransfer.files; - } else { - input.type = "hidden"; - input.value = value; - } - form.append(input); - - await new Promise((resolve) => { - form.submit(); - formTargetFrame.onload = resolve; - }); - - const serialized = unescape( - formTargetFrame.contentDocument.body.textContent, - ); - const boundary = serialized.split("\r\n")[0]; - assert_equals(serialized, expectedPayload(expected, boundary)); - }, `multipart/form-data: ${description} (normal form)`); - - // formdata event - promise_test(async (testCase) => { - if (document.readyState !== "complete") { - await new Promise((resolve) => addEventListener("load", resolve)); - } - - const formTargetFrame = Object.assign(document.createElement("iframe"), { - name: "formtargetframe", - }); - document.body.append(formTargetFrame); - testCase.add_cleanup(() => { - document.body.removeChild(formTargetFrame); - }); - - const form = Object.assign(document.createElement("form"), { - acceptCharset: formEncoding, - // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py - // because we're doing tests with \x00, which can cause the response to be - // detected as a binary file and served as a download). - action: "/FileAPI/file/resources/echo-content-escaped.py", - method: "POST", - enctype: "multipart/form-data", - target: formTargetFrame.name, - }); - document.body.append(form); - testCase.add_cleanup(() => { - document.body.removeChild(form); - }); - - form.addEventListener("formdata", (evt) => { - evt.formData.append(name, value); - }); - - await new Promise((resolve) => { - form.submit(); - formTargetFrame.onload = resolve; - }); - - const serialized = unescape( - formTargetFrame.contentDocument.body.textContent, - ); - const boundary = serialized.split("\r\n")[0]; - assert_equals(serialized, expectedPayload(expected, boundary)); - }, `multipart/form-data: ${description} (formdata event)`); -} - -function unescape(str) { - return str.replace(/\r\n?|\n/g, "\r\n").replace( - /\\x[0-9A-Fa-f]{2}/g, - (escape) => String.fromCodePoint(parseInt(escape.substring(2), 16)), - ).replace(/\\\\/g, "\\"); -} - -function expectedPayload({ name, filename, value }, boundary) { - let headers; - if (filename === undefined) { - headers = [`Content-Disposition: form-data; name="${name}"`]; - } else { - headers = [ - `Content-Disposition: form-data; name="${name}"; filename="${filename}"`, - "Content-Type: text/plain", - ]; - } - - return [ - boundary, - ...headers, - "", - value, - boundary + "--", - "", - ].join("\r\n"); -} diff --git a/html/semantics/forms/form-submission-0/text-plain.window.js b/html/semantics/forms/form-submission-0/text-plain.window.js index b68bec8cda9511..54bca9e169c062 100644 --- a/html/semantics/forms/form-submission-0/text-plain.window.js +++ b/html/semantics/forms/form-submission-0/text-plain.window.js @@ -1,3 +1,10 @@ +// META: script=enctypes-helper.js + +const form = formSubmissionTemplate( + "text/plain", + (expected) => expected, +); + form({ name: "basic", value: "test", @@ -209,111 +216,3 @@ form({ expected: "\xE1=💩\r\n", description: "character not in encoding in filename", }); - -function form({ - name, - value, - expected, - formEncoding = "utf-8", - description, -}) { - // Normal form - promise_test(async (testCase) => { - if (document.readyState !== "complete") { - await new Promise((resolve) => addEventListener("load", resolve)); - } - - const formTargetFrame = Object.assign(document.createElement("iframe"), { - name: "formtargetframe", - }); - document.body.append(formTargetFrame); - testCase.add_cleanup(() => { - document.body.removeChild(formTargetFrame); - }); - - const form = Object.assign(document.createElement("form"), { - acceptCharset: formEncoding, - // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py - // because we're doing tests with \x00, which can cause the response to be - // detected as a binary file and served as a download). - action: "/FileAPI/file/resources/echo-content-escaped.py", - method: "POST", - enctype: "text/plain", - target: formTargetFrame.name, - }); - document.body.append(form); - testCase.add_cleanup(() => { - document.body.removeChild(form); - }); - - const input = document.createElement("input"); - input.name = name; - if (value instanceof File) { - input.type = "file"; - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(value); - input.files = dataTransfer.files; - } else { - input.type = "hidden"; - input.value = value; - } - form.append(input); - - await new Promise((resolve) => { - form.submit(); - formTargetFrame.onload = resolve; - }); - - const serialized = formTargetFrame.contentDocument.body.textContent; - assert_equals(unescape(serialized), expected); - }, `text/plain: ${description} (normal form)`); - - // formdata event - promise_test(async (testCase) => { - if (document.readyState !== "complete") { - await new Promise((resolve) => addEventListener("load", resolve)); - } - - const formTargetFrame = Object.assign(document.createElement("iframe"), { - name: "formtargetframe", - }); - document.body.append(formTargetFrame); - testCase.add_cleanup(() => { - document.body.removeChild(formTargetFrame); - }); - - const form = Object.assign(document.createElement("form"), { - acceptCharset: formEncoding, - // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py - // because we're doing tests with \x00, which can cause the response to be - // detected as a binary file and served as a download). - action: "/FileAPI/file/resources/echo-content-escaped.py", - method: "POST", - enctype: "text/plain", - target: formTargetFrame.name, - }); - document.body.append(form); - testCase.add_cleanup(() => { - document.body.removeChild(form); - }); - - form.addEventListener("formdata", (evt) => { - evt.formData.append(name, value); - }); - - await new Promise((resolve) => { - form.submit(); - formTargetFrame.onload = resolve; - }); - - const serialized = formTargetFrame.contentDocument.body.textContent; - assert_equals(unescape(serialized), expected); - }, `text/plain: ${description} (formdata event)`); -} - -function unescape(str) { - return str.replace(/\r\n?|\n/g, "\r\n").replace( - /\\x[0-9A-Fa-f]{2}/g, - (escape) => String.fromCodePoint(parseInt(escape.substring(2), 16)), - ).replace(/\\\\/g, "\\"); -} diff --git a/html/semantics/forms/form-submission-0/urlencoded2.window.js b/html/semantics/forms/form-submission-0/urlencoded2.window.js index 0c20b2c2e4e2bd..df86abb093dc56 100644 --- a/html/semantics/forms/form-submission-0/urlencoded2.window.js +++ b/html/semantics/forms/form-submission-0/urlencoded2.window.js @@ -1,3 +1,10 @@ +// META: script=enctypes-helper.js + +const form = formSubmissionTemplate( + "application/x-www-form-urlencoded", + (expected) => expected, +); + form({ name: "basic", value: "test", @@ -209,113 +216,3 @@ form({ expected: "%E1=%26%23128169%3B", description: "character not in encoding in filename", }); - -function form({ - name, - value, - expected, - formEncoding = "utf-8", - description, -}) { - // Normal form - promise_test(async (testCase) => { - if (document.readyState !== "complete") { - await new Promise((resolve) => addEventListener("load", resolve)); - } - - const formTargetFrame = Object.assign(document.createElement("iframe"), { - name: "formtargetframe", - }); - document.body.append(formTargetFrame); - testCase.add_cleanup(() => { - document.body.removeChild(formTargetFrame); - }); - - const form = Object.assign(document.createElement("form"), { - acceptCharset: formEncoding, - // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py - // to work around WebKit not percent-encoding \x00 (which causes the - // response to be detected as a binary file and served as a download). - // The output should not change if the urlencoded serializer is correct. - action: "/FileAPI/file/resources/echo-content-escaped.py", - method: "POST", - enctype: "application/x-www-form-urlencoded", - target: formTargetFrame.name, - }); - document.body.append(form); - testCase.add_cleanup(() => { - document.body.removeChild(form); - }); - - const input = document.createElement("input"); - input.name = name; - if (value instanceof File) { - input.type = "file"; - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(value); - input.files = dataTransfer.files; - } else { - input.type = "hidden"; - input.value = value; - } - form.append(input); - - await new Promise((resolve) => { - form.submit(); - formTargetFrame.onload = resolve; - }); - - const serialized = formTargetFrame.contentDocument.body.textContent; - assert_equals(unescape(serialized), expected); - }, `application/x-www-form-urlencoded: ${description} (normal form)`); - - // formdata event - promise_test(async (testCase) => { - if (document.readyState !== "complete") { - await new Promise((resolve) => addEventListener("load", resolve)); - } - - const formTargetFrame = Object.assign(document.createElement("iframe"), { - name: "formtargetframe", - }); - document.body.append(formTargetFrame); - testCase.add_cleanup(() => { - document.body.removeChild(formTargetFrame); - }); - - const form = Object.assign(document.createElement("form"), { - acceptCharset: formEncoding, - // Using echo-content-escaped.py rather than /fetch/api/resources/echo-content.py - // to work around WebKit not percent-encoding \x00 (which causes the - // response to be detected as a binary file and served as a download). - // The output should not change if the urlencoded serializer is correct. - action: "/FileAPI/file/resources/echo-content-escaped.py", - method: "POST", - enctype: "application/x-www-form-urlencoded", - target: formTargetFrame.name, - }); - document.body.append(form); - testCase.add_cleanup(() => { - document.body.removeChild(form); - }); - - form.addEventListener("formdata", (evt) => { - evt.formData.append(name, value); - }); - - await new Promise((resolve) => { - form.submit(); - formTargetFrame.onload = resolve; - }); - - const serialized = formTargetFrame.contentDocument.body.textContent; - assert_equals(unescape(serialized), expected); - }, `application/x-www-form-urlencoded: ${description} (formdata event)`); -} - -function unescape(str) { - return str.replace(/\r\n?|\n/g, "\r\n").replace( - /\\x[0-9A-Fa-f]{2}/g, - (escape) => String.fromCodePoint(parseInt(escape.substring(2), 16)), - ).replace(/\\\\/g, "\\"); -} From 78139ceb3f164280d9edc3b6e4796d5eb967a5da Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Fri, 9 Apr 2021 12:35:24 +0200 Subject: [PATCH 13/13] Documentation and some refactoring --- .../form-submission-0/enctypes-helper.js | 13 ++++++ .../multipart-formdata.window.js | 43 +++++++++---------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/html/semantics/forms/form-submission-0/enctypes-helper.js b/html/semantics/forms/form-submission-0/enctypes-helper.js index c22e50230089e4..53b76b019545a5 100644 --- a/html/semantics/forms/form-submission-0/enctypes-helper.js +++ b/html/semantics/forms/form-submission-0/enctypes-helper.js @@ -18,6 +18,12 @@ .replace(/\\\\/g, "\\"); } + // `expectedBuilder` is a function that takes in the actual form body + // (necessary to get the multipart/form-data payload) and returns the form + // body that should be expected. + // If `testFormData` is false, the form entry will be submitted in for + // controls. If it is true, it will submitted by modifying the entry list + // during the `formdata` event. async function formSubmissionTest({ name, value, @@ -82,6 +88,13 @@ assert_equals(serialized, expected); } + // This function returns a function to add individual form tests corresponding + // to some enctype. + // `expectedBuilder` is a function that takes two parameters: `expected` (the + // `expected` value of a test) and `serialized` (the actual form body + // submitted by the browser), and returns the correct form body that should + // have been submitted. This is necessary in order to account for th + // multipart/form-data boundary. window.formSubmissionTemplate = (enctype, expectedBuilder) => { function form({ name, diff --git a/html/semantics/forms/form-submission-0/multipart-formdata.window.js b/html/semantics/forms/form-submission-0/multipart-formdata.window.js index bed3d06a2076b8..f26c0723dba4bf 100644 --- a/html/semantics/forms/form-submission-0/multipart-formdata.window.js +++ b/html/semantics/forms/form-submission-0/multipart-formdata.window.js @@ -3,32 +3,29 @@ // Form submissions in multipart/form-data are also tested in // /FileAPI/file/send-file* -function expectedPayload({ name, filename, value }, boundary) { - let headers; - if (filename === undefined) { - headers = [`Content-Disposition: form-data; name="${name}"`]; - } else { - headers = [ - `Content-Disposition: form-data; name="${name}"; filename="${filename}"`, - "Content-Type: text/plain", - ]; - } - - return [ - boundary, - ...headers, - "", - value, - boundary + "--", - "", - ].join("\r\n"); -} - const form = formSubmissionTemplate( "multipart/form-data", - (expected, serialized) => { + ({ name, filename, value }, serialized) => { + let headers; + if (filename === undefined) { + headers = [`Content-Disposition: form-data; name="${name}"`]; + } else { + headers = [ + `Content-Disposition: form-data; name="${name}"; filename="${filename}"`, + "Content-Type: text/plain", + ]; + } + const boundary = serialized.split("\r\n")[0]; - return expectedPayload(expected, boundary); + + return [ + boundary, + ...headers, + "", + value, + boundary + "--", + "", + ].join("\r\n"); }, );