From d0bc450d02b1d52675d2ca567d87d13196dfd8d5 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Fri, 4 Sep 2026 00:19:27 +0300 Subject: [PATCH] =?UTF-8?q?EDITORIAL-333:=20=D0=B2=D1=8B=D1=87=D0=B8=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D1=8C=D1=8E=20?= =?UTF-8?q?=D0=BE=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- editorial/agent-rewrites/333.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editorial/agent-rewrites/333.json b/editorial/agent-rewrites/333.json index d63c368..c662bad 100644 --- a/editorial/agent-rewrites/333.json +++ b/editorial/agent-rewrites/333.json @@ -3,5 +3,5 @@ "slug": "editorial-2018-10-mechanism-image-workflow", "title": "Bitrix: почему preview не означает сохранённую картинку", "excerpt": "В форме уже виден новый preview, но карточка после сохранения показывает старое изображение. Разделяем браузерный файл, запись в b_file и ссылку PREVIEW_PICTURE, чтобы найти разрыв и проверить результат.", - "contentHtml": "

В форме товара появляется новая картинка. Пользователь нажимает «Сохранить», открывает карточку и видит старую обложку. Иногда интерфейс сообщает об успехе, хотя сервер сохранил файл отдельно и не связал его с элементом инфоблока.

\n

Цена ошибки выше, чем один неудачный POST. Оператор повторяет загрузку, каталог показывает устаревшие данные, а в хранилище остаются лишние файлы. При разборе инцидента команда видит preview и решает, что загрузка прошла. Но preview доказывает только состояние браузера.

\n

Тезис статьи простой: изображение проходит несколько границ. Браузер показывает выбранный File. PHP получает multipart-данные. Bitrix регистрирует файл и выдаёт числовой ID. Элемент инфоблока хранит ссылку на этот ID. Успех нужно проверять на каждой границе. Последний обязательный факт — после обновления элемент возвращает ожидаемый PREVIEW_PICTURE.

\n
\"Состояния
Preview принадлежит интерфейсу. Подтверждённая картинка появляется только после связи ID файла с элементом инфоблока.
\n

Четыре состояния вместо одного слова «картинка»

\n

Первое состояние живёт в браузере. Диалог выбора создал объект File, а JavaScript показал его имя, размер или локальный preview. В этот момент сервер ещё ничего не знает. Перезагрузка страницы удалит это состояние.

\n

Второе состояние возникает в HTTP-запросе. Сервер получает поле из multipart/form-data. Имя поля может отличаться от имени, которое видит разработчик в шаблоне: Ajax, вложенная структура и повторная отрисовка часто меняют фактический POST. Поэтому проверяют не DOM, а сетевой запрос и PHP-массив.

\n

Третье состояние создаёт Bitrix. CFile::SaveFile() принимает файловый массив, сохраняет файл и регистрирует его в b_file. Положительный ID означает, что у приложения появился зарегистрированный файл. Он ещё не означает, что файл стал изображением товара.

\n

Четвёртое состояние хранит сам элемент. В поле PREVIEW_PICTURE лежит ссылка на зарегистрированный файл. Её меняет операция обновления элемента. Если обработчик только вызвал SaveFile(), карточка останется со старым ID.

\n
Граница, доказательство и следующий шаг
СостояниеВладелецДоказательствоСледующий шаг
Файл выбранБраузер и DOMЕсть имя, размер или previewНе считать сохранением
Данные пришлиPHP-обработчикОжидаемое поле и UPLOAD_ERR_OKПроверить размер, тип, права и передать дальше
Файл зарегистрированb_fileCFile::SaveFile() вернул ID, GetFileArray() его находитСобрать файловый массив для элемента
Карточка измененаЭлемент инфоблокаCIBlockElement::Update() вернул true, свежее чтение вернуло новый IDПоказать результат новым запросом
\n

Контрол задаёт интерфейс, а не контракт хранения

\n

В старой установке Bitrix форма может использовать \\Bitrix\\Main\\UI\\FileInput. Контрол рисует поля, кнопки, preview и JavaScript. Метод show() выводит интерфейс для текущего ID, но не обновляет элемент инфоблока сам по себе. На границе обработчика всё равно нужны имя поля, файловый массив и явное действие: заменить, оставить или удалить.

