Загрузка файла(ов) на сервер из формы средствами PHP
Сегодня загрузка файлов является практически неотъемлемым атрибутом современного web приложения. В данной статье речь пойдёт о том, как же загрузить файл(ы) на сервер с помощью PHP.
Настройка php.ini
[Resource Limits] ; Максимальное время выполнения скрипта в секундах max_execution_time = 60 ; Максимальное потребление памяти одним скриптом memory_limit = 64M [Data Handling] ; Максимально допустимый размер данных отправляемых методом POST post_max_size = 5M [File Uploads] ; Разрешение на загрузку файлов file_uploads = On ; Папка для хранения файлов во время загрузки upload_tmp_dir = home/user/temp ; Максимальный размер загружаемого файла upload_max_filesize = 5M ; Максимально разрешённое количество одновременно загружаемых файлов max_file_uploads = 10
Конфигурационный файл
php.ini
необходимо настраивать согласно бизнес-логики проекта! Например, мы планируем загружать не более десяти файлов до 2 Мбайт, а это значит нам понадобиться ~20 Мбайт памяти.
Загрузка одного файла на сервер из формы
Для начала разберём механизм загрузки одной картинки на сервер. Для загрузки картинки с компьютера пользователя необходимо с помощью HTML-формы отправить нужный (выбранный) файл PHP-скрипту upload.php
методом POST
и указать способ кодирования данных enctype="multipart/form-data"
(в данном случае данные не кодируются и это значение применяется только для отправки бинарных файлов).
<form action="upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="image"> <button type="submit">Загрузить</button> </form>
После отправки файла PHP-скрипту upload.php
его можно перехватить с помощью суперглобальной переменной $_FILES
с таким же именем, которая в массиве содержит информацию о файле (в нашем случае image
):
var_dump($_FILES);
Получим массив:
Array ( [name] => image.jpg // оригинальное имя файла [type] => image/jpeg // MIME-тип файла [tmp_name] => home\user\temp\phpD07E.tmp // бинарный файл [error] => 0 // код ошибки [size] => 17170 // размер файла в байтах )
Не всем данным из $_FILES
можно доверять: MIME-тип и размер файла можно подделать, т. к. они формируются из HTTP-ответа, а расширению в имени файла не стоит доверять в силу того, что за ним может скрываться совершенно другой файл. Тем не менее, дальше нам нужно проверить корректно ли загрузился наш файл и загрузился ли он вообще. Для этого необходимо проверить ошибки в $_FILES['image']['error']
и удостовериться, что файл загружен методом POST с помощью функции is_uploaded_file()
. Если что-то идёт не по плану, значит выводим ошибку на экран:
// Если в $_FILES существует "image" и она не NULL if (isset($_FILES['image'])) { $image = $_FILES['image']; // Получаем нужные элементы массива "image" $fileTmpName = $_FILES['image']['tmp_name']; $errorCode = $_FILES['image']['error']; // Проверим на ошибки if ($errorCode !== UPLOAD_ERR_OK || !is_uploaded_file($fileTmpName)) { // Массив с названиями ошибок $errorMessages = [ UPLOAD_ERR_INI_SIZE => 'Размер файла превысил значение upload_max_filesize в конфигурации PHP.', UPLOAD_ERR_FORM_SIZE => 'Размер загружаемого файла превысил значение MAX_FILE_SIZE в HTML-форме.', UPLOAD_ERR_PARTIAL => 'Загружаемый файл был получен только частично.', UPLOAD_ERR_NO_FILE => 'Файл не был загружен.', UPLOAD_ERR_NO_TMP_DIR => 'Отсутствует временная папка.', UPLOAD_ERR_CANT_WRITE => 'Не удалось записать файл на диск.', UPLOAD_ERR_EXTENSION => 'PHP-расширение остановило загрузку файла.', ]; // Зададим неизвестную ошибку $unknownMessage = 'При загрузке файла произошла неизвестная ошибка.'; // Если в массиве нет кода ошибки, скажем, что ошибка неизвестна $outputMessage = isset($errorMessages[$errorCode]) ? $errorMessages[$errorCode] : $unknownMessage; // Выведем название ошибки die($outputMessage); } else { echo 'Ошибок нет.'; } };
Для того, чтобы "редиска" не загрузил вредоносный код, встроенный в изображение, нельзя доверять функции getimagesize()
, которая также возвращает MIME-тип. Функция ожидает, что первый аргумент является ссылкой на корректный файл изображения. Определить настоящий MIME-тип картинки можно через расширение FileInfo
. Код ниже проверит наличие ключевого слова image
в типе нашего загружаемого файла и если его не окажется, выдаст ошибку:
// Создадим ресурс FileInfo $fi = finfo_open(FILEINFO_MIME_TYPE); // Получим MIME-тип $mime = (string) finfo_file($fi, $fileTmpName); // Проверим ключевое слово image (image/jpeg, image/png и т. д.) if (strpos($mime, 'image') === false) die('Можно загружать только изображения.');
Таким образом, при необходимости, делаем проверку и на другие MIME-типы. Например, для zip архивов проверка будет такая:
// Проверим ключевое слово zip (application/zip) if (strpos($mime, 'zip') === false) die('Можно загружать только архивы ZIP.');
На данном этапе мы уже можем загружать абсолютно любые картинки на наш сервер, прошедшие проверку на MIME-тип, но для загрузки изображений по определённым характеристикам нам необходимо валидировать их с помощью функции getimagesize()
, которой отдадим сам бинарный файл $_FILES['image']['tmp_name']
. В результате мы получим массив из элементов:
// Результат функции запишем в переменную $image = getimagesize($fileTmpName); var_dump($image);die; // Результат Array ( [0] => 1280 // ширина [1] => 768 // высота [2] => 2 // тип [3] => width="1280" height="768" // атрибуты для HTML [bits] => 8 // глубина цвета [channels] => 3 // цветовая модель [mime] => image/jpeg // MIME-тип )
Для дальнейшей валидации изображения и работы над ним нам необходимо знать только 3 значения: ширину, высоту и размер файла (для вычисления размера применим функцию filesize()
для бинарного файла из временной папки).
// Результат функции запишем в переменную $image = getimagesize($fileTmpName); // Зададим ограничения для картинок $limitBytes = 1024 * 1024 * 5; $limitWidth = 1280; $limitHeight = 768; // Проверим нужные параметры if (filesize($fileTmpName) > $limitBytes) die('Размер изображения не должен превышать 5 Мбайт.'); if ($image[1] > $limitHeight) die('Высота изображения не должна превышать 768 точек.'); if ($image[0] > $limitWidth) die('Ширина изображения не должна превышать 1280 точек.');
После всех проверок мы можем с уверенностью переместить наш загружаемый файл в какую-нибудь директорию с картинками. Делать лучше это через функцию move_uploaded_file(), которая работает в безопасном режиме. Перед перемещением файла нельзя забыть сгенерировать случайное имя и расширение из типа изображения для нашего файла. Вот так это выглядит:
// Сгенерируем новое имя файла на основе MD5-хеша $name = md5_file($fileTmpName); // Сгенерируем расширение файла на основе типа картинки $extension = image_type_to_extension($image[2]); // Сократим .jpeg до .jpg $format = str_replace('jpeg', 'jpg', $extension); // Переместим картинку с новым именем и расширением в папку /upload if (!move_uploaded_file($fileTmpName, __DIR__ . '/upload/' . $name . $format)) { die('При записи изображения на диск произошла ошибка.'); } echo 'Картинка успешно загружена!';
Вместо простого способа генерации имени файла на основе MD5-хеша можно пойти более продвинутым путём, а именно написать отдельную функцию, которая будет проверять уникальность названия картинки для того, чтобы случайно не перезаписать уже загруженный файл. Если такого названия ещё нет, функция сгенерирует его. Такая проблема появляется в больших проектах и с большим количеством картинок. Но всё же)
function getRandomFileName($path) { $path = $path ? $path . '/' : ''; do { $name = md5(microtime() . rand(0, 9999)); $file = $path . $name; } while (file_exists($file)); return $name; }
Генерация имени для картинки теперь будет такой:
// Сгенерируем новое имя файла через функцию getRandomFileName() $name = getRandomFileName($fileTmpName);
Мы реализовали простой, но в тоже время практичный (с точки зрения безопасности) механизм загрузки файла на сервер. Весь код целиком лежит здесь .
Загрузка нескольких файлов на сервер из формы
Разберём механизм загрузки нескольких изображений за один раз с локальной машины пользователя. Продолжим дальше работать с $_FILES
. Наша новая HTML-форма будет немного отличаться от старой.
<form action="upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="images[]" multiple> <button type="submit">Загрузить</button> </form>
Как видно в конец имени поля выбора файла name="images[]"
добавились фигурные скобки и атрибут multiple
, который разрешает браузеру выбрать несколько файлов. Все файлы снова загрузятся во временную папку, если не будет никаких ошибок в php.ini
. Перехватить их можно в $_FILES
, но на этот раз суперглобальная переменная будет иметь неудобную структуру для обработки данных в массиве. Решается эта задача небольшими манипуляциями с массивом:
// Изменим структуру $_FILES foreach($_FILES['images'] as $key => $value) { foreach($value as $k => $v) { $_FILES['images'][$k][$key] = $v; } // Удалим старые ключи unset($_FILES['images'][$key]); } // Загружаем все картинки по порядку foreach ($_FILES['images'] as $k => $v) { // Загружаем по одному файлу $fileTmpName = $_FILES['images'][$k]['tmp_name']; $errorCode = $_FILES['images'][$k]['error']; // Проверим на ошибки // ... }
Мы реализовали механизм загрузки нескольких файлов на сервер. Весь код целиком лежит здесь .