Chapter18 投稿フォーム

概要と目標 投稿フォームを作成し、
記事を登録できるようになろう。

投稿フォームから新着情報をデータベースに登録する方法を学習しましょう。

今回のゴール

RERUN

投稿フォームのHTML まずはHTMLで投稿フォームを作成しよう。

新着情報のタイトルや内容を入力できるフォームを作成し、
データの送信先を「add.php」にする。

投稿フォームを作成しよう。

  1. 前回作成したデータベース「mini-cms」がない場合は、MySQLモニタを起動し、
    「mini-cms」 › 「setup」フォルダ内にある「create-db.sql」で、データベースを作成。
  2. 「mini-cms」 › 「admin」フォルダ内に「post.php」を作成
  3. 入力フォームを作成
mini-cms/admin/post.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>投稿フォーム</title>
</head>
<body>
  <h1>投稿フォーム</h1>
  <form action="add.php" method="post">
    <dl>
      <dt><label for="title">記事のタイトル</label></dt>
      <dd>
        <input type="text" id="title" name="title">
      </dd>
      <dt><label for="category_id">カテゴリーID</label></dt>
      <dd>
        <input type="text" id="category_id" name="category_id">
      </dd>
      <dt><label for="content">記事の内容</label></dt>
      <dd>
        <textarea name="content" id="content" cols="30" rows="10"></textarea>
      </dd>
    </dl>
    <p><input type="submit" value="投稿"></p>
  </form>
</body>
</html>
  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「post.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/post.php
ブラウザでの表示例

RERUN

レコードの挿入 受け取ったデータを、
データベースに格納。

$_POSTで受け取ったデータをINSERT文で挿入する。
ただし、「add.php」にダイレクトでアクセスされた場合は、
$_POSTにデータが存在しないので、「post.php」にリダイレクトする。
$_SERVER['REQUEST_METHOD']というスーパーグローバル変数には、
リクエスト時のメソッドが格納されている。この中に「POST」が入っていれば、
フォームの送信ボタンが押されたことを意味する。

レコードを挿入してみよう。

  1. 「mini-cms」 › 「admin」フォルダ内に「add.php」を作成
  2. 設定ファイルや関数ファイルの読み込みを行い、 $_SERVER['REQUEST_METHOD']が、
    POSTではない場合は、投稿フォームにリダイレクトする
mini-cms/admin/add.php
<?php
  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // 投稿ボタンが押されたかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== 'POST') {
    // ダイレクトでアクセスされた時
    header('Location: post.php');
    exit();
  }
?>
  1. データベースに「$_POST」で受けっとたデータを挿入する処理と、
    「登録完了」のメッセージ表示する
mini-cms/admin/add.php
<?php
  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // 投稿ボタンが押されたかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== 'POST') {
    // ダイレクトでアクセスされた時
    header('Location: post.php');
    exit();
  }

  try {
    // データベースへ接続
    $dbh = new PDO(DSN, DB_USER, DB_PASSWORD);

    // エラー発生時に「PDOException」という例外を投げる設定に変更
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // SQL文の作成
    $sql = 'INSERT INTO posts (title, category_id, content, created) VALUES(?, ?, ?, now())';

    // ステートメント用意
    $stmt = $dbh->prepare($sql);

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, $_POST['title'], PDO::PARAM_STR);
    $stmt->bindValue(2, (int)$_POST['category_id'], PDO::PARAM_INT);
    $stmt->bindValue(3, $_POST['content'], PDO::PARAM_STR);

    // ステートメントを実行
    $stmt->execute();


    // データベースとの接続を終了
    $dbh = null;

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>登録完了</title>
</head>
<body>
  <h1>登録完了</h1>
  <p>新着情報を登録しました。</p>
</body>
</html>
  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「post.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/post.php
  3. 新着情報が投稿できるを確認
ブラウザでの表示例

RERUN

テキストエリアの改行 テキストエリアで改行しても、
このままじゃ、改行されない。

テキストエリアでの改行は、改行コードというものが埋め込まれる。
ただし、これは、テキストエディタで改行を行っているのと同様で、
ブラウザ上では改行されない。
ブラウザ上でも改行を反映するには、改行コードを<br>に変換する必要がある。

テキストエリアの改行に対応してみよう。

  1. 「mini-cms」フォルダ内の「detail.php」をテキストエディタで開く
  2. $result['content']を出力する際、改行コードを<br>に変換する
mini-cms/index.php
<?php echo nl2br(h($result['content']), false); ?>

答えを見る

  1. 上書き保存
  2. ブラウザで「mini-cms」フォルダ内の「index.php」にアクセス
    http://localhost/php-lessons/mini-cms/
  3. 改行が反映されているかを確認
ブラウザでの表示例

RERUN

カテゴリの選択 セレクトボックスで、
カテゴリを選択できるようにしよう。

現在の投稿フォームはカテゴリを、カテゴリIDで入力しなければならない。
セレクトボックスで選択できるようにして、ユーザビリティ(使いやすさ)を向上させよう。

カテゴリのセレクトボックスを作成しよう。

  1. 「mini-cms」 › 「admin」フォルダ内にある「post.php」をテキストエディタで開く
  2. データベースからカテゴリを抽出し、 セレクトボックスを作成する
