Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Самоучитель по PHP 4

.pdf
Скачиваний:
82
Добавлен:
02.05.2014
Размер:
4.36 Mб
Скачать

Глава 28. Загрузка файлов на сервер

391

Скоро мы узнаем, как извлечь эту информацию в программе на PHP.

Закачка файлов и безопасность

Возможно, вы обратили внимание на то, что у последнего приведенного тэга <input type=file> отсутствует атрибут value. То есть когда пользователь открывает страницу, он никогда не увидит в элементе закачки ничего, кроме пустой строки. Поначалу это кажется довольно неприятным ограничением: в самом деле, мы ведь можем задавать параметры по умолчанию для, скажем, текстового поля.

Давайте задумаемся, почему разработчики HTML пошли на такое исключение из общего правила. Наверное, вы слышали о возможностях DHTML (Dynamic HTML — Динамический HTML) и JavaScript. С их помощью можно создавать интерактивные страницы, реагирующие на действия пользователя в реальном времени. Например, можно написать код на JavaScript, который запускается, когда пользователь нажимает какую-нибудь кнопку в форме на странице, или когда он вводит текст в одно из текстовых полей.

Применение DHTML не ограничивается упомянутыми возможностями. В частности, умелый программист, владеющий, к примеру, JavaScript, может создавать страницы, которые будут автоматически формировать и отсылать на сервер формы без ведома пользователя. В принципе, в этом нет никакого "криминала": ведь все данные, которые будут отосланы, сгенерированы этой же страницей.

Что же получится, если разрешить тэгу <input type=file> иметь параметр по умолчанию? Предположим, пользователь хранит все свои пароли в "засекреченном" файле C:\passwords.txt. Тогда "пассворднэппер" может написать на JavaScript и встроить в страницу программу, которая создает и отправляет на "свой" сервер форму незаметно для пользователя. При этом достаточно, чтобы в форме присутствовало единственное поле закачки файла с проставленным параметром value=C:\passwords.txt.

Естественный вывод: в случае, если бы параметр по умолчанию был разрешен для тэга закачки файла, то программист на JavaScript, "заманив" на свою страницу пользователя, мог бы иметь возможность скопировать любой файл с компьютера клиента.

Теперь вы понимаете, почему тэг <input type=file> не допускает использования атрибута value?..

Поддержка закачки в PHP

Так как PHP специально разрабатывался как язык для Web-приложений, то, естественно, он "умеет" работать как с привычными нам, так и с multipart-формами. Более того, он также поддерживает закачку файлов на сервер.

392

Часть V. Приемы программирования на PHP

Простые имена полей закачки

Как я уже говорил, интерпретатору совершенно все равно, в каком формате приходят данные из формы. Он умеет их обрабатывать и "рассовывать" по переменным в любом формате. Однако данные одного специального поля формы — а именно, поля закачки — он интерпретирует особым образом. Пусть у нас есть multipart-форма, а в ней — поле закачки файла:

<form action="script.php" method=POST enctype=multipart/form-data> <input type=file name="MyFile">

<input type=submit> </form>

После выбора в этом поле нужного файла и отправки формы (и загрузки на сервер того файла, который был указан) PHP определит, что нужно принять файл, и сохранит его во временном каталоге на сервере. Кроме того, в программе создадутся несколько переменных.

r$MyFile — имя временного файла на машине сервера, который содержит данные, переданные пользователем. С этим файлом теперь можно вытворять все что угодно: удалять, копировать, переименовывать, ñíîва удалять...

r$MyFile_name — исходное имя файла, которое он имел до своей отправки на сервер.

r$MyFile_size — размер закачанного файла в байтах.

r$MyFile_type — тип загруженного файла, если браузер смог его определить. К

примеру, image/gif, text/html и т. д.

Как видим, префикс у всех созданных переменных один и тот же — MyFile_. Этот префикс состоит из имени элемента закачки в форме, к которому присоединен знак _.

