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

Котеров Д. В., Костарев А. Ф. - PHP 5. 2-е издание (В подлиннике) - 2008

.pdf
Скачиваний:
6286
Добавлен:
29.02.2016
Размер:
11.36 Mб
Скачать

954

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

ния, то производится перемещение файла, иначе — копирование. Перемещение (или переименование) обычно работает быстрее копирования.

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

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

Листинг 43.2. Файл album.php

<?php ## Простейший фотоальбом с возможностью закачки. $imgDir = "img"; // каталог для хранения изображений @mkdir($imgDir, 0777); // создаем, если его еще нет

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

$data = $_FILES['file']; $tmp = $data['tmp_name'];

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

$info = @getimagesize($_FILES['file']['tmp_name']); // Проверяем, является ли файл изображением.

if (preg_match('{image/(.*)}is', $info['mime'], $p)) {

//Имя берем равным текущему времени в секундах, а

//расширение — как часть MIME-типа после "image/". $name = "$imgDir/".time().".".$p[1];

//Добавляем файл в каталог с фотографиями.

move_uploaded_file($tmp, $name);

}else {

echo "<h2>Попытка добавить файл недопустимого формата!</h2>";

}

}else {

echo "<h2>Ошибка закачки #{$data['error']}!</h2>";

}

}

// Теперь считываем в массив наш фотоальбом. $photos = array();

foreach (glob("$imgDir/*") as $path) {

$sz = getimagesize($path); //

размер

$tm = filemtime($path);

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

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

$photos[$tm] = array(

 

 

'time' => $tm,

 

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

'name' => basename($path),

// имя файла

'url'

=> $path,

 

// его URI

'w'

=> $sz[0],

 

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

'h'

=> $sz[1],

 

// ее высота

'wh'

=> $sz[3]

 

// "width=xxx height=yyy"

);

 

 

 

}

 

 

 

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

955

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

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

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

krsort($photos);

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

<body>

<form action="<?=$_SERVER['SCRIPT_NAME']?>" method="POST" enctype= "multipart/form-data">

<input type="file" name="file"><br>

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

</form>

<?foreach($photos as $n=>$img) {?> <p><img

src="<?=$img['url']?>" <?=$img['wh']?>

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

>

<?}?>

</body>

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

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

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

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

Листинг 43.3. Файл complex.php

<?php ## PHP обрабатывает и сложные имена полей закачки.

if (@$_REQUEST['doUpload'])

956

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

echo '<pre>Содержимое $_FILES: '.print_r($_FILES, true)."</pre><hr>"; ?>

<form action="<?=$_SERVER['SCRIPT_NAME']?>" method="POST" enctype= "multipart/form-data">

<h3>Выберите тип файлов в Вашей системе:</h3>

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

В листинге 43.3 приведен скрипт с формой, имеющей два элемента закачки с именами input[a][text] и input[a][bin]. Приведем результат вывода в браузер пользователя, который получается сразу же после выбора и закачки двух файлов:

Содержимое $_FILES: Array( [input] => Array(

[name] => Array( [a] => Array(

[text] => button.gif [bin] => button.php

)

)

[type] => Array( [a] => Array(

[text] => image/gif [bin] => text/plain

)

)

[tmp_name] => Array( [a] => Array(

[text] => C:\WINDOWS\php1BB.tmp [bin] => C:\WINDOWS\php1BC.tmp

)

)

[error] => Array( [a] => Array( [text] => 0 [bin] => 0

)

)

[size] => Array( [a] => Array(

[text] => 242 [bin] => 834

)

)

)

)

Как видите, данные для элемента формы вида input[a][text] превращаются в элементы массива $_FILES[input][*][a][text], где * — это один из "стандартных" клю-

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

957

чей (name, tmp_name, size и т. д.). То есть, исходное название поля формы как бы "расщепляется": сразу же после первой части имени поля (input) вставляется "служебный" ключ (выше мы его обозначали звездочкой), а затем уже идут остальные "измерения" массива.

Почему разработчики PHP пошли по такому пути, а не сделали поддержку имен вида $_FILES[input][a][text][*] — наиболее логичного? Неизвестно.

Резюме

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

ГЛАВА 4 4

Использование перенаправлений

Листинги данной главы можно найти в подкаталоге redirect.

Вэтой главе мы поговорим о переадресации. Данное слово, как и множество других компьютерных терминов, необходимо понимать "объемно", а не "поверхностно". То есть, когда речь заходит о переадресации, не нужно представлять себе почтальона с ноутбуком на багажнике велосипеда (даже и не пробуйте — растрясете винчестер) — лучше вспомнить об английском слове redirect, что на русский язык и переводится как переадресация, или, как часто говорят, просто "редирект". На наш взгляд, слово "редирект" все-таки удобнее, потому что оно не навевает побочных ассоциаций, так что в дальнейшем мы будем нередко использовать именно его.

Вчем же смысл редиректа?.. Он прост. Редирект — ситуация, в которой запускается некоторый скрипт, но страница, которую увидит в итоге пользователь, оказывается сгенерирована не этим, а другим сценарием.

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

Переадресация — в некотором роде передача полномочий, когда текущая программа "на смертном одре" объявляет: "Все, не могу больше! Пусть страницей займется ктонибудь еще". Затем она передает управление другому сценарию, а сама тихо и незаметно "уходит в иной мир".

Внешний редирект

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

Глава 44. Использование перенаправлений

959

Самый простой и универсальный способ выполнить внешний редирект — послать браузеру тег <meta>, а затем немедленно завершить работу. Вот как это делается на PHP:

#0 означает, что переадресация произойдет

#через 0 секунд, т. е. немедленно.

echo '

<meta http-equiv="Refresh"

content="0; URL=/some/other/script.html">

';

exit();

Внешняя переадресация еще иногда называется "перещелкиванием" из-за характерного звука, издаваемого браузером при приеме описанного выше тега <meta>. Второй способ редиректа — послать специальный заголовок Location, но уже не через тег

<meta>:

Header("Location: http://{$_SERVER['SERVER_NAME']}/other/script.html");

exit();

Обратите внимание на то, что во втором случае мы использовали полный URL (сформированный динамически), а не один лишь URI, как в предыдущем примере. Сейчас будет ясно, зачем так сделано.

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

Внутренний редирект

Кроме неприятного щелчка, внешний редирект имеет еще один недостаток: задержку из-за необходимости по модему передавать данные запроса-ответа. Иными словами, он не проходит незамеченным для браузера. Это означает, что, получив команду на внешний редирект, браузер полностью "забывает" о предыдущем скрипте и всецело отдает себя служению новому запросу. Новый адрес даже появляется в адресной строке окна. Хорошо это или плохо, мы выясним чуть позже, а пока посмотрим, как внутренний редирект решает вопрос (вернее, пытается решить).

Внутренняя переадресация работает только в CGI-версии PHP. Если же PHP установлен в виде модуля Apache, вы можете выполнять лишь внешние редиректы.

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

960

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

