{ "index": 333, "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 лежит 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" }