162
170

More than 5 years have passed since last update.

【PHP】正しいダウンロード処理の書き方

Last updated at Posted at 2019-05-28

ワリと需要のある処理だと思いますが、改めてググってみるとあまりマネして欲しくないコードが散見されたため、この記事を書いてみました。

  • 検索結果上位のページのコードをコピペで使ってる人
  • 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() を使うのが正しいです。

この点についても、間違って説明しているページがあるようですので、注意してください。


  1. Internet Explorer のような「普通ではないブラウザ」があるからこその苦肉の策だったと言えますが、もうそろそろこういうバッドノウハウは止めた方が良いと思います。 

  2. 出力内容を出力せずにメモリ内に溜め込んでおき、後から吐き出す方法のこと。 

162
170
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
162
170