Статьи

Как обрабатывать загрузки файлов с помощью PHP

Обычная проблема, с которой сталкиваются программисты PHP, — это как принимать файлы, загруженные посетителями вашего сайта. В этом бонусном отрывке из главы 12 недавно опубликованной книги SitePoint: Создайте свой собственный веб-сайт, управляемый базой данных, используя PHP и MySQL (4-е издание) Кевина Янка, вы узнаете, как безопасно принимать файлы от посетителей вашего веб-сайта и хранить их их.

Первые 4 главы из этой книги также доступны на sitepoint.com . Если вы предпочитаете читать их в автономном режиме, вы можете скачать главы в формате PDF .

Начнем с основ: напишем HTML-форму, которая позволяет пользователям загружать файлы. HTML делает это довольно легко с помощью <input type="file"/> . Однако по умолчанию отправляется только имя файла, выбранного пользователем. Чтобы сам файл был отправлен с данными формы, нам нужно добавить enctype="multipart/form-data" в <form> :

 <form action="index.php" method="post"    enctype="multipart/form-data">  <div><label id="upload">Select file to upload:    <input type="file" id="upload" name="upload"/></label></div>  <div>    <input type="hidden" name="action" value="upload"/>    <input type="submit" value="Submit"/>  </div>  </form> 

Как мы видим, PHP-скрипт (в данном случае index.php ) будет обрабатывать данные, представленные в форме выше. Информация о загруженных файлах появляется в массиве $_FILES который автоматически создается PHP. Как и следовало ожидать, запись в этом массиве с именем $_FILES['upload'] (из атрибута name <input/> ) будет содержать информацию о файле, загруженном в этом примере. Однако вместо хранения содержимого загруженного файла $_FILES['upload'] содержит еще один массив. Поэтому мы используем второй набор квадратных скобок, чтобы выбрать нужную информацию:

 $_FILES['upload']['tmp_name'] 

Предоставляет имя файла, хранящегося на жестком диске веб-сервера в системном каталоге временных файлов, если только другой каталог не был указан с помощью параметра upload_tmp_dir в файле php.ini . Этот файл сохраняется только до тех пор, пока работает PHP-скрипт, отвечающий за обработку отправки формы. Итак, если вы хотите использовать загруженный файл позже (например, сохранить его для отображения на сайте), вам необходимо сделать его копию в другом месте. Для этого используйте функцию копирования, описанную в предыдущем разделе.

$_FILES['upload']['name']

Предоставляет имя файла на клиентском компьютере до его отправки. Если вы делаете постоянную копию временного файла, вы можете захотеть дать ему оригинальное имя вместо автоматически сгенерированного временного имени файла, описанного выше.

$_FILES['upload']['size']

Предоставляет размер (в байтах) файла.

 $_FILES['upload']['type'] 

Предоставляет MIME-тип файла (иногда упоминаемый как тип файла или тип содержимого, идентификатор, используемый для описания формата файла, например, text/plain , image/gif и т. Д.).

Помните, что « upload » — это просто атрибут имени <input/> который отправил файл, поэтому фактический индекс массива будет зависеть от этого атрибута.

