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

Материал из Школьный портал: справочника
Перейти к: навигация, поиск
(Собственная таблица для произвольных данных)
(Пути к шаблонам)
 
(не показаны 84 промежуточные версии этого же участника)
Строка 1: Строка 1:
Статья подразумевает, что вы уже прочитали [[Создание встроенного модуля CMS|первую часть]].
+
Внимание! Модуль уже реализован в виде встроенного в Портале версии 5.3. Оставляем статью в качестве обучающего материала.
  
Рассматривается вопрос разделения модуля на часть для посетителей и часть для администратора сайта.
+
В статье подразумевается, что вы уже прочитали предыдущие статьи серии "Создание встроенного модуля CMS".
  
Статья пишется по горячим следам разработки модуля гостевой книги. Поэтому сразу сделаем допущение, что внутреннее имя у него '''guestbook'''.
+
Рассматривается вопрос разделения модуля на части для посетителей сайта и для владельца сайта (авторизованного), чтение и добавление контента на сайт, использование шаблонов.
 +
 
 +
Внутреннее имя модуля — '''guestbook'''.
  
 
== Самый простой вариант ==
 
== Самый простой вариант ==
Строка 48: Строка 50:
 
=== Добавление данных ===
 
=== Добавление данных ===
  
Форма ввода сообщения ничего необычного не содержит, но там нет Когда пользователь отправляет форму, происходит запись в базу.
+
Форма ввода сообщения в самом простом случае выглядит так:
Пример кода:
+
 
 +
<source lang="html4strict">
 +
<form class="guestbook" method="post">
 +
Имя:  <input type="text" name="name">
 +
Текст: <textarea name="text"></textarea>
 +
<button type="submit">Отправить</button>
 +
<input type="hidden" name="mod" value="guestbook">
 +
</form>
 +
</source>
 +
 
 +
Единственная особенность: скрытое поле по имени '''mod''' со значением guestbook. Значение определяет, какой модуль получит данные формы. В данном случае это тот же самый модуль, которым я пишу, его внутреннее имя — guestbook.
 +
 
 +
Пример реакции на отправку этой формы:
  
 
<source lang="perl">
 
<source lang="perl">
 +
my $name = param('name');
 +
my $text = param('text');
 
sqlb("INSERT INTO GUESTBOOK
 
sqlb("INSERT INTO GUESTBOOK
 
(NAME, TEXT, POST_DATE, published, owner)
 
(NAME, TEXT, POST_DATE, published, owner)
 
VALUES
 
VALUES
(?,    ?,    NOW()),   0,        $owner",
+
(?,    ?,    'NOW',     0,        $owner)",
 
$name, $text
 
$name, $text
 
);
 
);
 
</source>
 
</source>
 +
 +
=== Отступление о безопасности ===
 +
 +
