Розробка форми завантаження файлів на 1С-Бітрікс

Наша компанія займається розробкою, підтримкою та обслуговуванням рішень на Бітрікс та Бітрікс24 будь-якої складності. Від простих односторінкових сайтів до складних інтернет-магазинів, CRM систем з інтеграцією 1С та телефонії. Досвід розробників підтверджено сертифікатами від вендора.
Послуги, які ми пропонуємо
Показано 1 з 1Усі 1626 послуг
Розробка форми завантаження файлів на 1С-Бітрікс
Середній
~1-2 тижні
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1288
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    880
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Розробка на базі Бітрікс, Бітрікс24, 1С для компанії Development of an Online
    630
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Розробка на базі 1С Підприємство для компанії МИРСАНБЕЛ
    781
  • image_crm_dolbimby_434_0.webp
    Розробка сайту на CRM Бітрікс24 для компанії DOLBIMBY
    681
  • image_crm_technotorgcomplex_453_0.webp
    Розробка на базі Бітрікс24 для компанії ТЕХНОТОРГКОМПЛЕКС
    1010

Розробка форми завантаження файлів на 1С-Бітрікс

Стандартний компонент bitrix:main.feedback підтримує вкладення, але з жорсткими обмеженнями: немає превʼю для зображень, немає drag-and-drop, немає валідації типів на клієнті, немає завантаження по частинах (chunked upload) для великих файлів. Для сценаріїв, де клієнт прикріплює технічне завдання, фотографії дефекту товару або креслення — потрібна кастомна форма. Задача нетривіальна ще й тому, що Бітрікс має власну модель зберігання файлів (b_file, \CFile), і нові завантаження потрібно інтегрувати в цю модель, а не складати файли в довільні директорії.

Архітектура завантаження

Для форм з файлами використовуємо двоетапний процес:

  1. Файл завантажується окремим AJAX-запитом ще до відправки форми. Повертається тимчасовий file_id.
  2. При фінальній відправці форми передаються лише 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 тижні.