Портируемый php

  1. Введение
  2. Различия платформ
  3. Различия платформ в примерах
  4. Различия версий php
  5. Различия в конфигурации php.ini
  6. Выводы

Введение

Статья про универсальные методы для повышения портируемости php-скриптов. Немного теории, немного практики.

Различия платформ

Любая платформа начинается с файловой системы, поэтому самые важные отличия для php-проектов кроются именно в ней.

Вот так может выглядеть абсолютный путь к файлу в операционной среде Windows

D:\WebServers\home\test1\www\index.php

Первая буква обозначает имя логического диска, где расположен файл. Дальше идёт двоеточие и путь к файлу. Сам путь разделён символами обратного слеша (\). Имя диска может быть только одним символом английского алфавита от A до Z. При этом нет никакой разницы между прописными и строчными буквами во всём пути файла.

Практически так же выглядит абсолютный путь к файлу и в UNIX

/www/home/test1/docs/index.php 

Внешне только два отличия - нет имени диска и слеш используется прямой (/). Однако, имена файлов уже регистрозависимы. Отсутствие имени диска объясняется тем, что разделы монтируются (т.е. подключаются) к структуре файловой системы по средствам точек монтирования. А вся файловая система строится от корня (root - англ.) - /. Этот корень всегда указывает на начало дерева файлов.

Вот так может выглядеть структура разделов диска на заурядном UNIX-сервере (FreeBSD):

# df -h
Filesystem     Size    Used   Avail Capacity  Mounted on
/dev/ad0s1a    496M     54M    402M    12%    /
devfs          1.0K    1.0K      0B   100%    /dev
/dev/ad0s1e    496M    2.1M    454M     0%    /tmp
/dev/ad0s1f    5.1G    929M    3.8G    19%    /usr
/dev/ad0s1d    1.2G    8.6M    1.1G     1%    /var

За разные папки отвечают разные разделы диска, но путь к файлу строится всегда от корня, а не от диска.

Помимо разделителя файлов пути, отличается также и разделитель путей в переменной PATH. В Windows этому служит точка с запятой, а в UNIX двоеточие. Однако, в PHP есть специальная константа, которая содержит в себе валидный разделитель путей - это PATH_SEPARATOR. Поэтому при расширении путей поиска файлов лучшим решением будет использование этой константы:

// Пути поиска подгружаемых файлов
$extendedPathArray = array('.', './classes');

// Расширение путей поиска файлов
ini_set('include_path', join(PATH_SEPARATOR, $extendedPathArray));

Следующим серьёзным отличием двух систем являются права доступа на файлы. В Windows само понятие "права доступа на файл" появилось с приходом NTFS, до этого обходились только атрибутами. Атрибутов у файла может быть несколько и иногда они зависят от самого файла, но есть и стандартный набор:

D:\WebServers>help dir
Вывод списка файлов и подкаталогов из указанного каталога.
...
  /A        Вывод файлов с указанными атрибутами.
  атрибуты   D  Каталоги                    R  Доступные только для чтения
             H  Скрытые файлы               A  Файлы для архивирования
             S  Системные файлы             Префикс "-" имеет значение НЕ

Таким образом, обычный файл в Windows может быть скрытым, системным, только для чтения, каталогом и для архивирования. Этот набор объясняется тем, что изначально Windows планировалась как однопользовательская система. Когда возникла необходимость в многопользовательской системе и потребовалось разделять доступ к файлам, то эту возможность заложили в файловой системе NTFS. Но использование прав доступа в ней получилось настолько сложным и незаурядным, что их поддержки стандартными функциями в PHP нет.

В UNIX дело обстояло совсем иначе, и права доступа появились ещё на этапе зарождения:

CHMOD(1)                FreeBSD General Commands Manual               CHMOD(1)

NAME
     chmod -- change file modes
...
HISTORY
     A chmod command appeared in Version 1 AT&T UNIX.

Любой файл в UNIX-системе имеет владельца и группу. Владелец может быть только один, в то время как в группу может входить несколько пользователей. Права на файл строятся соответственно. Можно задать права для владельца, для группы и остальных пользователей, которые не входят в группу и не являются владельцами. Самих прав всего три - чтение (read), запись (write) и выполнение (execute). Выставляются они следующим образом:

perms

