
MyFavorites
Компонент, который позволяет добавить на сайт списки избранного. Основной упор сделан на работу с анонимными пользователями, аналитикой и защитой от ботов.
Оглавление
- Основные возможности компонента
- Демонстрация
- Установка
- Быстрый старт
- Сниппеты
- Чанки и плейсхолдеры
- Системные настройки
- Синхронизация между устройствами
- Уведомления о срабатывании защиты
- Экспорт данных
- Dashboard (админка)
- Разделы в админке
- Настройка CSS стилей
- JavaScript
- REST API
- Системные события MODX
Основные возможности компонента
Избранное и списки
- Создание различных списков избранного.
- Пользовательские списки избранного (пользователь может сам создавать/переименовывать и удалять свои списки).
- Публичные ссылки на списки избранного — посторонний посетитель может просмотреть список по ссылке без авторизации, с копированием ссылки в буфер обмена одним кликом.
Идентификация и синхронизация
- Работа как с анонимными пользователями, так и только с зарегистрированными.
- Очистка сессий сайта и удаление кук пользователем не влияют на список избранного у анонимных пользователей.
- Идентификация анонимного пользователя и, как следствие, его списка избранного, посетившего сайт с разных браузеров (метод не срабатывает во всех 100% случаев. На сайте должна быть подключена Google Analytics или Яндекс.Метрика).
- Привязка списка избранного анонимного пользователя к зарегистрированному пользователю при его авторизации.
- Синхронизация индикатора избранного: изменение состояния сразу у всех кнопок товара на странице и в других открытых вкладках браузера, без перезагрузки страницы.
Безопасность
- Черные списки IP.
- Защита от CSRF-атаки.
- Защита с помощью reCAPTCHA 3 и Yandex SmartCaptcha.
- Лимит на запросы для анонимных и зарегистрированных пользователей.
- Корректная работа с Cloudflare и другими reverse-proxy (доверенные адреса для
X-Forwarded-For). - Уведомления администратора при срабатывании защиты (превышение лимита запросов, провал капчи, бан по IP) — по умолчанию email, канал доставки заменяем — см. «Уведомления о срабатывании защиты».
Аналитика
- Передача данных о событиях добавления/удаления и очистки списка избранного в Google Analytics и Яндекс.Метрика.
Администрирование
- Экспорт данных (избранное, посетители) из mgr в CSV/XLSX/ODS — см. «Экспорт данных».
- Дашборд со сводной статистикой (KPI, графики активности, топ ресурсов, лента событий) — см. «Dashboard (админка)».
Фронтенд
- Быстрая кастомизация стилей через CSS переменные.
- Возможность подписываться на JS события компонента для кастомизации его работы.
- Нативный TypeScript, без внешних JS-зависимостей.
Демонстрация
Видеообзор снят на версии для MODX 2, но в основном актуален и для текущей версии для MODX 3.
Кнопки добавления в избранное на карточках товаров каталога.
Установка
Установите транспортный пакет MyFavorites через стандартный менеджер пакетов MODX (Приложения → Установщик).
Зависимости:
- pdoTools — обязательно: листинги строятся через pdoPage/pdoResources, чанки компонента рендерятся через pdoTools (Fenom).
Первичная настройка:
- Создайте страницу избранного (вызов сниппета MyFavorites) и укажите её ID в настройке
myfavorites_favorites_page_id— на неё будет вести ссылка счётчика. - Разместите кнопку MyFavorites.btn в карточке ресурса/товара и счётчик MyFavorites.counter в шапке сайта.
- Для автоматической очистки журнала действий и истёкших блокировок IP добавьте в CRON вызов скрипта раз в неделю:Срок хранения журнала задаётся настройкой
полный_путь/core/components/myfavorites/cron/clear.phpmyfavorites_log_expires.
Быстрый старт
Вызов сниппетов
Для возможности добавления/удаления в список избранного используйте сниппет MyFavorites.btn.
Для вывода количества ресурсов в списке избранного используйте сниппет MyFavorites.counter.
Для получения всех ID ресурсов, находящихся в списке избранного, используйте сниппет MyFavorites.ids.
Для вывода ресурсов, находящихся в списке избранного, используйте сниппет MyFavorites — обёртку над листинг-сниппетом (по умолчанию pdoPage с element=pdoResources), которая сама подставляет ID из списка. Если список пуст, выводится чанк MyFavorites.empty (tplEmpty) с сообщением об этом; иначе результат оборачивается в чанк MyFavorites (tplWrapper) — блок с кнопкой очистки списка и пагинацией (page.nav):
[[!MyFavorites?
&element=`msProducts`
&limit=`12`
]]Если нужен больший контроль (свой листинг-сниппет без pdoPage, кастомная пагинация и т.п.), соберите вывод вручную через MyFavorites.ids:
[[!MyFavorites.ids? &toPlaceholder=`myf.ids`]]
[[!+myf.ids:is=`-0`:then=`
[[%myfavorites_info_list_empty]]
`:else=`
[[!pdoPage?
&element=`msProducts`
&parents=`0`
&limit=`12`
&resources=`[[!+myf.ids]]`
]]
<button class="btn btn-primary" data-myfavorites-clear>[[%myfavorites_clear_list]]</button>
[[!+page.nav]]
`]]Подробную информацию по всем параметрам каждого сниппета смотрите в разделе "Сниппеты".
Стилизация
Цветовая гамма и внешний вид элементов настраиваются через CSS-переменные — см. раздел «Настройка CSS стилей».
Сниппеты
MyFavorites - Вывод ресурсов из списка избранного
Обёртка над листинг-сниппетом (по умолчанию pdoPage), которая подставляет в его параметр resources ID элементов списка избранного. Если список пуст, листинг-сниппет не вызывается — сразу выводится чанк tplEmpty. По сути — короткая замена связки MyFavorites.ids + pdoPage, показанной в разделе "Быстрый старт".
Параметры
| Имя | Описание |
|---|---|
| list | Идентификатор списка. По умолчанию: default. |
| snippet | Имя вызываемого сниппета. По умолчанию: pdoPage. |
| element | Только если snippet=pdoPage (по умолчанию) — имя вложенного листинг-сниппета, который принимает этот параметр. По умолчанию: pdoResources (для товаров miniShop2 — msProducts). |
| tplWrapper | Имя чанка-обёртки результата (параметр pdoTools). По умолчанию: MyFavorites (чанк с кнопкой очистки списка data-myfavorites-clear и пагинацией page.nav). Укажите пустую строку, чтобы вывести результат без обёртки. Плейсхолдеры — см. «Чанки и плейсхолдеры». |
| tplEmpty | Имя чанка, выводимого вместо snippet, если список пуст. По умолчанию: MyFavorites.empty (сообщение [[%myfavorites_info_list_empty]]). Плейсхолдеры — см. «Чанки и плейсхолдеры». |
| sharePageId | Только если указан tplWrapper — ID страницы, на которую будет вести публичная ссылка (list.share_url, доступная в чанке tplWrapper). Если параметр не указан, используется системная настройка myfavorites_share_page_id. Если итоговое значение равно 0, ссылка будет вести на текущую страницу. На целевой странице должен быть вызван сниппет MyFavorites.share. По умолчанию: 0. |
Остальные параметры вызова передаются в snippet как есть (parents и resources при этом сниппет всегда выставляет сам).
Пример вызова
[[!MyFavorites?
&element=`msProducts`
&limit=`12`
]]Пагинация ([[!+page.nav]]) выводится автоматически внутри дефолтного чанка tplWrapper (см. «Чанки и плейсхолдеры») — отдельно вызывать её не нужно.
Если пагинация не нужна, вызовите листинг-сниппет напрямую через snippet (аналогично MyFavorites.share):
[[!MyFavorites? &snippet=`msProducts` &limit=`12`]]MyFavorites.btn - Вывод кнопки для добавления/удаления избранного
Параметры
| Имя | Описание |
|---|---|
| id | ID ресурса, для которого нужно вернуть данные. По умолчанию — текущий ресурс. |
| list | Идентификатор списка. По умолчанию: default. |
| tpl | Имя чанка для оформления результата. По умолчанию: MyFavorites.btn. Плейсхолдеры — см. «Чанки и плейсхолдеры». |
| remove | Нужно ли удалять или перезагрузить страницу при удалении из избранного. Если нужна перезагрузка страницы, укажите 1. Если удаление HTML элемента, то укажите префикс его ID, к которому через дефис будет добавлен ID ресурса. |
| label | Метка для аналитики. Текст из этого параметра будет передан в событие Google Analytics или Яндекс.Метрика. |
| classes | Дополнительные CSS классы для HTML элемента кнопки (класс added, если ресурс уже в списке, проставляется автоматически). |
Пример вызова в чанке
[[!MyFavorites.btn? &id=`[[+id]]`]]Пример вызова в шаблоне
[[!MyFavorites.btn? &id=`[[*id]]`]]Пример вызова, когда необходимо перезагрузить страницу, может выглядеть так:
[[!MyFavorites.btn? &id=`[[*id]]` &remove=`1`]]Пример вызова, когда необходимо удалить HTML элемент
<div id="product-item-[[+id]]">
[[!MyFavorites.btn? &id=`[[+id]]` &remove=`product-item`]]
.
.
.
</div>Пример вызова, когда необходимо передать название ресурса в событие аналитики, может выглядеть так:
[[!MyFavorites.btn? &id=`[[+id]]` &label=`[[+pagetitle]]`]]MyFavorites.counter - Вывод счетчика с количеством элементов в избранном
Параметры
| Имя | Описание |
|---|---|
| id | ID страницы избранного для формирования ссылки. По умолчанию — настройка myfavorites_favorites_page_id. |
| list | Идентификатор списка. По умолчанию: default. |
| scheme | Схема формируемой ссылки (см. параметр scheme modX::makeUrl()). По умолчанию: -1 (авто). |
| tpl | Имя чанка для оформления результата. По умолчанию: MyFavorites.counter. Плейсхолдеры — см. «Чанки и плейсхолдеры». |
Пример вызова
[[!MyFavorites.counter? &id=`5`]]MyFavorites.lists - Вывод списков избранного с количеством в них элементов
Параметры
| Имя | Описание |
|---|---|
| user | ID пользователя MODX. Если не указан, используются списки текущего анонимного/авторизованного посетителя. |
| withItems | В чанке доступен массив ID элементов списка. По умолчанию 1. |
| onlyEnable | Выводить только включённые списки (enable=1). По умолчанию 1. |
| limit | Лимит выборки результатов. По умолчанию: 0. |
| offset | Пропуск результатов с начала выборки. По умолчанию: 0 |
| sortby | Сортировка выборки. По умолчанию: id. |
| sortdir | Направление сортировки. По умолчанию: ASC. |
| tpl | Имя чанка для оформления результата. По умолчанию: MyFavorites.lists. Плейсхолдеры — см. «Чанки и плейсхолдеры». |
| sharePageId | ID страницы, на которую будет вести публичная ссылка (share_url). Если параметр не указан, используется системная настройка myfavorites_share_page_id. Если итоговое значение равно 0, ссылка будет вести на текущую страницу. На целевой странице должен быть вызван сниппет MyFavorites.share. По умолчанию: 0. |
Сниппет построен на pdoTools (pdoFetch) — поддерживаются и остальные стандартные параметры pdoTools (fastMode, cacheKey и т.д.).
MyFavorites.ids - Возвращает ID элементов списка избранного
Параметры
| Имя | Описание |
|---|---|
| list | Идентификатор списка. По умолчанию: default. |
| return | Формат возвращаемых данных. Допустимые значения: data - массив ID; str - строка ID через запятую. По умолчанию: str. |
| toPlaceholder | Если не пусто, сниппет сохранит все данные в плейсхолдер с этим именем, вместо вывода на экран. |
Пример вывода списка избранных элементов
[[!MyFavorites.ids? &toPlaceholder=`myf.ids`]]
[[!+myf.ids:is=`-0`:then=`
<div class="alert alert-primary d-flex align-items-center" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"></use></svg>
<div>[[%myfavorites_info_list_empty]]</div>
</div>
`:else=`
<div class="row">
[[!pdoPage?
&element=`msProducts`
&parents=`0`
&limit=`12`
&resources=`[[!+myf.ids]]`
]]
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary" data-myfavorites-clear>[[%myfavorites_clear_list]]</button>
</div>
</div>
<div class="row">
<div class="col text-center">
[[!+page.nav]]
</div>
</div>
`]]Fenom-синтаксис
{set $ids = $_modx->runSnippet('!MyFavorites.ids')}
{if $ids != '-0'}
<div class="row">
{$_modx->runSnippet('!pdoPage', [
'element'=>'msProducts',
'parents' => 0,
'resources' => $ids,
'limit' => 12,
])}
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary" data-myfavorites-clear>{'myfavorites_clear_list' | lexicon}</button>
</div>
</div>
<div class="row">
<div class="col text-center">
{'page.nav' | placeholder}
</div>
</div>
{else}
<div class="alert alert-primary d-flex align-items-center" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"></use></svg>
<div>{'myfavorites_info_list_empty' | lexicon}</div>
</div>
{/if}MyFavorites.share - Вывод избранного по публичной ссылке
Позволяет постороннему посетителю просмотреть список избранного по значению его публичной ссылки (share_url, которую формирует сниппет MyFavorites.lists).
Параметры
| Имя | Описание |
|---|---|
| key | Значение ключа публичной ссылки. Если не передано, берётся из GET-параметра с именем из настройки myfavorites_share_key_var (по умолчанию: share). |
| output | Режим работы. list - вывод ресурсов списка через вложенный сниппет (см. snippet); ids - вернуть только ID ресурсов списка. По умолчанию: list. |
| snippet | Только если output=list. Имя сниппета, которому будут переданы ID ресурсов списка избранного (в параметре resources) для вывода. По умолчанию: pdoPage. |
| idsFormat | Только если output=ids. Формат возвращаемых данных: array - массив ID; string - строка ID через запятую. По умолчанию: string. |
| toPlaceholder | Только если output=ids. Если не пусто, сниппет сохранит ID в плейсхолдер с этим именем, вместо вывода на экран. |
| tplEmpty | Только если output=list. Имя чанка, выводимого вместо snippet, если ссылка недействительна (ключ не найден, список отключён) или список пуст. По умолчанию: MyFavorites.empty (сообщение [[%myfavorites_err_share_not_found]]). Плейсхолдеры — см. «Чанки и плейсхолдеры». |
Если output=list, а ссылка недействительна (ключ не найден, список отключён) или список пуст, сниппет сам выводит tplEmpty вместо вызова snippet. Для output=ids в этом случае возвращается -0 (или пустой массив при idsFormat=array) — сообщать об этом пользователю должен сам вызывающий код, как в примере ниже.
В режиме output=list в вызываемый snippet также передаются все остальные параметры, указанные при вызове MyFavorites.share (кроме parents и resources, которые сниппет всегда выставляет сам).
По умолчанию вызывается pdoPage — постраничный вывод с пагинацией page.nav, как в старой версии. pdoPage, в свою очередь, принимает свой параметр element — имя вложенного листинг-сниппета (по умолчанию pdoResources, для товаров miniShop2 — msProducts):
[[!MyFavorites.share?
&element=`msProducts`
&limit=`12`
]]
[[!+page.nav]]Если пагинация не нужна, вызовите нужный листинг-сниппет напрямую через snippet (это уже терминальный сниппет, а не обёртка вроде pdoPage, поэтому именно snippet, а не element):
[[!MyFavorites.share? &snippet=`msProducts` &limit=`12`]]Пример вызова (список ID для дальнейшей сборки страницы вручную)
[[!MyFavorites.share? &output=`ids` &toPlaceholder=`myf.share.ids`]]
[[!+myf.share.ids:is=`-0`:then=`
[[%myfavorites_err_share_not_found]]
`:else=`
[[!pdoPage?
&element=`msProducts`
&limit=`12`
&resources=`[[!+myf.share.ids]]`
]]
`]]Чанки и плейсхолдеры
Все чанки рендерятся через pdoTools, синтаксис — Fenom. Каждый сниппет принимает имя своего чанка параметром (tpl, tplWrapper, tplEmpty) — создайте копию дефолтного чанка и укажите её имя, чтобы изменить оформление.
MyFavorites.btn
Кнопка добавления/удаления (сниппет MyFavorites.btn, параметр tpl).
| Плейсхолдер | Описание |
|---|---|
| id | ID ресурса |
| key | Идентификатор списка (пустой — список default) |
| added | Ресурс уже в избранном (кнопке добавляется класс added) |
| remove | Значение параметра remove сниппета (атрибут data-remove) |
| label | Метка для аналитики (атрибут data-label) |
| classes | Дополнительные CSS-классы из параметра classes |
MyFavorites.counter
Счётчик-ссылка (сниппет MyFavorites.counter, параметр tpl).
| Плейсхолдер | Описание |
|---|---|
| url | URL страницы избранного |
| key | Идентификатор списка |
| count | Количество элементов в списке |
MyFavorites.lists
Перечень списков посетителя (сниппет MyFavorites.lists, параметр tpl).
| Плейсхолдер | Описание |
|---|---|
| lists | Массив списков; у каждого: key, name, items (массив ID элементов при withItems=1), share_url (публичная ссылка) |
MyFavorites
Обёртка результата листинга (сниппет MyFavorites, параметр tplWrapper) — блок с кнопкой очистки data-myfavorites-clear, share-ссылкой и пагинацией.
| Плейсхолдер | Описание |
|---|---|
| list | Массив полей списка (key, name, share_url, ...) |
| output | Готовый HTML, который вернул листинг-сниппет |
Пагинация выводится через {'page.nav' | placeholder} — это не переданный сниппетом плейсхолдер, а стандартный системный плейсхолдер page.nav, который выставляет pdoPage при рендере (доступен уже к моменту рендера обёртки). Если вызвать MyFavorites с snippet, отличным от pdoPage (без поддержки page.nav), используйте свою копию чанка без этой строки.
MyFavorites.empty
Заглушка пустого результата (сниппеты MyFavorites и MyFavorites.share, параметр tplEmpty).
| Плейсхолдер | Описание |
|---|---|
| share | Флаг контекста публичной ссылки: не пусто — «ссылка не найдена» (MyFavorites.share), пусто — «список пуст» |
Системные настройки
Настройки сгруппированы по разделам (namespace myfavorites), значения по умолчанию перенесите из старой установки самостоятельно.
Main
myfavorites_custom_favorite_lists— разрешить посетителям сайта создавать, переименовывать и удалять свои списки избранного (по умолчанию запрещено).myfavorites_lists_allowed— список ключей, доступных при выключенных пользовательских списках (списокdefaultразрешён всегда).myfavorites_sync_favorite— объединять избранное при опознании одного и того же посетителя на разных устройствах (слияние списков при авторизации или совпадении меток аналитики; выключено — устройства просто привязываются к пользователю без слияния). Подробнее — см. «Синхронизация между устройствами».myfavorites_use_ctx/myfavorites_use_lang/myfavorites_use_host— учитывать контекст / язык (cultureKey) / хост в идентификации списка избранного (для мультиконтекстных/мультиязычных/мультидоменных установок).myfavorites_favorites_page_id— ID ресурса страницы избранного (используется сниппетом MyFavorites.counter для ссылки).myfavorites_log_expires— сколько дней хранить записи журнала действий,0— не удалять (используется скриптом cron).
Security
myfavorites_access_anonymous— разрешить анонимным посетителям пользоваться избранным.myfavorites_enable_blacklist— отклонять не-GET запросы REST API с забаненных IP (my_favorite_bans).myfavorites_enable_csrf— проверять CSRF-токен на не-GET запросах.myfavorites_enable_rate_limit,myfavorites_rate_limit_max_attempts,myfavorites_rate_limit_decay— лимит запросов на действие с одного IP за период.myfavorites_trusted_proxies— список IP доверенных reverse-proxy/балансировщиков через запятую; только тогда заголовкуX-Forwarded-Forдоверяют для определения реального IP клиента.
reCAPTCHA 3
myfavorites_enable_recaptcha,myfavorites_recaptcha_public_key,myfavorites_recaptcha_secret_key,myfavorites_recaptcha_score,myfavorites_recaptcha_hidden,myfavorites_recaptcha_reg_api_script.
Yandex SmartCaptcha
myfavorites_enable_ya_smartcaptcha,myfavorites_ya_smartcaptcha_client_key,myfavorites_ya_smartcaptcha_server_key,myfavorites_ya_smartcaptcha_verify_actions— список действий (через запятую) для которых используется проверка; пусто — для всех действий.
Notifications
myfavorites_enable_notification— уведомлять администратора при срабатывании защиты (превышение лимита запросов, провал капчи, бан по IP).myfavorites_notification_alerts— типы срабатываний, о которых уведомлять, через запятую:rate_limit,captcha,banned.myfavorites_notification_emails— адреса получателей через запятую (используется дефолтным каналом доставки — email).myfavorites_notifier_class— класс канала доставки уведомлений, реализующийMyFavorites\Notification\NotifierInterface. По умолчанию:MyFavorites\Notification\EmailNotifier; в комплекте также естьMyFavorites\Notification\TelegramNotifier.myfavorites_notification_throttle— минимальный интервал (сек.) между двумя уведомлениями одного типа с одного IP-адреса.myfavorites_telegram_bot_token,myfavorites_telegram_chat_ids— токен бота и список ID чатов/каналов через запятую (используютсяTelegramNotifier, только если он указан вmyfavorites_notifier_class).
Sharing
myfavorites_share_key_length— длина случайно генерируемого ключа публичной ссылки на список избранного (минимум 8).myfavorites_share_page_id— ID страницы, на которую ведёт публичная ссылка.0— текущий ресурс. На целевой странице должен быть вызван сниппет MyFavorites.share.myfavorites_share_key_var— имя GET-параметра, содержащего ключ публичной ссылки.myfavorites_share_copy_link— перехватывать клик по ссылке "поделиться" в JS-виджете и копировать её в буфер обмена вместо перехода.
Frontend
myfavorites_css,myfavorites_js— пути к скомпилированным ассетам фронтенда ({assets_url}поддерживается), пусто — не регистрировать.myfavorites_yandex_metrika,myfavorites_google_analytics— отправлять события виджета (add/remove/...) в Яндекс.Метрику / Google Analytics.myfavorites_tab_sync— передавать изменения избранного в другие открытые вкладки браузера (см. «Синхронизация индикатора избранного»).
Export
myfavorites_export_handlers— JSON-реестр доступных отчётов экспорта по гридам mgr, см. «Экспорт данных».myfavorites_export_format— формат файла экспорта по умолчанию:csv,xlsxилиods.myfavorites_export_save_to_file— сохранять экспорт файлом на сервере (вmyfavorites_export_path) вместо скачивания в браузер.myfavorites_export_path— путь для сохранения файла при включённойmyfavorites_export_save_to_file(поддерживает{core_path}и т.п.).myfavorites_export_add_field_names— добавлять строку с названиями колонок первой строкой файла.
Право mgr, дающее доступ к экспорту: myfavorites_export_execute.
Синхронизация между устройствами
Компонент идентифицирует посетителя без регистрации:
- основной идентификатор — собственная cookie (
_mfuid), дублируемая в localStorage: очистка cookies сайта не теряет избранное — при следующем визите идентификатор восстанавливается; - дополнительно — метки систем аналитики, если они подключены на сайте:
_ym_uid(Яндекс.Метрика) и_ga(Google Analytics). По ним один и тот же человек опознаётся в разных браузерах/устройствах (метод срабатывает не в 100% случаев); - при авторизации анонимное избранное привязывается к пользователю MODX — и далее узнаётся на любом устройстве, где он авторизуется.
Когда два ранее независимых «устройства» опознаны как один посетитель (авторизация под тем же пользователем, совпадение меток аналитики), поведение зависит от настройки myfavorites_sync_favorite:
- включена — выполняется одноразовое слияние: списки обоих устройств объединяются без потерь (одноимённые списки сливаются, уникальные переносятся), дальше оба устройства работают с общим избранным. Слияние можно отменить или обработать событиями
OnMyFavoritesBeforeMergeUsers/OnMyFavoritesMergeUsers(см. «Системные события MODX»); - выключена — старое поведение: устройство привязывается к пользователю, но уже накопленные на разных устройствах списки не объединяются.
Изменения, сделанные на одном устройстве, появляются на другом при следующем обращении к сайту — без ручного обновления кэшей.
Уведомления о срабатывании защиты
При срабатывании защиты REST API — превышении лимита запросов, провале капчи (reCAPTCHA/Yandex SmartCaptcha) или запросе с забаненного IP — компонент всегда пишет запись в журнал действий (лог, действие «Срабатывание защиты»), независимо от того, включены ли уведомления. CSRF и запрет анонимного доступа не считаются срабатыванием защиты и записи/уведомления не создают — это штатная блокировка настройками, а не признак атаки или абьюза.
Если включена настройка myfavorites_enable_notification, дополнительно администратору отправляется уведомление — по умолчанию на email (myfavorites_notification_emails), не чаще одного уведомления на пару «тип срабатывания + IP-адрес» за период, заданный myfavorites_notification_throttle (чтобы не заваливать почту при атаке; сама запись в журнале действий при этом создаётся на каждое срабатывание без ограничений). Какие именно типы срабатываний уведомлять — задаётся списком в myfavorites_notification_alerts (rate_limit, captcha, banned).
Канал доставки уведомлений заменяем: реализуйте MyFavorites\Notification\NotifierInterface (notify(string $subject, string $message, array $context): bool) своим классом и укажите полное имя класса в настройке myfavorites_notifier_class — правки кода компонента не требуются. Конструктор канала доставки принимает ровно (modX $modx, Tools $tools) — специфичные для канала параметры (токены, ID чатов и т.п.) читаются из системных настроек внутри notify(), а не через конструктор.
В комплекте, помимо EmailNotifier, есть MyFavorites\Notification\TelegramNotifier — отправляет уведомление через Telegram Bot API (sendMessage) во все чаты/каналы, перечисленные в myfavorites_telegram_chat_ids, используя токен myfavorites_telegram_bot_token. Чтобы включить: создать бота через @BotFather, получить токен, добавить бота в нужный чат/канал и узнать его chat_id, заполнить обе настройки и указать myfavorites_notifier_class = MyFavorites\Notification\TelegramNotifier.
Экспорт данных
В mgr, на страницах гридов избранного и посетителей, доступен экспорт данных в CSV, XLSX или ODS.
Доступные отчёты:
- Избранное — экспорт по гриду избранного (Favorites): видимые в гриде колонки, с учётом активных фильтров и поиска; если в гриде выбраны конкретные строки — экспортируются только они.
- Посетители — то же самое по гриду посетителей (Users).
Формат файла по умолчанию, режим «сохранить на сервере / скачать в браузер» и вывод строки заголовков задаются системными настройками (см. «Системные настройки → Export»); состав и порядок колонок, фильтры и выбор конкретных строк — прямо в гриде на момент клика по экспорту.
Список отчётов, доступных для каждого грида, задаётся настройкой myfavorites_export_handlers — JSON-объект вида:
{
"favorites": ["MyFavorites\\Export\\Reports\\FavoritesReport"],
"users": ["MyFavorites\\Export\\Reports\\UsersReport"],
"lists": [],
"logs": [],
"bans": []
}Ключ — топик (соответствует гриду), значение — список полных имён PHP-классов отчётов (реализуют интерфейс MyFavorites\Export\ExportReport). Отчёты для гридов Lists/Logs/Bans пока не реализованы — пустые массивы зарезервированы под будущее расширение. Добавление стороннего отчёта не требует правки кода компонента — достаточно реализовать ExportReport и указать полное имя класса в настройке.
Доступ к экспорту в mgr контролируется правом myfavorites_export_execute.
Сторонние плагины могут вмешаться в процесс экспорта через системные события OnMyFavoritesBeforeExport/OnMyFavoritesExport (весь прогон целиком), OnMyFavoritesExportPrepareQuery (доработать запрос выборки данных) и построчные OnMyFavoritesExportBeforePrepareRow/OnMyFavoritesExportAfterPrepareRow/OnMyFavoritesExportBeforeWriteRow/OnMyFavoritesExportAfterWriteRow (изменить или отфильтровать конкретную строку) — см. «Системные события MODX».
Dashboard (админка)
Первая вкладка страницы дополнения — сводный дашборд: KPI-плитки, динамика активности (7/30/90 дней), топ-10 ресурсов, распределения действий/типов пользователей/браузеров и лента последних событий. Видимость блоков настраивается через меню «Блоки» (запоминается на пользователя менеджера). Клик по названию ресурса в «Топ-10 ресурсов» открывает его на редактирование в новой вкладке.
В метриках, графиках и ленте учитываются только успешные действия; неуспешные попытки (отклонённые лимитом запросов, ошибки) видны в гриде «Лог» с фильтром по статусу. Глубина рядов «Добавления/Удаления» ограничена ретеншном логов (myfavorites_log_expires); «Новые пользователи» считаются по таблице пользователей и от ретеншна не зависят.
Отдельный блок «Безопасность» сводит срабатывания защиты REST API (см. «Уведомления о срабатывании защиты»): KPI и распределение по типам (rate_limit, капча, бан IP), «Топ IP по алертам» с кликом для создания бана по выбранному адресу, динамику «Алерты по дням» и ленту последних алертов. Он читает записи журнала «Срабатывание защиты», которые остальной дашборд намеренно не учитывает.
Право доступа: myfavorites_dashboard.
Разделы в админке
Управление данными компонента доступно на вкладках страницы дополнения в менеджере MODX.
Пользователи — все посетители (анонимные и авторизованные): идентификаторы (IP, Yandex Client ID, Google Client ID), браузер, число позиций в избранном; фильтры и поиск.
Избранное — все позиции избранного по всем посетителям, с группировкой по ресурсам и фильтрами.
Списки — все списки избранного (default и пользовательские): идентификатор, публичный ключ, контекст, язык и хост.
Бан лист — управление чёрным списком IP: добавление, редактирование и удаление, временные и перманентные баны.
Лог — журнал действий (включая «Срабатывание защиты») со статусом, IP и агентом.
Настройка CSS стилей
Изменение стилей элементов осуществляется через CSS переменные.
Быстрое изменение цветовой гаммы элементов:
- --myf-primary-color
- --myf-secondary-color
:root {
--myf-primary-color:red;
--myf-secondary-color:silver;
}Тонкая настройка
Кнопка добавления/удаления в избранное
- --myf-btn-size - размер кнопки
- --myf-btn-icon-color - цвет иконки
- --myf-btn-added-icon-color - цвет иконки, когда ресурс добавлен в избранное
- --myf-btn-icon-color-hover - цвет декоративного эффекта при наведении (модификатор
.glow-hoverвclasses) - --myf-btn-icon - svg иконка в base64
- --myf-btn-added-icon - svg иконка виде base64, когда ресурс добавлен в избранное
- --myf-btn-animate - анимация при клике для добавления в избранное
- --myf-btn-transition - transition
Счетчик количества в избранном
--myf-counter-size - размер иконки счётчика
--myf-counter-icon-color - цвет иконки
--myf-counter-icon-color-hover - цвет иконки при наведении
--myf-counter-icon - svg иконка в base64 (по умолчанию — та же, что у кнопки)
--myf-counter-transition - transition
--myf-counter-value-size - размер бейджа со значением счетчика
--myf-counter-value-offset - смещение бейджа относительно иконки
--myf-counter-value-bg - цвет фона бейджа
--myf-counter-value-color - цвет текста
--myf-counter-font-size - размер шрифта значения
--myf-counter-value-opacity - прозрачность
--myf-counter-value-opacity-hover - прозрачность при наведении
JavaScript
Виджет — самоинициализирующийся: при наличии window.MyFavoritesConfig (формирует плагин LoadWebDocument) на странице автоматически создаётся window.myFavorites. После этого генерируется CustomEvent('myfavorites:init', { detail: myFavorites }) на document — самая надёжная точка подписки для стороннего кода.
Общий список событий
- init
- beforeRequest
- successRequest
- failureRequest
- afterRequest
- add
- remove
- clearList
- createList
- renameList
- removeList
- share
- accessDenied — HTTP 401, анонимному посетителю запрещено пользоваться избранным (
myfavorites_access_anonymous) - banned — HTTP 403, IP посетителя заблокирован (
myfavorites_enable_blacklist) - tabSync — принято изменение избранного из другой вкладки браузера (см. «Синхронизация индикатора избранного»)
Синхронизация индикатора избранного
Если один и тот же товар выведен на странице несколько раз (например, в каталоге и в блоке «недавно просмотренные»), добавление/удаление из избранного обновляет состояние всех его кнопок с одинаковым ключом списка (data-list; отсутствующий или пустой атрибут равнозначен списку default).
Кроме того, изменения избранного (add/remove/очистка списка) автоматически передаются в другие открытые вкладки этого же сайта через BroadcastChannel: там обновляются кнопки и счётчики, без перезагрузки страницы. В принимающей вкладке события add/remove не генерируются и аналитика не отправляется — вместо этого генерируется событие tabSync с сообщением { action: 'add' | 'remove' | 'clearList', id?, list, count }.
Межвкладочная синхронизация управляется системной настройкой myfavorites_tab_sync (по умолчанию включена); точечно переопределить её можно опцией tabSync в опциях конструктора при ручном создании инстанса (опции конструктора имеют приоритет над серверным конфигом). В браузерах без BroadcastChannel (Safari < 15.4) межвкладочная синхронизация тихо отключается сама; синхронизация кнопок в пределах одной страницы работает всегда.
Переопределение класса уведомлений
По умолчанию показ уведомлений происходит через window.ms3.message (MiniShop2), а при его отсутствии — через нативный alert. Для того, чтобы добавить свою реализацию, необходимо подписаться на событие init компонента и через вызов метода setNotifier передать объект своего класса уведомлений.
Пример:
class MyNotifier {
success(msg) {
console.info(msg);
}
error(msg) {
console.error(msg);
}
}
document.addEventListener('myfavorites:init', (e) => {
e.detail.setNotifier(new MyNotifier());
});Показ окна регистрации/авторизации для анонимных пользователей
Если для анонимных пользователей системной настройкой запрещено добавлять в избранное, то по умолчанию им будет показано соответствующее уведомление. Для того, чтобы для такого случая показать свое модальное окно регистрации/авторизации, необходимо подписаться на событие accessDenied, в котором реализовать всю логику.
Пример:
document.addEventListener('myfavorites:init', (e) => {
e.detail.on('accessDenied', (self, el, response) => {
// showAuthModal();
});
});Аналогично можно подписаться на событие banned, чтобы кастомизировать сообщение о блокировке IP.
Пользовательские списки избранного
Если в системных настройках пакета разрешены пользовательские списки избранного (по умолчанию запрещены), доступны два способа управления списками:
1. Декларативная разметка (данные-атрибуты) — обработчики навешиваются автоматически:
| Атрибут | Поведение |
|---|---|
data-myfavorites-list-create (форма, поле input[name=name]) | submit → создание списка; форма сбрасывается при успехе |
data-myfavorites-list-rename="key" (форма, поле input[name=name]) | submit → переименование списка key |
data-myfavorites-list-remove="key" (кнопка) | click → удаление списка key |
2. JS API (методы возвращают Promise с ответом REST — { success, message, data }):
createList(name)- создание списка избранногоrenameList(key, name)- переименование списка избранногоremoveList(key)- удаление списка избранного
Пример:
document.addEventListener('myfavorites:init', (e) => {
const myFavorites = e.detail;
document.getElementById('new-list')?.addEventListener('click', () => {
myFavorites.createList('my list').then((response) => {
console.log(response);
});
});
document.getElementById('rename-list')?.addEventListener('click', () => {
myFavorites.renameList('01hv1yjq6drnrn0hk2vmyn1wez', 'new list name').then((response) => {
console.log(response);
});
});
document.getElementById('remove-list')?.addEventListener('click', () => {
myFavorites.removeList('01hv1yjq6drnrn0hk2vmyn1wez').then((response) => {
console.log(response);
});
});
});По умолчанию, если ни на одно из событий createList/renameList/removeList/clearList нет подписчиков, виджет сам перезагружает страницу (списочную разметку рендерит сервер — сниппет MyFavorites.lists).
Своя передача данных в аналитику
Если вас не устраивает встроенная реализация (отправка в Google Analytics/Яндекс.Метрику через настройки myfavorites_google_analytics/myfavorites_yandex_metrika), выключите соответствующие настройки и реализуйте свою обработку событий:
- add
- remove
- clearList
- createList / renameList / removeList
- share
Пример:
document.addEventListener('myfavorites:init', (e) => {
e.detail
.on('add', (self, el, data, payload) => {
})
.on('remove', (self, el, data, payload) => {
})
.on('clearList', (self, el, data, payload) => {
});
});Пример обработки событий запроса:
document.addEventListener('myfavorites:init', (e) => {
e.detail
.on('beforeRequest', (self, el, payload) => {
})
.on('successRequest', (self, el, response, payload) => {
})
.on('failureRequest', (self, el, response, payload) => {
})
.on('afterRequest', (self, el, payload) => {
});
});REST API
Виджет работает через публичный REST API — его же можно использовать из своего кода (мобильное приложение, кастомный фронтенд).
Базовый URL: /assets/components/myfavorites/api.php?_rest=<действие>. Ответ — JSON вида { "success": bool, "message": string, "data": {...} } (при ошибке добавляется code); исключение — GET lists, который возвращает { "results": [...], "total": N } без этой обёртки. Запросы должны отправляться с cookies (credentials: 'include').
Правила:
- GET-запросы доступны без проверок.
- Не-GET запросы требуют CSRF-токен (при включённой
myfavorites_enable_csrf): получите его черезGET config(data.csrf) и передайте полемcsrfв теле запроса. Также к не-GET запросам применяются (по настройкам) чёрный список IP, rate limit и капчи. - ID и прочие данные передаются в JSON-теле запроса, а не сегментом в
_rest. - Коды ошибок:
401— анонимному посетителю запрещено пользоваться избранным (myfavorites_access_anonymous),403— запрос отклонён проверками (невалидный CSRF-токен, бан IP, превышение rate limit, провал капчи).
Действия:
| Действие | Метод | Тело запроса | Описание |
|---|---|---|---|
config | GET | — | Конфиг виджета: csrf-токен, лексикон, ключи капч и т.д. |
favorites | POST | {id, list?, csrf} | Добавить ресурс id в список (list пуст — default); в data.count — новое количество |
favorites | DELETE | {id, list?, csrf} | Удалить ресурс из списка |
lists | GET | — | Списки текущего посетителя: {results: [...], total} |
lists | POST | {name, csrf} | Создать пользовательский список (требует myfavorites_custom_favorite_lists) |
lists | PUT | {key, name, csrf} | Переименовать список |
lists | DELETE | {key, csrf} | Удалить список |
lists/clear | POST | {list?, csrf} | Очистить список |
user/recovery | POST | {csrf} | Восстановить посетителя по cookie (используется виджетом автоматически) |
Пример:
const api = '/assets/components/myfavorites/api.php';
const config = await fetch(`${api}?_rest=config`, { credentials: 'include' }).then((r) => r.json());
const response = await fetch(`${api}?_rest=favorites`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 15, list: '', csrf: config.data.csrf })
}).then((r) => r.json());Системные события MODX
Все обработчики событий вызываются через Tools::invokeEvent() (обёртка над modX::invokeEvent()): *OnBefore*-события могут отменить операцию (см. документацию по системным событиям MODX — OnBeforeAdd), а также переопределить rid/listId через возвращаемое значение (для OnMyFavoritesBeforeAdd).
OnMyFavoritesBeforeAdd - Запускается перед добавлением ресурса в избранное.
Параметры
| Имя | Описание |
|---|---|
| rid | ID ресурса |
| userId | ID пользователя |
| listId | ID списка |
| list | Данные списка (массив полей MyFavoriteLists) |
OnMyFavoritesAdd - Запускается после добавления ресурса в избранное.
Параметры
| Имя | Описание |
|---|---|
| rid | ID ресурса |
| userId | ID пользователя |
| listId | ID списка |
| list | Данные списка (массив полей MyFavoriteLists) |
| count | Количество ресурсов в избранном после операции |
OnMyFavoritesBeforeRemove - Запускается перед удалением ресурса из избранного.
Параметры
| Имя | Описание |
|---|---|
| rid | ID ресурса |
| userId | ID пользователя |
| listId | ID списка |
| list | Данные списка (массив полей MyFavoriteLists) |
OnMyFavoritesRemove - Запускается после удаления ресурса из избранного.
Параметры
| Имя | Описание |
|---|---|
| rid | ID ресурса |
| userId | ID пользователя |
| listId | ID списка |
| list | Данные списка (массив полей MyFavoriteLists) |
| count | Количество ресурсов в избранном после операции |
OnMyFavoritesBeforeClear - Запускается перед очисткой списка избранного.
Параметры
| Имя | Описание |
|---|---|
| userId | ID пользователя |
| listId | ID списка |
| list | Данные списка (массив полей MyFavoriteLists) |
OnMyFavoritesClear - Запускается после очистки списка избранного.
Параметры
| Имя | Описание |
|---|---|
| userId | ID пользователя |
| listId | ID списка |
| list | Данные списка (массив полей MyFavoriteLists) |
OnMyFavoritesBeforeCreateUser - Запускается перед созданием посетителя MyFavorites.
Параметры
| Имя | Описание |
|---|---|
| muid | ID пользователя MODX, если в этот момент авторизирован |
OnMyFavoritesCreateUser - Запускается после создания посетителя MyFavorites.
Параметры
| Имя | Описание |
|---|---|
| user | Данные объекта MyFavoriteUsers |
OnMyFavoritesBeforeUserRecovery - Запускается перед восстановлением посетителя по куке.
Без параметров.
OnMyFavoritesUserRecovery - Запускается после восстановления посетителя по куке.
Параметры
| Имя | Описание |
|---|---|
| user | Данные объекта MyFavoriteUsers |
OnMyFavoritesBeforeCreateList - Запускается перед созданием списка избранного.
Параметры
| Имя | Описание |
|---|---|
| data | Массив полей для создания списка (до сохранения) |
OnMyFavoritesCreateList - Запускается после создания списка избранного.
Параметры
| Имя | Описание |
|---|---|
| list | Данные объекта MyFavoriteLists после сохранения |
OnMyFavoritesBeforeRenameList - Запускается перед переименованием списка избранного.
Параметры
| Имя | Описание |
|---|---|
| newName | Новое название |
| list | Текущие данные объекта MyFavoriteLists (до переименования) |
OnMyFavoritesRenameList - Запускается после переименования списка избранного.
Параметры
| Имя | Описание |
|---|---|
| list | Данные объекта MyFavoriteLists после переименования |
OnMyFavoritesBeforeRemoveList - Запускается перед удалением списка избранного.
Параметры
| Имя | Описание |
|---|---|
| data | Данные объекта MyFavoriteLists (до удаления) |
OnMyFavoritesRemoveList - Запускается после удаления списка избранного.
Параметры
| Имя | Описание |
|---|---|
| data | Данные удалённого объекта MyFavoriteLists |
OnMyFavoritesBeforeMergeUsers - Запускается перед слиянием избранного двух посетителей (см. «Синхронизация между устройствами»).
Событие может отменить слияние (см. Tools::invokeEvent() выше).
Параметры
| Имя | Описание |
|---|---|
| targetId | ID посетителя-получателя (каноническая строка) |
| sourceId | ID посетителя-источника (его списки будут перенесены) |
OnMyFavoritesMergeUsers - Запускается после успешного слияния избранного двух посетителей.
Параметры
| Имя | Описание |
|---|---|
| targetId | ID посетителя-получателя |
| sourceId | ID опустевшего посетителя-источника |
OnMyFavoritesBeforeExport - Запускается перед началом экспорта данных из mgr-грида (см. «Экспорт данных»).
Событие может отменить экспорт (см. Tools::invokeEvent() выше).
Параметры
| Имя | Описание |
|---|---|
| report | Объект отчёта экспорта (реализует MyFavorites\Export\ExportReport) |
| payload | Пейлоад запроса из грида: ids (выбранные строки), fields (видимые колонки), params (фильтры/поиск) |
OnMyFavoritesExport - Запускается после успешного завершения экспорта.
Параметры
| Имя | Описание |
|---|---|
| report | Объект отчёта экспорта |
| count | Количество экспортированных строк |
OnMyFavoritesExportPrepareQuery - Запускается перед выполнением запроса на выборку данных для экспорта.
Плагин может доработать запрос, вернув изменённый объект через возвращаемое значение.
Параметры
| Имя | Описание |
|---|---|
| query | Объект запроса выборки (xPDOQuery) |
| report | Объект отчёта экспорта |
OnMyFavoritesExportBeforePrepareRow - Запускается перед форматированием очередной строки экспорта, до преобразования значений полей для вывода в файл.
Параметры
| Имя | Описание |
|---|---|
| data | Сырые данные строки (результат выборки) |
| report | Объект отчёта экспорта |
OnMyFavoritesExportAfterPrepareRow - Запускается после форматирования очередной строки экспорта.
Параметры
| Имя | Описание |
|---|---|
| data | Отформатированные данные строки, которые будут записаны в файл |
| report | Объект отчёта экспорта |
OnMyFavoritesExportBeforeWriteRow - Запускается перед записью очередной строки в файл экспорта.
Параметры
| Имя | Описание |
|---|---|
| data | Данные строки, которые будут записаны |
| report | Объект отчёта экспорта |
OnMyFavoritesExportAfterWriteRow - Запускается после записи очередной строки в файл экспорта.
Параметры
| Имя | Описание |
|---|---|
| data | Записанные данные строки |
| report | Объект отчёта экспорта |
OnMyFavoritesBeforeSecurityAlert - Запускается перед регистрацией срабатывания защиты (превышение rate-limit, провал капчи, бан по IP — см. «Уведомления о срабатывании защиты»).
Событие может отменить регистрацию алерта целиком — не будет ни записи в audit-лог, ни уведомления (см. Tools::invokeEvent() выше). Также может переопределить message через возвращаемое значение.
Параметры
| Имя | Описание |
|---|---|
| type | Тип алерта (rate_limit / captcha / banned) |
| message | Текст ошибки, показанный посетителю |
| ip | IP-адрес посетителя |
OnMyFavoritesSecurityAlert - Запускается после регистрации срабатывания защиты (audit-лог уже записан, уведомление отправлено или отфильтровано настройками/троттлингом).
Параметры
| Имя | Описание |
|---|---|
| type | Тип алерта (rate_limit / captcha / banned) |
| message | Текст ошибки, показанный посетителю |
| ip | IP-адрес посетителя |
| notified | true, если уведомление было фактически отправлено (не троттлилось и не отфильтровано настройками) |












