Вобъемном проекте часто трудно проконтролировать все возможные ситуации, и рано или поздно отображение информации в окно браузера осуществляется перед выполнением функции header(), что приводит к выводу предупреждения: Warning: Cannot modify header information - headers already sent by. Такая ситуация может возникнуть например, при наличии случайного пробела или какой-либо ошибки, которая ни разу не возникала в процессе тестирования. Не стоит и говорить, что вывод подобного предупреждения искажает дизайн сайта и выглядит непрофессионально. Конечно можно подавить вывод предупреждения, предварив функцию header() символом @ или отключить вывод сообщений в окно браузера при помощи функции error_reporting(0), однако это лишь скроет предупреждение и не заставит сработать функцию header().
Выходом из ситуации является буферизация вывода, при котором весь объем данных, выводимых в окно браузера, помещается в буфер и отправляется клиенту тогда, когда скрипт завершит работу. Это позволяет безболезненно выполнять все функции header() в теле скрипта, где бы они не находились.
Самый простой способ установить буферизацию вывода — воспользоваться директивой output_buffering конфигурационного файла php.ini, установив ее значение равной on.
Включение буферизации при помощи директивы output_buffering
. . .
output_buffering = On
. . .
Однако настройки конфигурационного файла php.ini могут отличаться на разных серверах, поэтому при переносе приложения не стоит расчитывать на то, что директива output_buffering окажется включенной (тем более, что по умолчанию она принимает значение Off — отключена).
К счастью РНР предоставляет альтернативный способ управления буферизацией вывода. Для этого предназначены функции группы управления выводом, список которых представлен в таблице.
Функции управления выводом
Функция | Описание |
ob_clean() |
Очищает буфер вывода |
ob_end_clean() |
Очищает буфер вывода и отключает буферизацию вывода |
ob_end_flush() |
Отправляет буфер вывода клиенту и отключает буферизацию вывода |
ob_flush() |
Отправляет буфер вывода клиенту |
ob_get_clean() |
Возвращает текущее содержание буфера и удаляет текущий буфер |
ob_get_contents() |
Возвращает содержимое буфера вывода |
ob_get_flush() |
Очищает буфер, возвращая его содержимое в виде строки, и отключает буферизацию |
ob_get_length() |
Возвращает размер буфера вывода |
ob_get_level() |
Возвращает уровень вложения буфера |
ob_get_status([$full_status]) |
Возвращает статус буфера вывода в виде массива; если необязательный параметр $full_status принимает значение true, возвращается более подробная информация. |
ob_gzhandler($buffer, $mode) |
Функция обратного вызова, которая используется функцией ob_start() для задания обработчика сжатия данных |
ob_implicit_flush([$flag]) |
Включает (если значение параметра $flag устанавливается в 1) или выключает (если значение $flag устанавливается в 0) неявную очистку буфера |
ob_list_handlers() |
Возвращает список всех используемых обработчиков буферизации |
ob_start([$output_callback [, $chunk_size [, $erase]]]) |
Включает буферизацию вывода; в качестве параметра $output_callback может принимать название функции обратного вызова, которую задает обработчик. Необязательный параметр $chunk_size задает количество байт буфера, через которые будет осуществляться повторный вызов функции $output_callback. Если необязательный параметр $erase принимает значение false, то буфер не будет удаляться после завершения работы скрипта |
output_add_rewrite_var ($name, $value) |
Добавляет к URL, обнаруженным в буфере вывода, дополнительный GET-параметр с именем $name и значением $value |
output_reset_rewrite_vars() |
Уничтожает дополнительные GET-параметры, добавленные функцией output_add_rewrite_var() |
Рассмотрим пример буферизации вывода при помощи функций из таблицы.
Буферизация вывода
<?php
//Весь вывод направляем в буфер
ob_start();
//Выводим содержимое страницы
echo "Hello world!";
//Отправляем HTTP-заголовок
header("X-my-header: Hello world!");
//Отправляем содержимое буфера вывода клиенту
ob_end_flush();
?>
В окне веб-браузера это будет выглядеть ТАК.
При загрузке объемного файла сервер обычно отправляет размер документа в байтах при помощи HTTP-заголовка Content-length. Это позволяет оценить объем загружаемого документа (файла), а также время, необходимое для его загрузки. Следует помнить, сто веб-сервер способен вычислить только размер статического файла, например, изображения HTML-страницы или zip-архива. Для динамических файлов, таких как РНР-скрипт, отправка размера документа при помощи HTTP-заголовка Content-length ложится на плечи разработчика. Однако часто определить размер документа можно только после того, как скрипт завершит свою работу и клиенту отправлена значительная часть HTTP-документа. В такой ситуации удобно воспользоваться функциями управления выводом.
Рассмотрим пример, где скрипт, формирующий объемный текстовой файл, размер которого вычисляется при помощи функции ob_get_length() и отправляется клиенту при помощи HTTP-заголовка Content-length.
Формирование HTTP-заголовка Content-length
<?php
//весь вывод отправляем в буфер
ob_start();
//выводи содержимое страницы
for($i=0; $i<300000; $i++) echo "<pre>01\r\n";
//отправляем клиенту размер страницы
//в HTTP-заголовке Content-length
header("Content-length: ".ob_get_length());
//отправляем содержимое буфера клиенту
ob_end_flush();
?>
При загрузке скрипта веб-сервер и браузер интерпретируют его содержимое как HTML-документ, который выводится в окно браузера. Поскольку HTTP-заголовок Content-type, устанавливающий тип документа отсутствует, сервер назначает ему значение text/html, и браузер интерпретирует такой документ по умолчанию как HTML-страницу. Однако такая интерпретация не всегда удобна: для объемных файлов было бы разумнее предложить пользователю сохранить его на жесткий диск, а для этого необходимо изменить файл.
Изменение типа файла
<?php
//весь вывод отправляем в буфер
ob_start();
//выводи содержимое страницы
for($i=0; $i<300000; $i++) echo "01\r\n";
//задаем имя, которое будет предложено клиенту
//для сохранения файла
header("Content-Disposition: attachment; filename=text.txt");
//в качестве типа файла задаем бинарный поток
header("Contenr-type: application/octet-stream");
//от правляем клиенту размер страницы
//в HTTP-заголовке Content-length
header("Content-length: ".ob_get_length());
//отправляем содержимое буфера клиенту
ob_end_flush();
?>
В окне веб-браузера это будет выглядеть ТАК.
В качестве типа документа в HTTP-заголовке Contenr-type указывается значение application/octet-stream, соответствующее бинарному потоку. Заголовок Content-Disposition позволяет задать имя файла, в данном случае text.txt. В результате загрузки страницы со скриптом на экран посетителя выводится окно.
При этом если будет случайно или умышленно пропущен HTTP-заголовок, то размер загружаемого файла не отразится.
Механизм кэширования применяется с целью оптимизации пересылки данных между клиентом и сервером. Запрошенный пользователем по HTTP-протоколу документ может быть сохранен в кэше промежуточного сервера или браузера, и при повторном запросе будет выдаваться без обращения к источнику.
Кэши принято подразделять на два вида: локальные и глобальные. Локальный кэш создается браузером клиента, тогда как глобальный располагается на прокси-сервере провайдера (или организации, в которой имеется свой внутренний прокси-сервер).
Примечание. Прокси-сервером, в отличие от обычного сервера, предоставляющего доступ какому-либо ресурсу, называют сервер-посредник, расположенный между клиентом и обычным сервером. В отличие от шлюз-сервера, осуществляющего обычное транслирование запросов клиента и ответов сервера, в задачи пркси-сервера входит предоставление дополнительных услуг, таких как преобразование медиатипа, анонимная фильтрация, сжатие протокола, кэширование и др.
В большинстве случаев кэширование позволяет ускорить работу с Интернетом и значительно снизить трафик, но иногда кэширование может мешать работе веб-приложений. Если посетители часто загружают страницы веб-сайта с редко изменяющейся информацией, такой как рассписание пригородного транспорта, кэширование полезно, поскольку экономит трафик и сокращает время выполнения запроса. Если же посетитель загружает динамические страницы, например, активного форума, кэширование, без сомнения, вредно, поскольку содержимое таких страниц меняется слишком часто, и посетитель вынужден будет постоянно в ручную обновлять содержимое страницы.
Для подавления кэширования можно использовать HTTP-заголовки.
Примечание. К сожалению, в последнее время кэширующие прокси-серверы настраиваются крайне агрессивно. Дело в том, что динамические страницы, созданные например, при помощи РНР, вообще не должны подвергаться кэшировнию. Однако сплошь ирядом наблюдается обратное. Более того, ряд кэширующих серверов игнорируют HTTP-заголовки, подавляющие кэширование. В этом случае разработчику ничего не остается делать, как добавлять к URL GET-параметр со случайно сгенерируемым значением.
Подавление кэширования
<?php
//любая дата в прошлом
header ("Expires: Mon, 23 May 2008 02:00:00 GMT");
header ("Last-Modified: " .gmdate(D, d M Y H:i:s"). "GMT";
header ("Cache-Control: no-cache, must-revalidate");
header ("Pragma: no-cache");
. . .
?>
HTTP-заголовок Expires задает дату, по достижении которой документ считается устаревшим, поэтому задание для этого загаловка уже прошедшей даты предотвращает кэширование данной страницы.
HTTP-заголовок Last-Modified определяет дату последнего изменения веб-страницы. Если с момента последнего обращения к ней прокси-сервера значение параметра этого заголовка изменилось, происходит повторная загрузка страницы, поэтому присвоение этой директиве текущего времени при каждом обращении предотвращает кэширование.
HTTP-заголовок Cache-Control предназначен для управления кэшированием, и указание его значения равным no-cache также приводит к запрету кэширования. В устаревшем стандарте HTTP 1.0 для запрета кэширования нужно присвоить значение no-cache HTTP-заголовку Pragma.
Для того, чтобы сообщить промежуточному прокси-серверу о том, что данный документ можно кэшировать, HTTP-заголовку Cache-Control следует передать значение public. Если информация не предназначена для публичных кэш-серверов и может быть сохранена только в локальном кэше браузера, HTTP-заголовку Cache-Control следует передать значение private.
Кэширование документов
<?php
header ("Cache-Control: public");
header ("Cache-Control: private");
. . .
?>
Если содержимое страницы обновляется с определенной регулярностью, задать период обновления можно путем передачи HTTP-заголовку Cache-Control параметра max-age. Данный параметр определяет количество секунд, определяющее время жизни копии страницы в кэше. В листинге скрипт сообщает, что страница может храниться на промежуточном прокси-сервере 1 час.
Управление временем кэширования документа
<?php
header ("Cache-Control: public");
header ("Cache-Control: max-age=3600");
. . .
?>
Для управления кэшем в РНР предусмотрено две специальные функции: session_cache_limiter() и session_cache_expire().
Примечание. В РНР достаточно много функций, которые берут на себя формирование и отправку HTTP-заголовков (session_cache_limiter(), session_cache_expire(), setcookie(), session_start() и т.д.). При отсутствии буферизации использование таких функций после вывода какой-либо информации в окно браузера приводит к появлению предупреждения: Warning: Cannot modify header information - headers already sent by
Функция session_cache_limiter() возвращает и/или устанавливае ограничение кэширования и имеет следующий синтаксис:
string session_cache_limiter([$cache_limiter])
Данная функция возвращает одну из следующих строк, определяющих ограничение кэширования:
Если определен необязательный параметр $cache_limiter, то текущее значение ограничителя заменяется на новое.
Использование функции session_cache_limiter()
<?php
//получаем текущее значение ограничения
$cache_limiter_fst=session_cache_limiter();
//устанавливаем ограничение кэша в "public"
session_cache_limiter('public');
//получаем текущее значение ограничителя
$cache_limiter_snd=session_cache_limiter();
//выводим отчет
echo "Ограничение на кэш изменено ". " с $cache_limiter_fst на $cache_limiter_snd";
?>
В окне веб-браузера это будет выглядеть ТАК.
Функция session_cache_expire() возвращает и/или устанавливает текущее время жизни HTTP-документа в кэше и имеет следующий вид:
int session_cache_expire([$new_cache_expire])
Необязательный параметр $new_cache_expire определяет время жизни документа в кэше в минутах (по умолчанию этот параметр равен 180). Если вызов функции session_cache_expire() осуществляется с параметром $new_cache_expire, то текущее время жизни (180 минут) заменяются на значение, переданные в этом параметре.
Использование функции session_cache_expire()
<?php
//устанавливаем врямя жизни кэша, равное 30 минутам
$cache_expire=session_cache_expire(30*60);
echo $cache_expire;
?>
В окне веб-браузера это будет выглядеть ТАК.
Веб-сервер позволяет защитить страницу при помощи базовой аутентификации — пока пользователь не введет правильные логин и пароль, он не будет допущен к страницам сайта.
Обычно такой вид аутентификации задается при помощи конфигурационных файлов .htaccess и .htpasswd, однако это не всегда удобно, т.к. добавление нового пользователя требует редактирования файла паролей .htpasswd.
Создадим систему базовой аутентификации, логин и пароль для которой, будет храниться в таблице userslist.
Таблица userslist
Каждая запись таблицы userslist состоит из трех полей:
Для реализации базовой аутентификации браузеру клиента необходимо послать следующие HTTP-заголовки:
WWW-Authenticate: Basic realm = "Admin Page"
HTTP/1.0 401 Unauthorized
Именно они приводят к выводу формы для вывода логина и пароля.
Имя пользователя будет помещено сервером в элемент суперглобального массива $_SERVER['PHP_AUTH_USER'], а пароль в $SERVER['PHP_AUTH_PW'].
Примечание. Элементы суперглобального массива $_SERVER['PHP_AUTH_USER'] и $_SERVER['PHP_AUTH_PW'] доступны только в том случае, если РНР установлен в качестве модуля, а не CGI-приложения.
Для удобства код аутентификации удобно выделить в отдельный файл security_mod.php, включение которого при помощи директивы require_once() будет приводить к защите страницы паролем.
Файл security_mod.php
<?php
//устанавливаем соединение с базой данных
require_once("config.php");
//если пользователь не авторизовался - авторизуемся
if(!isset($_SERVER['PHP_AUTH_USER']))
{
header("WWW-Authenticate: Basic realm = \"Admin Page\"");
header("HTTP/1.0 401 Unauthorized");
exit();
}
else
{
//проверяем переменные $_SERVER['PHP_AUTH_USER']
//и $SERVER['PHP_AUTH_PW'], чтобы предотвратить
//SQL-инъекцию
if(!get_magic_quotes_gpc())
{
$_SERVER['PHP_AUTH_USER']= mysql_escape_string($_SERVER['PHP_AUTH_USER']);
$_SERVER['PHP_AUTH_PW']= mysql_escape_string($_SERVER['PHP_AUTH_PW']);
}
$query="SELECT pass FROM userslist WHERE name='{$_SERVER[PHP_AUTH_USER]}'";
$lst=@mysql_query($query);
//если найдена ошибка в SQL-запросе-
//открываем диалоговое окно ввода пароля
if (!$lst)
{
header("WWW-Authenticate: Basic realm = \"Admin Page\"");
header("HTTP/1.0 401 Unauthorized");
exit();
}
//если такого пользователя нет-
//открываем диалоговое окно ввода пароля
if(mysql_num_rows($lst)==0)
{
header("WWW-Authenticate: Basic realm = \"Admin Page\"");
header("HTTP/1.0 401 Unauthorized");
exit();
}
//если все проверки пройдены, сравниваем кэши паролей
$pass=@mysql_fetch_array($lst);
if(md5($_SERVER['PHP_AUTH_PW'])!=$pass['pass'])
{
header("WWW-Authenticate: Basic realm = \"Admin Page\"");
header("HTTP/1.0 401 Unauthorized");
exit();
}
}
?>
Как видно из листинга, при неудачной авторизации клиенту отправляется повторное приглашение для ввода пароля:
WWW-Authenticate: Basic realm = "Admin Page"
HTTP/1.0 401 Unauthorized
Далее работа скрипта останавливается при помощи функции exit(). Дополнительно можно реализовать ограничение количества попыток ввода пароля. Для этого достаточно вместо приведенных выше HTTP-заголовков послать заголовок "Страница не найдена":
HTTP/1.0 404 Not Found
После того, как модуль защиты security_mod.php создан, его можно включить перед загрузкой защищаемых страниц при помощи директивы require_once().
Защита страницы
<?php
//Модуль безопастности
require_once("security_mod.php");
?>
//Далее идут данные страницы, к которой нужно получить доступ
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<title>Untitled Document</title>
</head><body>
<h2>Привет всем!</h2>
</body>
</html>
В окне веб-браузера это будет выглядеть ТАК.