Первые три бита предназначены для особых режимов запуска файла, подробнее chmod(1), в php они редко бывают полезными. Затем идут три бита прав доступа к файлу для владельца, потом для группы и последние три - права для остальных пользователей. Каждый бит отвечает за одно разрешение. Поскольку биты используются триадами, то самый удобный способ представления - восьмеричная система счисления. Показанные на схеме права (0751) означают, что владелец имеет все права на файл, члены группы только чтение/запуск, остальные - только запуск.

Поскольку PHP разрабатывался для UNIX-систем, то функции для работы с файловой системой были ориентированы в первую очередь на UNIX, их поддержка встроена в стандартный набор функций. Однако, нужно помнить, что права задаются в восьмеричном виде, а это значит, что не надо забывать ставить ноль перед числом с правами, иначе они будут заданы неверно:

<?php
chmod('file.txt', 0644); // Первый ноль означает, что число задаётся в восьмеричной системе
?>

Ещё одним камнем преткновения является символ перевода строки. В Windows используется \r\n, UNIX - \n, MAC - \r. Для браузера пользователя это не имеет значения, но критично для записи файлов, открытых с помощью функции fopen(). Дело в том, что есть текстовый режим записи и бинарный. Их разница в том, что при записи текстовый режим будет заменять символ новой строки на валидный по отношению к платформе, а бинарный записывает данные как есть. Основное неудобство состоит в том, что в разных версиях PHP под разные платформы установлены разные дефолтные значения для режима wirte. Одна и та же конструкция fopen(.., 'w') может открыть файл как в текстовом режиме, так и в бинарном. Стоит отметить, что на UNIX режим по умолчанию был и остаётся бинарный, неразбериха была с версиями под Windows. Это критично только по отношению к бинарным файлам, например, картинкам. Но не стоит этим пренебрегать и лучше явно указывать режим записи - 'b' или 't'.

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

Это далеко не все отличия, которые есть, но они самые опасные. Дальше я наглядно поясню почему.

Различия платформ в примерах

Возьмём простой пример:

<?php
print_r(pathinfo('C:\\windows\\temp\\tmp1.txt'));
?>

Функция pathinfo() разбирает переданный ей путь на составляющие. Двойной обратный слеш используется потому, что в php он является специальным символом экранирования. Проверяем результат выполнения:

Win32
------
Array
(
    [dirname] => C:\windows\temp
    [basename] => tmp1.txt
    [extension] => txt
    [filename] => tmp1
)

Unix-like
------
Array
(
    [dirname] => .
    [basename] => C:\windows\temp\tmp1.txt
    [extension] => txt
    [filename] => C:\windows\temp\tmp1
)

Хорошо видно, что в первом случае под Win32 всё отработало нормально. Во втором случае, под Unix, родительский каталог (dirname) определился как текущий (.), а имя файла (basename и filename) определилось вместе со всеми каталогами и диском. Вызвано это как раз тем, что в UNIX обратный слеш не является символом разделения имён файлов и каталогов, а используется для экранирования специальных символов.

Решением проблемы будет замена всех обратных слешей на прямые:

<?php
$path = 'C:\\windows\\temp\\tmp1.txt';
$path = strtr($path, '\\', '/'); # замена обратных слешей на прямые

print_r(pathinfo($path));

?>

Win32
------
Array
(
    [dirname] => C:/windows/temp
    [basename] => tmp1.txt
    [extension] => txt
    [filename] => tmp1
)

Unix-like
------
Array
(
    [dirname] => C:/windows/temp
    [basename] => tmp1.txt
    [extension] => txt
    [filename] => tmp1
)

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

<?php
$file = __FILE__;
$file = strtr($file, '\\', '/'); # Замена слешей

echo 'Файл ', $file, ' ', file_exists($file) ? 'существует' : 'не существует';
?>

Win32
------
Файл D:/WebServers/home/test1/www/index.php существует

Помимо слешей существует ещё одна серьёзная проблема. Ни для кого не секрет, что многие проекты изначально разрабатываются на компьютерах под управлением MS Windows, поскольку в чём-то она бывает удобней и привычней. И вот на этапе разразботки большого и серьёзного проекта, скажем, возникает необходимость сделать проверку на правильность имени файла. Получилось примерно так:

<?php

$name = 'Document.doc';

echo $name, ' это ', checkFileName($name) ? 'правильное' : 'неправильное', ' имя';

function checkFileName($fileName)
{
    return (bool) preg_match('/^\\w+\\.\\w+$/', $fileName);
}

?>

