settingModel = new Setting(); $this->captchaSettings = $this->settingModel->getCaptchaSettings(); } /** * Verify CAPTCHA response based on configured provider * * @param string|null $response CAPTCHA response token from client * @param string|null $remoteIp Remote IP address of the user * @return array ['success' => bool, 'error' => string|null, 'score' => float|null] */ public function verifyCaptcha(?string $response, ?string $remoteIp = null): array { $provider = $this->captchaSettings['provider'] ?? 'disabled'; // If CAPTCHA is disabled, always return success if ($provider === 'disabled') { return ['success' => true, 'error' => null, 'score' => null]; } // Validate that response token is provided if (empty($response)) { return ['success' => false, 'error' => 'CAPTCHA verification failed. Please try again.', 'score' => null]; } // Verify based on provider switch ($provider) { case 'recaptcha_v2': return $this->verifyRecaptchaV2($response, $remoteIp); case 'recaptcha_v3': return $this->verifyRecaptchaV3($response, $remoteIp); case 'turnstile': return $this->verifyTurnstile($response, $remoteIp); default: // Unknown provider - allow through but log error_log("Unknown CAPTCHA provider: $provider"); return ['success' => true, 'error' => null, 'score' => null]; } } /** * Verify reCAPTCHA v2 response */ private function verifyRecaptchaV2(string $response, ?string $remoteIp): array { $secretKey = $this->captchaSettings['secret_key'] ?? ''; if (empty($secretKey)) { error_log('reCAPTCHA v2 secret key is not configured'); return ['success' => false, 'error' => 'CAPTCHA is misconfigured. Please contact administrator.', 'score' => null]; } $data = [ 'secret' => $secretKey, 'response' => $response ]; if ($remoteIp) { $data['remoteip'] = $remoteIp; } $result = $this->sendVerificationRequest(self::RECAPTCHA_VERIFY_URL, $data); if ($result === null) { return ['success' => false, 'error' => 'CAPTCHA verification service unavailable. Please try again later.', 'score' => null]; } if (!isset($result['success']) || !$result['success']) { $errorCodes = $result['error-codes'] ?? []; error_log('reCAPTCHA v2 verification failed: ' . json_encode($errorCodes)); return ['success' => false, 'error' => 'CAPTCHA verification failed. Please try again.', 'score' => null]; } return ['success' => true, 'error' => null, 'score' => null]; } /** * Verify reCAPTCHA v3 response (score-based) */ private function verifyRecaptchaV3(string $response, ?string $remoteIp): array { $secretKey = $this->captchaSettings['secret_key'] ?? ''; $threshold = floatval($this->captchaSettings['score_threshold'] ?? 0.5); if (empty($secretKey)) { error_log('reCAPTCHA v3 secret key is not configured'); return ['success' => false, 'error' => 'CAPTCHA is misconfigured. Please contact administrator.', 'score' => null]; } $data = [ 'secret' => $secretKey, 'response' => $response ]; if ($remoteIp) { $data['remoteip'] = $remoteIp; } $result = $this->sendVerificationRequest(self::RECAPTCHA_VERIFY_URL, $data); if ($result === null) { return ['success' => false, 'error' => 'CAPTCHA verification service unavailable. Please try again later.', 'score' => null]; } if (!isset($result['success']) || !$result['success']) { $errorCodes = $result['error-codes'] ?? []; error_log('reCAPTCHA v3 verification failed: ' . json_encode($errorCodes)); return ['success' => false, 'error' => 'CAPTCHA verification failed. Please try again.', 'score' => null]; } // Check score $score = floatval($result['score'] ?? 0); if ($score < $threshold) { error_log("reCAPTCHA v3 score too low: $score (threshold: $threshold)"); return ['success' => false, 'error' => 'Security verification failed. Please try again or contact support.', 'score' => $score]; } return ['success' => true, 'error' => null, 'score' => $score]; } /** * Verify Cloudflare Turnstile response */ private function verifyTurnstile(string $response, ?string $remoteIp): array { $secretKey = $this->captchaSettings['secret_key'] ?? ''; if (empty($secretKey)) { error_log('Turnstile secret key is not configured'); return ['success' => false, 'error' => 'CAPTCHA is misconfigured. Please contact administrator.', 'score' => null]; } $data = [ 'secret' => $secretKey, 'response' => $response ]; if ($remoteIp) { $data['remoteip'] = $remoteIp; } $result = $this->sendVerificationRequest(self::TURNSTILE_VERIFY_URL, $data); if ($result === null) { return ['success' => false, 'error' => 'CAPTCHA verification service unavailable. Please try again later.', 'score' => null]; } if (!isset($result['success']) || !$result['success']) { $errorCodes = $result['error-codes'] ?? []; error_log('Turnstile verification failed: ' . json_encode($errorCodes)); return ['success' => false, 'error' => 'CAPTCHA verification failed. Please try again.', 'score' => null]; } return ['success' => true, 'error' => null, 'score' => null]; } /** * Send verification request to CAPTCHA provider API */ private function sendVerificationRequest(string $url, array $data): ?array { $options = [ 'http' => [ 'method' => 'POST', 'header' => 'Content-Type: application/x-www-form-urlencoded', 'content' => http_build_query($data), 'timeout' => 10 ] ]; $context = stream_context_create($options); $response = @file_get_contents($url, false, $context); if ($response === false) { error_log("Failed to connect to CAPTCHA verification service: $url"); return null; } $result = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { error_log("Failed to parse CAPTCHA verification response: " . json_last_error_msg()); return null; } return $result; } /** * Get current CAPTCHA settings for view rendering */ public function getCaptchaSettings(): array { return $this->captchaSettings; } /** * Check if CAPTCHA is enabled */ public function isEnabled(): bool { $provider = $this->captchaSettings['provider'] ?? 'disabled'; return $provider !== 'disabled'; } }