3流プログラマのメモ書き

元開発職→社内SE→派遣で営業支援の三流プログラマのIT技術メモ書き。 このメモが忘れっぽい自分とググってきた技術者の役に立ってくれれば幸いです。(jehupc.exblog.jpから移転中)

(PHP)PHPでファイルをダンロード(アクセス制限付き)

PHPでWebサーバ参照域以外のファイルをダウンロードさせる方法です。
しかも、アクセス制限つきです。

要件は下記の通り。
1.ダウンロードさせるファイルはWebサーバの参照域外(つまりURL直打ちでダウンロードさせない)。
2.入力フォームのHTMLを用意し、そこで入力した値がOKならダウンロードさせる。(パスワードみたいなもの)。
3.ダウンロードダイアログ表示と同時にブラウザはサンクスページを表示させる(Vectorみたいな感じ。本当はダウンロード完了後に開きたかったけどかなり無理があるみたいなので、この方法で妥協)。

まず、1.の方法ですが、これはPHPからHTTPヘッダでタイプがapplication/octet-stream(バイナリ)であることとファイルサイズをブラウザに教えてやり、あとはファイルをロードしてやればいいだけみたいです。
で、ダウンロード部分をそっくり関数化されてるこちらのページをそのまま使わせてもらいました。

2.は入力フォームのHTMLで値を入力させ、POST(GET)先のPHPでDBなり、定数なりと比較することで可能です。

で、一番問題だったのは3.でした。
ファイルをダウンロードさせる関数呼び出し後にLocationで別ページに飛ばそうと思ったのですが、うまくいきません。
ということで、さがしていると「PHPでファイルをダウンロードさせるには」のページに行き着きました。
で、SourceForge.jpの動きを研究されてたみたいですが、html の head に refresh でダウンロード用ファイルへのパスを渡すと方法で実現されてることを突き止めたみたいです。

これで自身の要件に沿ったページを作ることができました。
流れとしては下記のような感じです。

down.html      入力フォーム用HTML(パスワード等を入力)
 ↓(POSTかGETで投げる)
check_msg.php  パスワード等チェック(DBとかと)とダウンロードサンクスページ
               チェックがOKならサンクスページのヘッダに

今回Smartyを使って表示とロジックを分離したので、そのサンプルソースをあげておきます。

check_msg.php
最初のHTMLの入力正当性検証とサンクス・エラーページの表示、ダウンロードページへの遷移処理

<?php
/*
 * パスワードのチェックとサンクスメッセージ、ダウンロードページへの遷移処理
 */
 
//POST情報取得
$postInfo = $_POST;
 
//入力チェック
//ここで入力された情報が正しくてダウンロードさせてよいかチェックする!!
$res = 入力チェック($postInfo);
 
if (res){
$msg = 'お使いのブラウザや設定によっては「セキュリティ保護のため...」という情報バーが表示される場合があります。<br/>
その場合は情報バーを押下していただき、ファイルのダウンロードを選択してください。<br/>
もしくは<a href="down.php?passwd=' . $postInfo[passwd] . '">こちらのリンク</a>からダウンロードしてください。';
//ダウンロードページ遷移のヘッダを埋め込む

$metaRefresh = '<meta http-equiv="Refresh" content="1;URL=down.php?passwd=' . $postInfo[passwd] . '">';
displaySmarty($msg,$metaRefresh,true);
}else{
$msg = 'エラーだよん';
displaySmarty($msg,"",false);
}
 
/**
 * Smartyで表示する処理
 * @msg 本文に表示するメッセージ
 * @metaRefresh メタタグに入れる文字列(ダウンロード時はダウンロードページへのリダイレクトが入る)
 * @okFlg 処理がOKか否か。OKならタイトルにサンクスメッセージ、NGならエラーを表示する
 */
function displaySmarty($msg,$metaRefresh , $okFlg){
//Smarty読み込み
require_once('../Smarty/libs/Smarty.class.php');
//Smartyオブジェクト作成
$smarty = new Smarty();
//Smartyがらみのディレクトリ設定
$smarty->template_dir = "../Smarty/libs/templates";
$smarty->compile_dir = "../Smarty/libs/templates_c";
$smarty->cache_dir ="../Smarty/libs/cache";

if ($okFlg){
$title="ダウンロードありがとうございます";
}else{
$title="エラーが発生しました。";
}

$smarty->assign("msg",$msg);
$smarty->assign("metaRefresh",$metaRefresh);
$smarty->assign("title",$title);

//テンプレート表示
$smarty->display("check_msg.tpl");
}
?>


check_msg.tpl
check_msg.phpでのSmartyテンプレート

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
各種ヘッダー。。。
 
<!--ダウンロードページへの遷移-->
{$metaRefresh}
 
</head>
<body>
 
<h1>{$title}</h1>
 
<p>
{$msg}
</p>
 
</body>
</html>


down.php実際にダウンロードさせる処理。

<?php
 
/**
 * ダウンロードページ(実際にファイルをダウンロードさせる)
 */
 
//GET情報取得
$getData = $_GET;
 
//入力チェック。もし、このURLが直打ちされたら駄目だからここでもチェックさせる。
//ここで入力された情報が正しくてダウンロードさせてよいかチェックする!!
$res = 入力チェック($postInfo);
 
if ($res){
//Webサーバ参照域以外のファイルダウンロード開始
download_file("../file.exe");
}
 
/*
 * ファイルをダウンロード
 引数はターゲットファイルへの相対又は絶対パス
 */
function download_file($path_file)
{
/* ファイルの存在確認 */
if (!file_exists($path_file)) {
die("Error: File(".$path_file.") does not exist");
}
/* オープンできるか確認 */
if (!($fp = fopen($path_file, "r"))) {
die("Error: Cannot open the file(".$path_file.")");
}
fclose($fp);
/* ファイルサイズの確認 */
if (($content_length = filesize($path_file)) == 0) {
die("Error: File size is 0.(".$path_file.")");
}
/* ダウンロード用のHTTPヘッダ送信 */
header("Content-Disposition: inline; filename=\"".basename($path_file)."\"");
header("Content-Length: ".$content_length);
header("Content-Type: application/octet-stream");
/* ファイルを読んで出力 */
if (!readfile($path_file)) {
die("Cannot read the file(".$path_file.")");
}
}
?>


この方式の問題はURL down.php と直にダウンロードされるのを防ぐために、check_msg.phpとdown.phpの2つで2回も最初のHTMLで入れたデータをチェックしているということです。
これがDBにアクセスして、OKなユーザならとかになるとパフォーマスは劇的に悪いと思うので改善の余地ありですね。
なんかいい方法ないでしょうか。。