php コンタクトフォームの作成(PHP)

2013年11月18日

PHP を使ったコンタクトフォームの作り方のメモ。
(2016年2月14日 全面的に書き換えました。)

目次

基本的なコンタクトフォーム

このコンタクトフォームは「入力ページ → 確認ページ → 完了ページ」の順で遷移します。

サンプル1(メールは送信されません)

ページデザイン

入力ページ

ユーザーが「名前」「e-mail」「件名」「問い合わせ内容」を入力するページです。
スパム防止のために、画像認証機能(Securimage)を付けます。(reCAPTCHA を使ったサンプルもあります)

確認ページ

入力された値を確認・検証して、不備がある場合は、エラーを表示して再度入力フォームを表示します。
入力された値に問題がなければ、入力された値をユーザーに確認してもらい、間違いがなければ送信ボタンをクリックして送信してもらいます。

完了ページ

問い合わせの受付が完了したことを知らせるページで、入力されたデータをメールで送信し、問題がなければ送信完了のメッセージを表示します。

デザインとプログラムの分離

デザイン(HTML)とプログラム(PHP)が混在している場合、プログラムが長くなってくるとそれぞれの内容がわかりづらくなってきてしまいます。そこで HTML ファイルと PHP プログラムを別々のファイルに分けて管理するようにします。

これは、PHP の include 文や require 文を使うことで簡単にできます。デザイン部分(HTML)を別のファイル(テンプレートと呼びます)にして、プログラム(PHP)からその HTML(テンプレート)を読み込むようにします。

ファイル構成

  • contact1.php:入力ページのプログラム部分
  • contact2.php:確認ページのプログラム部分
  • contact3.php:完了ページのプログラム部分
  • contact1_view.php:入力ページのデザイン部分(テンプレート)
  • contact2_view.php:確認ページのデザイン部分(テンプレート)
  • contact3_view.php:完了ページのデザイン部分(テンプレート)
  • functions.php:必要な関数をまとめて記述したファイル(テンプレートエンジン)

テンプレート「contact1_view.php」「contact2_view.php」「contact3_view.php」と「functions.php」は Webブラウザから直接アクセスする必要はないので、可能であれば Webブラウザ(外部)からアクセスできない領域に配置します。

Webブラウザ(外部)からアクセスできない領域に配置できない場合は適当な場所にフォルダを作成して「.htaccess」で全てのアクセスを拒否(deny from all)するように設定します。

以下はこのサンプルでのフォルダ構成です。

ここでは、わかりやすいように「contact」フォルダ内に全てのファイルを配置します。「libs」フォルダ内に「templates」フォルダを作成し、テンプレートファイルを配置します。また、「libs」フォルダには、「.htaccess」「functions.php」を配置します。

contact_01

このサンプルでは、上記のフォルダ構成にしているので、「libs」フォルダは外部からアクセスできないように、以下の内容の「.htaccess」を配置します。

deny from all

ページ遷移

contact1.php にアクセス
初回:データを初期化して、contact1_view.php にデータを渡し入力画面を表示
初回以降:セッション変数よりデータを取得して、contact1_view.php にデータを渡し入力画面を表示

入力画面にユーザーが情報を入力後、「確認画面へ」をクリック
form 要素の action=”contact2.php” により contact2.php へ

contact2.php では固定トークンを確認し POST されたデータをチェック、整形、及び画像認証を確認
エラーがあった場合:データを contact1_view.php に渡して再度入力画面を表示
エラーがない場合:データをセッション変数に代入、さらにデータを contact2_view.php に渡して確認画面を表示

contact2_view.php で確認画面で入力されたデータを表示
入力画面に戻るオプションとメールを送信するオプションを表示
入力画面に戻るオプション:form 要素の action=”contact1.php” により contact1.php へ
メールを送信するオプション:form 要素の action=”contact3.php” により contact3.phpへ

contact3.php
POSTされたデータをチェック、固定トークンを確認し、メール送信で使用する変数にセッション変数の値を代入してメールを送信。
メールの送信の結果によりメッセージを作成し、そのデータを contact3_view.php に渡して完了画面を表示。

テンプレートを表示する関数

以下はプログラムから、テンプレートを表示する関数です。

テンプレートの表示には、include 文を使います。プログラム部分でセッション変数や POST された値などを配列に格納しているので、それらを配列のキーを変数名とする変数に代入して渡すようにしています。また、その際に変数の値を全てエスケープ処理します。

  • 第1パラメータ($_template):テンプレートファイル名
  • 第2パラメータ($data):テンプレートで使うデータ(配列)
function display($_template, $data) {
  foreach($data as $key => $val){
    //テンプレートで使う変数を生成し、値をエスケープ処理
    $$key = h($val);  //可変変数
  }
  
  //変数作成後は、受け取った「$data」は、不要になるので破棄
  unset($data);
  
  //テンプレートを読み込む
  include dirname(__FILE__) . '/templates/'. $_template;
}

foreach 構文を使い、配列「$data」の全ての要素をループ処理します。その際、可変変数を使ってキーの値($key)を変数名としています。h() 関数は、別途定義してあるエスケープ処理の関数で、値をエスケープ処理します。

$$key = h($val);  //可変変数

「$$key」は $key の値が変数として展開されます。例えば、$data[‘name’] の場合、変数名は $name になります。

またテンプレートを読み込む(表示する) include 文は以下のようになっています。

include dirname(__FILE__) . '/templates/'. $_template;

「__FILE__」は、PHP の“マジック”定数の1つで、実行中のファイルが絶対パスで定義されます。このサンプルの場合、「C:\xampp\htdocs\…\contact\libs\functions.php」のような functions.php への絶対パスになります。

また、dirname() 関数は、親ディレクトリのパスを返すので、dirname(__FILE__) は、「libs」フォルダの絶対パスになり、dirname(__FILE__) . ‘/templates/’. $_template は、テンプレートファイルへの絶対パスになります。

functions.php

functions.php には、前述のテンプレートを表示する display() 関数の他に、エスケープ処理をする h() 関数、値を検証する checkInput() 関数を記述します。

<?php
//エスケープ処理を行う関数
function h($var) {
  if(is_array($var)){
    //$varが配列の場合、h()関数をそれぞれの要素について呼び出す(再帰)
    return array_map('h', $var);
  }else{
    return htmlspecialchars($var, ENT_QUOTES, 'UTF-8');
  }
}

//テンプレートを表示する関数
//第1パラメータ($_template):テンプレートファイル名
//第2パラメータ($data):テンプレートで使うデータ(配列)
function display($_template, $data) {
  //テンプレートで使う変数を生成し、値をエスケープ処理
  foreach($data as $key => $val){
    $$key = h($val);  
  }
  //変数作成後は、受け取った「$data」は、不要になるので破棄
  unset($data);
  //テンプレートを読み込む
  include dirname(__FILE__) . '/templates/'. $_template;
}

//入力値に不正なデータがないかなどを検証する関数
function checkInput($var){
  if(is_array($var)){
    return array_map('checkInput', $var);
  }else{
    //php.iniでmagic_quotes_gpcが「on」の場合の対策
    if(get_magic_quotes_gpc()){  
      $var = stripslashes($var);
    }
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){  
      die('不正な入力です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){ 
      die('不正な入力です。');
    }
    //改行、タブ以外の制御文字のチェック
    if(preg_match('/\A[\r\n\t[:^cntrl:]]*\z/u', $var) === 0){  
      die('不正な入力です。制御文字は使用できません。');
    }
    return $var;
  }
}

画像認証 CAPTCHA

画像認証 CAPTCHA(スパム投稿防止)には Securimage を利用します。

Securimage のサイトからダウンロードして解凍し、適当な場所に「securimage」というフォルダを作成してそこに配置します。このサンプルでは、「contact」フォルダ内に配置します。

