Валидация и отправка файлов на сервер с помощью ajax
Сегодня я хочу рассказать, как отправить файлы на сервер из 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. Подходит ли файл по формату
Например, ошибка может выглядеть так.
{ 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-ку, а в третье - валидную картинку, то увидите примерно такую картину
Ссылки и исходники
Демо приложения
Скачать исходники
Истории из жизни айти и обсуждение кода.