FetchIt 4.0.0 - один пакет для MODX 2 и 3 и защита от спама из коробки

Долгожданное обновление: три года компонент почти не развивался, и это было заметно. В четвёртой версии — один пакет для MODX 2.8 и MODX 3, защита от спама из коробки, закрытые уязвимости и новые примеры в документации.
Привет форум!

Последний раз я писал про FetchIt в 2023 году, когда вышла версия 3.0.0-pl для MODX 3. С тех пор компонент жил двумя линиями: 1.* для MODX 2 и 3.* для MODX 3. Работал, но почти не развивался. Этим летом я наконец-то за него взялся, и в итоге вышла версия 4.0.0. Ниже рассказываю, что к ней привело и что изменилось.

FetchIt 4

С чего всё началось


Сначала накопились баги. На PHP 8.3 формы падали с фатальной ошибкой Serialization of 'PDO' is not allowed, сообщения об успешной отправке через AJAX не показывались, на PHP 8 сыпались предупреждения про $_SESSION. Тут очень помог @Иван Бочкарев: прислал исправления для обеих веток и настроил выпуск релизов через GitHub Actions. Так вышли 1.1.3 и 3.1.4.

Потом на форуме появилась статья про спам в формах AjaxForm и FetchIt. Суть простая: бот один раз открывает страницу, а дальше сколько угодно шлёт запросы прямо в action.php. Меня спросили, решает ли это FetchIt на MODX 2.8. Честно? Нет, не решал.

Иван предложил отправлять с формой токен, сразу для обеих веток. Пока я это ревьюил, вылезло несколько неприятных вещей. На сайтах без анонимных сессий формы с защитой просто переставали работать. А отказ из плагина через return false на самом деле ничего не отменял, MODX его только записывал в журнал. Стало понятно, что защиту надо делать по-другому. И делать её два раза, отдельно для 1.x и для 3.x, мне совсем не хотелось.

Так и появилась четвёртая версия: один пакет для MODX 2.8 и MODX 3 и защита от спама, которая включена по умолчанию.

Один пакет вместо двух


FetchIt 4 ставится и на MODX 2.8, и на MODX 3. Поверх 1.x на MODX 2 и поверх 3.x на MODX 3 он встаёт обычным обновлением через Менеджер пакетов, системные настройки и чанки остаются как есть. Вызовы сниппета на страницах менять не нужно.

Прежний API работает на обеих версиях. В своих сниппетах теперь проще всего брать FetchIt так:

$FetchIt = FetchIt::service($modx);
Вызовы из 1.x ($modx->getService(...)) и из 3.x ($modx->services->get('FetchIt')) возвращают тот же объект, класс FetchIt\FetchIt тоже на месте. Чанки на Fenom и через @FILE работают через pdoTools на обеих версиях.

И ещё одна вещь, которая раньше раздражала: на MODX 3 FormIt не ставился вместе с FetchIt. Теперь ставится, а если скачать его не получилось, установщик прямо напишет почему.

Защита от спама


Это главное в релизе. Каждая форма FetchIt теперь защищена без всякой настройки, причём и при отправке через FetchIt, и при обычной отправке без JavaScript. Проверки проходят до FormIt или вашего сниппета:

  • Одноразовый токен. В форму добавляется скрытое поле fetchit_token с подписью. Сессия для него не нужна, так что сайты без анонимных сессий тоже работают. Следующий токен приходит в ответе, поэтому форму можно отправлять повторно без перезагрузки.
  • Время заполнения. Форма, отправленная быстрее чем через 3 секунды после вывода страницы, отклоняется. Человек, который исправил поле после ошибки, отказ не получит: время считается от того же вывода.
  • Ловушка. Скрытое поле со случайным для каждой установки именем. Бот, который его заполнил, получает «успех», но форма не обрабатывается и письмо никуда не уходит.
  • Лимит. По умолчанию не больше 10 отправок одной формы с одного адреса за 10 минут. Если сайт за прокси или CDN, укажите их адреса и заголовок с IP посетителя, иначе лимит станет общим на весь сайт.
Служебные поля убираются из $_POST до FormIt, так что в письма они не попадают. А если страница долго была открыта или пришла из кеша, FetchIt сам отправит форму ещё раз с новым токеном, посетитель ничего не заметит.

Proof-of-work и капча

Если спам всё равно проходит, есть ещё два уровня. Оба выключены по умолчанию.

Первый это proof-of-work: перед отправкой браузер решает небольшую задачку на SHA-256. Считать он начинает, как только посетитель зашёл в форму, так что к нажатию кнопки всё обычно уже готово. А вот боту каждая отправка стоит процессорного времени. При 16 битах компьютеру нужно около трети секунды, телефону в несколько раз больше.

Второй это капча: Cloudflare Turnstile, Google reCAPTCHA v3 или Яндекс SmartCaptcha. Выбираете сервис в fetchit.captcha, указываете два ключа, а скрипт сервиса FetchIt подключит сам. Ответ капчи проверяется на сервере последним, так что боты без токена до сервиса даже не доходят.

Свои правила

