1. 程式人生 > >【轉】正確處理瀏覽器在下載文件時HTTP頭的編碼問題(Content-Disposition)

【轉】正確處理瀏覽器在下載文件時HTTP頭的編碼問題(Content-Disposition)

彈出 強調 header tex 不一定 瀏覽器支持 接下來 解決方案 兼容

最近在做項目時遇到了一個 case :需要實現一個強制在瀏覽器中的下載功能(即強制讓瀏覽器彈出下載對話框),並且文件名必須保持和用戶之前上傳時相同(可能包含非 ASCII 字符)。 前一個需求很容易實現:使用 HTTP Header 的 Content-Disposition: attachment 即可,還可以配合 Content-Type: application/octet-stream 來確保萬無一失。而後一個需求就比較蛋疼了,牽扯到 Header 的編碼問題(文件名是作為 filename 參數放在 Content-Disposition 裏面的)。眾所周知, HTTP Header 中的 Content-Type 可以指定內容的編碼,可 Header 本身的編碼又該如何制定?甚至, Header 究竟是否允許非 ASCII 編碼呢? 如果放任編碼問題不管,那麽恭喜你,你一定會遇到在某個系統及瀏覽器下下載文件時文件名亂碼的情況。如果你嘗試搜索解決,那麽再一次恭喜你,你會找到一堆自相矛盾的解決方案(我可以負責任地告訴你,其中的99%都是不符合標準的 trick 罷了)。讓我們來看看到底應該如何優雅完美地解決這個問題吧! 為了探索這個問題,我走了不少彎路。從自己嘗試,到 Google 、百度(分別嘗試過中英文搜索),再到閱讀 Discuz 等經典項目的源碼,眾說紛紜、莫衷一是。最後我才想到回歸 RFC ,從標準文檔中找辦法,果然有所收獲。由於探究過程實在太曲折,我就先把標準做法寫下來。

應該這樣設置 Content-Disposition :
Content-Disposition: attachment;
                      filename="$encoded_fname";
                      filename*=utf-8‘‘$encoded_fname
其中,$encoded_fname指的是將 UTF-8 編碼的原始文件名按照 RFC 3986 進行百分號 urlencode 後得到的( PHP 中使用 rawurlencode() 函數)。這幾行也可以合並為一行,推薦使用一個空格隔開。 另外,為了兼容 IE6 ,請保證原始文件名必須包含英文擴展名!

好了,接下來我們來看看為什麽要這麽做以及為什麽能這麽做。 首先,根據 HTTP 1.1 協議規範( RFC 2616 Section 4 ), HTTP 消息格式其實是基於古老的 ARPA INTERNET TEXT MESSAGES ( RFC 822 Section 3 ),根據其規定,消息只能是 ASCII 編碼的。 RFC 2616 Section 2.2 又一次強調, TEXT 中若要使用其他字符集,必須使用 RFC 2047 的規則將字符串編碼為 ASCII 碼(事實上這個規則原本是針對 MIME 的擴展,使用的是 base64 編碼,格式與百分號編碼有很大不同)。總而言之,按照標準, HTTP Header 中的文本數據必須是 ASCII 編碼的。

filename="TEXT"
 ;這是 RFC 2616 標準,TEXT必須是 ASCII 字符且被認為就是“原文”
filename*=charset‘lang‘encoded-text
 ;這是按照 RFC 2047 擴展後的,註意格式上的細微區別,采用 base64 編碼(編碼結果也是 ASCII 字符)

然而,事實上在1999年 HTTP 1.1 標準推出之時, Content-Dispostion 這個 Header 尚不是正式標準的一部分,只不過是因為被廣泛使用而從 MIME 標準中直接借用過來了而已( RFC 2616 Section 19.5.1 )。因而幾乎沒有瀏覽器去支持 Content-Disposition 的多語言編碼特性這樣一個“擴展特性的擴展特性”(事實上, HTTP 1.1 草案中建議的使用 RFC 2047 來進行多語言編碼的特性從未被主流瀏覽器支持過)。 可是這個問題卻的確是現實需要的,所以瀏覽器就各自想出了一些辦法:

  • IE支持兩種格式的混合版:filename="encoded_text" (這裏采用的是百分號編碼)。本來按照 RFC 2616 ,引號內的部分應當直接被當作內容,就算它“看起來像是編碼後的字符串”;可是IE卻會“自動”對這樣的文件名進行解碼——前提是該文件名必須有一個不會被編碼的後綴名(即正常的英文字母後綴名)!
  • 其他一些瀏覽器則支持一種更為粗暴的方式——允許在 filename="TEXT" 中直接使用 UTF-8 編碼的字符串!

這兩類瀏覽器的行為是彼此互不兼容的。所以你可以判斷 UA 然後對IE使用前一種辦法,其他瀏覽器使用後一種,這樣便可以達到一般情況下能夠 just work 的效果( Discuz 就是這麽做的)。不過對於 Opera 和 Safari ,這樣做可能不一定有效。 時代在進步,2010年 RFC 5987 發布,正式規定了 HTTP Header 中多語言編碼的處理方式,應當采用類似 MIME 擴展的 parameter*=charset‘lang‘value 的格式,但是其中 value 應根據 RFC 3986 Section 2.1 使用百分號進行編碼,並且規定瀏覽器至少應該支持 ASCII 和 UTF-8 。隨後,2011年 RFC 6266 發布,正式將 Content-Disposition 納入 HTTP 標準,並再次強調了 RFC 5987 中多語言編碼的方法,還給出了一個範例用於解決向後兼容的問題——就是我在一開始給出的例子:

Content-Disposition: attachment;
                      filename="encoded_text";
                      filename*=utf-8‘‘encoded_text

在這個例子中,對於較新的 Firefox 、 Chrome 、 Opera 、 Safari 等瀏覽器,都支持新標準規定的 filename* ,並且會優先使用,所以盡管 filename=”encoded_text” 不被它們支持,仍然不會有問題;至於使用 UTF-8 只是因為它是標準中強制要求必須支持的。而對於舊版本的IE瀏覽器,它們無法識別後面的 filename* ,會自動忽略並使用舊的 filename 。這樣一來就完美解決了多瀏覽器的多語言兼容問題,既不需要 UA 判斷,也符合標準。 P.S. 為什麽 PHP 要使用 rawurlencode() 函數呢?因為這才是真正符合 RFC 3986 的“百分號URL編碼”,只是由於歷史原因,之前先有了一個 urlencode() 函數用於實現 HTTP POST 中的類似的編碼規則,故而只好用這麽一個奇怪的名字。兩者的區別在於前者會把空格編碼為%20,而後者則會編碼為+號。如果使用後者,那麽IE6在下載帶有空格的文件名時空格會變為加號。一般情況下,你是不會用到 urlencode() 這個函數的( Discuz 某些版本中錯誤地使用它來進行文件名編碼,從而導致空格變加號的BUG)。 via:Robot Shell

【轉】正確處理瀏覽器在下載文件時HTTP頭的編碼問題(Content-Disposition)