Вы можете использовать эти переменные, чтобы решить, принимать или отклонять загруженный файл. Например, в фотогалерее нас действительно будут интересовать только файлы JPEG и, возможно, GIF и PNG. Эти файлы имеют MIME-типы image/jpeg , image/gif и image/png соответственно, но чтобы удовлетворить различия между браузерами, вы должны использовать регулярные выражения для проверки типа загружаемого файла:

 if (preg_match('/^image/p?jpeg$/i', $_FILES['upload']['type']) or    preg_match('/^image/gif$/i', $_FILES['upload']['type']) or    preg_match('/^image/(x-)?png$/i', $_FILES['upload']['type']))  {  // Handle the file...  }  else  {  $error = 'Please submit a JPEG, GIF, or PNG image file.';  include $_SERVER['DOCUMENT_ROOT'] . '/includes/error.html.php';  exit();  } 

Точный тип MIME зависит от используемого браузера. Internet Explorer использует image/pjpeg для изображений JPEG и image/x-png для изображений PNG, тогда как Firefox и другие браузеры используют image/jpeg и image/png соответственно. См. Главу 8 «Форматирование содержимого с помощью регулярных выражений» для получения справки о синтаксисе регулярных выражений.

Хотя вы можете использовать аналогичную технику для запрета слишком больших файлов (проверяя $_FILES['upload']['size'] ), я бы посоветовал против этого. Перед проверкой этого значения файл уже загружен и сохранен во временном каталоге. Если вы попытаетесь отклонить файлы из-за ограниченного дискового пространства и / или пропускной способности, проблема в том, что большие файлы все еще могут быть загружены, даже если они были удалены почти сразу, может быть проблемой.

Вместо этого вы можете заранее указать PHP максимальный размер файла, который вы хотите принять. Есть два способа сделать это. Первый — настроить параметр upload_max_filesize в вашем файле php.ini . Значение по умолчанию составляет 2 МБ, поэтому, если вы хотите принимать загружаемые файлы, размер которых больше этого, вам необходимо немедленно изменить это значение. Второе ограничение, влияющее на общий размер отправляемых форм, применяется post_max_size в php.ini . Его значение по умолчанию составляет 8 МБ, поэтому, если вы хотите принимать действительно большие загрузки, вам также необходимо изменить этот параметр.

Второй способ заключается в том, чтобы включить в форму скрытое поле <input/> с именем MAX_FILE_SIZE и максимальным размером файла, который вы хотите принять с этой формой в качестве значения. По соображениям безопасности это значение не может превышать параметр upload_max_filesize в вашем php.ini , но он позволяет вам принимать разные максимальные размеры на разных страницах. Например, следующая форма позволит загружать до 1 килобайта (1024 байта):

 <form action="upload.php" method="post"    enctype="multipart/form-data">  <p><label id="upload">Select file to upload:  <input type="hidden" name="MAX_FILE_SIZE" value="1024"/>    <input type="file" id="upload" name="upload"/></label></p>  <p>    <input type="hidden" name="action" value="upload"/>    <input type="submit" value="Submit"/>  </p>  </form> 

Обратите внимание, что скрытое поле MAX_FILE_SIZE должно предшествовать любым тегам <input type="file"/> в форме, поэтому PHP информируется об этом ограничении до того, как получит любые отправленные файлы. Также обратите внимание, что это ограничение может быть легко обойдено злоумышленником, который просто пишет свою собственную форму без поля MAX_FILE_SIZE . Для обеспечения отказоустойчивой защиты при загрузке больших файлов используйте параметр upload_max_filesize в php.ini .

Назначение уникальных имен файлов

Как я объяснил выше, чтобы сохранить загруженный файл, вам необходимо скопировать его в другой каталог. И хотя у вас есть доступ к имени каждого загруженного файла с его $_FILE['upload']['name'] , вы не можете гарантировать, что два файла с одинаковым именем не будут загружены. В таком случае хранение файла с его исходным именем может привести к загрузке более новых файлов с перезаписью старых.

По этой причине вы обычно захотите принять схему, которая позволяет назначать уникальное имя файла каждому загруженному файлу. Используя системное время (доступ к которому можно получить с помощью функции времени PHP), вы можете легко создать имя на основе количества секунд с 1 января 1970 года. Но что, если два файла будут загружены в течение одной секунды друг от друга? Чтобы избежать этой возможности, мы также будем использовать IP-адрес клиента (автоматически сохраняемый в $_SERVER['REMOTE_ADDR'] PHP) в имени файла. Поскольку вы вряд ли получите два файла с одного и того же IP-адреса в течение одной секунды, это приемлемое решение для большинства целей:

 // Pick a file extension  if (preg_match('/^image/p?jpeg$/i', $_FILES['upload']['type']))  {  $ext = '.jpg';  }  else if (preg_match('/^image/gif$/i', $_FILES['upload']['type']))  {  $ext = '.gif';  }  else if (preg_match('/^image/(x-)?png$/i',    $_FILES['upload']['type']))  {  $ext = '.png';  }  else  {  $ext = '.unknown';  }   // The complete path/filename  $filename = 'C:/uploads/' . time() . $_SERVER['REMOTE_ADDR'] . $ext;   // Copy the file (if it is deemed safe)  if (!is_uploaded_file($_FILES['upload']['tmp_name']) or    !copy($_FILES['upload']['tmp_name'], $filename))  {  $error = "Could not  save file as $filename!";  include $_SERVER['DOCUMENT_ROOT'] . '/includes/error.html.php';  exit();  } 

Важно отметить, что в приведенном выше коде используется функция is_uploaded_file чтобы проверить, является ли файл «безопасным». Все, что делает эта функция, это возвращает TRUE если имя файла передается в качестве параметра ( $_FILES['upload']['tmp_name'] в данном случае) фактически был загружен как часть формы. Если злонамеренный пользователь загрузил этот сценарий и вручную указал имя файла, например /etc/passwd (хранилище системных паролей на серверах Linux), и вы не смогли использовать is_uploaded_file чтобы проверить, что $_FILES['upload'] действительно ссылается на загруженный $_FILES['upload'] файл, ваш скрипт может быть использован для копирования конфиденциальных файлов на вашем сервере в каталог, из которого они станут общедоступными через Интернет! Таким образом, прежде чем вы начнете доверять переменной PHP, в которой вы ожидаете is_uploaded_file имя файла загруженного файла, обязательно используйте is_uploaded_file чтобы проверить это.

Второй трюк, который я использовал в приведенном выше коде, — это объединение is_uploaded_file и его копирование в качестве условия оператора if. Если результат is_uploaded_file($_FILES['upload']['tmp_name']) равен FALSE (что делает !is_uploaded_file($_FILES['upload']['tmp_name']) TRUE ), PHP немедленно узнает, что все условие будет TRUE когда увидит оператор или, разделяющий два вызова функций. Чтобы сэкономить время, он воздержится от запуска копирования, поэтому файл не будет скопирован, когда is_uploaded_file вернет FALSE . С другой стороны, если is_uploaded_file возвращает TRUE , PHP is_uploaded_file и копирует файл. Затем результат copy определяет, будет ли отображаться сообщение об ошибке. Точно так же, если бы мы использовали оператор and вместо or , результат FALSE в первой части условия заставил бы PHP пропустить оценку второй части. Эта характеристика операторов if известна как оценка короткого замыкания и работает в других условных структурах, таких как циклы while и for , тоже.

Наконец, обратите внимание, что в приведенном выше скрипте я использовал косые черты (/) в стиле UNIX, несмотря на то, что это путь Windows. Если бы я использовал обратную косую черту, мне пришлось бы заменить их двойной обратной косой чертой (\), чтобы PHP не интерпретировал их как экранированные символы. Тем не менее, PHP достаточно умен, чтобы преобразовывать прямые косые черты в пути к файлу в обратную косую черту, когда он работает в системе Windows. Поскольку мы также можем использовать одинарные косые черты (/), как обычно, в системах, отличных от Windows, применение косых черт в целом для путей к файлам в PHP сделает ваши сценарии более переносимыми.