Content-Disposition ヘッダを使って日本語のファイル名でダウンロードさせる

ブラウザに次のような HTTP レスポンスヘッダを出力すると、任意のファイル名で出力内容をダウンロードさせることができる。

Accept-Ranges: bytes
Content-Type: application/octet-stream
Content-Disposition: attachment; filename=ファイル名
Content-Length: ファイルサイズ

問題はファイル名が日本語の文字など、ASCII 以外の文字を含む場合である。ASCII 以外の文字をエンコードする方法はさまざまあり、ブラウザによって対応がまちまちである。
そこで次の PHP コードを用いてテストを行った。

手元の環境でテストした結果はこちら。

Opera 18 Chrome 31 Firefox 26 Opera 12 MSIE 8 MSIE 11 Safari 4-osx Safari 5.1-win
UTF-8 Raw OK OK OK OK NG NG NG OK
UTF-8 URL Encoded OK OK NG OK OK OK NG OK
UTF-8 Base64 OK OK OK NG NG NG NG NG
RFC 2231 OK OK OK OK NG OK NG NG
Shift_JIS Raw OK OK OK OK OK OK OK OK
Shift_JIS URL Encoded NG NG NG NG NG NG NG NG

比較的最近のブラウザでは RFC 2231 形式に対応しているので、通常はこの形式を使えば問題は少ないだろう。
また古いブラウザでは、Shift_JIS で表現できる文字列の場合、Shift_JIS 文字列をそのまま記述すれば正しい結果が得られる。

PHP でのコード例

以上を踏まえてコードを記述すると、次のようになる。

<?php
header('Accept-Ranges: bytes');
header('Content-Type: application/octet-stream');
if (preg_match('/\bMSIE\b|\bSafari [12345]\b/', getenv('HTTP_USER_AGENT'))) {
	header('Content-Disposition: attachment; filename="' .
		mb_convert_encoding($filename, 'Shift_JIS', 'UTF-8') . '"');
}
else {
	header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($filename));
}
header('Content-Length: ' . filesize($file));
readfile($file);

注意点

ユーザの環境においてファイル名として使えない文字を指定したとき、その動作はブラウザによって異なる。次のページに書かれているようにエスケープするとよい。

<?php
$filename = preg_replace('/\\xE2\\x80\\xAE|\\xE2\\x80\\xAD/', '', $filename);
$filename = preg_replace('/\\s/u', '_', $filename);
$filename = preg_replace('{[\\\\/:*?"<>|]}', '', $filename);