Calendar 26.125.100: карточка события без полного доступа больше не выдаёт хоста, участников и раздел
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
В Calendar 26.125.100 карточка события и планировщик перестали отдавать пользователю без полного доступа хоста, автора, участников, данные раздела, ссылки и пользовательские поля. Режет данные новый класс Bitrix\Calendar\View\EventViewData, а ответы экшенов getViewEventSlider, getCalendarViewSliderParams и updatePlanner поменялись даже при полном доступе. PHP-сигнатуры и схема БД остались прежними, так что смотреть надо свой JS поверх этих экшенов, переопределённый шаблон calendar.view.slider и собственные эндпоинты, которые отдают события из CCalendarEvent::GetList.
Проверьте, что ваш код ждёт от карточки события
CalendarAjax::getViewEventSliderAction раньше рендерил компонент, даже если раздел события не нашёлся, и отдавал в section значение null. Теперь, если CCalendarSect::GetList не вернул активный раздел, экшен отвечает ошибкой SECTION_NOT_FOUND и пустым массивом.
Дальше запись, раздел и userIndex проходят через EventViewData::prepare(). Без полного доступа entry урезан до белого списка полей, userIndex пуст, от раздела остаются ID, CAL_TYPE, PERM и обнулённые NAME, COLOR и OWNER_ID, а entryUrl и sectionName приходят пустыми строками. При полном доступе sectionName, как и раньше, берётся прямо из $_REQUEST['section_name']. В параметры компонента добавлены isRestrictedEventView и userIndex.
При полном доступе userIndex тоже похудел. Раньше экшен отдавал весь \CCalendarEvent::getUserIndex(), теперь в индексе только автор, хост, создатель встречи, владелец личного календаря и участники этого события.
CalendarEventViewForm::getCalendarViewSliderParamsAction при ограниченном просмотре выходит раньше обычного через getRestrictedViewParams() и отдаёт фиксированный набор ключей:
meetingHostсID => 0и пустыми полями, пустыеattendees;description => null,location => '',showComments => false;filesView,crmView,signedEventиguestLinkUrlравныnull,downloadIcsEnabledиguestLinkAvailableравныfalse;canEditCalendar,canAttendeeEditCalendarиcanDeleteEventравныfalse;parentIdсовпадает сIDсамой записи,isRestrictedEventView => true.
В обычной ветке появился isRestrictedEventView => false, участников экшен собирает из уже суженного userIndex, а \CCalendarEvent::getEventPermissions() вызывается раньше, до EventViewData::prepare().
Если ваш JS разбирает этот ответ сам, ограниченную карточку отличайте по isRestrictedEventView.
Не рассчитывайте на hostId в updatePlanner
CalendarAjax::updatePlannerAction теперь подставляет cur_event_id в entryId до того, как вычисляет хоста. При entryId > 0 параметр hostId из запроса игнорируется. Хоста экшен берёт из MEETING_HOST события, прочитанного через \CCalendarEvent::GetById(). Если текущий пользователь не хост, событие ему не «своё» и полного доступа к нему нет, hostId обнуляется.
Без entryId, cur_event_id и hostId хост теперь подставляется сам. Для CAL_TYPE = user это $ownerId ?: $userId (раньше просто $ownerId), для остальных типов, кроме open_event, текущий пользователь, хотя раньше hostId здесь оставался нулём.
Если переопределяли шаблон calendar.view.slider
Шаблон .default компонента bitrix:calendar.view.slider при $arParams['isRestrictedEventView'] === true выставляет $arParams['UF'] = [] и выходит до вызова CCalendarEvent::GetEventUserFields. Своей разметки, кроме плашки «событие не найдено», у шаблона нет: данные карточки уходят в JSON-ответе экшена. userIndex он берёт из $arParams['userIndex'], а CCalendarEvent::getUserIndex() остался запасным вариантом.
Копию в шаблоне сайта, которая выводит данные события, обновление не тронет. Такая копия по-прежнему вызовет CCalendarEvent::GetEventUserFields(), а он читает пользовательские поля из базы по ID события в обход белого списка. Перенесите в начало копии ранний выход по isRestrictedEventView с обнулением $arParams['UF'].
Проверьте свои эндпоинты поверх CCalendarEvent::GetList
Утечку закрыли в контроллерах. CCalendarEvent::GetList прячет поля через ApplyAccessRestrictions, а тот по-прежнему вырезает только DESCRIPTION, LOCATION, REMIND, ATTENDEE_LIST, ATTENDEES_CODES, UF_CRM_CAL_EVENT и UF_WEBDAV_CAL_EVENT. MEETING_HOST, CREATED_BY, OWNER_ID, PARENT_ID, RRULE, IMPORTANCE, MEETING и RELATIONS остаются в записи. Индекс пользователей GetList собирает через getUsersDetails() (DISPLAY_NAME, URL, AVATAR) ещё до цикла с ApplyAccessRestrictions и копит в статическом self::$userIndex, то есть без учёта прав. Раздел оба контроллера читают через CCalendarSect::GetList с 'checkPermissions' => false. Если ваш эндпоинт отдаёт результат GetList и getUserIndex() как есть, он повторяет утечку, которую закрыл релиз.
Календарь теперь строит ответ по белому списку, и у себя можно делать так же. У EventViewData три публичных статических метода:
prepare(array $entry, array $section, array $permissions, array $userIndex, int $userId): arrayвозвращаетentry,section,userIndexи флагrestricted. Без полного доступа в записи остаютсяID,NAME, даты и длительность, таймзоны и смещения,SECTION_ID/SECT_ID,CAL_TYPEиACCESSIBILITY. Остальные поля отброшены, а 25 ключей, отOWNER_ID,CREATED_BYиMEETING_HOSTдоDESCRIPTIONиRRULE, заданы заглушками. Права урезаются доview_timeиview_title. Еслиview_titleнет (у приватного личного события его нет всегда),NAMEзаменяется маской занятости.canReadFullEntry(array $entry, array $permissions, int $userId): boolотвечает «нет», еслиIS_ACCESSIBLE_TO_USER === false. В остальных случаях «да», если событие «своё» или если оно не приватное личное и в правах естьview_full.isOwnEntry(array $entry, int $userId): boolвозвращаетtrueавтору, владельцу личного календаря и участнику встречи. Права метод не проверяет, поэтому дешёвый.
В updatePlannerAction первым вызывают isOwnEntry. Комментарий в коде объясняет, что права стоят «два чтения и пакетную проверку». В своём сервисе тот же порядок пригодится для списков, а карточку одного события можно собрать по той же схеме, что и getViewEventSliderAction:
<?php declare(strict_types=1);
namespace Vendor\Agenda\Application\Service;
use Bitrix\Calendar\View\EventViewData;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;
final class EventCardService
{
public function getCard(int $eventId, int $userId): Result
{
$result = new Result();
if (!Loader::includeModule('calendar')) {
return $result->addError(new Error('Модуль calendar не установлен', 'CALENDAR_MODULE_MISSING'));
}
// календарь берёт запись через getEventForViewInterface() с датой экземпляра; для повторяющихся событий используйте его
$event = \CCalendarEvent::GetById($eventId);
if (!$event) {
return $result->addError(new Error('Событие не найдено', 'EVENT_NOT_FOUND'));
}
// раздел читаем без проверки прав, как это делает календарь: при ограниченном просмотре NAME, COLOR и OWNER_ID обнулит prepare()
$sections = \CCalendarSect::GetList([
'arFilter' => ['ID' => (int)($event['SECTION_ID'] ?? 0), 'ACTIVE' => 'Y'],
'checkPermissions' => false,
'getPermissions' => true,
]);
$section = $sections[0] ?? null;
if (!is_array($section)) {
return $result->addError(new Error('Раздел события не найден', 'SECTION_NOT_FOUND'));
}
$view = EventViewData::prepare(
$event,
$section,
\CCalendarEvent::getEventPermissions($event, $userId),
\CCalendarEvent::getUserIndex(),
$userId,
);
// entry, section, userIndex и флаг restricted для фронта
return $result->setData($view);
}
/**
* @param list<array> $events строки из \CCalendarEvent::GetList()
* @return array<int, int> ID события => ID хоста, 0 если хоста показывать нельзя
*/
public function getVisibleHostIds(array $events, int $userId): array
{
$hosts = [];
foreach ($events as $event) {
// права стоят дорого, поэтому сначала дешёвая проверка «своего» события
$visible = EventViewData::isOwnEntry($event, $userId)
|| EventViewData::canReadFullEntry(
$event,
\CCalendarEvent::getEventPermissions($event, $userId),
$userId,
);
$hosts[(int)$event['ID']] = $visible ? (int)($event['MEETING_HOST'] ?? 0) : 0;
}
return $hosts;
}
}
Не берите название события из pull
Комментарий в view-event-slider.js говорит, что поля pull-сообщения не режутся по правам получателя. Отправку pull релиз не менял, поменялась только карточка ViewEventSlider. При ограниченном просмотре она игнорирует pullData.NAME и после перезапроса берёт name из ответа сервера. Строка this.name = newData.name появилась впервые, до этого название из ответа сервера в карточке не обновлялось вовсе.
В урезанной записи PARENT_ID заменён на собственный ID, так что связь с родительской встречей из записи не видна. Поэтому сопоставление pull по PARENT_ID в JS дополнили сравнением по ID в новом методе isPullOfCurrentEntry(fields). Ограниченную карточку JS узнаёт по параметру isRestrictedEventView.
Если ваш интерфейс подписан на pull календаря, берите из сообщения только факт изменения, а название и остальные поля перезапрашивайте через контроллер. Код отправки подтверждает комментарий: Util::addPullEvent() шлёт edit_event с полными fields ещё и в тег calendar-planner-<ID>, а updatePlannerAction подписывает на этот тег каждого, кто открыл планировщик с этим пользователем. На стенде это не проверяли.
Мелочи и находки
- Маску названия в
CCalendarEvent::ApplyAccessRestrictionsиGetAccessibilityForUsersстроит новый публичный статическийCCalendarEvent::getBusyMaskedName(array $event): string, он возвращает[<фраза доступности>]. При пустомACCESSIBILITYподставляетсяBUSY, раньше читался ключEC_ACCESSIBILITY_без суффикса. На практике это касается толькоGetAccessibilityForUsers, потому чтоApplyAccessRestrictionsи раньше заранее ставил'busy'. - В
EventViewDataесть приватная копияgetBusyMaskedName, идентичная публичной. По докблоку класс держит правило у себя и не полагается на вызывающий код. MEETINGиRELATIONSв урезанной записи заданы какnew \stdClass(), поэтому в JSON приходят пустым объектом{}.- Если при ограниченном просмотре в
updatePlannerActionпришли пустыеcodes, экшен добавит код'U' . $hostIdс нулевым хостом, то естьU0. Текущего пользователя он в список не добавит, потому чтоcodesуже не пуст (если не переданadd_cur_user_to_list = 'Y'). Как это выглядит в интерфейсе, по диффу не видно. - URL создателя встречи шаблон по-прежнему строит из
$meetingCreator["PATH_TO_USER"], то есть путь берётся из массива пользователя вместо$arParams. Релиз добавил к нему только?? '', так же как кPATH_TO_USERв$arParams. - Изменения в
bitrix/components/bitrix/calendar.view.slider/...иbitrix/js/calendar/eventviewform/...побайтно совпадают с копиями вbitrix/modules/calendar/install/.... Бандлыdist/*.bundle.js,*.min.jsи*.mapпересобраны из того же исходника. - Deprecated в релизе нет, схема БД не менялась. Версия выросла с 26.125.0,
VERSION_DATEу новой 2026-09-24.