При работе с Apache существует лишь один способ выполнить внутренний редирект: указать в заголовке Location не абсолютный URL (с префиксом http:// и именем хоста), а абсолютный URI (т. е. URL без имени хоста). Отсюда автоматически следует, что внутренний редирект, в отличие от внешнего, может происходить только в пределах одного сайта (листинг 44.1).

Листинг 44.1. Файл internal.php

<?php ## Внутренний редирект (только в CGI-версии PHP!)

//Вначале форсируем внутренний редирект. Header("Status: 200 OK");

//Получаем URI-каталог текущего скрипта. $dir = dirname($_SERVER['SCRIPT_NAME']);

//Осуществляем переадресацию по абсолютному (!) URI. Header("Location: $dir/result.php");

exit();

?>

Когда сервер получает от скрипта страницу и собирается отправить ее браузеру, он прежде всего проверяет: нет ли в ней заголовка Location с указанием URI документа. Если есть, то сервер порождает новый процесс — копию самого себя — и велит ей выполнить новый запрос, а о старом "забывает". Повторимся: все это происходит без участия браузера, который видит лишь конечную страницу с тем же самым URL, который был у самого первого скрипта.

Обратите внимание, что мы используем абсолютный URI при формировании заголовка Location. Дело в том, что при попытке указать относительный URI Apache сгенерирует отдельную страницу, на которой будет написано примерно следующее: "Document is moved here", где here — активная ссылка на новое расположение страницы.

Давайте рассмотрим файл, на который осуществляется переадресация в примере выше (листинг 44.2).

Листинг 44.2. Файл result.php

Это текст файла <?=__FILE__?>.

Как видим, он просто выводит свое имя. Теперь, введя в браузере путь к сценарию internal.php, мы сможем убедиться, что в действительности страницу сгенерирует скрипт result.php. При этом адресная строка в браузере по-прежнему будет содержать путь к internal.php: браузер "ничего не заметил".

Давайте теперь рассмотрим недостаток внутреннего редиректа. Он всего один: неведение браузера относительно тех процессов, которые в действительности происходят на сервере. Пусть, к примеру, скрипт, выполняющий переадресацию, располагается по адресу /forum/doit.php. Предположим, при его запуске было обнаружено, что пользователь еще не зарегистрировался, а значит, его нужно перенаправить на страницу регистрации /register/new.html. На этой странице присутствуют ссылки на изо-

Глава 44. Использование перенаправлений

961

бражения: <img src=login.gif>, причем считается, что картинка расположена в том же каталоге, что и сама страница (указан относительный путь).

Что же получается? Если неавторизованный пользователь запустит скрипт, произойдет внутренняя переадресация на страницу регистрации, однако URL в адресной строке останется прежним — /forum/doit.php. При этом в окне браузера будет отображаться страница регистрации, но браузер-то об этом не знает! А значит, он будет считать, что текущий каталог на сайте — /forum/, а не /register/, и, конечно же, не сможет правильно отобразить картинку.

Мы получаем, что одна и та же страница может либо "работать, как ей и положено", либо "не работать" — ужасный симптом при отладке!

Все это особенно неудобно, если применяется "дедовское" CGI-программирование с использованием каталога /cgi-bin/ для хранения скриптов (если вы помните, из части II следует, что на PHP также можно писать и CGI-скрипты). В этом случае текущим каталогом всегда будет /cgi-bin/, но ведь из него запрещено читать картинки и все остальное — можно только запускать скрипты! Решение проблемы — всегда использовать для изображений абсолютный путь (например, /register/login.gif). Так часто и делается, и иногда это действительно бывает оправданно, но чаще всего — нет. Почему? А вы только представьте, как будет кто-то мучиться, если решит переименовать каталог register во что-то еще: нужно будет пройтись по всем файлам и везде поменять абсолютный путь...

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

../../../somewhere. И то, и другое приводит к лишним зависимостям, которые весьма утомительно исправлять. Как показывает практика, использование ".." даже хуже, чем абсолютные пути. Зато вполне допустима конструкция img/login.gif — относительный путь в подкаталог.

Итак, внутренний редирект имеет принципиальные уязвимости. Хорошей альтернативой будет лишь внешняя переадресация, хотя она и связана с задержками и (иногда) с "перещелкиванием".

Самопереадресация

Про внутренний и внешний редиректы сказано достаточно, перейдем теперь к так называемому self-редиректу (самопереадресации), когда страница заставляет браузер перейти... на саму себя. Звучит довольно загадочно: зачем это вообще может понадобиться?.. Отвечаем: из-за особенности протокола HTTP.

Проще всего опять рассмотреть пример. Пусть у нас есть небольшая гостевая книга bad.php, работающая следующим образом: при вызове скрипта без параметров (набора его адреса в браузере) отображается форма с предложением ввести новое сообщение. Затем идут уже существующие комментарии книги. При нажатии кнопки текст передается тому же самому скрипту методом POST, при этом комментарий добавляется в книгу и тут же отображается вместе с остальными записями (листинг 44.3).

962

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

Листинг 44.3. Файл bad.php

<?php ## "Плохая" реализация гостевой книги. $FNAME = "book.txt";

if (@$_REQUEST['doAdd']) { $f = fopen($FNAME, "a");

if (@$_REQUEST['text']) fputs($f, $_REQUEST['text']."\n"); fclose($f);

}

$gb = @file($FNAME);

if (!$gb) $gb = array(); ?>

<form action="<?=$_SERVER['SCRIPT_NAME']?>" method="POST"> Текст:<br>

<textarea name="text"></textarea><br>

<input type="submit" name="doAdd" value="Добавить"> </form>

<?foreach($gb as $text) {?> <?=$text?><br><hr>

<?}?>

Метод передачи данных "самому себе", проиллюстрированный в листинге 44.3, прекрасно себя зарекомендовал в Web-программировании. Он имеет множество достоинств, например, отсутствие "некорректных" ссылок и ясность для пользователя. Для сокращения письма мы не используем в приведенном выше примере отдельный файл для шаблона вывода гостевой книги, а просто "прикрепляем" его в конец скрипта.

Пусть пользователь набрал свое послание и отправил его на сервер. Перед ним появится список сообщений, первым из которых будет его собственное. Пока вроде бы все верно. И теперь пользователь, ничего не подозревая, нажимает кнопку Обновить в браузере, заставляя последний, как он думает, перезагрузить страницу гостевой книги...

Но в действительности происходит совсем не то, что он ожидает! Если данные формы были посланы методом POST, браузер выведет на экран диалоговое окно запроса примерно такого содержания: "Вы пытаетесь обновить данные страницы, которая была сгенерирована с применением метода POST. Повторить отправку данных (да или нет)?" Если пользователь нажмет кнопку Нет, то гостевая книга не перезагрузится, а появится совершенно бесполезная стандартная страница с сообщением о том, что "данные устарели". Если же он подтвердит вторичную отправку данных, его сообщение будет добавлено в книгу еще раз, а потому "размножится". Довольно просто понять, почему так происходит: ведь браузер "не знает", что в действительности пользователь хочет лишь вторично "зайти" на адрес страницы книги, а не повторить отправку всех данных формы.

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

Глава 44. Использование перенаправлений

963

GET — не что иное, как простое изменение URL страницы, а именно добавление в его конец символа ?, после которого следуют параметры (в том числе текст записи).

Впрочем, метод GET практически никогда не применяется в интерактивных сценариях, таких как гостевые книги, форумы и т. д. Мы уже обсуждали в части I книги эту тему, но она настолько важна, что повторимся. Если для одних и тех же данных формы при их многократной отправке страница всегда выглядит одинаково, значит, эти данные логично передавать методом GET. В противном случае необходимо применять метод POST.

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

Решение лишь одно: после добавления сообщения в книгу выслать браузеру запрос на внешний редирект — конечно, к тому же самому скрипту. Перенаправление всегда осуществляется методом GET, а значит, кнопка Обновить будет работать так, как ей и положено. Все это и есть самопереадресация.

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

Обратите внимание, что самопереадресация не бывает внутренней — она может быть только внешней, иначе этот термин вообще теряет смысл.

Вот как будет выглядеть корректно работающий скрипт (листинг 44.4).

Листинг 44.4. Файл good.php

<?php ## Использование самопереадресации. $FNAME = "book.txt";

if (@$_REQUEST['doAdd']) { $f = fopen($FNAME, "a");

if (@$_REQUEST['text']) fputs($f, $_REQUEST['text']."\n"); fclose($f);

$rnd = time(); # ВНИМАНИЕ!

Header("Location: http://{$_SERVER['SERVER_NAME']}{$_SERVER['SCRIPT_NAME']}?$rnd");

exit();

}

$gb = @file($FNAME);

if (!$gb) $gb = array(); ?>

<form action="<?=$_SERVER['SCRIPT_NAME']?>" method="POST"> Текст:<br>

<textarea name="text"></textarea><br>