PHP
初心者
132
どのような問題がありますか?

この記事は最終更新日から1年以上が経過しています。

投稿日

更新日

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

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

  • 検索結果上位のページのコードをコピペで使ってる人
  • 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. 出力内容を出力せずにメモリ内に溜め込んでおき、後から吐き出す方法のこと。 

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
fallout

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
132
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー