|
Pers.narod.ru. PHP. Статьи. Загрузка файлов на сервер с помощью PHP |
Соответствующий код я сейчас попробую написать в учебных целях, возможно, какие-то моменты учесть не получится, но основа для Ваших собственных решений будет.
Сначала сформулируем требования к скрипту для загрузки файлов или картинок на PHP.
Как минимум, такой код должен уметь следующее:
Сначала все задаваемые пользователем настройки соберём в файл config.php:
<?php
define ("MAX_WIDTH","800"); //Макс. ширина картинки в пикселах
define ("MAX_HEIGHT","800"); //Макс. высота картинки в пикселах
define ("FOLDER","./uploads");
//Путь к папке для картинок, БЕЗ слеша в конце, латинские буквы
define ("MAX_SIZE","1048576"); //Максимальный размер файла, байт
define ("USE_GDLIB","1");
//Использовать библиотеку GDLib (php_gd2.dll) для масштабирования больших изображений
//1 - включить, 0 - выключить
?>
Никаких особенных настроек нашему скрипту не понадобится.
Если скрипт размещается на хостинге, на папку загрузок uploads достаточно прав 755
(про права говорится в
этом месте сайта
), при размещении на локалхосте и локальном сервере Windows проще всего дать права 777 для
гостевой учетной записи интернета и учётной записи для запуска IIS (
ссылка).
Следует также помнить, что для успешной загрузки файлов должны быть такие же права на временную папку PHP, узнать которую
можно из настройки upload_tmp_dir файла php.ini (на хостинге обычно всё уже настроено и настройка пуста).
Так как нашему скрипту параметры будут передаваться только методом POST, для обработки
параметров достаточно следующего кода (файл params.php):
<?php
while (list($num,$var) = each($params)) {
if (!empty($_POST[$var])) $$var = htmlspecialchars($_POST[$var]);
else $$var = '';
}
?>
Список разрешённых параметров будет задан в главном файле index.php, хотя уже ясно,
что таковой понадобится только один - имя кнопки "Загрузить".
Напишем несколько полезных для поставленной задачи функций и включим их в файл functions.php.
Во-первых, это функция gdVersion для определения версии библиотеки gdLib (для надёжности
потребуем версию не ниже 2, что соответствует PHP 5 версии).
Функция tumbmaker будет заниматься
обработкой изображений, её заголовок таков:
function tumbmaker ($src, $dest, $width, $height, $rgb=0xFFFFFF, $quality=65)
Параметры $src и $dest - ссылки на исходный и целевой файл, $width и $height - требуемые
ширина и высота рисунка в пикселах, параметр $rgb задаёт фоновый цвет нового изображения (по
умолчанию принят белый), а параметр $quality определяет качество генерируемого изображения
JPEG (по умолчанию 65%).
Эта функция преобразует размерности к тем, что мы указали. Чтобы вычислить "правильные" размерности и пропорционально сжать рисунок,
напишем маленькую вспомогательную функцию get_new_size:
function get_new_size($width,$height,$max_width,$max_height)
Она пропорционально преобразует размеры $width и $height с учётом
максимального прямоугольника, куда разрешено вписывать рисунок $max_width и $max_height.
Четвёртая функция - let_to_num - будет переводить размерности, хранящиеся в файле
php.ini, в нормальное число байт - потому что 2 мегабайта, к примеру, там обозначены
как 2M.
Наконец, функция cyr2lat будет заменять в переданном ей тексте символы кириллицы на
аналогичные латинские буквы. Просто далеко не каждый хостинг сможет записать файл с
кириллицей в имени.
Вот что получилось (файл functions.php):
<?php
require_once "config.php";
function gdVersion() {
$gdv=@gd_info();
$ver=$gdv['GD Version'];
$v=0;
if (preg_match("/.*([0-9]+)\.([0-9]+)\.([0-9]+).*/", $ver, $r))
$v=$r[1];//.".".$r[2].".".$r[3];
return $v;
}
function get_new_size($width,$height,$max_width,$max_height) {
$w=$width; $h=$height;
$dw=$max_width/$width; $dh=$max_height/$height;
if ($width>$max_width and $height>$max_height) {
if ($dw<$dh) { $w=$max_width; $h=floor($height*$dw); }
else { $h=$max_height; $w=floor($width*$dh); }
}
else if ($width>$max_width and $height<=$max_height) {
$w=$max_width; $h=floor($height*$dw);
}
else if ($width<=$max_width and $height>$max_height) {
$h=$max_height; $w=floor($width*$dh);
}
return array ($w, $h);
}
function tumbmaker ($src, $dest, $width, $height, $rgb=0xFFFFFF, $quality=65) {
if (!file_exists($src)) return false;
$size = getimagesize($src);
if ($size === false) return false;
$format = strtolower(substr($size['mime'], strpos($size['mime'], '/')+1));
$icfunc = "imagecreatefrom" . $format;
if (!function_exists($icfunc)) return false;
$x_ratio = $width / $size[0];
$y_ratio = $height / $size[1];
$ratio = min($x_ratio, $y_ratio);
$use_x_ratio = ($x_ratio == $ratio);
$new_width = $use_x_ratio ? $width : floor($size[0] * $ratio);
$new_height = !$use_x_ratio ? $height : floor($size[1] * $ratio);
$new_left = $use_x_ratio ? 0 : floor(($width - $new_width) / 2);
$new_top = !$use_x_ratio ? 0 : floor(($height - $new_height) / 2);
$isrc = $icfunc($src);
$idest = imagecreatetruecolor($width, $height);
imagefill($idest, 0, 0, $rgb);
imagecopyresampled($idest, $isrc, $new_left, $new_top, 0, 0,
$new_width, $new_height, $size[0], $size[1]);
imagejpeg($idest, $dest, $quality);
imagedestroy($isrc);
imagedestroy($idest);
return true;
}
function let_to_num($v){ //Размерности php.ini в байты
$l = substr($v, -1);
$ret = substr($v, 0, -1);
switch(strtoupper($l)){
case 'P': $ret *= 1024;
case 'T': $ret *= 1024;
case 'G': $ret *= 1024;
case 'M': $ret *= 1024;
case 'K': $ret *= 1024;
break;
}
return $ret;
}
function cyr2lat ($text) {
$cyr2lat_replacements = array (
"А" => "a","Б" => "b","В" => "v","Г" => "g","Д" => "d",
"Е" => "e","Ё" => "yo","Ж" => "dg","З" => "z","И" => "i",
"Й" => "y","К" => "k","Л" => "l","М" => "m","Н" => "n",
"О" => "o","П" => "p","Р" => "r","С" => "s","Т" => "t",
"У" => "u","Ф" => "f","Х" => "h","Ц" => "ts","Ч" => "ch",
"Ш" => "sh","Щ" => "csh","Ъ" => "","Ы" => "i","Ь" => "",
"Э" => "e","Ю" => "yu","Я" => "ya",
"а" => "a","б" => "b","в" => "v","г" => "g","д" => "d",
"е" => "e","ё" => "yo","ж" => "dg","з" => "z","и" => "i",
"й" => "y","к" => "k","л" => "l","м" => "m","н" => "n",
"о" => "o","п" => "p","р" => "r","с" => "s","т" => "t",
"у" => "u","ф" => "f","х" => "h","ц" => "ts","ч" => "ch",
"ш" => "sh","щ" => "sch","ъ" => "","ы" => "i","ь" => "",
"э" => "e","ю" => "yu","я" => "ya",
"-" => "_"," " => "_"
);
return strtr ($text,$cyr2lat_replacements);
}
?>
Осталось написать сам скрипт и поместить его в файл index.php.
После формирования обычной титульной части html-файла:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=windows-1251"> <title>Сервис для загрузки файла</title> </head> <body><div align="center">
подключим файл с функциями и наш обработчик параметров:
<?php
require_once "functions.php";
$params = array ('ok');
require_once ("params.php");
?>
Форма для загрузки файла будет включать только соответствующий элемент HTML и кнопку "Загрузить":
<form action="index.php" method="post" enctype="multipart/form-data"> <input type="file" name="url"> <input type="submit" name="ok" value="Загрузить"> </form>
Атрибуты method="post" и enctype="multipart/form-data" в такой форме обязательны -
иначе никакой загрузки не выйдет.
Вся остальная часть файла, если не считать закрывающих тегов в самом конце -
</div></body> </html>
- будет состоять из одного блока PHP-кода, основные действия которого мы сейчас напишем.
Если форма была отправлена кнопкой "Загрузить", в параметрах должна присутствовать переменная $ok, если её нет -
вернуться:
if (empty($ok)) return;
Далее установим служебные переменные - сообщение об ошибке $error, временное имя переданного файла $file,
флаг загрузки файла $load, флаг $need_move для функции загрузки временного файла move_uploaded_file
(дело в том, что при масштабировании рисунка он может быть загружен и функцией tumbmaker, тогда
стандартная move_uploaded_file не нужна):
$error = $file = ''; $load = true; $need_move= true;
Всё остальное поместим в блок условия, проверяющего, что файл был передан:
if (isset($_FILES['url']) and !empty($_FILES['url']['tmp_name'])) {
//здесь будет остальной код
}
else {
$error .= '<br>Файл не передан.';
if (!empty($_FILES['url']['error'])) $error .=' Код ошибки: '.$_FILES['url']['error'];
}
Получаем временное имя файла и его размер в байтах, а также ограничение на размер - это минимальная
из величин MAX_SIZE (наше собственное ограничение из конфигурации), post_max_size (настройка php.ini, определяющая
максимальный разрешённый размер файла для передачи методом POST) и
upload_max_filesize (стандартная настройка для максимального размера загружаемого файла).
Если размер превышен, формируем соответствующее сообщение:
$file = $_FILES['url']['tmp_name'];
$file_size = $_FILES['url']['size'];
$php_max_size = min(let_to_num(ini_get('post_max_size')),
let_to_num(ini_get('upload_max_filesize')));
$max_size=min(MAX_SIZE,$php_max_size);
if ($file_size > $max_size) {
$error .= '<br>Слишком большой по объёму файл. Ограничение в настройках '.
($max_size==MAX_SIZE?'скрипта (config.php)':
'PHP (см. настройки post_max_size,upload_max_filesize)').
': '.$max_size.' байт';
$load=false;
}
if ($_FILES['url']['error']!=UPLOAD_ERR_OK) {
$error .= '<br>Сервер вернул ошибку закачки файла. Код ошибки: '.
$_FILES['url']['error'];
$load=false;
}
Извлекаем тип переданного файла, формируем путь к записываемому файлу $filename, при этом имя файла,
возможно, будет изменено методом cyr2lat:
$path_parts = pathinfo($_FILES['url']['name']); $filetype = strtolower($path_parts['extension']); $filename = FOLDER.'/'.cyr2lat(strtolower($path_parts['basename']));
По типу файла (не самый надёжный способ на самом деле) проверяем, загружается ли картинка. Для этого
введём флаг $is_pic:
$is_pic = true;
switch ($filetype) {
case "jpeg":
case "jpg":
case "gif":
case "png": break;
default: $is_pic=false;
}
Если это картинка, проверяем её размер, при необходимости пытаемся сжать, если включена настройка
USE_GDLIB и библиотека доступна. Если сжать рисунок удалось, ставим $need_move=false, чтобы
не грузить рисунок повторно методом move_uploaded_file:
if ($is_pic) {
$size = getimagesize($file);
$width = $size[0];
$height = $size[1];
if ($width>MAX_WIDTH or $height>MAX_HEIGHT) {
$ver = gdVersion();
if (USE_GDLIB=='1') {
if ($ver>1) {
$new_size = get_new_size($width,$height,MAX_WIDTH,MAX_HEIGHT);
$r=tumbmaker ($file,$filename,$new_size[0],$new_size[1]);
if (!$r) {
$error .= '<br>Не удалось программно уменьшить изображение.
Пожалуйста, проверьте работу библиотеки GBLib или загрузите
картинку меньшего размера. Установки из настроек сайта:
ширина до '.MAX_WIDTH.', высота до '.MAX_HEIGHT.' пикс.';
$load=false;
}
else $need_move=false;
}
else {
$error .= '<br>В настройках сайта включено масштабирование
больших изображений, но библиотека GDLib недоступна.
Пожалуйста, настройте её или загрузите картинку меньшего размера.
Установки из настроек сайта: ширина до '.MAX_WIDTH.', высота до '.
MAX_HEIGHT.' пикс.';
$load=false;
}
}
else {
$error .= '<br>Превышен максимальный размер рисунка. Установки
из настроек сайта: ширина до '.MAX_WIDTH.', высота до '.MAX_HEIGHT.
' пикс. Автоматическое масштабирование изображений: выключено';
$load=false;
}
}
}
После всего этого, если установлены флаги $load и $need_move, можно загрузить картинку
стандартным методом. После этого ставим на загруженную картинку права 644:
if ($load) {
if ($need_move) {
if (move_uploaded_file ($file,$filename)) {
chmod ($filename,0644);
}
else {
$error .= '<br>Не удалось закачать файл '.$filename.' из временного '.
$file.'<br>Информация для отладки: ';
print_r($_FILES);
}
}
}
Теперь в конце основного блока кода можно проверить, удалён ли временный файл (по идее,
move_uploaded_file удалит его сама после успешной загрузки), проверить, не нужно ли вывести сообщение об
ошибке, если нет - дать ссылку на загруженный файл:
if (file_exists($file)) @unlink ($file);
if (!empty($error)) {
print 'Возникли проблемы!'.$error;
}
else {
print 'Файл загружен, вот он:
<a href="'.$filename.'" target="_blank">'.$filename.'</a>';
}
Вот что получилось у меня после проверки кода на хостинге в браузере Google Chrome:

