Calendar 26.150.0–26.200.0: двойная бронь переговорки стала ошибкой, а форма бронирования больше не отдаёт чужую занятость
Ломающее обновление
Удалены или изменены публичные API — прикладной код может перестать работать.
Календарь вышел двумя версиями, 26.150.0 и 26.200.0. В 26.150.0 закрыли утечку в анонимном экшене бронирования CRM-формы, через который любой посетитель мог узнать занятость и имена сотрудников и ресурсов портала. В 26.200.0 занятость переговорок проверяется по умолчанию в CCalendar::SaveEvent, REST и AJAX, и модули, агенты и интеграции, которые раньше молча сохраняли пересечения, теперь получат исключение или ошибку.
Новое
Ловите конфликт переговорки в своём коде (26.200.0)
При пересечении CCalendar::SaveEvent() бросает Bitrix\Calendar\Rooms\OccupancyCheckerException. У исключения есть getOccupancyData(), isInvalidInterval() и константы ERROR_CODE (CALENDAR_LOCATION_OCCUPANCY_CONFLICT) и INVALID_INTERVAL_ERROR_CODE. Если ту же переговорку в этот момент сохраняет другой запрос, прилетит новый Rooms\OccupancyLockedException с getErrorCode(). Он наследует \RuntimeException, поэтому catch (SystemException $e) его пропустит.
<?php declare(strict_types=1);
namespace Vendor\Rooms\Application\Service;
use Bitrix\Calendar\Rooms\Manager;
use Bitrix\Calendar\Rooms\OccupancyCheckerException;
use Bitrix\Calendar\Rooms\OccupancyLockedException;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;
final class RoomBookingService
{
public function save(array $fields, int $userId): Result
{
$result = new Result();
Loader::requireModule('calendar');
try {
$eventId = (int)\CCalendar::SaveEvent([
'arFields' => $fields,
'userId' => $userId,
// с 26.200.0 null тоже значит 'Y', а 'N' сохранит пересечение с меткой в MEETING
'checkLocationOccupancy' => 'Y',
]);
} catch (OccupancyLockedException $e) {
return $result->addError(new Error('Переговорную сейчас бронирует кто-то ещё, повторите позже', $e->getErrorCode()));
} catch (OccupancyCheckerException $e) {
if ($e->isInvalidInterval()) {
return $result->addError(new Error($e->getMessage(), OccupancyCheckerException::INVALID_INTERVAL_ERROR_CODE));
}
$occupancy = $e->getOccupancyData();
// в тексте исключения даты чужих броней, без view_full на переговорку их не показываем
if (!Manager::canExposeOccupancyDates($occupancy)) {
return $result->addError(new Error('Переговорная занята', OccupancyCheckerException::ERROR_CODE));
}
return $result->addError(new Error(
$e->getMessage(),
OccupancyCheckerException::ERROR_CODE,
['dates' => $occupancy['dates'] ?? [], 'truncated' => $occupancy['truncated'] ?? false],
));
}
if ($eventId <= 0) {
return $result->addError(new Error(implode('; ', \CCalendar::GetAndClearErrors()), 'EVENT_NOT_SAVED'));
}
return $result->setData(['id' => $eventId]);
}
}
Проверяйте серию разом и берите ту же блокировку (26.200.0)
Rooms\OccupancyChecker::checkBatch(array $events, array $excludedEventIds = []) проверяет несколько событий и загружает брони каждой переговорки один раз. Для массивов полей события есть обёртка CCalendarEvent::checkLocationOccupancyBatch(). У неё и у checkLocationOccupancy() появились флаги $throwOnConflict и $checkPermission. С $throwOnConflict = false метод вместо исключения возвращает true, если нашёл конфликт.
Rooms\OccupancyLock держит именованные блокировки БД с ключом calendar.room.occupancy.<roomId>. Статический resolveRoomIds($oldLocation, $newLocation) собирает отсортированные ID комнат из старого и нового LOCATION вместе со связанными переговорками. acquire(array $roomIds) захватывает их без ожидания и при неудаче бросает OccupancyLockedException, release() отпускает, деструктор вызывает его сам. Работает только на MySQL и PostgreSQL, на других подключениях acquire() с непустым списком комнат бросает OccupancyLockedException. Если ваш код меняет бронь переговорки, берите ту же блокировку, и календарь не запишет туда событие параллельно с вами.
Фильтруйте коды участников и показывайте лимит планировщика (26.200.0)
(new Planner\AccessCodeFilter($userId))->filter($codes) оставляет из кодов доступа UA, U<id>, DR<id>, группы SG<id>[_роль], которые пользователь видит, и видимые ему команды HR. Календарь прогоняет через него коды в планировщике и Живой ленте. Если ваш эндпоинт принимает коды участников от клиента, прогоните их через тот же фильтр.
CCalendarPlanner::prepareData теперь отдаёт plannerLimitExceeded, maxPlannerUsers, plannerPeopleCount и plannerLimitPeopleCount, а для своего интерфейса есть попап BX.Calendar.Controls.PlannerLimitHint (опции bindElement и maxPlannerUsers, методы show(), close() и destroy(), который снимает обработчик resize с window). С limitExceededPolicy => CCalendarPlanner::LIMIT_EXCEEDED_POLICY_OMIT_USERS при превышении лимита пользователи в ответ не попадают вовсе.
Что сломается
Пересечение больше не сохранится молча (26.200.0)
Раньше CCalendarEvent::Edit(), а через него CCalendar::SaveEvent() и SaveEventEx(), проверял занятость, только если checkLocationOccupancy === true. Теперь null означает 'Y', bool превращается в 'Y' или 'N', а любое другое значение даёт ArgumentException. С 'N' событие сохранится, но конфликт останется в MEETING меткой LOCATION_OCCUPANCY_OVERRIDE с TIMESTAMP.
Проверки нет для CAL_TYPE location, дочерних копий события и источников синхронизации: google, google_api_oauth, icloud, office365, caldav, caldav_google_oauth, exchange. На всём портале её выключает опция isOccupancyCheckerEnabled = N модуля calendar.
Заодно появилась проверка прав. Её не выключают ни опция isOccupancyCheckerEnabled, ни 'N'. Без view_full на переговорку SaveEvent бросит OccupancyCheckerException с фразой EC_MR_RESERVE_ERR_BUSY. Её отключает checkPermission => false, а если передан ключ checkLocationOccupancyPermission, то любое его значение, кроме true. Права проверяются у текущего пользователя (CCalendar::GetUserId(), обычно $USER), а не у userId из параметров SaveEvent. Агент или обработчик, который бронирует переговорку от имени сотрудника, может упасть, даже если у сотрудника доступ есть. Если права вы уже проверили сами, передайте checkLocationOccupancyPermission => false.
Разберите новые ошибки REST и AJAX (26.200.0)
calendar.event.add и calendar.event.update проверяют занятость по умолчанию, выключает проверку новый параметр doCheckOccupancy=N. Значение, кроме Y и N, вернёт HTTP 400 с текстом «Недопустимое значение параметра „doCheckOccupancy“». Новые коды ошибок:
CALENDAR_LOCATION_OCCUPANCY_CONFLICTсadditional.occupancy.dates(даты в UTC в формате ISO, только если у вызывающего естьview_fullна переговорку) иadditional.occupancy.truncated;CALENDAR_LOCATION_OCCUPANCY_INVALID_INTERVAL;CALENDAR_LOCATION_OCCUPANCY_LOCKED.
Все три приходят со статусом CRestServer::STATUS_OK, то есть HTTP 200, так что смотрите на поле error в теле ответа.
calendar.event.update для встреч (is_meeting: 'Y') раньше сохранял событие от имени пользователя из параметра host, теперь от имени того, кто вызывает метод.
У AJAX calendar.api.calendarentryajax.editEntry параметр doCheckOccupancy по умолчанию стал Y (был N), другое значение даёт edit_entry_invalid_argument. Новые коды: CALENDAR_LOCATION_OCCUPANCY_INVALID_INTERVAL и CALENDAR_LOCATION_OCCUPANCY_LOCKED. Конфликт у неповторяющегося события теперь приходит кодом edit_entry_location_busy, раньше это была пара edit_entry_location_busy_recurrence и edit_entry_location_repeat_busy.
Соберите обработчики команд с новым аргументом (26.200.0)
В конструкторах CreateEventCommand и UpdateEventCommand из Bitrix\Calendar\Application\Command параметр bool $checkLocationOccupancy заменён на string $occupancyCheckMode, метод isCheckLocationOccupancy() удалён, вместо него getOccupancyCheckMode(). CreateEventHandler и UpdateEventHandler требуют в конструкторе Core\Event\LegacyLocationAvailabilityCheckerInterface, через который подменяется проверка доступности старых переговорок на инфоблоке. new CreateEventHandler() без аргумента упадёт с ArgumentCountError. Передайте штатную реализацию: new CreateEventHandler(new \Bitrix\Calendar\Rooms\LegacyLocationAvailabilityChecker()).
Не вызывайте ExcludeInstance без пользователя (26.200.0)
CCalendarEvent::ExcludeInstance($eventId, $excludeDate) теперь возвращает Main\Result, раньше не возвращал ничего. Методу нужен текущий пользователь с правом ACTION_EVENT_EDIT и на событие, и на мастер серии. В агенте или CLI без $USER придёт ошибка exclude_recursion_date_access_denied. Дата должна строго совпадать с форматом CCalendar::Date. AJAX excludeRecursionDate отвечает ошибками exclude_recursion_date_*.
Планировщик показывает занятость первых 50 участников (26.200.0)
CCalendar::GetMaxPlannerUsers() по умолчанию возвращает 50 (константа DEFAULT_MAX_PLANNER_USERS), раньше лимита не было. На большой встрече планировщик покажет занятость первых 50 участников, хоста поставит первым вне лимита и выведет подсказку «В событии много участников...». Лимит хранится в опции maxPlannerUsers модуля calendar. Из JS-расширения calendar.planner удалены settings.maxPlannerUsers и статический Planner.getMaxPlannerUsers().
CalendarAjax::updatePlannerAction больше не учитывает add_cur_user_to_list и ownerId, хостом по умолчанию становится hostId из запроса или текущий пользователь.
Своя форма бронирования получит пустой ответ (26.150.0)
Анонимный экшен calendar.api.resourcebookingajax.getFillFormData (без фильтров Authentication и Csrf) брал settingsData и fieldName прямо из запроса. Так аноним получал занятость с ID событий, имена любых пользователей и ресурсов и настройки любого пользовательского поля. Теперь на запрос с settingsData экшен отвечает [] и, если задан LOG_FILENAME, пишет в лог «legacy request: settingsData present». Новые параметры: sourceType (поддерживается только crm-webform), formId, sec и fieldCode, окно дат по-прежнему задают from и to. Пользователей и ресурсы экшен берёт из SETTINGS_DATA поля веб-формы, когда код безопасности формы верный и форма активна. Пустой ответ придёт и при одной границе окна дат, дате, которую не удалось разобрать, окне длиннее 63 дней или to < from.
Ответ урезан белым списком. Сверху остались timezoneOffset, timezoneOffsetLabel, fieldSettings, usersAccessibility, resourcesAccessibility, userIndex, workTimeStart и workTimeEnd, в записях занятости только dateFrom, dateTo и fullDay без id события. В fieldSettings девять ключей, SELECTED_USERS и SETTINGS больше не приходят. Поле бронирования вне CRM-формы и старые закешированные бандлы resourcebooking на внешних сайтах получат пустые данные.
В редакторе формы несохранённое поле бронирования показывает уведомление «Предпросмотр доступности появится после сохранения формы...» вместо ошибки [UF_NOT_FOUND]. Парную правку получил landing 26.1300.100. Панель настроек формы передаёт CRM-форме объект identification, без которого превью в редакторе получило бы пустой ответ.
Мелочи и находки
- Уведомления об изменении и отмене встречи уходят и тогда, когда правит делегат с правом EDIT на секцию календаря хоста (26.200.0).
CCalendar::GetAccessNamesподставляет LOGIN или EMAIL приглашённым, которые раньше отображались сырым кодом вроде «U123» (26.200.0).- iCloud-синхронизация резервирует UUID события до PUT и больше не плодит дубли при повторах (26.200.0).
- Если вы наследовали
Rooms\OccupancyChecker, обновите переопределение protectedgetEventsForCheck(): у родителя появился параметр?array $limitEvent = null, и наследник со старой сигнатурой упадёт с Fatal error о несовместимом объявлении. УgetExistingEventsсигнатура прежняя, но логика переработана (26.200.0).
Прошлый разбор календаря — 26.125.100.
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
Bitrix\Calendar\Application\Command\CreateEventCommand::isCheckLocationOccupancy()
|
Удалено |
Bitrix\Calendar\Application\Command\CreateEventCommand::getOccupancyCheckMode()
|
Bitrix\Calendar\Application\Command\UpdateEventCommand::isCheckLocationOccupancy()
|
Удалено |
Bitrix\Calendar\Application\Command\UpdateEventCommand::getOccupancyCheckMode()
|
BX.Calendar.Planner.getMaxPlannerUsers()
|
Удалено | — |