Разработка формы загрузки файлов на 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Разработка формы загрузки файлов на 1С-Битрикс
Средняя
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1181
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    813
  • 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 Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    747
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Разработка формы загрузки файлов на 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 недели.