Валидация и отправка файлов на сервер с помощью ajax

октябрь 9 , 2016
Метки:
Демо Исходники

Сегодня я хочу рассказать, как отправить файлы на сервер из html-формы. Обычно эта процедура не вызывает никаких затруднений: кодировка multipart/form-data у формы, input type="file" и все. Остается принять файлы на сервере и скопировать их в нужное место.

Но мы сделаем интереснее, отправим файлы с помощью ajax, без перезагрузки страницы. А также посмотрим, как валидировать файлы на клиенте и сервере, а именно проверять максимальный размер загружаемых файлов и допустимые расширения.


Суть задачи

По итогу статьи у нас получится небольшое приложение, которое отрисует форму с тремя полями ввода и кнопкой Отправить. Будем считать, что мы хотим получить от пользователей три файла-картинки. При этом нужно задать максимальный размер загружаемых файлов и ограничить набор расширений. Проверять файлы нужно как на клиенте, для удобства пользователей, так и на сервере, для защиты от возможных атак. Если обе проверки пройдены, то скопируем файлы на сервере в папку upload.

Для нетерпеливых сразу ссылки на демо приложения и исходники

Подготовим шаблон проекта.

В корень проекта положим файл index.html, в папку js файлы jquery.min.js и main.js (не забудем их подключить в index.html). Серверный код будет выполнять upload.php из папки php, а для полученных файлов создадим папку upload.

Получится так:


html-заготовка формы

Создадим простейшую форму в index.html

    




Заметим, что никаких атрибутов, вроде enctype у формы или name у input-ов, навешивать не нужно - все сделает javascript.


Базовый клиентский код

    'use strict';
    
    // Модуль приложения
    var app = (function($) {
    
        // Отправка формы
        function submitForm(e) {
            e.preventDefault();
    
            var $photos = $('.js-photos'),
                formdata = new FormData;
    
            // Добавление файлов в formdata
            $photos.each(function(index, $photo) {
                if ($photo.files.length) {
                    formdata.append('photos[]', $photo.files[0]);
                }
            });
    
            // Отправка на сервер
            $.ajax({
                url: 'php/upload.php',
                data: formdata,
                type: 'POST',
                dataType: 'json',
                processData: false,
                contentType: false,
                success: function(responce) {
                    console.log('responce from server: ', responce);
                }
            });
        }
    
        // Инициализация
        function init() {
            $('#main-form').on('submit', submitForm);
        }
        
        return {
            init: init
        }    
    
    })(jQuery);
    
    // Запуск приложения
    jQuery(document).ready(app.init);    

Мы создали модуль приложения app, и в методе init подключили обработку сабмита формы. В функции submitForm данные для отправки готовятся с помощью объекта formdata соответствующего класса FormData. Перебирая все поля выбора файлов .js-photos, мы добавляем файлы к этому объекту методом append. Обратите внимание на название photos[] - квадратные скобки обязательны, так как в противном случае на сервер попадет не массив файлов, а только один.

Дальше в настройках метода $.ajax указываем стандартные параметры url, data и type. Отправляем данные, конечно, POST-ом. processData и contentType нужно отключить. Поставим dataType = "json", так как именно в этом формате сервер будет возвращать ответ. multipart/form-data опять не указывается явно, потому что она проставляется сама при использовании объекта FormData. Разумеется, кроме файлов Вы можете отправить и еще какие угодно данные, добавив их в formdata, например так: formdata.append('name', 'John');

Валидацию пока не делаем, рассмотрим ее ниже. А пока переходим к серверной части.


Принимаем файлы на сервере и перемещаем в нужную папку

Код обработки файлов на сервере довольно типовой, сначала привожу его, а потом кратко поясню

    $photos = $_FILES['photos'];
    $destPath = $_SERVER['DOCUMENT_ROOT'] . '/upload/';
    
    // Копирование файлов в нужную папку
    foreach ($photos['name'] as $key => $name) {
        $tempName = $photos['tmp_name'][$key];
        $destName = $destPath . $name;
    
        move_uploaded_file($tempName, $destName);
    }
    
    // Возвращаем ответ клиенту
    echo json_encode(array(
        'code' => 'success'
    ));    

