#author("2020-04-25T08:26:25+09:00","","")
#author("2020-05-31T18:09:41+09:00","","")
** recaptcha3.inc.php [#g81d06b9]
|RIGHT:100|LEFT:360|c
|~サマリ|Google reCAPTCHA v3 によるスパム対策|
|~リビジョン|1.0|
|~リビジョン|1.1|
|~対応バージョン|1.5.3|
|~投稿者|[[M.Taniguchi]]|
|~投稿日|&new{2020-04-25 (土) 02:21:45};|

**概要 [#e3a7bff8]
Google reCAPTCHA v3 によるスパム対策プラグイン。

ページ編集・コメント投稿・ファイル添付など、PukiWiki標準の編集機能をスパムから守ります。~
reCAPTCHA v3 は不審な送信者を学習により自動判定する不可視の防壁です。煩わしい文字入力をユーザーに要求せず、ウィキのユーザビリティーに影響しません。

追加ファイルはこのプラグインコードだけ。PukiWiki本体の変更も最小限にし、なるべく簡単に導入できるようにしています。~
が、そのための副作用として、JavaScriptを活用する高度な編集系サードパーティ製プラグインとは相性が悪いかもしれません。~
PukiWikiをほぼ素のままで運用し、手軽にスパム対策したいかた向けです。

バージョン1.1より、禁止語句によるスパム判定を追加しました。reCAPTCHAを使わず、禁止語句判定のみ用いることも可能です。


PukiWiki 1.5.3/PHP 7.4/UTF-8/主要モダンブラウザーで動作確認済み。旧バージョンでも動くかもしれませんが非推奨です。

**導入手順 [#h76b3c6e]

以下の手順に沿ってPukiWikiに導入してください。

+本プラグイン recaptcha3.inc.php  を plugin ディレクトリに設置する。
+Google reCAPTCHA サイト(https: //www.google.com/recaptcha/)でウィキのドメインを「reCAPTCHA v3」タイプで登録し、取得したサイトキー・シークレットキーを本プラグイン内の定数 PLUGIN_RECAPTCHA3_SITE_KEY, PLUGIN_RECAPTCHA3_SECRET_KEY に設定する。
+スキンファイル skin/pukiwiki.skin.php のほぼ末尾、「</body>」(275行目あたり)の直前に次のコードを挿入する。
 <?php if (exist_plugin_convert('recaptcha3')) echo do_plugin_convert('recaptcha3'); // reCAPTCHA v3 plugin ?>
+ライブラリファイル lib/plugin.php の「function do_plugin_action($name)」関数内、「$retvar = call_user_func('plugin_' . $name . '_action');」行の直前(92行目あたり)に次のコードを挿入する。
 if (exist_plugin_action('recaptcha3') && !call_user_func_array('plugin_recaptcha3_action', array($name))['body']) die_message('Rejected by Google reCAPTCHA v3'); // reCAPTCHA v3 plugin
+ライブラリファイル lib/plugin.php の「function do_plugin_action($name)」関数内、「$retvar = call_user_func('plugin_' . $name . '_action');」行の直前(92行目あたり)に次のコードを挿入する(''※バージョン1.1で変更'')。
 if (exist_plugin_action('recaptcha3') && ($__v = call_user_func_array('plugin_recaptcha3_action', array($name))['body'])) die_message($__v); // reCAPTCHA v3 plugin

**コード [#h037a1e3]

recaptcha3.inc.php~
(下記のコードをコピーして、plugin ディレクトリに recaptcha3.inc.php というファイル名で保存してください)

 <?php
 /**
 PukiWiki - Yet another WikiWikiWeb clone.
 recaptcha3.inc.php, v1.0 2020 M.Taniguchi
 recaptcha3.inc.php, v1.1 2020 M.Taniguchi
 License: GPL v3 or (at your option) any later version
 
 Google reCAPTCHA v3 によるスパム対策プラグイン。
 
 ページ編集・コメント投稿・ファイル添付など、PukiWiki標準の編集機能をスパムから守ります。
 reCAPTCHA v3 は不審な送信者を学習により自動判定する不可視の防壁です。煩わしい文字入力をユーザーに要求せず、ウィキのユーザビリティーに影響しません。
 reCAPTCHA v3 は不審な送信者を学習により自動判定する不可視の防壁です。煩わしい文字入力をユーザーに要求せず、ウィキの使用感に影響しません。
 
 追加ファイルはこのプラグインだけ。PukiWiki本体の変更も最小限にし、なるべく簡単に導入できるようにしています。
 が、そのための副作用として、JavaScriptを活用する高度な編集系サードパーティ製プラグインとは相性が悪いかもしれません。
 PukiWikiをほぼ素のままで運用し、手軽にスパム対策したいかた向けです。
 
 バージョン1.10より、禁止語句によるスパム判定を追加しました。reCAPTCHAを使わず、禁止語句判定のみ用いることも可能です。
 
 【導入手順】
 以下の手順に沿ってPukiWikiに導入してください。
 以下の手順に沿ってシステムに導入してください。
 
 1) Google reCAPTCHA サイトでウィキのドメインを「reCAPTCHA v3」タイプで登録し、取得したサイトキー・シークレットキーをこのプラグインの定数 PLUGIN_RECAPTCHA3_SITE_KEY, PLUGIN_RECAPTCHA3_SECRET_KEY に設定する。
 
 2) スキンファイル skin/pukiwiki.skin.php のほぼ末尾、「</body>」(275行目あたり)の直前に次のコードを挿入する。
 2) ファイル skin/pukiwiki.skin.php のほぼ末尾、「</body>」(275行目あたり)の直前に次のコードを挿入する。
    <?php if (exist_plugin_convert('recaptcha3')) echo do_plugin_convert('recaptcha3'); // reCAPTCHA v3 plugin ?>
 
 3) ライブラリファイル lib/plugin.php の「function do_plugin_action($name)」関数内、「$retvar = call_user_func('plugin_' . $name . '_action');」行の直前(92行目あたり)に次のコードを挿入する。
    if (exist_plugin_action('recaptcha3') && !call_user_func_array('plugin_recaptcha3_action', array($name))['body']) die_message('Rejected by Google reCAPTCHA v3'); // reCAPTCHA v3 plugin
 3) ファイル lib/plugin.php の「function do_plugin_action($name)」関数内、「$retvar = call_user_func('plugin_' . $name . '_action');」の直前(92行目あたり)に次のコードを挿入する。
    if (exist_plugin_action('recaptcha3') && ($__v = call_user_func_array('plugin_recaptcha3_action', array($name))['body'])) die_message($__v); // reCAPTCHA v3 plugin
 
 【ご注意】
 ・PukiWiki 1.5.3/PHP 7.4/UTF-8/主要モダンブラウザーで動作確認済み。旧バージョンでも動くかもしれませんが非推奨です。
 ・標準プラグイン以外の動作確認はしていません。サードパーティ製プラグインによっては機能が妨げられる場合があります。
 ・JavaScriptが有効でないと動作しません。
 ・サーバーからreCAPTCHA APIへのアクセスにcURLを使用します。
 ・reCAPTCHA v3 について詳しくはGoogleのreCAPTCHAサイトをご覧ください。https: //www.google.com/recaptcha/
 */
 
 define('PLUGIN_RECAPTCHA3_SITE_KEY',   '取得したサイトキーをここに書く');	// reCAPTCHA v3 サイトキー
 define('PLUGIN_RECAPTCHA3_SECRET_KEY', '取得したシークレットキーをここに書く');	// reCAPTCHA v3 シークレットキー
 /////////////////////////////////////////////////
 // スパム対策プラグイン設定(recaptcha3.inc.php)
 if (!defined('PLUGIN_RECAPTCHA3_SITE_KEY'))      define('PLUGIN_RECAPTCHA3_SITE_KEY',       '');   // Google reCAPTCHA v3 サイトキー。空の場合、reCAPTCHA判定は実施されない
 if (!defined('PLUGIN_RECAPTCHA3_SECRET_KEY'))    define('PLUGIN_RECAPTCHA3_SECRET_KEY',     '');   // Google reCAPTCHA v3 シークレットキー。空の場合、reCAPTCHA判定は実施されない
 if (!defined('PLUGIN_RECAPTCHA3_SITE_KEY'))      define('PLUGIN_RECAPTCHA3_SCORE_THRESHOLD', 0.5); // スコア閾値(0.0~1.0)。reCAPTCHAによる判定スコアがこの値より低い送信者はスパマーとみなして要求を拒否する。なお、直接プラグインURLを叩く種類のロボットはスコアによらず必ず拒否される
 if (!defined('PLUGIN_RECAPTCHA3_HIDE_BADGE'))    define('PLUGIN_RECAPTCHA3_HIDE_BADGE',      1);   // reCAPTCHAバッジを非表示にし、代替文言を出力する。Googleの規約によりバッジか文言どちらかの表示が必須
 if (!defined('PLUGIN_RECAPTCHA3_API_TIMEOUT'))   define('PLUGIN_RECAPTCHA3_API_TIMEOUT',     0);   // reCAPTCHA APIタイムアウト時間(秒)。0なら無指定
 if (!defined('PLUGIN_RECAPTCHA3_CENSORSHIP'))    define('PLUGIN_RECAPTCHA3_CENSORSHIP',     '');   // 投稿禁止語句を表す正規表現(例:'/((https?|ftp)\:\/\/[\w!?\/\+\-_~=;\.,*&@#$%\(\)\'\[\]]+|宣伝文句)/ui')
 if (!defined('PLUGIN_RECAPTCHA3_CHECK_REFERER')) define('PLUGIN_RECAPTCHA3_CHECK_REFERER',   0);   // 1ならリファラーを参照し自サイト以外からの要求を拒否。リファラーは未送や偽装があり得るため頼るべきではないが、一時的な防御には使える局面があるかもしれない
 if (!defined('PLUGIN_RECAPTCHA3_ERR_STATUS'))    define('PLUGIN_RECAPTCHA3_ERR_STATUS',      403); // 拒否時に返すHTTPステータスコード
 if (!defined('PLUGIN_RECAPTCHA3_DISABLED'))      define('PLUGIN_RECAPTCHA3_DISABLED',        0);   // 1なら本プラグインを無効化。メンテナンス用
 
 define('PLUGIN_RECAPTCHA3_SCORE_THRESHOLD', 0.5);	// スコア閾値(0.0~1.0)。reCAPTCHAによる判定スコアがこの値より低い送信者はスパマーとみなして要求を拒否する。なお、直接プラグインURLを叩く種類のロボットはスコアによらず必ず拒否される
 
 define('PLUGIN_RECAPTCHA3_DISABLED', 0);	// 本プラグイン機能を無効にする。メンテナンス用
 define('PLUGIN_RECAPTCHA3_HIDE_BADGE', 1);	// reCAPTCHAバッジを非表示にし、代替文言を出力する。Googleの規約によりバッジか文言どちらかの表示が必須
 define('PLUGIN_RECAPTCHA3_API_TIMEOUT', 0);	// reCAPTCHA APIタイムアウト時間(秒)。0なら無指定
 
 
 // プラグイン出力
 function plugin_recaptcha3_convert() {
 	// 本プラグインまたはJavaScriptが無効なら何もしない
 	if (PLUGIN_RECAPTCHA3_DISABLED || PKWK_READONLY || !PKWK_ALLOW_JAVASCRIPT) return '';
 	// 本プラグインが無効か書き込み禁止なら何もしない
 	if (PLUGIN_RECAPTCHA3_DISABLED || ((!PLUGIN_RECAPTCHA3_SITE_KEY || !PLUGIN_RECAPTCHA3_SECRET_KEY) && !PLUGIN_RECAPTCHA3_CENSORSHIP) || PKWK_READONLY || !PKWK_ALLOW_JAVASCRIPT) return '';
 
 	// 二重起動禁止
 	static	$included = false;
 	if ($included) return '';
 	$included = true;
 
 	$enabled = (PLUGIN_RECAPTCHA3_SITE_KEY && PLUGIN_RECAPTCHA3_SECRET_KEY);	// reCAPTCHA有効フラグ
 
 	// reCAPTCHAバッジ非表示なら代替文言設定
 	$protocol = 'https:';
 	$badge = (!PLUGIN_RECAPTCHA3_HIDE_BADGE)? '' : '<style>.grecaptcha-badge{visibility:hidden} #_p_recaptcha3_terms{font-size:70%}</style><div id="_p_recaptcha3_terms">This site is protected by reCAPTCHA and the Google <a href="' . $protocol . '//policies.google.com/privacy" rel="noopener nofollow external">Privacy Policy</a> and <a href="' . $protocol . '//policies.google.com/terms" rel="noopener nofollow external">Terms of Service</a> apply.</div>';
 	$badge = (!PLUGIN_RECAPTCHA3_HIDE_BADGE || !$enabled)? '' : '<style>.grecaptcha-badge{visibility:hidden;max-height:0;max-width:0;margin:0;padding:0;border:none} #_p_recaptcha3_terms{font-size:7px}</style><div id="_p_recaptcha3_terms">This site is protected by reCAPTCHA and the Google <a href="' . $protocol . '//policies.google.com/privacy" rel="noopener nofollow external">Privacy Policy</a> and <a href="' . $protocol . '//policies.google.com/terms" rel="noopener nofollow external">Terms of Service</a> apply.</div>';
 
 	// JavaScript
 	$siteKey = PLUGIN_RECAPTCHA3_SITE_KEY;
 	$apiUrl = $protocol . '//www.google.com/recaptcha/api.js?render=';
 	$libUrl = '//www.google.com/recaptcha/api.js?render=' . $siteKey;
 	$enabled = ($enabled)? 'true' : 'false';
 	$js = <<<EOT
 <script src="${apiUrl}${siteKey}" defer></script>
 <script>
 window.addEventListener('load', function(){
 'use strict';
 
 window.addEventListener('DOMContentLoaded', function(){
 	new __PluginRecaptcha3__();
 });
 
 __PluginRecaptcha3__ = function() {
 var	__PluginRecaptcha3__ = function() {
 	const	self = this;
 	this.timer = null;
 	this.libLoaded = false;
 
 	// 設定
 	this.update();
 
 	// DOMを監視し、もしページ内容が動的に変更されたら再設定する(モダンブラウザーのみ対応)
 	const observer = new MutationObserver(function(mutations){ mutations.forEach(function(mutation){ if (mutation.type == 'childList') self.update(); }); });
 	if (observer) {
 		const target = document.getElementsByTagName('body')[0];
 		if (target) observer.observe(target, { childList: true, subtree: true });
 	}
 };
 
 // reCAPTCHAライブラリーロード
 __PluginRecaptcha3__.prototype.loadLib = function() {
 	if (!this.libLoaded) {
 		this.libLoaded = true;
 		var scriptElement = document.createElement('script');
 		scriptElement.src = '${libUrl}';
 		scriptElement.setAttribute('defer', 'defer');
 		document.body.appendChild(scriptElement);
 	}
 };
 
 // 設定
 __PluginRecaptcha3__.prototype.setup = function() {
 	const	self = this;
 
 	// 全form要素を走査
 	var	elements = document.getElementsByTagName('form');
 	for (var i = elements.length - 1; i >= 0; --i) {
 		var	form = elements[i];
 
 		// こちらのタイミングで送信するため、既定の送信イベントを止めておく
 		form.addEventListener('submit', self.stopSubmit, false);
 
 		// form内全submitボタンを走査しクリックイベントを設定
 		var eles = form.querySelectorAll('input[type="submit"]');
 		for (var j = eles.length - 1; j >= 0; --j) eles[j].addEventListener('click', self.submit, false);
 		if (eles.length > 0) {
 			for (var j = eles.length - 1; j >= 0; --j) eles[j].addEventListener('click', self.submit, false);
 
 			// こちらのタイミングで送信するため、既定の送信イベントを止めておく
 			form.addEventListener('submit', self.stopSubmit, false);
 
 			// reCAPTCHAライブラリーロード
 			self.loadLib();
 		}
 	}
 };
 
 // 再設定
 __PluginRecaptcha3__.prototype.update = function() {
 	const	self = this;
 	if (this.timer) clearTimeout(this.timer);
 	this.timer = setTimeout(function() { self.setup(); self.timer = null; }, 50);
 };
 
 // 送信防止
 __PluginRecaptcha3__.prototype.stopSubmit = function(e) {
 	e.preventDefault();
 	e.stopPropagation();
 	return false;
 };
 
 // クリック時送信処理
 __PluginRecaptcha3__.prototype.submit = function(e) {
 	var	form;
 	if (this.closest) {
 		form = this.closest('form');
 	} else {
 		for (form = this.parentNode; form; form = form.parentNode) if (form.nodeName.toLowerCase() == 'form') break;	// 旧ブラウザー対策
 	}
 
 	// クリックされたsubmitボタンのname,value属性をhiddenにコピー(submitボタンが複数ある場合への対処)
 	if (form)  {
 		var nameEle = form.querySelector('.__plugin_recaptcha3_submit__');
 		var	name = this.getAttribute('name');
 		if (name) {
 			var	value = this.getAttribute('value');
 			if (!nameEle) {
 				form.insertAdjacentHTML('beforeend', '<input type="hidden" class="__plugin_recaptcha3_submit__" name="' + name + '" value="' + value + '"/>');
 			} else {
 				nameEle.setAttribute('name', name);
 				nameEle.setAttribute('value', value);
 			}
 		} else
 		if (nameEle) {
 			if (nameEle.remove) nameEle.remove();
 			else nameEle.parentNode.removeChild(nameEle);
 		}
 
 		// reCAPTCHAトークン取得
 		grecaptcha.ready(function() {
 			try {
 				grecaptcha.execute('${siteKey}').then(function(token) {
 					// 送信パラメーターにトークンを追加
 					var ele = form.querySelector('input[name="__plugin_recaptcha3__"]');
 					if (!ele) {
 						form.insertAdjacentHTML('beforeend', '<input type="hidden" name="__plugin_recaptcha3__" value="' + token + '"/>');
 					} else {
 						ele.setAttribute('value', token);
 					}
 					// フォーム送信
 					form.submit();
 				});
 			} catch(e) {}
 		});
 		if (${enabled}) {
 			// reCAPTCHAトークン取得
 			grecaptcha.ready(function() {
 				try {
 					grecaptcha.execute('${siteKey}').then(function(token) {
 						// 送信パラメーターにトークンを追加
 						var ele = form.querySelector('input[name="__plugin_recaptcha3__"]');
 						if (!ele) {
 							form.insertAdjacentHTML('beforeend', '<input type="hidden" name="__plugin_recaptcha3__" value="' + token + '"/>');
 						} else {
 							ele.setAttribute('value', token);
 						}
 						// フォーム送信
 						form.submit();
 					});
 				} catch(e) {}
 			});
 		} else {
 			// reCAPTCHA無効なら即フォーム送信
 			form.submit();
 		}
 	}
 	return false;
 };
 </script>
 EOT;
 
 	return $badge . $js;
 }
 
 
 // 受信リクエスト確認
 function plugin_recaptcha3_action() {
 	$result = true;	// 送信者判定結果(許可:true, 拒否:false)
 	$result = '';	// 送信者判定結果(許可:空, 拒否:エラーメッセージ)
 
 	// 機能有効かつPOSTメソッド?
 	if (!PLUGIN_RECAPTCHA3_DISABLED && !PKWK_READONLY && $_SERVER['REQUEST_METHOD'] == 'POST') {
 	if (!PLUGIN_RECAPTCHA3_DISABLED && ((PLUGIN_RECAPTCHA3_SITE_KEY && PLUGIN_RECAPTCHA3_SECRET_KEY) || PLUGIN_RECAPTCHA3_CENSORSHIP) && !PKWK_READONLY && $_SERVER['REQUEST_METHOD'] == 'POST') {
 		/* 【対象プラグイン設定テーブル】
 		   reCAPTCHA判定の対象とするプラグインを列挙する配列。
 		   パターン1 … array('name' => プラグイン名)
 		   パターン2 … array('name' => プラグイン名, 'vars' => 必須クエリーパラメーター名) */
 		   name   … プラグイン名
 		   censor … 検閲対象パラメーター名
 		   vars   … 併送パラメーター名
 		*/
 		$targetPlugins = array(
 			array('name' => 'article'),
 			array('name' => 'article',  'censor' => 'msg'),
 			array('name' => 'attach'),
 			array('name' => 'bugtrack'),
 			array('name' => 'comment'),
 			array('name' => 'edit', 'vars' => 'write'),	// editプラグインはページ更新(writeパラメーターあり)時のみ対象とする
 			array('name' => 'insert'),
 			array('name' => 'bugtrack', 'censor' => 'body'),
 			array('name' => 'comment',  'censor' => 'msg'),
 			array('name' => 'edit',     'censor' => 'msg', 'vars' => 'write'),	// editプラグインはwriteパラメーター併送(ページ更新)時のみ対象
 			array('name' => 'freeze'),
 			array('name' => 'insert',   'censor' => 'msg'),
 			array('name' => 'loginform'),
 			array('name' => 'memo'),
 			array('name' => 'pcomment'),
 			array('name' => 'memo',     'censor' => 'msg'),
 			array('name' => 'pcomment', 'censor' => 'msg'),
 			array('name' => 'rename'),
 			array('name' => 'tracker'),
 			array('name' => 'template'),
 			array('name' => 'tracker',  'censor' => 'Messages'),
 			array('name' => 'unfreeze'),
 			array('name' => 'vote'),
 		);
 
 		global	$vars;
 		list($name) = func_get_args();
 		$enabled = (PLUGIN_RECAPTCHA3_SITE_KEY && PLUGIN_RECAPTCHA3_SECRET_KEY);	// reCAPTCHA有効フラグ
 
 		foreach ($targetPlugins as $target) {
 			if ($target['name'] != $name) continue;	// プラグイン名一致?
 			if (!isset($target['vars']) || isset($vars[$target['vars']])) {	// クエリーパラメーター未指定、または指定名が含まれる?
 				if (!isset($vars['__plugin_recaptcha3__']) || $vars['__plugin_recaptcha3__'] == '') {	// reCAPTCHAトークンあり?
 				if ($enabled && (!isset($vars['__plugin_recaptcha3__']) || $vars['__plugin_recaptcha3__'] == '')) {	// reCAPTCHAトークンあり?
 					// トークンのない不正要求なら送信者を拒否
 					$result = false;
 					$result = 'Rejected by Google reCAPTCHA v3';
 				} else
 				if (PLUGIN_RECAPTCHA3_CHECK_REFERER && strpos($_SERVER['HTTP_REFERER'], get_script_uri()) === false) {
 					// 自サイト以外からのアクセスを拒否
 					$result = 'Deny access';
 				} else {
 					// 検閲対象パラメーターあり?
 					if (PLUGIN_RECAPTCHA3_CENSORSHIP && isset($target['censor']) && isset($vars[$target['censor']])) {
 						// 投稿禁止語句が含まれていたら受信拒否
 						if (preg_match(PLUGIN_RECAPTCHA3_CENSORSHIP, $vars[$target['censor']])) {
 							$result = 'Forbidden word detected';
 							break;
 						}
 					}
 
 					// reCAPTCHA API呼び出し
 					$ch = curl_init('https:'.'//www.google.com/recaptcha/api/siteverify');
 					curl_setopt($ch, CURLOPT_POST, true);
 					curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array('secret' => PLUGIN_RECAPTCHA3_SECRET_KEY, 'response' => $vars['__plugin_recaptcha3__'])));
 					curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 					curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
 					if (PLUGIN_RECAPTCHA3_API_TIMEOUT > 0) curl_setopt($ch, CURLOPT_TIMEOUT, PLUGIN_RECAPTCHA3_API_TIMEOUT);
 					$data = json_decode(curl_exec($ch));
 					curl_close($ch);
 					if ($enabled) {
 						$ch = curl_init('https:'.'//www.google.com/recaptcha/api/siteverify');
 						curl_setopt($ch, CURLOPT_POST, true);
 						curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array('secret' => PLUGIN_RECAPTCHA3_SECRET_KEY, 'response' => $vars['__plugin_recaptcha3__'])));
 						curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 						curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
 						if (PLUGIN_RECAPTCHA3_API_TIMEOUT > 0) curl_setopt($ch, CURLOPT_TIMEOUT, PLUGIN_RECAPTCHA3_API_TIMEOUT);
 						$data = json_decode(curl_exec($ch));
 						curl_close($ch);
 
 					// スコアが閾値未満なら送信者を拒否
 					if (!$data->success || $data->score < PLUGIN_RECAPTCHA3_SCORE_THRESHOLD) $result = false;
 						// スコアが閾値未満なら送信者を拒否
 						if (!$data->success || $data->score < PLUGIN_RECAPTCHA3_SCORE_THRESHOLD) $result = 'Rejected by Google reCAPTCHA v3';
 					}
 				}
 				break;
 			}
 		}
 
 		// エラー用のHTTPステータスコードを設定
 		if ($result && PLUGIN_RECAPTCHA3_ERR_STATUS) http_response_code(PLUGIN_RECAPTCHA3_ERR_STATUS);
 	}
 
 	return array('msg' => 'recaptcha3', 'body' => $result);
 }
 

**ご注意 [#x8fb80a9]

-標準プラグイン以外の動作確認はしていません。サードパーティ製プラグインによっては機能が妨げられる場合があります。
-JavaScriptが有効でないと動作しません。
-サーバーからreCAPTCHA APIへのアクセスにcURLを使用します。
-reCAPTCHA v3 について詳しくはGoogleのreCAPTCHAサイトをご覧ください。https: //www.google.com/recaptcha/

**詳細 [#pe9ff758]

***動作設定 [#ncf0cdef]

コード内の下記の定数で動作を制御することができます。

|~定数名|~値|~既定値|~意味|
|PLUGIN_RECAPTCHA3_SITE_KEY|CENTER:文字列||reCAPTCHA v3 サイトキー。取得したキーを必ず設定すること|
|PLUGIN_RECAPTCHA3_SECRET_KEY|CENTER:文字列||reCAPTCHA v3 シークレットキー。取得したキーを必ず設定すること|
|PLUGIN_RECAPTCHA3_SITE_KEY|CENTER:文字列||Google reCAPTCHA v3 サイトキー。空の場合、reCAPTCHA判定は実施されない|
|PLUGIN_RECAPTCHA3_SECRET_KEY|CENTER:文字列||Google reCAPTCHA v3 シークレットキー。空の場合、reCAPTCHA判定は実施されない|
|PLUGIN_RECAPTCHA3_SCORE_THRESHOLD|CENTER:0.0~1.0|CENTER:0.5|スコア閾値(0.0~1.0)。reCAPTCHAによる判定スコアがこの値より低い送信者は拒否される|
|PLUGIN_RECAPTCHA3_DISABLED|CENTER:0 or 1|CENTER:0|本プラグイン機能を無効にする。メンテナンス用|
|PLUGIN_RECAPTCHA3_HIDE_BADGE|CENTER:0 or 1|CENTER:1|reCAPTCHAバッジを非表示にし、代替文言を出力する。Googleの規約によりバッジか文言どちらかの表示が必須|
|PLUGIN_RECAPTCHA3_API_TIMEOUT|CENTER:任意の数値|CENTER:0|reCAPTCHA APIタイムアウト時間(秒)。0なら無指定|
|PLUGIN_RECAPTCHA3_CENSORSHIP|CENTER:文字列|CENTER:|投稿禁止語句を表す正規表現|
|PLUGIN_RECAPTCHA3_CHECK_REFERER|CENTER:0 or 1|CENTER:0|1ならリファラーを参照し自サイト以外からの要求を拒否。信頼性が低いため非推奨、応急処置に用いる|
|PLUGIN_RECAPTCHA3_ERR_STATUS|CENTER:HTTPステータスコード|CENTER:403|拒否時に返すHTTPステータスコード|
|PLUGIN_RECAPTCHA3_DISABLED|CENTER:0 or 1|CENTER:0|1なら本プラグインを無効化。メンテナンス用|

***スパム拒否の仕組み [#m18c40ac]

+ブラウザー側において、JavaScriptによってページ内のすべてのform要素を探し出し、submitボタンがクリックされたらreCAPTCHAトークンを取得して送信パラメーターに含めるよう細工する。&br;→ 副作用として、この細工がサードパーティ製プラグインの動作を妨げる可能性がある
+サーバー側において、受信したリクエストがPOSTメソッドかつ既知のプラグイン呼び出しなら次の判定を行う。
++パラメーターにreCAPTCHAトークンが含まれなければ、不正アクセスとみなしてリクエストを拒否する。&br;→ フォームを介さず直接プラグインURLを叩く種類のロボットはすべて弾かれる
++reCAPTCHA APIにトークンを送信し、応答スコアが閾値未満ならスパマーとみなしてリクエストを拒否する。&br;→ 機械的なフォーム操作や不審な送信元IPアドレスなどはスコアが低く弾かれる&br;→ 学習も絡みスコア基準は曖昧だが、もし効果が薄い・または効き過ぎるといった問題があれば PLUGIN_RECAPTCHA3_SCORE_THRESHOLD 定数値で調整できる&br;→ 手入力による散発的ないたずら書き込みの類いは、正当な編集と区別できず弾くことができない(矯激・卑俗な書き込みを極端に繰り返せば学習されるかもしれないが)
++reCAPTCHA APIにトークンを送信し、応答スコアが閾値未満ならスパマーとみなしてリクエストを拒否する。&br;→ 機械的なフォーム操作や不審な送信元IPアドレスなどはスコアが低く弾かれる&br;→ 学習も絡みスコア基準は曖昧だが、もし効果が薄い・または効き過ぎるといった問題があれば PLUGIN_RECAPTCHA3_SCORE_THRESHOLD 定数値で調整できる&br;→ 手入力による散発的ないたずら書き込みの類いは、正当な編集と区別できず弾くことができない(極端に繰り返せば学習されるかもしれないが)
++投稿禁止語句が設定されており、かつテキスト投稿を伴うプラグインであればその内容を判定&br;→ 特定の宣伝文句などを含むスパムをはじくことができる。URLを禁止するのが最も広範で効果的だが、不便にもなるので注意

***高度な設定:対象プラグインの追加 [#k29ab2be]

本プラグインはデフォルトで、PukiWikiに標準添付の編集系プラグインのみをスパム判定の対象としています。具体的には次の通り。

article, attach, bugtrack, comment, edit, insert, loginform, memo, pcomment, rename, tracker, vote
article, attach, bugtrack, comment, edit, freeze, insert, loginform, memo, pcomment, rename, template, tracker, unfreeze, vote

スパムボットは標準プラグインを標的にすると考えられるため、一般的にはこれで十分なはずです。~
しかし、もし特定のサードパーティ製プラグインを標的として攻撃されていたら、コード内の $targetPlugins 配列にそのプラグイン名を他行に倣って追加してください。~
ただし上で述べた通り、プラグインの編集・投稿機能がPOSTメソッドのform要素かつsubmitボタンで送信する仕組みになっていないと効果がなく、処理内容による相性にも左右されます。~

**動作確認 [#mecf0290]

本プラグインが正しく導入されていれば、ページ末尾に「This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.」との文言(定数設定によってはreCAPTCHAバッジ)が表示されます。~
この状態でページ編集やコメント投稿ができていればOKです。

逆に拒否される場合を確認したければ、シークレットキーの値をわざと不正にしてみてください。~
その状態でページ編集などを試みると、「Rejected by Google reCAPTCHA v3」とエラーメッセージが表示されるはずです。~
スパムに対する正しいテストケースではありませんが、少なくともプラグインが動作しreCAPTCHA APIと連絡していることは確かめられます。~
実際のスパム攻撃については、Google reCAPTCHAサイトの管理画面に統計が表示されます。スコア閾値調整の参考にもなるでしょう。

なお、本プラグインが正しく導入されていても、古いブラウザーでは常に編集に失敗するかもしれませんが、仕様としてご了承ください。~
編集ができないだけで、閲覧には支障ないと思います。

閲覧専用(PKWK_READONLY が 1)のウィキにおいては、本プラグインは何もしません。

**ライセンス [#y9d827c1]

GPL v3


**改版履歴 [#history]
-バージョン1.0
--初版
-バージョン1.1
--投稿禁止語句判定を追加
--リファラー判定を追加
--form要素submitボタンのないページではreCAPTCHAライブラリーをロードしないよう最適化
--拒否時に返すHTTPステータスコードの設定を追加
--コマンド呼び出しの返り値を変更。伴い、lib/plugin.phpへの挿入コードも変更


トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Site admin: PukiWiki Development Team

PukiWiki 1.5.3+ © 2001-2020 PukiWiki Development Team. Powered by PHP 5.6.40-0+deb8u12. HTML convert time: 0.229 sec.

OSDN