\n

Ниже учебный фрагмент для одной картинки. Он показывает настройки интерфейса и не заменяет серверную проверку. Название поля должно совпасть с тем, что реально приходит в POST. Если проект использует другую версию Bitrix или собственный Ajax-адаптер, сначала смотрят фактический запрос.

\n
<?php\nuse Bitrix\\Main\\UI\\FileInput;\n\n$currentFileId = (int) $arResult['PREVIEW_PICTURE'];\n\necho FileInput::createInstance([\n    'id' => 'catalog_preview',\n    'name' => 'CATALOG[PREVIEW_PICTURE]',\n    'upload' => true,\n    'allowUpload' => FileInput::UPLOAD_IMAGES,\n    'medialib' => false,\n    'fileDialog' => true,\n    'cloud' => false,\n    'delete' => true,\n    'edit' => true,\n    'maxCount' => 1,\n    'maxSize' => 5 * 1024 * 1024,\n])->show($currentFileId);\n?>
\n

maxSize помогает интерфейсу показать предел, но пользователь может изменить запрос вручную. allowUpload ограничивает сценарий контрола, но не доверенный источник данных. Сервер заново проверяет размер, фактический тип содержимого, расширение, права и лимит PHP. Заголовок Content-Type нельзя принимать за доказательство типа файла.

\n

Сначала регистрируем файл, потом меняем элемент

\n

Обработчик должен различать ошибку загрузки и ошибку привязки. Не сохраняйте локальный путь из браузера и не используйте имя файла как идентификатор. Получите файловый массив, проверьте его, зарегистрируйте файл, затем соберите массив для Update().

