Денис Нивников

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

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

Что такое РНР?

В том виде, в которым PHP знаком программистам сейчас, этот язык появился в 1997 г., хотя аббревиатура PHP появилась намного раньше - в 1995 г. был подготовлен проект PHP/FI (Personal Home Page/Forms Interpreter), представляющий собой набор Perl-сценариев для подсчета обращений к размещенному в Интернете резюме автора - Расмуса Лердорфа (Rasmus Lerdorf). Позже автор значительно расширил функциональность PHP/FI, полностью переписав приложение на С. Новая версия значительно больше походила на современный РНР, в нее была включена поддержка соединений с базами данных и средства разработки простейших динамических страниц. В ноябре 1997 г. была практически готова вторая версия PHP/FI, однако в то же время два программиста - Энди Гутманс (Andi Gutmans) и Зив Зуразки (Zeev Suraski) - разработали третью версию PHP (теперь именно РНР, без FI - расшифровывается как PHP Hypertext Processor), полностью переписав его ядро. Хотя авторы у третьей версии и были другие, по договоренности с Расмусом ее решено было считать преемником второй версии PHP/FI, и работы над последней были прекращены.

Третья версия PHP завоевала популярность благодаря широкому набору стандартных функций, совместимости с большинством распространенных протоколов и баз данных. Гибкий, С-образный язык сценариев, встраиваемых непосредственно в тело HTML-документа, простота и удобство работы с массивами и возможность использования объектно-ориентированного программирования (ООП) сделали РНР одной из самых распространенных технологий Интернета. Безусловно, немаловажен и тот факт, что с самого начала разработки РНР был проектом с открытым исходным кодом. Благодаря этому РНР остается одной из самых доступных технологий построения динамических сайтов, будучи при этом и одной из самых мощных.

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

Почему РНР?..

…а не, например, Perl, ASP, ColdFusion или еще какая-нибудь серверная технология? В чем преимущества РНР?

Во-первых, РНР - технология мультиплатформная. В настоящее время с Web-сайта php.net можно загрузить скомпилированные версии для Win32, MacOS и RISC OS; кроме того, PHP входит в состав инсталляционных пакетов большинства Linux-систем. Все это обеспечивает высокую переносимость проектов.

Во-вторых, PHP - это проект с открытым исходным кодом. Это означает более динамичное совершенствование продукта и исправление ошибок.

В-третьих, общая стоимость проекта, созданного на базе Unix/Linux + Apache + PHP + MySQL/PostgreSQL, значительно ниже, чем у большинства других средств разработки при той же надежности и мощности. Упомянутые базы данных и Web-сервер, так же, как и РНР, входят в инсталляционные пакеты Linux, а значит, обеспечена высокая скорость развертывания Web-сервера. В то же время это вовсе не означает, что РНР ориентирован на небольшие бесплатные базы данных - в нее включены функции взаимодействия с десятком самых распространенных СУБД, среди которых Microsoft SQL и Oracle.

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

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

Особенности проектирования

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

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

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

Пример приложения: служба почтовых рассылок

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

Описывая процесс создания службы рассылки, будем предполагать, что на нашем сервере уже установлена БД MySQL, Apache и PHP как модуль Web-сервера.

Для начала определим минимальный набор функций, выполняемый нашей программой. Она должна:

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

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

CREATE DATABASE maillist;
USE maillist;
GRANT ALL PRIVILEGES ON maillist.*
	TO maillist@localhost IDENTIFIED BY 'bytemag';
CREATE TABLE `subscriber` (
`id` SMALLINT UNSIGNED NOT NULL
	AUTO_INCREMENT PRIMARY KEY,
`groupid` SMALLINT UNSIGNED NOT NULL,
`name` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`descr` VARCHAR(255)
);
CREATE TABLE `list` (
`id` SMALLINT UNSIGNED NOT
	NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`descr` VARCHAR(255)
);
CREATE TABLE `log` (
`id` SMALLINT UNSIGNED NOT
	NULL AUTO_INCREMENT PRIMARY KEY,
`filldate` TIMESTAMP NOT NULL,
`subj` VARCHAR(255) NOT NULL,
`size` SMALLINT UNSIGNED NOT NULL,
`groupid` SMALLINT UNSIGNED NOT NULL
);

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

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

db.php
<?
// Устанавливаем соединение с базой
// данных
$host = "localhost";
$login = "maillist";
$passwd = "bytemag";
$conn_error = "<b>Невозможно
				подключиться к базе данных!</b>";
$mysql_link = @mysql_connect
				($host, $login, $passwd)
				or exit ($conn_error);
