コンタクトフォームで Google が提供するキャプチャ「reCAPTCHA」を使ってみたので、その使い方や設定に関するメモです。
目次
reCAPTCHA の API をサイトに導入するには、reCAPTCHA 公式ページでサイトを登録する必要があります。また、そのためには Google のユーザーアカウントが必要です。
ページの左側の「Create an API Key」のリンクをクリックして表示されるページで登録することができます。
Google のアカウントにログイン後、以下のページで登録します。
入力後、「Register」をクリックします。入力に問題がなければ、「Site key」と「Secret key」が表示されます。
サンプルのコンタクトフォームは「入力ページ → 確認ページ → 完了ページ」の順で遷移し、以下の5つのファイルから成ります。
以下はサンプルですが、実際にメールは送信されません。
サンプル(contact1.php)
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');
PHP で検証するために必要な reCAPTCHA のファイルをダウンロードします。
reCAPTCHA PHP client library のページの「Download ZIP」からダウンロードします。Composer を使ってのインストールが推奨されていますが、この例では直接ダウンロードします。
解凍したファイルの中の「src」フォルダの中身が必要なファイルになるので、これを適当な場所に保存します。
この例では、フォルダ名を src から ReCaptcha に変更して public_html の直下(/home/user/public_html/ReCaptcha/autoload.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>
但し、上記の方法だと使い勝手が良くないので、以下のように head 内でページの読み込み(onload)時に表示するコールバック関数(onloadCallback)を指定し、HTML 部分で div 要素と script 要素を使って表示するようにします。
このサンプルでは jQuery の検証を行うため以下の3つのコールバック関数を設定しています。
<head> ・・・中略・・・ <script type="text/javascript"> var onloadCallback = function() { //読み込み時のコールバック関数の指定 (1) grecaptcha.render('recaptcha', { //div 要素の id 属性 'sitekey' : "<?php echo RC_SITE_KEY; ?>", //サイトキー 'callback' : verifyRecaptcha, //コールバック関数の指定 (2) 'expired-callback' : expiredCallback //コールバック関数の指定 (3) }); }; var verifyRecaptcha = function(response) { // (2) if(response != "") { //reCAPTCHA でユーザーがウィジェットにチェックをいれて成功した場合の処理 } }; var expiredCallback = function() { // (3) //チェックをいれて成功したが期限が過ぎた場合の処理 }; </script> </head>
4行目:ウィジェットを表示する onload コールバック関数(onloadCallback)を指定します。この関数の中の grecaptcha.render() の最初のパラメータではウィジェットを表示する div 要素の id 属性を指定します。
また以下のようなオプションを指定することができます。「sitekey」のみ必須です。
5行目:フォーム内でウィジェットを表示する div 要素の id 属性を指定します。
6行目:サイトキーの値を格納した定数 RC_SITE_KEY を使ってサイトキーを指定しています。
7行目:jQuery の検証の際に利用するコールバック関数(verifyRecaptcha)を指定します。
8行目:2分ぐらいすると自動的に無効になり再度チェックを入れるようになっているので、期限が過ぎた場合に呼び出されるコールバック関数(expiredCallback)を指定します。
11行目~15行目:コールバック関数(verifyRecaptcha)の定義です。
16行目~18行目:コールバック関数(expiredCallback)の定義です。
以下がこのサンプルのウィジェットの表示をする関数部分の記述です。
<script type="text/javascript"> var onloadCallback = function() { grecaptcha.render('recaptcha', { 'sitekey' : "<?php echo RC_SITE_KEY; ?>", '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>
10行目~22行目:コールバック関数(verifyRecaptcha)の定義です。このコールバック関数は、ユーザーがウィジェットにチェックをいれて成功した場合に呼び出されます。
コールバック関数はパラメータに g-recaptcha-response の値(response)を受け取るので、それを利用します。この例では、jQuery の検証で response に何らかの値が入っていれば、reCAPTCHA の操作が行われたと想定して、reCAPTCHA を表示する div 要素(id 属性が recaptcha)にクラス属性「verified」を追加しています(g-recaptcha は予め付与されているクラスです)。そして jQuery の検証でエラーの場合に表示(追加)した要素を削除しています。(13~16行目)
また、17~20行目は期限が過ぎた場合にコールバック関数(expiredCallback)でエラーを表示した要素を削除しています。
23~34行目:期限が過ぎた場合のコールバック関数(expiredCallback)の定義です。このコールバック関数は、ユーザーがウィジェットにチェックをいれて何もしないで一定の期間が過ぎると呼び出されます。その時、ウィジェットのチェックは自動的に外れて以下のように表示されます。
期限が過ぎてチェックが外れたら「チェックボックスをもう一度オンにしてください。」と表示するようにしています(ウィジェット内にもその旨表示されるので、不要かもしれません)。但し、コールバック関数(verifyRecaptcha)で追加したクラス「verified」を削除しないと、チェックが入っていないのに jQuery の検証を抜けてしまうので、33行目の処理は必要になります。
24行目~32行目は、p 要素を作成し、テキストを指定してクラス属性と id 属性を付与してウィジェットの下に表示するようにしています。
フォーム内にウィジェットを表示するため、form 要素内にウィジェットを表示する div 要素を記述し以下の属性を指定します。
div 要素の data-sitekey 属性には発行されたサイトキーの値を指定します。このサンプルでは PHP の echo 文で出力しています。
<div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"> </div>
そして表示するための(リソースとしての)以下の JavaScript を適当な位置(body の閉じタグの直前等)に記述します。
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
onload パラメータには、head 内に記述した onload コールバック関数(この例では onloadCallback)を指定し、render パラメータには explicit を指定します。(onload=onloadCallback&render=explicit)
また、onload コールバック関数は reCAPTCHA API がロードする前に定義されている必要があります(このサンプルでは head 内で先に定義しています)。そして、上記のようにスクリプトタグ内で、async と defer パラメータを指定します。
<form action="contact2.php" method="post"> ・・・中略・・・ <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"> </div> ・・・中略・・・ </form> <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script> </body>
jQuery の検証では、ユーザーが何も操作していない場合(id 属性が recaptcha の div 要素に「verified」クラスが付与されていない場合)はエラーを表示します。
jQuery(function($){ ・・・中略・・・ if(!$('#recaptcha').hasClass('verified')) { $('#recaptcha').append("<p class='error' id='recaptcha_error'>チェックを入れてください</p>"); } ・・・中略・・・ });
これらの設定をしたページを読み込むと以下のような通信が発生しているのが確認できます。
また、ユーザーがウィジェットにチェックを入れると以下のように POST リクエストが発生しているのがわかります。
以下は前述の内容(仕組み)をまとめたサンプルで、reCAPTCHA にチェックを入れないと送信できないようになっています。
<?php //reCAPTCHA の情報(Site Key)の読み込み require_once('/home/user/etc/xxxxxx/ReCaptcha/recaptchavars.php'); //エスケープ処理を行う関数 function h($var) { return htmlspecialchars($var, ENT_QUOTES, 'UTF-8'); } ?> <!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 RC_SITE_KEY; ?>", 'callback' : verifyRecaptcha, 'expired-callback' : expiredCallback }); }; 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> reCAPTCHA サンプル </h2> <form action="" method="post"> <!-- reCAPTCHA --> <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div> <!-- reCAPTCHA --> <p><input class="btn_submit" type="submit" value="送信"></p> </form> </div> <!-- reCAPTCHA --> <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script> <!-- reCAPTCHA --> <script src="http://code.jquery.com/jquery-1.11.3.min.js"></script> <script type="text/javascript"> jQuery(function($){ $("form").submit(function(){ //submit イベント //エラー表示の初期化 $("p.error").remove(); //reCAPTCHA の検証 if(!$('#recaptcha').hasClass('verified')) { $('#recaptcha').append("<p class='error' id='recaptcha_error'>チェックを入れてください</p>"); } //エラーがあれば送信しない if($("p.error").size() > 0){ return false; } }) }); </script> </body> </html>
以下が contact1.php の全文です。
contact1.php
<?php //セッションを開始 session_start(); //セッションIDを変更(セッションハイジャック対策) session_regenerate_id(TRUE); //reCAPTCHA の情報(Site Key)の読み込み require_once('/home/user/etc/xxxx/ReCaptcha/recaptchavars.php'); //データの検証とエスケープ処理の関数の読み込み require 'libs/functions.php'; //初回以外ですでにセッション変数に値が代入されていれば、その値を。そうでなければNULLで初期化 $name = isset($_SESSION['name']) ? $_SESSION['name'] : NULL; $email = isset($_SESSION['email']) ? $_SESSION['email'] : NULL; $subject= isset($_SESSION['subject']) ? $_SESSION['subject'] : NULL; $body = isset($_SESSION['body']) ? $_SESSION['body'] : NULL; $error = isset($_SESSION['error']) ? $_SESSION['error'] : NULL; //CSRF対策の固定トークンを生成 if(!isset($_SESSION['ticket'])){ //セッション変数にトークンを代入 $_SESSION['ticket'] = sha1(uniqid(mt_rand(), TRUE)); } //トークンを変数に代入 $ticket = $_SESSION['ticket']; ?> <!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"> <!-- reCAPTCHA --> <script type="text/javascript"> var onloadCallback = function() { grecaptcha.render('recaptcha', { 'sitekey' : "<?php echo RC_SITE_KEY; ?>", '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> <!-- reCAPTCHA --> </head> <body> <div id="contents"> <h2>コンタクト </h2> <div id="errorDispaly"> <?php if(isset($error)): ?> <?php foreach($error as $val): ?> <?php echo h($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 h($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 h($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 h($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 h($body); ?></textarea> </div> <div class="form-group"><!-- reCAPTCHA --> <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div> <noscript><!-- JavaScript が無効な場合にエラーを表示 --> <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 h($ticket); ?>"> </form> </fieldset> </div><!--end of #formArea--> </div><!--end of #contents--> <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script> <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>") } //submit イベントを利用して値を検証 $("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"); //親要素にクラスを追加 //最初の error クラスがついた p 要素へスクロール $('html,body').animate({ scrollTop: $("p.error:first").offset().top-40 }, 'slow'); 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>
以下は、jQuery の検証用(エラーの表示用)の CSS です。
p.error { color: red; } .error input , .error textarea { background-color: #F9E9E9; }
参考ページ:Displaying the widget
このサンプルでは、jQuery の検証を付けているので少し複雑になっていますが、jQuery の検証が不要な場合は以下のようにウィジェットを表示する関数を指定するだけでとてもシンプルになります。
但し、入力内容に誤りがあった場合には再度ウィジェットにチェックを入れなければならない等使い勝手が落ちてしまいます。
<?php //セッションを開始 session_start(); //セッションIDを変更(セッションハイジャック対策) session_regenerate_id(TRUE); //reCAPTCHA の情報(Site Key)の読み込み require_once('/home/user/etc/xxxx/ReCaptcha/recaptchavars.php'); //データの検証とエスケープ処理の関数の読み込み require 'libs/functions.php'; //初回以外ですでにセッション変数に値が代入されていれば、その値を。そうでなければNULLで初期化 $name = isset($_SESSION['name']) ? $_SESSION['name'] : NULL; $email = isset($_SESSION['email']) ? $_SESSION['email'] : NULL; $subject= isset($_SESSION['subject']) ? $_SESSION['subject'] : NULL; $body = isset($_SESSION['body']) ? $_SESSION['body'] : NULL; $error = isset($_SESSION['error']) ? $_SESSION['error'] : NULL; //CSRF対策の固定トークンを生成 if(!isset($_SESSION['ticket'])){ //セッション変数にトークンを代入 $_SESSION['ticket'] = sha1(uniqid(mt_rand(), TRUE)); } //トークンを変数に代入 $ticket = $_SESSION['ticket']; ?> <!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"> <!-- reCAPTCHA --> <script type="text/javascript"> var onloadCallback = function() { grecaptcha.render('recaptcha', { 'sitekey' : "<?php echo RC_SITE_KEY; ?>", 'theme' : 'dark' }); }; </script> <!-- reCAPTCHA --> </head> <body> <div id="contents"> <h2>コンタクト </h2> <div id="errorDispaly"> <?php if(isset($error)): ?> <?php foreach($error as $val): ?> <?php echo h($val); ?><br /> <?php endforeach; ?> <?php endif; ?> </div> <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 h($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 h($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 h($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 h($body); ?></textarea> </div> <div class="form-group"> <!-- reCAPTCHA --> <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div> <noscript> <p class="error">JavaSrcipt を有効にしてください。JavaSrcipt が無効の場合、このフォームは機能しません。</p> </noscript> <!-- reCAPTCHA --> </div> <p> <input class="btn_submit" type="submit" value="確認画面へ"> </p> <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」--> <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>"> </form> </fieldset> </div> </div> <!-- reCAPTCHA --> <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script> <!-- reCAPTCHA --> <script src="http://code.jquery.com/jquery-1.11.3.min.js"></script> </body> </html>
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(シークレットキー); //インスタンスを生成 $resp = $recaptcha->verify(reCAPTCHA のレスポンス, リモートIPアドレス); 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 ; } }
通常ユーザーがウィジェットにチェックを入れて、jQuery の検証を通過していればこのエラーが表示されることはありませんが、もし jQuery の検証をしない場合は、エラーメッセージをもう少し工夫したほうが良いと思います。
ユーザーが全ての項目を入力し、ウィジェットにチェックを入れて「確認画面へ」のボタンをクリックすると contact2.php へデータが POST されます。
参考ページ:reCAPTCHA PHP client library
上記参考ページは reCAPTCHA の「Introduction」ページの「Code Examples」の下の「PHP」からアクセスできます。
以下は、contact2.php の全文です。
<?php session_start(); //セッションを開始 require 'libs/functions.php'; //データの検証とエスケープ処理の関数の読み込み //reCAPTCHA の情報の読み込み 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); //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_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 ; } } //POSTされたデータをセッション変数に保存 $_SESSION['name'] = $name; $_SESSION['email'] = $email; $_SESSION['subject'] = $subject; $_SESSION['body'] = $body; $_SESSION['error'] = $error; //チェックの結果にエラーがあった場合は入力フォームに戻します if(count($error) >0){ //エラーがあった場合 $dirname = dirname($_SERVER['SCRIPT_NAME']); $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname; $url = 'http://' . $_SERVER['SERVER_NAME']. $dirname . '/contact1.php'; header('HTTP/1.1 303 See Other'); header('location: ' . $url); }else{ //エラーがなかった場合 } ?> <!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 h($name); ?></dd> <dt>e-mail:</dt> <dd><?php echo h($email); ?></dd> <dt>件名:</dt> <dd><?php echo h($subject); ?></dd> <dt>内容:</dt> <dd><?php echo nl2br(h($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 h($ticket); ?>"> <p><input type="submit" value="送信する" class="btn_submit"></p> </form> </fieldset> </div><!--end of #contents--> <script src="http://code.jquery.com/jquery-1.11.3.min.js"></script> </body> </html>
その他のページでは、reCAPTCHA に関する設定はありませんが参考までに。
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']); //--------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); }*/ $result = true; //サンプルなのでメールの送信はしない。(上記コメントアウト) //送信結果により表示するメッセージの変数を初期化 $message = ''; //メール送信の結果判定 if($result) { $message = 'ありがとうございます。送信完了いたしました。<br>(実際には送信されていません)'; //成功した場合はセッションを破棄 $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去 session_destroy(); //セッションを破棄 }else{ $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 '<h3><a href="contact1.php">送信失敗</a></h3>'; } ?></p> <p><?php echo h($message); ?></p> </div><!--end of #message--> <?php if(!$result): ?> <p>送信失敗が継続する場合は、一度ブラウザを閉じてからやり直すとうまくいくことがあります。</p> <?php endif; ?> </div><!--end of #contents--> </body> </html>
functions.php
<?php //エスケープ処理を行う関数 function h($var) { if(is_array($var)){ //$varが配列の場合、h()関数をそれぞれの要素について呼び出す(再帰) return array_map('h', $var); }else{ return htmlspecialchars($var, ENT_QUOTES, 'UTF-8'); } } //入力値に不正なデータがないかなどをチェックする関数 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; } }
JavaScript を OFF にしている場合は、ウィジェットは表示されません。またこのサンプルの場合、PHP でユーザー操作のレスポンスを検証しているので、フォームを送信することができません。
そのため、JavaScript を OFF にしている場合には、noscript 要素で JavaScript を有効にする必要があることを伝える等するか、iframe を利用した noscript 要素を記述する必要があります。
reCAPTCHA の「Frequently asked questions」の「Does reCAPTCHA support users that don’t have JavaScript enabled?」に以下の noscript 要素が紹介されています。(ウィジェットを表示する要素のすぐ後に記述します)
<script src="https://www.google.com/recaptcha/api.js" async defer></script> <div class="g-recaptcha" data-sitekey="your_site_key"></div> <noscript> <div style="width: 302px; height: 422px;"> <div style="width: 302px; height: 422px; position: relative;"> <div style="width: 302px; height: 422px; position: absolute;"> <iframe src="https://www.google.com/recaptcha/api/fallback?k=your_site_key" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"> </iframe> </div> <div style="width: 300px; height: 60px; border-style: none; bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px; background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;"> <textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1; margin: 10px 25px; padding: 0px; resize: none;" > </textarea> </div> </div> </div> </noscript>
このサンプルで上記コードを記述した場合、ユーザーが認証用コードを入力するテキストエリア部分がうまく表示されなかったので、ウィジェットを表示する div 要素のスタイルの「position: absolute;」を削除して、外側の div 要素の高さをテキストエリアの高さ分(60px)増やして「 482px」にする必要がありました。
以下が、iframe を利用した noscript 要素を追加した場合のフォーム要素の例です。iframe の src 属性の最後にサイトキーを指定する必要があります。
<form action="contact2.php" method="post"> <div class="form-group"> <label for="name">お名前</label> <input type="text" name="name" value="<?php echo h($name); ?>" size="50" placeholder="お名前(必須)" class="validate required max30 form-control input_name"> </div> ・・・中略・・・ <div class="form-group"> <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div> <noscript><!--ここから--> <div style="width: 302px; height: 482px;"> <div style="width: 302px; height: 422px; position: relative;"> <div style="width: 302px; height: 422px;"> <iframe src="https://www.google.com/recaptcha/api/fallback?k=<?php echo RC_SITE_KEY; ?>" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"> </iframe> </div> <div style="width: 300px; height: 60px; border-style: none; bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px; background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;"> <textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1; margin: 10px 25px; padding: 0px; resize: none;" > </textarea> </div> </div> </div> </noscript><!--ここまで--> </div> <p> <input class="btn_submit" type="submit" value="確認画面へ"> </p> <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」--> <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>"> </form>
但し、 reCAPTCHA の「Frequently asked questions」には「If JavaScript is a requirement for your site, we advise that you do NOT include this.」とあり、サイトで JavaScript を必要とする場合は、このコードを記述しないことを推奨するとなっています。(使い勝手が悪いためのようです)
このサンプルでは単に noscript 要素で JavaScript を有効にする必要があることを表示するようにしています。
「Browser requirements for reCAPTCHA」に記載されています。
以下のブラウザの最新の2バージョンをサポートするとなっています。
desktop (Windows, Linux, Mac)
mobile
また、IE8~IE10 の場合、互換表示設定に google.com が含まれているとうまく表示されないことがあるようです。(reCAPTCHA isn’t displaying properly on Internet Explorer, what do I do?)
参考:「reCAPTCHA Help」
確認画面から入力画面に戻った場合(一度 reCAPTCHA の認証が行われた場合)に reCAPTCHA を表示させない場合の例。(これで問題がないのかは不明。一応機能するようですが、もっと別の方法があるかも知れません。)
「contact1.php」の変更点
// reCAPTCHA の認証が行われたかどうかを追加 $recaptcha_verified = isset($_SESSION['recaptcha_verified']) ? $_SESSION['recaptcha_verified'] : NULL; // reCAPTCHA の値「Site key」を変数に代入 $siteKey = RC_SITE_KEY; //Added
head 内の reCAPTCHA の JavaScript 部分を PHP で条件により出力
<?php if (!$recaptcha_verified) echo <<<EOT <script type="text/javascript"> var onloadCallback = function() { grecaptcha.render('recaptcha', { 'sitekey' : "{$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> EOT ?>
reCAPTCHA の HTML を PHP で条件により出力
<?php if (!$recaptcha_verified) echo <<<EOT <div class="form-group"> <div id="recaptcha" class="g-recaptcha" data-sitekey="<?php echo RC_SITE_KEY; ?>"></div> </div> EOT ?>
「contact2.php」の変更点
// reCAPTCHA の認証が行われたかどうかを追加 $recaptcha_verified = isset($_SESSION['recaptcha_verified']) ? $_SESSION['recaptcha_verified'] : NULL; //画像認証(reCAPTCHA)のチェック *一度認証したらチェックしないように変更。 if(!$recaptcha_verified) { $resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']); if (!$resp->isSuccess()) { foreach ($resp->getErrorCodes() as $code) { $error[] = 'reCAPTCHA エラー : ' . $code ; } } else { $recaptcha_verified = true; } } else { $recaptcha_verified = true; } //認証されているかどうかをセッション変数に保存 $_SESSION['recaptcha_verified'] = $recaptcha_verified;
関連ページ:「コンタクトフォームの作成」