salescenter 26.200.0 Безопасность

Salescenter 26.200.0: терминальная оплата проверяет право на чтение элемента CRM

2 мин чтения Устаревших API: 1

Обновление безопасности

Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.

В salescenter 26.200.0 (модуль «Продажи в чате» коробочного Битрикс24) экшен createTerminalPaymentAction() начал проверять право на чтение элемента CRM, для которого создаётся терминальная оплата. Вокруг НДС в слайдере приёма оплаты поменялись формат ставок в компоненте salescenter.app и смысл полей price и priceExclusive в ответе refreshBasket, а модулю теперь нужен catalog 26.400.0. В коробку версия вышла 22 сентября 2026 года (дата из официального канала «Битрикс24 changelog») вместе с catalog 26.400.0. Обновление заденет тех, кто вызывает экшены Order из своего PHP-кода, держит свой шаблон и JS слайдера или сам разбирает ответ refreshBasket.

Терминальная оплата требует права на чтение элемента CRM

В 26.100.0 права не проверял ни сам Bitrix\SalesCenter\Controller\Order::createTerminalPaymentAction(), ни Crm\Service\Sale\Terminal\PaymentService::createByProducts() из crm. Пользователь интранета мог создать терминальную оплату на любой элемент CRM, а к оплате подтягивался телефон основного контакта.

Теперь после загрузки элемента по ownerTypeId и ownerId экшен проверяет $userPermissions->item()->canReadItem($item). Без права на чтение он добавляет ошибку с текстом «Недостаточно прав для просмотра элемента CRM» (фраза SALESCENTER_CONTROLLER_ORDER_TERMINAL_PAYMENT_ACCESS_DENIED, код ошибки не задан) и возвращает пустой массив. Объект Bitrix\Crm\Service\UserPermissions контроллер получает через новый Order::getAutoWiredParameters(), поэтому AJAX-вызовы из слайдера работают как раньше.

Соседний updateTerminalPaymentAction(int $paymentId, array $options = []) проверку прав в экшене не получил. Мы поискали в crm/lib/Service/Sale/Terminal/PaymentService.php вхождения permission, canRead и canUpdate и не нашли ни одного.

Если в вашем модуле есть экшены, которые берут тип и ID элемента CRM из запроса, добавьте в них ту же проверку. UserPermissions удобно получать автоподстановкой, как теперь делает Order:

        <?php declare(strict_types=1);

namespace Vendor\Shop\Infrastructure\Controller;

use Bitrix\Crm\Service\Container;
use Bitrix\Crm\Service\UserPermissions;
use Bitrix\Main\Engine\AutoWire\Parameter;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;

final class Document extends Controller
{
    protected function init(): void
    {
        parent::init();
        Loader::requireModule('crm');
    }

    public function getAutoWiredParameters(): array
    {
        return [
            ...parent::getAutoWiredParameters(),
            new Parameter(
                UserPermissions::class,
                static fn(): UserPermissions => Container::getInstance()->getUserPermissions(),
            ),
        ];
    }

    public function createAction(UserPermissions $permissions, int $ownerTypeId, int $ownerId): ?array
    {
        $item = Container::getInstance()->getFactory($ownerTypeId)?->getItem($ownerId);
        if ($item === null || !$permissions->item()->canReadItem($item)) {
            $this->addError(new Error('Нет права на чтение элемента CRM', 'ACCESS_DENIED'));

            return null;
        }

        // ...работа с $item
        return ['id' => $item->getId()];
    }
}

    

getFactory() вернёт null для неизвестного типа, getItem() для несуществующего ID, и оба случая уходят в ту же ветку с ошибкой, что и отказ в правах.

Что сломается

Передайте UserPermissions в createTerminalPaymentAction()

Сигнатура экшена теперь (UserPermissions $userPermissions, array $basketItems = [], array $options = []). Код, который вызывал его из PHP с позиционными $basketItems, $options, передаст массив корзины в первый параметр и получит TypeError. Передавайте первым аргументом UserPermissions, например Container::getInstance()->getUserPermissions() из crm.