@mysql_select_db('maillist')
				or exit ($conn_error);
function insert_into_db($tb_name, $vals)
	{
	if ($vals)
	{
		$fields = '';
		$values = '';
		while (list($col, $val) = each($vals))
		{
			if ($fields) {
				$fields .= ", ";
				$values .= ", ";
			};
			$fields .= $col;
			$values .= "'".$val."'";
		};
		$query = "REPLACE INTO $tb_name
				($fields) VALUES ($values)";
		return mysql_query($query);
	};
};
?>

Процедуру подключения к БД имеет смысл вынести в отдельный сценарий и подключать в других сценариях с помощью функции require() - это дает возможность легко перенастраивать переменные подключения в нескольких независимых сценариях. Кроме того, мы подготовили процедуру insert_into_db() для сохранения данных в БД. В качестве параметров этой процедуре передается имя таблицы и ассоциативный массив значений, ключами которого служат названия полей в таблице. Оператор replace в запросе SQL, обнаружив в таблице уже существующую строку с аналогичными передаваемым значениями уникальных полей, заменит ее новой - в отличие от оператора insert, сообщающего в этом случае об ошибке. Таким образом, применение replace позволит нам одинаково легко вставлять новые строки и заменять отредактированные.

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

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

group.php
<?
require('db.php');
if ($HTTP_POST_VARS)
{
	if (insert_into_db('list',
				$HTTP_POST_VARS))
	{
		$saved_ok = "<i>сохранено</i>";
	}
	else
	{
		$saved_ok = "<b>Ошибка! Данные
				не сохранены!</b>";
	};
};
if ($HTTP_GET_VARS['id'])
{
	$query = "SELECT * FROM list WHERE
				id='".$HTTP_GET_VARS['id']."'";
	$result = mysql_query($query);
	$list = mysql_fetch_array($result);
};
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD
				HTML 4.0 Transitional//EN">
<html>
<head>	<title>Список рассылки</title></head>
<body>
[ <a href="index.php">в начало</a> ]
				<?=$saved_ok?>
<hr>
<form action="group.php" method="post">
<input type="hidden" name="id" value='
				<?=$list['id']?>'>
<table border="0" cellpadding="3"
				cellspacing="0">
<tr>
	<td>Название:</td>
	<td><input type="text" size="25"
				name="name" value='<?=
				$list['name']?>'></td>
</tr>
<tr>
	<td>Примечание:</td>
	<td><input type="text" size="25"
				name="descr" value='<?=
				$list['descr']?>'></td>
</tr>
<tr>
	<td colspan=2><input type=submit
				value="подтвердить"></td>
</tr>
</table>
</form>
</body>

Начинаем сценарий с подключения файла db.php и устанавливаем соединение с базой данных. Оператор require(), в отличие от оператора include(), прервет работу сценария, если произойдет ошибка чтения файла, - это гарантирует, что дальнейшие инструкции будут выполнены только в том случае, когда соединение с базой прошло успешно.

Затем мы проверяем, не переданы ли сценарию какие-либо данные. Параметры, передаваемые методом POST, содержатся в ассоциативном массиве HTTP_POST_VARS, а методом GET - в HTTP_GET_VARS. Здесь необходимо отметить, что при определенных настройках (в том числе и принимаемых по умолчанию) PHP обладает замечательным свойством, значительно облегчающим программирование: при инициализации сценария эти массивы "разбираются" на переменные. Иначе говоря, программисту доступны идентичные переменные, например, $HTTP_POST_VARS['var'] и $var. Несмотря на подкупающее удобство этого подхода, мы не рекомендовали бы использовать данную возможность при разработке серьезных проектов, так как это, во-первых, отрицательно сказывается на переносимости приложения, а во-вторых, при недостаточно аккуратном программировании делает сценарий более уязвимым для злоумышленников. Не безупречен и предложенный нами вариант - для максимальной защищенности сценария следовало бы добавить проверку источника полученных данных (getenv('HTTP_REFERER')). Кроме того, при работе с базой данных следует использовать функции addslashes() и stripslashes(), а при выводе данных в HTML - функции htmlspecialchars() и htmlentities().

После заполнения формы и нажатия кнопки "подтвердить" данные будут переданы сценарию и сохранены им с помощью уже описанной функции insert_into_db(). Сообщение о результатах записи заносится в переменную $saved_ok, а форма отображается незаполненной. Если сценарий был вызван с параметром id в строке URL, то в базе ищется строка с соответствующим номером и создается ассоциативный массив $list, значениями которого потом заполняется форма.