Сначала мы извлекаем массив файлов из $_FILES['photos'] и отпределяем папку назначения - куда мы копируем искомые файлы. PHP предварительно копирует файлы во временную папку, свойство tmp_name из $_FILES даст нам полный путь к этому временному файлу. Далее перебираем наш массив и перемещаем файлы в нужную папку под тем же названием, с которым он пришел с клиента. Этим занимается функция move_uploaded_file. В конце возвращаем клиенту успешный код ответа.

В принципе, чтобы просто отправить файлы на сервер, приведенного кода достаточно. Но при этом у нас нет никаких средств для проверки правильности выбранных файлов и защиты от атак. Поэтому дальше займемся валидацией файлов на клиенте и сервере. Начнем с клиента.


Валидация файлов на клиенте

Нам нужна функция, которая проверит массив файлов с формы на некоторые условия, а именно:

  • 1. Выбран ли вообще файл
  • 2. Не превышает ли его размер максимальный
  • 3. Подходит ли файл по формату
Если какие-то условия не выполняются, то будем складывать в массив имя файла, в котором произошла ошибка, и указывать код ошибки. Дополнительно укажем, в каком по счету элементе input ошибка.

Например, ошибка может выглядеть так.

    {
        index: 0,
        name: 'test.pdf',
        errorCode: 'wrong_type'
    }

Это означает, что в input[0] (в первом по счету поле) пользователь добавил файл test.pdf, формат которого не поддерживается, так как мы просим изображения. Это одна ошибка валидации. А массив таких ошибок и будет результатом выполнения функции. Если ошибок нет, все файлы выбраны и заданы правильно, то вернем пустой массив.

Впрочем, сейчас посмотрим, как это работает. Добавим функцию валидации validateFiles.

    // Валидация файлов
    function validateFiles(options) {
        var result = [],
            file;

        // Перебираем файлы
        options.$files.each(function(index, $file) {
            // Выбран ли файл
            if (!$file.files.length) {
                result.push({index: index, errorCode: 'no_file'});
                // Остальные проверки не имеют смысла, переходим к следующему файлу
                return;
            }

            file = $file.files[0];
            // Проверяем размер
            if (file.size > options.maxSize) {
                result.push({index: index, name: file.name, errorCode: 'big_file'});
            }
            // Проверяем тип файла
            if (options.types.indexOf(file.type) === -1) {
                result.push({index: index, name: file.name, errorCode: 'wrong_type'});
            }
        });

        return result;
    }

В параметре options мы передаем объект из трех полей: $files, maxSize и types. Соответственно, это jQuery-массив элементов input, максимальный размер файла в байтах и массив допустимых типов-расширений файлов, например, image/jpg, image/png или application/pdf - список типов легко можно загуглить.

Мы перебираем массив файлов и последовательно делаем проверки. Сначала смотрим, выбран ли вообще файл. Если нет, добавляем в результирующий массив ошибок пукнт с кодом no_file. Это будет объект-ошибка {index: index, errorCode: 'no_file'} - имени предсказуемо нет. Дальше мы сразу переходим к следующему файлу, проверит размер и расширение мы не сможем.

Проверка на максимальный размер в случае неуспеха вернет объект {index: index, name: file.name, errorCode: 'big_file'}, а несоответствие типа - {index: index, name: file.name, errorCode: 'wrong_type'}

Обратите внимание, если в одном файле ловится несколько ошибок (максимум две в нашем случае), например, test.pdf слишком большого веса, то в результате мы получим 2 объекта. Имеет смысл группировать эти ошибки по имени файла, но не хочется усложнять код. В конце концов, если это необходимо, Вы сможете обработать выводимый результат как угодно.

Функцию написали, осталось задействовать ее в основном потоке кода. Немного расширим код submitForm:

    // Отправка формы
    function submitForm(e) {
        e.preventDefault();

        var $photos = $('.js-photos'),
            formdata = new FormData,
            validationErrors = validateFiles({
                $files: $photos,
                maxSize: 2 * 1024 * 1024,
                types: ['image/jpeg', 'image/jpg', 'image/png']
            });

        // Валидация
        if (validationErrors.length) {
            console.log('client validation errors: ', validationErrors);
            return false;
        }

        // Добавление файлов в formdata
        ...

        // Отправка на сервер
        ...
    }