vatList в salescenter.app стал vatRateList

Раньше $arResult['vatList'] был отсортированным списком ставок-чисел. Теперь вместо него $arResult['vatRateList'], массив элементов ['taxId' => int, 'value' => ...], отсортированный по значению ставки, а при равных значениях по исходному порядку. template.php отдаёт весь $arResult в JS как options через Json::encode(), так что переименование видно и в JS. Дальше product.js передаёт список в catalog.product-form опцией taxRateList вместо taxList, эту опцию мы разбирали в статье про catalog 26.400.0.

Кастомный шаблон или JS, который читает vatList, после обновления останется без ставок. Если нужен прежний плоский список, его можно собрать из нового:

        <?php declare(strict_types=1);

if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
    die();
}

/** @var array $arResult */

// плоский список значений, как в прежнем vatList
$vatValues = array_column($arResult['vatRateList'] ?? [], 'value');

    

В JS то же самое даёт options.vatRateList.map((rate) => rate.value).

Ответ refreshBasketAction() считается иначе

В ответе Order::refreshBasketAction() поменялся смысл полей позиции и итогов:

  • price теперь брутто, priceExclusive нетто, а раньше в оба поля шёл BasketItem::getPrice();
  • добавились поля vatAmount, vatRate, vatIncluded и taxSum;
  • discount переводится из брутто-координат sale в нетто через DiscountFormatter::grossToNet(), discountRate округляется до 4 знаков вместо 2;
  • блок total считает сервис sale.basketCalculator (private aggregateCurrentPaymentTotals()) вместо ручного суммирования, скидка в итогах тоже переводится в нетто через grossToNet().

Если ваш JS разбирает этот ответ сам, сверьте его на товаре с НДС сверху (taxIncluded = 'N'). На стороне sale в 26.500.0 конвертер строк той же формы товаров тоже по-новому выбирает цену в зависимости от taxIncluded, подробности в разборе sale 26.500.0.

Без catalog 26.400.0 не обновляйтесь

fillVat() в salescenter.app вызывает BasketItem::setTaxSum() из catalog/lib/v2/Integration/JS/ProductForm/BasketItem.php. В catalog 26.300.100, который стоял у нас до этого обновления, такого метода нет, как нет и опции taxRateList в JS-форме товаров. Если ставите обновления выборочно, берите salescenter 26.200.0 только вместе с catalog 26.400.0.

Цены из слайдера помечаются ручными

Order::processBasketItems() ставит каждой строке isCustomPrice = 'Y'. После сборки заказа buildOrder() вызывает private restoreCustomPriceBasePrices(), и тот возвращает BASE_PRICE и DISCOUNT_PRICE из формы, если Basket::refreshData() перезаписал их ценой каталога. Строки сопоставляются по PRODUCT_ID, товары с расходящимися базовыми ценами в строках пропускаются.

Ещё refreshBasketAction(), refreshDeliveryAction() и getCompatibleDeliverySystemsAction() теперь вызывают VatRate::prepareTaxPrices() перед сборкой заказа, как уже делали экшены, которые сохраняют заказ.

Новое

НДС и суммы модуль теперь считает DI-сервисами sale вместо Bitrix\Sale\Tax\VatCalculator. Это sale.basketItemInputFactory, sale.basketCalculator, sale.basketItemCalculator, sale.vatCalculator и sale.priceRounder. Переход затронул контроллер Order (в том числе obtainPaymentFields()), OrderBuilder::buildPayments(), Component\VatRate и класс компонента salescenter.app.

