main · Главный модуль 26.1000.0 Ломающее

Main 26.900.0–26.1000.0: агенты без eval, AJAX-контроллеры прячут текст исключений, экспорт только с подписью

1 мин чтения Устаревших API: 13
Узнавайте о ломающих изменениях Битрикса раньше, чем они доедут до прода: свежие разборы — в канале @bxmax_updates

Ломающее обновление

Удалены или изменены публичные 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

Новая версия вышла — разбор уже в Telegram

В канале @bxmax_updates публикуем каждый свежий разбор: что сломается, что устарело и что появилось в API. Узнаете до того, как обновите прод.

Подписаться

Читайте дальше

main 26.800.0 Безопасность Свежее

Main 26.800.0: закрыт SSRF через ftp://, а translit и выбор SMTP-отправителя работают иначе

Main 26.800.0 закрывает SSRF в `CFile::MakeFileArray()` через ссылки `ftp://` и в RSS-гаджете, а заодно чистит ввод при инлайн-редактировании в `CAdminUiList` и в `menu_edit.php`. Сильнее по проектам ударят изменения поведения: `CUtil::translit()` оставляет на конце `_`, почта по-другому выбирает SM…

1 мин
main 26.750.0 Заметное

Main 26.750.0: точка в имени плейсхолдера больше не ломает почтовые шаблоны

Main 26.750.0 меняет три файла ядра, +16/−5 строк, и заметная правка среди них одна (вторая, в лицензионном `ActivationSystem`, служебная). Но сидит она в месте, где из почтового шаблона генерируется PHP-код, так что посмотреть на неё стоит всем, у кого в шаблонах есть плейсхолдеры с точкой в имени,…

5 мин
main 26.700.0 Заметное

Main 26.700.0: в ядре появились фича-флаги

Битрикс выкатил main 26.700.0 — и это тот редкий случай, когда в ядре появляется целая новая подсистема: фича-флаги `Bitrix\Main\Config\Feature` с двумя таблицами в БД. Плюс несколько тихих поведенческих изменений, которые стоит проверить, если у вас не-UTF-8 сайт, кастомные фильтры пользователей ил…

4 мин
main 26.650.0 Заметное

main 26.650.0: сброс счётчиков ленты пачкой и превью своих ссылок без похода наружу

Два изменения в ядре, оба — про лишнюю работу, которую система делала на ровном месте. Счётчики живой ленты научились сбрасываться одним вызовом вместо цикла, а превью ссылок перестало ходить HTTP-запросом на собственный портал. Ломающих удалений нет: все правки сигнатур — добавление аргументов с де…

3 мин
Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…

Можно закрыть чат и перейти на другую страницу. Когда ответ будет готов, на кнопке Домового появится отметка.

Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти