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

13 KiB

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