Files
3dealer/app/Rules/HCaptcha.php
T
2026-09-25 16:19:56 +03:00

67 lines
2.3 KiB
PHP

<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Verifies the `h-captcha-response` token the hCaptcha widget adds to a form.
* Use it as `'h-captcha-response' => ['required', new HCaptcha]`.
*
* Fails closed: if hCaptcha can't be reached the submission is rejected, since
* letting it through would reopen the hole this exists to close (bots making
* us send email to arbitrary addresses).
*/
class HCaptcha implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value) || $value === '') {
$fail(__('storefront.auth.captcha_failed'));
return;
}
try {
$response = Http::asForm()
->timeout(5)
->post('https://api.hcaptcha.com/siteverify', [
'secret' => config('services.hcaptcha.secret'),
'response' => $value,
// Rejects tokens solved against someone else's sitekey.
'sitekey' => config('services.hcaptcha.sitekey'),
'remoteip' => request()->ip(),
]);
} catch (ConnectionException $e) {
Log::warning('hCaptcha siteverify unreachable', ['error' => $e->getMessage()]);
$fail(__('storefront.auth.captcha_failed'));
return;
}
if (! $response->successful() || $response->json('success') !== true) {
// A bad/missing secret or sitekey would otherwise look like every
// shopper failing the captcha.
$configErrors = array_intersect((array) $response->json('error-codes'), [
'missing-input-secret',
'invalid-input-secret',
'sitekey-secret-mismatch',
'invalid-sitekey',
]);
if ($response->failed() || $configErrors) {
Log::warning('hCaptcha siteverify error', [
'status' => $response->status(),
'error-codes' => $response->json('error-codes'),
]);
}
$fail(__('storefront.auth.captcha_failed'));
}
}
}