documentgenerator 26.300.0 Безопасность

Documentgenerator 26.300.0: file.delete требует права на шаблоны и закрыт для REST

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

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

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

До 26.300.0 любой авторизованный сотрудник портала мог удалить файл генератора документов по ID через documentgenerator.api.file.delete, в том числе PDF и превью документов. Теперь экшен требует права на изменение шаблонов и принимается только из AJAX, FileTable::delete() не удаляет превью и PDF, на которые ещё ссылается документ, а в нумераторах закрыли обход проверки типа в AJAX и подмену базовой секции через settings. Свой код проверьте, если вызываете file.delete из REST или CLI, удаляете превью и PDF через ORM, наследуете FileModel или передаёте settings в numerator.update.

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

Проверьте, кто у вас вызывает file.delete

Своих фильтров у экшена delete в Controller\File раньше не было. Работали общие из Controller\Base::getDefaultPreFilters(): Authentication, HttpMethod, Csrf, CheckScope (он действует только в REST) и IntranetUser для тех, кто не может менять настройки. Теперь в configureActions()['delete'] стоит new CheckPermissions(UserPermissions::ENTITY_TEMPLATES), и без canModifyTemplates() файл не удалить.

Перед вызовом FileTable::delete() экшен сам проверяет, что строка в b_documentgenerator_file существует и FileTable::isReferenced($fileId) возвращает false. Если строки нет или файл занят, в ответе будет ошибка 'Access denied' с кодом DOCGEN_ACCESS_ERROR (Base::ERROR_ACCESS_DENIED). Прежних ORM-ошибок из onBeforeDelete вы больше не увидите, и по ответу уже не отличить отсутствующий файл от занятого. Параметр экшена получил тип, сигнатура теперь deleteAction(int $fileId).

Сильнее всего по интеграциям бьёт новый скоуп. File переопределяет getDefaultPreFilters() и добавляет new Scope(Scope::AJAX), поэтому оба его экшена, upload и delete, принимаются только в AJAX-скоупе. Из REST и CLI придёт «Requested scope is invalid». В documentgenerator/.settings.php включён restIntegration, и через RestManager::onFindMethodDescription() контроллеры модуля доступны из REST. Для File этот путь закрыт, так что приложение или вебхук, который загружал или удалял файлы генератора через REST, после обновления получит ошибку.

У нового File::getDefaultPreFilters() объявлен тип возврата : array, а у родителя Base::getDefaultPreFilters() типа нет. Если в /local/ есть наследник Controller\File, который переопределяет этот метод без типа, он упадёт с fatal error о несовместимой сигнатуре. Допишите : array в своё переопределение.

Превью и PDF не удалить, пока на них ссылается документ

FileTable::onBeforeDelete() раньше блокировал удаление, только если файл стоял в FILE_ID документа или шаблона. Теперь он проверяет ссылки через isReferenced(), который смотрит ещё IMAGE_ID и PDF_ID документа. Код, который вызывает FileTable::delete() для превью или PDF, пока документ на них ссылается, теперь получит ошибку.

Если снять ссылку через DocumentTable::update(), старый файл из PDF_ID или IMAGE_ID удалит сам FileModel. onBeforeUpdate() ставит файл в очередь, onAfterUpdate() удаляет. Отдельный FileTable::delete() после этого не нужен.

