From 6fec3ac7ea1dd95dfa47ed29240f3d83bb769726 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Fri, 4 Sep 2026 00:26:26 +0300 Subject: [PATCH] Editorial: revise article 332 --- editorial/agent-rewrites/332.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editorial/agent-rewrites/332.json b/editorial/agent-rewrites/332.json index f97b121..c2c1393 100644 --- a/editorial/agent-rewrites/332.json +++ b/editorial/agent-rewrites/332.json @@ -3,5 +3,5 @@ "slug": "bitrix-api-add-foto-editor", "title": "Bitrix API: как встроить редактор изображений и сохранить файл в элементе", "excerpt": "FileInput показывает выбранную картинку, но не сохраняет её сам. Разбираем контракт формы, проверку загрузки и привязку файла к PREVIEW_PICTURE в Bitrix.", - "contentHtml": "

В форме Bitrix пользователь выбирает новую фотографию и сразу видит preview. После нажатия «Сохранить» страница открывается со старым изображением. Иногда поле остаётся пустым. В файловом хранилище при этом появляются новые записи, которые не привязаны к элементу.

\n

Цена ошибки выше, чем у сломанной кнопки. Менеджер считает карточку обновлённой, а каталог продолжает показывать старую обложку. Повторная загрузка создаёт мусорные файлы. При массовом редактировании ошибка превращается в неверные фотографии, ручную сверку и восстановление данных.

\n

Тезис. Bitrix\\Main\\UI\\FileInput решает задачу интерфейса. Он не доказывает, что файл записан в нужное поле сущности. Готовность наступает только после трёх подтверждений: запрос принял ожидаемый файл, Bitrix зарегистрировал его и повторное чтение элемента вернуло этот ID в PREVIEW_PICTURE или другое согласованное свойство.

\n

Где возникает разрыв

\n

У одной картинки есть несколько состояний. Браузер хранит выбранный объект File. HTML-форма передаёт его как multipart-часть. PHP получает массив в $_FILES. Bitrix создаёт запись в b_file и возвращает числовой ID. Элемент инфоблока хранит ссылку на этот ID в поле изображения. Ни один этап не заменяет следующий.

\n

Preview относится к DOM. Он может измениться до отправки формы, после Ajax-перерисовки или даже при ошибочном ответе сервера. Поэтому текст «файл выбран» и URL картинки в интерфейсе нельзя использовать как доказательство сохранения. Сервер должен получить конкретное поле, проверить его и записать связь.

\n
Состояния изображения и границы проверки
СостояниеПричина сбояПроверкаДействие
Preview обновилсяИзменился только DOMОткрыть Network и найти multipart-полеНе считать файл сохранённым
В $_FILES нет поляНеверное name или форма без enctypeПроверить имя input, метод и Request PayloadИсправить контракт формы
Upload завершился ошибкойЛимит PHP, размер или права каталогаПроверить error, size и серверный логВернуть ошибку формы и остановить запись
Есть ID файла, но карточка прежняяНе вызван Update() или передано не то полеПовторно прочитать элемент и поле изображенияСохранить файловый массив в сущность
После удаления картинка вернуласьУдаление выведено из пустого previewПроверить явный флаг удаления в POSTРазвести ветки «оставить», «заменить» и «удалить»
\n
\"Интерфейс
Preview помогает выбрать файл, но итогом считается только связь зарегистрированного ID с элементом Bitrix.
\n

Контракт FileInput

\n

Контрол создают на сервере. Имя поля выбирают вместе с обработчиком. Если шаблон отправляет CATALOG_PREVIEW, PHP не должен ждать picture. Такая ошибка выглядит как «Bitrix не загрузил картинку», хотя файл просто не попал в ожидаемую ветку.

\n

Ниже учебный пример для формы с одной картинкой. Он показывает состав параметров, а не готовую конфигурацию конкретного проекта. Константы, доступность источников и набор опций нужно сверить с версией модуля main и установленными правами.

