Webフォーム:ブラウザからサーバにデータを送るためのしくみ ~その2~

2015年4月1日(水)
野田 貴子

はじめに

これから始めるPHP入門コラムでは、PHPを学ぶ人が、PHPで簡単なプログラムを書けるようになるまでに必要な知識とポイントをTips的に書いていきます。今後PHPのスキルを身につけて仕事に役立てたい、という方のために「PHP技術者認定初級試験」の出題範囲を意識しながら進めていきますので、ぜひ最後までお付き合いください。

今回のあらすじ

  1. リクエストとレスポンス
  2. データを取得する
  3. データを表示する
  4. セキュリティ上の問題を修正する

このトピックはお伝えしたいことが多くて、前回はテキストが多くなってしまい、編集部に前後編にされてしまいました(笑)。ぜひ合わせてお読みください。

受け取ったデータを表示する

(4)適切でないデータがある場合

利用者が入力したデータが不適切だった場合(年齢が1200歳など)、エラーメッセージと再入力用のWebフォームを表示する必要があります。

前回までのチェック処理により、不適切なデータが存在した場合は、配列の$errorMsgにエラーメッセージが格納されています。エラーメッセージが存在するかどうかをチェックして、処理を分岐させましょう。Webフォームを表示する処理は関数に分けて再利用するようにします。

<?php
// エラーメッセージ保存用配列
$errorMsg = array();

// 選択肢
$sexes = array("male" => "男", "female" => "女");
$hobbies = array(
    "cooking"  => "料理",
    "swimming" => "水泳",
    "running" => "ランニング"
);
$addresses = array(
    "north" => "北日本",
    "east"  => "東日本",
    "west"  => "西日本",
    "south" => "南日本"
);

// 初期値
$form = array();
$form["name"] = "John";
$form["age"] = 25;
$form["sex"] = "male";
$form["hobbies"] = array("cooking", "swimming");
$form["address"] = "east";
$form["comment"] = "Hello";

/*------------------------------------------------------------
    2回目以降のアクセス
------------------------------------------------------------*/
if (array_key_exists("not_the_first_time", $_POST)) {

    /*------------------------------------------------------------
        名前の入力チェック
    ------------------------------------------------------------*/
    $form["name"] = $_POST["name"];

    // $_POST["name"] が存在しない場合
    if (! array_key_exists("name", $_POST)) {
        $errorMsg[] = "名前を入力してください。";
    }
    // $_POST["name"] が存在する場合
    else {
        // 前後の空白を除去する(空白文字のみの場合は空になる)
        $form["name"] = trim($form["name"]);

        // 中身が空("")である場合
        if ($form["name"] === "") {
            $errorMsg[] = "名前を入力してください。";
        }
        // 中身が空("")でない場合
        else {
            $length = strlen($form["name"]);
            if ($length < 3 || $length > 20) {
                $errorMsg[] = "名前は3文字以上20文字以内で入力してください。";
            }
        }
    }

    /*------------------------------------------------------------
        年齢の入力チェック
    ------------------------------------------------------------*/
    // $_POST["age"] が存在する場合
    if (array_key_exists("name", $_POST)) {

        $form["age"] = $_POST["age"];

        // 中身が空("")でない場合
        if ($form["age"] !== "") {

            // 整数でない場合
            if (strval(intval($form["age"])) !== $form["age"]) {
                $errorMsg[] = "年齢は整数で入力してください。";
            }
            // 正の整数でない場合
            else if ($form["age"] < 0) {
                $errorMsg[] = "年齢は0歳以上で入力してください。";
            }
        }
    }

    /*------------------------------------------------------------
        性別の入力チェック
    ------------------------------------------------------------*/
    // $_POST["sex"] が存在しない場合
    if (! array_key_exists("sex", $_POST)) {
        $errorMsg[] = "性別を選択してください。"; // ラジオボタンなので「入力」ではなく「選択」で。
    }
    // $_POST["sex"] が存在する場合
    else {
        $form["sex"] = $_POST["sex"];

        // 選択肢にないものの場合
        if (! array_key_exists($form["sex"], $sex)) {
            $errorMsg[] = "性別を正しく選択してください。";
        }
    }

    /*------------------------------------------------------------
        趣味の入力チェック
    ------------------------------------------------------------*/
    $form["hobbies"] = $_POST["hobbies"]; // hobbiesは配列で取得される

    // $_POST["hobbies"] が存在する場合
    if (array_key_exists("hobbies", $_POST)) {

        // 選択肢にないものの場合
        foreach($form["hobbies"] as $value) {
            if (! array_key_exists($value, $hobbies)) {
                $errorMsg[] = "趣味を正しく選択してください。";
            }
        }
    }

    /*------------------------------------------------------------
        住所の入力チェック
    ------------------------------------------------------------*/
    $form["address"] = $_POST["address"];

    // $_POST["address"] が存在する場合
    if (array_key_exists("address", $_POST)) {
        // 選択肢にないものの場合
        if (! array_key_exists($form["address"], $addresses)) {
            $errorMsg[] = "住所を正しく選択してください。";
        }
    }

    /*------------------------------------------------------------
        コメントの入力チェック
    ------------------------------------------------------------*/
    $form["comment"] = $_POST["comment"];

    // $_POST["comment"] が存在する場合
    if (array_key_exists("name", $_POST)) {
        if (strlen($form["comment"]) > 100) {
            $errorMsg[] = "コメントは100文字以内で入力してください。";
        }
    }

    /*------------------------------------------------------------
        適切でないデータがある場合
    ------------------------------------------------------------*/
    if (count($errorMsg) > 0) {

        // エラーメッセージを表示する
        foreach($errorMsg as $message) {
            echo $message . '<br>';
        }

        // Webフォームを表示する
        showForm();
    }

    /*------------------------------------------------------------
        すべてのデータが適切である場合
    ------------------------------------------------------------*/
    else {

    }
}

