validateSubmission($token, 'newsletter_submit'); * ``` * * @package ROITheme\Shared\Application\Services */ final class RecaptchaValidationService { private const COMPONENT_NAME = 'recaptcha-settings'; public function __construct( private RecaptchaValidatorInterface $validator, private ComponentSettingsRepositoryInterface $settingsRepository ) {} /** * Validar un envio de formulario con reCAPTCHA * * @param string $token Token de reCAPTCHA del frontend * @param string $action Accion a validar (ej: 'newsletter_submit') * @return bool True si la validacion pasa, false si falla */ public function validateSubmission(string $token, string $action): bool { $settings = $this->getSettings(); // Si reCAPTCHA esta deshabilitado, permitir siempre if (!$this->isEnabled($settings)) { return true; } // Validar que tengamos las credenciales $secretKey = $this->getSecretKey($settings); if (empty($secretKey)) { $this->log('reCAPTCHA: Secret key no configurada, permitiendo envio'); return true; } // Si no hay token y fail-open esta habilitado, permitir if (empty($token)) { if ($this->isFailOpen($settings)) { $this->log('reCAPTCHA: Token vacio, fail-open habilitado'); return true; } $this->logBlocked('Token vacio', 0.0, $action); return false; } // Validar con API de Google $result = $this->validator->validate($token, $action, $secretKey); // Manejar errores de API if (!$result->isSuccess()) { if ($this->isFailOpen($settings)) { $this->log('reCAPTCHA: API error, fail-open habilitado: ' . implode(', ', $result->getErrorCodes())); return true; } $this->logBlocked('API error: ' . implode(', ', $result->getErrorCodes()), 0.0, $action); return false; } // Verificar threshold $threshold = $this->getThreshold($settings); $isValid = $result->isValid($threshold); if (!$isValid && $this->shouldLogBlocked($settings)) { $this->logBlocked('Score bajo', $result->getScore(), $action, $threshold); } return $isValid; } /** * Verificar si reCAPTCHA esta habilitado * * @return bool True si esta habilitado */ public function isRecaptchaEnabled(): bool { return $this->isEnabled($this->getSettings()); } /** * Obtener el Site Key para uso en frontend * * @return string Site key o cadena vacia si no configurado */ public function getSiteKey(): string { $settings = $this->getSettings(); return $settings['credentials']['site_key'] ?? ''; } /** * Obtener la accion configurada para newsletter * * @return string Nombre de la accion */ public function getNewsletterAction(): string { $settings = $this->getSettings(); return $settings['behavior']['action_newsletter'] ?? 'newsletter_submit'; } /** * Obtener la accion configurada para contacto * * @return string Nombre de la accion */ public function getContactAction(): string { $settings = $this->getSettings(); return $settings['behavior']['action_contact'] ?? 'contact_submit'; } /** * Obtener configuracion del componente * * @return array */ private function getSettings(): array { return $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME); } /** * Verificar si esta habilitado * * @param array $settings * @return bool */ private function isEnabled(array $settings): bool { $isEnabled = $settings['visibility']['is_enabled'] ?? false; return $isEnabled === true || $isEnabled === '1' || $isEnabled === 1; } /** * Obtener secret key * * @param array $settings * @return string */ private function getSecretKey(array $settings): string { return $settings['credentials']['secret_key'] ?? ''; } /** * Obtener threshold configurado * * @param array $settings * @return float */ private function getThreshold(array $settings): float { $threshold = $settings['behavior']['score_threshold'] ?? '0.5'; return (float) $threshold; } /** * Verificar si fail-open esta habilitado * * @param array $settings * @return bool */ private function isFailOpen(array $settings): bool { $failOpen = $settings['behavior']['fail_open'] ?? true; return $failOpen === true || $failOpen === '1' || $failOpen === 1; } /** * Verificar si se debe loguear intentos bloqueados * * @param array $settings * @return bool */ private function shouldLogBlocked(array $settings): bool { $logBlocked = $settings['behavior']['log_blocked'] ?? true; return $logBlocked === true || $logBlocked === '1' || $logBlocked === 1; } /** * Registrar mensaje en log * * @param string $message */ private function log(string $message): void { error_log('ROI Theme ' . $message); } /** * Registrar intento bloqueado * * @param string $reason Razon del bloqueo * @param float $score Score obtenido * @param string $action Accion intentada * @param float|null $threshold Threshold configurado */ private function logBlocked(string $reason, float $score, string $action, ?float $threshold = null): void { $ip = $this->getClientIP(); $thresholdStr = $threshold !== null ? ", threshold: {$threshold}" : ''; $this->log("reCAPTCHA BLOCKED: {$reason} | action: {$action} | score: {$score}{$thresholdStr} | IP: {$ip}"); } /** * Obtener IP del cliente (para logging) * * @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 'unknown'; } }