画像と表示された画像を再表示するリンクを表示するには、以下のコードをフォームの表示したい位置に記述します。

画像と表示された画像を再表示するリンクを表示するには、以下のコードをフォームの表示したい位置に記述します。

<p>
  <img id="captcha" src="securimage/securimage_show.php" alt="CAPTCHA Image">
  <a href="#" onclick="document.getElementById('captcha').src = 'securimage/securimage_show.php?' + Math.random(); return false">
    <img src="securimage/images/refresh.png" alt="別の画像を表示">
  </a>
</p>

また、表示された画像のコードを入力する input 要素を適当な位置に記述します。

<input type="text" name="captcha_code" id="captcha_code" size="15" maxlength="6" placeholder="確認キーワード(必須)">

必要であれば securimage フォルダの中にある securimage_show.php を編集することで、大きさや色などのカスタマイズが可能です。例えば、表示される文字を指定するには以下のように securimage_show.php に記述します。

$img->charset = 'ABCDEFGHKLMNPRSTUVWYZacegkpuvwy23456789';

このサンプルでは、securimage_show.php に画像の大きさ、ひずみ具合、表示される線の数を以下のように指定しています。
カスタマイズ方法は、Securimage Customizing Securimage(カスタマイズ)に記載されています。

// setting height and calculating optimal width
$img->image_height = 50;
$img->image_width = (int)($img->image_height * 2.875);

// set how distorted the characters will be
$img->perturbation = 0.4; 

// drawing lines over the image
$img->num_lines = 1; //number of lines on the image

Securimage の読み込みと生成は以下のように記述します。このサンプルでは「contact2.php」に記述します。

//画像認証ライブラリの読み込み(securimage)
include_once 'securimage/securimage.php';
$securimage = new Securimage();

キーワードが正しいかを判定するには、以下のように記述します。このサンプルではキーワードは name 属性が「captcha_code」の input 要素から POST されます。

if ($securimage->check($_POST['captcha_code']) == false) {
  echo '*画像認証の確認キーワードが誤っています。';   
}

入力ページ

contact1.php 入力ページのプログラム

contact1.php は唯一ユーザーが直接コンタクトフォームにアクセスできるページになります。テンプレートの contact1_view.php, contact2_view.php, contact3_view.php は、.htaccess でアクセスできないように設定されているか、外部からアクセスできない場所に保存されています。contact2.php, contact3.php は、contact1.php にアクセスしてトークンを発行されていないと直接アクセスしてもエラーになります。

この処理は、ユーザーの初回アクセス時、フォーム送信後エラーがあって再度表示される時、フォーム送信後エラーがなく確認ページで「入力画面へ戻る」をクリックした時に行われます。

contact1.php の PHP での処理 は以下のようになります。

最初にセッションを使えるように session_start() 関数を呼び出します。session_start()関数は、必ず、Webブラウザへの出力が行われる前に、呼び出す必要があります。

その直後に session_regenerate_id() 関数を呼び出してセッション ID を変更します(セッションハイジャック対策)。第1パラメータには必ず「TRUE」を指定します。

9行目:テンプレートを表示する display() 関数、エスケープ処理をする h() 関数、値を検証する checkInput() 関数が記述されているファイル(functions.php)を読み込みます。

$data はテンプレート(contact1_view.php)に渡す配列です。最初に初期化しておきます。

15~18行目:テンプレート(contact1_view.php)を表示する際、初回アクセス時は入力データがないのでこの初期化をしないとエラーとなってしまいます。フォーム送信後エラーがなく確認ページから戻ってきた場合は、$_SESSION 変数には値が入っています。但し、フォーム送信後エラーがある場合は、$_SESSION 変数にはまだ何も入っていません。

21~24行目:CSRF 対策の固定トークンを生成しています。

27行目:生成された固定トークンの値を $data[‘ticket’] に代入しています。30行目の display() 関数によって、固定トークンの値は変数 $ticket に代入されて、contact1_view.php に渡され、その後隠しフィールドに挿入します。

30行目: display() 関数は、配列 $data のキーを変数名とする変数を生成し、それらの値をエスケープ処理してからテンプレート contact1_view.php を表示します。

contact1.php

<?php
//セッションを開始
session_start();   

//セッションIDを変更(セッションハイジャック対策)
session_regenerate_id(TRUE);    

//テンプレートエンジン functions.php の読み込み
require 'libs/functions.php';   

//テンプレートに渡す変数の初期化
$data = array(); //配列を初期化

//初回以外ですでにセッション変数に値が代入されていれば、その値を。そうでなければNULLで初期化
$data['name'] = isset($_SESSION['name']) ? $_SESSION['name'] : NULL; 
$data['email'] = isset($_SESSION['email']) ? $_SESSION['email'] : NULL;
$data['subject'] = isset($_SESSION['subject']) ? $_SESSION['subject'] : NULL;
$data['body'] = isset($_SESSION['body']) ? $_SESSION['body'] : NULL;

//CSRF対策の固定トークンを生成
if(!isset($_SESSION['ticket'])){
  //$_SESSION['ticket']がセットされていなければ、トークンを生成して代入
  $_SESSION['ticket'] = sha1(uniqid(mt_rand(), TRUE));
}

//トークンの値を配列に代入してテンプレートに渡す(隠しフィールドに挿入)
$data['ticket'] = $_SESSION['ticket'];

//入力ページのテンプレートにデータを渡して表示
display('contact1_view.php', $data);	

contact1_view 入力ページのテンプレート

contact1_view は、ユーザーがコンタクトフォームにアクセスした際に表示されるページです。

14行目~20行目:ユーザーがフォームを contact2.php に送信してエラーがあった場合、contact2.php の display(‘contact1_view.php’, $data)によりこのページが表示され、その際にエラーメッセージを表示するための部分です。

27行目~56行目:ユーザーに入力してもらうフォームです。

44行目~49行目:画像認証(Securimage)の部分です。

54行目:CSRF 対策用の確認ページへトークンを POST する、隠しフィールドです。

contact1_view.php

<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />   
<title>コンタクト</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="contents">

<h2>コンタクト </h2>

<!--入力値にエラーがあった場合エラーメッセージを表示-->
<div id="errorDispaly">
<?php if(isset($error)): ?>
<?php foreach($error as $val): ?>
<?php echo $val; ?><br />
<?php endforeach; ?>
<?php endif; ?>
</div><!-- end of #errorDispaly-->

<div id="formArea">
<fieldset id="contactForm">
<legend class="contact_form">お問い合わせフォーム</legend>
<form action="contact2.php" method="post">
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo $name; ?>" size="50" placeholder="お名前(必須)">
</div>
<div class="form-group">  
  <label for="email">e-mail</label>
  <input type="text" name="email" value="<?php echo $email; ?>" size="50" placeholder="メールアドレス(必須)">
</div>
<div class="form-group"> 
  <label for="subject">件名</label>
  <input type="text" name="subject" value="<?php echo $subject; ?>" size="50" placeholder="件名(必須)">
</div>
<div class="form-group">  
  <label for="body">内容</label>
  <textarea name="body" cols="50" rows="8" placeholder="内容(必須:500文字まで)"><?php echo $body; ?></textarea>
</div>
<div class="form-group">
  <label for="captcha_code">確認キーワード</label>
  <p><img id="captcha" src="securimage/securimage_show.php" alt="CAPTCHA Image"><a id="different_img" href="#" onclick="document.getElementById('captcha').src = 'securimage/securimage_show.php?' + Math.random(); return false"><img id="dif_img" src="securimage/images/refresh2.png" alt="別の画像を表示">(別の画像)</a> </p>
  <p>* 表示されているキーワードを半角英数字でご記入ください。</p>   
  <input type="text" name="captcha_code" id="captcha_code" size="15" maxlength="6" placeholder="確認キーワード(必須)">
