PHP/ファイル入出力

< 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が表示されたままで)、ダウンロードのポップアップが出るので、あとはブラウザ側でユーザーにダウンロードしてもらえば済む。