Files
progcode/editorial/agent-rewrites/333.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
21 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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>&lt;?php\nuse Bitrix\\Main\\UI\\FileInput;\n\n$currentFileId = (int) $arResult['PREVIEW_PICTURE'];\n\necho FileInput::createInstance([\n 'id' =&gt; 'catalog_preview',\n 'name' =&gt; 'CATALOG[PREVIEW_PICTURE]',\n 'upload' =&gt; true,\n 'allowUpload' =&gt; FileInput::UPLOAD_IMAGES,\n 'medialib' =&gt; false,\n 'fileDialog' =&gt; true,\n 'cloud' =&gt; false,\n 'delete' =&gt; true,\n 'edit' =&gt; true,\n 'maxCount' =&gt; 1,\n 'maxSize' =&gt; 5 * 1024 * 1024,\n])-&gt;show($currentFileId);\n?&gt;</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 &lt; 1 || $size &gt; 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 &lt; 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-&gt;Update($elementId, [\n 'PREVIEW_PICTURE' =&gt; $picture,\n]);\n\nif (!$updated) {\n throw new RuntimeException($element-&gt;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>"
}