Files
roi-theme/_openspec/changes/recaptcha-anti-spam/spec.md
FrankZamora 0f6387ab46 refactor: reorganizar openspec y planificacion con spec recaptcha
- renombrar openspec/ a _openspec/ (carpeta auxiliar)
- mover specs de features a changes/
- crear specs base: arquitectura-limpia, estandares-codigo, nomenclatura
- migrar _planificacion/ con design-system y roi-theme-template
- agregar especificacion recaptcha anti-spam (proposal, tasks, spec)
- corregir rutas y referencias en todas las specs

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

326 lines
13 KiB
Markdown

# Especificación: reCAPTCHA v3 Anti-Spam Protection
## Purpose
Define la integración de Google reCAPTCHA v3 para proteger los formularios del sitio (Newsletter y Contact Form) contra spam automatizado, siguiendo Clean Architecture.
## Requirements
### Requirement: Configuración de reCAPTCHA
El sistema DEBE permitir configurar reCAPTCHA v3 desde el panel de administración.
#### Scenario: Schema JSON para configuración
- **WHEN** se crea el schema de configuración
- **THEN** DEBE ubicarse en `Schemas/recaptcha-settings.json`
- **AND** `component_name` DEBE ser `recaptcha-settings`
- **AND** DEBE incluir grupo VISIBILITY con `is_enabled`
- **AND** DEBE incluir grupo BEHAVIOR con `site_key`, `secret_key`, `score_threshold`
#### Scenario: Campos obligatorios del schema
- **GIVEN** el schema `recaptcha-settings.json`
- **WHEN** se define la estructura
- **THEN** `is_enabled` DEBE ser tipo boolean con default true
- **AND** `site_key` DEBE ser tipo text (clave pública)
- **AND** `secret_key` DEBE ser tipo text (clave secreta, almacenada encriptada)
- **AND** `score_threshold` DEBE ser tipo select con options: 0.3, 0.5, 0.7, 0.9
#### Scenario: Sincronización con BD
- **WHEN** se sincroniza el schema
- **THEN** ejecutar `wp roi-theme sync-component recaptcha-settings`
- **AND** los datos DEBEN ir a `wp_roi_theme_component_settings`
- **AND** `component_name` en BD DEBE ser `recaptcha-settings`
---
### Requirement: Contrato de Validación (Domain)
El Domain DEBE definir la interfaz de validación de reCAPTCHA.
#### Scenario: Ubicación de RecaptchaValidatorInterface
- **WHEN** se crea la interfaz
- **THEN** DEBE ubicarse en `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
- **AND** namespace DEBE ser `ROITheme\Shared\Domain\Contracts`
#### Scenario: Firma del método validate
- **WHEN** se define RecaptchaValidatorInterface
- **THEN** DEBE tener método `validate(string $token, string $action): RecaptchaResult`
- **AND** `$token` es el token generado por reCAPTCHA frontend
- **AND** `$action` es el nombre de la acción (newsletter_submit, contact_submit)
- **AND** retorna objeto `RecaptchaResult` con score y success
#### Scenario: Entidad RecaptchaResult
- **WHEN** se define el resultado de validación
- **THEN** DEBE existir `Shared/Domain/Entities/RecaptchaResult.php`
- **AND** DEBE tener propiedades: `success` (bool), `score` (float), `action` (string), `errorCodes` (array)
- **AND** DEBE tener método `isValid(float $threshold): bool`
---
### Requirement: Servicio de Aplicación
La capa Application DEBE orquestar la validación de reCAPTCHA.
#### Scenario: Ubicación del servicio
- **WHEN** se crea el servicio de aplicación
- **THEN** DEBE ubicarse en `Shared/Application/Services/RecaptchaValidationService.php`
- **AND** namespace DEBE ser `ROITheme\Shared\Application\Services`
#### Scenario: Inyección de dependencias
- **WHEN** se implementa RecaptchaValidationService
- **THEN** DEBE inyectar `RecaptchaValidatorInterface` via constructor
- **AND** DEBE inyectar `ComponentSettingsRepositoryInterface` para obtener configuración
- **AND** NO DEBE instanciar servicios directamente con `new`
#### Scenario: Lógica de validación con threshold
- **GIVEN** un token de reCAPTCHA y una acción
- **WHEN** se llama a `validateSubmission(string $token, string $action): bool`
- **THEN** DEBE obtener `score_threshold` de la configuración en BD
- **AND** DEBE llamar a `RecaptchaValidatorInterface::validate()`
- **AND** DEBE retornar `true` si `RecaptchaResult::isValid($threshold)` es true
- **AND** DEBE retornar `false` si score está por debajo del threshold
#### Scenario: Bypass cuando está deshabilitado
- **GIVEN** `is_enabled` es false en configuración
- **WHEN** se llama a `validateSubmission()`
- **THEN** DEBE retornar `true` sin llamar a la API de Google
- **AND** NO DEBE generar errores
---
### Requirement: Implementación de Infraestructura
La capa Infrastructure DEBE implementar la comunicación con API de Google.
#### Scenario: Ubicación del validador
- **WHEN** se implementa el validador
- **THEN** DEBE ubicarse en `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
- **AND** namespace DEBE ser `ROITheme\Shared\Infrastructure\Services`
- **AND** DEBE implementar `RecaptchaValidatorInterface`
#### Scenario: Llamada a API de Google
- **WHEN** se valida un token
- **THEN** DEBE hacer POST a `https://www.google.com/recaptcha/api/siteverify`
- **AND** DEBE enviar `secret` (secret key) y `response` (token)
- **AND** DEBE usar `wp_remote_post()` de WordPress
- **AND** timeout DEBE ser máximo 5 segundos
#### Scenario: Parseo de respuesta exitosa
- **GIVEN** API de Google responde exitosamente
- **WHEN** se parsea la respuesta
- **THEN** DEBE extraer `success` (bool)
- **AND** DEBE extraer `score` (float 0.0-1.0)
- **AND** DEBE extraer `action` (string)
- **AND** DEBE retornar `RecaptchaResult` con estos valores
#### Scenario: Manejo de errores de API
- **GIVEN** API de Google no responde o responde con error
- **WHEN** se procesa la respuesta
- **THEN** DEBE retornar `RecaptchaResult` con `success = false`
- **AND** DEBE incluir códigos de error en `errorCodes`
- **AND** NO DEBE lanzar excepciones no controladas
---
### Requirement: Integración Frontend
Los Renderers DEBEN incluir el script de reCAPTCHA y generar tokens.
#### Scenario: Script de reCAPTCHA en FooterRenderer
- **WHEN** FooterRenderer genera HTML del newsletter
- **AND** reCAPTCHA está habilitado
- **THEN** DEBE incluir script: `<script src="https://www.google.com/recaptcha/api.js?render={site_key}"></script>`
- **AND** DEBE agregar input hidden `recaptcha_token` al formulario
- **AND** DEBE agregar JS para ejecutar `grecaptcha.execute()` al submit
#### Scenario: Script de reCAPTCHA en ContactFormRenderer
- **WHEN** ContactFormRenderer genera HTML del formulario
- **AND** reCAPTCHA está habilitado
- **THEN** DEBE incluir script de reCAPTCHA con site_key
- **AND** DEBE agregar input hidden `recaptcha_token`
- **AND** DEBE agregar JS para ejecutar `grecaptcha.execute()` al submit
#### Scenario: JavaScript de ejecución de reCAPTCHA
- **WHEN** usuario hace submit del formulario
- **THEN** JS DEBE interceptar el submit
- **AND** DEBE llamar `grecaptcha.execute(siteKey, {action: 'action_name'})`
- **AND** DEBE esperar el token (Promise)
- **AND** DEBE insertar token en input hidden
- **AND** DEBE continuar con el submit del formulario
#### Scenario: No cargar script si está deshabilitado
- **GIVEN** reCAPTCHA `is_enabled` es false
- **WHEN** se renderiza el formulario
- **THEN** NO DEBE incluir script de reCAPTCHA
- **AND** NO DEBE agregar input hidden de token
- **AND** formulario DEBE funcionar normalmente
---
### Requirement: Integración Backend (AjaxHandlers)
Los AjaxHandlers DEBEN validar el token de reCAPTCHA antes de procesar.
#### Scenario: Validación en NewsletterAjaxHandler
- **WHEN** se procesa suscripción de newsletter
- **AND** reCAPTCHA está habilitado
- **THEN** DEBE obtener `recaptcha_token` del POST
- **AND** DEBE llamar a `RecaptchaValidationService::validateSubmission()`
- **AND** si retorna false, DEBE responder con error JSON
- **AND** si retorna true, DEBE continuar procesamiento normal
#### Scenario: Validación en ContactFormAjaxHandler
- **WHEN** se procesa envío de formulario de contacto
- **AND** reCAPTCHA está habilitado
- **THEN** DEBE obtener `recaptcha_token` del POST
- **AND** DEBE llamar a `RecaptchaValidationService::validateSubmission()`
- **AND** si retorna false, DEBE responder con error JSON
- **AND** si retorna true, DEBE continuar procesamiento normal
#### Scenario: Mensaje de error por reCAPTCHA fallido
- **GIVEN** validación de reCAPTCHA falla
- **WHEN** AjaxHandler responde
- **THEN** DEBE retornar JSON con `success: false`
- **AND** mensaje DEBE ser genérico: "No se pudo verificar que eres humano. Intenta de nuevo."
- **AND** NO DEBE revelar detalles técnicos del score
#### Scenario: Inyección de dependencias en AjaxHandlers
- **WHEN** se modifican los AjaxHandlers
- **THEN** DEBEN inyectar `RecaptchaValidationService` via constructor
- **AND** NO DEBE instanciar servicios directamente
- **AND** DEBE seguir principio de Inversión de Dependencias
---
### Requirement: Panel de Administración
DEBE existir un FormBuilder para configurar reCAPTCHA.
#### Scenario: Ubicación del FormBuilder
- **WHEN** se crea el FormBuilder
- **THEN** DEBE ubicarse en `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
- **AND** namespace DEBE ser `ROITheme\Admin\RecaptchaSettings\Infrastructure\Ui`
#### Scenario: Registro en getComponents
- **WHEN** se registra el FormBuilder
- **THEN** DEBE registrarse en `getComponents()` con ID `recaptcha-settings`
- **AND** DEBE aparecer en el menú del admin dashboard
#### Scenario: Campos del formulario admin
- **WHEN** se renderiza el formulario de configuración
- **THEN** DEBE mostrar toggle para `is_enabled`
- **AND** DEBE mostrar input text para `site_key`
- **AND** DEBE mostrar input password para `secret_key`
- **AND** DEBE mostrar select para `score_threshold` con opciones 0.3, 0.5, 0.7, 0.9
- **AND** DEBE seguir Design System: gradiente #0E2337#1e3a5f, borde #FF8600
#### Scenario: FieldMapper para mapeo de campos
- **WHEN** se crea el componente admin
- **THEN** DEBE existir `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
- **AND** DEBE implementar `FieldMapperInterface`
- **AND** DEBE registrarse en `FieldMapperRegistry`
---
### Requirement: Seguridad
La implementación DEBE seguir mejores prácticas de seguridad.
#### Scenario: Almacenamiento de secret key
- **WHEN** se guarda el secret key
- **THEN** DEBE almacenarse encriptado en BD
- **AND** NO DEBE aparecer en código fuente
- **AND** NO DEBE exponerse en frontend
#### Scenario: Site key en frontend
- **GIVEN** site key es público por diseño de Google
- **WHEN** se incluye en frontend
- **THEN** PUEDE incluirse en atributo de script
- **AND** DEBE obtenerse de configuración en BD
#### Scenario: Validación de token en backend
- **WHEN** se recibe token de reCAPTCHA
- **THEN** DEBE sanitizarse con `sanitize_text_field()`
- **AND** DEBE validarse que no esté vacío
- **AND** DEBE enviarse a API de Google para verificación real
#### Scenario: No confiar solo en frontend
- **GIVEN** tokens pueden ser fabricados
- **WHEN** se valida reCAPTCHA
- **THEN** SIEMPRE DEBE verificarse con API de Google en backend
- **AND** NUNCA confiar en validación solo de frontend
---
### Requirement: Logging y Monitoreo
El sistema DEBE registrar intentos de spam bloqueados.
#### Scenario: Log de intentos bloqueados
- **WHEN** reCAPTCHA bloquea un intento
- **THEN** DEBE registrar en log de WordPress
- **AND** DEBE incluir: timestamp, IP, action, score obtenido, threshold configurado
- **AND** NO DEBE incluir datos personales del usuario
#### Scenario: Log de errores de API
- **WHEN** API de Google falla
- **THEN** DEBE registrar error en log
- **AND** DEBE incluir código de error y mensaje
- **AND** DEBE permitir diagnóstico del problema
---
### Requirement: Fallback y Resiliencia
El sistema DEBE manejar fallos graciosamente.
#### Scenario: Fail-open cuando API no responde
- **GIVEN** API de Google no responde (timeout)
- **WHEN** se intenta validar
- **THEN** DEBE permitir el envío del formulario (fail-open)
- **AND** DEBE registrar el evento en log
- **AND** NO DEBE bloquear usuarios legítimos por falla de terceros
#### Scenario: Degradación cuando reCAPTCHA deshabilitado
- **GIVEN** administrador deshabilita reCAPTCHA
- **WHEN** se envía formulario
- **THEN** DEBE procesarse normalmente
- **AND** validaciones existentes (nonce, rate limit) DEBEN seguir activas
- **AND** NO DEBE generar errores por ausencia de token
---
## Checklist de Implementación
### Archivos a Crear
- [ ] `Schemas/recaptcha-settings.json`
- [ ] `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
- [ ] `Shared/Domain/Entities/RecaptchaResult.php`
- [ ] `Shared/Application/Services/RecaptchaValidationService.php`
- [ ] `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
- [ ] `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
- [ ] `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
### Archivos a Modificar
- [ ] `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
- [ ] `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
- [ ] `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
- [ ] `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
- [ ] `functions.php` (registro DI)
- [ ] `Admin/Infrastructure/Ui/AdminDashboardRenderer.php` (registrar componente)
- [ ] `Admin/Shared/Infrastructure/FieldMapping/FieldMapperRegistry.php`
### Validaciones
- [ ] Schema tiene campos de visibilidad
- [ ] Domain no tiene dependencias de Infrastructure
- [ ] Application solo depende de interfaces de Domain
- [ ] Todos los servicios inyectados via constructor
- [ ] CSS generado via CSSGeneratorService (si aplica)
- [ ] Secret key nunca expuesto en frontend
---
## Última actualización
2025-01-08