\n
<?php\nuse Bitrix\\Main\\UI\\FileInput;\n\n$currentFileId = (int) $arResult['PREVIEW_PICTURE'];\n\necho FileInput::createInstance(array(\n    'id' => 'catalog_preview',\n    'name' => 'CATALOG_PREVIEW',\n    'upload' => true,\n    'allowUpload' => FileInput::UPLOAD_IMAGES,\n    'maxCount' => 1,\n    'maxSize' => 5 * 1024 * 1024,\n    'delete' => true,\n))->show($currentFileId);\n?>
\n

name связывает разметку с серверным кодом. upload включает загрузку. allowUpload сужает сценарий до изображений, если это поддерживает установленная версия API. maxCount и maxSize уменьшают число ошибочных действий в интерфейсе. Они не заменяют серверную проверку: запрос можно отправить вручную.

\n

delete только даёт пользователю возможность выбрать удаление через контрол. Правило хранения остаётся в обработчике. Для редактирования существующего элемента нужно заранее решить, что означает отсутствие нового файла: обычно сохранить старую картинку. Явное удаление передают отдельным признаком.

\n

Форма должна отправить файл

\n

Обычная HTML-форма с файлом использует method=\"post\" и enctype=\"multipart/form-data\". Для Ajax нужен объект FormData. Вызов jQuery serialize() собирает текстовые поля, но не переносит бинарное содержимое файла. Если заменить multipart-запрос сериализацией, preview останется, а сервер получит только остальную форму.

\n

В legacy-шаблоне частая ошибка появляется после .html(). Старый input удаляют, новый вставляют, а обработчик остаётся привязан к уничтоженному узлу. Повторная инициализация добавляет второй обработчик. Делегируйте событие стабильному контейнеру и снимайте только свой namespace.

\n
(function ($) {\n  function bindPhotoEditor(root) {\n    var $root = $(root);\n\n    $root.off('change.photoEditor', 'input[name=CATALOG_PREVIEW]')\n      .on('change.photoEditor', 'input[name=CATALOG_PREVIEW]', function () {\n        var file = this.files && this.files[0];\n        $root.find('.js-photo-status').text(\n          file ? 'Файл выбран. Сохранение ещё не выполнено.' : 'Файл не выбран.'\n        );\n      });\n  }\n\n  $(function () {\n    bindPhotoEditor(document);\n  });\n}(jQuery));
\n

Этот фрагмент учебный. Он меняет статус в DOM и не создаёт ID файла. Проверить его можно по одному простому признаку: после повторной отрисовки контейнера изменение файла вызывает один статус, а Network показывает один запрос при сохранении. Общий off('change') здесь опасен: он может снять обработчики других компонентов.

\n

Серверный путь: файл, затем связь

\n

Обработчик не должен брать «последний созданный файл» из базы. В форме одновременно работают несколько пользователей. Связь строят из данных конкретного запроса и конкретного элемента.

\n

Сначала проверяют результат загрузки и входные ограничения. Поле type из запроса — это заявление клиента. Нужны серверные проверки размера, расширения, MIME и содержимого изображения. Перечень допустимых форматов зависит от продукта. Не принимайте его только потому, что контрол показал кнопку выбора картинки.

\n
<?php\n\nfunction validateImageUpload(array $upload): array\n{\n    if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {\n        throw new RuntimeException('Загрузка изображения не завершена');\n    }\n\n    $size = (int) ($upload['size'] ?? 0);\n    if ($size < 1 || $size > 5 * 1024 * 1024) {\n        throw new RuntimeException('Размер изображения не подходит');\n    }\n\n    if (!isset($upload['tmp_name']) || !is_uploaded_file($upload['tmp_name'])) {\n        throw new RuntimeException('Временный файл не подтверждён PHP');\n    }\n\n    return $upload;\n}\n\n$upload = validateImageUpload($_FILES['CATALOG_PREVIEW'] ?? array());\n$fileId = (int) CFile::SaveFile($upload, 'catalog');\nif ($fileId < 1 || !CFile::GetFileArray($fileId)) {\n    throw new RuntimeException('Bitrix не зарегистрировал файл');\n}
\n

