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

Самоучитель PHP 4 - Котеров Д. В

..pdf
Скачиваний:
92
Добавлен:
24.05.2014
Размер:
4.38 Mб
Скачать

48

Часть I. Основы Web-программирования

Передача информации CGI-сценарию

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

Переменные окружения

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

HTTP_ACCEPT

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

HTTP_REFERER

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

HTTP_USER_AGENT

Идентифицирует браузер пользователя. Если в данной переменной окружения присутствует подстрока MSIE, то это — Internet Explorer, в противном случае, если в наличии лишь слово Mozilla, — Netscape.

HTTP_HOST

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

Глава 3. CGI изнутри

49

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

SERVER_PORT

Порт сервера (обычно 80), к которому обратился браузер пользователя. Также может привлекаться для генерации параметра заголовка Location.

REMOTE_ADDR

Эта переменная окружения задает IP-адрес (или доменное имя) узла пользователя, на котором был запущен браузер.

REMOTE_PORT

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

SCRIPT_NAME

Виртуальное имя выполняющегося сценария (то есть часть URL после имени сервера, но до символа ?). Эту переменную окружения, опять же, очень удобно брать на вооружение при формировании заголовка Location при переадресации на себя (self-redirect), а также при проставлении значения атрибута action тэга <form> на странице, которую выдает сценарий при запуске без параметров (для того чтобы не привязываться к конкретному имени сценария).

REQUEST_METHOD

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

QUERY_STRING

Параметры, которые в URL указаны после вопросительного знака. Напомню, что они доступны как при методе GET, так и при методе POST (если в последнем случае они были определены в атрибуте action тэга <form>).

CONTENT_LENGTH

Количество байтов данных, присланных пользователем. Эту переменную необходимо анализировать, если вы занимаетесь приемом и обработкой POST-формы.

50

Часть I. Основы Web-программирования

Передача параметров методом GET

Тут все просто. Все параметры передаются единой строкой (а именно, точно такой же, какая была задана в URL после ?) в переменной QUERY_STRING. Единственная проблема — то, что все данные поступят URL-кодированными. Так что нам понадобится функция декодирования. Но это отдельная тема, пока мы не будем ее касаться.

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

Листинг 3.2. Работа с переменными окружения

#include <stdio.h> // Включаем функции ввода/вывода

#include <stdlib.h> // Включаем функцию getenv()

void main(void) {

//получаем значение переменной окружения REMOTE_ADDR char *RemoteAddr = getenv("REMOTE_ADDR");

//... и еще QUERY_STRING

char *QueryString = getenv("QUERY_STRING"); // печатаем заголовок

printf("Content-type: text/html\n\n"); // печатаем документ

printf("<html><body>");

printf("<h1>Здравствуйте. Мы знаем о вас все!</h1>"); printf("Ваш IP-адрес: %s<br>",RemoteAddr);

printf("Вот параметры, которые Вы указали: %s",QueryString); printf("</body></html>");

}

Откомпилируем сценарий и поместим его в "CGI-каталог". Теперь в адресной строке введем:

http://www.myhost.com/cgi-bin/script.cgi?a=1&b=2

Мы получим примерно такой документ:

Здравствуйте. Мы знаем о Вас все!

Ваш IP-адрес: 192.232.01.23

Вот параметры, которые Вы указали: a=1&b=2

Глава 3. CGI изнутри

51

Передача параметров методом POST

В отличие от метода GET, здесь параметры передаются сценарию не через переменные окружения, а через стандартный поток ввода (в Си он называется stdin). То есть программа должна работать так, будто никакого сервера не существует, а она читает данные, которые вводит пользователь с клавиатуры. (Конечно, на самом деле никакой клавиатуры нет и быть не может, а заправляет всем сервер, который "изображает из себя" клавиатуру.)

Следует заметить очень важную деталь: то, что был использован метод POST, вовсе не означает, что не был применен также и метод GET. Иными словами, метод POST подразумевает также возможность передачи данных через URL- строку. Эти данные будут, как обычно, помещены в переменную окружения

QUERY_STRING.

Но как же узнать, сколько именно данных переслал пользователь методом POST? До каких пор нам читать входной поток? Для этого служит переменная окружения CONTENT_LENGTH, в которой хранится строка с десятичным представлением числа переданных байтов данных (разумеется, перед использованием ее надо перевести в обычное число).

Модифицируем предыдущий пример так, чтобы он принимал POST-данные, а также выводил и GET-информацию, если она задана:

Листинг 3.3. Получение данных POST

#include <stdio.h> #include <stdlib.h>

void main(void) {

//извлекаем значения переменных окружения char *RemoteAddr = getenv("REMOTE_ADDR");

char *ContentLength = getenv("CONTENT_LENGTH"); char *QueryString = getenv("QUERY_STRING");

//вычисляем длину данных — переводим строку в число int NumBytes = atoi(ContentLength);

//выделяем в свободной памяти буфер нужного размера char *Data = (char *)malloc(NumBytes + 1);

//читаем данные из стандартного потока ввода fread(Data, 1, NumBytes, stdin);

//добавляем нулевой код в конец строки

//(в Си нулевой код сигнализирует о конце строки) Data[NumBytes] = 0;

52

Часть I. Основы Web-программирования

// выводим заголовок printf("Content-type: text/html\n\n");

// выводим документ printf("<html><body>");

printf("<h1>Здравствуйте. Мы знаем о вас все!</h1>"); printf("Ваш IP-адрес: %s<br>",RemoteAddr); printf("Количество байтов данных: %d<br>",NumBytes); printf("Вот параметры, которые Вы указали: %s<br>",Data); printf("А вот то, что мы получили через URL: %s",

QueryString);

printf("</body></html>");

}

Странслируем этот сценарий и запишем то, что получилось, под именем script.cgi в каталог, видимый извне как /cgi-bin/. Откроем в браузере следующий HTMLфайл с формой:

Листинг 3.4. POST-форма

<html><body>

<form action=/cgi-bin/script.cgi?param=value method=post> Name1: <input type=text name="name1"><br>

Name2: <input type=text name="name2"><br> <input type=submit value="Запустить сценарий!"> </form>

</body></html>

Теперь, если набрать в полях ввода какой-нибудь текст и нажать кнопку, получим HTML-страницу, сгенерированную сценарием, например, следующего содержания:

Здравствуйте. Мы знаем о вас все!

Ваш IP-адрес: 136.234.54.2

Количество байтов данных: 23

Вот параметры, которые Вы указали: name1=Vasya&name2=Petya А вот то, что мы получили через URL: param=value

Как можно заметить, обработка метода POST устроена сложнее, чем GET. Тем не менее, метод POST используется чаще, особенно если нужно передавать большие объемы данных или "закачивать" файл на сервер (эта возможность также поддерживается протоколом HTTP и HTML).

Глава 3. CGI изнутри

53

Расшифровка URL-кодированных данных

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

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

Мы не можем сначала все данные (например, полученные из стандартного по- тока ввода) декодировать, а уж потом работать с ними (в частности, разбивать по месту вхождения символов & и =). Действительно, вдруг после перекоди- ровки появятся символы & и =, которые могут быть введены пользователем? Как мы тогда узнаем, разделяют ли они параметры или просто набраны с кла- виатуры? Очевидно, никак. Поэтому такой способ нам не подходит, и придется работать с каждым значением отдельно, уже после разделения строки на час- ти.

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

Листинг 3.5. Функция URL-декодирования

//Функция преобразует строку данных st в нормальное представление.

//Результат помещается в ту же строку, что была передана в параметрах. void UrlDecode(char *st) {

char *p=st; // указывает на текущий символ строки char hex[3]; // временный буфер для хранения %XX

int code;

// преобразованный код

//запускаем цикл, пока не кончится строка (то есть, пока не

//появится символ с кодом 0, см. ниже)

do {

// Если это %-код ...

if(*st == '%') { // тогда копируем его во временный буфер hex[0]=*(++st); hex[1]=*(++st); hex[2]=0;

54

Часть I. Основы Web-программирования

//переводим его в число sscanf(hex,"%X",&code);

//и записываем обратно в строку

*p++=(char)code;

//указатель p всегда отмечает то место в строке, в которое

//будет помещен очередной декодированный символ

}

//иначе, если это "+", то заменяем его на " " else if(*st=='+') *p++=' ';

//а если не то, ни другое — оставляем как есть else *p++=*st;

}while(*st++!=0); // пока не найдем нулевой код

}

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

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

Листинг 3.6. Получение POST-данных с URL-декодированием

#include <stdio.h> #include <stdlib.h>

void main(void) {

//получаем значения переменных окружения char *RemoteAddr = getenv("REMOTE_ADDR");

char *ContentLength = getenv("CONTENT_LENGTH");

//выделяем память для буфера QUERY_STRING

char *QueryString = malloc(strlen(getenv("QUERY_STRING")) + 1);

//копируем QUERY_STRING в созданный буфер strcpy(QueryString, getenv("QUERY_STRING"));

//декодируем QUERY_STRING UrlDecode(QueryString);

//вычисляем количество байтов данных — переводим строку в число int NumBytes = atoi(ContentLength);

Глава 3. CGI изнутри

55

//выделяем в свободной памяти буфер нужного размера char *Data = (char*)malloc(NumBytes + 1);

//читаем данные из стандартного потока ввода fread(Data, 1, NumBytes, stdin);

//добавляем нулевой код в конец строки

//(в Си нулевой код сигнализирует о конце строки) Data[NumBytes] = 0;

//декодируем данные (хоть это и не совсем осмысленно, но выполняем

//сразу для всех POST-данных, не разбивая их на параметры) UrlDecode(Data);

//выводим заголовок

printf("Content-type: text/html\n\n"); // выводим документ

printf("<html><body>");

printf("<h1>Здравствуйте. Мы знаем о Вас все!</h1>"); printf("Ваш IP-адрес: %s<br>",RemoteAddr); printf("Количество байтов данных: %d<br>", NumBytes); printf("Вот параметры, которые Вы указали: %s<br>",Data); printf("А вот то, что мы получили через URL: %s",

QueryString);

printf("</body></html>");

}

Обратите внимание на строки, выделенные жирным шрифтом. Теперь мы используем промежуточный буфер для хранения QUERY_STRING. Зачем? Попробуем поставить все на место, т. е. не задействовать промежуточный буфер, а работать с переменной окружения напрямую, как это было в листинге 3.5. Тогда в одних операционных системах этот код будет работать прекрасно, а в других — генерировать ошибку общей защиты, что приведет к немедленному завершению работы сценария. В чем же дело? Очень просто: переменная QueryString ссылается на значение переменной окружения QUERY_STRING, которая расположена в системной области памяти, а значит, доступна только для чтения. В то же время функция UrlDecode(), как я уже замечал, помещает результат своей работы в ту же область памяти, где находится ее параметр, что и вызывает ошибку.

Чтобы избавиться от указанного недостатка, мы и копируем значение переменной окружения QUERY_STRING в область памяти, доступной сценарию для записи — например, в какой-нибудь буфер, а потом уже преобразовываем его. Что и было сделано в последнем сценарии.

56

Часть I. Основы Web-программирования

Несколько однообразно и запутанно, не так ли? Да, пожалуй. Но, как говорится, "это даже хорошо, что пока нам плохо" — тем больше будет причин предпочитать PHP другим языкам программирования (так как в PHP эти проблемы изжиты как класс).

Формы

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

Все элементы формы по именам соответствующих им тэгов делятся на 3 категории:

r<input...>

r<textarea...>...</textarea>

r<select...><option...>...</option>...</select>

Каждый из этих тэгов, конечно, может иметь имя. Ранее уже упоминалось, что пары имя=значение перед тем, как отправятся сценарию, будут разделены в строке параметров символом &. Кроме того, следует учитывать, что для тех компонентов формы, у тэгов которых не задан параметр name, соответствующая строка имя=значение передана не будет. Это ограничение введено для того, чтобы можно было в форме определять служебные элементы, которые не будут посылаться сценарию. Например, в их число входят кнопки (подтверждения отправки или обычные, используемые при программировании на JavaScript) и т. д. Так, создадим форму:

<form action=script.cgi>

... какие-то поля ...

<input type=submit value="Go!"> </form>

Несмотря на то, что кнопка Go! формально является полем ввода, ее данные не будут переданы сценарию, поскольку у нее отсутствует параметр name.

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

<form action=script.cgi>

<input type=submit name="submit" value="Go!"> </form>

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

Глава 3. CGI изнутри

57

или же простым указанием его URL (для этого достаточно проанализировать командную строку сценария и определить, присутствует ли в ней атрибут submit).

В принципе, все тэги, за исключением <select>, с точки зрения сценария выглядят одинаково — как один они генерируют строки вида имя=значение, где имя — то, что задано в атрибуте name, а значение — либо текст, введенный пользователем, либо содержимое атрибута value (например, так происходит у независимых и зависимых переключателей, которые мы вскоре рассмотрим).

Тэг <input> — различные поля ввода

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

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

Текстовое поле (text)

<input type=text name=имя [value=значение] [size=размер] [maxlen=число]

>

Создает поле ввода текста размером примерно в size знакомест и максимально допустимой длиной maxlen символов (то есть пользователь сможет ввести в нем не больше этого количества символов).

Не советую, тем не менее, в программе на Си полагаться, что придет не боль-

ше maxlen символов и выделять для их получения буфер фиксированного размера. Дело в том, что злоумышленник вполне может запустить ваш сцена- рий в обход стандартной формы (содержащей "правильный" тэг <input>) и задать большой объем данных, чтобы этот буфер переполнить известный прием взлома недобросовестно написанных программ.

Если задано значение атрибута value, то в текстовом поле будет изначально отображена указанная строка.