</div>    
  <p>
  <input type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
</form>
</fieldset>
</div><!--end of #formArea-->
</div><!--end of #contents--> 

</body>
</html>

確認ページ

contact2.php 確認ページのプログラム

ユーザーが入力ページでフォームを入力後、「確認画面へ」のボタンをクリックすると以下の処理が行われます。

セッションを利用するために、session_start() でセッションを開始します。

7~8行目:include_once で画像認証ライブラリ(securimage)を読み込み、new で securimage のインスタンスを生成して画像認証が利用できるようにします。

11行目:POST された値に問題がないかを functions.php に記述されている checkInput() で検証します。

13~21行目:固定トークンを確認(CSRF対策)。
isset($_POST[‘ticke’], $_SESSIONT[‘ticke’]) で両方の値がセットされているかを確認して、されていなければ処理を中止します。セットされている場合は、隠しフィールドから POST されたトークンの値とセッションに保存されているトークンの値を比較して、値が一致しなければ処理を中止します。

24~29行目:POSTされたデータを変数に格納しています。

33~37行目:入力ページから POST で送信されたデータを、trim() で前後にあるホワイトスペースを削除して整形し、再び変数に代入しています。

40行目:この後の値の検証で発生するエラーを格納するための配列を初期化します。

42~77行目:入力された値が空でないか、形式があっているか、文字数が制限範囲内かなどを全ての値に対して検証しています。詳細は「フォームの検証」を参照ください。

75行目:画像認証の画像に表示された文字列と $_POST[‘captcha_code’] の値が正しいかを検証しています。

80~88行目:値を検証してエラーがあった場合の処理です。配列 $data を初期化して、エラーの内容の配列($error)、POST された値、トークンの値を代入し、 display() で $data を contact1_view.php に渡して入力ページを表示します。contact1_view.php では、渡された $data からエラーを表示し、POST された値をそれぞれのフィールドに表示します。

89~103行目:値を検証してエラーがない場合の処理です。POST された値をセッション変数に代入し、セッション変数を使えるようにします。配列 $data を初期化して、POST された値、トークンの値を代入し、 display() で $data を contact2_view.php に渡して確認ページを表示します。

contact2.php

<?php
session_start();    //セッションを開始

require 'libs/functions.php';   //テンプレートエンジンの読み込み

//画像認証ライブラリの読み込み(securimage)
include_once 'securimage/securimage.php';
$securimage = new Securimage();

//POSTされたデータをチェック
$_POST = checkInput($_POST);

//固定トークンを確認(CSRF対策)
if(isset($_POST['ticket'], $_SESSION['ticket'])){
  $ticket = $_POST['ticket'];
  if($ticket !== $_SESSION['ticket']){
    die('不正アクセスの疑いがあります。');
  }
}else{
  die('不正アクセスの疑いがあります。');
}

//POSTされたデータを変数に格納
$name = isset($_POST['name']) ? $_POST['name'] : NULL;
$email = isset($_POST['email']) ? $_POST['email'] : NULL;
$subject = isset($_POST['subject']) ? $_POST['subject'] : NULL;
$body = isset($_POST['body']) ? $_POST['body'] : NULL;
//以下は画像認証用データ
$captcha_code = isset($_POST['captcha_code']) ? $_POST['captcha_code'] : NULL;


//POSTされたデータを整形(前後にあるホワイトスペースを削除)
$name = trim($name);
$email = trim($email);
$subject = trim($subject);
$body = trim($body);
$captcha_code = trim($captcha_code);

//エラーメッセージを保存する配列の初期化
$error = array();

if($name == '') {
  $error[] = '*お名前は必須項目です。';
  //制御文字でないことと文字数をチェック
}else if(preg_match('/\A[[:^cntrl:]]{1,30}\z/u', $name) == 0) {   
  $error[] = '*お名前は30文字以内でお願いします。';
}

if($email == ''){
  $error[] = '*メールアドレスは必須です。';
}else{   //メールアドレスを正規表現でチェック
  $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD';
  if(!preg_match($pattern, $email)){
    $error[] = '*メールアドレスの形式が正しくありません。';
  }
}

if($subject == '') {
  $error[] = '*件名は必須項目です。';
  //制御文字でないことと文字数をチェック
}else if(preg_match('/\A[[:^cntrl:]]{1,100}\z/u', $subject) == 0) {   
  $error[] = '*件名は100文字以内でお願いします。';
}

if($body == '') {
  $error[] = '*内容は必須項目です。';
  //制御文字(タブ、改行を除く)でないことと文字数をチェック
}else if(preg_match('/\A[\r\n\t[:^cntrl:]]{1,500}\z/u', $body) == 0) {   
  $error[] = '*内容は500文字以内でお願いします。';
}

//画像認証のチェック
if(mb_strlen($captcha_code) <> 6){
  $error[] = '*画像認証の確認キーワードは6文字で入力してください。';
}else if ($securimage->check($captcha_code) == false) {
  $error[] = '*画像認証の確認キーワードが誤っています。';
}

//チェックの結果にエラーがあった場合は、テンプレートの表示に必要な入力されたデータとエラーメッセージを配列「$data」に代入し、display()関数でform1_view.phpを表示
if(count($error) >0){    //エラーがあった場合
  $data = array();
  $data['error'] = $error;
  $data['name'] = $name;
  $data['email'] = $email;
  $data['subject'] = $subject;
  $data['body'] = $body;
  $data['ticket'] = $ticket;
  display('contact1_view.php', $data);
}else{    //エラーがなかった場合
  //POSTされたデータをセッション変数に保存
  $_SESSION['name'] = $name;
  $_SESSION['email'] = $email;
  $_SESSION['subject'] = $subject;
  $_SESSION['body'] = $body;
  
  //確認画面を表示
  $data = array();
  $data['name'] = $name;
  $data['email'] = $email;
  $data['subject'] = $subject;
  $data['body'] = $body;
  $data['ticket'] = $ticket;
  display('contact2_view.php', $data);
}

contact2_view.php 確認ページのテンプレート

ユーザーが入力ページでフォームを入力後、「確認画面へ」のボタンをクリックして、contact2.php で値などが検証されて問題がない場合に、このページは表示されます。

contact2.php の display(‘contact2_view.php’, $data) によりこのページが表示され、渡された $name, $email, $subject, $body を使って入力された値を表示して、ユーザーに確認してもらいます。これらの変数の値は、display() 関数ですでにエスケープ処理されているので、出力の際のエスケープ処理は不要です。

「内容($body)」は改行を表示するため、nl2br() 関数を使用します。

27~29行目:入力ページへ戻るためのフォームです。ユーザーが入力した値は、すでにセッション変数に入っているので、それらの値を POST する必要はありません。

32~35行目:入力された値に問題がない場合は contact3.php に POST します。この時、CSRF対策のトークンの値(33行目)を POST する必要があります。ユーザーが入力した値は、すでにセッション変数に入っているので、それらの値を POST する必要はありません。

contact2_view.php

<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>コンタクト(確認)</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="contents">
<h2>コンタクト(確認)</h2>
<div id="confirmArea">
<p>以下の内容でよろしければ送信ボタンを押してください。</p>
  <dl>
  <dt>お名前:</dt>
  <dd><?php echo $name; ?></dd>
  <dt>e-mail:</dt>
  <dd><?php echo $email; ?></dd>
  <dt>件名:</dt>
  <dd><?php echo $subject; ?></dd>
  <dt>内容:</dt>
  <dd><?php echo nl2br($body); ?></dd>
  </dl>
  </div>
  <fieldset class="confirm">
