# セッションを利用して値を保持する

セッションを利用して、ユーザの入力値がバリデーションを通らなかった際に、ユーザにお知らせするテキストを表示できる状態を目指します。

ニックネーム30_投稿内容必須

# 本セクションの流れ

  1. セッションの取り扱いについて。
  2. insert_message.php にセッション変数を追加する。
  3. session_values.php でセッションを制御する。
  4. index.php でテキストを表示する。

# 0. セッションの取り扱いについて。

【基礎編:セッションとは】にあるように、セッションは、サイトを訪れた個々のユーザーのデータを個別に管理する機能を提供します。

$_SESSION は PHP の定義済み変数(=スーパーグローバル変数)の 1 つで、現在のセッションに登録されている値の変数を連想配列として保存します。

PHP の定義済み変数

PHP には定義済み変数が複数あります。その 1 つが$_SESSION です。 PHP の定義済み変数はスーパーグローバル変数ともいい、すべてのスクリプトで使用することができます。
外部から来る変数 や組み込みの環境変数、直近のエラーメッセージや最後に取得したヘッダなどのあらゆる内容が取得できます。

スーパーグローバル変数には次のようなものがあります。

$GLOBALS — グローバルスコープで使用可能なすべての変数への参照
$_SERVER — サーバー情報および実行時の環境情報
$_GET — HTTP GET 変数
$_POST — HTTP POST 変数
$_FILES — HTTP ファイルアップロード変数
$_REQUEST — HTTP リクエスト変数
$_SESSION — セッション変数
$_ENV — 環境変数

引用:PHP 公式ドキュメント_定義済の変数 (opens new window)

セッションの情報を確認

以下のように、session_id()session_name() でセッション ID やセッション名を確認できます。

<?php
session_start();//セッションスタート

$_SESSION['user_name'] = 'hogehoge';//セッション変数に登録

echo $_SESSION['user_name']."<br/>\n";//セッション変数の呼び出し

echo session_id()."<br/>\n";//セッションIDの確認

echo session_name()."<br/>\n";//セッション名の確認

echo session_unset()."<br/>\n";//セッション変数の開放
?>
1
2
3
4
5
6
7
8
9
10
11
12
13

実行結果

hogehoge
faar309bnhnk5ljobankkcaleijfm()
PHPSESSID
1
2
3

今回は、ページを遷移しても値が保持されるというセッションの仕組みを利用して、~~/public/index.php ではないファイルで定義されるエラーメッセージを ~~/public/index.php 内で表示する処理を実装します。

# 1. insert_message.php にセッション変数を追加する。

~~/src/insert_message.php にコードを追加します。

















 






 







 






 




 





















 
 
 
 





<?php

/**
 * 両端の空白を除去する関数です。マルチバイトを含みます。
 * 参考 https://qiita.com/fallout/items/a13cebb07015d421fde3
 */
function mbTrim($pString)
{
  return preg_replace('/\A[\p{Cc}\p{Cf}\p{Z}]++|[\p{Cc}\p{Cf}\p{Z}]++\z/u', '', $pString);
}

// 入力値を確認する(投稿者)
$is_valid_auther_name = true;
$input_author_name = '';
if (isset($_POST['author_name'])) {
  $input_author_name = mbTrim(str_replace("\r\n", "\n", $_POST['author_name']));
  $_SESSION['input_pre_author_name'] = $_POST['author_name'];
} else {
  $is_valid_auther_name = false;
}

if ($is_valid_auther_name && mb_strlen($input_author_name) > 30) {
  $is_valid_auther_name = false;
  $_SESSION['input_error_author_name'] = 'ニックネームは 30 文字以内で入力してください。(現在 ' . mb_strlen($input_author_name) . ' 文字)';
}

// 入力値を確認する(投稿内容)
$is_valid_message = true;
$input_message = '';
if (isset($_POST['message'])) {
  $input_message = mbTrim(str_replace("\r\n", "\n", $_POST['message']));
  $_SESSION['input_pre_message'] = $_POST['message'];
} else {
  $is_valid_message = false;
}

if ($is_valid_message && $input_message === '') {
  $is_valid_message = false;
  $_SESSION['input_error_message'] = '投稿内容の入力は必須です。';
}

if ($is_valid_message && mb_strlen($input_message) > 1000) {
  $is_valid_message = false;
  $_SESSION['input_error_message'] = '投稿内容は 1000 文字以下で入力してください。(現在 ' . mb_strlen($input_message) . ' 文字)';
}

