PHP/ファイル入出力
ファイルとHTMLフォーム
[編集]PHPには、(サーバー側にある)ファイルを読書きする機能があります。
ファイル読込み
[編集]- 例
<?php declare(strict_types=1); header("Content-Type: text/plain"); $fp = fopen("/etc/motd", "r"); if (!$fp) { die('Fail: fopen("/etc/motd", "r");'); } var_dump($fp); while ($line = fgets($fp)) { echo "> $line"; } fclose($fp);
- 実行結果(/etc/motdが開けた場合)
resource(5) of type (stream) > System Maintenance Notice > > This weekend, we will shut down at 3:00 p.m. on Saturday for system updates.
- 実行結果(/etc/motdが開けなかった場合)
PHP Warning: fopen(/etc/motd): Failed to open stream: No such file or directory in /workspace/Main.php on line 5 Fail: fopen("/etc/motd", "r");
- /etc/motd
System Maintenance Notice This weekend, we will shut down at 3:00 p.m. on Saturday for system updates.
- fopen()関数は、Cと同じ名前ですが大きく機能が拡張されており、ネットワーク上のリソースも開くことができます。
- ここではローカルファイルシステムの /etc/motd を読出し、内容を表示しようとしています。
- /etc/motd は、UNIXにログインした時に表示するメッセージが保存されているファイルです。
- fopen() に限らず、関数は戻値で成功/失敗を表しています。
- ここで失敗を無視すると、それ以後の $fp を使った処理はことごとくエラーとなり、開けなかったハンドルで fclose() するという醜態に至ります。
- var_dump()の結果から、fopen() が返す値は、リソース型だとわかります。
例外によるエラーハンドリング
[編集]- 例
<?php declare(strict_types=1); header("Content-Type: text/plain"); set_error_handler(function ($errno, $errstr, $errfile, $errline) { throw new ErrorException($errstr, 0, $errno, $errfile, $errline); }); try { $fp = fopen("/etc/motd", "r"); var_dump($fp); while ($readString = fgets($fp)) { echo "> $readString"; } fclose($fp); } catch (Exception $e) { echo sprintf("%s(%d): %s", $e->getFile(), $e->getLine(), $e->getMessage()), PHP_EOL; }
書込み
[編集]PHPをウェブサーバー上で動かしている場合のファイル書込みは、セキュリティ上の脅威に直結するので慎重になるべきです。
ここでは、ごく小さな永続オブジェクトが必要な、アクセスカウンターを例に取ります。
- 例
<?php header("Content-Type: text/plain"); $fp = fopen("counter.txt", "c+"); if (!$fp) { die('Fail: fopen("counter.txt", "c+");'); } if (flock($fp, LOCK_EX)) { $counter = (int) fgets($fp); $counter++; rewind($fp); if (fwrite($fp, $counter) === false) { print "ファイル書き込みに失敗しました"; } flock($fp, LOCK_UN); } else { echo "flock: error"; } fclose($fp); echo "COUNT: ", $counter;
- 実行結果(1)
COUNT: 1
- 実行結果(2)
COUNT: 2
- 実行結果(3)
COUNT: 3
- fopen()のモード "c+" はR/Wで開き、ファイルがなければ新規に作ります。
- flock()は、ファイルへのアクセスをアトミックにする関数です。
- ロックを掴んだあと、ファイルから1行読み出し、整数に変換しインクリメントしたあと先頭まで seek() しインクリメントした値を書込みます。
シンプルでダーティーなログインフォーム
[編集]- login.php
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>シンプルでダーティーなログインフォーム</title> </head> <body> <?php ?> <?php if (!array_key_exists('username', $_POST)): ?> <!-- GET Method --> <form action="login.php" method="POST""> <div><label for="username">ユーザー名:</label><input type="text" name="username" id="username"></div> <div><label for="password">パスワード:</label><input type="password" name="password" id="password"></div> <input type="submit" value="ログイン"> </form> <?php elseif ($_POST['username'] == "UserID" && $_POST['password'] == "PassWord" ): ?> <!-- POST Method --> <p>ログイン成功</p> <?php else: ?> <!-- POST Method --> <p>ユーザー名 または パスワード が違います。<p> <?php endif; ?> </body> </html>
- このスクリプトは、ファーム入力とフォームの両方が含まれており
- 分岐コード
<?php if (!array_key_exists('username', $_POST)): ?>
- スーパーグローバル変数 $_POST が(間違っていないかではなく)あるのかをテストして、なければGETメソッドと判断しています。
- GETメソッドならばフォーム
<!-- GET Method --> <form action="index.php" method="POST""> <div><label for="username">ユーザー名:</label><input type="text" name="username" id="username"></div> <div><label for="password">パスワード:</label><input type="password" name="password" id="password"></div> <input type="submit" value="ログイン"> </form>
- を生成します。
- POSTメソッドならば
- ユーザー名とパスワードの両方が一致したら
<!-- POST Method --> <p>ログイン成功</p>
- を生成します。
- 片方でも一致しなければ
<!-- POST Method --> <p>ユーザー名 または パスワード が違います。<p>
- を生成します。
- (ここでどちらが不一致だったかを教えてはいけません。ブルートフォース攻撃が格段に易しくなります。)
- ファームのレンダリング例
どこがダーティーか?
[編集]このコードには以下のような欠点があり、アジャイルにはともかく実務には使えません。
- スクリプトに認証情報がハードコードされている
- サーバーに侵入されなければ、JavaScript のように丸見えにはなりませんが「サーバーに侵入されない」前提で認証関係のコードを書いてはいけません。
- メソッドを拠り所にフォームと認証を切り替えている
- 細工したユーザーエージェントからならば、メソッドを自由に切替えてチャレンジできます。
- チャレンジ回数に限界がない
- ブルートフォース攻撃に遭った時に、回数制限がないのは致命的な欠点です。
サーバーからのダウンロード
[編集]概要
[編集]PHPでダウンロードをブラウザに問いかけるには、下記のように header 関数というのを使って、ブラウザに問い掛けできます。
- コード例
<?php // 画像のパスとファイル名 (拡張子ごと) $fpath = "/var/www/html/phpgra2.png"; $fname = "phpgra2.png"; // ヘッダーの設定 header('Content-Type: application/octet-stream'); header('Content-Length: ' . filesize($fpath)); header('Content-Disposition: attachment; filename="' . $fname . '"'); // 環境によっては必要 ob_end_clean(); // 画像のダウンロード readfile($fpath); ?>
書式は
<?php $パス変数 = 'パスのアドレス'; $ファイル変数 = 'ファイル名'; header('Content-Type: application/octet-stream'); header('Content-Length: ' . filesize($パス変数)); header('Content-Disposition: attachment; filename="' . $ファイル変数 . '"'); ob_end_clean(); readfile($パス変数); ?>
です。
実験のさいには、あらかじめ画像データを作成しておいてください。
そして、ブラウザから、上記のPHPを実行します。(コマンドラインから実行しても、意味不明の文字列が表示されるだけです。)
成功すれば、ページ起動時に
- 「次のファイルを開こうとしています:」
と出て、「キャンセル」または「OK」のボタンが出てきます。
Content-Type: application/octet-stream の 「octet-stream 」は、種類を特定しないバイナリデータであることを宣言しています。画像データなどをダウンロードさせたい場合は画像ならバイナリ形式ですので、この 「octet-stream」を指定してもダウンロード可能です。
ダウンロードしたいファイルのファイル形式によっては、Content-Type で具体的に指定することもできます。
画像の場合、
- PNG画像なら image/png をつかって
Content-Type: image/png
と指定しても、かまいません。 - もしGIF画像なら image/gif で
Content-Type: image/gif
とも書けます。 - JPEG画像なら
Content-Type: image/jpeg
とも書けます。
画像以外でも、
- もしPDFをダウンロードさせるなら application/pdf のようになり、
Content-Type: application/pdf
とも書けます。 - あるいは、もしテキストファイルなら text/plain で、
Content-Type: text/plain
とも書けます。
さて、
header('Content-Disposition: attachment; filename="' . $fname . '"');
は、ダウンロードしたときのファイル名を上記コードでは $fname で指定しています。なので、ほかの名称でも構いません。たとえば
header('Content-Disposition: attachment; filename="' . "test" . '"');
とすれば、ダウンロードされたファイル名は「test」になります。
ダウンロード開始は readfile関数でなくても、 file_get_contents 関数でもダウンロード問い掛けを出来ます。
環境や、アップロードするファイルの種類によっては
ob_end_clean();
が必要です。
これがないと、ファイルにバッファ内の余計なデータがついたままブラウザに送信されてしまい、ダウンロード自体はできても、読込みエラーになってしまい、さっかくダウンロードした価値が無くなってしまいます。
実際の例
[編集]上記のコードだと、ページが表示される前にダウンロードが始まってしまう。そのため、とても見づらくなります。
上記のPHPコードにprintなどの命令を書いても、うまく動作しないです。(基本的に、ダウンロード用のリンクでは、画像表示や文字表示は、あまり機能しないです。)
実務的な方法としては、別のHTMLファイルで上記PHPコードにアクセスするリンクを配置し、
- コード例
<a href="dlTest.php">ダウンロード</a>
- (※ これはHTMLファイルです。PHPではありません)
のようにして、このHTMLファイルに先にリンクしてもらうようにするのが良い。
すると、先にこのHTMLだけが表示されます。
そして、「ダウンロード」リンクをクリックすると、ページはそのままで(このHTMLが表示されたままで)、ダウンロードのポップアップが出るので、あとはブラウザ側でユーザーにダウンロードしてもらえば済む。