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