Забрать данные из запроса пользователя с помощью функции param() и сразу использовать их небезопасно. В этом цикле статей используется простейший код, не содержащий проверок на корректность данных. Для ознакомления с методами защиты данных настоятельно рекомендуется прочитать статью [http://dj-andrey.ru/articles/perl-web-application-security Защита веб-приложений на Perl].
 +
 +
В портале уже есть встроенные методы проверки данных. Например, прочитайте функции sec_* в common.pl.
 +
 +
Использовать их просто:
 +
 +
<source lang="perl">
 +
# обязательная проверка
 +
my $id = param('id');
 +
sec_gt0($id);
 +
 +
# проверка параметра, которого может и не быть
 +
my $opt = param('opt');
 +
sec_gt0($opt) if defined($opt);
 +
</source>
 +
 +
В common.pl с версии 5.2 доступна функция, упрощающая безопаное чтение данных для записи в базу и последующего показа на сайте:
 +
 +
<source lang="perl">
 +
# пример использования
 +
my $a = param_dexss_lim('a', 64);
 +
</source>
 +
 +
Смысл: прочитать параметр по имени a, отразить XSS, взять не более 64 символа.
  
 
=== Чтение данных ===
 
=== Чтение данных ===
  
 
<source lang="perl">
 
<source lang="perl">
my $query = sqlp("select * from guestbook where published = 1 and owner = $owner");
+
my $query = sqlpcms("select * from guestbook where published = 1 and owner = $owner");
 
# sqlp выполняет DBI::prepare, затем DBI::execute
 
# sqlp выполняет DBI::prepare, затем DBI::execute
 
# $query — это результат функции DBI::prepare
 
# $query — это результат функции DBI::prepare
Строка 69: Строка 112:
  
 
Обратите внимание на условие выборки. В выдачу попадут только опубликованные материалы и только принадлежащие владельцу сайта.
 
Обратите внимание на условие выборки. В выдачу попадут только опубликованные материалы и только принадлежащие владельцу сайта.
 +
 +
== Отладка ==
 +
 +
Наши удобные обёртки (а именно функции sql* в common.pl) скрывают от нас DBI с его громоздкими конструкицями, предназначенными для обработки ошибок.
 +
 +
Стоит вам сформировать неверный запрос, как функция sql* выбросит сообщение, понятное постороннему человеку.
 +
 +
[[Файл:sql_error.png]]
 +
 +
Но нам-то это не помогает понять, что произошло, а главное где. У портала есть режим отладки, который сильно помогает при разработке, который очень рекомендуем включить у себя для удобства отладки, но крайне не рекомендуем включать на боевом сервере, поскольку раскрывается множество технических подробностей, которые: во-первых, никак не помогут посетителям, если что-то случилось; во-вторых, выдадут лишнюю информацию злоумышленникам.
 +
 +
Итак: та же самая ошибка, в режиме отладки:
 +
 +
[[Файл:sql_error_and_stack_trace.png]]
 +
 +
Как это сделать: открываем [[Файл конфигурации | sp.conf]], пишем:
 +
<pre>
 +
DEBUG = 1
 +
</pre>
 +
 +
Теперь все сообщения об ошибках обращения к базе будет с дампом запроса и stack trace-ом (функции '''error''' и '''stop''' тоже поднимут стектрейс), кроме того, вверху страницы вы увидите служебную информацию и времени выполнения скрипта (при отсутствии ошибки итоговое, а при наличии стопа или ошибка время от начала до ошибки), количество SQL-запросов и текст всех запросов по клику на их количестве, в котором указывается время выполнения, предупреждения об идентичных запросах и пометка долго выполнявшихся запросов.
 +
 +
Здорово, правда? :)
 +
 +
Поехали дальше.
 +
 +
== Разделение на выдачу контента и принятие формы ==
 +
 +
Приведённый ниже код с учётом формы отправки сообщения пока не затрагивает вопрос поведения модуля в админке — мы всё ещё занимаемся частью кода для сайта.
 +
 +
<source lang="perl">
 +
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><form>...</form>Сообщения: $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 "Данные записаны.";
 +
}
 +
}
 +
}
 +
</source>
 +
 +
Что нового в коде: есть две ветки. Одна читает из базы (формируя HTML-вёрстку), другая пишет в базу принятое из формы и сообщает, что "Данные записаны.".
 +
 +
HTML-вёрстка в этом варианте зашита в perl-код. Это вносит неудобства и ограничения. Далее мы от этого избавимся.
 +
 +
== Шаблоны ==
 +
 +
Что плохого в том, что HTML-вёрстка зашита в Perl-коде?
 +
# Неудобство редактирования кода: синтаксическая подсветка, автодополнение — всё это не работает внутри простой строки на языке Perl.
 +
# Невозможность применить разную вёрстку для разных [[Тема оформления|тем оформления]] сайта.
 +
 +
Эти проблемы решаются с помощью вынесения вёрстки в шаблоны. Шаблон — это просто внешний HTML-файл. Шаблон загружается в переменную функцией <code>tpl_str</code>.
 +
 +
Примеры:
 +
 +
<source lang="perl">
 +
# в переменную
 +
my $html = tpl_str('test.html');
 +
 +
# сразу на экран
 +
print tpl_str('test.html');
 +
</source>
 +
 +
=== Передача даных в шаблон ===
 +
 +
tpl_str с одним параметром (именем файла) просто читает файл.
 +
 +
Чтобы заполнить шаблон данными, следует вызывать tpl_str с двумя параметрами:
 +
# Имя файла шаблона;
 +
