Самоучитель PHP 4 - Котеров Д. В.
.pdf
Глава 28
Загрузка файлов на сервер
Иногда бывает просто необходимо позволить пользователю не только заполнить текстовые поля формы и установить соответствующие флажки и радиокнопки, но также и указать несколько файлов, которые будут впоследствии загружены с компьютера пользователя на сервер. Для этого в языке HTML и протоколе HTTP предусмотрены специальные средства.
Чтобы не применять двусмысленной терминологии, я буду использовать слово "закачать" для обозначения загрузки файла клиента на сервер, и термин "ска- чать" для иллюстрации обратного процесса (с сервера — клиенту). Я уже предчувствую, как будет недоволен, услышав об этом, редактор книги, и он в чем-то прав. Так что, уважаемый читатель, если вы читаете здесь эти рассуж- дения, — значит, я победил в споре, а если не читаете… Что ж, вы об этом и не догадаетесь.1
Мы уже рассматривали механизм, который применяется при закачке файлов, в главе 3. Вы, возможно, помните, что он выглядел не очень-то привлекательно. На мой взгляд, закачка файлов и вообще работа с multipart-методом передачи формы — до-
1 Русский язык, изначально обладающий гигантской свободой в выборе слова, постоянно развивается. То, что казалось неприемлемым вчера, сегодня становится нормой, и наоборот. Безусловно, у любого обратившего внимание на эти строки при прочтении слов "закачать" и "скачать" вряд ли возникнут ассоциации с бригадой мускулистых администраторов, придающих передаваемым по сети файлам необходимую кинетическую энергию для последующего перемещения под напором, или другие неверные мысли, несмотря на большое количество смысловых оттенков употребления этих слов (убаюкивать, вызвать головокружение, или же в понимании "подлого приема садовников, торговцев присадками (раскачивать деревце, не давая ему укорениться)"). Вообще говоря, о твердых правилах в условиях возрастающего слияния разговорных терминов и литературного языка говорить не приходится. В толковом словаре С. И. Ожегова и Н. Ю. Шведовой дано пояснение идиоме "Закачаешься!" как выражения высокой оценки чего-либо. Редакторы вовсе не стремятся убивать живое изложение, и поэтому если вы, уважаемые читатели, также видите эти строки, значит было решено — "быть закачиваемому" (из примеров к статье "Закачать" толкового словаря живого великорусского языка В. И. Даля). — Ред.
390 |
Часть V. Приемы программирования на PHP |
вольно нетривиальные задачи. Однако спешу обрадовать: в PHP все это давно реализовано и отлажено. Но обо всем по порядку.
Multipart-формы
Мы помним, что в большинстве случаев данные из формы в браузере, передающиеся методом GET или POST, приходят к нам в одинаковом формате:
поле1=значение1&поле2=значение2&...
При этом все символы, отличные от "английских" букв и цифр (и еще некоторых) URL-кодируются: заменяются на %XX, где XX — шестнадцатеричный код символа. Это сильно замедляет закачку больших файлов.
В принципе, multipart-формы призваны одним махом решить эту проблему. Нам нужно в соответствующем тэге <form> задать параметр:
enctype=multipart/form-data
После этого данные, полученные от нашей формы, будут разбиты на несколько блоков информации (по одному на каждый элемент формы). Каждый такой блок очень похож на обычную посылку "заголовки-данные" протокола HTTP:
-----------------Идентификатор_начала\n
Content-Disposition: form-data; name="имя" [;другие параметры]\n \n
значение\n
Браузер автоматически формирует строку Идентификатор_начала из расчета, чтобы она не встречалась ни в одном из передаваемых файлов (и ни в одном из других полей формы). Это означает, что сегодня идентификатор будет одним, а завтра, возможно, совсем другим.
Тэг выбора файла
Давайте просмотрим, какой тэг надо вставить в форму, чтобы в ней появился элемент управления загрузкой файла — текстовое поле с кнопкой Browse справа. Таким тэгом является разновидность <input>:
<input type=file name=имя_элемента [size=размер_поля]>
Сценарию вместе с содержимым файла передается и некоторая другая информация, а именно:
rразмер файла;
rимя файла в системе клиента;
rтип файла.
Глава 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) оказывается довольно проблематичным (представьте только, сколько всего нам придется изменять, если мы захотим расположить нашу программу в другом месте).
