PHP 實現文件下載時,`Content-Disposition` 標頭中的中文文件名在不同瀏覽器編碼規則不一致:IE/Edge 需 URL 編碼、Firefox 支援 RFC 5987 `filename*` 格式、Chrome/Safari 等現代瀏覽器支援 UTF-8 直傳。直接輸出中文會導致文件名亂碼、截斷或下載失敗。
<?php
/**
* 跨瀏覽器安全輸出下載文件名
* @param string $filename 原始文件名 (UTF-8)
* @return string 適配當前瀏覽器的 Content-Disposition 值
*/
function getContentDisposition($filename) {
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
// URL 編碼,空格轉 %20 而非 +
$encoded = str_replace('+', '%20', urlencode($filename));
if (preg_match('/MSIE|Trident|Edge/i', $ua)) {
// IE / Edge (舊版) / Trident 內核
return 'attachment; filename="' . $encoded . '"';
} elseif (preg_match('/Firefox/i', $ua)) {
// Firefox 支援 RFC 5987
return "attachment; filename*=utf-8''" . $encoded;
} else {
// Chrome, Safari, Opera, 現代 Edge (Chromium) 等
// 直接發送 UTF-8 (RFC 6266 建議雙重保險)
return 'attachment; filename="' . $filename . '"; filename*=utf-8\'\'' . $encoded;
}
}
// --- 使用示例 ---
$filename = "年度報表_2024_中文版.xlsx";
$filepath = "/data/reports/" . $filename;
if (!file_exists($filepath)) {
header("HTTP/1.1 404 Not Found");
exit("文件不存在");
}
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($filepath));
header('Content-Disposition: ' . getContentDisposition($filename));
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Expires: 0');
// 輸出文件流 (大文件分塊讀取避免內存溢出)
$handle = fopen($filepath, 'rb');
while (!feof($handle)) {
echo fread($handle, 8192);
flush(); // 及時發送到客戶端
}
fclose($handle);
exit;
現代瀏覽器 (Chrome 65+, Firefox 52+, Edge 79+, Safari 13+) 均支援 `filename*` 參數。可簡化為統一發送雙標頭,無需 UA 判斷:
function getModernContentDisposition($filename) {
$encoded = str_replace('+', '%20', urlencode($filename));
// 同時提供 filename (向後兼容) 和 filename* (標準 UTF-8)
// 瀏覽器會優先使用 filename*
return 'attachment; filename="' . $encoded . '"; filename*=utf-8\'\'' . $encoded;
}
urlencode() 將空格編碼為 +,但 HTTP 標頭要求 %20,必須替換。str_replace(['"', "\r", "\n", ';'], '', $filename)。
// 關閉輸出緩衝,避免佔用 PHP 內存
while (ob_get_level()) ob_end_clean();
// 分塊輸出
$chunkSize = 8192; // 8KB
$handle = fopen($filepath, 'rb');
while (!feof($handle)) {
echo fread($handle, $chunkSize);
flush(); // 強制發送到網絡層
}
fclose($handle);
生產環境大文件下載建議支援 Range 請求,可參考 x-sendfile (Nginx/Apache 模塊) 或 PHP 實現範圍讀取,減少腳本佔用資源。
realpath() 確保在允許目錄內。Content-Type 為具體 MIME 或 application/octet-stream 強制下載。Content-Security-Policy 相關標頭。| 瀏覽器 | 版本 | filename (URL編碼) | filename* (RFC 5987) | UTF-8 直傳 |
|---|---|---|---|---|
| IE | 6-11 | ✓ | ✗ | ✗ |
| Edge (Legacy) | < 79 | ✓ | ✗ | ✗ |
| Edge (Chromium) | 79+ | ✓ | ✓ | ✓ |
| Chrome | 65+ | ✓ | ✓ | ✓ |
| Firefox | 52+ | ✓ | ✓ | ✓ |
| Safari | 13+ | ✓ | ✓ | ✓ |
針對中文文件名下載亂碼,核心在於根據瀏覽器特性發送正確編碼的 `Content-Disposition`。現代項目推薦使用 RFC 6266 雙標頭方案 (`filename` + `filename*`) 一體化兼容;老項目維護可保留 UA 判斷邏輯。配合大文件分塊輸出、路徑安全校驗、Range 請求支撐,可構建健壯的下載服務。