// 投稿をデータベースへ保存する処理
if ($is_valid_auther_name && $is_valid_message) {
  if ($input_author_name === '') {
    $input_author_name = '匿名さん';
  }

  // INSERT クエリを作成する
  // :author_name、:message はプレースホルダという。後で $stmt->bindValue を使用して値をセットするときのニックネームのようなもの。自分で決められる。
  $query = 'INSERT INTO posts (author_name, message) VALUES (:author_name, :message)';

  // SQL 実行の準備 (実行はされない)
  $stmt = $dbh->prepare($query);

  // プレースホルダに値をセットする
  $stmt->bindValue(':author_name', $input_author_name, PDO::PARAM_STR);
  $stmt->bindValue(':message', $input_message, PDO::PARAM_STR);

  // クエリを実行する
  $stmt->execute();
  $_SESSION['input_error_author_name'] = '';
  $_SESSION['input_error_message'] = '';
  $_SESSION['input_pre_author_name'] = '';
  $_SESSION['input_pre_message'] = '';
}

header('Location: /');
exit();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
  • 12-20 行目





 




// 入力値を確認する(投稿者)
$is_valid_auther_name = true;
$input_author_name = '';
if (isset($_POST['author_name'])) {
  $input_author_name = mbTrim(str_replace("\r\n", "\n", $_POST['author_name']));
  $_SESSION['input_pre_author_name'] = $_POST['author_name'];
} else {
  $is_valid_auther_name = false;
}
1
2
3
4
5
6
7
8
9

$_SESSION['input_pre_author_name'] に投稿者名を格納します。



 


if ($is_valid_auther_name && mb_strlen($input_author_name) > 30) {
  $is_valid_auther_name = false;
  $_SESSION['input_error_author_name'] = 'ニックネームは 30 文字以内で入力してください。(現在 ' . mb_strlen($input_author_name) . ' 文字)';
}
1
2
3
4

整形が完了しているかつ、整形された投稿者名文字列が 30 文字より大きい場合にセッションにエラーメッセージを格納します。






 




// 入力値を確認する(投稿内容)
$is_valid_message = true;
$input_message = '';
if (isset($_POST['message'])) {
  $input_message = mbTrim(str_replace("\r\n", "\n", $_POST['message']));
  $_SESSION['input_pre_message'] = $_POST['message'];
} else {
  $is_valid_message = false;
}
1
2
3
4
5
6
7
8
9

投稿者名と同様に、入力された投稿内容の整形を行い、セッションに値を格納します。



 




 


if ($is_valid_message && $input_message === '') {
  $is_valid_message = false;
  $_SESSION['input_error_message'] = '投稿内容の入力は必須です。';
}

if ($is_valid_message && mb_strlen($input_message) > 1000) {
  $is_valid_message = false;
  $_SESSION['input_error_message'] = '投稿内容は 1000 文字以下で入力してください。(現在 ' . mb_strlen($input_message) . ' 文字)';
}
1
2
3
4
5
6
7
8
9

投稿者名のバリデーションと同様に、投稿内容が空文字の場合と、1000 文字より大きい場合にセッションにエラーメッセージを格納しています。




















 
 
 
 





// 投稿をデータベースへ保存する処理
if ($is_valid_auther_name && $is_valid_message) {
  if ($input_author_name === '') {
    $input_author_name = '匿名さん';
  }

  // INSERT クエリを作成する
  // :author_name、:message はプレースホルダという。後で $stmt->bindValue を使用して値をセットするときのニックネームのようなもの。自分で決められる。
  $query = 'INSERT INTO posts (author_name, message) VALUES (:author_name, :message)';

  // SQL 実行の準備 (実行はされない)
  $stmt = $dbh->prepare($query);

  // プレースホルダに値をセットする
  $stmt->bindValue(':author_name', $input_author_name, PDO::PARAM_STR);
  $stmt->bindValue(':message', $input_message, PDO::PARAM_STR);

  // クエリを実行する
  $stmt->execute();
  $_SESSION['input_error_author_name'] = '';
  $_SESSION['input_error_message'] = '';
  $_SESSION['input_pre_author_name'] = '';
  $_SESSION['input_pre_message'] = '';
}

header('Location: /');
exit();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

最終的に投稿者名と投稿内容のバリデーションがともに通った場合に、セッション変数の文字列をリセットしています。

