如何在HTTP的Content-Disposition头中对文件名参数进行编码?

624

想要强制浏览器下载资源而不是直接在Web浏览器中呈现的Web应用程序,在HTTP响应中发出一个形式为:

Content-Disposition: attachment; filename=文件名

Content-Disposition标头。 filename参数可用于建议浏览器下载资源时使用的文件名称。然而RFC 2183(Content-Disposition)在section 2.3(The Filename Parameter)中指出,文件名只能使用US-ASCII字符:

当前[RFC 2045]语法约束 参数值(因此也就限制了 Content-Disposition文件名)为 US-ASCII。我们认识到允许文件名中使用任意字符集的优越性,但是本文档无法定义必要的机制。

尽管如此,有经验的证据表明,大多数流行的Web浏览器似乎仍允许使用非US-ASCII字符(由于缺乏标准),但它们对文件名的编码方案和字符集规范存在争议。问题是,如果需要将文件名“naïvefile”(不带引号,并且第三个字母为U +00EF)编码到Content-Disposition头中,则流行浏览器使用的各种方案和编码是什么?

对于这个问题,所谓流行的浏览器是:

  • 谷歌浏览器
  • 苹果Safari浏览器
  • 微软Internet Explorer或Edge浏览器
  • 火狐浏览器
  • 欧朋浏览器

已经在移动版Safari上运行成功(如@Martin Ørding-Thomsen所建议的使用原始UTF-8编码),但同一设备上的GoodReader无法正常工作。有什么想法吗? - Thilo
还可以参考这个类似的问题 - juergen d
1
Kornel的回答证明了这是最简单的方法,只要你可以设置路径的最后一段; 再加上 Content-Disposition: attachment - Antti Haapala -- Слава Україні
1
最新的RFC规范为**RFC 8187**,它取代了RFC 5987。 - Константин Ван
23个回答

6
从.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

2
我将这个与 https://dev59.com/zXVC5IYBdhLWcg3wliGe#56797567 中的“ASCII折叠”结合起来,生成了h.FileName。注意:h.FileName不能包含引号字符(来自ContentDispositionHeaderValue源代码:“只允许边界引号”)。 - Miha Pirnat

6

这是一个关于客户问题的更新,以下是我今天尝试的一些内容:

  • 除了使用日语配置的Safari浏览器外,我们的客户测试过的所有浏览器都最好使用文件名为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; filename * = utf-8''texturlencoded.pdf进行设置。在上述任何一种情况下,Rfc5987 / 8187的任何功能似乎都没有被正确处理。

2
这是一个不错的更新。你能详细说明一下你尝试过的具体测试吗? - Brad

5
如果您正在使用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);
}

1
最好使用 encodeURI(str)。例如,文件名中包含日期: encodeURIComponent('"Kornél Kovács 1/1/2016') => "Kornél Kovács 1%2F1%2F2016" 与 encodeURI('"Kornél Kovács 1/1/2016') => "Kornél Kovács 1/1/2016" - gdibble
这在Safari中可以工作吗? - Flimm

4

最终在我的“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 和 "安全字符" 的情况下,它才使用 filename="..." 的标准方式;否则,它将添加 filename*=UTF-8'' url-encoded 方式。根据 这个特定的测试案例,它应该可以在 MSIE9 及以上版本以及最近的 FF、Chrome、Safari 上工作;在较低版本的 MSIE 上,它应该提供包含文件名的 ISO8859-1 版本,并对不属于此编码的字符使用下划线。

最后说明:每个头部字段的最大大小为 8190 字节。UTF-8 每个字符最多可以达到四个字节;在进行原始 URL 编码之后,每个字符是 x3 = 12 个字节。非常低效,但理论上仍然可能在文件名中有超过 600 个笑脸 %F0%9F%98%81。


...但是可传输的文件名长度也取决于客户端。刚发现在MSIE11中最多可以传输[89个笑脸].pdf文件名。在Firefox37中,最多为[111x] .pdf。Chrome41将文件名截断到第110个笑脸。有趣的是,后缀传输正常。 - apurkrt

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

1
请注意,这个答案是从2012年的。 - Flimm

2

对于需要使用JavaScript编码标头的人,我发现以下函数效果很好:

function createContentDispositionHeader(filename:string) {
    const encoded = encodeURIComponent(filename);
    return `attachment; filename*=UTF-8''${encoded}; filename="${encoded}"`;
}

这是基于Nextcloud下载文件时的做法。文件名首先以UTF-8编码显示,可能为了与一些浏览器兼容,文件名也会出现没有UTF-8前缀的情况。


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

0

经典 ASP 解决方案

现在大多数现代浏览器都支持将 Filename 作为 UTF-8 传递,但是就像我使用的基于 FreeASPUpload.Net 的文件上传解决方案一样(该网站已不存在,链接指向 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

感谢Pure ASP文件上传,通过在我的代码中实现include_aspuploader.asp中的BytesToString()函数,我成功让UTF-8文件名正常工作。


有用的链接


0
在 PHP 中,只需要使用标准函数 mb_encode_mimeheader() 即可。

0
这段PHP代码对于所有浏览器(Chrome,Safari,Firefox,IE11)都有效...
header('Content-Disposition: attachment; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName) . ';');

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接