/*------------------------------------------------------------
    1回目のアクセス
------------------------------------------------------------*/
else {
    // Webフォームを表示する
    showForm();
}

// Webフォームを表示する関数
function showForm() {
global $sexes, $hobbies, $addresses, $form;
    echo '
        <form method="POST" action="profile.php">
            名前: <input type="text" name="name" value="' . $form["name"] . '"><br>
            年齢: <input type="text" name="age" value="' . $form["age"] . '"><br>
    ';
    echo '性別: ';
    foreach($sexes as $value => $label) {
        $checked = '';
        // 初期値の場合、「checked」をつける
        if ($value === $form["sex"]) {
            $checked = ' checked';
        }
        echo '<label><input type="radio" name="sex" value="' . $value . '"' . $checked . '>' . $label . '</label>';
    }
    echo '<br>';
    echo '趣味: ';
    foreach($hobbies as $value => $label) {
        $checked = '';
        // 初期値の場合、「checked」をつける
        if (in_array($value, $form["hobbies"])) {
            $checked = ' checked';
        }
        echo '<label><input type="checkbox" name="hobbies[]" value="' . $value . '"' . $checked . '>' . $label . '</label>';
    }
    echo '<br>';
    echo '住所: <select name="address">';
    foreach($addresses as $value => $label) {
        $selected = '';
        // 初期値の場合、「selected」をつける
        if ($value === $form["address"]) {
            $selected = ' selected';
        }
        echo '<option value="' . $value . '"' . $selected . '>' . $label . '</option>';
    }
    echo '</select><br>';
    echo '
            コメント: <textarea type="text" name="name">' . $form["comment"] . '</textarea><br>
            <br>
            <input type="hidden" name="not_the_first_time" value="yes">
            <input type="submit" value="送信">
        </form>
    ';
}
?>

(5)すべてのデータが適切である場合

すべてのデータが適切であるかどうかは、先ほど$errorMsgでチェックしたので、データを画面に表示する処理を考えましょう。

選択肢の配列を用意してあるものについては、その配列を使います。入力されてくるのは性別であれば「male」などなので、それを「男」という表示にするためです。

コメントでは改行が入力されるので、textareaでの改行部分に
タグを挿入するnl2br関数を使います。

<?php
$nodata = "(未回答)";

echo '<h2>名前</h2>';
if (strlen($form["name"]) > 0) {
    echo $form["name"];
}
else {
    echo $nodata;
}
echo '<h2>年齢</h2>';
if (strlen($form["age"]) > 0) {
    echo $form["age"] . '歳';
}
else {
    echo $nodata;
}
echo '<h2>性別</h2>';
if (strlen($form["sex"]) > 0) {
    echo $sexes[$form["sex"]];
}
else {
    echo $nodata;
}
echo '<h2>趣味</h2>';
$count = count($form["hobbies"]);
if ($count > 0) {
    for($i = 0; $i < $count; $i++) {
        echo $hobbies[$form["hobbies"][$i]];
        if ($i + 1 < $count) {
            echo ', ';
        }
    }
}
else {
    echo $nodata;
}
echo '<h2>住所</h2>';
if (strlen($form["address"]) > 0) {
    echo $addresses[$form["address"]];
}
else {
    echo $nodata;
}
echo '<h2>コメント</h2>';
if (strlen($form["comment"]) > 0) {
    echo nl2br($form["comment"]);
}
else {
    echo $nodata;
}
?>
ブラウザ上での表示