Новых публичных методов три, ещё у одного поменялось поведение:

  • VatRate::restoreGrossDisplayPrice(array $item): array возвращает брутто в price для строки с taxIncluded = 'N', которую нельзя пересчитать от позиции корзины. Без него цена такой строки уменьшалась на каждом refresh.
  • Bitrix\Salescenter\Internal\Service\DiscountFormatter::grossToNet(float $discountGross, float $vatRatePercent, bool $vatIncluded): float переводит скидку из брутто-координат sale в нетто для UI через sale.vatCalculator. Класс final и лежит в пространстве Internal.
  • Order::getAutoWiredParameters() добавляет к параметрам родителя автоподстановку Bitrix\Crm\Service\UserPermissions.
  • Изменённый VatRate::prepareTaxPrices() пишет в строку taxRate, для taxIncluded = 'N' пересчитывает priceExclusive из брутто-price и поправляет basePrice через private canonicalizeNetBasePrice(), если значение совпало с брутто-вариантом с допуском 0.01.

В $arResult компонента salescenter.app появились pricePrecision из sale.priceRounder, displayPrecision и currencyFormat из CCurrencyLang::GetFormatDescription(), а в итогах getTotalSumList() поле taxSum. В форму товаров JS теперь передаёт pricePrecision, displayPrecision, showTaxSettingsSwitcher: 'Y' и taxIncluded по первой строке корзины. Что включает showTaxSettingsSwitcher, описано в разборе catalog 26.400.0.

Перед отправкой оплаты app.js в трёх местах эмитит JS-событие salescenter.app:onbeforepaymentsend. На него подписан components/stage-blocks/message-mixin.js, который вызывает messageSenderEditor.handleSendAttempt(), если такой метод есть.

БД и установщик

Схема прежняя. Файлы install/db/mysql/install.sql, uninstall.sql и их pgsql-копии удалены, InstallDB() и UnInstallDB() работают через CModule::installMigrations() и uninstallMigrations(). Таблицы b_salescenter_page, b_salescenter_meta и b_salescenter_page_param описаны в install/migrations/tables.php. Колонки и индексы совпадают с удалённым MySQL-скриптом, только целочисленные колонки объявлены без ширины отображения: int вместо int(10) и int(18). ID таблицы b_salescenter_page_param задан шорткатом $table->addId(), он даёт тот же INT NOT NULL AUTO_INCREMENT с первичным ключом.

В install/migrations/events.php лежат 19 обработчиков событий: девять от landing, шесть от sale, два от messageservice и по одному от pull и notifications. В migration_config.json в качестве defaultTableName указана b_salescenter_page, в маппинг каталогов входят install/components и install/js.

При переезде потерялось снятие обработчика crm:OnActivityAdd → CrmManager::onActivityAdd. Оно было только в старом UnInstallDB(), в events.php этот обработчик не попал, и при удалении модуля он больше не снимается.

Мелочи и находки

  • Комментарий в fillVat() описывает исправленную ошибку с двойным НДС при taxIncluded = 'N'. Вызов setPriceExclusive($priceWithVat) заменили на setPriceExclusive($price). В примере из комментария товар за 100 показывался за 122, а к оплате шло 148.84, цифры соответствуют ставке 22%.
  • В комментариях lib/component/vatrate.php упоминаются «jabber #248006» и «ALG-01 / VatRateTest». Тестов в поставке модуля нет.
  • formattedPrice в fillResultBasket() по-прежнему строится из BasketItem::getPrice(), а price рядом уже брутто из калькулятора.
  • Для платёжного обработчика platon расширение иконки захардкожено как ($handler === 'platon') ? 'webp' : 'svg', причём в трёх местах: lib/component/paysystem.php, salescenter.app/class.php и шаблон salescenter.paysystem.
  • В редакторе сообщения вне SMS-контекста выбор и создание пользовательского шаблона показываются только CRM-админам. Вызов прикрыт method_exists($editorLayout, 'setCustomTemplateSelectorShown'), а в комментарии сказано, что оба сеттера приходят вместе в messageservice. В JS так же защищён handleSendAttempt(), с пометкой, что метод приходит из более нового бандла crm.
  • BBCode-плейсхолдер ссылки на оплату получил атрибут salescenterPaymentLink=true (message-mixin.js).
  • Из зависимостей расширения salescenter.app убрали currency и landing.pageobject. Валюты теперь форматирует CurrencyCore из currency.currency-core с loadCurrencyFormat(). Если ваш JS рассчитывал, что эти расширения подтянет salescenter.app, подключите их сами.
  • Svg-иконки касс Бизнес.Ру и платёжных систем Platon оказались обёртками над base64-PNG. Их заменили на webp, у части появились версии -2x. Иконка в component.css из base64-PNG стала встроенным SVG, а в bundle.config.js приложения добавили cssImages: { type: 'inline', maxSize: 7.25 }. На картинки приходится больше двух третей файлов в диффе.
  • В salescenter.page.include вызов \CHTTP::urlAddParams() заменили на Bitrix\Main\Web\Uri::addParams(), а открывающий тег <? на <?php.
  • В модуле появился автосгенерированный .phpstorm.meta.php с картой сервисов salecenter.component.paysystem и salecenter.integration.salemanager. Коды сервисов пишутся через salecenter, без s.
  • Новых @deprecated в релизе нет.