mini-cms/admin/post.php
<?php
  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  try {
    // データベースへ接続
    $dbh = new PDO(DSN, DB_USER, DB_PASSWORD);

    // エラー発生時に「PDOException」という例外を投げる設定に変更
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // SQL文の作成
    $sql = 'SELECT * FROM categories';

    // SQLを実行
    $stmt = $dbh->query($sql);

    // 実行結果を連想配列として取得
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // データベースとの接続を終了
    $dbh = null;

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>投稿フォーム</title>
</head>
<body>
  <h1>投稿フォーム</h1>
  <form action="add.php" method="post">
    <dl>
      <dt><label for="title">記事のタイトル</label></dt>
      <dd>
        <input type="text" id="title" name="title">
      </dd>
      <dt><label for="category_id">カテゴリー</label></dt>
      <dd>
        <select name="category_id" id="category_id">
          <?php foreach($result as $row) : ?>
          <option value="<?php echo h($row['id']); ?>"><?php echo h($row['category_name']); ?></option>
          <?php endforeach; ?>
        </select>
      </dd>
      <dt><label for="content">記事の内容</label></dt>
      <dd>
        <textarea name="content" id="content" cols="30" rows="10"></textarea>
      </dd>
    </dl>
    <p><input type="submit" value="投稿"></p>
  </form>
</body>
</html>
  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内の「post.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/post.php
  3. カテゴリがセレクトボックスで選択できるかを確認
ブラウザでの表示例

RERUN

CSRF対策 外部のフォームから送信されても、
登録されてしまう。

投稿フォームの機能はこれで問題ないが、現在の仕様では外部で用意されたフォームから、
「add.php」に「POST」送信された場合でも、登録処理を行ってしまう CSRFの脆弱性がある。

CSRFの対策をしてみよう。

  1. 「mini-cms」 › 「inc」フォルダ内にある「functions.php」をテキストエディタで開く
  2. どのページでも利用できるように、 トークンの作成トークンの確認の処理を、
    独自関数として登録しておく
mini-cms/inc/functions.php
<?php
// XSS 対策
function h($s) {
  return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// CSRF対策 トークンの生成
function set_token() {
  if (!isset($_SESSION['token'])) {
      $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(16));
  }
}

// CSRF対策 トークンの確認
function check_token() {
  if (empty($_POST['token']) || $_POST['token'] != $_SESSION['token']) {
    echo '不正な投稿(トークンが一致しません。)';
    exit();
  }
}
  1. 「mini-cms」 › 「admin」フォルダ内にある「post.php」をテキストエディタで開く
  2. セッションを開始後、 作成した独自関数を利用してトークンをセットし、
    <input type="hidden">を使って、 トークンを埋め込む
mini-cms/admin/post.php
<?php
  // セッションの開始
  session_start();

  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // CSRF対策 ・・・ トークンの生成
  set_token();

  try {
    // データベースへ接続
    $dbh = new PDO(DSN, DB_USER, DB_PASSWORD);

    // エラー発生時に「PDOException」という例外を投げる設定に変更
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // SQL文の作成
    $sql = 'SELECT * FROM categories';

    // SQLを実行
    $stmt = $dbh->query($sql);

    // 実行結果を連想配列として取得
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // データベースとの接続を終了
    $dbh = null;

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>投稿フォーム</title>
</head>
<body>
  <h1>投稿フォーム</h1>
  <form action="add.php" method="post">
    <dl>
      <dt><label for="title">記事のタイトル</label></dt>
      <dd>
        <input type="text" id="title" name="title">
      </dd>
      <dt><label for="category_id">カテゴリー</label></dt>
      <dd>
        <select name="category_id" id="category_id">
          <?php foreach($result as $row) : ?>
          <option value="<?php echo h($row['id']); ?>"><?php echo h($row['category_name']); ?></option>
          <?php endforeach; ?>
        </select>
      </dd>
      <dt><label for="content">記事の内容</label></dt>
      <dd>
        <textarea name="content" id="content" cols="30" rows="10"></textarea>
      </dd>
    </dl>
    <p><input type="hidden" name="token" value="<?php echo h($_SESSION['token']); ?>"></p>
    <p><input type="submit" value="投稿"></p>
  </form>
</body>
</html>
  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内にある「post.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/post.php
  3. トークンが埋め込まれていることを確認
  4. 「mini-cms」 › 「admin」フォルダ内にある「add.php」をテキストエディタで開く
  5. セッションを開始後、 作成した独自関数を利用してトークンのチェックを行う。
mini-cms/admin/add.php
<?php
  // セッションの開始
  session_start();

  // ファイルの読み込み
  require_once('../inc/config.php');
  require_once('../inc/functions.php');

  // 投稿ボタンが押されたかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== 'POST') {
    // ダイレクトでアクセスされた時
    header('Location: post.php');
    exit();
  }

  // CSRF対策 ・・・ トークンのチェック
  check_token();

  try {
    // データベースへ接続
    $dbh = new PDO(DSN, DB_USER, DB_PASSWORD);

    // エラー発生時に「PDOException」という例外を投げる設定に変更
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // SQL文の作成
    $sql = 'INSERT INTO posts (title, category_id, content, created) VALUES(?, ?, ?, now())';

    // ステートメント用意
    $stmt = $dbh->prepare($sql);

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, $_POST['title'], PDO::PARAM_STR);
    $stmt->bindValue(2, (int)$_POST['category_id'], PDO::PARAM_INT);
    $stmt->bindValue(3, $_POST['content'], PDO::PARAM_STR);

    // ステートメントを実行
    $stmt->execute();


    // データベースとの接続を終了
    $dbh = null;

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
  1. 上書き保存
  2. ブラウザで「mini-cms」 › 「admin」フォルダ内にある「post.php」にアクセス
    http://localhost/php-lessons/mini-cms/admin/post.php
  3. テスト投稿を行いエラーが出ないことを確認
ブラウザでの表示例

RERUN

DBのバックアップ phpMyAdminというソフトや、
mysqldumpコマンドなどを活用する。

データベースに保存されている値を、他の環境で使う場合はバックアップを取る。

  1. 仮想サーバのデータベースフォルダを丸ごとコピー
    • XAMPP: C:\xampp\mysql\data\mini_cms_app
    • MAMP: /Applications/MAMP/db/mysql〇〇/mini_cms_app
  2. 「phpMyAdmin」のエクスポート機能を使う (復元はインポート)
  3. mysqldumpコマンドを使ってSQLファイルをエクスポート
    エクスポート
    mysqldump -u root -p mini_cms_app > backup.sql
    復元
    mysql -u root -p mini_cms_app < backup.sql 

練習問題 今回の理解度をチェック。

完成例を参考に「tranig」フォルダ内の「index.php」と「add.php」を使って
カテゴリ登録フォームを作成して下さい。

完成イメージ

RERUN

解答例
chapter18/training/config.php
<?php
  // データベースの定数
  define('DB_NAME', 'mini_cms_app');
  define('DB_USER', 'root');
  define('DB_PASSWORD', ''); // パスワード (MAMPは「root」)
  define('DSN', 'mysql:host=localhost;dbname='. DB_NAME . ';charset=utf8');
chapter18/training/functions.php
<?php
  // XSS 対策
  function h($s) {
    return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
  }

  // CSRF対策 トークンの生成
  function set_token() {
    if (!isset($_SESSION['token'])) {
        $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(16));
    }
  }

  // CSRF対策 トークンの確認
  function check_token() {
    if (empty($_POST['token']) || $_POST['token'] != $_SESSION['token']) {
      echo '不正な投稿(トークンが一致しません。)';
      exit();
    }
  }