Появилось событие OnFetchItBeforeProcess. Плагин получает поля формы и может отклонить отправку:

// Плагин на событие OnFetchItBeforeProcess
$email = isset($fields['email']) && is_string($fields['email']) ? $fields['email'] : '';
if (preg_match('/@(mailinator|tempmail)\./i', $email)) {
    $modx->event->output('Одноразовые адреса не принимаются.');
}
Обратите внимание: отклоняет именно $modx->event->output(). Строку из return MODX только запишет в журнал. Как раз на этом споткнулась первая версия защиты.

Безопасность


Заодно я закрыл две уязвимости, из-за которых форму FetchIt можно было обработать в обход проверок. Подробности есть в changelog. Если FetchIt стоит у вас на сайте с важными формами, обновиться стоит в любом случае.

Свои уведомления вместо Notyf


Раньше настройка fetchit.frontend.default.notifier подключала библиотеку Notyf. Теперь у FetchIt свои уведомления, и без лишних файлов:

Встроенные уведомления FetchIt

  • Программы экранного доступа их зачитывают: ошибку сразу, успех в свою очередь.
  • Пока на уведомлении курсор или фокус, оно не закрывается.
  • Одновременно видно не больше трёх.
  • Цвета задаются CSS-переменными, по умолчанию это пастельные зелёный и красный из Tailwind CSS 4.
  • Если на сайте Content-Security-Policy, стили берут nonce скрипта FetchIt.
На телефоне и в тёмной теме это выглядит так:

Уведомления на телефоне

Уведомления в тёмной теме

Скрипт


  • Собирается в синтаксис ES2019, так что работает во всех актуальных браузерах. Минифицированная версия весит около 7 КБ в gzip.
  • Рядом со скриптом лежат типы для TypeScript, fetchit.d.ts: FetchIt, экземпляры форм и detail каждого события.
  • Раньше при сбое сети или чужом ответе (страница с ошибкой PHP, JSON от файрвола) ошибка уходила только в консоль, и посетитель ничего не видел. Теперь он получает сообщение, а fetchit:error срабатывает и в этом случае.
  • fetchit:success можно отменить, тогда поля останутся заполненными.
  • Повторное нажатие, пока идёт запрос, игнорируется, а два одинаковых вызова сниппета на странице больше не отправляют форму дважды.

Что ещё исправлено


  • Форма без JavaScript не показывала сообщение об успехе или ошибке, если в чанке стояли фильтры на плейсхолдерах FormIt. Да, как в чанке-примере.
  • Скрипт не подключался на сайтах без анонимных сессий и когда тег head был с атрибутами или написан заглавными.
  • Поля, отключённые в разметке, после отправки становились активными.
  • Несуществующий набор параметров сниппета ронял action.php на PHP 8.
Полный список в changelog, он лежит в пакете и на GitHub.

Что может задеть при обновлении


  • В форму добавляются служебные поля защиты. Если у вас свой JS вместо встроенного скрипта, он должен отправлять fetchit_token и брать следующий токен из заголовка X-FetchIt-Token.
  • Notyf больше не подключается. Стили для .notyf__toast и вызовы new Notyf() придётся поменять или подключить Notyf самим.
  • Обрабатывающий сниппет получает только отправленную форму: $_POST и файлы, без GET-параметров и cookies.
  • fetchit:error срабатывает и при сбое запроса, тогда detail.response равен null.
  • Сниппет и плагин FetchIt при обновлении заменяются, так что правки в их коде пропадут.
Если у вас кешируется вся страница (nginx, CDN, плагины статического кеша), всем посетителям уходит один и тот же токен. Через FetchIt это работает за счёт повторной отправки, но страницы с формами лучше из такого кеша исключить. А на сайте для разработки удобно поставить fetchit.protection.min_time и fetchit.protection.rate_limit в 0.

Примеры в документации


Документацию тоже перебрал. Примеры уведомлений и модальных окон переписал под четвёртую версию, а заброшенные библиотеки (NOTY, toastr, iziToast, tingle.js и другие) убрал. Добавил раздел готовых сценариев: индикатор отправки, благодарность вместо формы, редирект, маска телефона, загрузка файлов, цели в Метрике и GA4, заявки в Telegram. Модальных окон стало больше: нативные dialog и popover, a11y-dialog, UIkit и Fancybox. А для проверки полей теперь есть примеры на Zod, Valibot, validator.js и силами самого браузера, без библиотек. Код можно брать как есть.

Требования


  • MODX 2.8 или MODX 3.
  • PHP 7.4+.
  • FormIt, если формы обрабатывает он. Ставится вместе с FetchIt.
  • pdoTools, если чанки на Fenom или в файлах.

Заключение


Все подробности про настройки, события и переход с прошлых версий есть в README на GitHub. Если после обновления что-то сломалось или спам всё равно пролезает, пишите в комментариях или создавайте issue.

Отдельное спасибо @Иван Бочкарев за помощь с обеими ветками.

Спасибо всем за внимание и мира над головой.

Баха Волков
Баха Волков
3 часа назад
modx.pro
21

Комментарии: 0

Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
0