# Хеш замены. В хеше ключи означают имена переменных в шаблоне, а на значения ключей переменные в шаблоне будут заменены.
 +
 +
Переменные в шаблоне — это просто слова со знаком доллара в начале, например <code>$name</code>.
 +
 +
Пример шаблона и заполнения:
 +
 +
HTML-шаблон test.html с 2 переменными:
 +
<source lang="html4strict">
 +
<p>Привет, меня зовут $name. Мне $age лет.</p>
 +
</source>
 +
 +
Perl-код:
 +
<source lang="perl">
 +
tpl_str('test.html',{
 +
name => 'Иван',
 +
age  => 10,
 +
});
 +
</source>
 +
 +
Результат:
 +
<source lang="html4strict">
 +
<p>Привет, меня зовут Иван. Мне 10 лет.</p>
 +
</source>
 +
 +
Посмотрите на содержимое файла:
 +
<pre>/var/www/html/sp/cms/themes/sp-blog/news.html</pre>
 +
Это шаблон новости из темы оформления sp-blog. Вот насколько просто у нас делаются шаблоны.
 +
 +
=== Пути к шаблонам ===
 +
 +
В примерах выше приведён вызов tpl_str с именем файла шаблона. Такое сработало бы, если бы шаблон был один на всех и лежат рядом со скриптом.
 +
 +
В реальности нам необходимо поддерживать темы оформления (шаблоны лежат в разных папках) и темы оформления блогов (наборы подпапок в личных папках блогеров).
 +
 +
Хорошие новости, [[CMS]] заботится и об этом. Выяснить вручную, какой сайт запрошен (школьный или блог), и какая тема оформления в нём выбрана, не нужно.
 +
 +
Путь к папке с шаблонами хранит переменная '''$theme_dir''', а выбранную тему хранит переменная '''$theme'''.
 +
 +
Таким образом, если стоит задача получить заполненный шаблон '''guestbook.html''' без забот, следует писать полный путь, построенный из этих переменных и имени нужного файла на конце:
 +
 +
<source lang="perl">
 +
my $html = tpl_str("$theme_dir/$theme/guestbook.html", {
 +
# замены...
 +
});
 +
</source>
 +
 +
Вынесение вёрстки из кода в шаблон:
 +
 +
<source lang="perl">
 +
sub guestbook()
 +
{
 +
# ...
 +
while ( $q->fetch )
 +
{
 +
$messages .= tpl_str("$theme_dir/$theme/guestbook-item.html",{
 +
name => $hash{name},
 +
date => $hash{post_date},
 +
text => $hash{text},
 +
});
 +
}
 +
return tpl_str("$theme_dir/$theme/guestbook.html",{
 +
messages => $messages,
 +
});
 +
# ...
 +
}
 +
</source>
 +
 +
Результат: вёрстка во внешних файлах:
 +
* '''guestbook.html''' — шаблон страницы с гостевой, заголовки, форма
 +
* '''guestbook-item.html''' — шаблон отдельной записи
 +
 +
Этот вариант максимально прост, но требует, чтобы шаблон был в каждой теме оформления.
 +
 +
=== Кеширование и шаблон по умолчанию ===
 +
 +
tpl_str хороша для админки или для простых страниц. Она читает шаблон с диска на каждый вызов, а в случае отсутствия шаблона не происходит автоматическая подгрузка одноимённого файла из темы sp-default.
 +
 +
Чтобы эффективнее использовать замены в шаблонах в циклах, а также поддерживать идею взятия отсутствующего шаблона из темы sp-default, используйте связку из двух функций:
 +
* '''open_tpl''' — только читает шаблон, возвращает массив строк. При повторном вызове быстро берёт шалблон из памяти без обращения к диску. Выигрыш при использовании SpeedyCGI (то есть в боевых условиях).
 +
* '''tpl_replace_ant''' — выполняет замену по массиву, прочитанному ранее функцией open_tpl, возвращает массив строк с заменами (конечную вёрстку, готовую для выдачи пользователю).
 +
 +
Параметры open_tpl:
 +
# Полный путь к файлу шаблона (используйте переменные для указания пути к темам и имени выбранной темы аналогично tpl_str)
 +
 +
Параметры tpl_replace_ant:
 +
# Анонимный хеш или ссылка на кэш
 +
