MW WP FormでCloudflare Turnstileを使う方法

<div class="cf-turnstile"></div>
[mwform_hidden name="turnstile-check" value="1"]
/* ===============================================
# turnstile
=============================================== */

// ▼ ここだけ設定
if ( ! defined( 'MY_TURNSTILE_SITE_KEY' ) )   define( 'MY_TURNSTILE_SITE_KEY',   'ここにサイトキー' );
if ( ! defined( 'MY_TURNSTILE_SECRET_KEY' ) ) define( 'MY_TURNSTILE_SECRET_KEY', 'ここにシークレットキー' );

// 通行証(確認画面からの送信を通すための Cookie)の有効時間
define( 'MY_TURNSTILE_PASS_TTL', 10 * MINUTE_IN_SECONDS );


/* ============================================================
 * 1. 画面側(ウィジェットの読み込み・送信ボタンの制御)
 *    ページに .cf-turnstile がある場合だけ Cloudflare のスクリプトを読み込む
 * ============================================================ */

// 確認画面ではウィジェットを非表示(確認画面は入力画面と同じ本文を使い回すため)
add_action( 'wp_head', function () {
	echo '<style>.mw_wp_form_confirm .cf-turnstile{display:none}</style>' . "\n";
} );

add_action( 'wp_footer', function () {
	?>
<script>
(function () {
	var SITE_KEY = <?php echo wp_json_encode( MY_TURNSTILE_SITE_KEY ); ?>;

	function setDisabled(disabled) {
		document.querySelectorAll('.mw_wp_form_input input[type="submit"], .mw_wp_form_input button[type="submit"]').forEach(function (btn) {
			btn.disabled = disabled;
			btn.style.opacity = disabled ? '0.5' : '';
			btn.style.cursor = disabled ? 'not-allowed' : '';
		});
	}
	window.myTurnstileOk = function () { setDisabled(false); };
	window.myTurnstileNg = function () { setDisabled(true); };

	function init() {
		// ウィジェットがあるページだけ処理する(確認画面のものは除く)
		var widgets = Array.prototype.filter.call(
			document.querySelectorAll('.cf-turnstile'),
			function (el) { return !el.closest('.mw_wp_form_confirm'); }
		);
		if (!widgets.length) { return; }

		widgets.forEach(function (el) {
			if (!el.getAttribute('data-sitekey')) { el.setAttribute('data-sitekey', SITE_KEY); }
			el.setAttribute('data-callback', 'myTurnstileOk');
			el.setAttribute('data-expired-callback', 'myTurnstileNg');
			el.setAttribute('data-error-callback', 'myTurnstileNg');
		});
		setDisabled(true); // 検証が終わるまで送信ボタンを押せなくする

		var s = document.createElement('script');
		s.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
		s.async = true;
		s.defer = true;
		document.head.appendChild(s);
	}

	// すでに読み込み完了していても動くようにする
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', init);
	} else {
		init();
	}
})();
</script>
	<?php
} );


/* ============================================================
 * 2. サーバー側(Cloudflare への検証)
 * ============================================================ */

function my_turnstile_verify( $token ) {
	if ( empty( $token ) ) {
		return false;
	}

	// トークンは1回しか使えないため、検証済みの結果を短時間キャッシュ
	$cache_key = 'cf_ts_' . md5( $token );
	if ( get_transient( $cache_key ) === 'ok' ) {
		return true;
	}

	$res = wp_remote_post(
		'https://challenges.cloudflare.com/turnstile/v0/siteverify',
		array(
			'timeout' => 10,
			'body'    => array(
				'secret'   => MY_TURNSTILE_SECRET_KEY,
				'response' => $token,
				'remoteip' => isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '',
			),
		)
	);
	if ( is_wp_error( $res ) ) {
		return false;
	}

	$body = json_decode( wp_remote_retrieve_body( $res ), true );
	if ( ! empty( $body['success'] ) ) {
		set_transient( $cache_key, 'ok', 5 * MINUTE_IN_SECONDS );
		return true;
	}
	return false;
}

// 通行証(署名付き Cookie)
function my_turnstile_set_pass() {
	$time  = time();
	$value = $time . '.' . hash_hmac( 'sha256', (string) $time, MY_TURNSTILE_SECRET_KEY );
	setcookie( 'my_ts_pass', $value, array(
		'expires'  => $time + MY_TURNSTILE_PASS_TTL,
		'path'     => '/',
		'secure'   => is_ssl(),
		'httponly' => true,
		'samesite' => 'Lax',
	) );
}

function my_turnstile_has_pass() {
	if ( empty( $_COOKIE['my_ts_pass'] ) ) {
		return false;
	}
	$parts = explode( '.', sanitize_text_field( wp_unslash( $_COOKIE['my_ts_pass'] ) ) );
	if ( 2 !== count( $parts ) ) {
		return false;
	}
	list( $time, $sig ) = $parts;
	if ( ! ctype_digit( $time ) || ( time() - (int) $time ) > MY_TURNSTILE_PASS_TTL ) {
		return false;
	}
	return hash_equals( hash_hmac( 'sha256', $time, MY_TURNSTILE_SECRET_KEY ), $sig );
}

// hidden の turnstile-check が付いた送信を検証し、失敗なら MW WP Form に渡す前に止める
add_action( 'init', function () {
	if ( 'POST' !== $_SERVER['REQUEST_METHOD'] || ! isset( $_POST['turnstile-check'] ) ) {
		return;
	}

	$token = isset( $_POST['cf-turnstile-response'] )
		? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) )
		: '';

	$ok     = false;
	$method = 'なし';

	if ( $token ) {
		// 入力画面からの送信: トークンを検証し、成功したら通行証を発行
		$ok     = my_turnstile_verify( $token );
		$method = 'token';
		if ( $ok ) {
			my_turnstile_set_pass();
		}
	} elseif ( my_turnstile_has_pass() ) {
		// 確認画面からの送信: 通行証があればOK
		$ok     = true;
		$method = 'cookie';
	}

	// 動作確認用ログ(WP_DEBUG_LOG 有効時のみ)
	if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
		error_log( '[turnstile] via=' . $method . ' result=' . ( $ok ? 'OK' : 'NG' ) );
	}

	if ( ! $ok ) {
		wp_die(
			'スパム対策の認証に失敗しました。ページを戻って、もう一度お試しください。',
			'認証エラー',
			array( 'response' => 403, 'back_link' => true )
		);
	}
}, 1 );

コメント

タイトルとURLをコピーしました