Текст ошибки один на все случаи, DOCUMENTGENERATOR_MODEL_FILE_DOCUMENT_EXISTS («К этому файлу привязаны документы»), даже когда файл держит шаблон. Фраза DOCUMENTGENERATOR_MODEL_FILE_TEMPLATE_EXISTS осталась в lang/*/lib/model/file.php, но код её больше не использует. Если вы отличали шаблон от документа по тексту ошибки, после обновления это перестанет работать.

Очередь FileModel принимает только массивы

Элементы FileModel::$filesToDelete (protected static) теперь массивы с ключами fileId и entityClass. deleteFiles() пропускает всё, что не массив. Наследник FileModel, который сам кладёт в очередь скалярные ID, перестанет удалять эти файлы и не выдаст ни одной ошибки. Ставьте файл в очередь так:

        static::$filesToDelete[] = ['fileId' => $fileId, 'entityClass' => static::class];

    

Результат deleteFiles() возвращают onAfterUpdate() и onAfterDelete(), а ORM результаты after-событий не читает (DataManager::callOnAfterDeleteEvent() только отправляет событие). Ошибки удаления из очереди не доходили до вызывающего кода ни раньше, ни теперь, увидеть их можно только в новом логе. Внутри deleteFiles() ошибки раньше перезаписывались через setErrors(), теперь копятся через addError().

Проверьте вызовы нумераторов

Base::getAutoWiredParameters() отдаёт \Bitrix\Main\Numerator\Numerator через ExactParameter, только если getConfig()[$className::getType()]['type'] равен Driver::NUMERATOR_TYPE, то есть 'DOCUMENT'. Для остальных нумераторов контроллер получает ошибку 'Numerator was not found.', а параметр становится null. Замыкание автовайринга пишет ошибку в $controller из use, а если его нет, в $this. CRM-прокси (crm/lib/controller/documentgenerator/base.php) зовёт тот же метод как getAutoWiredParameters($this), поэтому проверка работает и в crm.documentgenerator.numerator.*.

Фильтр CheckNumeratorType раньше был объявлен только для SCOPE_REST. Controller::attachFilters() молча пропускает фильтр, если текущего скоупа нет в его listAllowedScopes(), поэтому в AJAX-вызовах numerator.get, numerator.update и numerator.delete тип нумератора не проверялся совсем. В 26.300.0 в список добавлен Controller::SCOPE_AJAX. CRM-прокси подмешивает префильтры Controller\Numerator, и расширение скоупа касается его тоже.

Базовый ActionFilter\Base::listAllowedScopes() разрешает REST, AJAX и CLI. Если переопределяете его в своём фильтре, перечисляйте все скоупы, где фильтр нужен. В остальных attachFilters() его не подключит и ничего об этом не скажет.

В Numerator::updateAction() поменялся порядок слияния настроек (переносы внутри вызовов убраны):

        // было
$config = array_merge($numerator->getConfig(), [$numerator::getType() => $typeConfig], $fields['settings']);
// стало
unset($fields['settings'][$numerator::getType()]);
$config = array_merge($numerator->getConfig(), $fields['settings'], [$numerator::getType() => $typeConfig]);

    

Раньше ключ Bitrix_Main_Numerator_Numerator в settings из запроса целиком заменял базовую секцию нумератора вместе с type. Теперь этот ключ удаляется, а секция с type, name и template мёржится последней. Из базовой секции Numerator::setNumeratorConfig() в main берёт ещё и idFromDb и записывает его в $this->id, а save() передаёт этот ID в NumeratorTable::saveNumerator($this->id, …) и обновляет строку с ним. Так пользователь с правом на шаблоны мог через idFromDb в settings перезаписать строку любого нумератора в b_numerator, а не только того, что передал в запросе.

Если ваш фронтенд или интеграция отправляли базовую секцию в settings, модуль её теперь отбрасывает.

Новое

FileTable::isReferenced()

Bitrix\DocumentGenerator\Model\FileTable::isReferenced($fileId): bool возвращает true, если на файл ссылается документ (FILE_ID, IMAGE_ID или PDF_ID) или шаблон (FILE_ID). При $fileId <= 0 ответ false. Вызовите его перед FileTable::delete(), и вместо ORM-ошибки с общим текстом про документы вернёте ошибку со своим кодом:

        <?php declare(strict_types=1);

namespace Vendor\Docs\Application\Service;

use Bitrix\DocumentGenerator\Model\FileTable;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;

final class GeneratorFileRemover
{
    public function remove(int $fileId): Result
    {
        $result = new Result();

        if (!Loader::includeModule('documentgenerator')) {
            return $result->addError(new Error('Модуль documentgenerator не установлен', 'DOCGEN_MODULE_MISSING'));
        }

        if (!FileTable::getByPrimary($fileId, ['select' => ['ID']])->fetch()) {
            return $result->addError(new Error("Файл {$fileId} не найден", 'DOCGEN_FILE_NOT_FOUND'));
        }

        if (FileTable::isReferenced($fileId)) {
            return $result->addError(new Error(
                "Файл {$fileId} ещё нужен документу или шаблону",
                'DOCGEN_FILE_REFERENCED',
            ));
        }

        // onBeforeDelete проверит ссылки ещё раз перед удалением
        $deleteResult = FileTable::delete($fileId);
        if (!$deleteResult->isSuccess()) {
            $result->addErrors($deleteResult->getErrors());
        }

        return $result;
    }
}

    

У сервиса разные коды для «файла нет» и «файл занят», а штатный file.delete в обоих случаях отвечает Access denied. Если сервис работает по запросу пользователя, право на шаблоны проверяйте в своём контроллере, как теперь делает file.delete.

Соберите ошибки очереди удаления в отдельный файл

FileModel::deleteFiles() пишет каждое неудачное удаление в логгер documentgenerator.Default с сообщением Could not delete file {fileId} queued by {entityClass}. Errors: {errors}. Логгер создаётся через (new LoggerFactory())->createById('documentgenerator.Default', isCheckEnabledFromRegistry: false). Если в секции loggers в .settings.php записи documentgenerator.Default нет, фабрика берёт логгер по умолчанию. При заданной LOG_FILENAME это main.Default из той же секции, если он настроен, иначе файл из LOG_FILENAME. Без константы сообщение пропадает в NullLogger, даже если main.Default настроен.

Отдельный файл для модуля настраивается так:

        <?php declare(strict_types=1);

return [
    // ...остальные секции .settings.php
    'loggers' => [
        'value' => [
            'documentgenerator.Default' => [
                'className' => \Bitrix\Main\Diag\FileLogger::class,
                'constructorParams' => ['/var/log/bitrix/documentgenerator.log'],
            ],
        ],
        'readonly' => true,
    ],
];

    

Тот же логгер до релиза использовал только lib/integration/transformermanager.php, так что его сообщения попадут в этот же файл.

БД

Новых таблиц и колонок нет, agents.php и events.php не менялись. В декларативной схеме install/migrations/tables.php появились три индекса: ix_documentgenerator_template_file (FILE_ID) на b_documentgenerator_template, ix_documentgenerator_document_image (IMAGE_ID) и ix_documentgenerator_document_pdf (PDF_ID) на b_documentgenerator_document. Индекс ix_docgen_doc_file на FILE_ID документа был и раньше. Вместе они покрывают запросы isReferenced(), то есть OR по FILE_ID, IMAGE_ID и PDF_ID документа и FILE_ID шаблона.

Апдейтер в снапшот не попадает, поэтому добавит ли обновление эти индексы на уже установленных порталах, по диффу не видно. После обновления посмотрите, есть ли они у вас:

        SHOW INDEX FROM b_documentgenerator_document WHERE Column_name IN ('IMAGE_ID', 'PDF_ID');
SHOW INDEX FROM b_documentgenerator_template WHERE Column_name = 'FILE_ID';

    

Если их нет, isReferenced() ищет по IMAGE_ID и PDF_ID документа и по FILE_ID шаблона без индекса, и делает это при каждом удалении файла.

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

  • Пункт changelog «Улучшена совместимость с PHP 8» в коде соответствует трём местам. Docx::fillInnerDocuments() читает связи REL_TYPE_FOOTER, REL_TYPE_HEADER и REL_TYPE_NUMBERING через ?? null, и DOCX без колонтитулов или нумерации больше не даёт «Undefined array key». Docx::addNumberings() проверяет documentPath и document через empty(...). FileTable::onBeforeDelete() проверяет $data && $data['STORAGE_TYPE'] и не обращается к offset у false, когда строки нет. Пункт «Усилена безопасность модуля» соответствует правкам в controller/file.php, controller/base.php, engine/checknumeratortype.php, controller/numerator.php и model/file.php.
  • У шаблонов файл стоит в очереди удаления дважды. TemplateTable::onBeforeDelete() в релизе не менялся. Он кладёт в $filesToDelete голый ID и вызывает parent::onBeforeDelete(), который ставит тот же файл ещё раз. До релиза файл шаблона удалялся дважды. При втором FileTable::delete() строки уже нет, getById() возвращает false, и как раз этот случай закрывает новая проверка $data &&. Теперь скалярный дубль отсекает is_array, а код шаблона так и остался со старым форматом.
  • Ошибки 'Numerator was not found.' (controller/base.php) и 'Access denied' (controller/file.php) не локализованы и приходят в ответе по-английски на портале с любым языком.
  • Новые индексы названы ix_documentgenerator_*, а соседние индексы b_documentgenerator_document называются ix_docgen_*. Такой формат требует IndexNameProcessor::validate() в main (lib/UpdateSystem/Migration/IndexNameProcessor.php), у новых апдейтеров имя индекса должно начинаться с ix_ и имени таблицы, с префиксом b_ или без него.
  • По install/version.php у 26.300.0 VERSION_DATE 2026-09-02 13:08:13, на пять часов раньше, чем у предыдущей 26.200.100 (2026-09-02 18:15:38, разбор).
  • У шести файлов (controller/base.php, controller/file.php, controller/numerator.php, engine/checknumeratortype.php, model/file.php, model/filemodel.php) снят бит исполнения (100755 → 100644), а у четырёх из них (controller/file.php, controller/numerator.php, engine/checknumeratortype.php, model/filemodel.php) в конец дописан перевод строки.

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

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

Подписаться

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

documentgenerator 26.200.0 Ломающее Свежее

Documentgenerator 26.200.0: плейсхолдеры в атрибутах DOCX больше не вычищаются, у DocxXml новые сигнатуры

Генератор документов 26.200.0 заполняет DOCX обходом DOM вместо регулярки по всему XML, и плейсхолдеры в атрибутах разметки, кроме `descr` и `name` у картинок, теперь доходят до готового документа как текст. У `Bitrix\DocumentGenerator\Body\DocxXml` удалён protected-метод и сменились сигнатуры ещё д…

3 мин
documentgenerator 26.200.100 Ломающее Свежее

Documentgenerator 26.200.100: рисунки без файла больше не остаются в размноженных строках DOCX

Documentgenerator 26.200.100 перестал оставлять в разметке DOCX рисунки из размноженных строк, у которых файл картинки не существует или не читается. Если вы наследуете `Docx` и переопределяете protected `replaceImages()`, смените `: void` на `: array` и возвращайте ID связей на удаление, иначе полу…

1 мин
ui 26.693.0 Заметное Свежее

UI 26.693.0: загрузчик называет причину отказа, а вьювер прячет кнопку скачивания

В ui 26.693.0 загрузчик файлов перед сохранением ещё раз проверяет временный файл и при отказе отдаёт текст ошибки из `CFile::CheckFile`, а при превышении дисковой квоты кладёт в ошибку причину `disk_quota_exceeded`. Вьювер теперь прячет кнопку скачивания у отдельного файла, а `FeaturePromoter.show(…

3 мин
socialnetwork 26.250.0 Рутинное Свежее

Socialnetwork 26.250.0: комментарий о том, как дочерние чаты проекта попадают в папку «Все»

В socialnetwork 26.250.0 код не менялся, проверять после обновления нечего. В двух внутренних обработчиках, `SendChildAttachedMessage` и `SendChildDetachedMessage`, появился одинаковый docblock о том, как дочерний чат проекта попадает в папку «Все» мессенджера. Push `chatUpdate` только убирает строк…

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

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

на связи

пишет…

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

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

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

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

Войти