mirror of
https://github.com/lkddi/nexusphp.git
synced 2026-04-14 20:40:49 +08:00
Introduce a configurable captcha manager with drivers for image, Cloudflare Turnstile, and Google reCAPTCHA, including fallback behaviour. Refactor login, signup, complain, and related flows to use the new abstraction while simplifying the legacy image endpoint. Document captcha environment options and restore classic defaults in .env.example. Signed-off-by: Qi HU <github@spcsky.com>
144 lines
4.4 KiB
PHP
144 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Captcha\Drivers;
|
|
|
|
use App\Services\Captcha\CaptchaDriverInterface;
|
|
use App\Services\Captcha\Exceptions\CaptchaValidationException;
|
|
|
|
class TurnstileCaptchaDriver implements CaptchaDriverInterface
|
|
{
|
|
protected static bool $scriptInjected = false;
|
|
|
|
protected array $config;
|
|
|
|
public function __construct(array $config = [])
|
|
{
|
|
$this->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(
|
|
'<tr><td class="rowhead">%s</td><td align="left"><div %s></div>%s</td></tr>',
|
|
htmlspecialchars($label, ENT_QUOTES, 'UTF-8'),
|
|
$attributes,
|
|
self::$scriptInjected ? '' : '<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>'
|
|
);
|
|
|
|
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;
|
|
}
|
|
}
|