config = $config; } public function isEnabled(): bool { return !empty($this->config['site_key']) && !empty($this->config['secret_key']); } public function render(array $context = []): string { if (!$this->isEnabled()) { return ''; } $labels = $context['labels'] ?? []; $label = $labels['image'] ?? $labels['code'] ?? 'Security Check'; $theme = $this->config['theme'] ?? 'light'; $size = $this->config['size'] ?? 'auto'; if (is_string($size)) { $size = strtolower($size); } $validSizes = ['compact', 'normal', 'flexible']; if (!in_array($size, $validSizes, true)) { $size = 'auto'; } $attributes = sprintf( 'class="cf-turnstile" data-sitekey="%s" data-theme="%s"%s', htmlspecialchars($this->config['site_key'], ENT_QUOTES, 'UTF-8'), htmlspecialchars($theme, ENT_QUOTES, 'UTF-8'), $size === 'auto' ? '' : sprintf(' data-size="%s"', htmlspecialchars($size, ENT_QUOTES, 'UTF-8')) ); $markup = sprintf( '%s
%s', htmlspecialchars($label, ENT_QUOTES, 'UTF-8'), $attributes, self::$scriptInjected ? '' : '' ); self::$scriptInjected = true; return $markup; } public function verify(array $payload, array $context = []): bool { $token = trim((string) ($payload['request']['cf-turnstile-response'] ?? '')); if ($token === '') { throw new CaptchaValidationException('Captcha verification token is missing.'); } $secret = $this->config['secret_key'] ?? ''; if ($secret === '') { throw new CaptchaValidationException('Captcha secret key is not configured.'); } $data = [ 'secret' => $secret, 'response' => $token, ]; $remoteIp = $context['ip'] ?? null; if (!empty($remoteIp)) { $data['remoteip'] = $remoteIp; } $result = $this->sendVerificationRequest('https://challenges.cloudflare.com/turnstile/v0/siteverify', $data); if (!($result['success'] ?? false)) { throw new CaptchaValidationException('Captcha verification failed.'); } return true; } protected function sendVerificationRequest(string $url, array $data): array { $payload = http_build_query($data); if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, ]); $response = curl_exec($ch); if ($response === false) { $error = curl_error($ch); curl_close($ch); throw new CaptchaValidationException('Captcha verification request failed: ' . $error); } curl_close($ch); } else { $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Content-type: application/x-www-form-urlencoded\r\n", 'content' => $payload, 'timeout' => 10, ], ]); $response = file_get_contents($url, false, $context); if ($response === false) { throw new CaptchaValidationException('Captcha verification request failed.'); } } $decoded = json_decode($response, true); if (!is_array($decoded)) { throw new CaptchaValidationException('Unexpected captcha verification response.'); } return $decoded; } }