8 lines
21 KiB
JSON
8 lines
21 KiB
JSON
{
|
||
"index": 333,
|
||
"slug": "editorial-2018-10-mechanism-image-workflow",
|
||
"title": "Bitrix: почему preview не означает сохранённую картинку",
|
||
"excerpt": "В форме уже виден новый preview, но карточка после сохранения показывает старое изображение. Разделяем браузерный файл, запись в b_file и ссылку PREVIEW_PICTURE, чтобы найти разрыв и проверить результат.",
|
||
"contentHtml": "<p>В форме товара появляется новая картинка. Пользователь нажимает «Сохранить», открывает карточку и видит старую обложку. Иногда интерфейс сообщает об успехе, хотя сервер сохранил файл отдельно и не связал его с элементом инфоблока.</p>\n<p>Цена ошибки выше, чем один неудачный POST. Оператор повторяет загрузку, каталог показывает устаревшие данные, а в хранилище остаются лишние файлы. При разборе инцидента команда видит preview и решает, что загрузка прошла. Но preview доказывает только состояние браузера.</p>\n<p>Тезис статьи простой: изображение проходит несколько границ. Браузер показывает выбранный <code>File</code>. PHP получает multipart-данные. Bitrix регистрирует файл и выдаёт числовой ID. Элемент инфоблока хранит ссылку на этот ID. Успех нужно проверять на каждой границе. Последний обязательный факт — после обновления элемент возвращает ожидаемый <code>PREVIEW_PICTURE</code>.</p>\n<figure><img src=\"/assets/editorial/2018/bitrix-file-state-ownership-2018.svg\" alt=\"Состояния изображения: браузерный File, запрос формы, зарегистрированный файл Bitrix и PREVIEW_PICTURE элемента инфоблока\" loading=\"lazy\" /><figcaption>Preview принадлежит интерфейсу. Подтверждённая картинка появляется только после связи ID файла с элементом инфоблока.</figcaption></figure>\n<h2>Четыре состояния вместо одного слова «картинка»</h2>\n<p>Первое состояние живёт в браузере. Диалог выбора создал объект <code>File</code>, а JavaScript показал его имя, размер или локальный preview. В этот момент сервер ещё ничего не знает. Перезагрузка страницы удалит это состояние.</p>\n<p>Второе состояние возникает в HTTP-запросе. Сервер получает поле из <code>multipart/form-data</code>. Имя поля может отличаться от имени, которое видит разработчик в шаблоне: Ajax, вложенная структура и повторная отрисовка часто меняют фактический POST. Поэтому проверяют не DOM, а сетевой запрос и PHP-массив.</p>\n<p>Третье состояние создаёт Bitrix. <code>CFile::SaveFile()</code> принимает файловый массив, сохраняет файл и регистрирует его в <code>b_file</code>. Положительный ID означает, что у приложения появился зарегистрированный файл. Он ещё не означает, что файл стал изображением товара.</p>\n<p>Четвёртое состояние хранит сам элемент. В поле <code>PREVIEW_PICTURE</code> лежит ссылка на зарегистрированный файл. Её меняет операция обновления элемента. Если обработчик только вызвал <code>SaveFile()</code>, карточка останется со старым ID.</p>\n<table><caption>Граница, доказательство и следующий шаг</caption><thead><tr><th scope=\"col\">Состояние</th><th scope=\"col\">Владелец</th><th scope=\"col\">Доказательство</th><th scope=\"col\">Следующий шаг</th></tr></thead><tbody><tr><td>Файл выбран</td><td>Браузер и DOM</td><td>Есть имя, размер или preview</td><td>Не считать сохранением</td></tr><tr><td>Данные пришли</td><td>PHP-обработчик</td><td>Ожидаемое поле и <code>UPLOAD_ERR_OK</code></td><td>Проверить размер, тип, права и передать дальше</td></tr><tr><td>Файл зарегистрирован</td><td><code>b_file</code></td><td><code>CFile::SaveFile()</code> вернул ID, <code>GetFileArray()</code> его находит</td><td>Собрать файловый массив для элемента</td></tr><tr><td>Карточка изменена</td><td>Элемент инфоблока</td><td><code>CIBlockElement::Update()</code> вернул <code>true</code>, свежее чтение вернуло новый ID</td><td>Показать результат новым запросом</td></tr></tbody></table>\n<h2>Контрол задаёт интерфейс, а не контракт хранения</h2>\n<p>В старой установке Bitrix форма может использовать <code>\\Bitrix\\Main\\UI\\FileInput</code>. Контрол рисует поля, кнопки, preview и JavaScript. Метод <code>show()</code> выводит интерфейс для текущего ID, но не обновляет элемент инфоблока сам по себе. На границе обработчика всё равно нужны имя поля, файловый массив и явное действие: заменить, оставить или удалить.</p>\n<p>Ниже учебный фрагмент для одной картинки. Он показывает настройки интерфейса и не заменяет серверную проверку. Название поля должно совпасть с тем, что реально приходит в POST. Если проект использует другую версию Bitrix или собственный Ajax-адаптер, сначала смотрят фактический запрос.</p>\n<pre><code><?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?></code></pre>\n<p><code>maxSize</code> помогает интерфейсу показать предел, но пользователь может изменить запрос вручную. <code>allowUpload</code> ограничивает сценарий контрола, но не доверенный источник данных. Сервер заново проверяет размер, фактический тип содержимого, расширение, права и лимит PHP. Заголовок <code>Content-Type</code> нельзя принимать за доказательство типа файла.</p>\n<h2>Сначала регистрируем файл, потом меняем элемент</h2>\n<p>Обработчик должен различать ошибку загрузки и ошибку привязки. Не сохраняйте локальный путь из браузера и не используйте имя файла как идентификатор. Получите файловый массив, проверьте его, зарегистрируйте файл, затем соберите массив для <code>Update()</code>.</p>\n<pre><code>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}</code></pre>\n<p>Это учебный пример порядка операций. В конкретном проекте нужно проверить формат массива, версию API, модуль, права и обработчики событий. <code>GetFileArray()</code> отделяет зарегистрированный ID от случайного числа. <code>MakeFileArray()</code> готовит описание существующего файла. <code>Update()</code> отдельно сообщает, удалось ли изменить элемент. Один положительный ответ не заменяет два других.</p>\n<p>Если регистрация файла прошла, а <code>Update()</code> вернул ошибку, не показывайте пользователю успех. Новый файл уже может существовать без связи с карточкой. Политика очистки таких файлов зависит от проекта: временное состояние, очередь уборки или безопасная ручная обработка. Нельзя удалять файл вслепую, если другой элемент успел получить тот же ID.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностика формы с изображением</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Preview сменился, карточка нет</td><td>Обновился только DOM</td><td>Сравнить ID до и после свежего чтения элемента</td><td>Передать новый файловый массив в <code>Update()</code></td></tr><tr><td>POST пустой</td><td>Нет multipart, поле переименовано или Ajax отправляет другой набор</td><td>Посмотреть Network и <code>$_FILES</code> без содержимого файла</td><td>Исправить контракт формы и обработчик</td></tr><tr><td>Есть ID, но файл не находится</td><td>Сохранение вернуло невалидный результат или ID прочитан не из того поля</td><td>Вызвать <code>GetFileArray($fileId)</code></td><td>Остановить привязку и записать безопасную причину</td></tr><tr><td>Файл есть, поле старое</td><td>Вызвали <code>SaveFile()</code>, но не обновили элемент</td><td>Проверить вызов <code>Update()</code> и <code>LAST_ERROR</code></td><td>Разделить этапы и проверять оба ответа</td></tr><tr><td>После ошибки растёт число файлов</td><td>Файл зарегистрирован до неудачной привязки</td><td>Сопоставить ID файла с элементом и временем операции</td><td>Определить безопасную уборку сиротских записей</td></tr><tr><td>Загружается не изображение</td><td>Доверие расширению или заголовку клиента</td><td>Проверить содержимое, размер, расширение и серверные ограничения</td><td>Отклонить файл до регистрации</td></tr><tr><td>Удаляется старая картинка без команды</td><td>Пустой preview приняли за флаг удаления</td><td>Различить отсутствие нового файла и явное <code>DELETE</code></td><td>Сохранить старую картинку, если удаление не подтверждено</td></tr></tbody></table>\n<h2>Отрицательный путь: нет нового файла и есть удаление</h2>\n<p>Отсутствие нового файла не равно удалению. Пользователь мог открыть форму, ничего не выбрать и нажать «Сохранить». Для такого запроса правило должно быть явным: нет нового файла и нет флага удаления — оставить старую картинку; есть новый файл — заменить после успешной регистрации и обновления; есть явный флаг удаления — удалить по согласованному контракту.</p>\n<p>Не выводите решение из пустого preview. DOM может исчезнуть после Ajax-перерисовки, ошибки загрузки или закрытия диалога. Удаление должно приходить отдельным проверяемым полем, а сервер должен проверить право пользователя и принадлежность текущего файла элементу.</p>\n<p>Если FileInput сначала загружает файл отдельным запросом, а потом форма сохраняет карточку, временный ID нельзя считать готовым результатом. Пользователь может закрыть вкладку между запросами. Временный объект должен иметь понятный статус и срок жизни. Финальная операция должна повторно проверить владельца и связь с элементом.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Зафиксируйте ID элемента и текущий ID <code>PREVIEW_PICTURE</code> до изменения.</li><li>Проверьте форму: <code>method=\"post\"</code>, <code>enctype=\"multipart/form-data\"</code>, имя поля и CSRF-контракт.</li><li>Выберите небольшой учебный JPEG и сравните имя поля в DOM, Network и PHP-массиве.</li><li>Проверьте серверные размер, фактический тип, расширение, права и ограничения PHP до вызова <code>SaveFile()</code>.</li><li>Сохраните файл и подтвердите ID через <code>GetFileArray()</code>. При ошибке остановите процесс.</li><li>Соберите файловый массив через <code>MakeFileArray()</code> и вызовите <code>CIBlockElement::Update()</code>.</li><li>Проверьте оба результата: <code>true</code> от обновления и пустой <code>LAST_ERROR</code> при успехе.</li><li>Повторно прочитайте элемент новым запросом и сравните его <code>PREVIEW_PICTURE</code> с ожидаемым ID.</li><li>Отдельно проверьте три отрицательных случая: пустой POST, файл неверного типа и явное удаление без нового файла.</li><li>Уберите временные логи или оставьте только безопасные идентификаторы операции, элемента, файла и причину отказа.</li></ol>\n<h2>Ограничения модели</h2>\n<p>Код выше не является готовым обработчиком для любой версии Bitrix. Он не описывает транзакцию между файловым хранилищем и элементом, антивирус, ресайз, CDN, дисковую квоту, свойства инфоблока, несколько файлов и конкурентное редактирование. Для свойства типа «Файл» формат <code>PROPERTY_VALUES</code> проверяют отдельно. Для административной формы отдельно проверяют права и события Bitrix.</p>\n<p>Учебные имена <code>catalog</code>, <code>CATALOG_PREVIEW</code>, лимит 5 MiB и фиксированный сценарий с одной картинкой не являются требованиями production. Пример не доказывает, что конкретная установка принимает любой JPEG, и не даёт production-результатов. Его задача — показать порядок и точки проверки.</p>\n<p>Официальная документация Bitrix описывает API, но не знает правила вашего каталога. OWASP перечисляет меры для загрузки файлов, однако набор контролей зависит от угроз, типа данных и архитектуры. Без проверки реального POST, прав и настроек окружения нельзя объявлять интеграцию готовой.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Сценарий готов к интеграционной проверке, когда для одного тестового элемента можно показать четыре факта: сервер получил ожидаемый файловый массив; Bitrix зарегистрировал новый ID; <code>Update()</code> вернул успех; свежее чтение элемента вернуло этот ID в <code>PREVIEW_PICTURE</code>. Дополнительно пустая отправка сохраняет старый файл, неверный тип отклоняется, а явное удаление не возникает из пустого preview.</p>\n<p>Если виден только preview или только ID в <code>b_file</code>, работа не завершена. Источник истины для карточки — поле элемента после успешного обновления. Именно его нужно проверять новым запросом.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cfile/savefile.php?print=Y\" target=\"_blank\" rel=\"noopener noreferrer\">Bitrix: CFile::SaveFile</a> — сохранение и регистрация файла в <code>b_file</code>.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/update.php?print=Y\" target=\"_blank\" rel=\"noopener noreferrer\">Bitrix: CIBlockElement::Update</a> — обновление элемента и формат файлового поля.</li><li><a href=\"https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP File Upload Cheat Sheet</a> — серверная валидация, лимиты, права и защита загрузки.</li></ul>"
|
||
}
|