Так как проект большой и серьёзный, то этот скрипт протестировали на разных платформах:

Win32
------
Document.doc это правильное имя


Unix-like
------
Document.doc это правильное имя

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

<?php
$name = 'Документ.doc';
...
?>

Win32
------
Документ.doc это правильное имя

Unix-like
------
Документ.doc это неправильное имя

Подобные ситуации возникают у каждого программиста, который работает только под Windows. Некоторые разработчики не используют специальные множества \w и \W, а указывают диапазоны [_0-9a-zA-Zа-яА-ЯёЁ], что отчасти исправляет ситуацию. Но единственно верным решением будет настройка локалей. Предполагается, что проект разрабатывается в кодировке CP1251, поэтому установка локали может выглядеть примерно так:

<?php
// установка локали
setlocale(LC_ALL, 'ru_RU.CP1251', 'rus_RUS.CP1251', 'Russian_Russia.1251', 'russian');

$name = 'Документ.doc';
...
?>

После этого регулярные выражения и функции для работы со строками будут работать как часы.

Win32
------
Документ.doc это правильное имя


Unix-like
------
Документ.doc это правильное имя

Различия версий php

Несмотря на довольно обширную историю, PHP на сегодняшний день насчитывает три основных ветки: PHP 3, PHP 4 и PHP 5. Ветка PHP 3 уже устарела и больше не поддерживается, PHP 4 подходит к своему финалу, а PHP 5 с продвинутой объектной моделью уверенно заменяет PHP 4.

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

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

Различия в конфигурации php.ini

В php.ini находятся все значимые и незначимые настройки для всего, что только есть в PHP. Там присутствуют настройки как для самого PHP, так и для модулей, которые установлены вместе с ним. Трудно найти два хостинг-сервера, у которых бы php.ini совпадал на 100%, зачастую настройки у всех самые разные. Но критическими являются только некоторые из них:

Итак, обо всём по порядку.

register_globals

Эта опция регистрирует переменные из массивов EGPCS (Environment, GET, POST, Cookie, Server) в качестве глобальных переменных. Простой пример:

<?php
// При запросе example.php?id=123 и register_globals=On
echo $id; // Выведет 123
?>

С одной стороны, конечно, удобно, с двух других - сплошные проблемы.

В чём тут опасность? Во-первых, когда в запросе не будет параметра id, то и переменной $id не будет, echo начнёт выводить необъявленную переменную, что может привести к выводу ошибки. Во-вторых, использование необъявленной переменной всегда нарушает логику программы, что неизбежно приводит к путанице и скрытым багам. Поэтому нужно всегда перед работой с переменной её объявить, а ещё лучше дополнительно сделать проверку на валидность и присвоить значение по умолчанию:

<?php
// Правильный вариант работы с переданными переменными
// если в GET есть id, то присваеваем его числовое значение, иначе 0
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;

// Теперь в $id только числовой идентификатор
?>

Интересный эффект может получиться, когда переменная объявляется в одном файле, а используется в другом. Возьмём, к примеру, часть администраторского интерфейса:

Файл commander.php
-------------------------
<?php
// Файл, который авторизует пользователя и подключает модуль

// Проверка логина и пароля
$auth = @$_GET['login'] === 'test' && @$_GET['passw'] === '123';

// подключаем модуль
include('module.inc.php');
?>

Файл module.inc.php
-------------------------
<?php
if ( @$auth )
{
    echo 'Секретная информация, друг!';
}
else
{
    echo 'Прочь, неавторизованный пользователь!';
}
?>

Первый файл (commander.php) явно устанавливает переменную $auth, которая может быть либо true, либо false. Если логин и пароль совпали, то $auth будет true, иначе false. В подгружаемом модуле идёт проверка переменной $auth, и в случае успеха выводится секретная информация, в противном случае - отпугивающее сообщение. Тестируем:

# register_globals=On
// Запрос: commander.php
Прочь, неавторизованный пользователь!

// Запрос: commander.php?login=test&passw=123
Секретная информация, друг!

// Запрос: module.inc.php
Прочь, неавторизованный пользователь!

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

# register_globals=On
// Запрос: module.inc.php?auth=1
Секретная информация, друг!

Переменная $auth не устанавливается в явном виде, когда запрос идёт напрямую к файлу module.inc.php, поэтому её значение берётся из EGPCS. В данном случае адресная строка явилась источником значения $auth, а вовсе не файл commander.php.

