Main 26.900.0–26.1000.0: агенты без eval, AJAX-контроллеры прячут текст исключений, экспорт только с подписью
Ломающее обновление
Удалены или изменены публичные API — прикладной код может перестать работать.
Главный модуль приехал двумя версиями сразу, 26.900.0 и 26.1000.0 (прошлую, 26.800.0, мы разбирали здесь). В 26.900.0 агенты уходят с eval на Callback, в bitrix.php появляются команды main:*, в main.core — клиент HttpClient, а Tokenizer стал обязательным. В 26.1000.0 AJAX-контроллеры в боевом режиме прячут текст исключений, а сторонний экспорт через Main\Controller\Export требует подписанный пропуск, так что свои контроллеры, выгрузки и агенты проверьте до обновления.
Новое
Агенты без eval (26.900.0)
Bitrix\Main\Type\Callback описывает вызов агента: класс и метод (или имя функции) и список аргументов. CAgent::AddAgent(), Update() и RemoveAgent() принимают его вместо строки, в b_agent.NAME он ложится JSON-ом, а ExecuteAgents() вызывает его через call_user_func_array. В аргументах допустимы скаляры, null и массивы, на объект конструктор бросит ArgumentException. Агент, вернувший Callback, запустится снова с новыми аргументами, вернувший null удалится.
<?php declare(strict_types=1);
namespace Vendor\Reports\Agent;
use Bitrix\Main\Type\Callback;
final class ReportCleaner
{
public static function run(int $daysToKeep, int $lastId = 0): ?Callback
{
$nextId = self::deleteBatch($daysToKeep, $lastId);
// null удаляет агент, Callback перепишет его имя для следующего запуска
return $nextId === null ? null : new Callback([self::class, 'run'], [$daysToKeep, $nextId]);
}
private static function deleteBatch(int $daysToKeep, int $lastId): ?int
{
// удаляем пачку старых отчётов, возвращаем последний ID или null
return null;
}
}
// регистрация, например в DoInstall() модуля
\CAgent::AddAgent(new Callback([ReportCleaner::class, 'run'], [30]), 'vendor.reports', 'N', 3600);
Консоль main:* и свои команды с JSON (26.900.0)
В bitrix.php теперь есть опции (main:option:get|set|delete|list), очистка кеша по папке, тегу или целиком для управляемого (main:cache:clear, main:tagged-cache:clear, main:managed-cache:clear), фичафлаги, списки модулей и агентов, правка секций .settings.php. С --format=json они отвечают конвертом {success, data, meta, error}, так что их можно вызывать из CI и скриптов деплоя, если composer поставил symfony/console. main:sql и main:php-eval работают только при BX_LOCAL_CLI_ALLOW_UNSAFE_COMMANDS === true.
php bitrix/bitrix.php main:option:set vendor.reports keep_days 30 --format=json
php bitrix/bitrix.php main:cache:clear vendor/reports
php bitrix/bitrix.php main:managed-cache:clear --all --force
Свою команду с тем же выводом пишут на Cli\Command\AbstractFormattedCommand. Команды модулей теперь создаёт ServiceLocator, зависимости конструктора приходят автосвязыванием, а нетипизированный параметр или непубличный конструктор дадут ServiceNotFoundException.
<?php declare(strict_types=1);
namespace Vendor\Reports\Cli;
use Bitrix\Main\Cli\Command\AbstractFormattedCommand;
use Bitrix\Main\Cli\Output\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Vendor\Reports\ReportRepository;
// регистрируется в .settings.php модуля: 'console' => ['value' => ['commands' => [StaleListCommand::class]]]
final class StaleListCommand extends AbstractFormattedCommand
{
public function __construct(OutputFormatter $outputFormatter, private readonly ReportRepository $reports)
{
parent::__construct($outputFormatter);
}
protected function configure(): void
{
$this->setName('reports:stale:list')->setDescription('Reports older than 30 days');
}
protected function executeFormatted(InputInterface $input, OutputInterface $output): int
{
$rows = $this->reports->findStale(days: 30); // [['id' => 1, 'title' => '...'], ...]
return $this->renderSuccess($output, $rows, columns: ['id' => 'ID', 'title' => 'Title']);
}
}
HttpClient для нового JS (26.900.0)
В main.core появился HttpClient (глобально BX.HttpClient) на fetch. action() вызывает Engine-контроллер с CSRF-токеном, отдаёт содержимое data и превращает неуспешный ответ в ControllerError, а actionRaw() возвращает весь {status, data, errors}. Есть таймаут, AbortSignal, interceptors, повторы и crossTabLockKey, с которым через navigator.locks запрос уходит один на все открытые вкладки. README называет клиент заменой BX.ajax, но сам BX.ajax устаревшим не помечен.
import { HttpClient } from 'main.core';
try
{
const report = await HttpClient.action('vendor:reports.report.get', {
data: { id: 42 },
timeout: 5000,
});
renderReport(report);
}
catch (error)
{
if (error instanceof HttpClient.ControllerError)
{
showErrors(error.errors);
}
}
Пропуск для пошагового экспорта (26.1000.0)
Bitrix\Main\Component\ExportParameterSigner подписывает параметры выгрузки для пары «контроллер + компонент». Наследник Main\Controller\Export из lib/ выдаёт пропуск через signExportParameters(), а для контроллера из ajax.php компонента есть ExportParameterSigner::signForComponent(). Пропуск уходит POST-параметром signedParameters вместе с COMPONENT_NAME.
<?php declare(strict_types=1);
use Vendor\Reports\Controller\Export;
// executeComponent() компонента vendor:reports.list
$this->arResult['EXPORT_SIGNED_PARAMS'] = Export::signExportParameters(
'vendor:reports.list',
['GRID_ID' => $this->arParams['GRID_ID'], 'FILTER_ID' => $this->arParams['FILTER_ID']],
);
Свой тип UF убирает за собой (26.1000.0)
У UserField\Types\BaseType два новых статических хука: onBeforeDeleteValues() получает сохранённые значения перед удалением поля или сущности, onBeforeDeleteField() вызывается перед удалением самого поля. Ошибка в Result отменяет CUserTypeEntity::Delete(), при удалении сущности результат игнорируется.
<?php declare(strict_types=1);
namespace Vendor\Maps\UserField;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
use Bitrix\Main\UserField\Types\BaseType;
use Vendor\Maps\PointStorage;
final class PointType extends BaseType
{
// ...описание типа как обычно
public static function onBeforeDeleteValues(array $userField, iterable $savedValuesFromServer): Result
{
foreach ($savedValuesFromServer as $value) {
foreach ((array)$value as $pointId) {
PointStorage::delete((int)$pointId);
}
}
return new Result();
}
public static function onBeforeDeleteField(array $userField): Result
{
$result = new Result();
if (PointStorage::isUsedInExport((int)$userField['ID'])) {
$result->addError(new Error('Поле используется в выгрузке карт', 'POINT_FIELD_IN_USE'));
}
return $result;
}
}
Подписи в форме письма от своего модуля (26.1000.0)
Подписи для main.mail.form теперь приходят из обработчиков события main:OnMailSenderListBuild. Обработчик получает userId и senders, а возвращает signatures и senders — карту key => {availableSignatureIds, selectedSignatureId}.
Что сломается
Верните текст ошибок в контроллерах (26.1000.0)
При выключенном exception_handling.debug наследник Engine\Controller вместо текста исключения отдаёт 'Internal server error.' с прежним кодом. Текст сохраняют только ArgumentException с наследниками, BinderArgumentException и ValidationException. SystemException, AccessDeniedException, ObjectNotFoundException и PHP-ошибки клиент видит одной фразой, лог пишется как раньше. Если фронт показывает errors[0].message, отдавайте ошибку явно:
<?php declare(strict_types=1);
namespace Vendor\Reports\Controller;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Error;
final class Report extends Controller
{
public function approveAction(int $id): ?array
{
if (!$this->canApprove($id)) {
// этот текст дойдёт до клиента и в боевом режиме
$this->addError(new Error('Нет прав на согласование отчёта', 'ACCESS_DENIED'));
return null;
}
return ['id' => $id, 'status' => 'approved'];
}
private function canApprove(int $id): bool
{
return $id > 0;
}
}
Выдайте пропуск своему экспорту (26.1000.0)
Раньше COMPONENT_NAME из запроса уходил прямо в IncludeComponent(), и через контроллер экспорта подключался произвольный компонент. Теперь export и dispatcher без пропуска отвечают 'Component name is not allowed.', а подпись от getSignedParameters() имя компонента больше не допускает. Временно без пропуска пускают отдельные компоненты crm, lists, tasks, timeman, voximplant и imopenlines по списку и только через контроллер их модуля.
Ищите агенты main по обоим именам (26.900.0)
После первого запуска на новой версии агенты main переписывают имя в JSON, и поиск по строке промахивается: CAgent::GetList([], ['NAME' => 'CUser::CleanUpAgent();']), RemoveAgent('CEventLog::CleanUpAgent();', 'main'), свой SQL по b_agent.NAME. Конвертера старых имён в диффе нет, строки меняются по мере запуска агентов, и какое-то время в базе живут оба формата:
<?php declare(strict_types=1);
use Bitrix\Main\Type\Callback;
foreach (['CEventLog::CleanUpAgent();', new Callback([\CEventLog::class, 'CleanUpAgent'])] as $name) {
\CAgent::RemoveAgent($name, 'main');
}
CUser::SyncBithdaysAgent(), CUserTypeEntity::syncColumnsAgent() и ещё несколько агентных методов объявлены : ?Callback, и наследник, переопределивший такой метод без этого типа, упадёт с fatal error. Своя BX_AGENTS_LOG_FUNCTION получает JSON вместо PHP-строки.
Уберите объекты из сериализованных ORM-полей (26.900.0)
Field::unserialize(), ArrayField::decodePhp() и DB\Result вызывают unserialize() с allowed_classes => false. Поле с 'serialized' => true или configureSerializationPhp(), где лежит Type\DateTime или DTO, вернёт __PHP_Incomplete_Class. Скаляры и массивы читаются как раньше, поэтому дату храните строкой или timestamp, а записанные объекты перегоните своим скриптом.
Перечислите поля в main.profile (26.900.0)
Компонент сохраняет только UF из параметра USER_PROPERTY, при пустом параметре ни одного. Раньше через публичную форму можно было записать любое UF_* из POST. Тот же белый список доступен своему коду через опцию 'FIELDS' у CUserTypeManager::EditFormAddFields().
Проверьте сервер, сессии и бэкапы (26.900.0)
Проверка системы теперь считает ошибкой PHP ниже 8.2 (мастер установки требовал 8.2 и раньше), Tokenizer обязателен. Без token_get_all() ломаются разбор страниц, LPA и параметры компонентов, мастер установки останавливается. Крипто-куки получили версию v2: и HMAC-подпись, старые проверку не проходят, и на сайтах с сессией в куке все сессии сбросятся. Зашифрованные бэкапы теперь называются *.tar.gz.enc (без сжатия *.tar.enc) с частями *.enc.N вместо прежних *.enc и *.enc.gz. Формат тоже другой, и скрипты со старыми масками их не найдут.
php-parser в main/vendor обновлён с 4.9 до 5.x, ParserFactory::create() заменили createForNewestSupportedVersion() и createForHostVersion(). В эталоне xscan.php модуля security 26.200.0 ещё вызывает create(ParserFactory::PREFER_PHP7), и сканер xscan упадёт с fatal error, пока security не обновится.
Сверьте REST v3 и вызовы ConditionCompose
В 26.900.0 из REST v3 удалён main.userfieldconfig.getTypes, его место занял main.userfieldconfig.type.list со списком {userTypeId, description}, пагинацией и select. В той же версии main.like.list отклоняет OR, отрицания и операторы кроме =, а main.user.agreement.get не читает ключ REPLACE в верхнем регистре.
ConditionCompose($arRequest, $type) (26.1000.0) ждёт вторым параметром тип условия ('folder', 'ugroups', 'php'...), а не индекс в $_REQUEST['selected_type'], и на старый вызов вернёт null. Редактор меню fileman 26.0.0 из эталона зовёт функцию по-старому, и, судя по коду, условия пунктов меню при сохранении обнулятся, пока fileman не обновится.
Обновите копии шаблонов
В main.ui.grid (26.1000.0) кнопки строк, настроек, сортировки и панели действий стали <button type="button"> вместо <span> и <a href="#">. При ALLOW_ROWS_SORT ячейка drag выводится в каждой строке. Проверьте селекторы вида a.main-grid-row-action-button и код, который считает ячейки.
В main.ui.filter (26.900.0) фразы MAIN_UI_FILTER__AND и MAIN_UI_FILTER__MORE слиты в MAIN_UI_FILTER__AND_MORE, и старый search.js покажет undefined. В main.mail.form (26.1000.0) ключ signatureChoices стал signatureSelection.
Подписи пропадут из почтового клиента (26.1000.0)
Подписи main.mail.form берёт только из Sender::prepareUserMailboxesWithSignatures(). В эталоне mail 26.1200.0 обработчика OnMailSenderListBuild нет, и, судя по коду, в форме нового письма и ответа почтового клиента подписи пропадут, пока mail не обновится.
Проверьте, что пишете в URL согласия (26.1000.0)
В URL согласия сохраняются только http(s)://… и пути портала вида /…, остальное, от javascript: до схем мобильных приложений, записывается как NULL. Так закрыт XSS через URL согласия.
Deprecated
BX.Promise→ нативныйPromise,BX.ResizeObserver→window.ResizeObserver(26.900.0). Второй стал обёрткой над нативным и пишет предупреждение в консоль, а классы полифила и расширениеmain.polyfill.promiseуже удалены.Composite\Helper::readStatistic()→getCacheFileSize(),writeStatistic()→updateCacheFileSize()(26.1000.0). Обнуляет счётчик теперьresetCacheFileSize(), а неupdateCacheFileSize(false).BX.Grid.Grid#adjustIndex()(26.1000.0) нигде не вызывается и оставлен ради совместимости.
Мелочи и находки
main.coupon.activation(26.1000.0) активировал лицензионный ключ по POST от любого авторизованного пользователя. Теперь нужно правоinstall_updates.main:settings:crypto-getиmain:settings:smtp-get(26.900.0) печатают секцию.settings.phpс ключом шифрования и паролем SMTP, в логи CI их лучше не пускать.- В миграциях
AbstractIntColumn::autoincrement($startWith)(26.900.0) задаёт стартовое значение счётчика:AUTO_INCREMENT=Nв MySQL иIDENTITY START WITHв PostgreSQL. - Схема таблиц main ни в одной из двух версий не менялась.
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
BX.Promise
|
Deprecated |
Promise
|
BX.ResizeObserver
|
Deprecated |
window.ResizeObserver
|
Bitrix\Main\Composite\Helper::readStatistic()
|
Deprecated |
Bitrix\Main\Composite\Helper::getCacheFileSize()
|
Bitrix\Main\Composite\Helper::writeStatistic()
|
Deprecated |
Bitrix\Main\Composite\Helper::updateCacheFileSize()
|
BX.Grid.Grid.prototype.adjustIndex()
|
Deprecated | — |
PhpParser\ParserFactory::create()
|
Удалено |
PhpParser\ParserFactory::createForNewestSupportedVersion()
|
PhpParser\ParserFactory::PREFER_PHP7
|
Удалено | — |
REST v3 main.userfieldconfig.getTypes
|
Удалено |
main.userfieldconfig.type.list
|
main.polyfill.promise
|
Удалено |
Promise
|
BX.ResizeObserverCollection
|
Удалено |
window.ResizeObserver
|
BX.ResizeObserverItem
|
Удалено |
window.ResizeObserver
|
BX.ResizeObserverItemCollection
|
Удалено |
window.ResizeObserver
|
BX.ResizeObserverItemRect
|
Удалено |
window.ResizeObserver
|