Sale 26.600.0: закрыты дыры в правах контроллеров и REST, появился провайдер покупателей
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
Как и 26.500.0, Sale 26.600.0 закрывает пачку дыр в безопасности, на этот раз в правах контроллеров и REST. Контроллеры статистики интеграции с Б24 и синхронизатора не проверяли права на модуль sale, а sale.order.payment.change открывал чужую оплату; REST заявок доставки не сверял владельца службы, через sale.cashbox.update можно было перепривязать кассу к другому REST-обработчику, а допуслуги доставки и агент Маркета принимали внешние данные как есть. Обновиться стоит, но сначала проверьте вызовы Connector\Manager::add(), REST-приложения на sale.paysystem.handler.* и sale.cashbox.update, свои допуслуги доставки и CSS/JS сайта поверх шаблонов bootstrap_v4.
Новое
Берите покупателей с суммами оплат из BuyerProvider
Bitrix\Sale\Public\Provider\BuyerProvider отдаёт покупателей со статистикой оплат: сумма, число полностью и частично оплаченных заказов, дата последнего заказа и данные пользователя. Одна строка списка соответствует покупателю на одном сайте в одной валюте, так что кто платил в рублях и в тенге, придёт двумя строками. BuyerFilter принимает условия в формате админ-фильтра (>=SUM_PAID, %USER.PERSONAL_PHONE) и молча выкидывает поля не из своего белого списка. BuyerSort сортирует по одному полю, незнакомое заменяет на LAST_ORDER_DATE. Раньше для такого отчёта писали запрос к BuyerStatisticTable с джойном пользователя.
Лучшие покупатели сайта за всё время (PHP 8.2+):
<?php
declare(strict_types=1);
namespace Vendor\Shop\Report;
use Bitrix\Main\Loader;
use Bitrix\Main\Provider\Params\Pager;
use Bitrix\Sale\Public\Dto\BuyerStatistic;
use Bitrix\Sale\Public\Provider\BuyerProvider;
use Bitrix\Sale\Public\Provider\Params\BuyerFilter;
use Bitrix\Sale\Public\Provider\Params\BuyerSort;
final readonly class TopBuyersReport
{
public function __construct(
private string $siteId,
private string $currency,
private int $pageSize = 20,
) {
}
/**
* @return array{total: int, rows: list<array{userId: int, email: string, paid: float, lastOrder: ?string}>}
*/
public function page(float $minPaid, int $page = 1): array
{
Loader::requireModule('sale');
$provider = new BuyerProvider();
$filter = new BuyerFilter([
'=LID' => $this->siteId,
'=CURRENCY' => $this->currency,
'>=SUM_PAID' => $minPaid,
]);
$rows = $provider->getBuyers(
$filter,
new BuyerSort(['SUM_PAID' => 'DESC']),
(new Pager($this->pageSize))->setPage($page),
);
return [
'total' => $provider->countBuyers($filter),
'rows' => $rows->map(static fn(BuyerStatistic $row): array => [
'userId' => $row->userId,
'email' => $row->email,
'paid' => $row->sumPaid,
'lastOrder' => $row->lastOrderDate?->format('Y-m-d'),
]),
];
}
}
На провайдер переписана админка «Покупатели», и строки в ней теперь тоже в разрезе сайта и валюты.
Корзина и оформление заказа работают с клавиатуры
Шаблоны bootstrap_v4 корзины, оформления, личного кабинета и смены оплаты, а также все шаблоны sale.basket.basket.line переделаны под доступность. Корзина, оформление, список и карточка заказа в личном кабинете, пополнение счёта и малая корзина сами подключают ui.a11y. Кнопки количества, удаления и закрытия стали настоящими button, до них доходит Tab, и у каждой есть подпись для скринридера. Варианты товара размечены как группа радиокнопок, результат применения купона скринридер зачитывает вслух. В sale.order.ajax фокус не уходит из попапа согласия на обработку данных, окно оплаты в sale.payment-pay стало диалогом с удержанием фокуса. Копии шаблонов в local/ ничего из этого не получат, но LiveAnnouncer и FocusTrap из ui.a11y можно подключить к себе.
Отключите единицу измерения в чеках ЮKassa на ФФД 1.05
У кассы ЮKassa в блоке «Единицы измерения» появился флажок «Передавать единицу измерения» (MEASURE[SEND], по умолчанию включён). Тег 2108 есть только в ФФД 1.2, касса на ФФД 1.05 чек с ним отклоняет. Снимите флажок, и поле measure пропадёт из позиций чека.
Запускайте оплату в режиме менеджера
sale.entity.initiatepay теперь проверяет, доступна ли платёжная система по ограничениям, и по умолчанию смотрит глазами покупателя. Если ваш код вызывает InitiatePayAction со стороны менеджера, переключите режим:
$action = (new InitiatePayAction('initiatepay', $this))
->setRestrictionMode(Restrictions\Manager::MODE_MANAGER);
$result = $action->run([
'PAYMENT_ID' => $paymentId,
'PAY_SYSTEM_ID' => $paySystemId,
'ACCESS_CODE' => $order->getHash(),
]);
Restrictions здесь из Bitrix\Sale\Services\PaySystem, другие значения метод не примет.
Что сломается
Передайте хост в Connector\Manager::add()
Bitrix\Sale\Exchange\Integration\Connector\Manager::add() стал add(string $externalServerHost): bool. Раньше метод сам брал хост из опции server_name или заголовка Host, теперь его передаёт вызывающий код. Вызов без аргумента упадёт с ArgumentCountError. Если провайдер не зарегистрировался, метод удалит токен и вернёт false, результат нужно проверять. Штатная страница настроек синхронизатора уже переписана, так что касается это самописного подключения интеграции с Б24.
Приведите поля платёжных обработчиков и касс к строгому формату
sale.paysystem.handler.addи.update. Каждый элементSETTINGS[FORM_DATA|CHECKOUT_DATA|IFRAME_DATA][FIELDS]должен быть массивом с непустым строковымCODEили сCODE-массивом, иначе придёт ошибка «contains invalid field». В методеupdateзначениеCLIENT_TYPEтеперь проверяется вFIELDS[SETTINGS]. Раньше проверка по ошибке смотрела вSETTINGS, и неверныйCLIENT_TYPEвFIELDS[SETTINGS]сохранялся без ошибки. СамFIELDS[SETTINGS]обязан быть массивом. Уже сохранённые обработчики с полями-строками новая проверка не трогает: при оплате ядро приводит такое поле к['CODE' => строка].sale.cashbox.update.FIELDS[SETTINGS]тоже только массив.SETTINGS[REST][REST_CODE]всегда перезаписывается текущим значением кассы, перепривязать кассу к другому REST-обработчику через update больше нельзя.sale.delivery.request.update,.deleteи.sendmessageвернутAccessException, если служба доставки или родитель профиля не принадлежит вызывающему приложению.
Проверьте права у вызовов контроллеров
Интеграции и скрипты, которым не хватает прав, теперь получат отказ:
sale.synchronizer.getDefaultSettingsтребует прав на модуль sale вышеD,setDefaultSettingsтребуетW;sale.integration.statistic.*иsale.integration.statisticprovider.*на чтение требуют права на sale вышеD, на изменение ровноW;sale.propertyrelation.deleteByFilterпроверяет право на изменение, а не на чтение;sale.entity.initiatepayотвечаетINITIATE_PAY_ACTION_PAYMENT_SERVICE_NOT_FOUND, если платёжная система недоступна покупателю по ограничениям.
В sale.order.payment.change авторизованный пользователь, который не владеет заказом, получит ошибку, что заказа нет. Если менеджеры оплачивают заказы клиентов через публичную часть, этот сценарий перестанет работать.
Проверьте свои допуслуги доставки
sale.order.ajax больше не принимает DELIVERY_EXTRA_SERVICES из запроса как есть. Проходят только услуги, которые покупатель может менять (canUserEditValue()): Enum с существующим ключом цены, услуги типа Y/N со значением Y или N, Quantity и Lift. Значения остальных отбрасываются, включая ваших наследников ExtraServices\Base. Quantity и Lift сверяются по точному имени класса, поэтому наследник Quantity тоже не пройдёт. Если своя допуслуга должна выбираться при оформлении, наследуйте её от Enum или задайте ей тип Y/N.
Сверьте CSS и JS сайта с разметкой bootstrap_v4
Если шаблоны bootstrap_v4 компонентов sale подключены из ядра, после обновления разметка поменяется. Кнопки количества, удаления и закрытия были span, ссылки-фильтры корзины были a, теперь все они button, у фильтров добавился aria-pressed. Классы d-none d-sm-block и d-md-block в ячейках корзины заменены на basket-cell-*, в #basket-item-table появился скрытый <thead>.
Атрибуты data-entity остались, и скрипты на них переживут обновление. А вот селекторы вроде span.basket-item-amount-btn-minus или a.basket-items-list-header-filter-item перестанут находить элементы. Языковой ключ SBB_BASKET_ITEM_WARNING заменён на SBB_BASKET_ITEM_WARNING_MSGVER_1, SBB_COUPON удалён, и их переопределения перестанут действовать.
Не сравнивайте процент скидки с int
Sale\DiscountBase::calculateDiscountPercent() округляет процент через round() и возвращает float. В корзине, оформлении, малой корзине и деталях заказа DISCOUNT_PRICE_PERCENT может вырасти на единицу: при скидке 33,6% покупатель видел 33, увидит 34. Строгое сравнение === 34 в вашем коде перестанет срабатывать, там теперь 34.0.
Мелочи и находки
Sale\StatusBase::getUserGroups()для пользователя, отличного от текущего, берёт группы черезCUser::GetUserGroup(). Учитываются сроки членства и активность группы, добавляется группа 2, так что права таких пользователей на статусы заказа могут поменяться.- Обработчик Альфа-Банка переводит сумму в копейки через
roundвместо усечения float, а при проверке статуса сравнивает суммы с копейками. Раньше сравнивались только целые рубли.