Чтобы избежать подобных ситуаций, нужно, во-первых, использовать переменные, которые явно объявлены в этом же файле. Во-вторых, использовать строгое равенство (===) для проверки значений. И, в-третьих, делать проверку на количество подключенных файлов, если приходится работать с переменными из других файлов. Правильный вариант:

Файл module.inc.php
-------------------------
<?php
// Если файл вызывается напрямую, то выходим
if ( count(get_included_files()) < 2 ) exit();

// Строгая проверка булева типа
if ( @$auth === true )
{
    echo 'Секретная информация, друг!';
}
else
{
    echo 'Прочь, неавторизованный пользователь!';
}
?>

Этот код будет одинаково работать независимо от register_globals, а при обращении напрямую, будет выдавать пустую страницу.

magic_quotes_gpc

Эта опция призвана экранировать четыре самых опасных, как считают разработчики, символа - обратный слеш (\), одинарную кавычку ('), двойную кавычку (") и NULL-символ (\0). Эти символы провинились тем, что их использование в запросах, eval'ах и инклюдах приводят к очень печальным последствиям - инъекциям.

Описать суть инъекции можно следующим примером. Есть код, который конструируется из переданных пользователем параметров, которые могут дополнять основной код, нарушая тем самым логику его выполнения. К примеру, есть файл, занимающийся удалением пользователей из базы:

<?php
// magic_qoutes_gpc=Off
// Запрос: test.php?login=John

// Удаление пользователя из базы
if ( isset($_GET['login']) )
    mysql_query('DELETE FROM `users` WHERE `login` = "'. $_GET['login'] .'" LIMIT 1');
?>

Логин кандидата на удаление приходит в скрипт через адресную строку. Выполнение данного кода приведёт к тому, что следующий запрос будет передан в базу:

DELETE FROM `users` WHERE `login` = "John" LIMIT 1

При этом будет удалена одна запись, у которой поле login будет содержать John, что и требовалось. Однако стоит немного модифицировать запрос к странице, как к базе пойдёт совсем другой SQL-запрос:

// Запрос: test.php?login=John"+OR+1+--+
SQL: DELETE FROM `users` WHERE `login` = "John" OR 1 -- " LIMIT 1

После выполнения удалятся все записи из таблицы users. Это произойдёт потому, что условие OR 1 будет работать для каждой записи, а LIMIT 1 был отброшен символом комментария (--).

Подобный финт можно провернуть везде, где есть возможность вставить произвольный код в обход данных. Во избежание подобных вопиющих ситуаций и было предложено "гениальное решение" - заэкранировать самые опасные символы во всём, что поступает от пользователя ($_GET, $_POST, $_COOKIE). Так появились волшебные кавычки - magic quotes.

Кроме мнимого решения некоторых проблем, связанных с безопасностью, магические кавычки внесли проблемы с совместимостью. Включение magin_quotes_gpc приводит к обратимому изменению пользовательских данных, которые передаются через запрос, форму, или cookie. Вот небольшой пример последствий magic_quotes_gpc=On:

<?php
// magin_quotes_gpc=On

// Пускай будет файл, путь к которому передаётся через url
$file = isset($_GET['file']) ? $_GET['file'] : 'c:\\text\\1.txt';

// Вывод ссылки с параметром из $_GET
echo '<a href="?file=', $file, '">', $file, '</a>';
?>

Никаких обработок данных в этом коде нет, но достаточно запустить скрипт и несколько раз нажать на ссылку, чтобы увидеть проблему:

c:\text\1.txt
c:\\text\\1.txt
c:\\\\text\\\\1.txt
c:\\\\\\\\text\\\\\\\\1.txt
...

С каждым новым кликом обратные слеши удваиваются, поскольку для экранирования слеша требуется ещё один такой же слеш. Если выключить magic_quotes_gpc, то никаких изменений происходить не будет, а ссылка будет иметь одно и то же значение.

Хуже всего, если проект разрабатывается вовсе без учёта магических кавычек. Если такой код попадает на сервер с другой настройкой magic_quotes_gpc, в лучшем случае к данным GPC будет подставляться обратный слеш, в худшем - будут доступны скрытые инъекции.

Собственно и безопасность, ради которой была введена эта настройка, также оставляет желать лучшего. Проблема тут самая банальная - в разных данных используются разные символы экранирования. За примером далеко ходить не надо, достаточно взять HTML и PHP. В HTML нет экранирования как такового, но есть специальные конструкции, которые всегда начинаются с амперсанда (&). В PHP, напротив, символ экранирования есть. Поэтому магические кавычки могут защитить данные PHP от инъекций, но только до тех пор, пока они не будут выведены в HTML, там их "магия" заканчивается и начинается HTML-инъекция. Небольшой пример:

<?php
// magin_quotes=On

// Пускай будет какая-либо переменная из POST-данных
$name = isset($_POST['name']) ? $_POST['name'] : 'John';

// Выполнение произвольной команды или запроса с участием переменной
eval('echo "<p>Hello, <b>'. $name .'</b></p>";');
?>
<!-- HTML-форма для передачи переменной PHP-скрипту -->
<form method="POST">
  <input type="text" name="name" value="<?php echo $name;?>" />
  <input type="submit" value="Send" />
</form>

После запуска появится привествие "Hello, John" и форма для ввода имени. Если после имени добавить <script>alert()</script>, то HTML-код формы примет такой вид:

<p>Hello, <b>John<script>alert()</script></b></p>
<!-- HTML-форма для передачи переменной PHP-скрипту -->
<form method="POST">
  <input type="text" name="name" value="John<script>alert()</script>" />
  <input type="submit" value="Send" />
</form>

Произвольный код, который был передан через пользовательские данные, был успешно вставлен на страницу. Инъекция удалась! Конечно, можно возразить, что кавычки всё равно в этом скрипте работать не будут, потому что их заэкранируют магические кавычки. Но это будет лишь отчасти правдой. Действительно, в JavaScript есть экранирующий символ, и он даже совпадает с символом из PHP, но в HTML его нет! А значит, используя правила HTML можно сконструировать запрос, который вставит произвольный код с кавычками:

John<body onload=alert(&quot;Hello&quot;);>

В этом запросе используется специальная конструкция &quot;, которая сообщает браузеру, что в скрипт нужно подставить двойную кавычку. Такая инъекция невидима для magic_quotes_gpc, а значит, опасна вдвойне.

Но самый печальный вариант будет, когда наш PHP-скрипт попадёт на сервер, где магические кавычки отключены:

<?php
// magin_quotes=Off

// Пускай будет какая-либо переменная из POST-данных
$name = isset($_POST['name']) ? $_POST['name'] : 'John';

// Выполнение произвольной команды или запроса с участием переменной
eval('echo "<p>Hello, <b>'. $name .'</b></p>";');
?>
<!-- HTML-форма для передачи переменной PHP-скрипту -->
<form method="POST">
  <input type="text" name="name" value="<?php echo $name;?>" />
  <input type="submit" value="Send" />
</form>

Угроза уже будет не только браузеру пользователя, но и всему серверу в целом. Причина кроется в команде eval, которая выполняет произвольный PHP-код. Пока были включены магические кавычки, она всегда выполняла одну и ту же команду:

echo "...";

Но как только экранирование кавычек будет отключено, то появится возможность менять логику, добавляя произвольные команды:

// Пусть в $name будет «John";phpino();//»
...
eval('echo "<p>Hello, <b>John";phpinfo();//<b>";');

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

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

  1. Проверять настройку magic_quotes_gpc.
  2. Если магические кавычки включены, то принудительно убирать экранирование.
  3. При работе с данными всегда использовать специальные функции для экранирования, такие как htmlentities(), mysql_real_escape_string(), addcslashes() и так далее. Как правило, для каждого типа данных есть своя функция, поскольку спецсимволы у всех разные.

Правильный пример работы:

<?php

/** 
* @desc Функция убирает экранирующие слеши из строки или массива 
* @param mixed $data 
* @return void 
*/ 
function array_stripslashes (& $data ) 
{ 
    if ( is_array($data) )
    {
        array_walk($data, __FUNCTION__);
    }
    elseif ( is_string($data) )
    {
        $data = stripcslashes($data);
    }
}

// Если магические кавычки включены, то убираем экранирование
if ( get_magic_quotes_gpc() )
{
    if ( $_GET    ) array_stripslashes($_GET   );
    if ( $_POST   ) array_stripslashes($_POST  );
    if ( $_COOKIE ) array_stripslashes($_COOKIE);
}

...

// При работе с разными данными нужно использовать 
// специализированные функции экранирования

// Вывод в HTML
echo 'Hello, <b>', htmlentities($name, ...), '</b>';

// MySQL-запрос
$sql = 'DELETE ... WHERE `id` = "'. mysql_real_escape_string($id) .'" ... ';

// Выполнение PHP.
// Для одинарных кавычек нужно экранировать ' и \
// Для двойных кавычек нужно экранировать ", $ и \
eval('echo "hello, '. addcslashes($name, '"$\\') .'";');

// Вызов системной команды
passthru('touch '. escapeshellarg($file));

...
?>

Только такой подход обеспечит как совместимость, так и безопасность ваших проектов при любых значениях magic_quotes_gpc.

short_open_tag

Эта настройка позволяет использовать короткий открывающий тег <?, помимо двух других, <?php и <script language="php">.

Полезной особенностью этой опции является также возможность использования сокращённой формы вывода на экран <?=...?>. При создании шаблонов эта возможность обычно очень кстати.

Единственное узкое место, которое тут есть - short_open_tag может быть как включена, так и выключена. А значит, если вы использовали короткие теги, ваш скрипт где-то будет работать, а где-то и нет. Обычно это не имеет большого значения, когда проект разрабатывается для массового хостинга, там эта настройка зачастую включена. Но когда предполагается использование проекта не только на хостинге, но и на других серверах, то лучше позаботиться о совместимости при выключенных коротких тегах.

error_reporting

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

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

Помимо аспектов безопасности вывод ошибок также существенно влияет и на портируемость. Суть проблемы состоит в преждевременной отправке заголовков. На большинстве серверов отключён вывод сообщений E_NOTICE, поэтому появлется возможность безнаказанно использовать необъявленные переменные и константы, например:

<?php

// Подключаемся к базе данных
mysql_connect(localhost, root);

// Стартуем сессию
session_start();
?>

Этот скрипт успешно выполнится при отключенных E_NOTICE. Но как только он попадёт на сервер с выводом всех ошибок, то сессия перестанет стартовать:

<?php
// Включение вывода всех ошибок
error_reporting(E_ALL);

// Подключаемся к базе данных
mysql_connect(localhost, root);

// Стартуем сессию
session_start();
?>
====== Результат ======
<br />
<b>Notice</b>: Use of undefined constant localhost - assumed 'localhost' in 
<b>D:\WebServers\home\test1\www\index.php</b> on line <b>6</b><br />

<br />
<b>Notice</b>: Use of undefined constant root - assumed 'root' in 
<b>D:\WebServers\home\test1\www\index.php</b> on line <b>6</b><br />

<br />
<b>Warning</b>: session_start() [<a href='function.session-start'>
function.session-start</a>]: 
Cannot send session cookie - headers already sent by (output started at 
D:\WebServers\home\test1\www\index.php:6) in <b>
D:\WebServers\home\test1\www\index.php</b> 
on line <b>9</b><br /> 

<br />
<b>Warning</b>: session_start() [<a href='function.session-start'>
function.session-start</a>]: 
Cannot send session cache limiter - headers already sent (output started at 
D:\WebServers\home\test1\www\index.php:6) in <b>
D:\WebServers\home\test1\www\index.php</b> 
on line <b>9</b><br /> 

В результате запуска скрипт выдал два сообщения о необъявленных константах, что, в свою очередь, привело к двум сообщениям об ошибках, поскольку сессия не смогла стартовать. Это связано с тем, что идентификатор сессии передаётся в cookie-переменной, которая передаётся через HTTP-заголовки. Сами заголовки отправляются клиенту при первом выводе. Поэтому вызов функций setcookie(), header() и session_start() должен осуществляться до любого вывода на экран. Вывод ошибок может существенно повлиять на работоспособность проекта, поскольку преждевременный вывод может опередить установку нужных заголовков.

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

Помимо буферизации можно динамически управлять выводом ошибок через функцию error_reporting().

<?php
error_reporting(E_ALL); // Вывод всех ошибок
error_reporting(0);     // Запрет вывода ошибок
error_reporting(E_WARNING | E_ERROR); // Вывод только Warning и Fatal Error
error_reporting(E_ALL & ~E_NOTICE);   // Вывод всех ошибок, кроме Notice
?>

Таким образом, при разработке можно и нужно включать вывод всех ошибок, чтобы исключить как можно больше ошибок на этапе отладки проекта. Дополнительно нужно всегда и везде использовать предварительно объявленные переменные и константы.

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

<?php
// Функция обработчик ошибок
function fileErrorHandler($errno, $errstr, $errfile, $errline)
{
    file_put_contents('errors.txt',
        sprintf(
            "%s\tError: %s, in file %s, on line %s\n",
            date('Y-m-d H:i:s'), $errstr, $errfile, $errno
        ), 
    FILE_APPEND);
}

// Переопределение функции обработки ошибок
set_error_handler('fileErrorHandler');
?>

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

Разумные выводы

Для повышения портируемости ваших скриптов, а также для лучшей безопасности и стабильности работы в каждый скрипт я рекомендую включать такой файл:

<?php
// Включаем буферизацию
ob_start();

// Стартуем сессию
//session_start(); # Старт сессии должен идти перед любым выводом на экран

// Переопределение функции обработки ошибок
//set_error_handler('fileErrorHandler'); # Сокрытие ошибок от пользователя

// установка локали
setlocale(LC_ALL, 'ru_RU.CP1251', 'rus_RUS.CP1251', 'Russian_Russia.1251', 'russian');

// Включаем вывод всех ошибок
error_repoting(E_ALL);

// Пути поиска подгружаемых файлов
$extendedPathArray = array('.', './classes');

// Расширение путей поиска файлов
# Для разделения путей нужно использовать специальную констатну - PATH_SEPARATOR
//ini_set('include_path', join(PATH_SEPARATOR, $extendedPathArray)); 

// Функция обработчик ошибок
function fileErrorHandler($errno, $errstr, $errfile, $errline)
{
    file_put_contents('errors.txt',
        sprintf(
            "%s\tError: %s, in file %s, on line %s\n",
            date('Y-m-d H:i:s'), $errstr, $errfile, $errno
        ), 
    FILE_APPEND);
}

/** 
* @desc Функция убирает экранирующие слеши из строки или массива 
* @param mixed $data 
* @return void 
*/ 
function array_stripslashes (& $data ) 
{ 
    if ( is_array($data) )
    {
        array_walk($data, __FUNCTION__);
    }
    elseif ( is_string($data) )
    {
        $data = stripcslashes($data);
    }
}

// Если магические кавычки включены, то убираем экранирование
if ( get_magic_quotes_gpc() )
{
    if ( $_GET    ) array_stripslashes($_GET   );
    if ( $_POST   ) array_stripslashes($_POST  );
    if ( $_COOKIE ) array_stripslashes($_COOKIE);
}
?>

Обычно, этот файл я называю common.php, а потом подключаю его в каждый скрипт через include или require. Это позволяет забыть о разных настройках на серверах и сосредоточиться только на работе. Те команды, которые необязательны, закоментированы, остальные необходимы всегда.

Во время непосредственной разработки скриптов, я рекомендую следовать следующим правилам:

<?php
// Рекомендации во время работы

...

// При работе с разными данными нужно использовать 
// специализированные функции экранирования

// Вывод в HTML
echo 'Hello, <b>', htmlentities($name, ...), '</b>';

// MySQL-запрос
$sql = 'DELETE ... WHERE `id` = "'. mysql_real_escape_string($id) .'" ... ';

// Выполнение PHP
eval('echo "hello, '. addcslashes($name, '"$\\') .'";');

// Вызов системной команды
passthru('touch '. escapeshellarg($file));

...

// Перед использованием переменной её нужно всегда объявлять
$i = 0;
while(++$i < 10) {...}

...

// Нужно использовать массивы $_GET, $_POST, $_FILES и т.д. 
// вместо устаревших $HTTP_GET_VARS, $HTTP_POST_VARS и т.д.
// А также определять, установлена ли переменная перед её использованием
$someVar = isset($_GET['someVar']) ? $_GET['someVar'] : 'значение по умолчанию';

...

// Если есть вероятность, что путь может содержать обратные слеши,
// то нужно их всегда заменять на прямые
$path = strtr($_POST['path'], '\\', '/'); // Полный путь загруженного файла
$info = pathinfo($path);  // Информация о файле

...

// Если проект разрабатывается для широкого распространения,
// то лучше отказаться от сокращённых тегов <? и <?= в пользу <?php
?><p><?php echo 'Hello'?> world!</p><?php

...

// Правильно устанавливайте права доступа на файл,
// используя восьмеричную систему счисления
chmod('file.txt', 0755);

?>

Эти несложные правила помогут вашему проекту избавиться от разного рода "старнностей" при установке на "чужой" сервер.

Удачных разработок! :)