ブラウザ上での表示

セキュリティ上の問題を修正する

ここまでで、とりあえず動くレベルのものができましたが、実際に運用するにはまだバグとセキュリティ上の問題があります。

Webフォームを出力するこの部分を見てください。

<?php
echo '名前: <input type="text" name="name" value="' . $form["name"] . '">';
?>

$form["name"]には「trim($_POST["name"])」が入っているので、もし名前に「山田"太郎"花子」と入力された場合、以下のようにHTMLの構文エラーになってしまいます。

名前: <input type="text" name="name" value="山田"太郎"花子">

それではstr_replace関数を使い「"」を「"」に変換すればそれでよいでしょうか。しかしHTMLには、他にも「 (>)」「& (&)」など、エスケープしなければならない文字がいくつかあります。これらを一手に引き受けてくれるのが、htmlentities関数です。

<?php
echo '名前: <input type="text" name="name" value="' . htmlentities($form["name"], ENT_QUOTES, "UTF-8") . '">';
?>

この例のようにHTMLが崩れてしまうだけならちょっとしたバグで済みますが、入力されたデータをそのまま出力していると、もっと悪質な他人に害を及ぼすようなコードを埋め込まれてしまうことがあります。データ出力部分である、次のコードを見てください。

<?php
echo '<h2>名前</h2>';
if (strlen($form["name"]) > 0) {
    echo $form["name"];
}
?>

もし名前の入力欄に、悪質なWebサイトへ勝手に画面遷移してしまうようなJavaScriptが入力されてしまったらどうなるでしょう。

<h2>名前</h2><script>location.href='http://悪質なサイト.jp';</script>

今のところ、この画面を見るのはデータを入力した本人だけですが、今後プロフィールデータをデータベースに登録する処理を追加し、ある人のプロフィール画面に他の人もアクセスできるようになったとしたら、その画面を見た人が被害を受けてしまいます。このようにHTMLの中に悪意のあるスクリプトを埋め込んでしまうことを「クロスサイトスクリプティング」といいます。

クロスサイトスクリプティングも先ほどのhtmlentities関数で防ぐことができます。

echo '<h2>名前</h2>';
if (strlen($form["name"]) > 0) {
    echo htmlentities($form["name"], ENT_QUOTES, "UTF-8");
}

<h2>名前</h2><script>location.href='http://悪質なサイト.jp';</script>

以上で、プログラムが完成しました。

<?php
// エラーメッセージ保存用配列
$errorMsg = array();

// 選択肢
$sexes = array("male" => "男", "female" => "女");
$hobbies = array(
    "cooking"  => "料理",
    "swimming" => "水泳",
    "runnning" => "ランニング"
);
$addresses = array(
    "north" => "北日本",
    "east"  => "東日本",
    "west"  => "西日本",
    "south" => "南日本"
);

// 初期値
$form = array();
$form["name"] = "John";
$form["age"] = 25;
$form["sex"] = "male";
$form["hobbies"] = array("cooking", "swimming");
$form["address"] = "east";
$form["comment"] = "Hello";

