Создание встроенного модуля CMS. Часть 3

Материал из Школьный портал: справочника
Перейти к: навигация, поиск

В статье подразумевается, что вы уже прочитали предыдущие статьи.

Рассматривается вопрос разделения модуля на части для посетителей сайта и для администратора.

Статья пишется по горячим следам разработки модуля гостевой книги. Поэтому сразу сделаем допущение, что внутреннее имя у него guestbook.

Самый простой вариант

Вывод разных строк в зависимости от того, сайт это или админка.

sub guestbook()
{
	if ( defined param('edt') )
	{
		print "Hello, admin interface!";
	}
	else
	{
		return "Hello, site!";
	}
}

Обратите внимание, для сайта всё ещё действует правило, обязывающее возвращать данные (return). А в админке следует использовать print. Разумеется, это никак не мешает накапливать данные в переменных и под конец выдать их в нужном порядке.

Собственная таблица для произвольных данных

Сделаем таблицу:

CREATE TABLE GUESTBOOK (
    ID         INTEGER NOT NULL,
    NAME       VARCHAR(128),
    TEXT       BLOB SUB_TYPE 1 SEGMENT SIZE 80,
    POST_DATE  TIMESTAMP,
    published  SMALLINT DEFAULT 0,
    OWNER      INTEGER NOT NULL
);

Пройдёмся по полям:

  • ID — идентификатор объекта, Это общая рекомендация проектирования баз данных, рекомендуем всегда создавать его для любых, это намного упростит вам дальнейшую разработку.
  • NAME, TEXT, POST_DATE — это уже произвольные поля, которые могут быть какими угодно для потребностей вашего нового модуля. Для простейшего примера гостевой книги их будет достаточно.
  • published — признак опубликованности. Для модуля новостей это может быть признаком черновика; для навигации это может быть галочкой, включающей пункт в меню; для гостевой книги это будет признак одобрения модератором. 0 будет означать "сообщение на рассмотрении", 1 — "одобрено к показу на сайте".
  • OWNER — владелец сайта. 6 у нас всегда школльный сайт, остальное — блоги. Об этом важно помнить при разработке всех модулей, которые будут писать и читать из базы. Вам не нужно каждый раз выяснять владельца, CMS уже заботится об этом. Когда выполняется модуль, глобальная переменная $owner уже содержит какое-то число, равное идентификатору авторизованного пользователя в админке либо автора сайта. Вам остаётся только использовать его для вставки в базу данных для этого владельца или выбирать из базы данные только для такого владельца. Таким образом реализуется хранение данных для сайта и блогов в одной таблице. Ниже будет приведён пример использования владельца. $owner всегда определён, потому что выполнение модуля в режиме админки без авторизации невозможно, выполнение модуля на сайте без определения владельца тоже невозможно (будет выдана ошибка 404, ни один модуль не выполнится).

Добавление данных

Форма ввода сообщения в самом простом случае выглядит так:

<form class="guestbook" method="post">
	Имя:   <input type="text" name="name">
	Текст: <textarea name="text"></textarea>
	<button type="submit">Отправить</button>
</form>

Пример реакции на отправку этой формы:

my $name = param('name');
my $text = param('text');
sqlb("INSERT INTO GUESTBOOK
	(NAME, TEXT, POST_DATE, published, owner)
	VALUES
	(?,    ?,    NOW()),    0,         $owner",
	$name, $text
);

Отступление о безопасности

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

В портале уже есть встроенные методы проверки данных. Например, прочитайте функции sec_* в common.pl.

Использовать их просто:

# обязательная проверка
my $id = param('id');
sec_gt0($id);
 
# проверка параметра, которого может и не быть
my $opt = param('opt');
sec_gt0($opt) if defined($opt);

В common.pl с версии 5.2 доступна функция, упрощающая безопаное чтение данных для записи в базу и последующего показа на сайте:

# пример использования
my $a = param_dexss_lim('a', 64);

Смысл: прочитать параметр по имени a, отразить XSS, взять не более 64 символа.

Чтение данных

my $query = sqlpcms("select * from guestbook where published = 1 and owner = $owner");
# sqlp выполняет DBI::prepare, затем DBI::execute
# $query — это результат функции DBI::prepare

Обратите внимание на условие выборки. В выдачу попадут только опубликованные материалы и только принадлежащие владельцу сайта.

Отладка

Наши удобные обёртки (а именно функции sql* в common.pl) скрывают от нас DBI с его громоздкими конструкицями, предназначенными для обработки ошибок.

Стоит вам допустить ошибку в запросе, как функция sql* выбросит сообщение, понятное постороннему человеку.

Sql error.png

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

Итак: та же самая ошибка, в режиме отладки:

Sql error and stack trace.png

Как это сделать: открываем sp.conf, пишем:

DEBUG = 0

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

Здорово, правда? :)

Поехали дальше.

Разделение на выдачу контента и принятие формы

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

sub guestbook()
{
	if ( defined param('edt') ) # админка
	{
		print "Hello, admin interface!";
	}
	else # сайт
	{
		# Получить данные
		if ( $ENV{'REQUEST_METHOD'} eq 'GET' )
		{
			my $q = sqlpcms("select * from guestbook where published = 1 and owner = $owner");
			my %hash; $q->bind_columns( \( @hash{ @{$q->{NAME_lc} } } ));
			my $messages = '';
 
			while ($q->fetch)
			{
				$messages .= qq[
					<p>Имя: $hash{name}</p>
					<p>Дата: $hash{post_date}</p>
					<p>Сообщение: $hash{text}</p>];
			}
			return "<h2>Сообщения</h2>$messages";
		}
		# Записать данные
		elsif ( $ENV{'REQUEST_METHOD'} eq 'POST' )
		{
			my $name = param_dexss_lim('name', 128);
			my $text = param_dexss_lim('text', 1500);
			sqlbcms("INSERT INTO GUESTBOOK
				(NAME, TEXT, POST_DATE, published, owner)
				VALUES
				(?,    ?,    NOW()),    0,         $owner",
				$name, $text
			);
			return "Данные записаны.";
		}
	}
}

Что нового в коде: есть две ветки. Одна читает из базы (формируя HTML-вёрстку), другая пишет в базу принятое из формы и сообщает, что "Данные записаны.".

Шаблоны

TODO: tpl tpl_str

Админка

TODO список комментов, кнопки одобрить, удалить

Как понять, что сайт просматривает авторизованный пользователь

TODO