Статья про универсальные методы для повышения портируемости 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). Выставляются они следующим образом:
Первые три бита предназначены для особых режимов запуска файла, подробнее 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 3, PHP 4 и PHP 5. Ветка PHP 3 уже устарела и больше не поддерживается, PHP 4 подходит к своему финалу, а PHP 5 с продвинутой объектной моделью уверенно заменяет PHP 4.
С приходом новых версий неизбежно возникают проблемы с совместимостью старых программ. При выпуске нового PHP разработчики каждый раз стараются минимизировать все нестыковки, которые могут возникнуть при портировании старых проектов, и у них это хорошо получается. Но есть тонкости, о которых стоит помнить для написания качественных скриптов.
$_GET
, $_POST
и т.д. были введены только в PHP 4.1.0, а до этого использовались $HTTP_GET_VARS
, $HTTP_POST_VARS
и т.д., то иногда можно встретить примеры, где длинные названия ещё используются, особенно в старых учебниках. Однако, желательно полностью отказаться от старого стиля в пользу новых глобальных массивов. Так как, начиная с PHP 5.0.0, длинные предопределённые переменные массивов могут быть отключены директивой register_long_arrays
.register_globals
, которая позволяла устанавливать переменные из http-запроса без их явной инициализации. Это стало серьёзной угрозой безопасности, поскольку злоумышленник легко мог подобрать запрос, который заставлял работать скрипт не так, как задумывал его автор. Осознав это, разработчики в PHP 4.2.0 по умолчанию выключили register_globals
. Однако, до сих пор остаётся множество программ, которые без неё не работают. Взвесив все "за" и "против", разработчики решили, что полностью уберут поддержку опасной директивы только с шестой версии PHP. Но уже сейчас всё больше хостингов отказываются от register_globals
, поэтому следует помнить, что перед использованием переменной, её нужно сначала объявить.__clone
. Это мало сказывается на совместимости, но об этом нужно иногда вспоминать, когда разрабатывается проект под PHP 4-5.Эти рекомендации помогут уберечь проект от разного рода неожиданностей, связанных с различием веток PHP. Конечно, это не панацея, но иногда помогает.
В php.ini
находятся все значимые и незначимые настройки для всего, что только есть в PHP. Там присутствуют настройки как для самого PHP, так и для модулей, которые установлены вместе с ним. Трудно найти два хостинг-сервера, у которых бы php.ini
совпадал на 100%, зачастую настройки у всех самые разные. Но критическими являются только некоторые из них:
register_globals
- установка глобальных переменных из запроса. Существенно влияет на безопасность и работоспособность, даже когда код написан без её поддержки. magic_quotes_gpc
- экранирование входящих данных $_GET
, $_POST
, $_COOKIE
. Опасна тем, что усыпляет бдительность пользователя к проблеме экранирования данных. Не может одна функция быть панацеей для всех типов данных (html, sql, xml и т.д.). short_open_tag
- позволяет использовать в качестве открывающего тега PHP конструкцию <?
. При выключенной директиве PHP будет считать кодом только то, что будет идти после <?php
или <script language="php">
, всё остальное будет выведено в браузер.error_reporting
- уровень вывода информации об ошибках. При значении E_ALL
может показать много интересного в самых неожиданных местах. Из-за этого может пострадать не только дизайн, репутация, безопасность, но и работоспособность программы в целом, поскольку преждевременный вывод влияет на отправку заголовков страницы.Итак, обо всём по порядку.
Эта опция регистрирует переменные из массивов 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
, а при обращении напрямую, будет выдавать пустую страницу.
Эта опция призвана экранировать четыре самых опасных, как считают разработчики, символа - обратный слеш (\), одинарную кавычку ('), двойную кавычку (") и 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("Hello");> |
В этом запросе используется специальная конструкция "
, которая сообщает браузеру, что в скрипт нужно подставить двойную кавычку. Такая инъекция невидима для 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, но это самое безобидное, что может произойти. Зачастую, через такие "дыры" загружают скрипты-коммандеры, которые работают на стороне сервера, давая злоумышленнику полный доступ к вашему хостингу. Если такой скрипт удастся загрузить, то в любой момент времени атакованный сайт может быть "подправлен" или уничтожен.
Может быть, это, а может быть, ещё что-то побудило разработчиков сделать магические кавычки. Но надо было их делать либо для всех обязательными, либо не делать вовсе. Путаница, которую они вносят, даёт несомненно больший вред для безопасности и портируемости, чем реальной пользы. Но пока они есть, нужно делать следующее:
magic_quotes_gpc
.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
.
Эта настройка позволяет использовать короткий открывающий тег <?,
помимо двух других, <?php
и <script language="php">
.
Полезной особенностью этой опции является также возможность использования сокращённой формы вывода на экран <?=...?>.
При создании шаблонов эта возможность обычно очень кстати.
Единственное узкое место, которое тут есть - short_open_tag может быть как включена, так и выключена. А значит, если вы использовали короткие теги, ваш скрипт где-то будет работать, а где-то и нет. Обычно это не имеет большого значения, когда проект разрабатывается для массового хостинга, там эта настройка зачастую включена. Но когда предполагается использование проекта не только на хостинге, но и на других серверах, то лучше позаботиться о совместимости при выключенных коротких тегах.
Это настройка позволяет регулировать вывод на экран пользователя информацию об ошибках, которые возникли во время выполнения программы. Есть четыре основных вида сообщений об ошибках, с которыми сталкиваются пользователи:
E_NOTICE
- сообщения, которые носят информативный характер (Notice) и не являются критическими. Обычно их появление связано с использованием необъявленных переменных и констант. Поскольку PHP не обязывает объявлять переменную перед её использованием, то по умолчанию ей присваивается значение NULL, константе по умолчанию присваивается её имя. E_WARNING
- сообщения, которые информируют о некритичных ошибках (Warning), например, использовании невалидных данных или ресурсов. E_ERROR
- сообщения о критичных ошибках (Fatal Error). Их появление означает, что PHP не знает, каким образом должен выполнять программу, поэтому выводит сообщение и завершает свою работу.E_PARSE
- сообщения об ошибках синтаксиса программы (Parse Error).При выводе любого сообщения об ошибке 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); ?> |
Эти несложные правила помогут вашему проекту избавиться от разного рода "старнностей" при установке на "чужой" сервер.
Удачных разработок! :)