如何编码的文件名称参数的内容处置头在HTTP?

Web应用程序如果想在Web浏览器中强制资源为下载而不是直接为呈现,则在表单的HTTP响应中发出Content-Disposition报头:

# EYZ0

filename参数可用于建议浏览器将资源下载到其中的文件的名称。RFC 2183 (Content-Disposition),然而,在2.3节 (Filename参数)中声明文件名只能使用US-ASCII字符:

当前[RFC 2045]语法限制 参数值(因此 内容-处置文件名)到 us - ascii。我们认可伟大的 允许任意的可取性 文件名中的字符集,但它是 超出了本文档的范围 定义必要的机制

然而,有经验证据表明,目前大多数流行的Web浏览器似乎允许非us - ascii字符,但(由于缺乏标准)在文件名的编码方案和字符集规范上存在分歧。问题是,如果文件名“naïvefile”(不带引号,第三个字母是U+00EF)需要编码到Content-Disposition报头中,那么流行的浏览器采用了哪些不同的方案和编码?

对于这个问题,流行的浏览器表示:

  • 谷歌Chrome
  • Safari
  • Internet Explorer或Edge
  • 火狐
  • 歌剧
450838 次浏览

我通常对文件名进行url编码(使用%xx),它似乎在所有浏览器中都可以工作。你还是得做些检查。

RFC 5987“超文本传输协议(HTTP)报头字段参数的字符集和语言编码”中对此进行了讨论,包括浏览器测试和向后兼容性的链接。

RFC 2183表示这样的头文件应该根据RFC 2184编码,而RFC 2184在上面的RFC草案中已经被RFC 2231淘汰了。

以下文件链接自吉姆在他的回答中提到的RFC草案,进一步解决了这个问题,绝对值得在这里直接注意:

HTTP内容处理头和RFC 2231/2047编码的测试用例

有一个简单而健壮的替代方案:使用包含你想要的文件名的URL

当最后一个斜杠后面的名称是您想要的名称时,您不需要任何额外的头文件!

这个技巧很管用:

/real_script.php/fake_filename.doc

如果您的服务器支持URL重写(例如Apache中的mod_rewrite),那么您可以完全隐藏脚本部分。

url中的字符应该是UTF-8,逐字节url编码:

/mot%C3%B6rhead   # motörhead

在asp.net mvc2中,我使用这样的东西:

return File(
tempFile
, "application/octet-stream"
, HttpUtility.UrlPathEncode(fileName)
);

我想如果你不使用mvc(2),你可以只编码文件名使用

HttpUtility.UrlPathEncode(fileName)

我知道这是一个老帖子,但它仍然非常相关。我发现现代浏览器支持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,歌剧12日FF25, Safari 6,使用该文件名下载:你好abcABCæø一ÆØAaouieeiaeiaouyn½§! #¤%,()= ' @£€美元 {[]}+´¨^~'-_,;. 三种

在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:我不知道为什么,但正如在连接上讨论的那样,它似乎并不是很有效。

我在所有主流浏览器中测试了以下代码,包括老式的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.'"');

我使用以下代码片段进行编码(假设文件名包含文件的文件名和扩展名,即: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 ) );
}

Java:

fileName = request.getHeader ( "user-agent" ).contains ( "MSIE" ) ? URLEncoder.encode ( fileName, "utf-8") : MimeUtility.encodeWord ( fileName );
response.setHeader ( "Content-disposition", "attachment; filename=\"" + fileName + "\"");

RFC 6266描述了“超文本传输协议(HTTP)中内容处理报头字段的使用”。引用其中的话:

6. 国际化因素< / >

filename*”参数(4.3节),使用定义的编码 在[RFC5987]中,允许服务器传输外部的字符 ISO-8859-1字符集,也可以选择指定语言 在使用。< / p >

在他们的示例部分中:

这个例子与上面的例子相同,但是增加了"文件名" 参数,用于与未实现的用户代理的兼容性 # EYZ0: < / p >

Content-Disposition: attachment;
filename="EURO rates";
filename*=utf-8''%e2%82%ac%20rates

备注:不支持RFC 5987编码的用户代理 当“filename”后面出现“filename*”时,忽略“filename*

