feat(07-01): create captcha verification class

- Support for reCAPTCHA v2, v3, and hCaptcha
- Server-side verification with wp_remote_post
- Automatic script enqueuing based on provider
- Widget rendering for all three providers
- reCAPTCHA v3 score checking (>= 0.5)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 12:30:18 +09:00
parent 17cc2db0a0
commit 486d88e5b1

334
includes/class-captcha.php Normal file
View File

@@ -0,0 +1,334 @@
<?php
/**
* Captcha Verification
*
* Handles reCAPTCHA v2, v3, and hCaptcha integration
*
* @package Umzugsliste
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Captcha verification class
*/
class Umzugsliste_Captcha {
/**
* Single instance
*/
private static $instance = null;
/**
* Get singleton instance
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
}
/**
* Check if captcha is enabled
*
* @return bool
*/
public function is_enabled() {
$provider = $this->get_provider();
return 'none' !== $provider && ! empty( $provider );
}
/**
* Get current captcha provider
*
* @return string none|recaptcha_v2|recaptcha_v3|hcaptcha
*/
public function get_provider() {
return get_option( 'umzugsliste_captcha_provider', 'none' );
}
/**
* Get site key
*
* @return string
*/
private function get_site_key() {
return get_option( 'umzugsliste_captcha_site_key', '' );
}
/**
* Get secret key
*
* @return string
*/
private function get_secret_key() {
return get_option( 'umzugsliste_captcha_secret_key', '' );
}
/**
* Enqueue captcha provider scripts
*/
public function enqueue_scripts() {
if ( ! $this->is_enabled() ) {
return;
}
$provider = $this->get_provider();
$site_key = $this->get_site_key();
if ( empty( $site_key ) ) {
return;
}
switch ( $provider ) {
case 'recaptcha_v2':
wp_enqueue_script(
'recaptcha-v2',
'https://www.google.com/recaptcha/api.js',
array(),
null,
true
);
break;
case 'recaptcha_v3':
wp_enqueue_script(
'recaptcha-v3',
'https://www.google.com/recaptcha/api.js?render=' . $site_key,
array(),
null,
true
);
break;
case 'hcaptcha':
wp_enqueue_script(
'hcaptcha',
'https://js.hcaptcha.com/1/api.js',
array(),
null,
true
);
break;
}
}
/**
* Render captcha widget in form
*
* @return string HTML for captcha widget
*/
public function render_widget() {
if ( ! $this->is_enabled() ) {
return '';
}
$provider = $this->get_provider();
$site_key = $this->get_site_key();
if ( empty( $site_key ) ) {
return '';
}
ob_start();
switch ( $provider ) {
case 'recaptcha_v2':
?>
<div class="captcha-widget">
<div class="g-recaptcha" data-sitekey="<?php echo esc_attr( $site_key ); ?>"></div>
</div>
<?php
break;
case 'recaptcha_v3':
?>
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response">
<script>
grecaptcha.ready(function() {
var form = document.getElementById('umzugsliste-form');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
grecaptcha.execute('<?php echo esc_js( $site_key ); ?>', {action: 'submit'}).then(function(token) {
document.getElementById('g-recaptcha-response').value = token;
form.submit();
});
});
}
});
</script>
<?php
break;
case 'hcaptcha':
?>
<div class="captcha-widget">
<div class="h-captcha" data-sitekey="<?php echo esc_attr( $site_key ); ?>"></div>
</div>
<?php
break;
}
return ob_get_clean();
}
/**
* Verify captcha response
*
* @param array $post_data POST data from form submission
* @return bool True if verified, false otherwise
*/
public function verify_response( $post_data ) {
if ( ! $this->is_enabled() ) {
return true;
}
$provider = $this->get_provider();
switch ( $provider ) {
case 'recaptcha_v2':
return $this->verify_recaptcha_v2( $post_data );
case 'recaptcha_v3':
return $this->verify_recaptcha_v3( $post_data );
case 'hcaptcha':
return $this->verify_hcaptcha( $post_data );
default:
return true;
}
}
/**
* Verify reCAPTCHA v2 response
*
* @param array $post_data POST data
* @return bool
*/
private function verify_recaptcha_v2( $post_data ) {
$response = isset( $post_data['g-recaptcha-response'] ) ? $post_data['g-recaptcha-response'] : '';
if ( empty( $response ) ) {
return false;
}
$secret_key = $this->get_secret_key();
if ( empty( $secret_key ) ) {
return false;
}
$verify_url = 'https://www.google.com/recaptcha/api/siteverify';
$response = wp_remote_post(
$verify_url,
array(
'body' => array(
'secret' => $secret_key,
'response' => $response,
'remoteip' => $_SERVER['REMOTE_ADDR'],
),
)
);
if ( is_wp_error( $response ) ) {
return false;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
return isset( $body['success'] ) && $body['success'];
}
/**
* Verify reCAPTCHA v3 response
*
* @param array $post_data POST data
* @return bool
*/
private function verify_recaptcha_v3( $post_data ) {
$response = isset( $post_data['g-recaptcha-response'] ) ? $post_data['g-recaptcha-response'] : '';
if ( empty( $response ) ) {
return false;
}
$secret_key = $this->get_secret_key();
if ( empty( $secret_key ) ) {
return false;
}
$verify_url = 'https://www.google.com/recaptcha/api/siteverify';
$response = wp_remote_post(
$verify_url,
array(
'body' => array(
'secret' => $secret_key,
'response' => $response,
'remoteip' => $_SERVER['REMOTE_ADDR'],
),
)
);
if ( is_wp_error( $response ) ) {
return false;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
// Check success and score (must be >= 0.5)
return isset( $body['success'] ) && $body['success'] && isset( $body['score'] ) && $body['score'] >= 0.5;
}
/**
* Verify hCaptcha response
*
* @param array $post_data POST data
* @return bool
*/
private function verify_hcaptcha( $post_data ) {
$response = isset( $post_data['h-captcha-response'] ) ? $post_data['h-captcha-response'] : '';
if ( empty( $response ) ) {
return false;
}
$secret_key = $this->get_secret_key();
if ( empty( $secret_key ) ) {
return false;
}
$verify_url = 'https://hcaptcha.com/siteverify';
$response = wp_remote_post(
$verify_url,
array(
'body' => array(
'secret' => $secret_key,
'response' => $response,
'remoteip' => $_SERVER['REMOTE_ADDR'],
),
)
);
if ( is_wp_error( $response ) ) {
return false;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
return isset( $body['success'] ) && $body['success'];
}
}