Files
progcode/editorial/agent-rewrites/333.json
T

8 lines
22 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> лежит ID файла, зарегистрированного в файловой таблице. Его меняет обновление элемента. Если обработчик только вызвал <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>Выполнить согласованный шаг привязки; голый ID не считать контрактом без проверки версии</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> выводит интерфейс для текущего значения, но не обновляет элемент инфоблока сам по себе. На границе обработчика всё равно нужны имя поля, файловый массив и явное действие: заменить, оставить или удалить.</p>\n<p>Ниже учебный фрагмент для одной картинки. Он показывает настройки интерфейса и не заменяет серверную проверку. Название поля должно совпасть с тем, что реально приходит в POST. Если проект использует другую версию Bitrix или собственный Ajax-адаптер, сначала смотрят фактический запрос.</p>\n<pre><code>&lt;?php\nuse Bitrix\\Main\\UI\\FileInput;\n\n$currentFileId = (int) ($element['PREVIEW_PICTURE'] ?? 0);\n\necho FileInput::createInstance([\n 'id' =&gt; 'catalog_preview',\n 'name' =&gt; 'CATALOG_PREVIEW',\n 'upload' =&gt; false,\n 'allowUpload' =&gt; FileInput::UPLOAD_IMAGES,\n 'medialib' =&gt; false,\n 'fileDialog' =&gt; false,\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>upload =&gt; false</code> оставляет один проверяемый multipart-запрос; при AJAX-загрузке контракт будет другим. <code>allowUpload</code> ограничивает сценарий контрола, но не заменяет серверную проверку. Сервер проверяет размер, содержимое изображения, расширение, права и лимит PHP. Заголовок <code>Content-Type</code> нельзя принимать за доказательство типа файла.</p>\n<h2>Проверяем файл до обновления элемента</h2>\n<p>Обработчик должен различать ошибку загрузки и ошибку обновления элемента. Не сохраняйте локальный путь из браузера и не используйте имя файла как идентификатор. Получите файловый массив, проверьте его на сервере и передайте проверенный массив в <code>Update()</code>. Bitrix зарегистрирует файл и запишет его ID в поле элемента в рамках этого обновления.</p>\n<pre><code>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-&gt;Update($elementId, [\n 'PREVIEW_PICTURE' =&gt; $upload,\n ]);\n\n if (!$updated) {\n throw new RuntimeException($element-&gt;LAST_ERROR);\n }\n}\n\nupdateCatalogImage($elementId, $_FILES['CATALOG_PREVIEW']);</code></pre>\n<p>Это учебный пример одного multipart-сценария. <code>CheckImageFile()</code> проверяет файл до изменения состояния, а <code>Update()</code> сообщает, удалось ли обновить элемент; после этого нужно свежим чтением получить новый ID. В конкретном проекте дополнительно проверяют версию API, модуль, права, CSRF, обработчики событий и настройки хранения. Если архитектура использует отдельный <code>SaveFile()</code>, его ID остаётся промежуточным доказательством: контракт привязки и способ очистки сиротского файла нужно проверять отдельно.</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 до изменения состояния.</li><li>Передайте проверенный файловый массив в <code>CIBlockElement::Update()</code>; не подменяйте его голым ID без подтверждённого контракта версии.</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. Он не описывает транзакцию между файловым хранилищем и элементом, отдельный AJAX-контракт, антивирус, ресайз, CDN, дисковую квоту, свойства инфоблока, несколько файлов и конкурентное редактирование. Для свойства типа «Файл» формат <code>PROPERTY_VALUES</code> проверяют отдельно. Для административной формы отдельно проверяют права и события Bitrix.</p>\n<p>Учебное имя <code>CATALOG_PREVIEW</code>, модуль <code>iblock</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. При отдельном <code>SaveFile()</code> отдельно фиксируют проверку привязки и судьбу файла при ошибке обновления.</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/main/reference/cfile/checkimagefile.php?print=Y\" target=\"_blank\" rel=\"noopener noreferrer\">Bitrix: CFile::CheckImageFile</a> — проверка изображения, размера и параметров до сохранения.</li><li><a href=\"https://dev.1c-bitrix.ru/api_d7/bitrix/main/ui/fileinput/index.php\" target=\"_blank\" rel=\"noopener noreferrer\">Bitrix: FileInput</a> — параметры контрола, режим загрузки и <code>show()</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>"
}