<form action="contact1.php" method="post">
  <p><input type="submit" value="入力画面へ戻る" class="btn_submit"></p>
</form>
</fieldset>
<fieldset class="confirm">
<form action="contact3.php" method="post">
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
  <p><input type="submit" value="送信する" class="btn_submit"></p>
</form>
</fieldset>

</div><!--end of #contents--> 
</body>
</html>

完了ページ

contact3.php 完了ページのプログラム

ユーザーが確認ページで、「送信する」のボタンをクリックすると form の action 属性に指定されている contact3.php に POST されます。

完了ページでは、CSRF 対策の固定トークンのチェックとメール送信の処理を行います。

最初に session_start() を呼び出して、セッションを開始します。続いて functions.php を読み込み、checkInput($_POST) で POST されたデータをチェックします。

10~17行目:固定トークンを確認(CSRF対策)。
isset($_POST[‘ticke’], $_SESSIONT[‘ticke’]) で両方の値がセットされているかを確認して、されていなければ処理を中止します。セットされている場合は、隠しフィールドから POST されたトークンの値とセッションに保存されているトークンの値を比較して、値が一致しなければ処理を中止します。(contact2.php と同じ処理)

19~23行目:変数にセッション変数の値(contact2.php でユーザーの入力内容を検証・整形した値)を代入しています。23行目では、問い合わせ本文の前に問い合わせから来たことがわかりやすいように文字を追加しています。また、これらの値は検証及び整形はしていますが、エスケープ処理はしていないので、問い合わせ本文は h() 関数でエスケープ処理してから変数に代入しています。これらの変数を使って、メールを送信します。

28行目~61行目:メール(sendmail)の送信処理です。(後述)

62行目~65行目:contact3_view へ渡す配列 $data を初期化して、メール処理の結果($result)とその結果によるメッセージ($message)を配列に代入します。そしてそれらの値を display() 関数で、contact3_view へ渡します。

contact3.php

<?php
session_start();    //セッションを開始

require 'libs/functions.php';   //テンプレートエンジンの読み込み

//POSTされたデータをチェック
$_POST = checkInput($_POST);

//固定トークンを確認(CSRF対策)
if(isset($_POST['ticket'], $_SESSION['ticket'])){
  $ticket = $_POST['ticket'];
  if($ticket !== $_SESSION['ticket']){
    die('不正アクセスの疑いがあります。');
  }
}else{
  die('不正アクセスの疑いがあります。');
}

//変数にセッション変数の値を代入
$name = $_SESSION['name'];
$email = $_SESSION['email'];
$subject = $_SESSION['subject'];
$body = 'コンタクトページからの問い合わせ'. "\n\n" .h($_SESSION['body']);
//以下は PHPMailer を使って HTML メールを送信する場合
//$body = 'コンタクトページからの問い合わせ'. "<br>" . h($_SESSION['body']);

//--------sendmail------------

//メールの宛先
$mailTo = 'info@example.com';

//Return-Pathに指定するメールアドレス
$returnMail = 'info@example.com';

//mbstringの日本語設定
mb_language('ja');
mb_internal_encoding('UTF-8');

//From ヘッダーを作成
$header = 'From: ' . mb_encode_mimeheader($name). ' <' . $email. '>';

//メールの送信、セーフモードがOnの場合は第5引数が使えない
if(ini_get('safe_mode')){
  $result = mb_send_mail($mailTo, $subject, $body, $header);
}else{
  $result = mb_send_mail($mailTo, $subject, $body, $header, '-f'. $returnMail);
}

//送信結果により表示するメッセージの変数を初期化
$message = '';

//メール送信の結果判定
if($result) {
  $message = 'ありがとうございます。送信完了いたしました。';
  //成功した場合はセッションを破棄
  $_SESSION = array();   //空の配列を代入し、すべてのセッション変数を消去 
  session_destroy();   //セッションを破棄
}else{
  $message = '申し訳ございませんが、送信に失敗しました。しばらくしてもう一度お試しになるか、メールにてご連絡ください。';
}

$data = array();
$data['message'] = $message;
$data['result'] = $result;
display('contact3_view.php', $data);    

以下は、ローカル環境(XAMPP)で自分のドメインのメールサーバー()を使ってメールを送信する場合の、sendmail.ini の設定例です。

//sendmail.ini 
smtp_server=mail.example.com  //送信サーバーを指定
smtp_port=587  //送信ポートを指定
auth_username=info@example.com  //認証のためのユーザー ID
auth_password=xxxxxxxx  //パスワード   

43~47行目:mb_send_mail() 関数を使ってメールを送信します。戻り値の $result には、メールの送信が成功した場合は TRUE が、送信に失敗した場合は FALSE が入ります。

54行目~63行目:メールの送信の結果($result)により、contact3_view.php で表示するメッセージを設定します。

56~57行目:メールの送信が成功した場合はセッションを破棄します。まず、セッション変数を消去して、その後 session_destroy() でセッションを破棄します。

62~65行目:配列 $data にメッセージと送信結果を格納して、 display() で contact3_view.php を表示します。

PHPMailer を使う場合

この例では、メールアカウントのパスワード、送信先、Bcc、ホスト名などは、外部からアクセスできない別のファイル(phpmailvars.php)に定数として記述して require_once で読み込むようにしています。

phpmailvars.php

<?php
//メールサーバー
define('MAIL_HOST', 'localhost');
//PHPMailer を使って送信するための E-mail アカウント
define('MAIL_USER', 'info@example.com');
//パスワード
define('MAIL_PASSWORD', 'xxxxxxxxxxxx');
//送信先
define('SEND_TO', 'info@example.com');
//Bcc アドレス
define('BCC', 'xxxxxx@xxxxxxxxxx.com');
//TCP ポート
define('PORT', 587);   

以下の例では、PHPMailer を public_html の直下に配置した場合の例です。

PHPMailer のメールの処理部分以外は前述の sendmail の例と同じです。

//--------PHPMailer----------

//mbstring の日本語設定
mb_language("japanese");
mb_internal_encoding("UTF-8");

//メールアカウント情報(パスワード等)の読み込み
require_once('/home/username/etc/xxxx/PHPmailer/phpmailvars.php');  

//PHPMailer の読み込み
require_once('/home/username/public_html/PHPMailer/PHPMailerAutoload.php'); 

$mail = new PHPMailer();  //PHPMailer のインスタンスを生成

$mail->IsSMTP();   // SMTP を使用
$mail->Host = MAIL_HOST; // SMTP サーバーを指定(phpmailvars.phpで定義)
$mail->SMTPAuth = true;   // SMTP authentication を有効に
$mail->Username = MAIL_USER;  // SMTP ユーザ名(phpmailvars.phpで定義)
$mail->Password = MAIL_PASSWORD;  // SMTP パスワード(phpmailvars.phpで定義)
$mail->Port = PORT;    // TCP ポートを指定(phpmailvars.phpで定義)
$mail->From = $email;   //差出人アドレス
$mail->FromName = mb_encode_mimeheader($name);   //差出人名
$mail->AddAddress(SEND_TO);  //メールの宛先(phpmailvars.phpで定義)
//$mail->AddAddress(SEND_TO, mb_encode_mimeheader("宛先名")); //日本語の宛先名を表示する場合
$mail->AddBcc(BCC);  //Bcc アドレス(phpmailvars.phpで定義) 
$mail->Subject = mb_encode_mimeheader($subject);   //件名
$mail->WordWrap = 70;  //70 文字で改行(好みで)
$mail->IsHTML(true);   // HTML形式のメールに設定

