PHPで作るチャット(6) データ送信に関する問題の修正

2017年7月20日(更新: 2017年12月9日)

前回は、フォームから送信したデータをファイルに書き込み、その後書き込んだデータを読み込んでページに表示する方法について紹介しました。

今回は、フォームからのデータ送信に関する様々な問題を修正していきます。

データを何度も送信してしまう問題

前回は2つのメッセージを書き込みました。ここで、以下のように3つ目のメッセージを書き込んでください。

チャットにメッセージを書き込む

この書き込み自体は問題なく行われ、以下のように書き込んだ行が表示されます。

チャットに書き込まれた新しい行が表示される

問題はここからです。ここでページを更新再読み込み)してみてください。

お使いのブラウザによって若干異なりますが、次のような「フォーム再送信の確認」のダイアログが表示されると思います。

フォーム再送信の確認のダイアログ

「続行」を押すと、先ほど書き込んだ内容が再び書き込まれてしまいます。

フォーム再送信によって同じテキストが送信されてしまう

そしてこの現象は、更新するたびに起こります。つまり、更新する回数文だけ同じ行が増えてしまいます。これは、送信したPOSTのデータが残っているために起こる問題です。

(メッセージの最後に余計な半角スペースが入っていますが、後ほど修正しますので、今は無視してください)

HTTPヘッダによるリダイレクト

この問題を回避するには、データ送信後にPOSTデータを送信する前の元のページにジャンプ(リダイレクト)させる必要があります。これを行うには、データを書き込んだ処理の後に以下のスクリプトを追加します。

<?php

    ...

    if (isset($_POST['message'])) {
      $message = $_POST['message'];
      //書き込みモードでファイルを開く
      $fp = fopen($LOG_FILE_NAME, "a") or exit($LOG_FILE_NAME . "が開けません");

      // | を区切り文字として2つのデータを繋げて書き込む
      fwrite($fp, $name . "|" . $message . "\n");

      // リダイレクトのためのHTTPヘッダを送信
      header("Location: " . $_SERVER['PHP_SELF'], true, 303);
    }

    ...

PHPの関数 header は、HTTPヘッダを送信する関数です。