附录D中还有一长串提高互操作性的建议。它还指向比较实现的站点。适用于常用文件名的当前全通过测试包括:

  • attwithisofnplain:普通的ISO-8859-1文件名,双引号,没有编码。这要求文件名完全符合ISO-8859-1,并且不包含百分号,至少在十六进制数字前面不包含百分号。
  • attfnboth:上述顺序中的两个参数。应该适用于大多数浏览器上的大多数文件名,尽管IE8将使用“filename”参数。

RFC 5987又引用了描述实际格式的RFC 2231。2231主要用于邮件,5987告诉我们哪些部分也可以用于HTTP报头。不要将此与在multipart/form-data HTTP 身体中使用的MIME头信息混淆,后者由RFC 2388(特别是4.4节)和HTML 5草稿控制。

我们在一个web应用程序中遇到了类似的问题,最后从HTML <input type="file">中读取文件名,并在一个新的HTML <input type="hidden">中以url编码的形式设置它。当然,我们必须删除一些浏览器返回的“C:\fakepath\”这样的路径。

当然,这并不能直接回答OPs的问题,但可能是其他人的解决方案。

我最终在“download.php”脚本中编写了以下代码(基于这篇这些测试用例)。

$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和“safe”字符,就使用标准的filename="…";如果不是,它会添加文件名*=UTF-8 " url编码的方式。根据这个特定的测试用例,它应该从MSIE9起工作,在最近的FF, Chrome, Safari;在较低的MSIE版本中,它应该提供包含ISO8859-1版本的文件名,在非此编码的字符上使用下划线。

最后注意:最大值。在apache上,每个报头字段的大小为8190字节。UTF-8每个字符最多可以有四个字节;在rawurlencode之后,每个字符是x3 = 12字节。非常低效,但理论上仍然可以在文件名中有超过600个“smiles”%F0%9F%98%81。

在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;
}
}

IE 9 Not fixed
IE 9 Fixed

.

将文件名放在双引号中。帮我解决了问题。是这样的:

Content-Disposition: attachment; filename="My Report.doc"

http://kb.mozillazine.org/Filenames_with_spaces_are_truncated_upon_download

我测试了多种选择。浏览器不支持这些规格,并且表现不同,我相信双引号是最好的选择。

如果你使用的是nodejs后端,你可以使用下面的代码,我找到了在这里

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);
}

在PHP中,这为我做了(假设文件名是UTF8编码):

header('Content-Disposition: attachment;'
. 'filename="' . addslashes(utf8_decode($filename)) . '";'
. 'filename*=utf-8\'\'' . rawurlencode($filename));

在IE8-11、Firefox和Chrome浏览器上测试 如果浏览器可以解释文件名* = utf - 8,它将使用UTF8版本的文件名,否则它将使用解码后的文件名。如果你的文件名包含在ISO-8859-1中无法表示的字符,你可能要考虑使用iconv代替

经典ASP解决方案

大多数现代浏览器现在都支持将Filename传递为UTF-8,但正如我使用的基于FreeASPUpload。网 (网站已不存在,链接指向archive.org)的文件上传解决方案一样,它不会工作,因为二进制解析依赖于读取单字节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.aspBytesToString()函数,我能够得到UTF-8文件名的工作。


有用的链接

  • < p > # EYZ0

  • < p > # EYZ0

只是一个更新,因为我今天为了回应一个客户的问题而尝试了所有这些东西

  • 除了为日文配置的Safari,我们的客户测试的所有浏览器都使用filename=text.pdf运行得最好——其中的文本是由ASP序列化的客户值。Net/IIS在utf-8没有url编码。出于某种原因,配置为英语的Safari将接受并正确保存具有utf-8日文名称的文件,但配置为日语的相同浏览器将保存具有未解释的utf-8字符的文件。测试的所有其他浏览器在没有url编码的utf-8编码的文件名下似乎工作得最好/很好(无论语言配置如何)。
  • 我找不到一个实现Rfc5987/8187 在所有的浏览器。我用最新的Chrome、Firefox版本、IE 11和Edge进行了测试。我试着用文件名*=utf-8 " texturlencoded.pdf设置标题,同时设置文件名=text.pdf;文件名* = texturlencoded.pdf utf - 8”。Rfc5987/8187的任何一个特性似乎都没有在上述任何一种情况下得到正确处理。
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);

在。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

对于那些需要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;
}