Files
roi-theme/Shared/Infrastructure/Services/GoogleRecaptchaValidator.php
FrankZamora d135ec8a41 feat(api): implement recaptcha v3 anti-spam protection
- Add RecaptchaValidatorInterface and RecaptchaResult entity in Domain
- Create RecaptchaValidationService in Application layer
- Implement GoogleRecaptchaValidator for API integration
- Add recaptcha-settings schema and admin FormBuilder
- Integrate reCAPTCHA validation in NewsletterAjaxHandler
- Integrate reCAPTCHA validation in ContactFormAjaxHandler
- Update FooterRenderer and ContactFormRenderer with reCAPTCHA scripts
- Configure DIContainer with RecaptchaValidationService injection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:01:46 -06:00

113 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Shared\Domain\Contracts\RecaptchaValidatorInterface;
use ROITheme\Shared\Domain\Entities\RecaptchaResult;
/**
* GoogleRecaptchaValidator - Implementacion de validacion reCAPTCHA con API de Google
*
* RESPONSABILIDAD: Comunicarse con la API de Google reCAPTCHA v3 para
* validar tokens y obtener scores.
*
* API ENDPOINT: https://www.google.com/recaptcha/api/siteverify
*
* RESPUESTA DE GOOGLE:
* ```json
* {
* "success": true|false,
* "score": 0.0-1.0,
* "action": "string",
* "challenge_ts": "timestamp",
* "hostname": "string",
* "error-codes": ["string"]
* }
* ```
*
* @package ROITheme\Shared\Infrastructure\Services
*/
final class GoogleRecaptchaValidator implements RecaptchaValidatorInterface
{
private const API_URL = 'https://www.google.com/recaptcha/api/siteverify';
private const TIMEOUT_SECONDS = 5;
/**
* Validar token de reCAPTCHA v3 con API de Google
*
* @param string $token Token generado por reCAPTCHA en frontend
* @param string $action Accion esperada
* @param string $secretKey Clave secreta de reCAPTCHA
* @return RecaptchaResult Resultado de la validacion
*/
public function validate(string $token, string $action, string $secretKey): RecaptchaResult
{
// Validar parametros
if (empty($token) || empty($secretKey)) {
return RecaptchaResult::failure(['missing-input-secret']);
}
// Preparar request
$response = wp_remote_post(self::API_URL, [
'timeout' => self::TIMEOUT_SECONDS,
'body' => [
'secret' => $secretKey,
'response' => $token,
'remoteip' => $this->getClientIP(),
],
]);
// Manejar error de conexion
if (is_wp_error($response)) {
error_log('ROI Theme reCAPTCHA API error: ' . $response->get_error_message());
return RecaptchaResult::failure(['connection-error']);
}
// Verificar codigo HTTP
$statusCode = wp_remote_retrieve_response_code($response);
if ($statusCode !== 200) {
error_log('ROI Theme reCAPTCHA API HTTP error: ' . $statusCode);
return RecaptchaResult::failure(['http-error-' . $statusCode]);
}
// Parsear respuesta JSON
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
error_log('ROI Theme reCAPTCHA API invalid JSON: ' . $body);
return RecaptchaResult::failure(['invalid-json']);
}
// Construir resultado
return new RecaptchaResult(
success: (bool) ($data['success'] ?? false),
score: (float) ($data['score'] ?? 0.0),
action: (string) ($data['action'] ?? ''),
errorCodes: (array) ($data['error-codes'] ?? []),
hostname: (string) ($data['hostname'] ?? ''),
challengeTs: (string) ($data['challenge_ts'] ?? '')
);
}
/**
* Obtener IP del cliente
*
* @return string
*/
private function getClientIP(): string
{
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
}
if (!empty($_SERVER['REMOTE_ADDR'])) {
return sanitize_text_field($_SERVER['REMOTE_ADDR']);
}
return '';
}
}