想要强制下载资源而不是在 Web 浏览器中直接呈现的 Web 应用程序会在表单的 HTTP 响应中发出 Content-Disposition
标头:
Content-Disposition: attachment; filename=FILENAME
filename
参数可用于建议浏览器将资源下载到的文件的名称。但是,RFC 2183(Content-Disposition)在 section 2.3(文件名参数)中声明文件名只能使用 US-ASCII 字符:
当前 [RFC 2045] 语法将参数值(以及因此的 Content-Disposition 文件名)限制为 US-ASCII。我们认识到允许在文件名中使用任意字符集是非常可取的,但是定义必要的机制超出了本文档的范围。
然而,有经验证据表明,当今大多数流行的 Web 浏览器似乎允许非 US-ASCII 字符(由于缺乏标准)在文件名的编码方案和字符集规范上存在分歧。那么问题来了,如果需要将文件名“naïvefile”(不带引号且第三个字母是 U+00EF)编码到 Content-Disposition 标头中,流行的浏览器采用的各种方案和编码是什么?
对于这个问题,流行的浏览器是:
谷歌浏览器
苹果浏览器
Internet Explorer 或边缘
火狐
歌剧
Content-Disposition: attachment
结合起来。
我知道这是一个旧帖子,但它仍然非常相关。我发现现代浏览器支持 rfc5987,它允许 utf-8 编码、百分比编码(url 编码)。然后 Naïve file.txt 变为:
Content-Disposition: attachment; filename*=UTF-8''Na%C3%AFve%20file.txt
Safari (5) 不支持此功能。相反,您应该使用 Safari 标准将文件名直接写入 utf-8 编码的标头中:
Content-Disposition: attachment; filename=Naïve file.txt
IE8 及更早版本也不支持,需要使用 IE 标准的 utf-8 编码,百分比编码:
Content-Disposition: attachment; filename=Na%C3%AFve%20file.txt
在 ASP.Net 中,我使用以下代码:
string contentDisposition;
if (Request.Browser.Browser == "IE" && (Request.Browser.Version == "7.0" || Request.Browser.Version == "8.0"))
contentDisposition = "attachment; filename=" + Uri.EscapeDataString(fileName);
else if (Request.Browser.Browser == "Safari")
contentDisposition = "attachment; filename=" + fileName;
else
contentDisposition = "attachment; filename*=UTF-8''" + Uri.EscapeDataString(fileName);
Response.AddHeader("Content-Disposition", contentDisposition);
我使用 IE7、IE8、IE9、Chrome 13、Opera 11、FF5、Safari 5 测试了上述内容。
2013 年 11 月更新:
这是我目前使用的代码。我仍然要支持IE8,所以我无法摆脱第一部分。事实证明,Android 上的浏览器使用内置的 Android 下载管理器,它无法以标准方式可靠地解析文件名。
string contentDisposition;
if (Request.Browser.Browser == "IE" && (Request.Browser.Version == "7.0" || Request.Browser.Version == "8.0"))
contentDisposition = "attachment; filename=" + Uri.EscapeDataString(fileName);
else if (Request.UserAgent != null && Request.UserAgent.ToLowerInvariant().Contains("android")) // android built-in download manager (all browsers on android)
contentDisposition = "attachment; filename=\"" + MakeAndroidSafeFileName(fileName) + "\"";
else
contentDisposition = "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + Uri.EscapeDataString(fileName);
Response.AddHeader("Content-Disposition", contentDisposition);
以上内容现已在 IE7-11、Chrome 32、Opera 12、FF25、Safari 6 中测试,使用此文件名进行下载:你好abcABCæøåÆØÅäöüïëêîâéíáóúýñ½§!#¤%&()=`@£$€{[]}+´¨ ^~'-_,;.txt
在 IE7 上,它适用于某些字符,但不是全部。但是现在谁在乎IE7?
这是我用来为 Android 生成安全文件名的函数。请注意,我不知道 Android 支持哪些字符,但我已经测试过这些字符确实有效:
private static readonly Dictionary<char, char> AndroidAllowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-+,@£$€!½§~'=()[]{}0123456789".ToDictionary(c => c);
private string MakeAndroidSafeFileName(string fileName)
{
char[] newFileName = fileName.ToCharArray();
for (int i = 0; i < newFileName.Length; i++)
{
if (!AndroidAllowedChars.ContainsKey(newFileName[i]))
newFileName[i] = '_';
}
return new string(newFileName);
}
@TomZ:我在 IE7 和 IE8 中进行了测试,结果证明我不需要转义撇号 (')。你有一个失败的例子吗?
@Dave Van den Eynde:根据 RFC6266 将两个文件名组合在一行上,Android 和 IE7+8 除外,我已经更新了代码以反映这一点。感谢您的建议。
@Thilo:不知道 GoodReader 或任何其他非浏览器。使用 Android 方法可能会有一些运气。
@Alex Zhukovskiy:我不知道为什么,但正如在 Connect 上讨论的那样,它似乎不太好用。
在 Content-Disposition 中没有可互操作的方式对非 ASCII 名称进行编码。浏览器兼容性一团糟。
在 Content-Disposition 中使用 UTF-8 的理论上正确的语法非常奇怪:filename*=UTF-8''foo%c3%a4 (是的,这是一个星号,除了中间的空单引号外没有引号)
这个标头有点不太标准(HTTP/1.1 规范承认它的存在,但不要求客户端支持它)。
有一个简单且非常强大的替代方法:使用包含所需文件名的 URL。
当最后一个斜杠后面的名称是您想要的名称时,您不需要任何额外的标题!
这个技巧有效:
/real_script.php/fake_filename.doc
如果您的服务器支持 URL 重写(例如 Apache 中的 mod_rewrite
),那么您可以完全隐藏脚本部分。
URL 中的字符应为 UTF-8,按字节进行 urlencoded:
/mot%C3%B6rhead # motörhead
/:id/:filename
方法非常简单有效,谢谢!
Content-Disposition
并创建非常有趣的文件名(它们将从您的路径生成)。因此,保持理智的唯一解决方案就是设置 Content-Disposition: attachment
并将所需的文件名作为最后一个路径组件传递:
/
的内容,您会得到真的 奇怪的浏览器错误。以 this answer 作为参考,我使用了 s.replace(/[\000-\031\\\/:*?"<>\|]/g, '_')
在建议的 RFC 5987“超文本传输协议 (HTTP) 标头字段参数的字符集和语言编码”中对此进行了讨论,包括浏览器测试和向后兼容性的链接。
RFC 2183 表示此类标头应根据 RFC 2184 进行编码,该 RFC 2184 已被 RFC 2231 废弃,包含在上面的 RFC 草案中。
Content-Disposition: filename="foo, bar.pdf"
,Firefox(包括版本 4-9)会中断。结果是 Firefox 正确下载了文件,但保留了 .part
扩展名(例如 foo,bar.pdf-1.part
)。然后,当然该文件将无法正确打开,因为该应用程序未与 .part
关联。其他 ASCII 字符似乎可以正常工作。
RFC 6266 描述了“超文本传输协议 (HTTP) 中 Content-Disposition 标头字段的使用”。从中引用:
6. 国际化注意事项 “filename*”参数(第 4.3 节)使用 [RFC5987] 中定义的编码,允许服务器传输 ISO-8859-1 字符集之外的字符,并且还可以选择指定使用的语言。
在他们的 examples section 中:
此示例与上面的示例相同,但添加了“文件名”参数以与未实现 RFC 5987 的用户代理兼容:Content-Disposition: attachment; filename="欧元汇率"; filename*=utf-8''%e2%82%ac%20rates 注意:那些不支持 RFC 5987 编码的用户代理会忽略出现在“filename”之后的“filename*”。
在 Appendix D 中还有一长串提高互操作性的建议。它还指向 a site which compares implementations。当前适用于常见文件名的全通测试包括:
attwithisofnplain:带有双引号且不带编码的纯 ISO-8859-1 文件名。这需要一个全部为 ISO-8859-1 且不包含百分号的文件名,至少不包含在十六进制数字前面。
attfnboth:按上述顺序的两个参数。应该适用于大多数浏览器上的大多数文件名,尽管 IE8 将使用“文件名”参数。
该 RFC 5987 又引用 RFC 2231,它描述了实际格式。 2231 主要用于邮件,5987 告诉我们哪些部分也可用于 HTTP 标头。不要将此与 multipart/form-data
HTTP body 中使用的 MIME 标头混淆,后者由 RFC 2388(特别是 section 4.4)和 HTML 5 draft 管理。
Jim 在他的回答中提到的从 the draft RFC 链接的以下文档进一步解决了这个问题,在这里绝对值得直接注意:
Test Cases for HTTP Content-Disposition header and RFC 2231/2047 Encoding
我使用以下代码片段进行编码(假设 fileName 包含文件的文件名和扩展名,即:test.txt):
PHP:
if ( strpos ( $_SERVER [ 'HTTP_USER_AGENT' ], "MSIE" ) > 0 )
{
header ( 'Content-Disposition: attachment; filename="' . rawurlencode ( $fileName ) . '"' );
}
else
{
header( 'Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode ( $fileName ) );
}
爪哇:
fileName = request.getHeader ( "user-agent" ).contains ( "MSIE" ) ? URLEncoder.encode ( fileName, "utf-8") : MimeUtility.encodeWord ( fileName );
response.setHeader ( "Content-disposition", "attachment; filename=\"" + fileName + "\"");
filename*=
处置标头,它在 PHP 中应该是 rawurlencode
,因为 RFC 6266->RFC 5987 的 ext-value
中使用的 value-chars
(请参阅 tools.ietf.org/html/rfc6266#section-4.1 & tools.ietf.org/html/rfc5987#section-3.2.1 )没有允许没有百分比转义的空间(另一方面,filename=
似乎可以允许一个没有转义的空间,尽管这里应该只存在 ASCII)。没有必要使用 rawurlencode 的完全严格性进行编码,因此可以对一些字符进行非转义:gist.github.com/brettz9/8752120
将文件名放在双引号中。为我解决了这个问题。像这样:
Content-Disposition: attachment; filename="My Report.doc"
http://kb.mozillazine.org/Filenames_with_spaces_are_truncated_upon_download
我已经测试了多个选项。浏览器不支持规范并且行为不同,我相信双引号是最好的选择。
&
、%
、#
等的文件名。所以它解决了这个问题。
\"
)等,但它从来没有奏效。最终我不得不使用 gsub
来删除双引号。因此,如果 filename
是 My 2" Report.doc
,它最终会是 My 2 Report.doc
。不理想,但至少它有效。想法?
在 asp.net mvc2 我使用这样的东西:
return File(
tempFile
, "application/octet-stream"
, HttpUtility.UrlPathEncode(fileName)
);
我想如果你不使用 mvc(2) 你可以只使用编码文件名
HttpUtility.UrlPathEncode(fileName)
在 ASP.NET Web API 中,我对文件名进行 url 编码:
public static class HttpRequestMessageExtensions
{
public static HttpResponseMessage CreateFileResponse(this HttpRequestMessage request, byte[] data, string filename, string mediaType)
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
var stream = new MemoryStream(data);
stream.Position = 0;
response.Content = new StreamContent(stream);
response.Content.Headers.ContentType =
new MediaTypeHeaderValue(mediaType);
// URL-Encode filename
// Fixes behavior in IE, that filenames with non US-ASCII characters
// stay correct (not "_utf-8_.......=_=").
var encodedFilename = HttpUtility.UrlEncode(filename, Encoding.UTF8);
response.Content.Headers.ContentDisposition =
new ContentDispositionHeaderValue("attachment") { FileName = encodedFilename };
return response;
}
}
https://i.stack.imgur.com/Fsla4.jpg
在 PHP 中,这是为我做的(假设文件名是 UTF8 编码的):
header('Content-Disposition: attachment;'
. 'filename="' . addslashes(utf8_decode($filename)) . '";'
. 'filename*=utf-8\'\'' . rawurlencode($filename));
针对 IE8-11、Firefox 和 Chrome 进行了测试。
如果浏览器可以解释 filename*=utf-8,它将使用 UTF8 版本的文件名,否则它将使用解码的文件名。如果您的文件名包含 ISO-8859-1 中无法表示的字符,您可能需要考虑改用 iconv
。
如果您使用的是 nodejs 后端,则可以使用我找到的以下代码 here
var fileName = 'my file(2).txt';
var header = "Content-Disposition: attachment; filename*=UTF-8''"
+ encodeRFC5987ValueChars(fileName);
function encodeRFC5987ValueChars (str) {
return encodeURIComponent(str).
// Note that although RFC3986 reserves "!", RFC5987 does not,
// so we do not need to escape it
replace(/['()]/g, escape). // i.e., %27 %28 %29
replace(/\*/g, '%2A').
// The following are not required for percent-encoding per RFC5987,
// so we can allow for a little better readability over the wire: |`^
replace(/%(?:7C|60|5E)/g, unescape);
}
encodeURI(str)
。以文件名中的日期为例:encodeURIComponent('"Kornél Kovács 1/1/2016')
=> "Kornél Kovács 1%2F1%2F2016" vs. encodeURI('"Kornél Kovács 1/1/2016')
=> “科尔内尔·科瓦奇 2016 年 1 月 1 日”
我在所有主流浏览器中测试了以下代码,包括旧版 Explorer(通过兼容模式),它在任何地方都运行良好:
$filename = $_GET['file']; //this string from $_GET is already decoded
if (strstr($_SERVER['HTTP_USER_AGENT'],"MSIE"))
$filename = rawurlencode($filename);
header('Content-Disposition: attachment; filename="'.$filename.'"');
我最终在“download.php”脚本中使用了以下代码(基于 this blogpost 和 these test cases)。
$il1_filename = utf8_decode($filename);
$to_underscore = "\"\\#*;:|<>/?";
$safe_filename = strtr($il1_filename, $to_underscore, str_repeat("_", strlen($to_underscore)));
header("Content-Disposition: attachment; filename=\"$safe_filename\""
.( $safe_filename === $filename ? "" : "; filename*=UTF-8''".rawurlencode($filename) ));
只要仅使用 iso-latin1 和“安全”字符,这将使用标准的 filename="..." 方式;如果没有,它会添加 filename*=UTF-8'' url-encoded 方式。根据this specific test case,它应该从 MSIE9 开始工作,并且在最近的 FF、Chrome、Safari 上工作;在较低的 MSIE 版本上,它应该提供包含 ISO8859-1 版本的文件名的文件名,并在不采用这种编码的字符上带有下划线。
最后说明:最大。在 apache 上,每个标头字段的大小为 8190 字节。 UTF-8 每个字符最多可以有四个字节;在 rawurlencode 之后,每个字符 x3 = 12 个字节。效率很低,但理论上应该仍然可以在文件名中包含超过 600 个“微笑”%F0%9F%98%81。
只是一个更新,因为我今天尝试所有这些东西以响应客户问题
除了为日语配置的 Safari 之外,我们客户测试的所有浏览器在使用 filename=text.pdf 时效果最佳 - 其中 text 是由 ASP.Net/IIS 在 utf-8 中序列化的客户值,没有 url 编码。出于某种原因,配置为英文的 Safari 会接受并正确保存具有 utf-8 日文名称的文件,但配置为日文的同一浏览器会使用未解释的 utf-8 字符保存文件。测试的所有其他浏览器似乎在文件名 utf-8 编码但没有 url 编码的情况下工作得最好/很好(无论语言配置如何)。
我根本找不到一个实现 Rfc5987/8187 的浏览器。我使用最新的 Chrome、Firefox 构建以及 IE 11 和 Edge 进行了测试。我尝试仅使用 filename*=utf-8''texturlencoded.pdf 设置标题,同时使用 filename=text.pdf 设置它;文件名*=utf-8''texturlencoded.pdf。 Rfc5987/8187 的任何一项功能似乎都没有在上述任何一项中得到正确处理。
从 .NET 4.5(和 Core 1.0)开始,您可以使用 ContentDispositionHeaderValue 为您进行格式化。
var fileName = "Naïve file.txt";
var h = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
h.FileNameStar = fileName;
h.FileName = "fallback-ascii-name.txt";
Response.Headers.Add("Content-Disposition", h.ToString());
h.ToString()
将导致:
attachment; filename*=utf-8''Na%C3%AFve%20file.txt; filename=fallback-ascii-name.txt
PHP 框架 Symfony 4 在 HeaderUtils::makeDisposition
中有 $filenameFallback
。您可以查看此功能以获取详细信息 - 它类似于上面的答案。
使用示例:
$filenameFallback = preg_replace('#^.*\.#', md5($filename) . '.', $filename);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename, $filenameFallback);
$response->headers->set('Content-Disposition', $disposition);
经典的 ASP 解决方案
大多数现代浏览器现在都支持将 Filename
作为 UTF-8
传递,但就像我使用的基于 FreeASPUpload.Net 的文件上传解决方案一样(站点不再存在,链接指向 archive.org)< /em> 它不起作用,因为二进制文件的解析依赖于读取单字节 ASCII 编码的字符串,当你传递 UTF-8 编码的数据直到你得到 ASCII 不支持的字符时,它工作得很好。
但是,我能够找到一种解决方案来获取将二进制文件读取和解析为 UTF-8 的代码。
Public Function BytesToString(bytes) 'UTF-8..
Dim bslen
Dim i, k , N
Dim b , count
Dim str
bslen = LenB(bytes)
str=""
i = 0
Do While i < bslen
b = AscB(MidB(bytes,i+1,1))
If (b And &HFC) = &HFC Then
count = 6
N = b And &H1
ElseIf (b And &HF8) = &HF8 Then
count = 5
N = b And &H3
ElseIf (b And &HF0) = &HF0 Then
count = 4
N = b And &H7
ElseIf (b And &HE0) = &HE0 Then
count = 3
N = b And &HF
ElseIf (b And &HC0) = &HC0 Then
count = 2
N = b And &H1F
Else
count = 1
str = str & Chr(b)
End If
If i + count - 1 > bslen Then
str = str&"?"
Exit Do
End If
If count>1 then
For k = 1 To count - 1
b = AscB(MidB(bytes,i+k+1,1))
N = N * &H40 + (b And &H3F)
Next
str = str & ChrW(N)
End If
i = i + count
Loop
BytesToString = str
End Function
通过在我自己的代码中实现 include_aspuploader.asp
中的 BytesToString()
函数,我能够使 UTF-8
文件名正常工作,这归功于 Pure ASP File Upload。
有用的链接
ASP Classic 应用程序中的 Multipart/form-data 和 UTF-8
Unicode、UTF、ASCII、ANSI 格式差异
对于那些需要 JavaScript 编码标头的方法的人,我发现这个函数运行良好:
function createContentDispositionHeader(filename:string) {
const encoded = encodeURIComponent(filename);
return `attachment; filename*=UTF-8''${encoded}; filename="${encoded}"`;
}
这是基于 Nextcloud 在下载文件时似乎正在做的事情。文件名首先以 UTF-8 编码出现,并且可能为了与某些浏览器兼容,文件名也出现时不带 UTF-8 前缀。
库类 Unicode 中的 mimeHeaderEncode($string) 方法可以完成这项工作。
$file_name= Unicode::mimeHeaderEncode($file_name);
drupal/php 中的示例:
https://github.com/drupal/core-utility/blob/8.8.x/Unicode.php
/**
* Encodes MIME/HTTP headers that contain incorrectly encoded characters.
*
* For example, Unicode::mimeHeaderEncode('tést.txt') returns
* "=?UTF-8?B?dMOpc3QudHh0?=".
*
* See http://www.rfc-editor.org/rfc/rfc2047.txt for more information.
*
* Notes:
* - Only encode strings that contain non-ASCII characters.
* - We progressively cut-off a chunk with self::truncateBytes(). This ensures
* each chunk starts and ends on a character boundary.
* - Using \n as the chunk separator may cause problems on some systems and
* may have to be changed to \r\n or \r.
*
* @param string $string
* The header to encode.
* @param bool $shorten
* If TRUE, only return the first chunk of a multi-chunk encoded string.
*
* @return string
* The mime-encoded header.
*/
public static function mimeHeaderEncode($string, $shorten = FALSE) {
if (preg_match('/[^\x20-\x7E]/', $string)) {
// floor((75 - strlen("=?UTF-8?B??=")) * 0.75);
$chunk_size = 47;
$len = strlen($string);
$output = '';
while ($len > 0) {
$chunk = static::truncateBytes($string, $chunk_size);
$output .= ' =?UTF-8?B?' . base64_encode($chunk) . "?=\n";
if ($shorten) {
break;
}
$c = strlen($chunk);
$string = substr($string, $c);
$len -= $c;
}
return trim($output);
}
return $string;
}
我们在 Web 应用程序中遇到了类似的问题,最终通过从 HTML <input type="file">
中读取文件名,并将其设置为新 HTML <input type="hidden">
中的 url 编码形式。当然,我们必须删除某些浏览器返回的路径,例如“C:\fakepath\”。
当然,这并不能直接回答 OP 的问题,但可能是其他人的解决方案。
我通常对文件名进行 URL 编码(使用 %xx),它似乎适用于所有浏览器。无论如何,您可能都想做一些测试。
不定期副业成功案例分享
Content-Disposition: attachment; filename*=UTF-8''Na%C3%AFve%20file.txt; filename=Na%C3%AFve%20file.txt
并跳过浏览器嗅探?那行得通吗?