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>
This commit is contained in:
FrankZamora
2026-01-08 17:01:46 -06:00
parent 0f6387ab46
commit d135ec8a41
16 changed files with 1299 additions and 37 deletions

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ROITheme\Public\ContactForm\Infrastructure\Api\WordPress;
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
use ROITheme\Shared\Application\Services\RecaptchaValidationService;
/**
* ContactFormAjaxHandler - Procesa envios del formulario de contacto
@@ -12,6 +13,7 @@ use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
*
* SEGURIDAD:
* - Verifica nonce
* - reCAPTCHA v3 para proteccion anti-spam
* - Webhook URL NUNCA se expone al cliente
* - Webhook URL se obtiene de BD server-side
* - Rate limiting basico
@@ -25,7 +27,8 @@ final class ContactFormAjaxHandler
private const COMPONENT_NAME = 'contact-form';
public function __construct(
private ComponentSettingsRepositoryInterface $settingsRepository
private ComponentSettingsRepositoryInterface $settingsRepository,
private ?RecaptchaValidationService $recaptchaService = null
) {}
/**
@@ -52,7 +55,15 @@ final class ContactFormAjaxHandler
return;
}
// 2. Rate limiting basico (1 envio por IP cada 30 segundos)
// 2. Validar reCAPTCHA (si esta habilitado)
if (!$this->validateRecaptcha()) {
wp_send_json_error([
'message' => __('Verificacion de seguridad fallida. Por favor intenta de nuevo.', 'roi-theme')
], 403);
return;
}
// 3. Rate limiting basico (1 envio por IP cada 30 segundos)
if (!$this->checkRateLimit()) {
wp_send_json_error([
'message' => __('Por favor espera un momento antes de enviar otro mensaje.', 'roi-theme')
@@ -60,7 +71,7 @@ final class ContactFormAjaxHandler
return;
}
// 3. Sanitizar y validar inputs
// 4. Sanitizar y validar inputs
$formData = $this->sanitizeFormData($_POST);
$validation = $this->validateFormData($formData);
@@ -72,7 +83,7 @@ final class ContactFormAjaxHandler
return;
}
// 4. Obtener configuracion del componente (incluye webhook URL)
// 5. Obtener configuracion del componente (incluye webhook URL)
$settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
if (empty($settings)) {
@@ -98,10 +109,10 @@ final class ContactFormAjaxHandler
return;
}
// 5. Preparar payload para webhook
// 6. Preparar payload para webhook
$payload = $this->preparePayload($formData, $includePageUrl, $includeTimestamp);
// 6. Enviar a webhook
// 7. Enviar a webhook
$result = $this->sendToWebhook($webhookUrl, $webhookMethod, $payload);
if ($result['success']) {
@@ -300,4 +311,31 @@ final class ContactFormAjaxHandler
{
return $value === true || $value === '1' || $value === 1;
}
/**
* Validar reCAPTCHA token
*
* @return bool True si pasa la validacion o si reCAPTCHA no esta habilitado
*/
private function validateRecaptcha(): bool
{
// Si el servicio no esta inyectado, permitir
if ($this->recaptchaService === null) {
return true;
}
// Si reCAPTCHA no esta habilitado, permitir
if (!$this->recaptchaService->isRecaptchaEnabled()) {
return true;
}
// Obtener token del POST
$token = sanitize_text_field($_POST['recaptcha_token'] ?? '');
// Obtener accion configurada para contacto
$action = $this->recaptchaService->getContactAction();
// Validar con el servicio
return $this->recaptchaService->validateSubmission($token, $action);
}
}