\n
function saveCatalogImage(array $upload): int\n{\n    if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {\n        throw new RuntimeException('Upload did not finish');\n    }\n\n    $size = (int) ($upload['size'] ?? 0);\n    if ($size < 1 || $size > 5 * 1024 * 1024) {\n        throw new RuntimeException('Image size is outside the limit');\n    }\n\n    // Учебный пример: production-код должен добавить проверку типа,\n    // расширения, прав, CSRF и настроек хранилища.\n    $upload['MODULE_ID'] = 'catalog';\n    $fileId = (int) CFile::SaveFile($upload, 'catalog');\n\n    if ($fileId < 1 || !CFile::GetFileArray($fileId)) {\n        throw new RuntimeException('Registered file was not found');\n    }\n\n    return $fileId;\n}\n\n$fileId = saveCatalogImage($_FILES['CATALOG_PREVIEW']);\n$element = new CIBlockElement();\n$picture = CFile::MakeFileArray($fileId);\n\n$updated = $element->Update($elementId, [\n    'PREVIEW_PICTURE' => $picture,\n]);\n\nif (!$updated) {\n    throw new RuntimeException($element->LAST_ERROR);\n}
\n

Это учебный пример порядка операций. В конкретном проекте нужно проверить формат массива, версию API, модуль, права и обработчики событий. GetFileArray() отделяет зарегистрированный ID от случайного числа. MakeFileArray() готовит описание существующего файла. Update() отдельно сообщает, удалось ли изменить элемент. Один положительный ответ не заменяет два других.

\n

Если регистрация файла прошла, а Update() вернул ошибку, не показывайте пользователю успех. Новый файл уже может существовать без связи с карточкой. Политика очистки таких файлов зависит от проекта: временное состояние, очередь уборки или безопасная ручная обработка. Нельзя удалять файл вслепую, если другой элемент успел получить тот же ID.

\n

Симптом → причина → проверка → действие

\n
Диагностика формы с изображением
СимптомПричинаПроверкаДействие
Preview сменился, карточка нетОбновился только DOMСравнить ID до и после свежего чтения элементаПередать новый файловый массив в Update()
POST пустойНет multipart, поле переименовано или Ajax отправляет другой наборПосмотреть Network и $_FILES без содержимого файлаИсправить контракт формы и обработчик
Есть ID, но файл не находитсяСохранение вернуло невалидный результат или ID прочитан не из того поляВызвать GetFileArray($fileId)Остановить привязку и записать безопасную причину
Файл есть, поле староеВызвали SaveFile(), но не обновили элементПроверить вызов Update() и LAST_ERRORРазделить этапы и проверять оба ответа
После ошибки растёт число файловФайл зарегистрирован до неудачной привязкиСопоставить ID файла с элементом и временем операцииОпределить безопасную уборку сиротских записей
Загружается не изображениеДоверие расширению или заголовку клиентаПроверить содержимое, размер, расширение и серверные ограниченияОтклонить файл до регистрации
Удаляется старая картинка без командыПустой preview приняли за флаг удаленияРазличить отсутствие нового файла и явное DELETEСохранить старую картинку, если удаление не подтверждено
\n

Отрицательный путь: нет нового файла и есть удаление

\n

Отсутствие нового файла не равно удалению. Пользователь мог открыть форму, ничего не выбрать и нажать «Сохранить». Для такого запроса правило должно быть явным: нет нового файла и нет флага удаления — оставить старую картинку; есть новый файл — заменить после успешной регистрации и обновления; есть явный флаг удаления — удалить по согласованному контракту.

\n

Не выводите решение из пустого preview. DOM может исчезнуть после Ajax-перерисовки, ошибки загрузки или закрытия диалога. Удаление должно приходить отдельным проверяемым полем, а сервер должен проверить право пользователя и принадлежность текущего файла элементу.

\n

Если FileInput сначала загружает файл отдельным запросом, а потом форма сохраняет карточку, временный ID нельзя считать готовым результатом. Пользователь может закрыть вкладку между запросами. Временный объект должен иметь понятный статус и срок жизни. Финальная операция должна повторно проверить владельца и связь с элементом.

\n

Порядок проверки

\n
  1. Зафиксируйте ID элемента и текущий ID PREVIEW_PICTURE до изменения.
  2. Проверьте форму: method=\"post\", enctype=\"multipart/form-data\", имя поля и CSRF-контракт.
  3. Выберите небольшой учебный JPEG и сравните имя поля в DOM, Network и PHP-массиве.
  4. Проверьте серверные размер, фактический тип, расширение, права и ограничения PHP до вызова SaveFile().
  5. Сохраните файл и подтвердите ID через GetFileArray(). При ошибке остановите процесс.
  6. Соберите файловый массив через MakeFileArray() и вызовите CIBlockElement::Update().
  7. Проверьте оба результата: true от обновления и пустой LAST_ERROR при успехе.
  8. Повторно прочитайте элемент новым запросом и сравните его PREVIEW_PICTURE с ожидаемым ID.
  9. Отдельно проверьте три отрицательных случая: пустой POST, файл неверного типа и явное удаление без нового файла.
  10. Уберите временные логи или оставьте только безопасные идентификаторы операции, элемента, файла и причину отказа.
\n

Ограничения модели

\n

Код выше не является готовым обработчиком для любой версии Bitrix. Он не описывает транзакцию между файловым хранилищем и элементом, антивирус, ресайз, CDN, дисковую квоту, свойства инфоблока, несколько файлов и конкурентное редактирование. Для свойства типа «Файл» формат PROPERTY_VALUES проверяют отдельно. Для административной формы отдельно проверяют права и события Bitrix.

\n

Учебные имена catalog, CATALOG_PREVIEW, лимит 5 MiB и фиксированный сценарий с одной картинкой не являются требованиями production. Пример не доказывает, что конкретная установка принимает любой JPEG, и не даёт production-результатов. Его задача — показать порядок и точки проверки.

\n

Официальная документация Bitrix описывает API, но не знает правила вашего каталога. OWASP перечисляет меры для загрузки файлов, однако набор контролей зависит от угроз, типа данных и архитектуры. Без проверки реального POST, прав и настроек окружения нельзя объявлять интеграцию готовой.

\n

Проверяемый критерий готовности

\n

Сценарий готов к интеграционной проверке, когда для одного тестового элемента можно показать четыре факта: сервер получил ожидаемый файловый массив; Bitrix зарегистрировал новый ID; Update() вернул успех; свежее чтение элемента вернуло этот ID в PREVIEW_PICTURE. Дополнительно пустая отправка сохраняет старый файл, неверный тип отклоняется, а явное удаление не возникает из пустого preview.

\n

Если виден только preview или только ID в b_file, работа не завершена. Источник истины для карточки — поле элемента после успешного обновления. Именно его нужно проверять новым запросом.

\n

Проверяемые источники

\n" + "contentHtml": "

В форме товара появляется новая картинка. Пользователь нажимает «Сохранить», открывает карточку и видит старую обложку. Иногда интерфейс сообщает об успехе, хотя сервер сохранил файл отдельно и не связал его с элементом инфоблока.

\n

Цена ошибки выше, чем один неудачный POST. Оператор повторяет загрузку, каталог показывает устаревшие данные, а в хранилище остаются лишние файлы. При разборе инцидента команда видит preview и решает, что загрузка прошла. Но preview доказывает только состояние браузера.

\n

Тезис статьи простой: изображение проходит несколько границ. Браузер показывает выбранный File. PHP получает multipart-данные. Bitrix регистрирует файл и выдаёт числовой ID. Элемент инфоблока хранит ссылку на этот ID. Успех нужно проверять на каждой границе. Последний обязательный факт — после обновления элемент возвращает ожидаемый PREVIEW_PICTURE.

\n
\"Состояния
Preview принадлежит интерфейсу. Подтверждённая картинка появляется только после связи ID файла с элементом инфоблока.
\n

Четыре состояния вместо одного слова «картинка»

\n

Первое состояние живёт в браузере. Диалог выбора создал объект File, а JavaScript показал его имя, размер или локальный preview. В этот момент сервер ещё ничего не знает. Перезагрузка страницы удалит это состояние.

\n

Второе состояние возникает в HTTP-запросе. Сервер получает поле из multipart/form-data. Имя поля может отличаться от имени, которое видит разработчик в шаблоне: Ajax, вложенная структура и повторная отрисовка часто меняют фактический POST. Поэтому проверяют не DOM, а сетевой запрос и PHP-массив.

\n

Третье состояние создаёт Bitrix. CFile::SaveFile() принимает файловый массив, сохраняет файл и регистрирует его в b_file. Положительный ID подтверждает регистрацию файла, но не его привязку к элементу. Такой раздельный путь требует отдельного шага обновления и проверки его результата.

\n

Четвёртое состояние хранит сам элемент. В поле PREVIEW_PICTURE лежит ID файла, зарегистрированного в файловой таблице. Его меняет обновление элемента. Если обработчик только вызвал SaveFile(), карточка останется со старым ID.

\n
Граница, доказательство и следующий шаг
СостояниеВладелецДоказательствоСледующий шаг
Файл выбранБраузер и DOMЕсть имя, размер или previewНе считать сохранением
Данные пришлиPHP-обработчикОжидаемое поле и UPLOAD_ERR_OKПроверить размер, тип, права и передать дальше
Файл зарегистрированb_fileCFile::SaveFile() вернул ID, GetFileArray() его находитВыполнить согласованный шаг привязки; голый ID не считать контрактом без проверки версии
Карточка измененаЭлемент инфоблокаCIBlockElement::Update() вернул true, свежее чтение вернуло новый IDПоказать результат новым запросом
\n

Контрол задаёт интерфейс, а не контракт хранения

\n

В установке Bitrix, где доступен \\Bitrix\\Main\\UI\\FileInput, контрол рисует поле, кнопки, preview и JavaScript. Метод show() выводит интерфейс для текущего значения, но не обновляет элемент инфоблока сам по себе. На границе обработчика всё равно нужны имя поля, файловый массив и явное действие: заменить, оставить или удалить.

\n

Ниже учебный фрагмент для одной картинки. Он показывает настройки интерфейса и не заменяет серверную проверку. Название поля должно совпасть с тем, что реально приходит в POST. Если проект использует другую версию Bitrix или собственный Ajax-адаптер, сначала смотрят фактический запрос.

\n
<?php\nuse Bitrix\\Main\\UI\\FileInput;\n\n$currentFileId = (int) ($element['PREVIEW_PICTURE'] ?? 0);\n\necho FileInput::createInstance([\n    'id' => 'catalog_preview',\n    'name' => 'CATALOG_PREVIEW',\n    'upload' => false,\n    'allowUpload' => FileInput::UPLOAD_IMAGES,\n    'medialib' => false,\n    'fileDialog' => false,\n    'cloud' => false,\n    'delete' => true,\n    'edit' => true,\n    'maxCount' => 1,\n    'maxSize' => 5 * 1024 * 1024,\n])->show($currentFileId);\n?>
\n

maxSize помогает интерфейсу показать предел, но пользователь может изменить запрос вручную. В этом фрагменте upload => false оставляет один проверяемый multipart-запрос; при AJAX-загрузке контракт будет другим. allowUpload ограничивает сценарий контрола, но не заменяет серверную проверку. Сервер проверяет размер, содержимое изображения, расширение, права и лимит PHP. Заголовок Content-Type нельзя принимать за доказательство типа файла.

\n

Проверяем файл до обновления элемента

\n

Обработчик должен различать ошибку загрузки и ошибку обновления элемента. Не сохраняйте локальный путь из браузера и не используйте имя файла как идентификатор. Получите файловый массив, проверьте его на сервере и передайте проверенный массив в Update(). Bitrix зарегистрирует файл и запишет его ID в поле элемента в рамках этого обновления.

\n
function updateCatalogImage(int $elementId, array $upload): void\n{\n    if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {\n        throw new RuntimeException('Upload did not finish');\n    }\n\n    $upload['MODULE_ID'] = 'iblock';\n    $validationError = CFile::CheckImageFile($upload, 5 * 1024 * 1024);\n    if ($validationError !== '') {\n        throw new RuntimeException($validationError);\n    }\n\n    $element = new CIBlockElement();\n    $updated = $element->Update($elementId, [\n        'PREVIEW_PICTURE' => $upload,\n    ]);\n\n    if (!$updated) {\n        throw new RuntimeException($element->LAST_ERROR);\n    }\n}\n\nupdateCatalogImage($elementId, $_FILES['CATALOG_PREVIEW']);
\n

Это учебный пример одного multipart-сценария. CheckImageFile() проверяет файл до изменения состояния, а Update() сообщает, удалось ли обновить элемент; после этого нужно свежим чтением получить новый ID. В конкретном проекте дополнительно проверяют версию API, модуль, права, CSRF, обработчики событий и настройки хранения. Если архитектура использует отдельный SaveFile(), его ID остаётся промежуточным доказательством: контракт привязки и способ очистки сиротского файла нужно проверять отдельно.

\n

Если отдельная регистрация файла прошла, а Update() вернул ошибку, не показывайте пользователю успех. Новый файл может остаться без связи с карточкой. Политика очистки таких файлов зависит от проекта: временное состояние, очередь уборки или безопасная ручная обработка. Нельзя удалять файл вслепую, если другой элемент успел получить тот же ID.

\n

Симптом → причина → проверка → действие

\n
Диагностика формы с изображением
СимптомПричинаПроверкаДействие
Preview сменился, карточка нетОбновился только DOMСравнить ID до и после свежего чтения элементаПередать новый файловый массив в Update()
POST пустойНет multipart, поле переименовано или Ajax отправляет другой наборПосмотреть Network и $_FILES без содержимого файлаИсправить контракт формы и обработчик
Есть ID, но файл не находитсяСохранение вернуло невалидный результат или ID прочитан не из того поляВызвать GetFileArray($fileId)Остановить привязку и записать безопасную причину
Файл есть, поле староеВызвали SaveFile(), но не обновили элементПроверить вызов Update() и LAST_ERRORРазделить этапы и проверять оба ответа
После ошибки растёт число файловФайл зарегистрирован до неудачной привязкиСопоставить ID файла с элементом и временем операцииОпределить безопасную уборку сиротских записей
Загружается не изображениеДоверие расширению или заголовку клиентаПроверить содержимое, размер, расширение и серверные ограниченияОтклонить файл до регистрации
Удаляется старая картинка без командыПустой preview приняли за флаг удаленияРазличить отсутствие нового файла и явное DELETEСохранить старую картинку, если удаление не подтверждено
\n

Отрицательный путь: нет нового файла и есть удаление

\n

Отсутствие нового файла не равно удалению. Пользователь мог открыть форму, ничего не выбрать и нажать «Сохранить». Для такого запроса правило должно быть явным: нет нового файла и нет флага удаления — оставить старую картинку; есть новый файл — заменить после успешной регистрации и обновления; есть явный флаг удаления — удалить по согласованному контракту.

\n

Не выводите решение из пустого preview. DOM может исчезнуть после Ajax-перерисовки, ошибки загрузки или закрытия диалога. Удаление должно приходить отдельным проверяемым полем, а сервер должен проверить право пользователя и принадлежность текущего файла элементу.

\n

Если FileInput сначала загружает файл отдельным запросом, а потом форма сохраняет карточку, временный ID нельзя считать готовым результатом. Пользователь может закрыть вкладку между запросами. Временный объект должен иметь понятный статус и срок жизни. Финальная операция должна повторно проверить владельца и связь с элементом.

\n

Порядок проверки

\n
  1. Зафиксируйте ID элемента и текущий ID PREVIEW_PICTURE до изменения.
  2. Проверьте форму: method=\"post\", enctype=\"multipart/form-data\", имя поля и CSRF-контракт.
  3. Выберите небольшой учебный JPEG и сравните имя поля в DOM, Network и PHP-массиве.
  4. Проверьте на сервере размер, содержимое изображения, расширение, права и ограничения PHP до изменения состояния.
  5. Передайте проверенный файловый массив в CIBlockElement::Update(); не подменяйте его голым ID без подтверждённого контракта версии.
  6. Проверьте результат: метод вернул true, а LAST_ERROR не содержит ошибки.
  7. Повторно прочитайте элемент новым запросом и сравните его PREVIEW_PICTURE с ожидаемым ID.
  8. Отдельно проверьте три отрицательных случая: пустой POST, файл неверного типа и явное удаление без нового файла.
  9. Уберите временные логи или оставьте только безопасные идентификаторы операции, элемента, файла и причину отказа.
\n

Ограничения модели

\n

Код выше не является готовым обработчиком для любой версии Bitrix. Он не описывает транзакцию между файловым хранилищем и элементом, отдельный AJAX-контракт, антивирус, ресайз, CDN, дисковую квоту, свойства инфоблока, несколько файлов и конкурентное редактирование. Для свойства типа «Файл» формат PROPERTY_VALUES проверяют отдельно. Для административной формы отдельно проверяют права и события Bitrix.

\n

Учебное имя CATALOG_PREVIEW, модуль iblock, лимит 5 MiB и фиксированный сценарий с одной картинкой не являются требованиями production. Пример не доказывает, что конкретная установка принимает любой JPEG, и не даёт production-результатов. Его задача — показать порядок и точки проверки.

\n

Официальная документация Bitrix описывает API, но не знает правила вашего каталога. OWASP перечисляет меры для загрузки файлов, однако набор контролей зависит от угроз, типа данных и архитектуры. Без проверки реального POST, прав и настроек окружения нельзя объявлять интеграцию готовой.

\n

Проверяемый критерий готовности

\n

Сценарий готов к интеграционной проверке, когда для одного тестового элемента можно показать четыре факта: сервер получил ожидаемый файловый массив; Bitrix зарегистрировал новый ID; Update() вернул успех; свежее чтение элемента вернуло этот ID в PREVIEW_PICTURE. Дополнительно пустая отправка сохраняет старый файл, неверный тип отклоняется, а явное удаление не возникает из пустого preview. При отдельном SaveFile() отдельно фиксируют проверку привязки и судьбу файла при ошибке обновления.

\n

Если виден только preview или только ID в b_file, работа не завершена. Источник истины для карточки — поле элемента после успешного обновления. Именно его нужно проверять новым запросом.

\n

Проверяемые источники

\n" }