Теперь мы можем, например, скопировать только что полученный файл на новое место, при помощи Copy($MyFile,"uploaded.dat") или других средств, проверив предварительно, не слишком ли он велик, основываясь на значении переменной $MyFile_size.

Настоятельно рекомендую использовать функцию копирования, а не переиме- нования/перемещения. Дело в том, что в некоторых операционных системах временный каталог, в котором PHP хранит только что закачанные файлы, мо- жет находиться на другом носителе, и в результате операция переименования завершится с ошибкой. Хотя мы и оставили копию полученного файла во вре- менном каталоге, можно не заботиться о его удалении в целях экономии мес- та: PHP сделает это автоматически.

Глава 28. Загрузка файлов на сервер

393

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

Пример: фотоальбом

Давайте напишем небольшой сценарий, представляющий собой простейший фотоальбом с возможностью добавления в него новых фотографий.

Листинг 28.1. Сценарий photo.php: простейший фотоальбом

<?

$ImgDir="img"; // Каталог для хранения изображений

@mkdir($ImgDir,666); // Создаем, если его еще нет

// Проверяем, нажата ли кнопка добавления фотографии if(@$doUpload) {

// Проверяем, принят ли файл if(file_exists($File)) {

//Все в порядке — добавляем файл в каталог с фотографиями

//Используем то же имя, что и в системе пользователя

Copy($File,"$ImgDir/".basename($File_name));

}

}

// Теперь считываем в массив наш фотоальбом $d=opendir($ImgDir); // открываем каталог $Photos=array(); // изначально альбом пуст // Перебираем все файлы while(($e=readdir($d))!==false) {

// Это изображение GIF, JPG или PNG? if(!ereg("^(.*)\\.(gif|jpg|png)$",$e,$P)) continue;

//Если нет, переходим к следующему файлу,

//иначе обрабатываем этот

$path="$ImgDir/$e";

// адрес

$sz=GetImageSize($path);

// размер

$tm=filemtime($path);

// время добавления

// Вставляем изображение в массив $Photos

$Photos[$tm] = array(

 

'time'

=>

filemtime($path), //

время добавления

'name'

=>

$e,

//

имя файла

394

 

Часть V. Приемы программирования на PHP

'url'

=> $path,

// его URI

'w'

=> $sz[0],

// ширина картинки

'h'

=> $sz[1],

// ее высота

'wh'

=> $sz[3]

// "width=xxx height=yyy"

);

 

 

}

 

 

//Ключи массива $Photos — время в секундах, когда была добавлена

//та или иная фотография. Сортируем массив: наиболее "свежие"

//фотографии располагаем ближе к его началу.

krsort($Photos);

// Данные для вывода готовы. Дело за малым — оформить страницу. ?>

<body>

<form action=photo.php method=POST enctype=multipart/form-data> <input type=file name=File><br>

<input type=submit name=doUpload value="Закачать новую фотографию"> </form>

<?foreach($Photos as $n=>$Img) {?> <img

src=<?=$Img['url']?> <?=$Img['wh']?>

alt="Добавлена <?=date("d.m.Y H:i:s",$Img['time'])?>"

>

<?}?>

</body>

Конечно, этот сценарий далеко не идеален (например, он не поддерживает удаление фотографий из фотоальбома), но для иллюстрации заявленных возможностей, помоему, вполне подходит. Для простоты я совместил две функции (администрирование альбома и его просмотр) в одной программе. В реальной жизни, конечно, за каждую из них должен отвечать отдельный сценарий (первый из них, наверное, будет требовать от пользователя прохождения авторизации, чтобы добавлять фотографии в альбом могли лишь привилегированные пользователи).

Обратите внимание на то, как этот сценарий оформлен. В самом начале нахо- дится весь код на PHP, который, собственно, и работает с данными фотоаль- бома. В этом коде в принципе нет никаких указаний на то, как должна быть от- форматирована страница. Его задача просто сгенерировать данные. Наоборот, тот текст, который следует после закрывающей скобки ?>, содержит