Что делать

  • Если вызываете Order::createTerminalPaymentAction() из PHP, передавайте первым аргументом UserPermissions.
  • Добавьте canReadItem() в свои экшены, которые получают ID элемента CRM из запроса.
  • Найдите vatList в кастомных шаблонах и JS слайдера и перейдите на vatRateList.
  • Ставьте salescenter 26.200.0 только вместе с catalog 26.400.0.
  • Сверьте поля price, priceExclusive и total в своём JS, который читает ответ refreshBasket, на товаре с НДС сверху.
  • Соберите тестовую оплату из слайдера с ценой, отличной от каталожной, и со скидкой, и проверьте цены в получившемся заказе.

Устаревшие и удалённые API в этой версии

Символ Статус Чем заменять
salescenter.app: $arResult['vatList'] Удалено $arResult['vatRateList']

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

voximplant 26.700.0 Ломающее Свежее

Voximplant 26.700.0: оценку качества связи убрали, у notifyAdmins() появился тип

Модуль телефонии voximplant 26.700.0 вышел в коробочный Битрикс24 11 сентября 2026 года, по данным официального канала «Битрикс24 changelog». Из карточки звонка убрали оценку качества связи вместе с её JS-событиями, у `Im::notifyAdmins()` появился тип параметра, а публичные константы `SipStatusInfor...

3 мин
ui 26.687.0 Рутинное Свежее

UI 26.687.0: триал VibePlus в облаке и дизайн чипа TintedBitrixGpt

`Bitrix\UI\Controller\InfoHelper` при активации демо сначала спрашивает у модуля bitrix24, включён ли старт VibePlus, и если да, запускает триал VibePlus вместо обычного демо тарифа. Ветка срабатывает только при подключённом модуле bitrix24 и определённой константе `BX24_HOST_NAME`. В `ui.system.chi...

1 мин
ui 26.675.0 Ломающее Свежее

UI 26.675.0: ui.actionpanel переехал в бандл, а rich_text по-новому обходится с квадратными скобками

Модуль ui 26.675.0 собран 26 августа 2026. Из PHP поменялись `Converter` и `Whitelist` в `Bitrix\UI\Format\BBCode`, и пользовательские поля типа `rich_text` теперь иначе сохраняют и индексируют текст с квадратными скобками. На фронтенде старую панель групповых действий `ui.actionpanel` перевели на б...

4 мин
ui 26.650.0 Рутинное Свежее

UI 26.650.0: клавиатура в пикере реакций и аудио во вьюере

Релиз на 2 МБ, из PHP в нём только `config.php` расширений и номер версии. Пикером реакций теперь можно управлять с клавиатуры (роль `menu`, стрелки, Escape), у него появились опции `priorityReaction` и `contextAction` и методы `focus()` и `destroy()`. Если пикер привязан к кнопке, ссылке или элемен...

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

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

на связи

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

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

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

Войти