/*------------------------------------------------------------
    2回目以降のアクセス
------------------------------------------------------------*/
if (array_key_exists("not_the_first_time", $_POST)) {

    /*------------------------------------------------------------
        名前の入力チェック
    ------------------------------------------------------------*/
    $form["name"] = $_POST["name"];

    // $_POST["name"] が存在しない場合
    if (! array_key_exists("name", $_POST)) {
        $errorMsg[] = "名前を入力してください。";
    }
    // $_POST["name"] が存在する場合
    else {
        // 前後の空白を除去する(空白文字のみの場合は空になる)
        $form["name"] = trim($form["name"]);

        // 中身が空("")である場合
        if ($form["name"] === "") {
            $errorMsg[] = "名前を入力してください。";
        }
        // 中身が空("")でない場合
        else {
            $length = strlen($form["name"]);
            if ($length < 3 || $length > 20) {
                $errorMsg[] = "名前は3文字以上20文字以内で入力してください。";
            }
        }
    }

    /*------------------------------------------------------------
        年齢の入力チェック
    ------------------------------------------------------------*/
    // $_POST["age"] が存在する場合
    if (array_key_exists("name", $_POST)) {

        $form["age"] = $_POST["age"];

        // 中身が空("")でない場合
        if ($form["age"] !== "") {

            // 整数でない場合
            if (strval(intval($form["age"])) !== $form["age"]) {
                $errorMsg[] = "年齢は整数で入力してください。";
            }
            // 正の整数でない場合
            else if ($form["age"] < 0) {
                $errorMsg[] = "年齢は0歳以上で入力してください。";
            }
        }
    }

    /*------------------------------------------------------------
        性別の入力チェック
    ------------------------------------------------------------*/
    // $_POST["sex"] が存在しない場合
    if (! array_key_exists("sex", $_POST)) {
        $errorMsg[] = "性別を選択してください。"; // ラジオボタンなので「入力」ではなく「選択」で。
    }
    // $_POST["sex"] が存在する場合
    else {
        $form["sex"] = $_POST["sex"];

        // 選択肢にないものの場合
        if (! array_key_exists($form["sex"], $sex)) {
            $errorMsg[] = "性別を正しく選択してください。";
        }
    }

    /*------------------------------------------------------------
        趣味の入力チェック
    ------------------------------------------------------------*/
    $form["hobbies"] = $_POST["hobbies"]; // hobbiesは配列で取得される

    // $_POST["hobbies"] が存在する場合
    if (array_key_exists("hobbies", $_POST)) {

        // 選択肢にないものの場合
        foreach($form["hobbies"] as $value) {
            if (! array_key_exists($value, $hobbies)) {
                $errorMsg[] = "趣味を正しく選択してください。";
            }
        }
    }

    /*------------------------------------------------------------
        住所の入力チェック
    ------------------------------------------------------------*/
    $form["address"] = $_POST["address"];

    // $_POST["address"] が存在する場合
    if (array_key_exists("address", $_POST)) {
        // 選択肢にないものの場合
        if (! array_key_exists($form["address"], $addresses)) {
            $errorMsg[] = "住所を正しく選択してください。";
        }
    }

    /*------------------------------------------------------------
        コメントの入力チェック
    ------------------------------------------------------------*/
    $form["comment"] = $_POST["comment"];

    // $_POST["comment"] が存在する場合
    if (array_key_exists("name", $_POST)) {
        if (strlen($form["comment"]) > 100) {
            $errorMsg[] = "コメントは100文字以内で入力してください。";
        }
    }

    /*------------------------------------------------------------
        適切でないデータがある場合
    ------------------------------------------------------------*/
    if (count($errorMsg) > 0) {

        // エラーメッセージを表示する
        foreach($errorMsg as $message) {
            echo $message . '<br>';
        }

        // Webフォームを表示する
        showForm();
    }

    /*------------------------------------------------------------
        すべてのデータが適切である場合
    ------------------------------------------------------------*/
    else {
        $nodata = htmlentities("(未回答)", ENT_QUOTES, "UTF-8");

        echo '<h2>名前</h2>';
        if (strlen($form["name"]) > 0) {
            echo htmlentities($form["name"], ENT_QUOTES, "UTF-8");
        }
        else {
            echo $nodata;
        }
        echo '<h2>年齢</h2>';
        if (strlen($form["age"]) > 0) {
            echo htmlentities($form["age"], ENT_QUOTES, "UTF-8") . '歳';
        }
        else {
            echo $nodata;
        }
        echo '<h2>性別</h2>';
        if (strlen($form["sex"]) > 0) {
            echo htmlentities($sexes[$form["sex"]], ENT_QUOTES, "UTF-8");
        }
        else {
            echo $nodata;
        }
        echo '<h2>趣味</h2>';
        $count = count($form["hobbies"]);
        if ($count > 0) {
            for($i = 0; $i < $count; $i++) {
                echo htmlentities($hobbies[$form["hobbies"][$i]], ENT_QUOTES, "UTF-8");
                if ($i + 1 < $count) {
                    echo ', ';
                }
            }
        }
        else {
            echo $nodata;
        }
        echo '<h2>住所</h2>';
        if (strlen($form["address"]) > 0) {
            echo htmlentities($addresses[$form["address"]], ENT_QUOTES, "UTF-8");
        }
        else {
            echo $nodata;
        }
        echo '<h2>コメント</h2>';
        if (strlen($form["comment"]) > 0) {
            echo nl2br(htmlentities($form["comment"], ENT_QUOTES, "UTF-8")); // nl2brは最後にする
        }
        else {
            echo $nodata;
        }
    }
}

