Calendar 26.100.0: почтовые приглашения и calendar.section.update берут хоста, тип и владельца из базы
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
В Calendar 26.100.0 REST-метод calendar.section.update берёт тип и владельца секции из базы, а обработчик входящих iCal-приглашений ищет событие только среди встреч получателя и при обновлении встречи читает её хоста из базы. В участники встречи теперь можно звать команды из модуля humanresources, а JS-метод EntryManager.setMeetingStatus() научился отклонять промис. PHP-сигнатуры совместимы, схема БД не менялась.
Проверьте интеграции с calendar.section.update
Раньше CCalendarRestService::SectionUpdate собирал модель для проверки права ACTION_SECTION_EDIT из параметров запроса: SectionModel::createFromId($id)->setType($type)->setOwnerId($ownerId). Те же type и ownerId уходили в поля для сохранения, так что через REST можно было переписать секции тип и владельца.
Теперь метод читает CAL_TYPE и OWNER_ID через Internals\SectionTable и строит модель по ним:
$sectionModel = SectionModel::createNew()
->setId($id)
->setType($realType)
->setOwnerId($realOwnerId);
Что изменится для интеграций:
- права проверяются по владельцу секции из базы;
ownerIdприtypeне равномuserпо-прежнему обязателен (проверку оставили), но на результат не влияет;- сменить секции тип или владельца через
calendar.section.updateбольше нельзя, в сохранение уходят значения из базы; - если секции с таким
idнет, модель собирается с пустым типом и нулевым владельцем. Обычный пользователь на ней получитCAL_REST_ACCESS_DENIED, и только администратор дойдёт доCAL_REST_SECT_ID_EXCEPTION.
Почтовые приглашения сверяют организатора
Сильнее всего переписан обработчик входящих iCal-приглашений IncomingInvitationRequestHandler, в нём +233/−39 строк. Раньше он искал локальное событие по UID из письма среди всех событий портала, при обновлении писал в родительское событие организатора (ID = PARENT_ID, OWNER_ID = MEETING_HOST) и заново определял хоста встречи по адресу организатора из письма.
Что проверяется теперь:
- если в хите авторизован другой пользователь, не получатель письма, обработка останавливается;
- email организатора должен совпасть с одним из адресов письма, иначе письмо не обрабатывается;
- событие ищется только среди встреч получателя, через
Helper::getEventByUId($uid, $this->userId, true); - при обновлении организатор из письма должен совпасть по email с хостом уже существующей встречи;
- при обновлении
MEETING_HOSTберётся из локального события. У нового приглашения хост, как и раньше, определяется по адресу организатора черезHelper::getUserIdByEmail(); - обновляется копия получателя (
ID = $localEvent['ID'],OWNER_ID = $this->userId) черезCCalendar::SaveEventсcheckPermissionи новым ключом параметровcheckCurrentEventPermission; - список участников при обновлении больше не затирается до двух кодов, в него попадают коды родительского события плюс получатель и хост.
Во всех отказных ветках handle() возвращает false.
Поменялась и работа с SEQUENCE. Номер ревизии из письма больше не пишется в поле VERSION, он хранится в MEETING['ICAL_SEQUENCE']. Письмо с SEQUENCE меньше сохранённого считается устаревшим. Встречу оно не обновляет, но handle() всё равно возвращает true. Нечисловой SEQUENCE превращается в 0.
IncomingInvitationCancelHandler, который обрабатывает отмену, тоже ищет событие только среди встреч получателя.
Слова «уязвимость» в диффе нет ни здесь, ни в правке calendar.section.update. Проверка прав по данным из базы и сверка организатора с отправителем и хостом похожи на защиту от подделанных запросов и приглашений, но это вывод из кода.
Если вы зовёте Helper::getEventByUId() у себя, вызов с одним аргументом работает по-старому. Новые параметры необязательные:
public static function getEventByUId(
?string $uid,
?int $userId = null,
bool $includeChildUid = false,
): ?array
С $userId метод смотрит только встречи этого пользователя с неудалённым родителем. Событие он вернёт, если хост внутренний или если хост почтовый и встреча помечена EXTERNAL_TYPE = 'mail':
<?php declare(strict_types=1);
use Bitrix\Calendar\ICal\MailInvitation\Helper;
use Bitrix\Main\Loader;
Loader::requireModule('calendar');
// ищем только среди встреч получателя
$event = Helper::getEventByUId($uidFromIcs, userId: $recipientId, includeChildUid: true);
if ($event === null)
{
// у получателя такой встречи нет
}
Ловите отказ от EntryManager.setMeetingStatus
Раньше промис из BX.Calendar.EntryManager.setMeetingStatus() мог только разрешиться. Теперь он отклоняется в двух случаях. Первый: пользователь закрыл диалог подтверждения отказа, ничего не выбрав. У ConfirmStatusDialog для этого появилось событие onCancel, промис падает с Error('cancelled'). Второй: запрос вернул ошибку.
Вызовы в исходниках модуля получили обработчик ошибки. Если вы дёргаете метод из своего JS, без обработчика в консоли появится Uncaught (in promise):
BX.Calendar.EntryManager.setMeetingStatus(entry, 'N')
.then(() => myWidget.refresh())
.catch((error) => {
if (error?.message !== 'cancelled')
{
console.error(error);
}
});
Зовите на встречу команду целиком
В селекторе участников можно выбрать команду из humanresources (сущность structure-node). В событии она хранится кодом доступа SNT<id>, а CCalendar::GetDestinationUsers() разворачивает его в прямых участников команды. Подкоманды не обходятся. Рекурсивный код SNTR<id> календарь намеренно не распознаёт.
По умолчанию команд в интерфейсе не видно. FeatureService::isTeamsAsAttendeeEnabled() вернёт true, только если установлен humanresources, в нём доступны кросс-функциональные команды и включена опция календаря. Переключателя в админке в этом релизе нет, включать придётся кодом:
<?php declare(strict_types=1);
use Bitrix\Calendar\Integration\HumanResources\FeatureService;
use Bitrix\Main\Config\Option;
use Bitrix\Main\Loader;
Loader::requireModule('calendar');
Option::set('calendar', 'team_attendees_enabled', 'Y');
var_dump(FeatureService::isTeamsAsAttendeeEnabled());
Опция отвечает только за видимость команд в селекторе. В комментарии к FeatureService сказано лишь, что серверная часть активна всегда. По коду в неё входят разворачивание SNT в GetDestinationUsers() и пересчёт состава через watcher.
Команды, которые пользователь не вправе просматривать (ACTION_TEAM_VIEW в humanresources), отбрасываются при сохранении участников и в планировщике. Когда в команде меняется состав, обработчик Watcher\Membership\Handler\Team ставит в очередь пересчёт событий с кодом этой команды. Отделы он пропускает, у них свой watcher.
Для своего кода в Bitrix\Calendar\Integration\HumanResources лежат три небольших класса: TeamAccessCode, TeamAccessService и TeamMemberService. Это интеграционный слой календаря, так что на обещанный контракт он не тянет, но пользоваться можно:
<?php declare(strict_types=1);
namespace Vendor\Meetings\Application;
use Bitrix\Calendar\Integration\HumanResources\TeamAccessCode;
use Bitrix\Calendar\Integration\HumanResources\TeamAccessService;
use Bitrix\Calendar\Integration\HumanResources\TeamMemberService;
final class TeamAttendeeResolver
{
public function __construct(
private readonly TeamAccessService $access = new TeamAccessService(),
private readonly TeamMemberService $members = new TeamMemberService(),
) {}
/** @return int[] */
public function resolve(string $code, int $viewerId): array
{
// 'SNT15' -> 15, а 'SNTR15' и всё прочее -> null
$nodeId = TeamAccessCode::extractNodeId($code);
if ($nodeId === null || !$this->access->canViewTeam($nodeId, $viewerId))
{
return [];
}
return $this->members->getDirectMemberUserIds([$nodeId]);
}
}
canViewTeam() возвращает false, если нет модуля, ID неположительный или узел недоступен. getDirectMemberUserIds() молча пропускает узлы, которые не являются командой.
БД
Схема не менялась. В install/index.php добавлена регистрация пяти обработчиков событий humanresources: OnMemberAdded, OnMemberUpdated, OnMemberDeleted, OnNodeUpdated, OnNodeDeleted. Обновление ставит их и на уже установленный модуль. На эталонной установке после обновления все пять обработчиков Watcher\Membership\Handler\Team есть в b_module_to_module.
Мелочи и находки
CCalendarEvent::GetList()с пустымarSelectили['*']больше не читает колонкуSEARCHABLE_CONTENT. Select теперь раскрывается через новыйEventTable::getDefaultSelectFieldNames(). Из результата поле вырезалось и раньше, меняется только нагрузка на БД.- Встречи пользователя без секции теперь попадают в выборку по его секции встреч (
getListOrm). CCalendarEvent::SetMeetingStatusEx(): при ответе на повторяющуюся встречу связанные события выбираются до разделения серии. После разделения исключения перепривязывались к новому мастеру и терялись, в комментарии ссылка на Mantis #249047.CCalendar::SaveEventEx()понимает новый ключ$params['checkCurrentEventPermission'], сигнатураSaveEventEx($params = [])та же. Ключ передаёт в проверку права на редактированиеcheckCurrentEvent => 'Y'.- В комментариях видны внутренние метки задач
CFG-01иCODE-01. В JS-комментариях объяснён выборselectMode: 'departmentsOnly'в селекторе. Другие режимы даютSNTR<id>или постфикс:F, которые бэкенд не разбирает. isOrganizerEmailMatchingSender()вызывается сразу послеnormalizeAddressesByOrganizer(), которая уже добилась того же совпадения. По коду вторая проверка лишняя.- В
team.phpнетdeclare(strict_types=1), в остальных четырёх новых файлах он есть. VERSION_DATEу 26.100.0 (13 июля) раньше, чем у предыдущей 26.0.100 (5 августа).- В
calendar_event_handlers.phpиcalendar_notify.phpCHTTP::urlAddParamsзаменили наBitrix\Main\Web\Uri, а<?на<?php. - В интерфейсе двойной клик по «Принять» или «Отказаться» в компактной форме больше не шлёт два запроса, переговорка сравнивается по типу и ID, планировщик открывается на дате события.
- Deprecated в релизе нет.
Что делать
- Проверьте права вебхуков и приложений на секции, которые они меняют через
calendar.section.update. - Если пользователи принимают приглашения из внешней почты, отправьте тестовое приглашение, затем его обновление, и убедитесь, что встреча у получателя обновилась.