Код выше ограничен учебным примером. Он не знает вашу авторизацию, CSRF-защиту, политику форматов, антивирус, каталог хранения и способ очистки временных файлов. В рабочем обработчике эти условия должны быть явными. Если FileInput в вашей версии сначала загружает файл отдельным запросом, не копируйте ветку с $_FILES: сначала определите, какое значение вернул контрол и кто владеет временным ID.

\n

После регистрации ID нужно превратить в файловый массив для API элемента. Для поля PREVIEW_PICTURE недостаточно положить число в произвольное поле. Старый API Bitrix ожидает структуру файлового значения и возвращает ошибку через результат обновления и LAST_ERROR.

\n
<?php\n\n$element = new CIBlockElement();\n$picture = CFile::MakeFileArray($fileId);\n\nif (!$picture) {\n    throw new RuntimeException('Не удалось собрать файловый массив');\n}\n\n$updated = $element->Update($elementId, array(\n    'PREVIEW_PICTURE' => $picture,\n));\n\nif (!$updated) {\n    throw new RuntimeException($element->LAST_ERROR ?: 'Элемент не обновлён');\n}
\n

Сохранение файла и обновление элемента — разные операции. Первая может завершиться успешно, а вторая — нет из-за прав, неверного ID, ошибки поля или обработчика события. Тогда в b_file останется незакреплённая запись. Политику очистки таких файлов определите отдельно. Не удаляйте старую картинку до успешной привязки новой, если откат не гарантирован.

\n

Три состояния редактирования

\n

Обновление существующей картинки должно различать три команды. Новый файл означает замену после успешной проверки. Отсутствие нового файла и отсутствие флага удаления означает «оставить как есть». Явный флаг удаления означает очистить поле. Новый файл вместе с удалением — конфликт, который лучше отклонить, чем разрешить случайным порядком полей.

\n

Это правило нельзя выводить из пустого preview. DOM может очиститься после ошибки JavaScript или перерисовки формы, хотя пользователь не просил удалять изображение. Смысл операции передаёт серверное поле, которое обработчик проверяет явно.