Сценарий, используемый для редактирования данных о подписчике, почти не отличается от сценария редактирования списков рассылки. Единственное отличие - функция groups_list(), которая создает выпадающий список и заносит в него названия списков рассылки.

subscriber.php
<?
require('db.php');
function groups_list()
{
	$result = mysql_query("SELECT id,
					name FROM `list` ORDER BY id");
	if (mysql_num_rows($result))
	{
		while ($next = mysql_fetch_array
					($result))
		{
			if ($next['id'] ==
					$subscriber['groupid'])
			{
				echo "<option value='".$next['id']
				."' selected>".$next['name'];
			}
			else
			{
				echo "<option value='".$next['id'].
					"'>".$next['name'];
			};
		};
	};
};
if ($HTTP_POST_VARS)
{
	if (insert_into_db('`subscriber`',
					$HTTP_POST_VARS))
	{
		$saved_ok = "<i>сохранено</i>";
	};
};
if ($HTTP_GET_VARS['id'])
{
	$result = mysql_query("SELECT *
					FROM `subscriber` WHERE id='"
	.$HTTP_GET_VARS['id']."'");
	$subscriber = mysql_fetch_array($result);
};
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD
					HTML 4.0 Transitional//EN">
<html>
<head><title>Подписчик</title></head>
<body>
[ <a href="index.php">в начало</a> ]
					<?=$saved_ok?>
<hr>
<form action="subscriber.php"
					method="post">
<input type=hidden name=id value=<?=
					$subscriber['id']?>>
<table border="0" cellpadding="3"
				cellspacing="0">
<tr>
	<td>Имя:</td>
	<td><input type="text" size="25"
					name="name" value='<?=
					$subscriber['name']?>'></td>
</tr>
<tr>
	<td>e-mail:</td>
	<td><input type="text" size="25"
					name="email" value='<?=
					$subscriber['email']?>'></td>
</tr>
<tr>
	<td>Примечание:</td>
	<td><input type="text" size="25"
					name="descr" value='<?=
					$subscriber['descr']?>'></td>
</tr>
<tr>
	<td>Группа:</td>
	<td>
	<select name="groupid">
<? groups_list(); ?>
	</select>
	</td>
</tr>
<tr>
	<td colspan=2><input type=submit
					value="подтвердить"></td>
</tr>
</table>
</form>
</body>

Основную функцию нашего приложения - рассылку сообщений по списку адресов выполняет сценарий message.php. Для отправки сообщений в нем используется стандартная функция PHP mail(). Перед вызовом этой функции создается список адресов подписчиков (переменная $to), входящих в выбранные группы. Нередко авторы почтовых рассылок допускают ошибку, помещая список адресов в поле "To:" или "Cc:", - в результате этого каждый адресат получает полный список подписчиков и адресов их электронной почты. Избежать этого достаточно легко - список адресов необходимо поместить в поле "Bcc:" (скрытая копия).

message.php
<?
include('db.php');
function groups()
{
	$query = "select id, name from list";
	$result = mysql_query($query);
	if (mysql_num_rows($result))
	{
		while ($next = mysql_fetch_array
					($result))
		{
			echo "<input type=\"checkbox\"
					name=\"group[]\" value=\"".$next
					['id']."\">".$next['name']."<br>\n";
		};
	};
};
function sendmail($subj, $text, $lists)
{	
	while (list($idx, $list) =
					each($lists))
	{
		$query = "select name, email
					from subscriber
					where groupid='".$list."'";
		$result = mysql_query($query);
		if (mysql_num_rows($result))
		{
			$to = "";
			while ($subscriber =
					mysql_fetch_array($result))
			{
				$to .= (($to) ? ",\n\t" : "");
				$to .= "\"".$subscriber['name']."\"
					<".$subscriber['email'].">";
			};
		};
		if ($to && $text)
		{
			global $send_ok;
			if (@mail("\"MailList\"", $subj,
					$text, "Bcc: ".$to."\nContent-Type:
					text/plain; charset=windows-1251"))
			{
				$send_ok = "<i>Сообщение
					отправлено!</i>";
				insert_into_db("log",
					array('subj' => $subj, 'size' =>
					strlen($text), 'groupid' => $list));
			}
			else
			{
				$send_ok = "<b>Ошибка при отправке
					сообщения!</b>";
			};
		};
	};
};
if ($HTTP_POST_VARS)
{
	if ($HTTP_POST_VARS['group'])
	{
		sendmail($HTTP_POST_VARS['subj'],
					$HTTP_POST_VARS['text'],
					$HTTP_POST_VARS['group']);
	};
};
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD
					HTML 4.0 Transitional//EN">
<html>
<head><title>New Message</title></head>
<body>
[ <a href="index.php">в начало</a> ]
					<?=$send_ok?>
<hr>
<form action="message.php" method="post">
<table border=0 cellpadding=0
					cellspacing=5>
	<tr>
		<td>Тема:</td>
		<td></td>
	</tr>
	<tr>
		<td><input type=text size="40"
					name="subj"></td>
		<td></td>
	</tr>
	<tr>
		<td>Текст:</td>
		<td></td>
	</tr>
	<tr>
		<td valign="top"><textarea cols="50"
					rows="20" name="text"></textarea></td>
		<td valign="top" rowspan="2"><?
					groups(); ?></td>
	</tr>
	<tr>
		<td align="right"><input type="submit"
					value="отправить"></td>
	</tr>
</table>
</form>
</body>
</html>

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

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

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

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

log.php
<?
include('db.php');
function log_records()
{
	$query = "select *, DATE_FORMAT
					(filldate, \"%H:%i %d/%m/%Y\") as
					date from log order by filldate desc";
	$result = mysql_query($query);
	if (mysql_num_rows($result))
	{
		echo "<tr><th>Дата и время</th>
					<th>Тема рассылки</th>
					<th>Размер сообщения</th></tr>";
		while ($next =
					mysql_fetch_array($result))
		{
			echo "<tr><td>".$next['date']."</td>
					<td align=right>".$next['subj']."
					</td><td align=right>
					".$next['size']."</td></tr>";
		};
	}
	else
	{
		echo "<tr><td><b>Журнал рассылок
					пуст!</b></td></tr>";
	};
};
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD
					HTML 4.0 Transitional//EN">
<html>
<head><title>Журнал рассылок</title></head>
<body>
[ <a href="index.php">в начало</a> ]
<hr>
<table border=0 cellpadding=3 cellspacing=0>
<? log_records(); ?>
</table>
</body>
</html>
index.php
<?
require('db.php');
function user_list()
{
	$query = "SELECT s.id, s.name, s.email,
					s.groupid, g.name AS groupname
					FROM `subscriber` AS s, `list` AS g "
		."WHERE s.groupid=g.id ORDER BY
				groupname, name";
	$result = mysql_query($query);
	if (mysql_num_rows($result))
	{
		echo "<table border=0 cellspacing=0
					cellpadding=3>\n"
			."<tr><th>Имя:</th><th>Адрес:</th>
					<th>Группа:</th><th></th></tr>\n";
		while ($next = mysql_fetch_array($result))
		{
			echo "<tr><td>".$next['name']."</td>
					<td><a href=mailto:
					".$next['email'].">"
				.$next['email']."</a></td><td>"
					.$next['groupname']."</td>
					<td>[<a href=subscriber.php?id=
					".$next['id'].">редактировать</a>]
					</td></tr>";
		};
		echo "</table>";
	}
	else
	{
		echo "Не зарегистрировано ни одного
					подписчика!";
	};
};
$result = mysql_query("SELECT *
					FROM `list`");
if (mysql_num_rows($result))
{
	$menu = "[ <a href=message.php>Новое
					сообщение</a> ]
					[ <a href=subscriber.php>Добавить
					подписчика</a> ] [ <a href=group.php>
					Добавить список рассылки</a> ]
					[ <a href=log.php>Журнал
					рассылок</a> ]<hr>";
	$groups = "<table border=0 cellpading=2
					cellspacing=0>";
	while ($next =
					mysql_fetch_array($result))
	{
		$groups .= "<tr><td>".$next['name']."
					</td><td>[ <a href=group.php?id=
					".$next['id'].">редактировать</a> ]
					</td></tr>";
	};
	$groups .= "</table>";
}
else
{
	$menu = "<b>Не найдено ни одной группы
					подписчиков! <br>"
		."Пожалуйста, [ <a href=group.php>
					зарегистрируйте</a> ] хотя бы
					одну, прежде чем начать работать
					с системой!<br></b>";
};
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD
					HTML 4.0 Transitional//EN">
<html>
<head><title>Система управления списками
					рассылки</title></head>
<body>
<center>
<?=$menu?>
<? if ($groups) :?>
<table border=1 cellpadding=3
					cellspacing=0>
	<tr>
		<th>Подписчики:</th>
		<th>Группы:</th>
	</tr>
	<tr>
		<td valign="top"><? user_list(); ?>
					</td>
		<td valign="top"><?=$groups?></td>
	</tr>
</table>
<? endif; ?>
</center>
</body>
</html>