chapter18/training/index.php
<?php
  // セッションの開始
  session_start();

  // ファイルの読み込み
  require_once('config.php');
  require_once('functions.php');

  // CSRF対策 ・・・ トークンの生成
  set_token();
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>カテゴリを登録</title>
</head>
<body>
  <h1>カテゴリを登録</h1>

  <form action="add.php" method="post">
    <dl>
      <dt><label for="category_name">カテゴリ名</label></dt>
      <dd>
        <input type="text" id="category_name" name="category_name">
      </dd>
    </dl>
    <p><input type="hidden" name="token" value="<?php echo h($_SESSION['token']); ?>"></p>
    <p><input type="submit" value="カテゴリを登録"></p>
  </form>
</body>
</html>
chapter18/training/add.php
<?php
  // セッションの開始
  session_start();

  // ファイルの読み込み
  require_once('config.php');
  require_once('functions.php');


  // 投稿ボタンが押されたかをチェック
  if ( $_SERVER['REQUEST_METHOD'] !== 'POST') {
    // ダイレクトでアクセスされた時
    header('Location: index.php');
    exit();
  }

  // CSRF対策 ・・・ トークンのチェック
  check_token();

  try {
    // データベースへ接続
    $dbh = new PDO(DSN, DB_USER, DB_PASSWORD);

    // エラー発生時に「PDOException」という例外を投げる設定に変更
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // SQL文の作成
    $sql = 'INSERT INTO categories (category_name) VALUES(?)';

    // ステートメント用意
    $stmt = $dbh->prepare($sql);

    // プレースホルダーに値をガッチャンコ
    $stmt->bindValue(1, $_POST['category_name'], PDO::PARAM_STR);

    // ステートメントを実行
    $stmt->execute();


    // データベースとの接続を終了
    $dbh = null;

  } catch (PDOException $e) {
    // 例外発生時の処理
    echo 'エラー' . h($e->getMessage());
    exit();
  }
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>登録完了</title>
</head>
<body>
  <h1>登録完了</h1>
  <p>カテゴリを登録しました。</p>
</body>
</html>

解答例は全問題のチェックボックスが on になるとご覧いただけます。

まとめ INSERT文も
プリペアド・ステートメントで。

送信フォームもSQLインジェクションや、CSRFなどの
脆弱性に注意する必要がある。

  • INSERT文もプリペアド・ステートメントを使う
  • 送信フォームには、トークンを埋め込む
  • 受け取り側は、トークンをチェックする