PHPで返す304 Not Modified(2)

 前回は、Last-ModifiedとIf-Modified-Sinceを用いてファイル更新の有無を確認しましたが、同じURLが状態に応じて別のファイルを表示する事がある場合には、上手くゆかない場合があります。例えば、ユーザーログイン時には指定した画像、そうでなければ別の画像を表示する、という場合、最終更新日のみを見る場合は新しい方だけが使われる事になります。
 この問題を解決するため、今度はETagとIf-None-Matchを利用して目的を果たしてみました。ファイルの同一性確認のためには、対衝突性の高いSHA-256によるハッシュ値を使うことにします。

 なお、ETag/If-None-Matchの方が汎用性は高いので、タグ文字列さえ適切に設定できるのであれば、Last-Modified/If-Modified-Sinceよりも汎用性が高いです。実際には、両方使うのが好ましいようですが。

<?php
/**
 * ファイル内容をHTTPレスポンスヘッダーと標準出力に書き出します。
 * クライアントから渡されたIf-None-Matchリクエストヘッダーを確認し、返すファイルの内容が異なっていなければHTTPステータス304を、
 * 変更されていればETagレスポンスヘッダー、Content-Lengthレスポンスヘッダー、Content-Typeレスポンスヘッダーを出力します。
 * ファイルが存在しなければHTTPステータス404を出力します。
 *
 * @param string $filename 読み込もうとするファイルの名前。
 * @param string $mime ファイルのMIMEタイプ。自動で確認するならNULL、Content-Typeレスポンスヘッダーが不要ならFALSE。
 * @param string $download_as ファイルを「名前をつけて保存」する場合、ファイル名。ブラウザでの閲覧用の場合FALSE。
 * @param boolean $use_include_path readfile()の$use_include_path引数と同じ。 
 * @param resource $context コンテキストストリームリソース。
 * @return int HTTPステータスを返します。新しいコンテンツを返す場合は200、ファイルに変更がなければ304、ファイルの読み込みに失敗した場合は404を返します。
 */
function respondcontents_etag($filename, $mime = null, $download_as = false, $use_include_path = false, $context = null)
{
    if (!is_file($filename)) {
        // ファイルが見つからない
        header('HTTP/1.0 404 Not Found') ;
        return 404 ;
    }

    $hash = @hash_file('sha256', $filename) ;
    if (false !== $hash) {
        $inm = get_if_none_match() ;
        if (false !== $inm && $hash === $inm) {
            // 更新されていない
            header('HTTP/1.1 304 Not Modified') ;
            return 304 ;
        }

        // ETag、Content-Lengthを設定
        header('ETag: "' . $inm . '"') ;
        $filesize = @filesize($filename) ;
        if (false !== $filesize) header('Content-Length: ' . $filesize) ;
    }

    if (false !== $mime) {
        if (null === $mime) {
            $finfo = @finfo_open(FILEINFO_MIME) ;
            $mime = @finfo_file($finfo, $filename) ;
            @finfo_close($finfo) ;
            unset($finfo) ;
        }
        header('Content-Type: ' . $mime) ;
    }

    if (false !== $download_as) {
        set_content_disposition($download_as) ;
    }

    $result = @readfile($filename) ;
    if (false === $result) {
        // ファイル内容を取得できない
        header('HTTP/1.0 404 Not Found') ;
        return 404 ;
    }

    return 200 ;
}

/**
 * If-None-Matchリクエストヘッダーの値を、""の中のみ取得します。
 *
 * @return string タグ文字列。If-None-MatchリクエストヘッダーがなければFALSE。
 */
function get_if_none_match()
{
    // リクエストヘッダーの値は実行中不変なので、get_if_modified_since()の値は一度だけ評価する
    static $etag = null ;
    if (null !== $etag) return $etag ;

    $rh = apache_request_headers() ;
    if (!isset($rh['If-None-Match'])) {
        $etag = false ;
        return false ; // If-None-Matchがない
    }

    $rh = $rh['If-None-Match'] ;
    if (preg_match('/"(.*)"/', $rh, $match) {
        $etag = $match[1] ;
    }
    else {
        $etag = false ;
    }

    return $etag ;
}
?>

 set_content_disposition()関数は、前回と同じです。
 RangeやIf-Rangeやらへの対応とか、HTTPモジュールを使えばもっとあっさりいけるとか、その辺も前回と変わりません。