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 или другое согласованное свойство.
У одной картинки есть несколько состояний. Браузер хранит выбранный объект File. HTML-форма передаёт его как multipart-часть. PHP получает массив в $_FILES. Bitrix создаёт запись в b_file и возвращает числовой ID. Элемент инфоблока хранит ссылку на этот ID в поле изображения. Ни один этап не заменяет следующий.
Preview относится к DOM. Он может измениться до отправки формы, после Ajax-перерисовки или даже при ошибочном ответе сервера. Поэтому текст «файл выбран» и URL картинки в интерфейсе нельзя использовать как доказательство сохранения. Сервер должен получить конкретное поле, проверить его и записать связь.
\n| Состояние | Причина сбоя | Проверка | Действие |
|---|---|---|---|
| Preview обновился | Изменился только DOM | Открыть Network и найти multipart-поле | Не считать файл сохранённым |
В $_FILES нет поля | Неверное name или форма без enctype | Проверить имя input, метод и Request Payload | Исправить контракт формы |
| Upload завершился ошибкой | Лимит PHP, размер или права каталога | Проверить error, size и серверный лог | Вернуть ошибку формы и остановить запись |
| Есть ID файла, но карточка прежняя | Не вызван Update() или передано не то поле | Повторно прочитать элемент и поле изображения | Сохранить файловый массив в сущность |
| После удаления картинка вернулась | Удаление выведено из пустого preview | Проверить явный флаг удаления в POST | Развести ветки «оставить», «заменить» и «удалить» |
Контрол создают на сервере. Имя поля выбирают вместе с обработчиком. Если шаблон отправляет CATALOG_PREVIEW, PHP не должен ждать picture. Такая ошибка выглядит как «Bitrix не загрузил картинку», хотя файл просто не попал в ожидаемую ветку.
Ниже учебный пример для формы с одной картинкой. Он показывает состав параметров, а не готовую конфигурацию конкретного проекта. Константы, доступность источников и набор опций нужно сверить с версией модуля main и установленными правами.
<?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?>\nname связывает разметку с серверным кодом. upload включает загрузку. allowUpload сужает сценарий до изображений, если это поддерживает установленная версия API. maxCount и maxSize уменьшают число ошибочных действий в интерфейсе. Они не заменяют серверную проверку: запрос можно отправить вручную.
delete только даёт пользователю возможность выбрать удаление через контрол. Правило хранения остаётся в обработчике. Для редактирования существующего элемента нужно заранее решить, что означает отсутствие нового файла: обычно сохранить старую картинку. Явное удаление передают отдельным признаком.
Обычная HTML-форма с файлом использует method=\"post\" и enctype=\"multipart/form-data\". Для Ajax нужен объект FormData. Вызов jQuery serialize() собирает текстовые поля, но не переносит бинарное содержимое файла. Если заменить multipart-запрос сериализацией, preview останется, а сервер получит только остальную форму.
В legacy-шаблоне частая ошибка появляется после .html(). Старый input удаляют, новый вставляют, а обработчик остаётся привязан к уничтоженному узлу. Повторная инициализация добавляет второй обработчик. Делегируйте событие стабильному контейнеру и снимайте только свой namespace.
(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Сначала проверяют результат загрузки и входные ограничения. Поле type из запроса — это заявление клиента. Нужны серверные проверки размера, расширения, MIME и содержимого изображения. Перечень допустимых форматов зависит от продукта. Не принимайте его только потому, что контрол показал кнопку выбора картинки.
<?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.
После регистрации ID нужно превратить в файловый массив для API элемента. Для поля PREVIEW_PICTURE недостаточно положить число в произвольное поле. Старый API Bitrix ожидает структуру файлового значения и возвращает ошибку через результат обновления и LAST_ERROR.
<?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Это правило нельзя выводить из пустого 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Ответ 200 и положительный ID не закрывают задачу. После Update() нужно прочитать элемент новым запросом. Читайте то же поле, которое использует страница. Старый объект, заполненный до POST, может содержать прежнее значение.
Проверка должна сравнить ожидаемый ID с фактическим. Затем откройте карточку через отдельный HTTP-запрос и проверьте видимое изображение. Этот шаг ловит обработчики событий, кеш, неверный инфоблок и запись не в то свойство.
\nFileInput с согласованным name, одной картинкой и лимитами интерфейса.multipart/form-data, ожидаемое имя поля и отсутствие дубликатов input после Ajax.CIBlockElement::Update() и обработайте LAST_ERROR.Параметры FileInput, константы разрешённых типов и формат возвращаемого значения зависят от версии Bitrix. Старое ядро может требовать другой способ подготовки файлового массива. Проверяйте установленный API, а не переносите пример по названию метода.
Лимит в пять мегабайт в коде — учебное число. Он не доказывает подходящий размер для вашей витрины. На результат также влияют upload_max_filesize, post_max_size, права каталога, обратный прокси и антивирусная проверка. Несовпадение этих ограничений проявляется до Update().
Сохранение ID файла не заменяет проверку владельца. Если файл создаётся отдельным Ajax-запросом, временный ID нужно связать с пользователем и элементом или черновиком. Нельзя принять любой ID из скрытого поля и назначить его чужой карточке.
\nПримеры выше не сообщают production-результаты и не обещают совместимость с конкретной установкой. Они показывают проверяемую последовательность. В рабочем проекте добавьте журнал ошибки без содержимого файла и персональных данных, а также тесты для успешной загрузки, отказа и повторного чтения.
\nИнтеграция готова, если после выбора учебного изображения форма делает один ожидаемый multipart-запрос, сервер принимает только разрешённый вход, Bitrix возвращает ID файла, Update() завершается успешно, а свежая страница показывает именно этот файл. При отправке без нового файла старая картинка остаётся. При явном удалении поле очищается. При конфликте действий сервер возвращает понятную ошибку и не меняет элемент.
$_FILES и серверные ограничения.При редактировании карточки товара пользователь с ролью менеджера выбирает новую фотографию в форме Bitrix и сразу видит preview. Сначала он заметил: URL картинки в DOM сменился. После нажатия «Сохранить» страница открывается со старым изображением, а поле иногда остаётся пустым.
\nЦена ошибки выше, чем у сломанной кнопки. Менеджер считает карточку обновлённой, а каталог продолжает показывать старую обложку. Повторная загрузка создаёт мусорные файлы. При массовом редактировании ошибка превращается в неверные фотографии, ручную сверку и восстановление данных.
\nТезис. Bitrix\\Main\\UI\\FileInput формирует интерфейс выбора и загрузки. Он не доказывает, что файл записан в нужное поле сущности. Готовность наступает только после трёх подтверждений: запрос принял ожидаемое значение, Bitrix зарегистрировал разрешённый файл и повторное чтение элемента вернуло этот ID в PREVIEW_PICTURE или другое согласованное свойство.
Первое предположение менеджера естественно: раз preview изменился, браузер получил файл. Проверка Network меняет картину. Нужно увидеть фактический запрос, его поле, код ответа и значение, которое обработчик передал в Bitrix. Если обновился только DOM, сервер ещё ничего не знает о выборе.
\nУ изображения несколько независимых состояний: браузер хранит объект File, HTML-форма передаёт multipart-часть, PHP собирает данные в $_FILES, Bitrix регистрирует файл в b_file, а элемент хранит ссылку на ID. При асинхронной загрузке вместо сырого файла может прийти подготовленное значение или ID. Контракт подтверждают по конкретному запросу, а не по названию input.
| Состояние | Причина сбоя | Проверка | Действие |
|---|---|---|---|
| Preview обновился | Изменился только DOM | Открыть Network и найти значение поля | Не считать файл сохранённым |
В $_FILES нет поля | Неверное name, метод или enctype | Сверить Request Payload с обработчиком | Исправить контракт или ветку чтения |
| Есть ID файла, но карточка прежняя | Не вызван Update() или передано не то поле | Повторно прочитать элемент | Передать файловое значение в сущность |
| После удаления картинка вернулась | Удаление выведено из пустого preview | Проверить явный флаг удаления | Развести «оставить», «заменить» и «удалить» |
Контрол создают через createInstance(), а разметку и JavaScript выводят вызовом show(). Имя поля выбирают вместе с серверным обработчиком. Явный id помогает отличить экземпляр контрола от другого поля на странице.
<?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 существующего файла допустима, но не означает, что новый файл уже привязан к элементу.
У FileInput есть собственный JavaScript-контрол и дополнительные источники выбора. В одной конфигурации обработчик получает обычный multipart-файл, в другой — значение, подготовленное асинхронной загрузкой. Сначала сохраните один учебный файл, откройте Network и зафиксируйте точное поле. Ветка с $_FILES['CATALOG_PREVIEW'] ниже относится только к подтверждённому multipart-контракту.
Обычная HTML-форма использует method="post" и enctype="multipart/form-data". Для Ajax нужен объект FormData. Вызов jQuery serialize() собирает текстовые поля, но не переносит бинарное содержимое файла. Если заменить multipart-запрос сериализацией, preview останется, а сервер получит только остальную форму.
После 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') здесь опасен: он может снять обработчики других компонентов.
Поле type из запроса сообщает мнение клиента, а не доказанный тип файла. Сервер сначала проверяет код загрузки, размер и временный путь, затем определяет MIME по содержимому. В учебном примере разрешены три расширения; этот список должен совпадать с политикой проекта.
<?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 как единственное доказательство. Ошибку нужно вернуть до создания записи и до обновления элемента.
Обработчик не должен искать «последний созданный файл» в базе: одновременно работают несколько пользователей. Связь строят из данных конкретного запроса и конкретного элемента. CFile::SaveFile() регистрирует файл и возвращает числовой ID, но этот ID ещё не меняет карточку.
<?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<?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.
Ответ 200, положительный ID и обновлённый preview не закрывают задачу. После Update() нужно получить элемент новым запросом, прочитать то же поле и сравнить его с ожидаемым ID. Старый объект, заполненный до POST, способен показать прежнее значение.
<?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-запросом и проверьте видимое изображение. Такой шаг ловит обработчик событий, кеш, неверный инфоблок и запись не в то поле. Только после него можно считать исходный сценарий менеджера закрытым.
\nFileInput с согласованными name, id и лимитами.enctype, имя поля и фактическое значение запроса.CIBlockElement::Update() и обработайте LAST_ERROR.Параметры FileInput, доступные константы и формат возвращаемого значения зависят от версии Bitrix и подключённых модулей. Старое ядро может требовать другой способ подготовки файла. Проверяйте установленный API, а не переносите пример только по названию метода.
Пять мегабайт в примере — учебное число. Реальный предел задают также upload_max_filesize, post_max_size, права каталога, обратный прокси и антивирус. Для асинхронной загрузки временный ID нужно связать с пользователем и элементом или черновиком; нельзя принять любой ID из скрытого поля.
Примеры не являются готовым endpoint: в них опущены авторизация, CSRF, транзакционная политика, обработка дублей и очистка сиротских файлов. Они показывают воспроизводимую последовательность проверки. Добавьте тесты для успешной загрузки, отказа, оставления старого файла, удаления и повторного чтения.
\nИнтеграция готова, если после выбора учебного изображения запрос содержит ожидаемое значение, сервер принимает только разрешённый вход, Bitrix возвращает ID, Update() завершается успешно, новое чтение возвращает тот же ID, а свежая страница показывает файл. При отправке без нового файла старая картинка остаётся. При явном удалении поле очищается. При конфликте действий сервер возвращает понятную ошибку и не меняет элемент. В этот момент менеджер видит не просто новый preview, а подтверждённый результат записи.
$_FILES, коды ошибок и проверка входного файла.