Как видим, мы добавили вызов validateFiles с нужными параметрами. Максимальный размер файла ограничим 2 Мб, а типы возьмем jpg и png-картинки. После вызова проверяем, не пустой ли массив полученных ошибок, и если таки не пустой, то выводим результаты в консоль и выходим из функции сабмита. Файлы на сервер не уйдут.

Как обрабатывать ошибки и показывать их пользователю - дело исключительно хозяйское :-) Нам сейчас главное убедиться, что наша валидация работает.

Клиенсткий код закончен, можете побаловаться с файлами, добавить или убрать форматы разрешенных типов или изменить максимальный размер. Наш клиентский код должен точно реагировать на все попытки загрузить "неправильные" файлы. А мы переходим к валидации на стороне сервера.


Валидация файлов на сервере

Вообще для посетителей сайта будет срабатывать клиентская валидация. Тем самым наши посетители получат мгновенную обратную связь, а сервер освободится от ненужной работы. Но в случае получения пользовательских данных всегда нужно проводить дополнительную валидацию на стороне сервера. Этим мы и займемся.

php-шная функция валидации будет проверять ровно те же параметры, что и клиентская. Но кроме двух пунктов: проверки на наличие файла и возврат индекса. Это связано с тем, что мы проверяем все файлы пришедшие с клиента и не знаем, что некоторые клиент мог не заполнить. Лучше увидеть на примере. Напишем саму функцию

    // Валидация файлов
    function validateFiles($options) {
        $result = array();
    
        $files = $options['files'];
        foreach ($files['tmp_name'] as $key => $tempName) {
            $name = $files['name'][$key];
            $size = filesize($tempName);
            $type = $files['type'][$key];
    
            // Проверяем размер
            if ($size > $options['maxSize']) {
                array_push($result, array(
                    'name' => $name,
                    'errorCode' => 'big_file'
                ));
            }
    
            // Проверяем тип файла
            if (!in_array($type, $options['types'])) {
                array_push($result, array(
                    'name' => $name,
                    'errorCode' => 'wrong_type'
                ));
            }
        }
    
        return $result;
    }

Обращаю внимание, что для синхронизации клиента и сервера в обеих функциях валидации используется одинаковый формат объекта-ошибки и коды ошибок. Это облегчит нам обработку ошибок в интерфейсе независимо от того, сработала ли валидация клиентская или серверная.

После этого остается использовать написанную функцию в основном коде

    $photos = $_FILES['photos'];
    $destPath = $_SERVER['DOCUMENT_ROOT'] . '/upload/';
    
    // Валидация
    $validationErrors = validateFiles(array(
        'files' => $photos,
        'maxSize' => 2 * 1024 * 1024,
        'types' => array('image/jpeg', 'image/jpg', 'image/png')
    ));
    
    if (count($validationErrors) > 0) {
        // Возвращаем список ошибок клиенту
        echo json_encode($validationErrors);
        exit;
    }
    
    // Копирование файлов в нужную папку
    ...

    // Возвращаем ответ клиенту
    echo json_encode(array(
        'code' => 'success'
    ));

Вот и все. Как видим, валидация на сервере подключена ровно таким же способом, как и на клиенте.

Чтобы убедиться, что и серверная валидация успешно работает, закомментируйте одну строку в main.js

    if (validationErrors.length) {
        console.log('client validation errors: ', validationErrors);
        // return false;
    }

Теперь попробуйте ввести в форму ошибочные данные и увидите, что в консоли выпадет сообщение "client validation errors:" с массивом ошибок от клиентской валидации, а следом "responce from server:" с ошибками от серверной. Ошибки должны быть совершенно идентичными, за исключением no_file на клиенте и отсутствия поля index на сервере.

Например, если Вы пропустите первое поле, во второе загрузите pdf-ку, а в третье - валидную картинку, то увидите примерно такую картину


Ссылки и исходники

Демо приложения
Скачать исходники

Демо Исходники
Метки:
Заходите в группу в контакте - https://vk.com/webdevkin
Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.
Истории из жизни айти и обсуждение кода.
Как Вам статья? Оцените!