Разработка формы загрузки файлов на 1С-Битрикс
Стандартный компонент bitrix:main.feedback поддерживает вложения, но с жёсткими ограничениями: нет превью для изображений, нет drag-and-drop, нет валидации типов на клиенте, нет загрузки по частям (chunked upload) для больших файлов. Для сценариев, где клиент прикрепляет техническое задание, фотографии дефекта товара или чертежи — нужна кастомная форма. Задача нетривиальна ещё и потому, что Битрикс имеет собственную модель хранения файлов (b_file, \CFile), и новые загрузки нужно интегрировать в эту модель, а не складывать файлы в произвольные директории.
Архитектура загрузки
Для форм с файлами используем двухэтапный процесс:
- Файл загружается отдельным AJAX-запросом ещё до отправки формы. Возвращается временный
file_id. - При финальной отправке формы передаются только
file_id-шники, не сами файлы.
Это избавляет от таймаутов при загрузке больших файлов и позволяет показывать прогресс загрузки независимо для каждого файла.
Загрузка файла на сервер
// /local/api/upload.php
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
// CSRF
if (!\bitrix_sessid_check($_POST['sessid'] ?? '')) {
http_response_code(403);
exit(json_encode(['error' => 'Invalid session']));
}
$uploader = new \Local\Upload\FileUploader();
try {
$result = $uploader->handle($_FILES['file'] ?? null);
echo json_encode(['success' => true, 'file' => $result]);
} catch (\Local\Upload\UploadException $e) {
http_response_code(422);
echo json_encode(['error' => $e->getMessage()]);
}
namespace Local\Upload;
class FileUploader
{
private const ALLOWED_TYPES = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
];
private const MAX_SIZE = 20 * 1024 * 1024; // 20 MB
private const MAX_FILES_PER_SESSION = 10;
public function handle(?array $file): array
{
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
throw new UploadException($this->getUploadError($file['error'] ?? -1));
}
if ($file['size'] > self::MAX_SIZE) {
throw new UploadException('Файл слишком большой. Максимум 20 МБ.');
}
// Проверяем MIME через finfo, не по расширению
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, self::ALLOWED_TYPES, true)) {
throw new UploadException('Недопустимый тип файла: ' . htmlspecialchars($mimeType));
}
// Антивирус: если есть ClamAV
if (function_exists('cl_scanfile')) {
$scanResult = cl_scanfile($file['tmp_name']);
if ($scanResult !== CL_CLEAN) {
throw new UploadException('Файл не прошёл проверку безопасности.');
}
}
// Лимит файлов на сессию
$sessionKey = 'upload_count_' . session_id();
$count = (int)($_SESSION[$sessionKey] ?? 0);
if ($count >= self::MAX_FILES_PER_SESSION) {
throw new UploadException('Превышен лимит файлов в одной заявке.');
}
$_SESSION[$sessionKey] = $count + 1;
// Сохраняем через CFile Битрикс
$fileId = $this->saveToStorage($file, $mimeType);
return [
'id' => $fileId,
'name' => $file['name'],
'size' => $file['size'],
'mime' => $mimeType,
'is_image' => str_starts_with($mimeType, 'image/'),
'preview' => str_starts_with($mimeType, 'image/')
? \CFile::GetPath($fileId)
: null,
];
}
private function saveToStorage(array $file, string $mimeType): int
{
$fileData = [
'name' => $file['name'],
'size' => $file['size'],
'type' => $mimeType,
'tmp_name' => $file['tmp_name'],
'error' => 0,
'MODULE_ID' => 'local.upload',
'del' => '',
'old_file' => '',
];
$fileId = \CFile::SaveFile($fileData, 'upload/forms');
if (!$fileId) {
throw new UploadException('Ошибка сохранения файла.');
}
return $fileId;
}
}
Chunked upload для больших файлов
Для файлов свыше 50 МБ стандартный upload через один POST-запрос ненадёжен. Реализуем чанкованную загрузку:
class ChunkedUploader {
constructor(file, options = {}) {
this.file = file;
this.chunkSize = options.chunkSize || 5 * 1024 * 1024; // 5 MB
this.onProgress = options.onProgress || (() => {});
this.uploadId = null;
}
async upload() {
// Инициализируем multipart upload
const initRes = await fetch('/local/api/upload-init.php', {
method: 'POST',
body: JSON.stringify({
filename : this.file.name,
size : this.file.size,
mime : this.file.type,
sessid : BX.bitrix_sessid(),
}),
headers: { 'Content-Type': 'application/json' },
});
const { upload_id } = await initRes.json();
this.uploadId = upload_id;
const totalChunks = Math.ceil(this.file.size / this.chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, this.file.size);
const chunk = this.file.slice(start, end);
const formData = new FormData();
formData.append('upload_id', this.uploadId);
formData.append('chunk_index', i);
formData.append('total_chunks', totalChunks);
formData.append('chunk', chunk);
await fetch('/local/api/upload-chunk.php', {
method: 'POST',
body: formData,
});
this.onProgress(Math.round((i + 1) / totalChunks * 100));
}
// Финализируем
const finalRes = await fetch('/local/api/upload-finalize.php', {
method: 'POST',
body: JSON.stringify({ upload_id: this.uploadId }),
headers: { 'Content-Type': 'application/json' },
});
return finalRes.json(); // { file_id, name, size, ... }
}
}
Финальная отправка формы с file_id
// Обработчик финальной формы
$fileIds = array_map('intval', $data['file_ids'] ?? []);
// Проверяем, что все file_id принадлежат текущей сессии
$validFileIds = $this->validateFileOwnership($fileIds);
$leadFields = [
'TITLE' => 'Заявка с файлами',
'PHONE' => [['VALUE' => $phone, 'VALUE_TYPE' => 'WORK']],
'STATUS_ID' => 'NEW',
];
$lead = new \CCrmLead(false);
$leadId = $lead->Add($leadFields, true);
// Привязываем файлы к лиду через активности или пользовательские поля
foreach ($validFileIds as $fileId) {
$this->attachFileToLead($leadId, $fileId);
}
Привязка файла к лиду: пользовательское поле типа FILE
Создаём пользовательское поле UF_ATTACHMENTS типа File с флагом MULTIPLE = Y:
$userTypeManager = \Bitrix\Main\UserField\TypeManager::getInstance();
$connection = \Bitrix\Main\Application::getConnection();
// Или через интерфейс: CRM → Настройки → Пользовательские поля → Лиды
$uft = new \CUserTypeEntity();
$uft->Add([
'ENTITY_ID' => 'CRM_LEAD',
'FIELD_NAME' => 'UF_ATTACHMENTS',
'USER_TYPE_ID' => 'file',
'MULTIPLE' => 'Y',
'MANDATORY' => 'N',
'EDIT_FORM_LABEL' => ['ru' => 'Вложения'],
'LIST_COLUMN_LABEL' => ['ru' => 'Вложения'],
]);
При обновлении лида с file_id:
$lead->Update($leadId, [
'UF_ATTACHMENTS' => $validFileIds, // массив ID файлов
], true);
Состав работ
- Компонент
local:file.uploadс drag-and-drop зоной и превью - Серверный обработчик загрузки: валидация типов через finfo, CSRF, rate limit
- Сохранение через
CFile::SaveFile()вb_file - Chunked upload для файлов > 10 МБ
- Пользовательское поле
UF_ATTACHMENTS(FILE, MULTIPLE) в CRM - Привязка загруженных файлов к созданному лиду
- Уведомление менеджеру с прямыми ссылками на вложения
Сроки: базовая форма с одним вложением — 3–5 дней. Полная версия с drag-and-drop, превью, chunked upload и интеграцией CRM — 2–3 недели.