Глава 28. Загрузка файлов на сервер

395

минимум кода на PHP. Его главная задача оформить страницу так, чтобы она выглядела красиво. У меня нет никаких других стимулов, кроме как эконо- мии типографской краски, чтобы не разнести данные блоки по разным файлам. Мы еще вернемся к такому подходу в одной из следующих глав.

Сложные имена полей

Как вы, наверное, помните, элементы формы могут иметь имена, выглядящие, как элементы массива: A[10], B[1][text] и т. д. До недавнего времени (в третьей версии PHP) это касалось только "обычных" полей, но не полей закачки файлов. К счастью, в PHP версии 4 все изменилось в лучшую сторону.

Давайте применим указанные возможности в следующем примере формы и определим, какие переменные создаст PHP при ее отправке на сервер.

<form action="script.php" method=POST enctype=multipart/form-data> <h3>Выберите тип файлов в вашей системе:</h3>

Текстовый файл: <input type=file name="File[text]"><br> Бинарный файл: <input type=file name="File[bin]"><br> Картинка: <input type=file name="File[pic]"><br> <input type=submit name=Go value="Отправить файлы"> </form>

После того как программа script.php примет данные из формы, PHP создаст для нее следующие переменные:

rассоциативный массив $File, ключи которого — text, bin и pic, а соответствующие значения — имена временных файлов на сервере, созданных PHP при загрузке;

rмассив $File_name все с теми же ключами и значениями — именами файлов в системе пользователя;

rмассив $File_type с теми же ключами и значениями — типами соответствующих файлов;

rмассив $File_size со значениями — размерами этих файлов.

Мы видим, информация об индексах в именах полей формы попала в ключи соответствующих массивов и сохранилась в них. Вы можете убедиться в том, что переменные действительно инициализированы, воспользовавшись вызовом функции Dump($GLOBALS), код которой приведен в конце главы 11, и в полезности которой вы теперь можете убедиться на примере.

Еще раз напоминаю, что PHP версии 3 неправильно работает с подобными именами полей. Учитывайте это, если собираетесь использовать старый ин- терпретатор.

396

Часть V. Приемы программирования на PHP

Проблемы со сложными именами

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

Итак, напишем форму:

<form action="script.php" method=POST enctype=multipart/form-data> <input type=file name="File[a][b]">

<input type=submit> </form>

При приеме данных такой формы PHP "запутается" и, хотя и создаст массив $File, но не поместит в него никаких полезных данных. А именно, в моей версии 4.0.3pl1 в элемент с ключом a вместо имени файла попадает какое-то комплексное (судя по его странному виду) число.) Надеюсь, в будущих версиях интерпретатора это досадное недоразумение будет исправлено.

Но все же существует метод, с помощью которого мы сможем обработать и такие "неправильные" с точки зрения PHP формы. Я об этом еще не упоминал, но PHP, помимо установки вышеперечисленных переменных, создает также глобальный массив с именем $HTTP_POST_FILES. Как показывает практика, в этом массиве содержатся верные данные, какое бы имя не имело поле закачки в форме.

Массив $HTTP_POST_FILES создается не всегда, а только в том случае, если в настройках PHP задействован параметр track_vars. Так как, судя по доку- ментации, в PHP версии 4 он включен всегда (чего нельзя сказать о третьей версии), то беспокоиться не о чем.

Массив $HTTP_POST_FILES используется довольно редко, так что я предоставляю читателю возможность самостоятельно разобраться, в каком формате хранятся в нем данные. Это несложно. Вам не потребуется ничего, кроме функции Dump(), которая уже упоминалась в этой главе, и, конечно, желания экспериментировать.

Глава 29

Модульность программы. Написание "библиотекаря"

