Розробка форми завантаження файлів на 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 тижні.







