表单提交中的换行规范化
如果你使用表单提交,你可能已经注意到包含换行符的表单值被规范化为 CRLF,无论 DOM 值是 LF 还是 CR:
<form action="./post" method="post" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="hidden" value="a
b
c
d" />
<input type="submit" />
</form>
<script>
// 检查 DOM 是否有正确的换行符以及在表单提交期间进行规范化。
const hiddenInput = document.querySelector("input[type=hidden]");
console.log("%s", JSON.stringify(hiddenInput.value)); // "a\rb\nc\r\nd"
</script>
但是,尽管表面上看起来很简单,但表单提交中的换行规范化是一个比我想象的更深入的主题,存在规范中的错误和浏览器之间的差异。 这篇文章介绍了规范过去的用途、浏览器(过去)实现的内容以及我们如何修复它。
首先,表单提交的一些背景
从表单提交的数据被建模为条目列表——条目是名称(字符串)和值(字符串或文件对象)的对。 这是一个列表而不是地图,因为表单的每个名称可以有多个值——这就是 <input type="file" multiple>
和 <select multiple>
的工作方式——它们与其余表单条目的相对顺序很重要 .
完成与特定表单关联的每个可提交元素并收集其相应表单条目的算法是“构建条目列表”算法。 该算法可以达到我们的预期——丢弃禁用的控件和未按下的按钮,然后为每个控件调用“附加条目”算法,该算法用于替换条目名称和值中的任何换行符(如果该值是 string) 在附加条目之前使用 CRLF。
“构造条目列表”在表单提交算法的早期调用,然后将生成的条目列表传递给相应 enctype 的编码算法。 由于只有 multipart/form-data
enctype 支持上传文件,因此 application/x-www-form-urlencoded
和 text/plain
的算法都对值的文件名进行编码。
第一个麻烦的信号
我对表单有效负载编码的第一次尝试是精确定义条目名称(和文件条目值的文件名)必须如何在 multipart/form-data
有效负载中转义,并且由于 LF 和 CR 必须按百分比转义,换行出现了 在测试期间。
我注意到的一件事是,如果你在文件名中有换行符——是的,这是你可以做的——它们的规范化不同于条目名称或字符串值。
<form id="form" action="./post" method="post" enctype="multipart/form-data">
<input type="hidden" name="hidden a
b" value="a
b" />
<input id="fileControl" type="file" name="file a
b" />
</form>
<script>
// A file with filename "a\rb", empty contents, and "application/octet-stream"
// MIME type.
const file = new File([], "a\rb");
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
document.getElementById("fileControl").files = dataTransfer.files;
document.getElementById("form").submit();
</script>
这是在 Chrome 和 Safari 中生成的 multipart/form-data
有效负载(换行符始终为 CRLF):
------WebKitFormBoundaryjlUA0jn3NUYxIh2A
Content-Disposition: form-data; name="hidden a%0D%0Ab"
a
b
------WebKitFormBoundaryjlUA0jn3NUYxIh2A
Content-Disposition: form-data; name="file a%0D%0Ab"; filename="a%0Db"
Content-Type: application/octet-stream
------WebKitFormBoundaryjlUA0jn3NUYxIh2A--
这是在 Firefox 88中:
-----------------------------26303884030012461673680556885
Content-Disposition: form-data; name="hidden a b"
a
b
-----------------------------26303884030012461673680556885
Content-Disposition: form-data; name="file a b"; filename="a b"
Content-Type: application/octet-stream
-----------------------------26303884030012461673680556885--
如我们所见,Firefox 在条目名称和文件名的 multipart/form-data
编码中用空格替换任何换行符(CR、LF 或 CRLF),而不是像 Chrome 和 Safari 那样对它们进行百分比编码。 此行为在拉取请求 #6282 的规范中被定为非法,但在规范决定规范化行为之前无法在 Firefox 中修复。 对于值,Firefox 像其他浏览器一样规范化为 CRLF。
至于 Chrome 和 Safari,在这里我们看到条目名称和字符串值中的换行符被规范化为 CRLF,但文件名未被规范化。 从上面描述的条目列表构造算法来看,这是有道理的,因为条目值只有在它们是字符串时才会被规范化为 CRLF——文件没有改变,它们的文件名也是如此。
除此之外,如果你在上面的例子中将表单的 enctype 更改为 application/x-www-form-urlencoded
,你会在每个浏览器中得到这个:
hidden+a%0D%0Ab=a%0D%0Ab&file+a%0D%0Ab=a%0D%0Ab
由于 multipart/form-data
是唯一允许文件上传的 enctype,因此其他 enctype 使用它们的文件名。 但是这里似乎每个浏览器都在对文件名进行 CRLF 规范化,即使在规范中替换发生在构建条目列表很久之后。
使用 FormData 和 fetch 进行规范化
FormData
类最初是一种通过 XMLHttpRequest
和获取 API 发送multipart/form-data
表单有效负载的方法,而无需在 JavaScript 中生成该有效负载。 因此,FormData 实例基本上是一个 JS 可访问的条目列表包装器。
因此,让我们对 FormData 进行同样的尝试:
const formData = new FormData();
formData.append("hidden a\rb", "a\rb");
formData.append("file a\rb", new File([], "a\rb"));
// fetch 中的 FormData 对象将始终序列化为 multipart/form-data。
await fetch("./post", { method: "POST", body: formData });
Safari 发送与上面相同的表单有效负载,名称和值标准化为 CRLF,Firefox 88 也是如此,值标准化为 CRLF(名称和值的换行符转义为空格)。 但是 Chrome 保留名称、文件名和值未规范化(这里的 ?
字符代表 CR):
------WebKitFormBoundarySMGkMfD8mVOnmGDP
Content-Disposition: form-data; name="hidden a%0Db"
a?b
------WebKitFormBoundarySMGkMfD8mVOnmGDP
Content-Disposition: form-data; name="file a%0Db"; filename="a%0Db"
Content-Type: application/octet-stream
------WebKitFormBoundarySMGkMfD8mVOnmGDP--
由于 FormData 只是条目列表的包装器,并且 fetch 只是调用 multipart/form-data
编码算法,因此不应进行规范化。 所以看起来 Chrome 遵循了此处的规范,而 Firefox 和 Safari 显然在序列化为 multipart/form-data
时进行了一些换行规范化(对于 Firefox,仅针对字符串值)。
使用 FormData,我们还可以研究“构建条目列表”算法的作用,因为如果我们将 <form>
元素传递给 FormData 构造函数,它将在表单提交上下文之外调用该算法,并让我们检查结果条目 列表。
<form id="form">
<input type="hidden" name="a
b" value="a
b" />
</form>
<script>
const formData = new FormData(document.getElementById("form"));
for (const [name, value] of formData.entries()) {
console.log("%s %s", JSON.stringify(name), JSON.stringify(value));
}
// Firefox and Safari print: "a\rb" "a\rb"
// Chrome prints: "a\r\nb" "a\r\nb"
// These results don't depend on the form's enctype.
</script>
所以看起来 Firefox 和 Safari 在构造条目列表时并没有规范化,而是在将表单编码为 enctype 时规范化名称和值。 特别是,由于 application/x-www-form-urlencoded
和 text/plain
enctypes 不允许文件上传,因此文件条目值在规范化之前被替换为它们的文件名。 不是从“构建条目列表”算法创建的条目列表得到规范化。
Chrome 而不是遵循规范(就像过去一样)在“构建条目列表”中规范化,而不是稍后规范化,即使对于通过其他方式创建的条目列表也是如此。 但这并不能解释为什么 application/x-www-form-urlencoded
和 text/plain
enctypes 中的文件名被规范化。 Chrome 是否也有一个额外的规范化层?
使用 formdata 事件调查后期规范化
如果能够更详细地研究 Chrome 和其他浏览器在构建条目列表后执行的操作,那就太好了。 由于条目列表构造已经对条目进行了规范化,因此在常见情况下,可能会进一步发生的任何进一步规范化都被掩盖了。
在 multipart/form-data
的情况下,我们可以对此进行测试,因为将 FormData 对象与 fetch 一起使用不会调用“构建条目列表”,因此可以查看未规范化的条目会发生什么。 对于其他 enctypes,没有办法创建不通过“构建条目列表”的条目列表,但事实证明,“构建条目列表”算法本身提供了两种方法来结束非规范化条目: 与表单相关的自定义元素(目前仅在 Chrome 中实现)和 formdata 事件(在 Chrome 和 Firefox 中实现)。 在这里我们只讨论后者,因为它们的结果是等价的。
当我介绍上面的“构建一个条目列表”算法时,我跳过了一件事,即在算法结束时,在所有对应于控件的条目都被添加到条目列表之后,一个 formdata
事件在相关的 <form >
元素。 这个事件有一个 formData 属性,它允许我们不仅检查此时的条目列表,还可以修改它。
<form
id="form"
action="./post"
method="post"
enctype="application/x-www-form-urlencoded"
>
<!-- Empty -->
</form>
<script>
const form = document.getElementById("form");
form.addEventListener("formdata", (evt) => {
evt.formData.append("string a\rb", "a\rb");
evt.formData.append("file a\rb", new File([], "a\rb"));
});
form.submit();
</script>
对于 Chrome 和 Firefox(不是 Safari,因为它不支持 formdata 事件),尝试使用 application/x-www-form-urlencoded
enctype 可以得到一个规范化的结果:
string+a%0D%0Ab=a%0D%0Ab&file+a%0D%0Ab=a%0D%0Ab
Firefox 对 text/plain
enctype 显示相同的规范化; 相反,Chrome 仅规范化文件名,而不是名称和值。 使用 multipart/form-data
我们得到与上面的 fetch 和 FormData 相同的结果:Chrome 不规范化任何东西,Firefox 规范化字符串值(名称和文件名被空格替换)。
简而言之:
-
对于
application/x-www-form-urlencoded
,所有浏览器在序列化表单有效负载时都会执行额外的换行规范化,无论它们是否在构建条目列表时规范化。 请注意,换行规范化是幂等的,因此规范化已经规范化的字符串不会改变它。 -
对于
text/plain
,Firefox 和 Safari 似乎就像application/x-www-form-urlencoded
一样。 Chrome 而不是仅规范化文件名,可能在文件被替换为文件名的同时。 -
对于
multipart/form-data
,Chrome 不会规范化任何内容。 Safari 改为规范化名称和字符串值,但不规范化文件名。 Firefox 在值方面与 Safari 相同,但将名称和文件名中的所有换行符替换为空格。
请记住
,浏览器之间的这些差异并不会真正影响普通表单的编码,它们仅在我们将 FormData 与 fetch、formdata 事件或与表单相关的自定义元素一起使用时才重要。
相关文章
在 React 中获取表单提交的输入值
发布时间:2022/10/28 浏览次数:134 分类:React
-
在 React 中获取表单提交的输入值: 将输入字段的值存储在状态变量中。在表单元素上设置 onSubmit 属性。 在我们的 handleSubmit 函数中访问输入字段的值。