Во всех серьезных языках программирования имеется возможность писать модульные программы. Иными словами, при определенных навыках вы можете разбить свою программу на относительно независимые части, каждую из которых реализовать в виде модуля. Особенно это бывает полезно, если над программой работает сразу коллектив разработчиков (как чаще всего и бывает) — в этом случае остается лишь продумать связи между модулями, написание которых можно поручить разным программистам.

Модули обычно также используют другие модули в своей работе, те — третьи, и т. д., до самого низкого уровня. Хорошо написанный модуль подобен новому автомобилю: его интерфейсные функции — это аналог руля и педаль, а уж что там под капотом — программиста, подключающего модуль, волновать не должно.

Тем не менее, должен вас огорчить: к сожалению, разработчики PHP не предусмотрели в языке сколько-нибудь удобной поддержки модульности. Однако не впадайте в уныние: дело в том, что такую поддержку можно в язык добавить, причем относительно несложными приемами самого PHP и сравнительно небольшими затратами с точки зрения быстродействия. Этим мы и займемся в настоящей главе.

Наши требования

Возможно, вы возразите: "Как же нет никакой поддержки модульности? А инструкция include?" Да, разумеется, уж лучше использовать include, вместо того чтобы хранить всю программу в одном-единственном файле. Но дело в том, что применение этой инструкции довольно-таки неудобно по той простой причине, что поиск подключаемых файлов проводится только в тех каталогах, которые указал администратор при установке PHP. У многих хостинг-провайдеров мы не можем изменять по своему усмотрению эти каталоги, а указание относительных путей (например,

../../php/somefile.php) оказывается довольно проблематичным (представьте только, сколько всего нам придется изменять, если мы захотим расположить нашу программу в другом месте).

398

 

Часть V. Приемы программирования на PHP

 

 

 

 

 

 

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

Помните, что при помощи include или require нельзя один и тот же файл загружать дважды (как это часто бывает, если один модуль вызывает другой, но программа об этом "не знает" и еще раз подключает первый — опять же, стандартный случай). В самом деле, если в этом файле находится, к примеру, описание какой-нибудь функции, то при следующем его включении PHP выдаст ошибку: повторное объявление функции. Конечно, последняя проблема полностью решается подстановкой include_once вместо include, что работает, кстати, только в PHP версии 4.

Отсюда мы можем сформулировать главные два требования.

rМеханизм загрузки модуля должен сам решать, в каком каталоге располагается модуль, независимо от того, где выполняется сценарий. В любой программе возможность загрузить указанный по имени модуль должна быть легко осуществима. Мы хотели бы, чтобы это было так же просто, как мы делаем это с обычными файлами из текущего каталога при помощи include.

rОдин и тот же модуль не должен загружаться дважды, даже если программа попытается это выполнить.

К слову сказать, оба требования реализованы, например, в языке Perl.

Как я уже говорил, мы можем написать нужную нам "инструкцию", которая будет загружать модуль с применением указанных принципов прямо на PHP. Назовем ее Uses() и оформим в виде функции.

Далее для краткости модулем на PHP я буду называть файл (например, с рас- ширением phl), содержащий некоторые общеупотребительные функции, кон- станты и переменные, а также исполняемую часть, которая запускается при первой (и только первой) загрузке модуля.

Библиотекарь

Ту часть кода, которая будет содержать функцию Uses() (а мы реализуем ее именно в виде функции) и другие функции, нужные для загрузки модулей, назовем библио- текарем. Этот библиотекарь, очевидно, сценарию придется загружать первым, а каким именно образом, мы поговорим чуть позже.

Глава 29. Модульность программы. Написание "библиотекаря"

399

Теперь немного о том, как мы будем реализовывать Uses(). Это довольно несложно. Помните, я подчеркивал, что поскольку PHP является интерпретатором, то на нем осуществимы такие приемы, как описание функций внутри функций и многое другое. Так мы и сделаем: функция Uses() вначале будет проверять, не загружался ли уже модуль с таким именем, затем искать затребованный модуль в специальных "каталогах для модулей", фиксировать во внутреннем массиве факт, что указанный файл загружен, и, наконец, вызывать include_once для файла с модулем. Кроме того, на время загрузки текущий каталог будет сменяться на тот, в котором находится модуль, чтобы стартовые части всех модулей запускались в "своих" каталогах. Это как раз та возможность, которая отсутствует в Perl, и которая оказывается довольно удобной на практике.

