Создание встроенного модуля CMS. Часть 3 — различия между версиями
(→Собственная таблица для произвольных данных) |
(→Пути к шаблонам) |
||
(не показаны 84 промежуточные версии этого же участника) | |||
Строка 1: | Строка 1: | ||
− | + | Внимание! Модуль уже реализован в виде встроенного в Портале версии 5.3. Оставляем статью в качестве обучающего материала. | |
− | + | В статье подразумевается, что вы уже прочитали предыдущие статьи серии "Создание встроенного модуля CMS". | |
− | + | Рассматривается вопрос разделения модуля на части для посетителей сайта и для владельца сайта (авторизованного), чтение и добавление контента на сайт, использование шаблонов. | |
+ | |||
+ | Внутреннее имя модуля — '''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 | + | (?, ?, '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 = | + | 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* выбросит сообщение, понятное постороннему человеку.
Но нам-то это не помогает понять, что произошло, а главное где. У портала есть режим отладки, который сильно помогает при разработке, который очень рекомендуем включить у себя для удобства отладки, но крайне не рекомендуем включать на боевом сервере, поскольку раскрывается множество технических подробностей, которые: во-первых, никак не помогут посетителям, если что-то случилось; во-вторых, выдадут лишнюю информацию злоумышленникам.
Итак: та же самая ошибка, в режиме отладки:
Как это сделать: открываем 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-коде?
- Неудобство редактирования кода: синтаксическая подсветка, автодополнение — всё это не работает внутри простой строки на языке Perl.
- Невозможность применить разную вёрстку для разных тем оформления сайта.
Эти проблемы решаются с помощью вынесения вёрстки в шаблоны. Шаблон — это просто внешний HTML-файл. Шаблон загружается в переменную функцией tpl_str
.
Примеры:
# в переменную my $html = tpl_str('test.html'); # сразу на экран print tpl_str('test.html');
Передача даных в шаблон
tpl_str с одним параметром (именем файла) просто читает файл.
Чтобы заполнить шаблон данными, следует вызывать tpl_str с двумя параметрами:
- Имя файла шаблона;
- Хеш замены. В хеше ключи означают имена переменных в шаблоне, а на значения ключей переменные в шаблоне будут заменены.
Переменные в шаблоне — это просто слова со знаком доллара в начале, например $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:
- Полный путь к файлу шаблона (используйте переменные для указания пути к темам и имени выбранной темы аналогично tpl_str)
Параметры tpl_replace_ant:
- Анонимный хеш или ссылка на кэш
- Ссылка на массив, прочитанный 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. Смотрим, что получилось:
Интерфейс пока выглядит сделанным топором. Приведение в порядок и стилизование шаблонов под темы оформления — это дела за пределами темы этой статьи.
В двух словах: это делается редактированием вёрстки и внесением правил в CSS сответствующей темы оформления. Подробнее см. Тема оформления.
На данный момент самое главное: часть для посетителей сайта работает. Остальное будет доступно авторизованному владельцу сайта в админке.
Админка
Будем делать 3 простых и минимально необходимых действия для владельца сайта:
- Просмотр
- Одобрение
- Удаление
Вспомните простейший вариант кода модуля:
sub guestbook() { if ( defined param('edt') ) { # Для владельца сайта print "Hello, admin interface!"; } else { # Для посетителей сайта... } }
Сейчас допишем недостающую половину кода для владельца сайта.
Соблюдая првило делать всё простым, в голову пришла мысль оформить интерфейс в виде таблицы с 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>';
Как это выглядит в админке:
Параметр 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> ];
Это простейший вариант.
Домашнее задание: подумайте, как сделать подсветку ожидающих проверки записей?
Обработка разных значений 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 ...");