(HTTPヘッダ自体の説明は省略します。この関数についての詳しい説明は次のサイトをご覧ください → PHP: header – Manual

リダイレクトを行うためには、以下のようなヘッダを送信します。

header("Location: リダイレクト先のURI", true, 303)

第2引数はステータスコードも送信することを表し、第3引数に送信するステータスコードを書きます。ステータスコードについても今は詳しく説明しません。今はこのように書くとリダイレクトが可能ということをご理解いただければと思います。

今回はリダイレクト先に $_SERVER[‘PHP_SELF’] を指定しました。

まず、$_SERVER という変数はサーバーによって生成される変数(配列)で、ヘッダやパス、スクリプトの位置などの情報を持っています。

このうちの PHP_SELF は、現在実行しているスクリプトのファイル名が格納されています。

現在実行しているスクリプトのファイル名です。ドキュメントルートから取得されます。例えば、http://example.com/foo/bar.php というアドレス上にあるスクリプトでは $_SERVER[‘PHP_SELF’] は /foo/bar.php となります。
PHP: $_SERVER – Manual

今回作っているチャットでは、その元ファイルである chat.php を指すことになります。

したがって、以上の処理により、フォーム送信後、リダイレクトによって元のファイルにジャンプすることになります。先ほどのようにフォームを送信して更新を押しても「フォーム再送信の確認」が現れず、同じデータが送信されなくなっています。

区切り文字を送信できてしまう問題

今回、名前とメッセージは「|」という区切り文字で分けています。しかし、現時点では、この文字をデータに含めて送信することができてしまいます。

区切り文字を含んだ名前を送信する例

上のような名前を送信した場合、名前の途中に区切り文字が含まれているために、名前の一部がメッセージと誤認されてしまいます。

不適切な区切り文字によるメッセージの誤認

したがって、以下の条件を満たすように区切り文字と処理を見直す必要があります。

  • 普通にチャットで使用する可能性の高い文字は区切り文字にしない
  • 区切り文字が名前やメッセージなどに含まれている場合は、使用できない文字が含まれていることを利用者に伝え、送信できないようにする

区切り文字を複雑にする

区切り文字は「文字」ではなく「文字列」とすることができます。つまり、複数の文字を組み合わせたものを区切り文字とすれば、誤って入力される心配は少なくなります。

そこで、区切り文字(文字列)を格納する変数を作り、PHPスクリプトを以下のように書き換えます。変更箇所をハイライトしています。

<!-- chat.php -->

<?php
    // データを書き込むファイルの名前
    $LOG_FILE_NAME = "log.txt";

    // 区切りのための文字列
    $SPLIT = "|-|";

    // 名前を格納する変数
    $name = "";
    // メッセージを格納する変数
    $message = "";

    // 送信された名前とメッセージを変数に代入
    if (isset($_POST['name'])) {
      $name = $_POST['name'];

      if ($name == "") {
        $name = "noname";
      }
    }

    if (isset($_POST['message'])) {
      $message = $_POST['message'];
      //書き込みモードでファイルを開く
      $fp = fopen($LOG_FILE_NAME, "a") or exit($LOG_FILE_NAME . "が開けません");

      // | を区切り文字として2つのデータを繋げて書き込む
      fwrite($fp, $name . $SPLIT . $message . "\n");

      // リダイレクトのためのHTTPヘッダーを送信
      header("Location: " . $_SERVER['PHP_SELF'], true, 303);
    }

    // ファイルの全行を読み取る
    $lines = file($LOG_FILE_NAME);
?>

  ...

  <section>
    <?php

      // 読み込んだ行数
      $linesNum = count($lines);

      // 一行ずつ表示する処理
      for ($i = 0; $i < $linesNum; $i++) {

         // 区切り文字でデータを区切って配列に格納
         $array = explode($SPLIT, $lines[$i]);

         // 区切り文字の前の部分は名前
         $name = $array[0];

         // 区切り文字の後の部分はメッセージ
         $message = $array[1];

         // 名前とメッセージを表示
         echo '<p>' . $name . "「" . $message . '」</p>';
      }
    ?>
  </section>

</body>
</html>

区切り文字を「|-|」変更したため、今まで使用してきたログファイル「log.txt」は使えなくなりますので削除してください。

削除して再度読み込むと、以下のようなエラーが画面に表示されますが、後ほど修正しますので今は無視して構いません。

ログファイル削除後のエラー

区切り文字が含まれていた場合の処理

区切り文字を複雑にしましたが、利用者が入力してしまわないとは限りません。

もし入力されてしまった場合のために 使用できない文字列「|-|」が含まれています。 などのメッセージを出してデータを送信できないようにスクリプトを変更します。

    ...

    // 送信された名前とメッセージを変数に代入
    if (isset($_POST['name'])) {
      $name = $_POST['name'];

      if (strpos($name, $SPLIT) !== false) {
        // 名前に区切り文字が含まれている場合の処理
        echo "使用できない文字列「|-|」が含まれています。";
        return;
      }

      if ($name == "") {
        $name = "noname";
      }
    }
    if (isset($_POST['message'])) {
      $message = $_POST['message'];

      if (strpos($message, $SPLIT) !== false) {
        // メッセージに区切り文字が含まれている場合の処理
        echo "使用できない文字列「|-|」が含まれています。";
        return;
      }

      //書き込みモードでファイルを開く
      $fp = fopen($LOG_FILE_NAME, "a") or exit($LOG_FILE_NAME . "が開けません");

    ...

PHPの関数 strpos は、第1引数に指定した文字列に、第2引数に指定した文字列が含まれていれば、その位置を返します。見つからなかった場合は false が返されるので、見つかった場合のみメッセージ表示の処理を行うようにします。

return は、それ以降の処理を行わないという命令です。もし送信されたデータに区切り文字が含まれていた場合は、そこで処理を中断します。

これで、区切り文字を含んだ名前やメッセージを送信しようとすると、次の画面が現れるようになります。

区切り文字を含むデータを送信した場合の処理

メッセージの最後の余計な空白を取り除く

メッセージの最後に、以下のように不自然なスペースが入ってしまっていました。

ログファイルに書き込んだデータを読み込んでページに表示した例

これは、行を読み込む際に、最後にある改行コードを一緒に読み込んでしまっているためです。したがって、メッセージを表示する際に改行コードを取り除くようにPHPスクリプトを変更します。

...

// 名前とメッセージを表示
echo '<p>' . $name . "「" . str_replace(PHP_EOL, "", $message) . '」</p>';

...

PHPの関数 str_replace は、第3引数の文字列に含まれている第1引数の文字列を、第2引数に置き換えます。

PHP_EOL は改行コードを表しますので、この命令によって「$message に含まれる改行コード(\n)を空の文字に置き換える」という意味になります。

ログファイルがない場合のエラーを修正する

先ほど、区切り文字を変更してログファイルを削除した後にページにエラーが出ました。これは、ログファイルが見つからなかった時の処理が正常に行われていないことが原因です。

以前紹介した関数 file_exists でファイルの有無をチェックして、ない場合はログの読み込み及び表示処理を行わないように変更します。

...

    if (!file_exists($LOG_FILE_NAME)) {
      // ファイルがない場合

      echo "書き込みはありません。";
      $linesNum = 0;
    } else {

      // ファイルの全行を読み取る
      $lines = file($LOG_FILE_NAME);
      // 読み込んだ行数
      $linesNum = count($lines);
    }

...

  <section>
    <?php

      // $linesNum = count($lines); を上に移動させたので、ここの文は削除

      // 一行ずつ表示する処理
      for ($i = 0; $i < $linesNum; $i++) {

         // 区切り文字でデータを区切って配列に格納
         $array = explode($SPLIT, $lines[$i]);

         // 区切り文字の前の部分は名前
         $name = $array[0];

         // 区切り文字の後の部分はメッセージ
         $message = $array[1];

         // 名前とメッセージを表示
         echo '<p>' . $name . "「" . str_replace(PHP_EOL, "", $message) . '」</p>';
      }
    ?>
  </section>

...

これで、ログファイルがない場合は以下のように表示されます。

ログファイルがない場合の画面

まとめ

以上の変更を全て加えた後の全ソースコードは以下のようになります。

<!-- chat.php -->

<?php
    // データを書き込むファイルの名前
    $LOG_FILE_NAME = "log.txt";

    // 区切りのための文字列
    $SPLIT = "|-|";

    // 名前を格納する変数
    $name = "";
    // メッセージを格納する変数
    $message = "";

    // 送信された名前とメッセージを変数に代入
    if (isset($_POST['name'])) {
      $name = $_POST['name'];

      if (strpos($name, $SPLIT) !== false) {
        // 名前に区切り文字が含まれている場合の処理
        echo "使用できない文字列「|-|」が含まれています。";
        return;
      }

      if ($name == "") {
        $name = "noname";
      }
    }
    if (isset($_POST['message'])) {
      $message = $_POST['message'];

      if (strpos($message, $SPLIT) !== false) {
        // メッセージに区切り文字が含まれている場合の処理
        echo "使用できない文字列「|-|」が含まれています。";
        return;
      }

      //書き込みモードでファイルを開く
      $fp = fopen($LOG_FILE_NAME, "a") or exit($LOG_FILE_NAME . "が開けません");

      // | を区切り文字として2つのデータを繋げて書き込む
      fwrite($fp, $name . $SPLIT . $message . "\n");

      // リダイレクトのためのHTTPヘッダーを送信
      header("Location: " . $_SERVER['PHP_SELF'], true, 303);
    }


    if (!file_exists($LOG_FILE_NAME)) {
      // ファイルがない場合
      echo "書き込みはありません。";

      $linesNum = 0;
    } else {

      // ファイルの全行を読み取る
      $lines = file($LOG_FILE_NAME);

      // 読み込んだ行数
      $linesNum = count($lines);
    }
?>

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>1行メッセージ</title>
</head>

<body>

  <h1>1行メッセージ</h1>

  <form method="post" action="chat.php">
    <div>
      <b>おなまえ</b>
      <input name="name" type="text" size="20" maxlength="10">
    </div>
    <div>
      <b>コメント</b>
      <input name="message" type="text" size="100" maxlength="50" required>
    </div>
    <button name="submit" type="submit">送信</button>
  </form>

  <section>
    <?php

      // 一行ずつ表示する処理
      for ($i = 0; $i < $linesNum; $i++) {

         // 区切り文字でデータを区切って配列に格納
         $array = explode($SPLIT, $lines[$i]);

         // 区切り文字の前の部分は名前
         $name = $array[0];

         // 区切り文字の後の部分はメッセージ
         $message = $array[1];

         // 名前とメッセージを表示
         echo '<p>' . $name . "「" . str_replace(PHP_EOL, "", $message) . '」</p>';
      }
    ?>
  </section>

</body>
</html>

以上で、チャットのデータ送信時の問題が修正できました。

厳密には、まだ同期やセキュリティ面の問題が残っています。次回その問題を修正します。

PHPで作るチャット(6) データ送信に関する問題の修正」への2件のフィードバック

  1. ピンバック: PHPで作るチャット(5) フォームから送信されたデータをファイルに書き込む | JoyPlotドキュメント

  2. ピンバック: PHPで作るチャット(7) クロスサイトスクリプティング(XSS)の修正 - JoyPlotドキュメント

コメントを残す

メールアドレスが公開されることはありません。