バリデーションがともに通らない場合は 48 行目の if 文の条件 if ($is_valid_auther_name && $is_valid_message) が false となり、セッション変数の文字列がリセットされないため、その値を index.php でフォームに入力値を表示したり、エラーメッセージを表示していく流れとなります。

  $_SESSION['input_error_author_name'] = '';
  $_SESSION['input_error_message'] = '';
  $_SESSION['input_pre_author_name'] = '';
  $_SESSION['input_pre_message'] = '';
}
1
2
3
4
5
  • input_error_author_name 投稿者名のエラーを表示します
  • input_error_message 投稿内容のエラーを表示します
  • input_pre_author_name 投稿者名の文字列
  • input_pre_message 投稿内容の文字列

クエリの実行と同時に、エラーメッセージと投稿データが格納されているセッションをそれぞれ空にします。

# 2. session_values.php でセッションを制御する。

~/src/session_values.php を作成し、以下のコードを記述します。

<?php
// ページ内で使用する変数を初期化する
$messages['input_error_author_name'] = '';
$messages['input_error_message'] = '';
$messages['input_pre_author_name'] = '';
$messages['input_pre_message'] = '';

// ページ内で使用する変数にセッションから代入する
if (isset($_SESSION['input_error_author_name'])) {
  $messages['input_error_author_name'] = $_SESSION["input_error_author_name"];
  unset($_SESSION["input_error_author_name"]);
}

if (isset($_SESSION['input_error_message'])) {
  $messages['input_error_message'] = $_SESSION["input_error_message"];
  unset($_SESSION["input_error_message"]);
}

if (isset($_SESSION['input_pre_author_name'])) {
  $messages['input_pre_author_name'] = $_SESSION["input_pre_author_name"];
  unset($_SESSION["input_pre_author_name"]);
}