//日本語用の設定
$mail->CharSet = "iso-2022-jp";
$mail->Encoding = "7bit";
$mail->Body  = mb_convert_encoding($body,"JIS","UTF-8");
$mail->AltBody = mb_convert_encoding($body,"JIS","UTF-8");  //テキスト表示の場合の本文 

//メール送信の結果(真偽値)を $result に代入
$result = $mail->Send();

//送信結果により表示するメッセージの変数を初期化
$message = '';

//メール送信の結果判定
if($result) {
  $message = 'ありがとうございます。送信完了いたしました。';
  //成功した場合はセッションを破棄
  $_SESSION = array();   //空の配列を代入し、すべてのセッション変数を消去 
  session_destroy();   //セッションを破棄
}else{
  $message = '申し訳ございませんが、送信に失敗しました。しばらくしてもう一度お試しになるか、メールにてご連絡ください。';
}

$data = array();
$data['message'] = $message;
$data['result'] = $result;
display('contact3_view.php', $data);

contact3_view 完了ページのテンプレート

contact3.php でメールの送信処理が完了すると、成功した場合は $result に true が入り、$message には「ありがとうございます。送信完了いたしました。」という文字列が入ります。

失敗した場合は、$result に false が入り、$message には「申し訳ございませんが、送信に失敗しました。…」という文字列が入ります。

これらの値を利用して、メッセージを表示します。

<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>コンタクト(完了)</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="contents">
<h2>コンタクト(<?php echo $result ? "完了" : "送信失敗"; ?>)</h2>
<div id="message">
<p><?php
if($result) {
	echo '<h3>送信完了!</h3>';
}else{
	echo '<a href="contact1.php">送信失敗(入力ページへ)</a>';
}
?></p>
<p><?php echo $message; ?></p>
</div><!--end of #message-->
<?php if(!$result): ?>
<p>送信失敗が継続する場合は、一度ブラウザを閉じてからやり直すとうまくいくことがあります。</p>
<?php endif; ?>

</div><!--end of #contents--> 
</body>
</html>

サンプル1

サンプルでは、実際にはメールは送信されません。

jQuery の検証を追加

jQuery を使った検証を追加する例です。

contact1_view.php の各コントロール(input/textarea 要素)にはバリデーションの条件を示す「validate」「required」「number」「mail」などの class 属性を付与します。

  • validate:検証対象の要素に付与するクラス
  • required:必須項目に付与するクラス
  • max30, max50 :文字の最大数を検証する項目に付与するクラス
  • mail:メールアドレスを検証する項目に付与するクラス
<form action="contact2.php" method="post" role="form">
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo $name; ?>" size="50" placeholder="お名前(必須)" class="validate required max30 ">
</div>
<div class="form-group">  
  <label for="email">e-mail</label>
  <input type="text" name="email" value="<?php echo $email; ?>" size="50" placeholder="メールアドレス(必須)" class="validate required mail">
</div>
<div class="form-group"> 
  <label for="subject">件名</label>
  <input type="text" name="subject" value="<?php echo $subject; ?>" size="50" placeholder="件名(必須)" class="validate required max50">
</div>
<div class="form-group">  
  <label for="body">内容</label> <span id="count"> </span>/500
  <textarea name="body" cols="50" rows="8" placeholder="内容(必須:500文字まで)" class="validate required max500"><?php echo $body; ?></textarea>
</div>
<div class="form-group">
  <label for="captcha_code">確認キーワード</label>
  <p><img id="captcha" src="securimage/securimage_show.php" alt="CAPTCHA Image"><a id="different_img" href="#" onclick="document.getElementById('captcha').src = 'securimage/securimage_show.php?' + Math.random(); return false"><img id="dif_img" src="securimage/images/refresh2.png" alt="別の画像を表示">(別の画像)</a></p>
  <p><span>* 表示されているキーワードを<strong>半角英数字</strong>でご記入ください。<br>大文字小文字はどちらでも(全て小文字で)大丈夫です。</span></p>   
  <input type="text" name="captcha_code" id="captcha_code" size="15" maxlength="6" placeholder="確認キーワード(必須)" class="validate required exact6">
</div>  
  <p>
  <input class="btn_submit" type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
</form> 

CSS ではコントロールなどの基本スタイルを定義するとともに、以下のようなエラー(入力漏れやメールアドレスの入力ミスなど)用のスタイルを記述しておきます。

p.error {
  color: red;
}
.error input , 
.error textarea {
  background-color: #F9E9E9;
}

jQuery の検証は以下のようになります。検証用のスクリプトは、 submit イベント内に記述します。

<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">
jQuery(function($){
  //エラーを表示する関数
  function show_error(message, this$) {
    text = this$.parent().find('label').text() + message;
    this$.parent().append("<p class='error'>" + text + "</p>")
  }
  
  $("form").submit(function(){  
    //エラー表示の初期化
    $("p.error").remove();
    $("div").removeClass("error");
    var text = "";
    
    //1行テキスト入力フォームとテキストエリアの検証
    $(":text,textarea").filter(".validate").each(function(){
        
      //必須項目の検証
      $(this).filter(".required").each(function(){
        if($(this).val()==""){
          show_error("は必須項目です", $(this));
        }
      })
      
      //文字数の検証
      $(this).filter(".max30").each(function(){
        if($(this).val().length > 30){
          show_error("は30文字以内です", $(this));
        }
      })
      
      //文字数の検証
      $(this).filter(".max50").each(function(){
        if($(this).val().length > 50){
          show_error("は50文字以内です", $(this));
        }
      })
      
      //文字数の検証
      $(this).filter(".exact6").each(function(){
        if($(this).val() !=="" && $(this).val().length !== 6){
          show_error("は6文字です", $(this));
        }
      })
      
      //文字数の検証
      $(this).filter(".max500").each(function(){
        if($(this).val().length > 500){
          show_error("は500文字以内です", $(this));
        }
      })
      
      //メールアドレスの検証
      $(this).filter(".mail").each(function(){
        if($(this).val() && !(/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/gi).test($(this).val())){
          $(this).parent().append("<p class='error'>メールアドレスの形式が異なります</p>")
        }
      }) 
    })
     
    //error クラスの追加の処理
    if($("p.error").size() > 0){
      $("p.error").parent().addClass("error");
      return false;
    }
  }) //ここまでが submit イベントを使った検証
  
  //テキストエリアに入力された文字数を表示し、500文字を超えると色を変えて表示
  $("textarea").on('keydown keyup change', function() {
    var count = $(this).val().length;
    $("#count").text(count);
    if(count > 500) {
      $("#count").css({color: 'red', fontWeight: 'bold'});
    }else{
      $("#count").css({color: '#333', fontWeight: 'normal'});
    }
  });
 
});
</script>

以下は jQuery の検証を追加した contact1_view.php の全文です。

contact1_view.php jQuery 検証追加

<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>コンタクト</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="contents">

<h2>コンタクト </h2>
<div id="errorDispaly">
<?php if(isset($error)): ?>
<?php foreach($error as $val): ?>
<?php echo $val; ?><br />
<?php endforeach; ?>
<?php endif; ?>
<!--/#errorDispaly--></div>

<div id="formArea">
<fieldset id="contactForm">
<legend class="contact_form"><span class="form_name">お問い合わせフォーム</span></legend>
<form action="contact2.php" method="post" role="form">
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo $name; ?>" size="50" placeholder="お名前(必須)" class="validate required max30 ">
</div>
<div class="form-group">  
  <label for="email">e-mail</label>
  <input type="text" name="email" value="<?php echo $email; ?>" size="50" placeholder="メールアドレス(必須)" class="validate required mail">
</div>
<div class="form-group"> 
  <label for="subject">件名</label>
  <input type="text" name="subject" value="<?php echo $subject; ?>" size="50" placeholder="件名(必須)" class="validate required max50">
