> For the complete documentation index, see [llms.txt](https://docs.johncms.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.johncms.com/10.0/obshie-svedeniya/captcha.md).

# Капча

Гостевая книга, регистрация, форма обратной связи, вход после нескольких неудачных попыток — везде, где форма открыта для незарегистрированных, её кто-нибудь рано или поздно начнёт заполнять скриптом. Между формой и этой проверкой стоит один объект — `Johncms\Captcha\CaptchaManager`.

Какая именно капча стоит на сайте — настройка, а не код. Картинка с кодом, которую CMS рисует сама, hCaptcha, Yandex SmartCaptcha, Google reCAPTCHA v3 или провайдер, который принёс с собой сторонний модуль — форма во всех случаях написана одинаково.

## Главное правило

**Спрашивайте `CaptchaManager`, а не конкретную капчу.**

Не создавайте `Mobicms\Captcha\Image` в контроллере, не читайте код из сессии руками и не пишите в форме `name="code"`. Всё это привязывает форму к одной капче, и сайт, который переключится на SmartCaptcha, получит форму, которая молча перестанет проверять что бы то ни было.

{% hint style="info" %}
Тот же приём, что у [хранилища файлов](/10.0/obshie-svedeniya/storage.md) и [санитайзера HTML](/10.0/obshie-svedeniya/html-sanitizer.md): вызывающий код говорит, **что** ему нужно, а чем это сделано — настройка и деталь реализации.
{% endhint %}

## Что есть в комплекте

| Ключ           | Что это                                                                               |
| -------------- | ------------------------------------------------------------------------------------- |
| `image`        | картинка с кодом, которую рисует сама CMS. Ничего не требует и работает без интернета |
| `hcaptcha`     | флажок «я не робот», с головоломкой для подозрительных посетителей                    |
| `smartcaptcha` | Yandex SmartCaptcha. То же самое; ключи у неё называются клиентским и серверным       |
| `recaptcha_v3` | Google reCAPTCHA v3: посетитель ничего не видит, сервис возвращает оценку             |

Ни одному из трёх сервисов не нужен дополнительный пакет — это токен, один HTTP-запрос и тег `<script>`. Включаются они вводом ключей в админке.

{% hint style="warning" %}
reCAPTCHA v3 не показывает посетителю ничего и не даёт ему шанса доказать, что он человек: тот, чья оценка ниже порога, просто получает отказ. Кроме того, она подгружает скрипты Google на каждую страницу с формой — это решение не только про спам, но и про посетителей сайта. Если нужен видимый флажок, берите hCaptcha или SmartCaptcha.
{% endhint %}

## Настройка

Админка: **Система → Капча** (`/admin/settings/captcha`). На странице перечислены все провайдеры, которые есть в системе, у каждого — свои поля. Переключатель слева от названия выбирает тот, который будут показывать формы.

Настройки сохраняются в `config/autoload/captcha.local.php` — то есть в ту половину конфигурации, которая не лежит в репозитории. Ключи сервисов туда и должны попадать, рядом с паролем от базы. Значения по умолчанию — в `captcha.global.php`.

```php
return [
    'captcha' => [
        'default'   => 'smartcaptcha',
        'providers' => [
            'smartcaptcha' => [
                'options' => [
                    'site_key'   => '...',
                    'secret_key' => '...',
                ],
            ],
        ],
    ],
];
```

**Провайдер без ключей не показывают посетителям.** Если выбранный сервис не настроен или его вообще никто не регистрирует (модуль удалили, в конфиге опечатка), формы вернутся к встроенной картинке. Капча, которая не может работать, не должна незаметно превращаться в отсутствие капчи.

## Капча в своей форме

### 1. Контроллер: попросить задание

```php
use Johncms\Captcha\CaptchaManager;

final readonly class FeedbackController
{
    private const CAPTCHA_SCOPE = 'feedback';

    public function __construct(private CaptchaManager $captcha)
    {
    }

    public function form(): ViewResponse
    {
        return new ViewResponse('@feedback/public/form.twig', [
            'captcha' => $this->captcha->challenge(self::CAPTCHA_SCOPE),
        ]);
    }
}
```

`challenge()` возвращает `CaptchaChallenge` — всё, что нужно шаблону: имя шаблона виджета, имя поля и его параметры (картинка, публичный ключ). Что именно там окажется, зависит от выбранного провайдера, и знать это форме не нужно.

Если капча нужна не всем — например, только гостям, — передавайте `null` и проверяйте в шаблоне:

```php
'captcha' => $this->currentUser->isValid() ? null : $this->captcha->challenge(self::CAPTCHA_SCOPE),
```

### 2. Шаблон: один компонент

```twig
{% if captcha %}
    {% include '@theme/components/captcha.twig' with {captcha: captcha, errors: errors.code|default([])} only %}
{% endif %}
```

Компонент сам подключит шаблон нужного виджета. Своей разметки — `<img src="...">` и поля ввода — в форме быть не должно.

### 3. Чтение ответа: по имени от провайдера

Имя поля у каждой капчи своё: у встроенной — `code`, у reCAPTCHA — `g-recaptcha-response`, у SmartCaptcha — `smart-token`. Поэтому имя не пишут руками:

```php
$formData = [
    'message' => $request->body('message', ''),
    'code'    => $request->body($this->captcha->fieldName(), ''),
];
```

Ключ массива (`code`) — ваш, по нему потом придёт сообщение об ошибке. Из запроса читают по `fieldName()`.

### 4. Правило валидации

```php
use Johncms\Validator\Rules\Captcha;

$rules['code'] = [new Captcha(scope: self::CAPTCHA_SCOPE)];
```

Правило само сходит к активному провайдеру. Подробности — в разделе [правил валидации](/10.0/obshie-svedeniya/validaciya/rules.md).

## Области (scope)

Область — это имя формы, которую защищает капча: `login`, `registration`, `guestbook`, `contacts`. Она нужна, чтобы две формы, открытые в двух вкладках, не затирали ответы друг друга: раньше все они держали код под одним ключом сессии, и открытая форма регистрации ломала проверку на странице входа.

Правило простое: **область, с которой выдали задание, и область, с которой его проверяют, должны совпадать.** Поэтому её выносят в константу той формы, которой она принадлежит, а не пишут строкой в двух местах.

## Ответ действует один раз

Проверка **тратит** ответ: одну и ту же картинку нельзя ответить дважды, а перехваченную отправку формы нельзя повторять, пока не пройдёт. Из этого следует одно практическое требование: **задание выдают после валидации**, чтобы форма, показанная снова из-за ошибки, несла новое:

```php
$errors = $result->getErrors();

// ...и только теперь, при отрисовке формы:
'captcha' => $this->captcha->challenge(self::CAPTCHA_SCOPE),
```

Ничего чистить в сессии после проверки не нужно.

## Почему проверка не прошла

Провайдер отвечает не «да/нет», а `CaptchaResult` с причиной, и посетитель видит разный текст:

| Причина         | Когда                                                                    |
| --------------- | ------------------------------------------------------------------------ |
| `Missing`       | ничего не отправлено                                                     |
| `Mismatch`      | ответ неверный                                                           |
| `Expired`       | задание устарело: обновилась сессия, токен уже использован или просрочен |
| `LowScore`      | сервис счёл посетителя ботом, не дав ему ничего решить (reCAPTCHA v3)    |
| `Unavailable`   | сервис не ответил или отказал                                            |
| `NotConfigured` | у провайдера нет ключей                                                  |

Различать их важно: недоступный сервис — не ошибка посетителя, и в журнале это тоже должно выглядеть по-разному.

## Дальше

* [Свой провайдер капчи](/10.0/moduli/svoi-provaider-kapchi.md) — как добавить ещё одну капчу из модуля.