if (isset($_SESSION['input_pre_message'])) {
  $messages['input_pre_message'] = $_SESSION["input_pre_message"];
  unset($_SESSION["input_pre_message"]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • 2-6 行目
// ページ内で使用する変数を初期化する
$messages['input_error_author_name'] = '';
$messages['input_error_message'] = '';
$messages['input_pre_author_name'] = '';
$messages['input_pre_message'] = '';
1
2
3
4
5

index.php 内で使用する変数を初期化しています。

  • 9-12 行目
if (isset($_SESSION['action_error_text'])) {
  $messages['action_error_text'] = $_SESSION["action_error_text"];
  unset($_SESSION["action_error_text"]);
}

// 以下同様の処理なので省略
1
2
3
4
5
6

issetでセッション変数が存在するかどうかを確認し、存在する場合は対応する $messages に値を格納しています。

index.php 側で $messages を使用して、テキスト表示を行います。

その後、セッション変数から不要になった割当をunsetで削除しています。

# 3. index.php でテキストを表示する。

~~/public/index.php に以下のコードを追加、修正します。


 








 




























 
 
 
 
 
 

 
 
 
 
 
 


<?php
session_start();
require_once(__DIR__ . '/../src/db_connect.php');

if (isset($_POST['action_type']) && $_POST['action_type']) {
  if ($_POST['action_type'] === 'insert') {
    require(__DIR__ . '/../src/insert_message.php');
  }
}

require(__DIR__ . '/../src/session_values.php');
?>
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="robots" content="noindex" />
  <title>ひとこと掲示板</title>
  <link rel="stylesheet" href="./assets/main.css" />
</head>

<body>
  <div class="page-cover">

    <p class="page-title">ひとこと掲示板</p>
    <hr class="page-divider" />
    <div class="form-cover">
      <form action="/" method="post">
        <div class="form-input-title">投稿者ニックネーム</div>
        <input type="text" name="author_name" maxlength="40" value="<?php echo htmlspecialchars($messages['input_pre_author_name'], ENT_QUOTES); ?>" class="input-author-name" />
        <?php if ($messages['input_error_author_name'] !== '') { ?>
          <div class="form-input-error">
            <?= $messages['input_error_author_name']; ?>
          </div>
        <?php } ?>
        <div class="form-input-title">投稿内容<small>(必須)</small></div>
        <textarea name="message" class="input-message"><?php echo htmlspecialchars($messages['input_pre_message'], ENT_QUOTES); ?></textarea>
        <?php if ($messages['input_error_message'] !== '') { ?>
          <div class="form-input-error">
            <?= $messages['input_error_message']; ?>
          </div>
        <?php } ?>
        <input type="hidden" name="action_type" value="insert" />
        <button type="submit" class="input-submit-button">投稿する</button>
      </form>
    </div>

    <hr class="page-divider" />

~~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  • 2 行目
session_start();
1

PHP ではsession_start関数を呼び出し、$_SESSION(セッション変数)を記述することでセッションを扱うことができます。

読み込みファイル、insert_message.php 側で$_SESSION(セッション変数)を利用しているため、index.phpであらかじめ、session_start()を呼び出しています。

  • 11 行目
require(__DIR__ . '/../src/session_values.php');
1

セッション変数を定義している /../src/session_values.php を読み込んで使用します。

  • 40,47 行目
<input type="text" name="author_name" maxlength="40" value="<?php echo htmlspecialchars($messages['input_pre_author_name'], ENT_QUOTES); ?>" class="input-author-name" />
1
<textarea name="message" class="input-message"><?php echo htmlspecialchars($messages['input_pre_message'], ENT_QUOTES); ?></textarea>
1

htmlspecialcharsによってエスケープ処理がされています。 エスケープ処理は、フォームから送られてきた値や、データベースから取り出した値をブラウザ上に表示する際に、特殊文字や記号の変換を行っています。こうすることで、悪意のあるコードの埋め込みを防いだり、HTML 上で特殊文字を適切に表示することができます。

例えば `echo 'I'm fine.' のように文字列を出力させたい場合、エスケープ処理がされないと、プログラム側でシングルクォーテーションで囲われている「I」だけを文字列として認識し、それ以降の「m fine.'」が文字列という判断ができずにエラーを返してしまいます。htmlspecialchars を使うと、「I'm fine.」のように変換されるため、HTML を生成する際には「I'm fine.」という文字列が表示されます。

また、エスケープ処理がないことによって生まれた脆弱性を狙った攻撃手法として、クロスサイトスクリプティング(XSS)があります。

クロスサイトスクリプティング(XSS)とは

クロスサイトスクリプティングとは攻撃者が悪意のあるスクリプトを埋め込み、ユーザーに実行させる攻撃手法。この攻撃により利用者は Cookie のデータや個人情報等を攻撃者に送信してしまい、意図しない操作を実行させられてしまいます。

htmlspecialcharsは基本的に、第一引数にはエスケープする文字列、第二引数のエスケープの種類はENT_QUOTESを指定します。 ENT_QUOTES を指定することで、下記のようにエスケープがされます。

# 変換対象となる文字

変換前 変換後
& (アンパサンド) &
" (ダブルクォート) ENT_NOQUOTES が指定されていない場合、"
' (シングルクォート) ' (ENT_HTML401 の場合) あるいは ' ( ENT_XML1、ENT_XHTML、 ENT_HTML5 の場合)。ただし ENT_QUOTES が指定されている場合に限る
< (小なり) <
> (大なり) >

引用:PHP 公式ドキュメント_htmlspecialchars (opens new window)

他にも「ENT_COMPAT」や「ENT_NOQUOTES」等複数ありますが、「ENE_QUOTES」よりエスケープする文字が少ないので使用される機会は少ないです。

# 動作確認

  • 投稿者名が 30 文字を超えている場合
  • 投稿内容が空の場合
ニックネーム30_投稿内容必須
  • 投稿内容が 1000 文字を超えている場合
1001文字

バリデーションエラーが起きている場合はフォームの中にユーザの入力した値が残っていることも確認できます。

また、phpMyAdmin で posts テーブルの中身を確認しても、バリデーションを通らない場合はデータが追加されていないことが確認できます。

# 最終的なコードとファイル構成

~~/public/index.php

<?php
session_start();
require_once(__DIR__ . '/../src/db_connect.php');

if (isset($_POST['action_type']) && $_POST['action_type']) {
  if ($_POST['action_type'] === 'insert') {
    require(__DIR__ . '/../src/insert_message.php');
  }
}

require(__DIR__ . '/../src/session_values.php');
?>
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="robots" content="noindex" />
  <title>ひとこと掲示板</title>
  <link rel="stylesheet" href="./assets/main.css" />
</head>

<body>
  <div class="page-cover">
    <p class="page-title">ひとこと掲示板</p>
    <hr class="page-divider" />
    <div class="form-cover">
      <form action="/" method="post">
        <div class="form-input-title">投稿者ニックネーム</div>
        <input type="text" name="author_name" maxlength="40" value="<?php echo htmlspecialchars($messages['input_pre_author_name'], ENT_QUOTES); ?>" class="input-author-name" />
        <?php if ($messages['input_error_author_name'] !== '') { ?>
          <div class="form-input-error">
            <?= $messages['input_error_author_name']; ?>
          </div>
        <?php } ?>
        <div class="form-input-title">投稿内容<small>(必須)</small></div>
        <textarea name="message" class="input-message"><?php echo htmlspecialchars($messages['input_pre_message'], ENT_QUOTES); ?></textarea>
        <?php if ($messages['input_error_message'] !== '') { ?>
          <div class="form-input-error">
            <?= $messages['input_error_message']; ?>
          </div>
        <?php } ?>
        <input type="hidden" name="action_type" value="insert" />
        <button type="submit" class="input-submit-button">投稿する</button>
      </form>
    </div>
    <hr class="page-divider" />
    <div class="message-list-cover">
    </div>
  </div>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

~~/src/insert_message.php

<?php

/**
 * 両端の空白を除去する関数です。マルチバイトを含みます。
 * 参考 https://qiita.com/fallout/items/a13cebb07015d421fde3
 */
function mbTrim($pString)
{
    return preg_replace('/\A[\p{Cc}\p{Cf}\p{Z}]++|[\p{Cc}\p{Cf}\p{Z}]++\z/u', '', $pString);
}

// 入力値を確認する(投稿者)
$is_valid_auther_name = true;
$input_author_name = '';
if (isset($_POST['author_name'])) {
    $input_author_name = mbTrim(str_replace("\r\n", "\n", $_POST['author_name']));
    $_SESSION['input_pre_author_name'] = $_POST['author_name'];
} else {
    $is_valid_auther_name = false;
}

if ($is_valid_auther_name && mb_strlen($input_author_name) > 30) {
    $is_valid_auther_name = false;
    $_SESSION['input_error_author_name'] = 'ニックネームは 30 文字以内で入力してください。(現在 ' . mb_strlen($input_author_name) . ' 文字)';
}

// 入力値を確認する(投稿内容)
$is_valid_message = true;
$input_message = '';
if (isset($_POST['message'])) {
    $input_message = mbTrim(str_replace("\r\n", "\n", $_POST['message']));
    $_SESSION['input_pre_message'] = $_POST['message'];
} else {
    $is_valid_message = false;
}

if ($is_valid_message && $input_message === '') {
    $is_valid_message = false;
    $_SESSION['input_error_message'] = '投稿内容の入力は必須です。';
}

if ($is_valid_message && mb_strlen($input_message) > 1000) {
    $is_valid_message = false;
    $_SESSION['input_error_message'] = '投稿内容は 1000 文字以下で入力してください。(現在 ' . mb_strlen($input_message) . ' 文字)';
}

// 投稿をデータベースへ保存する処理
if ($is_valid_auther_name && $is_valid_message) {
    if ($input_author_name === '') {
        $input_author_name = '匿名さん';
    }

    // INSERT クエリを作成する
    // :author_name、:message はプレースホルダという。後で $stmt->bindValue を使用して値をセットするときのニックネームのようなもの。自分で決められる。
    $query = 'INSERT INTO posts (author_name, message) VALUES (:author_name, :message)';

    // SQL 実行の準備 (実行はされない)
    $stmt = $dbh->prepare($query);

    // プレースホルダに値をセットする
    $stmt->bindValue(':author_name', $input_author_name, PDO::PARAM_STR);
    $stmt->bindValue(':message', $input_message, PDO::PARAM_STR);

    // クエリを実行する
    $stmt->execute();
    $_SESSION['input_error_author_name'] = '';
    $_SESSION['input_error_message'] = '';
    $_SESSION['input_pre_author_name'] = '';
    $_SESSION['input_pre_message'] = '';
}

header('Location: /');
exit();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

~~/src/session_value.php

<?php
// ページ内で使用する変数を初期化する
$messages['input_error_author_name'] = '';
$messages['input_error_message'] = '';
$messages['input_pre_author_name'] = '';
$messages['input_pre_message'] = '';

// ページ内で使用する変数にセッションから代入する
if (isset($_SESSION['input_error_author_name'])) {
    $messages['input_error_author_name'] = $_SESSION["input_error_author_name"];
    unset($_SESSION["input_error_author_name"]);
}

if (isset($_SESSION['input_error_message'])) {
    $messages['input_error_message'] = $_SESSION["input_error_message"];
    unset($_SESSION["input_error_message"]);
}

if (isset($_SESSION['input_pre_author_name'])) {
    $messages['input_pre_author_name'] = $_SESSION["input_pre_author_name"];
    unset($_SESSION["input_pre_author_name"]);
}

if (isset($_SESSION['input_pre_message'])) {
    $messages['input_pre_message'] = $_SESSION["input_pre_message"];
    unset($_SESSION["input_pre_message"]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

ファイル構成

.
├── docker-compose.yml
├── php
│   └── Dockerfile
├── public
│   ├── assets
│   │   └── main.css
│   └── index.php
└── src
    ├── db_connect.php
    ├── insert_message.php
    └── session_values.php
1
2
3
4
5
6
7
8
9
10
11
12