ワリと需要のある処理だと思いますが、改めてググってみるとあまりマネして欲しくないコードが散見されたため、この記事を書いてみました。
- 検索結果上位のページのコードをコピペで使ってる人
-
application/force-download
は、いわゆる「バッドノウハウ」だという事を知らない人 -
readfile()
の正しい使い方を知らない人(特にファイルサイズの大きなファイルに対して使う場合)
などに参考にして頂ければ幸いです。
コード
function download($pPath, $pMimeType = null)
{
//-- ファイルが読めない時はエラー(もっときちんと書いた方が良いが今回は割愛)
if (!is_readable($pPath)) { die($pPath); }
//-- Content-Typeとして送信するMIMEタイプ(第2引数を渡さない場合は自動判定) ※詳細は後述
$mimeType = (isset($pMimeType)) ? $pMimeType
: (new finfo(FILEINFO_MIME_TYPE))->file($pPath);
//-- 適切なMIMEタイプが得られない時は、未知のファイルを示すapplication/octet-streamとする
if (!preg_match('/\A\S+?\/\S+/', $mimeType)) {
$mimeType = 'application/octet-stream';
}
//-- Content-Type
header('Content-Type: ' . $mimeType);
//-- ウェブブラウザが独自にMIMEタイプを判断する処理を抑止する
header('X-Content-Type-Options: nosniff');
//-- ダウンロードファイルのサイズ
header('Content-Length: ' . filesize($pPath));
//-- ダウンロード時のファイル名
header('Content-Disposition: attachment; filename="' . basename($pPath) . '"');
//-- keep-aliveを無効にする
header('Connection: close');
//-- readfile()の前に出力バッファリングを無効化する ※詳細は後述
while (ob_get_level()) { ob_end_clean(); }
//-- 出力
readfile($pPath);
//-- 最後に終了させるのを忘れない
exit;
}
使い方
download('path/to/file.zip');
//-- Content-Typeを自動判定せずに指定する時
download('path/to/file.zip', 'application/zip');
Internet Explorer を相手にする時
IE を使っている人にはエラーを出し、新しいウェブブラウザの案内をしてあげた方が、世の中の色んな人が幸せになると思いますが…。
//-- Internet Explorer
if (preg_match('/MSIE (\d{1,2})\.\d;/', getenv('HTTP_USER_AGENT'), $matchAry)) {
//-- IEでダウンロードしたファイルを直接開く操作を抑止する
header('X-Download-Options: noopen');
//-- 未対策のIE8以下且つSSL環境では以下の2行が必要(IEのバグ対策)
//-- https://support.microsoft.com/ja-jp/help/323308/internet-explorer-file-downloads-over-ssl-do-not-work-with-the-cache-c
if (getenv('HTTPS') && (int) $matchAry[1] <= 8) {
header('Cache-Control: public');
header('Pragma: public');
}
//-- application/force-downloadという存在しないContent-Typeを指定する仕方なしのバッドノウハウ(後述)
download('path/to/file.zip', 'application/force-download');
}
コードを見るだけでゲンナリしますね…。
Content-Type について
Content-Type に application/force-download
を指定すると書いているページをよく見かけます。
いかにもそれっぽい名前ですが、application/force-download
という MIME タイプは存在しません。
ウェブブラウザが、解釈できない Content-Type を受け取った際に、ダウンロード処理になるという挙動(その保証は無いが)を利用しているだけで、application/qiita-banzai
でも同じ事です。
ファイルをダウンロードさせる際は、Content-Disposition: attachment
を送信すれば、ダウンロード処理になるのが「普通のブラウザ」の挙動です。1
readfile() の正しい使い方
readfile() 自体にはメモリに関する問題はなく、 巨大なファイルを送ってもかまいません。
out of memoryエラーが出る場合は、 ob_get_level() で出力バッファリングを無効にしてください。
と書いてあるにも関わらず、readfile()
はメモリを食うから、fopen()
してから fread()
を使おう…のように説明しているページが散見されます。もちろん、それは間違いです。
readfile()
がメモリを大量に食ってしまう時は、出力バッファリング2が影響しています。それを無効化するのが、以下のコードです。
while (ob_get_level()) { ob_end_clean(); }
PHP の出力バッファリング機構はネストできるため、ネストしている場合でもそれらを全部無効化するために、このようなコードとなっています。
「破棄」と「フラッシュ」の違いに注意
今回のように、ファイルの内容を単純に出力する前に用いるのは、ob_end_flush()
ではなく ob_end_clean()
です。
-
- 出力バッファを「破棄」してから、出力のバッファリングをオフにする。
-
- 出力バッファを「フラッシュ(出力)」してから、出力のバッファリングをオフにする。
もしダウンロード処理までに出力バッファがあった場合、ob_end_flush()
を使うと、そのバッファの中身が出力されます。
そうすると、出力された中身の分だけ Content-Length の値と辻褄が合わなくなってしまい、ファイルを全てダウンロードする前にダウンロード処理が終了してしまう事になりますので、ob_end_clean()
を使うのが正しいです。
この点についても、間違って説明しているページがあるようですので、注意してください。