/*------------------------------------------------------------
    1回目のアクセス
------------------------------------------------------------*/
else {
    // Webフォームを表示する
    showForm();
}

// Webフォームを表示する関数
function showForm() {
global $sexes, $hobbies, $addresses, $form;
    echo '
        <form method="POST" action="profile.php">
            名前: <input type="text" name="name" value="' . htmlentities($form["name"], ENT_QUOTES, "UTF-8") . '"><br>
            年齢: <input type="text" name="age" value="' . htmlentities($form["age"], ENT_QUOTES, "UTF-8") . '"><br>
    ';
    echo '性別: ';
    foreach($sexes as $value => $label) {
        $checked = '';
        // 初期値の場合、「checked」をつける
        if ($value === $form["sex"]) {
            $checked = ' checked';
        }
        echo '<label><input type="radio" name="sex" value="' . htmlentities($value, ENT_QUOTES, "UTF-8") . '"' . $checked . '>' . htmlentities($label, ENT_QUOTES, "UTF-8") . '</label>';
    }
    echo '<br>';
    echo '趣味: ';
    foreach($hobbies as $value => $label) {
        $checked = '';
        // 初期値の場合、「checked」をつける
        if (in_array($value, $form["hobbies"])) {
            $checked = ' checked';
        }
        echo '<label><input type="checkbox" name="hobbies[]" value="' . htmlentities($value, ENT_QUOTES, "UTF-8") . '"' . $checked . '>' . htmlentities($label, ENT_QUOTES, "UTF-8") . '</label>';
    }
    echo '<br>';
    echo '住所: <select name="address">';
    foreach($addresses as $value => $label) {
        $selected = '';
        // 初期値の場合、「selected」をつける
        if ($value === $form["address"]) {
            $selected = ' selected';
        }
        echo '<option value="' . htmlentities($value, ENT_QUOTES, "UTF-8") . '"' . $selected . '>' . htmlentities($label, ENT_QUOTES, "UTF-8") . '</option>';
    }
    echo '</select><br>';
    echo '
            コメント: <textarea type="text" name="name">' . htmlentities($form["comment"], ENT_QUOTES, "UTF-8") . '</textarea><br>
            <br>
            <input type="hidden" name="not_the_first_time" value="yes">
            <input type="submit" value="送信">
        </form>
    ';
}
?>

おまけ

Webシステムを作る際に、よく必要となりそうなデータ形式のチェック方法を2点ほど紹介します。

日付の入力値チェック方法

一週間後までの予約ができるシステムがあるとしましょう。予約日として入力された日付が、一週間以内であるかどうかをチェックするには以下のようにします。

<?php
$date = $_POST["date"]; // 例:2015/03/02
// $dataのタイムスタンプと一週間後のタイムスタンプを比較する
if (strtotime($date) > strtotime('+1 week')) {
echo '予約日は一週間以内の日付を入力してください。';
}
?>

strtotimeは、文字列をタイムスタンプ(1970年1月1日からの経過秒数)に変換する関数です。数値であるタイムスタンプは、大小の比較に適しています。使用できる文字列の種類は多数ありますので、詳細はマニュアルをご覧ください。

メールアドレスの入力値チェック方法

「正規表現」という、文字列の規則を表す特殊な書き方があります。例えば「小文字のアルファベット1文字と数字1文字」は「[a-z][0-9]」と書きます。入力されたデータが規則に適合しているかどうかを調べる際には、preg_matchを利用できます。

メールアドレスも、ある程度までなら正規表現でチェックすることができます。「@マークが入っていればOK」というくらい簡単でゆるいものにするか、正規表現で厳密にチェックするかどうかは、システムの性格にもよるでしょう。

おわりに

いかがでしたか。前回・今回にわたって、がっつりとコードを書いたので量が多かったかもしれませんが、ブラウザで入力したデータがPHPに送られ、画面に表示されるというインタラクティブな部分を楽しめたのではないでしょうか。次回はWebシステムになくてはならないデータベースについて取り上げます。それではまた。

1983年生まれ。大学卒業後、ソフトウェア開発の営業を経て、ソフトウェア開発業務に転向。現在は自社パッケージのフロントエンド開発のほか、PHPでの受託開発案件、日→英のローカライズ案件などを担当。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています