Catalog 26.400.0: остатки по складам в REST теперь режутся правами
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
Catalog 26.400.0 затрагивает 145 файлов и больше двадцати тысяч строк, но почти весь объём дали пересобранные JS-бандлы формы товаров и дубли файлов из install/. Содержательных изменений три. REST-выдача остатков и строк складских документов начала учитывать права на склады, установщик модуля переехал с install.sql на миграции ядра, а форма товаров научилась работать со ставками НДС. Первое касается интеграций, которые ходят в REST не под администратором на установке с модулем crm. Без crm выдача остатков правами не режется.
Проверьте REST-интеграции со складами
До этого релиза контроллер Bitrix\Catalog\Controller\StoreProduct брал listAction из общего трейта ListAction, и фильтра по складам в нём не было. Теперь listAction у контроллера свой. getAction по-прежнему приходит из трейта GetAction, но переопределённый protected get($id) читает запись через getByPrimary() с фильтром доступа. Оба пути добавляют к запросу getEntityFilter(ActionDictionary::ACTION_STORE_VIEW, StoreProductTable::class) текущего AccessController, общее число строк тоже считается по итоговому фильтру.
Для StoreProductTable класс StoreViewFilter сразу уходит в getProductFilter(), а тот возвращает пустой фильтр администратору и в legacy-режиме, то есть когда модуль crm не установлен или идёт установка. На редакциях без crm REST-выдача остатков правами не режется вообще. В режиме с CRM пользователь с доступом к двум складам из пяти получит остатки только по двум.
Со строками складских документов (Controller\Document\Element::listAction) обошлись строже. К фильтру по типам документов добавился фильтр по складам, а новый getList() обнуляет поля там, куда пользователю смотреть нельзя:
STORE_FROM,STORE_TO,AMOUNTиPURCHASING_PRICEстановятсяnull, если склад строки не входит в разрешённые;PURCHASING_PRICEстановитсяnullбез права на закупочные цены (ACTION_PRODUCT_PURCHASE_INFO_VIEW).
Вдобавок RestView\DocumentElement вешает на эти поля атрибуты DISABLED_FILTER и DISABLED_ORDER для всех, у кого нет полного доступа к складам. Отфильтровать или отсортировать строки по AMOUNT такой пользователь уже не сможет.
Для остальных сущностей поменялась сама проверка прав. Раньше StoreViewFilter досрочно выходил только для администратора, а пользователь с правом на все склады получал пустой фильтр уже внутри getStoreFilter() и getDocumentFilter(). Теперь для всех сущностей, кроме StoreProductTable (документы и их строки, склады, отгрузки), решение принимает checkCompleteRight() контроллера доступа, который для админа и в legacy-режиме вызывает обычный check(). Условие legacy-режима вынесли в публичный isLegacyAccessMode().
Если вы читаете остатки через ORM в своём коде и хотите показывать то же, что отдаст REST, фильтр можно взять у того же контроллера доступа:
<?php declare(strict_types=1);
use Bitrix\Catalog\Access\AccessController;
use Bitrix\Catalog\Access\ActionDictionary;
use Bitrix\Catalog\StoreProductTable;
use Bitrix\Main\Loader;
Loader::requireModule('catalog');
function fetchVisibleStock(int $productId): array
{
$accessFilter = AccessController::getCurrent()->getEntityFilter(
ActionDictionary::ACTION_STORE_VIEW,
StoreProductTable::class,
) ?? [];
return StoreProductTable::getList([
'select' => ['STORE_ID', 'AMOUNT', 'QUANTITY_RESERVED'],
// условия во вложенных массивах, чтобы OR из своего фильтра не обошёл доступ
'filter' => [
['=PRODUCT_ID' => $productId],
$accessFilter,
],
])->fetchAll();
}
Так же фильтр собирает само ядро. В прежней версии Document\Element это объяснял комментарий: условия объединяются через новый массив, чтобы OR не обходил фильтр доступа.
Не ищите install.sql
Файлы install/db/{mysql,pgsql}/install.sql и uninstall.sql удалены. InstallDB() теперь вызывает installMigrations(), UnInstallDB() вызывает uninstallMigrations(), оба метода модуль наследует от CModule. Таблицы описаны в install/migrations/tables.php, 72 обработчика событий лежат в events.php, агент SystemField::execAgent() в agents.php. В migration_config.json в качестве defaultTableName указана b_catalog_iblock.
Скрипты разворачивания или сравнения схемы, которые читали catalog/install/db/mysql/install.sql, после обновления его не найдут.
Итог строки в форме товаров теперь с НДС
BasketItem::getSum() в интеграции с JS-формой товаров считает итог строки от price, а не от priceExclusive. К методу добавили комментарий с примером: в режиме «НДС сверху» (taxIncluded = 'N') в priceExclusive лежит цена нетто, и строка с товаром за 100 и ставкой 22% показывала 100 вместо 122. В JS-модели заодно перестали умножать налог на количество, taxSum теперь хранится на всю строку.
Если вы передаёте ставки в форму из своего кода, переименуйте опцию. Вместо taxList форма ждёт taxRateList, массив объектов {taxId, value}. По комментарию в tax.js, value: null означает ставку «без НДС» (EXCLUDE_VAT = 'Y'). Тип BasketTax удалён. Событие changeTax по-прежнему отдаёт taxId и taxValue, но смысл полей другой: раньше в taxId приходил индекс ставки в массиве taxList, теперь приходит ID ставки, а taxValue может быть null. Список удобно собрать из справочника ставок каталога:
<?php declare(strict_types=1);
use Bitrix\Catalog\VatTable;
use Bitrix\Main\Loader;
Loader::requireModule('catalog');
$taxRateList = array_map(
static fn(array $vat): array => [
'taxId' => (int)$vat['ID'],
'value' => $vat['EXCLUDE_VAT'] === 'Y' ? null : (float)$vat['RATE'],
],
VatTable::getList([
'select' => ['ID', 'RATE', 'EXCLUDE_VAT'],
'filter' => ['=ACTIVE' => 'Y'],
])->fetchAll(),
);
Сверьте ссылки в письмах о поступлении товара
Ссылки для писем подписки на товар строят SubscribeTable::prepareDataForNotice() и SubscribeManager. Макросы CHECKOUT_URL_PARAMETERS, UNSUBSCRIBE_URL_PARAMETERS и URL_PARAMETERS раньше строились через CHTTP::urlAddParams('', ...), теперь через Uri::getQuery(), а полные ссылки через (new Uri(...))->addParams().
Сам дифф разницу в формате не показывает, она видна по коду main в эталонной установке. CHTTP::urlAddParams('', ...) возвращал строку с ведущим ? и без кодирования значений, например ?userContact=user@example.com. Uri::getQuery() отдаёт её без ? и кодирует значения по RFC 3986, так что @ превращается в %40. Шаблон вида …/catalog/item/#URL_PARAMETERS# после обновления получит ссылку без вопросительного знака. Если эти макросы есть в ваших почтовых шаблонах, отправьте тестовое письмо и сравните ссылку с прежней.
Новое: настройки НДС в форме товаров
Пункты про налоги в настройках формы раньше были закомментированы, а showTaxBlock выключался принудительно. Теперь их включает опция showTaxSettingsSwitcher: 'Y'. По умолчанию она 'N', так что существующие формы выглядят как раньше. С опцией в настройках появляются пункты «Налог включен в цену» и «Показать налоги», а рядом со ставкой в строке выводится сумма налога. Первый добавленный товар задаёт форме режим TAX_INCLUDED. Цены следующих товаров пересчитываются под этот режим новым ProductCalculator.convertBasePriceForTaxIncluded(), и такая строка получает isCustomPrice: 'Y'.
Ещё в форме появились опция displayPrecision (по умолчанию 2 знака) и хелпер MoneyInput для полей цены, скидки и итога. Он чистит ввод, не сбивая курсор, меняет запятую на точку и отправляет значение с задержкой 500 мс.
БД
Схема не менялась. tables.php описывает те же 48 таблиц, что и старый MySQL-скрипт, с теми же колонками, типами, значениями по умолчанию и индексами. Одно расхождение касается PostgreSQL. В старом pgsql/install.sql у TIMESTAMP_X в b_catalog_price, b_catalog_product, b_catalog_discount и b_catalog_vat стоял DEFAULT CURRENT_TIMESTAMP, в миграции его нет. Какой DDL билдер соберёт для PostgreSQL, по диффу не видно. Существующих баз это не касается, ALTER в релизе нет.
Мелочи и находки
- При удалении модуля старый
UnInstallDB()регистрировал обработчикPropertyFeature::OnPropertyFeatureBuildListзаново, потому что в коде стоялregisterEventHandlerвместоunRegisterEventHandler.OnIBlockElementDeleteснимали для классаCProduct, хотя вешали наCCatalogProduct, аOnGroupDeleteне снимали вовсе. Теперь список обработчиков один, вevents.php. - Вызовы
CHTTP::urlAddParams()иurlDeleteParams()вычистили из семи файлов модуля в пользуBitrix\Main\Web\Uri. - В
core_tree.js(дерево условий правил) один объект параметров логики мутировался и уходил во все связки уровня. Теперь каждая связка получает свою копию. - Выбор вариантов в
sku-treeразмечен как ARIAradiogroupс навигацией стрелками, аproduct-selectorвозвращает фокус после выбора с клавиатуры. В комментариях к этим правкам встречаются метки «TPL-01» и «(B7)», похожие на номера задач. VERSION_DATEу 26.400.0 указан 16 июля 2026, на три недели раньше, чем у предыдущей 26.300.100 (8 августа).- Deprecated в релизе нет.
Что делать
- Если на установке есть модуль
crm, прогоните REST-интеграции, которые читаютStoreProductи строки складских документов, под тем пользователем, под которым они работают. Проверьте, что выгрузка не опирается на фильтр или сортировку поAMOUNT,STORE_FROM,STORE_TOиPURCHASING_PRICE. - Поищите в скриптах и CI обращения к
catalog/install/db/*/install.sql. - Если передаёте в
catalog.product-formопциюtaxList, замените её наtaxRateList. - Если в шаблонах писем о поступлении товара есть
*_URL_PARAMETERS, отправьте тестовое письмо.
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
catalog.product-form: taxList
|
Удалено |
taxRateList
|