UI 26.600.0: параллельная и presigned-загрузка файлов, поле rich_text и Bootstrap JS 4.6
Ломающее обновление
Удалены или изменены публичные API — прикладной код может перестать работать.
Дифф UI 26.600.0 даже без языковых файлов и .min.js весит 19 МБ, и почти весь этот объём занимают пересобранные бандлы и вендорные библиотеки (один mermaid дал 136 тысяч строк). Из содержательного загрузчик файлов научился принимать части параллельно и отдавать их браузеру для загрузки прямо в S3. Ещё в релизе тип пользовательского поля rich_text, серверный BBCode-конвертер, новая таблица в БД и Bootstrap JS 4.6.2 вместо 4.1.3 в ui.bootstrap4. Удалённого PHP API нет, но в нескольких местах поменялось поведение.
Проверьте страницы на ui.bootstrap4
В ui.bootstrap4 обновили только JavaScript: bootstrap.js и bootstrap.bundle.js теперь версии 4.6.2 вместо 4.1.3. CSS расширения в релизе не менялся, и в нём по-прежнему лежит Bootstrap v5.0.0-alpha1, так что скрипты и стили расходились по версиям и до обновления. Если шаблон сайта подключает ui.bootstrap4 и пользуется его JS-компонентами, откройте после обновления страницы с модальными окнами, дропдаунами и тултипами и убедитесь, что они ведут себя как раньше.
Атрибуты из BBCode больше не попадают в HTML
ui.bbcode.formatter.html-formatter раньше переносил атрибуты BBCode-узла в создаваемый DOM-элемент. В форматтерах b, i, u, s, списков и пунктов списка для этого стоял ...node.getAttributes(), а форматтер ссылок сначала складывал атрибуты в const nodeAttributes = node.getAttributes() и разворачивал их через ...nodeAttributes. Эти строки удалили. Теперь у <b>, <li> или <a> остаются только атрибуты, которые ставит сам форматтер: класс типографики, href, target и атрибуты из настроек ссылок.
По виду правки похоже, что закрыли перенос произвольных атрибутов из пользовательского BBCode в DOM, но сам дифф мотив не называет. Если ваш JS искал в отрендеренном HTML атрибуты, пришедшие из BBCode, их там больше не будет.
Если вы наследовали Uploader
Bitrix\UI\FileUploader\Uploader не объявлен final, а у его upload() появился третий параметр:
public function upload(Chunk $chunk, ?string $token = null, ?string $strategy = null): UploadResult
Наследник со старым upload(Chunk $chunk, string $token = null) станет несовместим с родителем, и PHP упадёт с fatal error на загрузке класса. У контроллера Bitrix\UI\Controller\FileUploader в uploadAction() тоже добавился ?string $strategy = null. Чтобы починить, допишите параметр в переопределение и передайте его в parent::.
Что ещё ведёт себя иначе
- Из пути временных файлов загрузчика пропал уровень
file-uploader/. РаньшеTempFileпередавал вCTempFile::getDirectoryName()иCCloudTempFile::getDirectoryName()подкаталогfile-uploader/<32 случайных символа>, теперь не передаёт ничего. Судя по коду main и clouds в эталоне, без подкаталога ядро само создаёт внутри часового каталога случайную папку (32 символа уCTempFile, md5 уCCloudTempFile). Скрипты очистки или мониторинга, которые ищутfile-uploaderв пути, перестанут его находить. Подкаталог убрали, потому что из-за негоCCloudTempFileопрашивал черезListFiles()каждый часовой каталог, по сетевому запросу на час. CopilotName::BITRIX_GPT_AGENTтеперь равен'BitrixGPT 5.6 Agent 1M'вместо'BitrixGPT 5.5 Agent'. Если вы где-то сохраняли строковое значение этого enum,CopilotName::from()на старой строке броситValueError.Button::setMenu()с непустым спискомitemsтеперь снимает с кнопки системное меню (о нём ниже).LockedButtonиDocumentButtonдобавляют свои расширения к списку родителя, раньше они его заменяли.
Загрузчик научился грузить части параллельно и прямо в S3
Почти весь новый PHP в релизе приходится на Bitrix\UI\FileUploader: около тысячи строк в Uploader.php и ещё почти четыреста в TempFile.php. К обычной последовательной загрузке кусками добавились две стратегии.
parallel. Браузер шлёт части в любом порядке. Сервер пишет их в разреженный файл по смещению(partNo - 1) * PART_SIZEили в multipart-сессию облака. Последняя принятая часть запускает сборку файла.presigned. Сервер открывает multipart-сессию в S3 и раздаёт подписанные URL, браузер кладёт части прямо в бакет, и байты через PHP не идут. Изображения так не загружаются, их отсекает\CFile::isImage().
Под это в контроллере Bitrix\UI\Controller\FileUploader появились действия uploadPart, initPresigned, refreshPresigned, registerPart, registerParts и completePresigned. В JS-расширении ui.uploader.core добавились ParallelUploadController и PresignedUploadController.
Обе стратегии по умолчанию выключены. Настройки читает Configuration::getGlobalSettings(), сам метод в релизе не менялся:
| Ключ | По умолчанию | Что задаёт |
|---|---|---|
parallelChunkUpload |
false |
включает parallel; на Windows метод всегда отвечает false |
maxParallelChunks |
2 |
сколько частей грузится одновременно, в обеих стратегиях |
presignedChunkUpload |
false |
включает presigned; без модуля clouds всегда false |
presignedThreshold |
не задан | минимальный размер файла для presigned, понимает запись вида 100M |
presignedUrlTtl |
1800 |
время жизни подписанного URL в секундах, от 60 секунд до 7 дней |
presignedUrlBatchLimit |
100 |
сколько URL подписать заранее при старте; тот же лимит режет каждый refreshPresigned и пачку в registerParts |
presignedRegisterInterval |
15000 |
как часто браузер отчитывается серверу об ETag частей, мс |
Если presigned включён, а presignedThreshold не задан, JS отправит этим путём любой файл больше одного чанка, кроме изображений. С заданным порогом туда пойдут файлы не меньше порога, изображения по-прежнему нет.
Перед включением проверьте:
- Сервер не проверяет
parallelChunkUpload. МетодisParallelChunkUploadEnabled()новый, и в PHP его вызывает толькоconfig.phpрасширенияui.uploader.core, чтобы передать флаг в браузер.Uploader::upload()приметstrategy=parallelот любого клиента, в том числе на Windows. У presigned серверная проверка есть, её делаетisPresignedAvailable()(настройка плюсLoader::includeModule('clouds')). - Presigned вызывает методы clouds, которых в диффе ui нет:
supportsPresignedUrls()у бакета,presignPart()иsetParts()уCCloudStorageUpload. В clouds они появились в 26.100.0, так что ставьте ui 26.600.0 не раньше этой версии clouds. - Если у вас свой
UploaderController, в режиме presigned егоcanUpload()получает имя, тип и размер со слов клиента, байтов сервер не видит. Изображения отсекаются ещё до этой проверки, а у остальных файлов ширина и высота обнуляются, так что габариты вUploadRequestвсегда 0×0. PHPDocvalidatePresignedRequest()предупреждает, что для строгой гарантии файл нужно перепроверять после коммита. - Локальный parallel пишет разреженные файлы. В комментарии к
writePartLocal()названы ext4, xfs и apfs, а про NFS и SMB сказано «may misbehave». Если временный каталог лежит на сетевой шаре, parallel лучше не включать.
Проверьте миграцию БД
В install.sql у b_ui_file_uploader_temp_file четыре новые колонки: STRATEGY, PART_SIZE, PART_COUNT, UPLOAD_ID. Рядом появилась таблица b_ui_file_uploader_temp_file_part с уникальным индексом по (TEMP_FILE_ID, PART_NO). Внешних ключей нет, части удаляет обработчик TempFileTable::onDelete.
TempFileTable::getMap() уже описывает новые колонки. Если на существующей установке миграция по какой-то причине не прошла, ORM-запросы к временным файлам загрузчика, скорее всего, начнут падать на несуществующих полях. Скрипт миграции в снапшот модуля не попадает, по диффу его не проверить, так что после обновления посмотрите сами:
<?php declare(strict_types=1);
use Bitrix\Main\Application;
$connection = Application::getConnection();
$columns = $connection->getTableFields('b_ui_file_uploader_temp_file');
$ok = $connection->isTableExists('b_ui_file_uploader_temp_file_part')
&& array_key_exists('STRATEGY', $columns)
&& array_key_exists('UPLOAD_ID', $columns);
echo $ok ? "ui 26.600.0: схема на месте\n" : "ui 26.600.0: миграция не прошла\n";
Появилось поле rich_text
Bitrix\UI\UserField\Types\RichTextType наследует StringType, хранит BBCode в строке и рендерится компонентом bitrix:ui.field.richtext (шаблоны для формы, просмотра, фильтра, админского списка и настроек поля). Тип регистрируется обработчиком main:OnUserTypeBuildList в install/index.php модуля.
Перед сохранением значение проходит Whitelist::normalize(). Он вырезает запрещённые теги (quote, code, table, spoiler, user, font, size, color, align и ещё несколько), а одиночные квадратные скобки превращает в [ и ]. В поисковый индекс попадает plain text.
Показ типа в интерфейсе закрыт фича-флагом Bitrix\UI\Config\Feature\RichTextUserFieldFlag, по умолчанию выключенным. Флаг проверяет только JS-фабрика полей ui.userfieldfactory, и от него зависит лишь то, появится ли rich_text в её списке типов. В самом RichTextType проверки флага нет.
Чтобы вывести такое значение в своём компоненте, есть Bitrix\UI\Format\BBCode\Converter:
<?php declare(strict_types=1);
use Bitrix\Main\Loader;
use Bitrix\UI\Format\BBCode\Converter;
Loader::requireModule('ui');
$bb = '[b]Скидка 20%[/b] на [url=https://example.com/catalog/]весь каталог[/url]';
$html = Converter::toHtml($bb); // для страницы
$text = Converter::toPlainText($bb); // для meta description, писем, поиска
toHtml() пропускает BBCode через CTextParser и CBXSanitizer с белым списком тегов. В списке есть iframe (под видео) и атрибут style у p и img. Если результат выводится туда, где iframe недопустим, фильтруйте его дополнительно.
Кнопку Copilot в редакторе поля включает новое событие ui:onRichTextUserFieldGetCopilotOptions. Шаблон main.edit передаёт в него userField, берёт первый успешный EventResult, в параметрах которого лежит массив copilot, и добавляет кнопку в тулбар. Какие ключи ждёт сам массив, из релиза не видно, поэтому в примере он вынесен в заглушку:
<?php declare(strict_types=1);
namespace Vendor\Catalog\Integration\Ui;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
final class RichTextCopilotHandler
{
public static function handle(Event $event): ?EventResult
{
$userField = (array)$event->getParameter('userField');
if (($userField['FIELD_NAME'] ?? '') !== 'UF_PRODUCT_DESCRIPTION') {
return null;
}
return new EventResult(EventResult::SUCCESS, ['copilot' => self::copilotOptions()]);
}
private static function copilotOptions(): array
{
return []; // формат опций в релизе не описан
}
}
Регистрируйте обработчик в install/index.php своего модуля через EventManager::registerEventHandler('ui', 'onRichTextUserFieldGetCopilotOptions', ...). Пустой массив copilot шаблон тоже примет и покажет кнопку.
Кнопкам дали системное меню
Bitrix\UI\Buttons\Button::setSystemMenu(?array $options) (или ключ systemMenu в параметрах кнопки) вешает на кнопку меню из ui.system.menu вместо main.popup. Меню у кнопки одно. Если заданы оба, системное побеждает, а про выброшенный menu в лог пишется строка через AddMessage2Log. Туда же уходит предупреждение, если в опциях нет непустого списка items, и меню тогда не ставится вовсе. null снимает системное меню. Стрелку дропдауна метод при этом убирает, только если сам её добавил.
На стороне JS за это отвечают новые MenuController и SystemMenuController в ui.buttons. Сам ui.system.menu заодно получил клавиатурную навигацию: стрелки, Home/End и поиск по первым буквам без учёта диакритики.
Группу прав можно закрыть от правок
UserGroup из ui.accessrights.v2 получил setIsReadOnly(bool) и isReadOnly(), флаг уходит в toArray(). В JS-сторе редактора прав такую группу нельзя переименовать, удалить или поменять её участников, каждая попытка заканчивается console.warn. Смену значений прав для неё стор пропускает молча, потому что массовые действия, как сказано в комментарии к коду, законно задевают все показанные группы. В PHP-части ui это только флаг в DTO, серверной проверки при сохранении в диффе нет.
Что добавилось в JS
ui.system.popoverиui.system.popover.vue— новый поповер на TypeScript поверхui.floating-ui. В модуле ui его пока никто не подключает.ui.floating-uiупаковывает @floating-ui/dom 1.8.0 вBX.UI.FloatingUi. README называет расширение внутренней библиотекой с единственным потребителем, поповером.ui.mermaidделает то же с mermaid 11.15.0 (BX.UI.Mermaid), бандл на 133 тысячи строк. Тоже пока без потребителей.ui.entity-selectorиui.uploader.coreпереписаны на TypeScript и получили.d.ts. Экспорты entity-selector те же, у uploader добавились два контроллера, удалённых нет.ui.a11yполучил focus trap и проверку интерактивности элементов, его подключилиui.counterpanel,ui.navigationpanel,ui.system.menuиui.uploader.tile-widget. В entity-selector появился announcer, который объявляет скринридеру результаты поиска.
Удалённых расширений нет. Из 70 удалённых файлов 55 приходятся на старые .js-исходники ui.entity-selector и ui.uploader.core, переписанные на .ts. Остальные 15 оказались юнит-тестами (entity-selector/test/*, uploader/core/test/*.test.js), и их убрали без .ts-замены.
Мелочи и находки
- Parsedown 1.8.0 лежит в
ui/vendorи регистрируется вinclude.phpмодуля как глобальный классParsedown. В коде ui он пока не используется. Вui.mermaidесть комментарий про markdown viewer, который вставляет SVG-диаграммы в sandbox-iframe без скриптов, так что похоже на заготовку под просмотр markdown. Если у проекта свойerusev/parsedown, проверьте, какой из двух классовParsedownу вас реально загружается. - В комментарии к
writePartCloud()описан off-by-one.CCloudStorageUpload::Partждёт индекс с нуля, а номера частей идут с единицы. Без конвертации части уехали бы в S3 под номерами 2..N+1, и строгие провайдеры отклонили быCompleteMultipartUpload. - Гонку за финализацию разводит
TempFileTable::updateByFilter(). Атомарный UPDATE с условиемUPLOADED = falseвозвращает число затронутых строк, и коммитит тот, кому досталась одна. В PHPDoc метода написано «not exposed by stock DataManager yet». writePartLocal()игнорируетContent-Rangeот клиента и считает смещение сам, чтобы часть нельзя было подложить в произвольное место файла.- Playwright-фикстуру авторизации
ui/test/e2e/auth/index.tsпереписали, файл вырос с 31 до 286 строк, а в поставку добавились e2e-хелперы поповера и tile-widget. Паролей в коде нет, они читаются из переменных окружения. ui.floating-uiвезёт в дистрибутиве собственный rollup-плагин сборки, и chef собрал для него.min.jsи.map.js.- Кнопки
ui.button.panelполучилиdata-testid.CHTTP::urlAddParamsзаменён наBitrix\Main\Web\UriвManualиInfoHelper, аCHTTP::urlDeleteParamsв админском шаблонеui.toolbar. VERSION_DATEу 26.600.0 стоит 23 июня 2026, на месяц раньше, чем у предыдущей 26.550.0 (23 июля).
Что делать
- После обновления проверьте, что в БД есть
b_ui_file_uploader_temp_file_partи новые колонкиb_ui_file_uploader_temp_file. - Если сайт подключает
ui.bootstrap4, пройдитесь по страницам с его JS-компонентами: скрипт стал 4.6.2, а CSS остался прежним (5.0.0-alpha1). - Найдите наследников
UploaderиController\FileUploaderи добавьте вupload()/uploadAction()параметр?string $strategy = null. - Parallel и presigned включайте после проверки версии clouds, файловой системы временного каталога и
canUpload()своих контроллеров. Запрос соstrategy=parallelсервер примет и при выключенной настройке.