{ "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.
Первое состояние живёт в браузере. Диалог выбора создал объект File, а JavaScript показал его имя, размер или локальный preview. В этот момент сервер ещё ничего не знает. Перезагрузка страницы удалит это состояние.
Второе состояние возникает в HTTP-запросе. Сервер получает поле из multipart/form-data. Имя поля может отличаться от имени, которое видит разработчик в шаблоне: Ajax, вложенная структура и повторная отрисовка часто меняют фактический POST. Поэтому проверяют не DOM, а сетевой запрос и PHP-массив.
Третье состояние создаёт Bitrix. CFile::SaveFile() принимает файловый массив, сохраняет файл и регистрирует его в b_file. Положительный ID подтверждает регистрацию файла, но не его привязку к элементу. Такой раздельный путь требует отдельного шага обновления и проверки его результата.
Четвёртое состояние хранит сам элемент. В поле PREVIEW_PICTURE лежит ID файла, зарегистрированного в файловой таблице. Его меняет обновление элемента. Если обработчик только вызвал SaveFile(), карточка останется со старым ID.
| Состояние | Владелец | Доказательство | Следующий шаг |
|---|---|---|---|
| Файл выбран | Браузер и DOM | Есть имя, размер или preview | Не считать сохранением |
| Данные пришли | PHP-обработчик | Ожидаемое поле и UPLOAD_ERR_OK | Проверить размер, тип, права и передать дальше |
| Файл зарегистрирован | b_file | CFile::SaveFile() вернул ID, GetFileArray() его находит | Выполнить согласованный шаг привязки; голый ID не считать контрактом без проверки версии |
| Карточка изменена | Элемент инфоблока | CIBlockElement::Update() вернул true, свежее чтение вернуло новый ID | Показать результат новым запросом |
В установке Bitrix, где доступен \\Bitrix\\Main\\UI\\FileInput, контрол рисует поле, кнопки, preview и JavaScript. Метод show() выводит интерфейс для текущего значения, но не обновляет элемент инфоблока сам по себе. На границе обработчика всё равно нужны имя поля, файловый массив и явное действие: заменить, оставить или удалить.
Ниже учебный фрагмент для одной картинки. Он показывает настройки интерфейса и не заменяет серверную проверку. Название поля должно совпасть с тем, что реально приходит в 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?>\nmaxSize помогает интерфейсу показать предел, но пользователь может изменить запрос вручную. В этом фрагменте upload => false оставляет один проверяемый multipart-запрос; при AJAX-загрузке контракт будет другим. allowUpload ограничивает сценарий контрола, но не заменяет серверную проверку. Сервер проверяет размер, содержимое изображения, расширение, права и лимит PHP. Заголовок Content-Type нельзя принимать за доказательство типа файла.
Обработчик должен различать ошибку загрузки и ошибку обновления элемента. Не сохраняйте локальный путь из браузера и не используйте имя файла как идентификатор. Получите файловый массив, проверьте его на сервере и передайте проверенный массив в Update(). Bitrix зарегистрирует файл и запишет его ID в поле элемента в рамках этого обновления.
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 остаётся промежуточным доказательством: контракт привязки и способ очистки сиротского файла нужно проверять отдельно.
Если отдельная регистрация файла прошла, а Update() вернул ошибку, не показывайте пользователю успех. Новый файл может остаться без связи с карточкой. Политика очистки таких файлов зависит от проекта: временное состояние, очередь уборки или безопасная ручная обработка. Нельзя удалять файл вслепую, если другой элемент успел получить тот же ID.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Preview сменился, карточка нет | Обновился только DOM | Сравнить ID до и после свежего чтения элемента | Передать новый файловый массив в Update() |
| POST пустой | Нет multipart, поле переименовано или Ajax отправляет другой набор | Посмотреть Network и $_FILES без содержимого файла | Исправить контракт формы и обработчик |
| Есть ID, но файл не находится | Сохранение вернуло невалидный результат или ID прочитан не из того поля | Вызвать GetFileArray($fileId) | Остановить привязку и записать безопасную причину |
| Файл есть, поле старое | Вызвали SaveFile(), но не обновили элемент | Проверить вызов Update() и LAST_ERROR | Разделить этапы и проверять оба ответа |
| После ошибки растёт число файлов | Файл зарегистрирован до неудачной привязки | Сопоставить ID файла с элементом и временем операции | Определить безопасную уборку сиротских записей |
| Загружается не изображение | Доверие расширению или заголовку клиента | Проверить содержимое, размер, расширение и серверные ограничения | Отклонить файл до регистрации |
| Удаляется старая картинка без команды | Пустой preview приняли за флаг удаления | Различить отсутствие нового файла и явное DELETE | Сохранить старую картинку, если удаление не подтверждено |
Отсутствие нового файла не равно удалению. Пользователь мог открыть форму, ничего не выбрать и нажать «Сохранить». Для такого запроса правило должно быть явным: нет нового файла и нет флага удаления — оставить старую картинку; есть новый файл — заменить после успешной регистрации и обновления; есть явный флаг удаления — удалить по согласованному контракту.
\nНе выводите решение из пустого preview. DOM может исчезнуть после Ajax-перерисовки, ошибки загрузки или закрытия диалога. Удаление должно приходить отдельным проверяемым полем, а сервер должен проверить право пользователя и принадлежность текущего файла элементу.
\nЕсли FileInput сначала загружает файл отдельным запросом, а потом форма сохраняет карточку, временный ID нельзя считать готовым результатом. Пользователь может закрыть вкладку между запросами. Временный объект должен иметь понятный статус и срок жизни. Финальная операция должна повторно проверить владельца и связь с элементом.
\nPREVIEW_PICTURE до изменения.method=\"post\", enctype=\"multipart/form-data\", имя поля и CSRF-контракт.CIBlockElement::Update(); не подменяйте его голым ID без подтверждённого контракта версии.true, а LAST_ERROR не содержит ошибки.PREVIEW_PICTURE с ожидаемым ID.Код выше не является готовым обработчиком для любой версии Bitrix. Он не описывает транзакцию между файловым хранилищем и элементом, отдельный AJAX-контракт, антивирус, ресайз, CDN, дисковую квоту, свойства инфоблока, несколько файлов и конкурентное редактирование. Для свойства типа «Файл» формат PROPERTY_VALUES проверяют отдельно. Для административной формы отдельно проверяют права и события Bitrix.
Учебное имя CATALOG_PREVIEW, модуль iblock, лимит 5 MiB и фиксированный сценарий с одной картинкой не являются требованиями production. Пример не доказывает, что конкретная установка принимает любой JPEG, и не даёт production-результатов. Его задача — показать порядок и точки проверки.
Официальная документация Bitrix описывает API, но не знает правила вашего каталога. OWASP перечисляет меры для загрузки файлов, однако набор контролей зависит от угроз, типа данных и архитектуры. Без проверки реального POST, прав и настроек окружения нельзя объявлять интеграцию готовой.
\nСценарий готов к интеграционной проверке, когда для одного тестового элемента можно показать четыре факта: сервер получил ожидаемый файловый массив; Bitrix зарегистрировал новый ID; Update() вернул успех; свежее чтение элемента вернуло этот ID в PREVIEW_PICTURE. Дополнительно пустая отправка сохраняет старый файл, неверный тип отклоняется, а явное удаление не возникает из пустого preview. При отдельном SaveFile() отдельно фиксируют проверку привязки и судьбу файла при ошибке обновления.
Если виден только preview или только ID в b_file, работа не завершена. Источник истины для карточки — поле элемента после успешного обновления. Именно его нужно проверять новым запросом.
b_file.show().