# Ссылка на массив, прочитанный open_tpl
 +
 +
Пример:
 +
 +
<source lang="perl">
 +
# Чтение шаблонов
 +
my @tpl_guestbook            = open_tpl("$theme_dir/$theme/guestbook.html");
 +
my @tpl_guestbook_item_cache = open_tpl("$theme_dir/$theme/guestbook-item.html");
 +
my @tpl_guestbook_item;
 +
 +
# Замены в цикле
 +
while ( ... )
 +
{
 +
@tpl_guestbook_item = @tpl_guestbook_item_cache;
 +
$messages .= join '', tpl_replace_ant(
 +
{
 +
name => $hash{name},
 +
date => $hash{post_date},
 +
text => $hash{text},
 +
},
 +
\@tpl_guestbook_item
 +
);
 +
}
 +
 +
# Возврат конечного фрагмента пользователю
 +
return join '', tpl_replace_ant(
 +
{
 +
messages => $messages,
 +
},
 +
\@tpl_guestbook
 +
);
 +
</source>
 +
 +
== Промежуточный итог ==
 +
 +
После всего сделанного пробуем модуль. Заполняем форму несколько раз. Она отвечает, что данные в базу внесены. Затем (пока что вручную) у нескольких записей в таблице '''guestbook''' меняем 0 в поле published на 1. Смотрим, что получилось:
 +
 +
[[Файл:guestbook_draft.png]]
 +
 +
Интерфейс пока выглядит сделанным топором. Приведение в порядок и стилизование шаблонов под темы оформления — это дела за пределами темы этой статьи.
 +
 +
В двух словах: это делается редактированием вёрстки и внесением правил в CSS сответствующей темы оформления. Подробнее см. [[Тема оформления]].
 +
 +
На данный момент самое главное: часть для посетителей сайта работает. Остальное будет доступно авторизованному владельцу сайта в админке.
 +
 +
== Админка ==
 +
 +
Будем делать 3 простых и минимально необходимых действия для владельца сайта:
 +
# Просмотр
 +
# Одобрение
 +
# Удаление
 +
 +
Вспомните простейший вариант кода модуля:
 +
 +
<source lang="perl">
 +
sub guestbook()
 +
{
 +
if ( defined param('edt') ) {
 +
# Для владельца сайта
 +
print "Hello, admin interface!";
 +
}
 +
else {
 +
# Для посетителей сайта...
 +
}
 +
}
 +
</source>
 +
 +
Сейчас допишем недостающую половину кода для владельца сайта.
 +
 +
Соблюдая првило делать всё простым, в голову пришла мысль оформить интерфейс в виде таблицы с 3 колонками.
 +
# Автор и Текст
 +
# Команда одобрения
 +
# Команда удаления
 +
 +
Над выборкой данных думать не надо, запрос, который делает именно то, что нам нужно уже написан выше:
 +
 +
<source lang="sql">
 +
select * from guestbook where published = 1 and owner = $owner
 +
</source>
 +
 +
Что следует сразу же сделать:
 +
* определить порядок выдачи (пусть это будет сортировка по дате от большего к меньшему (новое сверху))
 +
* не ограничиваться опубликованными записями
 +
 +
<source lang="sql">
 +
select * from guestbook
 +
where owner = $owner
 +
order by post_date desc
 +
</source>
 +
 +
Наброски таблицы:
 +
 +
<source lang="perl">
 +