Раз библиотекарь всегда подключается к программе в первую очередь, разумно доверить ему выполнение еще некоторых действий.

rПоместим в файл библиотекаря функции, чаще всего необходимые почти каждому сценарию. Таким образом, мы как бы "расширим" набор встроенных в PHP функций. Однако помните, что встроенные функции переопределять все же нельзя, можно лишь создавать новые с уникальными именами.

rБиблиотекарь, как никто другой, должен приложить максимум усилий, чтобы сделать сценарии переносимыми с одной платформы на другую. Для нас это будет заключаться в корректировке некоторых переменных, которые PHP создает перед выполнением программы. Первым кандидатом на такую правку будет $SCRIPT_NAME (а также одноименная переменная окружения), которая, как мы знаем, в Windows-версии PHP содержит не то значение, которое мы ожидаем.

rИ еще нам хочется, чтобы на момент загрузки модуля текущий каталог сменялся на тот, в котором расположен файл модуля. Таким образом, стартовая часть библиотеки всегда сможет определить, где она находится, — например, при помощи вызова getcwd().

Вот что у нас получится в результате:

Листинг 29.1. Библиотекарь: librarian.phl

<?if(!defined("LIBRARIAN_LOADED")) { define("LIBRARIAN_LOADED",1);

//Расширение библиотечных файлов по умолчанию define("LibExt","phl");

//Пути поиска библиотек. Если начинаются с точки, то поиск

//ВСЕГДА ведется относительно текущего каталога, даже если его

//сменят, в противном случае при следующем вызова Uses() будет

//выполнен перевод пути в абсолютный.

$INC=array(".","./lib");

400

Часть V. Приемы программирования на PHP

//Функция преобразует указанный относительный путь в абсолютный.

//Если путь уже является абсолютным (т. е. отсчитывается от корневого

//каталога системы), то с ним ничего не происходит, в противном случае

//используется имя текущего каталога (или заданного в $cur) с

//необходимыми преобразованиями. Существование файла с полученным полным

//именем не проверяется. Функция лишена некоторых недостатков

//встроенной в PHP realpath() и имеет по сравнению с ней несколько

//большие возможности, работая, правда, чуть медленнее.

function GetAbsPath($name,$cur="") { return abs_path($name,$cur); } function abs_path($name,$cur="")

{// Преобразуем обратные слэши в прямые $name=strtr(trim($name),"\\","/");

//Сначала разбиваем путь по знакам "/" $Parts=explode("/",$name);

$Path=($cur===""?getcwd():$cur); // начальный каталог поиска foreach($Parts as $i=>$s) if($s!=".") {

//Признак корневого каталога?

if(!$i && (strlen($s)>1&&$s[1]==":"||$s=="")) $Path=$s; // Ссылка на родительский каталог?

elseif($s=="..") {

//Если это уже корневой каталог, то куда спускаться?.. if(strlen($Path)>1 && $Path[1]==":") continue;

//Иначе используем dirname()

$p=dirname($Path);

if($p=="/"||$p=="\\"||$p==".") $Path=""; else $Path=$p;

}

// Иначе просто имя очередного каталога elseif($s!=="") $Path.="/$s";

}

return ($Path!==""?$Path:"/");

}

//Преобразует URL в абсолютный файловый путь.

//Т. е. если адрес начинается со слэша, то результат рассматривается

//по отношению к каталогу DOCUMENT_ROOT, а если нет — то относительно

//dirname($SCRIPT_NAME). Конечно, функция не безупречна (например, она

//не умеет обрабатывать URL, заданные Alias-директивами Apache, но в

//большинстве случаев это и не нужно.