</div>
<div class="form-group">  
  <label for="body">内容</label> <span id="count"> </span>/500
  <textarea name="body" cols="50" rows="8" placeholder="内容(必須:500文字まで)" class="validate required max500"><?php echo $body; ?></textarea>
</div>
<div class="form-group">
  <label for="captcha_code">確認キーワード</label>
  <p><img id="captcha" src="securimage/securimage_show.php" alt="CAPTCHA Image"><a id="different_img" href="#" onclick="document.getElementById('captcha').src = 'securimage/securimage_show.php?' + Math.random(); return false"><img id="dif_img" src="securimage/images/refresh2.png" alt="別の画像を表示">(別の画像)</a></p>
  <p><span>* 表示されているキーワードを<strong>半角英数字</strong>でご記入ください。<br>大文字小文字はどちらでも(全て小文字で)大丈夫です。</span></p>   
  <input type="text" name="captcha_code" id="captcha_code" size="15" maxlength="6" placeholder="確認キーワード(必須)" class="validate required exact6a">
</div>  
  <p>
  <input class="btn_submit" type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
</form>
</fieldset>
</div><!--end of #formArea-->
</div><!--end of #contents--> 
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">
jQuery(function($){
  
  function show_error(message, this$) {
    text = this$.parent().find('label').text() + message;
    this$.parent().append("<p class='error'>" + text + "</p>")
  }
  
  $("form").submit(function(){  
    //エラー表示の初期化
    $("p.error").remove();
    $("div").removeClass("error");
    var text = "";
    
    //1行テキスト入力フォームとテキストエリアの検証
    $(":text,textarea").filter(".validate").each(function(){
        
      //必須項目の検証
      $(this).filter(".required").each(function(){
        if($(this).val()==""){
          show_error("は必須項目です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".max30").each(function(){
        if($(this).val().length > 30){
          show_error("は30文字以内です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".max50").each(function(){
        if($(this).val().length > 50){
          show_error("は50文字以内です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".exact6").each(function(){
        if($(this).val() !=="" && $(this).val().length !== 6){
          show_error("は6文字です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".max500").each(function(){
        if($(this).val().length > 500){
          show_error("は500文字以内です", $(this));
        }
      })
      //メールアドレスの検証
      $(this).filter(".mail").each(function(){
        if($(this).val() && !(/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/gi).test($(this).val())){
          $(this).parent().append("<p class='error'>メールアドレスの形式が異なります</p>")
        }
      }) 
    })
     
    //error クラスの追加の処理
    if($("p.error").size() > 0){
      $("p.error").parent().addClass("error");
      return false;
    }
  }) //ここまでが submit イベントを使った検証
  
  //テキストエリアに入力された文字数を表示
  $("textarea").on('keydown keyup change', function() {
    var count = $(this).val().length;
    $("#count").text(count);
    if(count > 500) {
      $("#count").css({color: 'red', fontWeight: 'bold'});
    }else{
      $("#count").css({color: '#333', fontWeight: 'normal'});
    }
  });
 
});
</script>
</body>
</html>

サンプル2 (jQuery 検証追加)

サンプルでは、実際にはメールは送信されません。

reCAPTCHA を使ってみる

関連ページ:Google reCAPTCHA を使ってみる

Google の キャプチャ「reCAPTCHA」を使ってみます。
reCAPTCHA を利用するには、Google のユーザーアカウントと Web サイトのドメイン(URL)が必要です。Google にログイン後「Getting Started」ページの左側の「Create an API Key」のリンクをクリックして表示されるページで登録することができます。

reCAPTCHA_01

  • Label:キャプチャを識別するための名前です。自分でわかりやすい名前を付けます。
  • Domains:キャプチャを使用するドメインを指定します。
  • Owners:Google アカウントのメールアドレスを指定します。

入力後、「Register」をクリックします。入力に問題がなければ、「Site key」と「Secret key」が表示されます。
reCAPTCHA_02

  • Site Key:HTML などで使用するサイトのキーです。
  • Secret Key:PHP の処理で使用する秘密のキーです。(公開してはいけません)

Secret Key を別ファイルに保存

Secret Key は公開しないので、外部からアクセスできない場所に保存します。この例では「recaptchavars.php」というファイルに定数として保存します。Site Key はソースコードを見ればわかってしまいますが、こちらも一緒に定数として保存しておきます。

recaptchavars.php

<?php
//reCAPTCHA サイトキー
define('RC_SITE_KEY', 'xxxxxxxxxx Site Key xxxxxxxxxxxxxxxxxxxxxxxxxx');
//reCAPTCHA シークレット
define('RC_SECRET', 'xxxxxxxxx Secret Key xxxxxxxxxxxxxxxxxx');   

reCAPTCHA のダウンロード

PHP で検証するために必要な reCAPTCHA のファイルをダウンロードします。
reCAPTCHA PHP client library のページの「Download ZIP」からダウンロードします。Composer を使ってのインストールが推奨されていますが、この例では直接ダウンロードします。

reCAPTCHA_03

解凍したファイルの中の「src」フォルダの中身が必要なファイルになるので、これを適当な場所に保存します。

reCAPTCHA_04

この例では、フォルダ名を src から ReCaptcha に変更して public_html の直下(/home/user/public_html/ReCaptcha/autoload.php)に配置します。

contact1.php

9行目:require_once で「recaptchavars.php」(Site Key の値)を読み込みます。

32行目:その値を変数に格納して Site Key の値をテンプレートに渡します。

contact1.php

<?php
//セッションを開始
session_start();   

//セッションIDを変更(セッションハイジャック対策)
session_regenerate_id(TRUE);  

//reCAPTCHA の情報(Site Key)の読み込み
require_once('/home/user/etc/xxxxxx/ReCaptcha/recaptchavars.php'); 

//テンプレートエンジンの読み込み
require 'libs/functions.php';   

//テンプレートに渡す変数の初期化
$data = array(); //配列を初期化
//初回以外ですでにセッション変数に値が代入されていれば、その値を。そうでなければNULLで初期化
$data['name'] = isset($_SESSION['name']) ? $_SESSION['name'] : NULL; 
$data['email'] = isset($_SESSION['email']) ? $_SESSION['email'] : NULL;
$data['subject'] = isset($_SESSION['subject']) ? $_SESSION['subject'] : NULL;
$data['body'] = isset($_SESSION['body']) ? $_SESSION['body'] : NULL;

//CSRF対策の固定トークンを生成
if(!isset($_SESSION['ticket'])){
  //セッション変数にトークンを代入
  $_SESSION['ticket'] = sha1(uniqid(mt_rand(), TRUE));
}

//トークンをテンプレートに渡す
$data['ticket'] = $_SESSION['ticket'];

//reCAPTCHA(Site Key)の値をテンプレートに渡す
$data['siteKey'] = RC_SITE_KEY;

//テンプレートの表示
display('contact1_view.php', $data);	

contact1_view.php

簡単にウィジェットを表示するには、以下のように head の閉じタグの直前などで reCAPTCHA の Javascript(https://www.google.com/recaptcha/api.js) を読み込み、フォーム要素内で class 属性「g-recaptcha」と data-sitekey 属性「Site Key(サイトキー)」を指定した div 要素で表示します。

<html>
  <head>
    <title>reCAPTCHA demo: Simple page</title>
     <script src="https://www.google.com/recaptcha/api.js" async defer></script>
  </head>
  <body>
    <form action="?" method="POST">
      <div class="g-recaptcha" data-sitekey="発行されたサイトキー"></div>
      <br/>
      <input type="submit" value="Submit">
    </form>
  </body>
</html>

但し、上記の方法だと実用的ではないので、以下のように読み込み時明示的に表示するようにします(Explicitly render the reCAPTCHA widget)。

3行目:フォーム内でウィジェットを表示する div 要素の id 属性(この例では recaptcha)を指定します。

4行目:サイトキーの値を格納した変数 $siteKey を使ってサイトキーを指定します。

5行目:jQuery の検証の際に利用するコールバック関数(verifyRecaptcha)を指定します。

6行目:2分ぐらいすると自動的に無効になり再度チェックを入れるようになっているので、期限が過ぎた場合のコールバック関数(expiredCallback)を指定します。

7行目:テーマの色を指定しています。(指定しない場合は、light :明るい色になります)

10行目~22行目:コールバック関数(verifyRecaptcha)の定義です。このコールバック関数は、ユーザーがウィジェットにチェックをいれて成功した場合に呼び出されます。

コールバック関数はパラメータに g-recaptcha-response の値(response)を受け取るので、それを利用します。この例では、jQuery の検証で response に何らかの値が入っていれば、reCAPTCHA の操作が行われたと想定してクラス属性「verified」を追加しています(g-recaptcha は元から付与されているクラスです)。そして jQuery の検証でエラーの場合に表示(追加)した要素を削除しています。(12~16行目)

また、17~20行目は期限が過ぎた場合にコールバック関数(expiredCallback)でエラーを表示した要素を削除しています。

23~34行目:期限が過ぎた場合のコールバック関数(expiredCallback)の定義です。このコールバック関数は、ユーザーがウィジェットにチェックをいれて何もしないで一定の期間が過ぎると呼び出されます。その時、ウィジェットのチェックは自動的に外れて以下のように表示されます。

reCAPTCHA_08

期限が過ぎてチェックが外れたら「チェックボックスをもう一度オンにしてください。」と表示するようにしています(ウィジェット内にもその旨表示されるので、不要かもしれません)。但し、コールバック関数(verifyRecaptcha)で追加したクラス「verified」を削除しないと、チェックが入っていないのに jQuery の検証を抜けてしまうので、33行目の処理は必要になります。

24行目~32行目は、p 要素を作成し、テキストを指定してクラス属性と id 属性を付与してウィジェットの下に表示するようにしています。

<script type="text/javascript">
  var onloadCallback = function() {
    grecaptcha.render('recaptcha', {
      'sitekey' : "<?php echo $siteKey; ?>",
      'callback' : verifyRecaptcha,
      'expired-callback' : expiredCallback,
      'theme' : 'dark'
    });
  };
  var verifyRecaptcha = function(response) {
    if(response != "") { 
        document.getElementById('recaptcha').className = "g-recaptcha verified"; 
        var recaptchaError = document.getElementById('recaptcha_error');
        if(recaptchaError != null) {
           recaptchaError.parentNode.removeChild(recaptchaError);
        } 
        var recaptchaExpired  = document.getElementById('expired');  
        if(recaptchaExpired != null) {
           recaptchaExpired.parentNode.removeChild(recaptchaExpired);
        }      
    }
  };
  var  expiredCallback = function() {
    var p = document.createElement('p');
    p.textContent = 'チェックボックスをもう一度オンにしてください。';
    p.setAttribute('class', 'error');
    p.setAttribute('id', 'expired');
    var recaptcha_div = document.getElementById('recaptcha');
    var recaptchaExpired  = document.getElementById('expired');
    if(recaptchaExpired == null) {
      recaptcha_div .appendChild(p);
    }   
    document.getElementById('recaptcha').className = "g-recaptcha";
  };
</script>

フォーム内にウィジェットを表示するため、Securimage の代わりに以下を記述します。

<div class="form-group">
  <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo $siteKey; ?>"></div>
  <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
  <noscript>
    <p class="error">JavaSrcipt を有効にしてください。JavaSrcipt が無効の場合、このフォームは機能しません</p>
  </noscript>
</div>

jQuery の検証では、ユーザーが何も操作していない場合はエラーを表示します。

if(!$('#recaptcha').hasClass('verified')) {
  $('#recaptcha').append("<p class='error' id='recaptcha_error'>チェックを入れてください</p>");
}

以下が contact1_view.php の全文です。

contact1_view.php

<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>コンタクト</title>
<link rel="stylesheet" href="style.css">
<script type="text/javascript">
  var onloadCallback = function() {
    grecaptcha.render('recaptcha', {
      'sitekey' : "<?php echo $siteKey; ?>",
      'callback' : verifyRecaptcha,
      'expired-callback' : expiredCallback,
      'theme' : 'dark'
    });
  };
  var verifyRecaptcha = function(response) {
    if(response != "") { 
        document.getElementById('recaptcha').className = "g-recaptcha verified"; 
        var recaptchaError = document.getElementById('recaptcha_error');
        if(recaptchaError != null) {
           recaptchaError.parentNode.removeChild(recaptchaError);
        } 
        var recaptchaExpired  = document.getElementById('expired');  
        if(recaptchaExpired != null) {
           recaptchaExpired.parentNode.removeChild(recaptchaExpired);
        }      
    }
  };
  var  expiredCallback = function() {
    var p = document.createElement('p');
    p.textContent = 'チェックボックスをもう一度オンにしてください。';
    p.setAttribute('class', 'error');
    p.setAttribute('id', 'expired');
    var recaptcha_div = document.getElementById('recaptcha');
    var recaptchaExpired  = document.getElementById('expired');
    if(recaptchaExpired == null) {
      recaptcha_div .appendChild(p);
    }   
    document.getElementById('recaptcha').className = "g-recaptcha";
  };
</script>
</head>
<body>
<div id="contents">

<h2>コンタクト </h2>
<div id="errorDispaly">
<?php if(isset($error)): ?>
<?php foreach($error as $val): ?>
<?php echo $val; ?><br />
<?php endforeach; ?>
<?php endif; ?>
<!--/#errorDispaly--></div>

<div id="formArea">
<fieldset id="contactForm">
<legend class="contact_form"><span class="form_name">お問い合わせフォーム</span></legend>
<form action="contact2.php" method="post" role="form">
<div class="form-group">
  <label for="name">お名前</label>
  <input type="text" name="name" value="<?php echo $name; ?>" size="50" placeholder="お名前(必須)" class="validate required max30 form-control input_name">
</div>
<div class="form-group">  
  <label for="email">e-mail</label>
  <input type="text" name="email" value="<?php echo $email; ?>" size="50" placeholder="メールアドレス(必須)" class="validate required mail form-control input_email">
</div>
<div class="form-group"> 
  <label for="subject">件名</label>
  <input type="text" name="subject" value="<?php echo $subject; ?>" size="50" placeholder="件名(必須)" class="validate required max50 form-control input_subject">
</div>
<div class="form-group">  
  <label for="body">内容</label> <span id="count"> </span>/500
  <textarea name="body" cols="50" rows="8" placeholder="内容(必須:500文字まで)" class="validate required max500 form-control input_body"><?php echo $body; ?></textarea>
</div>
<div class="form-group">
  <!-- reCAPTCHA の表示   -->
  <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo $siteKey; ?>"></div>
  <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
  <noscript>
    <p class="error">JavaSrcipt を有効にしてください。JavaSrcipt が無効の場合、このフォームは機能しません</p>
  </noscript>
</div>  
  <p>
  <input class="btn_submit" type="submit" value="確認画面へ">
  </p>
  <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
  <input type="hidden" name="ticket" value="<?php echo $ticket; ?>">
</form>
</fieldset>
</div><!--end of #formArea-->
</div><!--end of #contents--> 
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">

jQuery(function($){
  
  function show_error(message, this$) {
    text = this$.parent().find('label').text() + message;
    this$.parent().append("<p class='error'>" + text + "</p>")
  }
  
  $("form").submit(function(){  
    //エラー表示の初期化
    $("p.error").remove();
    $("div").removeClass("error");
    var text = "";
    
    //reCAPTCHA の検証
    if(!$('#recaptcha').hasClass('verified')) {
      $('#recaptcha').append("<p class='error' id='recaptcha_error'>チェックを入れてください</p>");
    }
    
    //1行テキスト入力フォームとテキストエリアの検証
    $(":text,textarea").filter(".validate").each(function(){
        
      //必須項目の検証
      $(this).filter(".required").each(function(){
        if($(this).val()==""){
          show_error("は必須項目です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".max30").each(function(){
        if($(this).val().length > 30){
          show_error("は30文字以内です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".max50").each(function(){
        if($(this).val().length > 50){
          show_error("は50文字以内です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".exact6").each(function(){
        if($(this).val() !=="" && $(this).val().length !== 6){
          show_error("は6文字です", $(this));
        }
      })
      //文字数の検証
      $(this).filter(".max500").each(function(){
        if($(this).val().length > 500){
          show_error("は500文字以内です", $(this));
        }
      })
      //メールアドレスの検証
      $(this).filter(".mail").each(function(){
        if($(this).val() && !(/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/gi).test($(this).val())){
          $(this).parent().append("<p class='error'>メールアドレスの形式が異なります</p>")
        }
      }) 
    })
     
    //error クラスの追加の処理
    if($("p.error").size() > 0){
      $("p.error").parent().addClass("error");
      return false;
    }
  }) //ここまでが submit イベントを使った検証
  
  //テキストエリアに入力された文字数を表示
  $("textarea").on('keydown keyup change', function() {
    var count = $(this).val().length;
    $("#count").text(count);
    if(count > 500) {
      $("#count").css({color: 'red', fontWeight: 'bold'});
    }else{
      $("#count").css({color: '#333', fontWeight: 'normal'});
    }
  });
 
});
</script>
</body>
</html>

参考ページ:Displaying the widget

contact2.php / PHP での検証

contact2.php では、ユーザーが操作して送信された値が正しいかの検証をします。

Site Key と Secret Key の情報を読み込み、続いてダウンロードして保存した「autoload.php」を読み込みます。

そして reCAPTCHA のインスタンスを生成します。new のパラメータには発行された Secret Key を指定します。(この例では、recaptchavars.php に定数で定義されています)

//reCAPTCHA の情報(Site Key と Secret Key)の読み込み
require_once('/home/user/etc/xxxx/ReCaptcha/recaptchavars.php'); 

//reCAPTCHA の読み込み
require_once('/home/user/public_html/ReCaptcha/autoload.php'); 
$recaptcha = new \ReCaptcha\ReCaptcha(RC_SECRET);

ユーザーの操作した結果の値を検証するには、以下のようにします。

<?php
$recaptcha = new \ReCaptcha\ReCaptcha($secret);  //インスタンスを生成
$resp = $recaptcha->verify($gRecaptchaResponse, $remoteIp);
if ($resp->isSuccess()) {
    // 認証された場合
} else {
    $errors = $resp->getErrorCodes();
}

この例では、認証されなかった場合にエラーの内容を配列 $error[] に代入します。

ユーザーの操作の結果は $_POST[‘g-recaptcha-response’] に格納されています。$_SERVER[‘REMOTE_ADDR’] はユーザーの IP アドレスで、オプションです。

//画像認証(reCAPTCHA)のチェック
$resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
if (!$resp->isSuccess()) {
  foreach ($resp->getErrorCodes() as $code) {
    $error[] = 'reCAPTCHA エラー : ' . $code ;
  }
}

以下は、contact2.php の全文です。その他のファイル(contact2_view.php, contact3.php, contact2_view.php)には変更はありません。

<?php
session_start();    //セッションを開始

require 'libs/functions.php';   //テンプレートエンジンの読み込み

//reCAPTCHA の情報の読み込み
require_once('/home/user/etc/xxxxx/ReCaptcha/recaptchavars.php'); 

//reCAPTCHA の読み込み
require_once('/home/user/public_html/ReCaptcha/autoload.php'); 
$recaptcha = new \ReCaptcha\ReCaptcha(RC_SECRET);

//POSTされたデータをチェック
$_POST = checkInput($_POST);

//固定トークンを確認(CSRF対策)
if(isset($_POST['ticket'], $_SESSION['ticket'])){
  $ticket = $_POST['ticket'];
  if($ticket !== $_SESSION['ticket']){
    die('不正アクセスの疑いがあります。');
  }
}else{
  die('不正アクセスの疑いがあります。');
}

//POSTされたデータを変数に格納
$name = isset($_POST['name']) ? $_POST['name'] : NULL;
$email = isset($_POST['email']) ? $_POST['email'] : NULL;
$subject = isset($_POST['subject']) ? $_POST['subject'] : NULL;
$body = isset($_POST['body']) ? $_POST['body'] : NULL;
//以下は画像認証用データ(reCAPTCHA)
$recaptcha_response = isset($_POST['g-recaptcha-response']) ? $_POST['g-recaptcha-response'] : NULL;  


//POSTされたデータを整形(前後にあるホワイトスペースを削除)
$name = trim($name);
$email = trim($email);
$subject = trim($subject);
$body = trim($body);
$recaptcha_response = trim($recaptcha_response);

//エラーメッセージを保存する配列の初期化
$error = array();

if($name == '') {
  $error[] = '*お名前は必須項目です。';
  //制御文字でないことと文字数をチェック
}else if(preg_match('/\A[[:^cntrl:]]{1,30}\z/u', $name) == 0) {   
  $error[] = '*お名前は30文字以内でお願いします。';
}

if($email == ''){
  $error[] = '*メールアドレスは必須です。';
}else{   //メールアドレスを正規表現でチェック
  $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD';
  if(!preg_match($pattern, $email)){
    $error[] = '*メールアドレスの形式が正しくありません。';
  }
}

if($subject == '') {
  $error[] = '*件名は必須項目です。';
  //制御文字でないことと文字数をチェック
}else if(preg_match('/\A[[:^cntrl:]]{1,100}\z/u', $subject) == 0) {   
  $error[] = '*件名は100文字以内でお願いします。';
}

if($body == '') {
  $error[] = '*内容は必須項目です。';
  //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
}else if(preg_match('/\A[\r\n\t[:^cntrl:]]{1,500}\z/u', $body) == 0) {   
  $error[] = '*内容は500文字以内でお願いします。';
}

//画像認証(reCAPTCHA)のチェック
$resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
if (!$resp->isSuccess()) {
  foreach ($resp->getErrorCodes() as $code) {
    $error[] = 'reCAPTCHA エラー : ' . $code ;
  }
}

//チェックの結果にエラーがあった場合は、テンプレートの表示に必要な入力されたデータとエラーメッセージを配列「$data」に代入し、display()関数でform1_view.phpを表示
if(count($error) >0){    //エラーがあった場合
  $data = array();
  $data['error'] = $error;
  $data['name'] = $name;
  $data['email'] = $email;
  $data['subject'] = $subject;
  $data['body'] = $body;
  $data['ticket'] = $ticket;
  display('contact1_view.php', $data);
}else{    //エラーがなかった場合
  //POSTされたデータをセッション変数に保存
  $_SESSION['name'] = $name;
  $_SESSION['email'] = $email;
  $_SESSION['subject'] = $subject;
  $_SESSION['body'] = $body;
  
  //確認画面を表示
  $data = array();
  $data['name'] = $name;
  $data['email'] = $email;
  $data['subject'] = $subject;
  $data['body'] = $body;
  $data['ticket'] = $ticket;
  display('contact2_view.php', $data);
}

サンプル3 reCAPTCHA

サンプルでは、実際にはメールは送信されません。