my $q = sqlpcms("select * from guestbook
 +
where owner = $owner
 +
order by post_date desc");
 +
my %hash; $q->bind_columns( \( @hash{ @{$q->{NAME_lc} } } ));
 +
my $table = '';
 +
while ( $q->fetch )
 +
{
 +
# заполнение шаблона сообщения
 +
$table .= qq[
 +
<tr>
 +
<td>
 +
Имя: <b>$hash{name}</b><br>Дата: $hash{post_date}
 +
<p>text => $hash{text}</p>
 +
</td>
 +
<td>Одобрить</td>
 +
<td>Удалить</td>
 +
</tr>
 +
];
 +
}
 +
 +
print q[<h1>Модерация гостевой книги</h1>
 +
<table width="75%" class="adminlist">
 +
<tr>
 +
<th>Сообщение</th>
 +
<th colspan="2">Действия</th>
 +
</tr>
 +
], $table , '</table>';
 +
 +
</source>
 +
 +
Как это выглядит в админке:
 +
 +
[[Файл:guestbook_admin.png]]
 +
 +
== Параметр edt и свой пункт в меню ==
 +
 +
Забегая вперёд, адрес модуля в режиме администрарования такой:
 +
<pre>
 +
http://ваш_сервер/cgi-bin/sp/cms/cms.pl?mod=guestbook&edt=lst
 +
</pre>
 +
 +
=== Параметр edt ===
 +
 +
Разберём подробно этот адрес: во-первых, это адрес cms.pl, параметр со значением '''mod=guestbook''' определяет модуль. Параметр '''edt''' с каким-то значением определяет, что мы действуем в административном режиме строго под авторизацией.
 +
 +
Значение параметра '''edt''' определает административное действие.
 +
 +
Например, у нас есть вутреннее соглашение о том, что edt=lst — это вывод списка объектов (все эти параметры и значения сокращаем, edt — это edit, lst — list), с которыми имеет дело модуль. edt=del — это удаление объекта и так далее.
 +
 +
Я кроме вывода полного списка сообщений (edt='''lst''', единственного пока действия), запланировал реализовать реакцию на значения '''del''' и '''approve'''.
 +
 +
=== Свой пункт в меню ===
 +
 +
Поскольку реализуем поведение модуля не только на сайте (а это автоматически помещает его в список модулей, даёт интерфейс к настройкам. Пункт меню: Сайт → Компоненты), но и в админке, необходимо добавить новый пункт в главное меню для авторизованного пользователя.
 +
 +
В файле
 +
<pre>/var/www/cgi-bin/sp/menu.pl</pre>
 +
происходит хоть и доволько нетривиальная, но всё-таки по сути своей обыкновенная склейка строк.
 +
 +
Найдите, например, такую:
 +
<pre>          { text: "Аналитические системы", url: "$cms?mod=analytics&edt=code", id: "spm_analytics" },</pre>
 +
 +
Вам нужен код, не входящий в ветку:
 +
<pre>if ( $gconf{'light'} )</pre>
 +
 +
Добавите новый пункт в виде объекта, аналогичного существующим:
 +
 +
<pre>
 +
          { text: "Аналитические системы", url: "$cms?mod=analytics&edt=code", id: "spm_analytics" },
 +
          { text: "Модерация гостевой книги", url: "$cms?mod=guestbook&edt=lst" },
 +
</pre>
 +
 +
Мы, по сути, динамически формируем фрагмент кода на JavaScript. Именно его синтаксис и следует союлбдать при редактировании кода генерации меню авторизованного пользователя.
 +
 +
Как видите, не нужно беспокоиться об имени сервера, адресе скрипта cms.pl, всё это уже есть в переменной $cms. К ней я приписал параметры '''mod=guestbook''' (определяем модуль) и '''edt=lst''' (определяем административное действие, а именно вывод списка сообщений).
 +
 +
== Одобрение и удаление ==
 +
 +
Тело цикла while для формирования таблицы а админке меняется в соответствии с планами по значениями параметра '''edt'''.
 +
 +
<source lang="perl">
 +
$table .= qq[
 +
<tr>
 +
...
 +
<td><a href="$cms?mod=guestbook&edt=approve&i_id=$hash{id}">Одобрить</a></td>
 +
<td><a href="$cms?mod=guestbook&edt=del&i_id=$hash{id}">Удалить</a></td>
 +
</tr>
 +
];
 +
</source>
 +
 +
Это простейший вариант.
 +
 +
Домашнее задание: подумайте, как сделать подсветку ожидающих проверки записей?
 +
 +
[[Файл:guestbook_admin_color_and_approve_button_only_for_unapproved.png]]
 +
 +
Обработка разных значений edt — совсем нетрудная задача, просто разветвитесь:
 +
 +
<source lang="perl">
 +
sub guestbook()
 +
{
 +
# админка
 +
if ( defined param('edt') )
 +
{
 +
if ( param('edt') eq 'lst' ) # список сообщений
 +
{
 +
# ...
 +
}
 +
elsif ( param('edt') eq 'del' ) # удаление сообщения
 +
{
 +
# ...
 +
}
 +
elsif ( param('edt') eq 'approve' ) # одобрение сообщения
 +
{
 +
# ...
 +
}
 +
}
 +
# сайт
 +
else
 +
{
 +
# ...
 +
}
 +
}
 +
 +
</source>
 +
 +
Дополнение к домашнему заданию, чтобы получить не просто 5, а 5+:
 +
 +
Допишите самостоятельно ветки кода для удаления и одобрения сообщений.
 +
 +
Подсказки:
 +
* my $item_id = param(...); # получение значения параметра HTTP-запроса
 +
* sec_gt0($item_id); # проверка значения на то, что это целое число больше нуля
 +
* sqlcms("delete ..."); # выполнение SQL-запроса
 +
* sqlcms("update ...");

Текущая версия на 16:26, 31 октября 2016

Внимание! Модуль уже реализован в виде встроенного в Портале версии 5.3. Оставляем статью в качестве обучающего материала.

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

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

Внутреннее имя модуля — 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>
	<input type="hidden" name="mod" value="guestbook">
</form>

Единственная особенность: скрытое поле по имени mod со значением guestbook. Значение определяет, какой модуль получит данные формы. В данном случае это тот же самый модуль, которым я пишу, его внутреннее имя — guestbook.

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

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 = 1

Теперь все сообщения об ошибках обращения к базе будет с дампом запроса и stack trace-ом (функции 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><form>...</form>Сообщения: $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-вёрстку), другая пишет в базу принятое из формы и сообщает, что "Данные записаны.".

HTML-вёрстка в этом варианте зашита в perl-код. Это вносит неудобства и ограничения. Далее мы от этого избавимся.

Шаблоны

Что плохого в том, что HTML-вёрстка зашита в Perl-коде?

  1. Неудобство редактирования кода: синтаксическая подсветка, автодополнение — всё это не работает внутри простой строки на языке Perl.
  2. Невозможность применить разную вёрстку для разных тем оформления сайта.

Эти проблемы решаются с помощью вынесения вёрстки в шаблоны. Шаблон — это просто внешний HTML-файл. Шаблон загружается в переменную функцией tpl_str.

Примеры:

# в переменную
my $html = tpl_str('test.html');
 
# сразу на экран
print tpl_str('test.html');

Передача даных в шаблон

tpl_str с одним параметром (именем файла) просто читает файл.

Чтобы заполнить шаблон данными, следует вызывать tpl_str с двумя параметрами:

  1. Имя файла шаблона;
  2. Хеш замены. В хеше ключи означают имена переменных в шаблоне, а на значения ключей переменные в шаблоне будут заменены.

Переменные в шаблоне — это просто слова со знаком доллара в начале, например $name.

Пример шаблона и заполнения:

HTML-шаблон test.html с 2 переменными:

<p>Привет, меня зовут $name. Мне $age лет.</p>

Perl-код:

tpl_str('test.html',{
	name => 'Иван',
	age  => 10,
});

Результат:

<p>Привет, меня зовут Иван. Мне 10 лет.</p>

Посмотрите на содержимое файла:

/var/www/html/sp/cms/themes/sp-blog/news.html

Это шаблон новости из темы оформления sp-blog. Вот насколько просто у нас делаются шаблоны.

Пути к шаблонам

В примерах выше приведён вызов tpl_str с именем файла шаблона. Такое сработало бы, если бы шаблон был один на всех и лежат рядом со скриптом.

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

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

Путь к папке с шаблонами хранит переменная $theme_dir, а выбранную тему хранит переменная $theme.

Таким образом, если стоит задача получить заполненный шаблон guestbook.html без забот, следует писать полный путь, построенный из этих переменных и имени нужного файла на конце:

my $html = tpl_str("$theme_dir/$theme/guestbook.html", {
	# замены...
});

Вынесение вёрстки из кода в шаблон:

sub guestbook()
{
	# ...
	while ( $q->fetch )
	{
		$messages .= tpl_str("$theme_dir/$theme/guestbook-item.html",{
			name => $hash{name},
			date => $hash{post_date},
			text => $hash{text},
		});
	}
	return tpl_str("$theme_dir/$theme/guestbook.html",{
		messages => $messages,
	});
	# ...
}

Результат: вёрстка во внешних файлах:

  • guestbook.html — шаблон страницы с гостевой, заголовки, форма
  • guestbook-item.html — шаблон отдельной записи

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

Кеширование и шаблон по умолчанию

tpl_str хороша для админки или для простых страниц. Она читает шаблон с диска на каждый вызов, а в случае отсутствия шаблона не происходит автоматическая подгрузка одноимённого файла из темы sp-default.

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

  • open_tpl — только читает шаблон, возвращает массив строк. При повторном вызове быстро берёт шалблон из памяти без обращения к диску. Выигрыш при использовании SpeedyCGI (то есть в боевых условиях).
  • tpl_replace_ant — выполняет замену по массиву, прочитанному ранее функцией open_tpl, возвращает массив строк с заменами (конечную вёрстку, готовую для выдачи пользователю).

Параметры open_tpl:

  1. Полный путь к файлу шаблона (используйте переменные для указания пути к темам и имени выбранной темы аналогично tpl_str)

Параметры tpl_replace_ant:

  1. Анонимный хеш или ссылка на кэш
  2. Ссылка на массив, прочитанный open_tpl

Пример:

# Чтение шаблонов
my @tpl_guestbook            = open_tpl("$theme_dir/$theme/guestbook.html");
my @tpl_guestbook_item_cache = open_tpl("$theme_dir/$theme/guestbook-item.html");
my @tpl_guestbook_item;
 
# Замены в цикле
while ( ... )
{
	@tpl_guestbook_item = @tpl_guestbook_item_cache;
	$messages .= join '', tpl_replace_ant(
		{
			name => $hash{name},
			date => $hash{post_date},
			text => $hash{text},
		},
		\@tpl_guestbook_item
);
}
 
# Возврат конечного фрагмента пользователю
return join '', tpl_replace_ant(
	{
		messages => $messages,
	},
	\@tpl_guestbook
);

Промежуточный итог

После всего сделанного пробуем модуль. Заполняем форму несколько раз. Она отвечает, что данные в базу внесены. Затем (пока что вручную) у нескольких записей в таблице guestbook меняем 0 в поле published на 1. Смотрим, что получилось:

Guestbook draft.png

Интерфейс пока выглядит сделанным топором. Приведение в порядок и стилизование шаблонов под темы оформления — это дела за пределами темы этой статьи.

В двух словах: это делается редактированием вёрстки и внесением правил в CSS сответствующей темы оформления. Подробнее см. Тема оформления.

На данный момент самое главное: часть для посетителей сайта работает. Остальное будет доступно авторизованному владельцу сайта в админке.

Админка

Будем делать 3 простых и минимально необходимых действия для владельца сайта:

  1. Просмотр
  2. Одобрение
  3. Удаление

Вспомните простейший вариант кода модуля:

sub guestbook()
{
	if ( defined param('edt') ) {
		# Для владельца сайта
		print "Hello, admin interface!";
	}
	else {
		# Для посетителей сайта...
	}
}

Сейчас допишем недостающую половину кода для владельца сайта.

Соблюдая првило делать всё простым, в голову пришла мысль оформить интерфейс в виде таблицы с 3 колонками.

  1. Автор и Текст
  2. Команда одобрения
  3. Команда удаления

Над выборкой данных думать не надо, запрос, который делает именно то, что нам нужно уже написан выше:

SELECT * FROM guestbook WHERE published = 1 AND owner = $owner

Что следует сразу же сделать:

  • определить порядок выдачи (пусть это будет сортировка по дате от большего к меньшему (новое сверху))
  • не ограничиваться опубликованными записями
SELECT * FROM guestbook
WHERE owner = $owner
ORDER BY post_date DESC

Наброски таблицы:

my $q = sqlpcms("select * from guestbook
	where owner = $owner
	order by post_date desc");
my %hash; $q->bind_columns( \( @hash{ @{$q->{NAME_lc} } } ));
my $table = '';
while ( $q->fetch )
{
	# заполнение шаблона сообщения
	$table .= qq[
		<tr>
			<td>
				Имя: <b>$hash{name}</b><br>Дата: $hash{post_date}
				<p>text => $hash{text}</p>
			</td>
			<td>Одобрить</td>
			<td>Удалить</td>
		</tr>
	];
}
 
print q[<h1>Модерация гостевой книги</h1>
<table width="75%" class="adminlist">
<tr>
	<th>Сообщение</th>
	<th colspan="2">Действия</th>
</tr>
], $table , '</table>';

Как это выглядит в админке:

Guestbook admin.png

Параметр edt и свой пункт в меню

Забегая вперёд, адрес модуля в режиме администрарования такой:

http://ваш_сервер/cgi-bin/sp/cms/cms.pl?mod=guestbook&edt=lst

Параметр edt

Разберём подробно этот адрес: во-первых, это адрес cms.pl, параметр со значением mod=guestbook определяет модуль. Параметр edt с каким-то значением определяет, что мы действуем в административном режиме строго под авторизацией.

Значение параметра edt определает административное действие.

Например, у нас есть вутреннее соглашение о том, что edt=lst — это вывод списка объектов (все эти параметры и значения сокращаем, edt — это edit, lst — list), с которыми имеет дело модуль. edt=del — это удаление объекта и так далее.

Я кроме вывода полного списка сообщений (edt=lst, единственного пока действия), запланировал реализовать реакцию на значения del и approve.

Свой пункт в меню

Поскольку реализуем поведение модуля не только на сайте (а это автоматически помещает его в список модулей, даёт интерфейс к настройкам. Пункт меню: Сайт → Компоненты), но и в админке, необходимо добавить новый пункт в главное меню для авторизованного пользователя.

В файле

/var/www/cgi-bin/sp/menu.pl

происходит хоть и доволько нетривиальная, но всё-таки по сути своей обыкновенная склейка строк.

Найдите, например, такую:

          { text: "Аналитические системы", url: "$cms?mod=analytics&edt=code", id: "spm_analytics" },

Вам нужен код, не входящий в ветку:

if ( $gconf{'light'} )

Добавите новый пункт в виде объекта, аналогичного существующим:

          { text: "Аналитические системы", url: "$cms?mod=analytics&edt=code", id: "spm_analytics" },
          { text: "Модерация гостевой книги", url: "$cms?mod=guestbook&edt=lst" },

Мы, по сути, динамически формируем фрагмент кода на JavaScript. Именно его синтаксис и следует союлбдать при редактировании кода генерации меню авторизованного пользователя.

Как видите, не нужно беспокоиться об имени сервера, адресе скрипта cms.pl, всё это уже есть в переменной $cms. К ней я приписал параметры mod=guestbook (определяем модуль) и edt=lst (определяем административное действие, а именно вывод списка сообщений).

Одобрение и удаление

Тело цикла while для формирования таблицы а админке меняется в соответствии с планами по значениями параметра edt.

$table .= qq[
<tr>
	...
	<td><a href="$cms?mod=guestbook&edt=approve&i_id=$hash{id}">Одобрить</a></td>
	<td><a href="$cms?mod=guestbook&edt=del&i_id=$hash{id}">Удалить</a></td>
</tr>
];

Это простейший вариант.

Домашнее задание: подумайте, как сделать подсветку ожидающих проверки записей?

Guestbook admin color and approve button only for unapproved.png

Обработка разных значений edt — совсем нетрудная задача, просто разветвитесь:

sub guestbook()
{
	# админка
	if ( defined param('edt') )
	{
		if ( param('edt') eq 'lst' ) # список сообщений
		{
			# ...
		}
		elsif ( param('edt') eq 'del' ) # удаление сообщения
		{
			# ...
		}
		elsif ( param('edt') eq 'approve' ) # одобрение сообщения
		{
			# ...
		}
	}
	# сайт
	else
	{
		# ...
	}
}

Дополнение к домашнему заданию, чтобы получить не просто 5, а 5+:

Допишите самостоятельно ветки кода для удаления и одобрения сообщений.

Подсказки:

  • my $item_id = param(...); # получение значения параметра HTTP-запроса
  • sec_gt0($item_id); # проверка значения на то, что это целое число больше нуля
  • sqlcms("delete ..."); # выполнение SQL-запроса
  • sqlcms("update ...");