Documentgenerator 26.300.0: file.delete требует права на шаблоны и закрыт для REST
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
До 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.0VERSION_DATE2026-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) в конец дописан перевод строки.