\n
<?php\n\n$hasNewFile = isset($_FILES['CATALOG_PREVIEW'])\n    && ($_FILES['CATALOG_PREVIEW']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK;\n$deleteRequested = ($_POST['DELETE_PICTURE'] ?? '') === '1';\n\nif ($hasNewFile && $deleteRequested) {\n    throw new RuntimeException('Выберите новый файл или удаление');\n}\n\nif (!$hasNewFile && !$deleteRequested) {\n    // Старое изображение остаётся. Update() для поля не вызываем.\n    return;\n}\n\n$fields = $deleteRequested\n    ? array('PREVIEW_PICTURE' => array('del' => 'Y'))\n    : array('PREVIEW_PICTURE' => CFile::MakeFileArray($fileId));\n\n$element = new CIBlockElement();\nif (!$element->Update((int) $elementId, $fields)) {\n    throw new RuntimeException($element->LAST_ERROR);\n}
\n

Фрагмент показывает отрицательный путь, но не является готовым endpoint. В реальном коде до него должны выполняться проверка сессии, права на конкретный элемент, CSRF, нормализация входа и проверка принадлежности временного файла пользователю или черновику. Для свойства типа «Файл» формат обновления может отличаться от поля элемента. Сверяйте документацию именно для своего метода.

\n

Проверка после записи

\n

Ответ 200 и положительный ID не закрывают задачу. После Update() нужно прочитать элемент новым запросом. Читайте то же поле, которое использует страница. Старый объект, заполненный до POST, может содержать прежнее значение.

\n

Проверка должна сравнить ожидаемый ID с фактическим. Затем откройте карточку через отдельный HTTP-запрос и проверьте видимое изображение. Этот шаг ловит обработчики событий, кеш, неверный инфоблок и запись не в то свойство.

\n

Порядок действий

\n
  1. Назовите элемент, поле изображения и пользователя, который имеет право его менять.
  2. Зафиксируйте текущий ID картинки и правило для случая без нового файла.
  3. Выведите FileInput с согласованным name, одной картинкой и лимитами интерфейса.
  4. Проверьте HTML формы: multipart/form-data, ожидаемое имя поля и отсутствие дубликатов input после Ajax.
  5. Отправьте небольшой учебный файл и проверьте в Network multipart-часть, код ответа и тело ответа.
  6. На сервере проверьте права, код загрузки, размер, временный путь, MIME и содержимое файла.
  7. Зарегистрируйте файл через согласованный API Bitrix и проверьте положительный ID.
  8. Передайте файловое значение в CIBlockElement::Update() и обработайте LAST_ERROR.
  9. Повторно прочитайте элемент и сравните новое поле изображения с ожидаемым ID.
  10. Повторите тест без файла, с явным удалением и с конфликтом «новый файл плюс удаление».
  11. Только после проверок решите, как очищать незакреплённые файлы и временную диагностику.
\n

Ограничения

\n

Параметры FileInput, константы разрешённых типов и формат возвращаемого значения зависят от версии Bitrix. Старое ядро может требовать другой способ подготовки файлового массива. Проверяйте установленный API, а не переносите пример по названию метода.

\n

Лимит в пять мегабайт в коде — учебное число. Он не доказывает подходящий размер для вашей витрины. На результат также влияют upload_max_filesize, post_max_size, права каталога, обратный прокси и антивирусная проверка. Несовпадение этих ограничений проявляется до Update().

\n

Сохранение ID файла не заменяет проверку владельца. Если файл создаётся отдельным Ajax-запросом, временный ID нужно связать с пользователем и элементом или черновиком. Нельзя принять любой ID из скрытого поля и назначить его чужой карточке.

\n

Примеры выше не сообщают production-результаты и не обещают совместимость с конкретной установкой. Они показывают проверяемую последовательность. В рабочем проекте добавьте журнал ошибки без содержимого файла и персональных данных, а также тесты для успешной загрузки, отказа и повторного чтения.

\n

Критерий готовности

\n

Интеграция готова, если после выбора учебного изображения форма делает один ожидаемый multipart-запрос, сервер принимает только разрешённый вход, Bitrix возвращает ID файла, Update() завершается успешно, а свежая страница показывает именно этот файл. При отправке без нового файла старая картинка остаётся. При явном удалении поле очищается. При конфликте действий сервер возвращает понятную ошибку и не меняет элемент.

\n

Проверяемые источники

\n" + "contentHtml": "

При редактировании карточки товара пользователь с ролью менеджера выбирает новую фотографию в форме Bitrix и сразу видит preview. Сначала он заметил: URL картинки в DOM сменился. После нажатия «Сохранить» страница открывается со старым изображением, а поле иногда остаётся пустым.

\n

Цена ошибки выше, чем у сломанной кнопки. Менеджер считает карточку обновлённой, а каталог продолжает показывать старую обложку. Повторная загрузка создаёт мусорные файлы. При массовом редактировании ошибка превращается в неверные фотографии, ручную сверку и восстановление данных.

\n

Тезис. Bitrix\\Main\\UI\\FileInput формирует интерфейс выбора и загрузки. Он не доказывает, что файл записан в нужное поле сущности. Готовность наступает только после трёх подтверждений: запрос принял ожидаемое значение, Bitrix зарегистрировал разрешённый файл и повторное чтение элемента вернуло этот ID в PREVIEW_PICTURE или другое согласованное свойство.

\n

Поворот проверки: preview не равен сохранению

\n

Первое предположение менеджера естественно: раз preview изменился, браузер получил файл. Проверка Network меняет картину. Нужно увидеть фактический запрос, его поле, код ответа и значение, которое обработчик передал в Bitrix. Если обновился только DOM, сервер ещё ничего не знает о выборе.

\n

У изображения несколько независимых состояний: браузер хранит объект File, HTML-форма передаёт multipart-часть, PHP собирает данные в $_FILES, Bitrix регистрирует файл в b_file, а элемент хранит ссылку на ID. При асинхронной загрузке вместо сырого файла может прийти подготовленное значение или ID. Контракт подтверждают по конкретному запросу, а не по названию input.

\n
Состояния изображения и границы проверки
СостояниеПричина сбояПроверкаДействие
Preview обновилсяИзменился только DOMОткрыть Network и найти значение поляНе считать файл сохранённым
В $_FILES нет поляНеверное name, метод или enctypeСверить Request Payload с обработчикомИсправить контракт или ветку чтения
Есть ID файла, но карточка прежняяНе вызван Update() или передано не то полеПовторно прочитать элементПередать файловое значение в сущность
После удаления картинка вернуласьУдаление выведено из пустого previewПроверить явный флаг удаленияРазвести «оставить», «заменить» и «удалить»
\n
\"Интерфейс
Preview помогает выбрать файл, но результатом считается только связь зарегистрированного ID с элементом Bitrix.
\n

Контракт FileInput

\n

Контрол создают через createInstance(), а разметку и JavaScript выводят вызовом show(). Имя поля выбирают вместе с серверным обработчиком. Явный id помогает отличить экземпляр контрола от другого поля на странице.

\n
<?php\nuse Bitrix\\Main\\UI\\FileInput;\n\n$currentFileId = (int) $arResult['PREVIEW_PICTURE'];\n\necho FileInput::createInstance(array(\n    'id' => 'catalog_preview',\n    'name' => 'CATALOG_PREVIEW',\n    'upload' => true,\n    'allowUpload' => FileInput::UPLOAD_IMAGES,\n    'maxCount' => 1,\n    'maxSize' => 5 * 1024 * 1024,\n    'delete' => true,\n))->show($currentFileId);\n?>
\n

В примере UPLOAD_IMAGES разрешает изображения, maxCount ограничивает количество, а maxSize задаёт ограничение контрола. Документация допускает для show() текущие значения как массив, ID или строку и возвращает HTML с JavaScript. Передача ID существующего файла допустима, но не означает, что новый файл уже привязан к элементу.

\n

У FileInput есть собственный JavaScript-контрол и дополнительные источники выбора. В одной конфигурации обработчик получает обычный multipart-файл, в другой — значение, подготовленное асинхронной загрузкой. Сначала сохраните один учебный файл, откройте Network и зафиксируйте точное поле. Ветка с $_FILES['CATALOG_PREVIEW'] ниже относится только к подтверждённому multipart-контракту.

\n

Форма должна отправить файл

\n

Обычная HTML-форма использует method="post" и enctype="multipart/form-data". Для Ajax нужен объект FormData. Вызов jQuery serialize() собирает текстовые поля, но не переносит бинарное содержимое файла. Если заменить multipart-запрос сериализацией, preview останется, а сервер получит только остальную форму.

\n

После Ajax-перерисовки старый input может быть уничтожен вместе с обработчиком. Делегируйте событие стабильному контейнеру и снимайте только собственный namespace. Иначе повторная инициализация даст два обработчика или удалит подписки соседнего компонента.

\n
(function ($) {\n  function bindPhotoEditor(root) {\n    var $root = $(root);\n\n    $root.off('change.photoEditor', 'input[name=CATALOG_PREVIEW]')\n      .on('change.photoEditor', 'input[name=CATALOG_PREVIEW]', function () {\n        var file = this.files && this.files[0];\n        $root.find('.js-photo-status').text(\n          file ? 'Файл выбран. Сохранение ещё не выполнено.' : 'Файл не выбран.'\n        );\n      });\n  }\n\n  $(function () {\n    bindPhotoEditor(document);\n  });\n}(jQuery));
\n

Этот фрагмент меняет только статус в DOM и не создаёт ID файла. После повторной отрисовки изменение должно дать один статус, а сохранение — один ожидаемый запрос. Общий off('change') здесь опасен: он может снять обработчики других компонентов.

\n

Проверяем вход до регистрации

\n

Поле type из запроса сообщает мнение клиента, а не доказанный тип файла. Сервер сначала проверяет код загрузки, размер и временный путь, затем определяет MIME по содержимому. В учебном примере разрешены три расширения; этот список должен совпадать с политикой проекта.

\n
<?php\n\nfunction validateImageUpload(array $upload): array\n{\n    if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {\n        throw new RuntimeException('Загрузка изображения не завершена');\n    }\n\n    $size = (int) ($upload['size'] ?? 0);\n    if ($size < 1 || $size > 5 * 1024 * 1024) {\n        throw new RuntimeException('Размер изображения не подходит');\n    }\n\n    $tmpName = $upload['tmp_name'] ?? '';\n    if ($tmpName === '' || !is_uploaded_file($tmpName)) {\n        throw new RuntimeException('Временный файл не подтверждён PHP');\n    }\n\n    $allowed = array(\n        'jpg' => 'image/jpeg',\n        'jpeg' => 'image/jpeg',\n        'png' => 'image/png',\n    );\n    $extension = strtolower(pathinfo($upload['name'] ?? '', PATHINFO_EXTENSION));\n    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($tmpName);\n    if (!isset($allowed[$extension]) || $allowed[$extension] !== $mime\n        || @getimagesize($tmpName) === false) {\n        throw new RuntimeException('Файл не является разрешённым изображением');\n    }\n\n    $upload['type'] = $mime;\n    return $upload;\n}\n\n$upload = validateImageUpload($_FILES['CATALOG_PREVIEW'] ?? array());
\n

Проверка MIME по содержимому и getimagesize() не заменяют права, CSRF-защиту, антивирус и безопасное хранение. Они закрывают только границу входного файла. Не используйте имя файла или присланный MIME как единственное доказательство. Ошибку нужно вернуть до создания записи и до обновления элемента.

\n

Сначала файл, затем связь

\n

Обработчик не должен искать «последний созданный файл» в базе: одновременно работают несколько пользователей. Связь строят из данных конкретного запроса и конкретного элемента. CFile::SaveFile() регистрирует файл и возвращает числовой ID, но этот ID ещё не меняет карточку.

\n
<?php\n\n$fileId = (int) CFile::SaveFile($upload, 'catalog');\nif ($fileId < 1 || !CFile::GetFileArray($fileId)) {\n    throw new RuntimeException('Bitrix не зарегистрировал файл');\n}\n\n$picture = CFile::MakeFileArray($fileId);\nif (!$picture) {\n    throw new RuntimeException('Не удалось собрать файловый массив');\n}\n\n$element = new CIBlockElement();\nif (!$element->Update((int) $elementId, array(\n    'PREVIEW_PICTURE' => $picture,\n))) {\n    throw new RuntimeException($element->LAST_ERROR ?: 'Элемент не обновлён');\n}
\n

Для поля изображения старый API ожидает файловое значение, а не произвольное число. MakeFileArray() готовит массив для методов работы с файлами; Update() возвращает true при успехе и false при ошибке, текст которой доступен в LAST_ERROR. Если регистрация прошла, а обновление нет, в b_file может остаться незакреплённая запись. Старую картинку не удаляйте до успешной привязки новой, если откат не гарантирован.

\n

Три команды редактирования

\n

Отсутствие нового файла не должно случайно означать удаление. Для существующей карточки нужны три команды: оставить старое изображение, заменить его после проверки или удалить по явному признаку. Новый файл вместе с удалением безопаснее отклонить.

\n
<?php\n\n$hasNewFile = isset($_FILES['CATALOG_PREVIEW'])\n    && ($_FILES['CATALOG_PREVIEW']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK;\n$deleteRequested = ($_POST['DELETE_PICTURE'] ?? '') === '1';\n\nif ($hasNewFile && $deleteRequested) {\n    throw new RuntimeException('Выберите новый файл или удаление');\n}\n\nif (!$hasNewFile && !$deleteRequested) {\n    // Старое изображение остаётся: поле не передаём в Update().\n    return;\n}\n\n$fields = $deleteRequested\n    ? array('PREVIEW_PICTURE' => array('del' => 'Y'))\n    : array('PREVIEW_PICTURE' => CFile::MakeFileArray($fileId));\n\n$element = new CIBlockElement();\nif (!$element->Update((int) $elementId, $fields)) {\n    throw new RuntimeException($element->LAST_ERROR ?: 'Элемент не обновлён');\n}
\n

Этот фрагмент предполагает, что авторизация, права на конкретный элемент, CSRF и нормализация входа уже выполнены. Для свойства инфоблока с типом «Файл» формат обновления может отличаться от поля PREVIEW_PICTURE. Сверяйте именно свой тип поля и реальный контракт FileInput.

\n

Проверяем запись новым чтением

\n

Ответ 200, положительный ID и обновлённый preview не закрывают задачу. После Update() нужно получить элемент новым запросом, прочитать то же поле и сравнить его с ожидаемым ID. Старый объект, заполненный до POST, способен показать прежнее значение.

\n
<?php\n\n$result = CIBlockElement::GetList(\n    array(),\n    array('ID' => (int) $elementId),\n    false,\n    false,\n    array('ID', 'PREVIEW_PICTURE')\n);\n$row = $result->GetNext();\n\nif (!$row || (int) $row['PREVIEW_PICTURE'] !== $fileId) {\n    throw new RuntimeException('Повторное чтение вернуло другой файл');\n}
\n

Затем откройте карточку отдельным HTTP-запросом и проверьте видимое изображение. Такой шаг ловит обработчик событий, кеш, неверный инфоблок и запись не в то поле. Только после него можно считать исходный сценарий менеджера закрытым.

\n

Порядок проверки

\n
  1. Назовите элемент, поле изображения и пользователя, который имеет право его менять.
  2. Зафиксируйте текущий ID картинки и правило для отправки без нового файла.
  3. Выведите FileInput с согласованными name, id и лимитами.
  4. Проверьте форму и Network: метод, enctype, имя поля и фактическое значение запроса.
  5. Отправьте небольшой учебный файл и проверьте код ответа и тело ответа.
  6. На сервере проверьте права, код загрузки, размер, путь, MIME и содержимое.
  7. Зарегистрируйте файл через согласованный API Bitrix и проверьте положительный ID.
  8. Передайте файловое значение в CIBlockElement::Update() и обработайте LAST_ERROR.
  9. Новым чтением сравните поле изображения с ожидаемым ID, затем проверьте карточку по HTTP.
  10. Повторите тест без файла, с явным удалением и с конфликтом «новый файл плюс удаление».
  11. Только после проверок определите политику очистки незакреплённых файлов и журналирования ошибок.
\n

Ограничения

\n

Параметры FileInput, доступные константы и формат возвращаемого значения зависят от версии Bitrix и подключённых модулей. Старое ядро может требовать другой способ подготовки файла. Проверяйте установленный API, а не переносите пример только по названию метода.

\n

Пять мегабайт в примере — учебное число. Реальный предел задают также upload_max_filesize, post_max_size, права каталога, обратный прокси и антивирус. Для асинхронной загрузки временный ID нужно связать с пользователем и элементом или черновиком; нельзя принять любой ID из скрытого поля.

\n

Примеры не являются готовым endpoint: в них опущены авторизация, CSRF, транзакционная политика, обработка дублей и очистка сиротских файлов. Они показывают воспроизводимую последовательность проверки. Добавьте тесты для успешной загрузки, отказа, оставления старого файла, удаления и повторного чтения.

\n

Критерий готовности

\n

Интеграция готова, если после выбора учебного изображения запрос содержит ожидаемое значение, сервер принимает только разрешённый вход, Bitrix возвращает ID, Update() завершается успешно, новое чтение возвращает тот же ID, а свежая страница показывает файл. При отправке без нового файла старая картинка остаётся. При явном удалении поле очищается. При конфликте действий сервер возвращает понятную ошибку и не меняет элемент. В этот момент менеджер видит не просто новый preview, а подтверждённый результат записи.

\n

Проверяемые источники

\n" }