Конечно, в реальном скрипте нужно ещё добавить авторизацию пользователя, контроль лимита общего объёма загруженных файлов, возможность работать с их списком и т.д. Но всё это можно сделать на основе других статей, например, вот здесь есть построение дерева папок на PHP, а здесь - про авторизацию с помощью сессий.
Код этого примера в архиве ZIP (4 Кб)
|
Возможна ли безопасная загрузка файлов |
Отдельно нужно сказать несколько слов о безопасности загрузки файлов. Следует понимать, что любая форма для загрузки юзером файлов на ваш сайт потенциально опасна. Например, толковый юзер может загрузить shell-файл и получить доступ к вашему серверу. Чтобы такого не произошло, надо убрать права "остальным пользователям", то есть, поставить права 666 на папку, куда пользователи загружают файлы. По умолчанию там обычно права 755.
Но и этого может оказаться недостаточно. Я считаю, что
файлы можно "складывать" на сервер как обычно, но под именем,
полученным скриптом (функцией, uniqid, например), а настоящее имя файла (если оно нужно) и
MIME-тип лучше хранить в базе. При этом "отдавать" файлы тоже лучше не "напрямую",
а скриптом. Вот пример простого класса для поддержки скачивания файлов:
<?php
class download {
var $properties = array(
'old_name' => "",
'new_name' => "",
'type' => "",
'size' => "",
'resume' => "",
'max_speed' => ""
);
var $range = 0;
function download($path, $name="", $resume=0, $max_speed=0) {
$name = ($name == "") ? substr(strrchr("/".$path,"/"),1) : $name;
$file_size = @filesize($path);
$this->properties = array (
'old_name' => $path,
'new_name' => $name,
'type'=> "application/force-download",
'size' => $file_size,
'resume' => $resume,
'max_speed' => $max_speed
);
if ($this->properties['resume']) {
if (isset($_SERVER['HTTP_RANGE'])) {
$this->range = $_SERVER['HTTP_RANGE'];
$this->range = str_replace("bytes=", "", $this->range);
$this->range = str_replace("-", "", $this->range);
}
else {
$this->range = 0;
}
if ($this->range > $this->properties['size']) $this->range = 0;
}
else {
$this->range = 0;
}
}
function download_file() {
if ($this->range) { header($_SERVER['SERVER_PROTOCOL']." 206 Partial Content"); }
else { header($_SERVER['SERVER_PROTOCOL']." 200 OK"); }
header("Pragma: public");
header("Expires: 0");
header("Cache-Control:");
header("Cache-Control: public");
header("Content-Description: File Transfer");
header("Content-Type: ".$this->properties["type"]);
header('Content-Disposition: attachment; filename="'.$this->properties["new_name"].'";');
header("Content-Transfer-Encoding: binary");
if ($this->properties['resume']) header("Accept-Ranges: bytes");
if ($this->range) {
header("Content-Range: bytes {$this->range}-".($this->properties['size']-1)."/".$this->properties['size']);
header("Content-Length: ".($this->properties['size']-$this->range));
}
else {
header("Content-Length: ".$this->properties['size']);
}
@ini_set('max_execution_time', 0);
@set_time_limit();
$this->_download($this->properties["old_name"], $this->range);
}
function _download ($filename, $range=0) {
@ob_end_clean();
if (($speed = $this->properties['max_speed']) > 0) $sleep_time = (8 / $speed) * 1e6;
else $sleep_time = 0;
$handle = fopen($filename, 'rb');
fseek($handle,$range);
if ($handle === false) { return false; }
while (!feof($handle)) {
print (fread($handle, 1024*8));
ob_flush();
flush();
usleep($sleep_time);
}
fclose($handle);
return true;
}
}
?>
|
|