Печать

Продолжаем серию «Чайникам от чайника». Теперь поговорим о CMS (Content Management System — Система управления контентом). Должен сказать, что я как и Б.Н. Ельцин, «долго думал, страдал», но все же решил сначала полностью все рассказать и объяснить, подготовить минимально рабочую CMS на простом линейном коде, а не делать все помаленьку, начиная с какого-нибудь «Hello World». Это и есть путь джедая, сначала познать себя, а потом спасти мир. Поэтому приготовьтесь к постепенному освоению матчасти прежде, чем увидите мало-мальский результат. Этот результат действительно мало-мальский, несмотря на то, что полностью рабочий. Во-первых, мне стало жалко времени на полную разработку сайта на говнокоде. Во-вторых, это лишь попытка изучить азы создания CMS. Реально использовать линейный код без классов для больших проектов нереально, так как любое изменение предполагает внесение правок в  бесчисленное количество файлов. Для маленьких сайтов действительно можно использовать такой код, и он должен быть более легким и быстрым по сравнению с большими CMS. Для ленивых весь сайт доступен для загрузки из нашего хранилища.

 

 

1. Введение.

 

В жизни каждого скрипткиддиса наступает момент, когда стандартных возможностей бесплатных CMS не хватает для реализации какого-то проекта (пора взрослеть). Или просто захочется сделать что-то свое. Для реализации своего компонента в CMS надо знать не только php, но и структуру самой CMS, поэтому возникает решение запилить особенную систему. В ней мы можем реализовать только те компоненты, которые нам нужны, упростив схему CMS, однако вся забота о безопасности и быстродействии теперь будет наша.

Что мы знаем про сайты?

1. Виды браузеров и html.

Сайт должен отдать по запросу браузера html-страничку, и пользователь увидит то, что мы ему приготовили. Здесь кроется первая проблема: существует около десятка распространенных браузеров и неизвестно сколько нераспространенных, у каждого из них «овердофига» версий, не каждый из браузеров поддерживает автоматическое обновление, а те, которые поддерживают, не всегда это делают. С другой стороны, стандарты html постоянно изменяются (html-html2 -...-html5), а для красоты и удобства в сайты вставляют java-код, флэш-картинки и прочие красотули. Не все браузеры поддерживают эти фичи, поэтому пользователь вместо красивой картинки может получить кракозябры. Если учесть все факторы, выходит, что нормальное отображение контента — очень редкое и случайное событие. Но, расслабьтесь, все не так страшно. На самом деле, распространенных движков у браузеров всего 3-4, все они поддерживают некие базовые функции, старые браузеры умирают вместе со старыми компьютерами, поэтому около 95% пользователей используют совместимые и современные браузеры. Это позволяет при написании сайта ориентироваться на три-четыре основных движка (ie, mozilla, chrome и opera, а сафари в i-девайсах будет работать в этих условиях, на него отдельно заморачиваться не надо), если мы используем html5, добавляем java-библиотеку html5.js для старых ie. Тогда мы можем быть уверены, что наш сайт правильно отобразится на 99.99% устройств.

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

2. Динамический сайт.

«Динамический» - условное понятие. В нашем контексте это означает , что html страничка не существует в постоянном виде, как файл типа page.html. Она формируется сервером по запросу пользователя. Происходит это следующим образом: пользователь вбивает в браузере http://www.seczone.ru/index.php/stati/12-unix-os/29-freezfs4, браузер отправляет на сайт http://www.seczone.ru запрос «покажи мне страничку index.php/stati/12-unix-os/29-freezfs4». Сервер по этому запросу формирует на лету html-код и отдает его браузеру так, будто он и является этой страничкой. Зачем все это наворочено? Это делается для сайтов, контент которых будет постоянно изменяться. Сайт новостей, блог, интернет магазин и т. д. В случае использования чистого html при каждом изменении контента необходимо менять код. Приглашать красноглазого «специалиста», платить ему денюшку.

В основе динамического сайта стоит (лежит) CMS — система управления контентом. Это программа, написанная на одном из языков программирования, наиболее подходящем для данных целей и для данного сервера. В настоящее время мир захвачен php. Это всего лишь один из языков, но он реализован и для windows, и *nix-систем. Обладает своими минусами и плюсами, разбирать которые мы не будем, сравнивать с asp и perl не собираемся, просто в нашем примере мы используем именно его, как наиболее популярный.

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

Итак, задача php-кода — выдать одну страницу по запросу, при этом он выполняется от начала и до конца.

3. Типы запросов.

Есть всего два типа запроса — GET и POST. Из названия понятно, что один из них (GET) является собственно запросом, то есть говорит «Дай», а второй, POST, служит для отправки каких-то данных на сервер. Позже мы разберем на примерах их применение.

4. Стили.

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

5. MySQL.

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

 

Подведем итог. Настоящий джедай-сайтомаг должен как минимум знать основы функционирования сервера Apache, html, язык программирования php, css-разметку, язык sql-запросов. И это не должно нас беспокоить. Сейчас мы выясним, что вы это все знаете, но почему-то забыли. На самом деле все гораздо проще, чем кажется. Пора начинать.

 

2. Готовим поле.

 

Для наших экспериментов подойдет любой AMP (Apache-MySQL-PHP) сервер. Если Вы работаете на Linux станции, можете установить это из репозитория. Если же, как я и все ламеры, владеете Windows станцией, рекомендую Denver (http://www.denwer.ru/). Установка описана там на сайте и не представляет сложности. Редактируем код любым редактором. Я использую Far Manager (http://www.farmanager.com/download.php?l=ru) и его встроенный редактор. Он немного подсвечивает код, но это не принципиально, хоть и полезно. Главное, при создании файлов сразу указываем их кодировку — UTF-8. Это необходимо для предупреждения различных ошибок, правильного отображения символов различными браузерами и обеспечения кроссплатформенности кода. В FARе при редактировании файла нажмем Shift+F2 и выбираем кодировку, в которой хотим сохранить файл (рис. 1).

pic01

Обязательно убираем галку из «Добавить сигнатуру BOM». Из графических редакторов рекомендую GIMP (https://www.gimp.org/downloads/) для точечной графики и Inkscape (https://inkscape.org/ru/download/) для векторной. Они обладают одним неоспоримым преимуществом — бесплатность. Ну и офисный пакет LibreOffice (https://ru.libreoffice.org/download/), чтобы написать эту статью.

Наш сайт для начала будет простой, содержать три типа контента: страницы (по которым будет формироваться главное меню), записи (их будем выводить в виде блога колонкой справа), объявления (будем выводить колонкой слева). Данные будем хранить в mysql-базе dingo09, пользователь proba c паролем proba.

 

3. Готовим сайт. .htacces и index.html.

 

В каталоге сайта создаем структуру нашего сайта. У нас будут такие подкаталоги:

cfg – для хранения конфигурации.

com — файлы основных компонент.

css — стили.

images – для изображений.

install – в этом каталоге на время разработки будем укладывать скрипты создания баз, потом этот каталог необходимо будет удалить.

js – для java-скриптов

lib – библиотеки

mod — модули

template – шаблон

 

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

 

AddDefaultCharset utf-8

 

RewriteEngine On

RewriteBase /

 

RewriteCond %{REQUEST_FILENAME} !-d

RewriteCond %{REQUEST_FILENAME} !-f

 

# ЗАПРЕЩЁННЫЕ ФАЙЛЫ

RewriteRule .htaccess - [F]

 

 

# ПРАВИЛА mod_rewrite

RewriteRule ([a-z0-9_-]+)([\/]{0,1})([a-z0-9_-]+)\.htm$ index.php?option=$1&alias=$3 [L]

Первая строка объявляет кодировку по умолчанию для сервера. Вторая включает mod_rewrite. Его должен поддерживать Ваш сервер, если же нет — меняйте хостинг.Третья строка устанавливает корневую папку, что, вообще говоря, необязательно. Четвертая и пятая запрещают просмотр файловой системы. Нам ведь не надо, чтоб кто угодно мог индексировать наши каталоги. Шестая строка запрещает доступ к файлу .htaccess, этот файл только для сервера. Седьмая определяет правила преобразования адреса. Наш сервер должен отдавать нам страницу только из корня сайта, где будет лежать наш главный контроллер — index.php. Остальные каталоги не будут иметь никакого отношения к карте сайта. Поэтому, если пользователь наберет в адресной строке <наш сайт>/pages/edititem1023.htm, сервер преобразует это в GET запрос <нашсайт>/index.php?option=pages&alias=edititem1023. Это запустит нашу программу через index.php, подав в нее две переменные: option со значением «pages» и alias со значением «edititem1023». Мы в программе обработаем эти переменные, выдав соответствующую страничку. Запросы, которые не укладываются в указанные правила, сервер будет отвергать с ответом «ошибка 404», то есть «страница не существует».

Для того, чтобы сервер не пустил пользователя в реальные каталоги, в каждый из них поместим файл .htaccess с другим содержанием:

 

deny from all

 

Теперь сервер ответит на запросы только через index.php в корне сайта.

 

Еще в каждый каталог поместим html-заглушку: пустой файл index.html такого содержания:

 

<html><body bgcolor="#FFFFFF"></body></html>

 

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

 

4. Конфигурационный файл.

 

В каталоге cfg создадим файл core.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

ini_set( "display_errors", true ); //debug

date_default_timezone_set( "Asia/Yekaterinburg" ); // http://www.php.net/manual/en/timezones.php

define ( "NAMEOFSITE", "Di&Go" ); // ИМЯ САЙТА

define ( "ROWSPERPAGE", "10" ); // Количество строк на страницу в выводе

define ( "COMPATH", $_SERVER[DOCUMENT_ROOT] . '/com/');

define ( "CSSPATH", $_SERVER[DOCUMENT_ROOT] . '/css/');

define ( "JSPATH", $_SERVER[DOCUMENT_ROOT] . '/js/');

define ( "LIBPATH", $_SERVER[DOCUMENT_ROOT] . '/lib/');

define ( "MODPATH", $_SERVER[DOCUMENT_ROOT] . '/mod/');

define ( "TEMPATH", $_SERVER[DOCUMENT_ROOT] . '/template/');

?>

 

Первая строка запрещает прямое обращение к файлу. Мы уже запретили это с помощью .htaccess, но на всякий случай (опять же ошибка сервера, например) усилим безопасность.

Вторая строка устанавливает php-переменную, указывающую необходимость показывать ошибки. После отладки эту переменную надо будет установить в false, то есть запрет показа ошибок.

Третья строка устанавливает часовой пояс сайта. В комментарии адрес сайта, где Вы можете найти свою timezone и заменить. Далее устанавливаем имя сайта (Di&Go), количество строк в выводе списков, и пути к каталогам. Для усиления безопасности Вы можете использовать свои имена каталогов, для этого надо будет только изменить эти записи в соответствии с названиями каталогов. Никогда нехорошие парни не узнают, что Ваши библиотеки находятся в каталоге /hd6js9/, Вам же не надо будет менять код, а только одну строку:

define ( "LIBPATH", $_SERVER[DOCUMENT_ROOT] . '/hd6js9/');

 

5. Куки и с чем их едят.

 

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

Вернемся к схеме работы сервера. По умолчанию сервер ничего не помнит о подключившемся клиенте, не помнит результат предыдущего запроса, каждый новый запрос для него — это новый клиент. Совсем как рыбка, памяти — ноль. В этом случае бесполезна авторизация, если в текущем запросе мы авторизовались, в следующем — мы снова никто. Каждый новый запрос должен содержать полную информацию о каких-то предыдущих результатах, типа «я в предыдущем запросе установил фильтр по дате и сортировку по имени, теперь мне нужна четвертая страница этого списка, а зовут меня Иванов Иван и я имею уровень доступа администратор». Сервер должен будет поверить Вам на слово и выдать результат. Мало того, что это неудобно, так это и еще крайне небезопасно. Поэтому существует встроенный механизм «запоминания» клиента. Он тоже таит в себе угрозу безопасности, как, впрочем, все остальные. Этот механизм называется «куки». Сервер при обращении клиента выдает ему название его «личного» файла на сервере. Браузер запоминает это имя и предъявляет при каждом обращении. Личный файл создается в момент обращения, имеет случайное имя, поэтому другой человек сможет попасть туда, только если перехватит Ваш трафик. Для предупреждения такого перехвата либо организуем на сайте шифрование (к сожалению, за легитимный сертификат надо регулярно платить), либо сам клиент должен позаботиться и установить vpn канал через какой-нибудь бесплатный сервер. Если Вы серьезно относитесь к клиентам своего сайта и они должны там авторизоваться, тогда придется использовать шифрование и https. Но это уже не относится к самим кукам. С другой стороны, владелец сервера имеет полный доступ к этим файлам, что является второй угрозой безопасности. Для очень серьезных сайтов используются собственные механизмы запоминания клиента через файлы, в sql-базе, для максимального обеспечения конфиденциальности данных. В этом случае придется самостоятельно беспокоиться и об удалении устаревших кук, сервер же свои куки чистит самостоятельно. Итак, нам вполне достаточно встроенного в сервер механизма. Если сервер не поддерживает куки — меняйте хостинг.

 

6. Главный контроллер index.php.

 

В корне сайта создаем файл index.php:

<?php

session_start();

define("INDEX", ""); // УСТАНОВКА КОНСТАНТЫ ГЛАВНОГО КОНТРОЛЛЕРА

require_once($_SERVER[DOCUMENT_ROOT]."/cfg/core.php"); // ПОДКЛЮЧЕНИЕ ЯДРА

// ПОДКЛЮЧЕНИЕ К БД

require_once(LIBPATH."sfms.php");

$db = new SafeMySQL();

// УСТАНАВЛИВАЕМ АНОНИМУСА

// user_level=0 - анонимус

// 1 - пользователь

// 2 - редактор

// 3 - админ

$option = $_GET['option'];

$alias = $_GET['alias'];

if (!isset($_SESSION['user_id'])) {

$_SESSION['user_id']='0';

$_SESSION['user_level']='0';

}

if ($_POST){

include(COMPATH."compost/compost.php");

}

else {

unset($_SESSION['firstrow']);

unset($_SESSION['page_id']);

}

// ГЛАВНЫЙ КОМПОНЕНТ

require_once(COMPATH."page.php");

// ШАБЛОН

require_once(TEMPATH."template.php");

?>

Поясню каждую строку.

session_start();

Запуск сессии, создаем свою куку.

 

define("INDEX", ""); // УСТАНОВКА КОНСТАНТЫ ГЛАВНОГО КОНТРОЛЛЕРА

Устанавливаем константу INDEX, и будем проверять её наличие в каждом php файле во избежание запуска наших PHP мимо главного контроллера. В файле core.php (первая строка) мы это уже использовали. И во всех остальных файлах php у нас будет такая первая строка.

 

require_once($_SERVER[DOCUMENT_ROOT]."/cfg/core.php"); // ПОДКЛЮЧЕНИЕ ЯДРА

Подключаем конфигурационный файл.

Здесь необходимо пояснение.

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

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

Когда интерпретатор дойдет до этой строки он просто включит в код указанный файл.

 

require_once(LIBPATH."sfms.php");

Для безопасного и удобного выполнения SQL-запросов я использовал библиотеку отсюда (http://phpfaq.ru/safemysql). Что и Вам настоятельно рекомендую. По крайней мере до тех пор, пока не разберетесь с языком SQL-запросов и не напишите свою библиотеку.

 

$db = new SafeMySQL();

Создаем поключение к базе данных. Использование библиотеки я разберу подробнее при выполнении первого запроса. Пока же обращаю Ваше внимание, что переменные для подключения к базе (имя базы, имя пользователя и его пароль) я не стал выносить в core.php, а оставил в библиотеке sfms.php

Качаем эту библиотеку с указанного адреса и сохраняем в папке для библиотек с указанным именем (у нас /lib/sfms.php). Приблизительно в 75 строке начинаются параметры подключения к базе:

private $defaults = array(

'host' => 'localhost',

'user' => 'proba',

'pass' => 'proba',

'db' => 'dingo09',

'port' => NULL,

'socket' => NULL,

'pconnect' => FALSE,

'charset' => 'utf8',

'errmode' => 'exception', //or 'error'

'exception' => 'Exception', //Exception class name

);

Указываем свои host, user, pass, db и при необходимости остальное.

 

if (!isset($_SESSION['user_id'])) {

$_SESSION['user_id']='0';

$_SESSION['user_level']='0';

}

Здесь наше первое обращение к куке. Массив $_SESSION содержит хранящиеся в куке данные, доступен как на запись, так и на чтение. Для хранения данных пользователя используем две переменные: user_id и user_level. При первом проходе они не определены и мы устанавливаем их в ноль, то есть анонимус. Этому пользователю можно только посмотреть, а трогать ему ничего нельзя. Эти переменные мы и будем проверять при всех обращениях к базе и при отображении страниц.

 

$option = $_GET['option'];

$alias = $_GET['alias'];

Получаем параметры из GET запроса.

 

if (isset($_POST)){

include(COMPATH."compost/compost.php");

Если есть POST запрос, подключаем его обработку.

 

require_once(COMPATH."page.php");

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

 

require_once(TEMPATH."template.php");

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

Пока все, с расширением функционала этот файл будем изменять, добавляя нужные строки.

 

7. Форма редактирования страницы.

 

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

Создаем в каталоге mod подкаталог mod_edit, в котором еще два каталога data и view (рис. 2).

pic02

Понятно, что в первом (data) будет код для подготовки данных, которые потребуются для корректного отображения собственно формы, расположенной во втором (view). Последний раз напоминаю, что в каждый новый подкаталог кладем заглушку (пустой файл) index.html. Ограничения, установленные в .htaccess, распространяются на все подкаталоги, поэтому класть этот файл ниже уже не надо, если мы не хотим изменять политику доступа.

Начнем с формы. В каталоге view создадим файл mod_editForm.php со следующим содержанием:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

// Входные данные -> $comrow

//$_SESSION['page_id']

//$_SESSION['errorMessage']

// buttons actionEdit

//

//

?>

<h1><?php echo $_SESSION['page_act'].' '.$comrow['page_h1']?></h1>

<form method="post">

<input type="hidden" name="page_id" value="<?php echo $comrow['page_id'] ?>"/>

<?php if ( isset( $_SESSION['errorMessage'] ) ) { ?>

<div class="errorMessage"><?php echo $_SESSION['errorMessage'];

unset( $_SESSION['errorMessage'] ); ?></div>
<?php } ?>

 

<div class="editfrm">

<ul>

<li>

<label for="title">Название</label>

<input type="text" name="page_title" id="title" placeholder="Название для заголовка меню" required autofocus maxlength="255" value="<?php echo htmlspecialchars( $comrow['page_title'] )?>" />

</li>

<li>

<label for="p_alias">Алиас</label>

<input type="text" name="page_alias" id="p_alias" placeholder="Наименование анг. буквами для формирования меню" required maxlength="255" value="<?php echo htmlspecialchars($comrow['page_alias'])?>" />

</li>

<li>

<label for="ph1">Название для заголовка статьи</label>

<input type="text" name="page_h1" id="ph1" placeholder="Название для заголовка страницы" required maxlength="255" value="<?php echo htmlspecialchars($comrow['page_h1'])?>" />

</li>

<li>

<label for="cat">Категория статьи</label>

<select name="page_category" id="cat" required placeholder="Выберите категорию статьи" maxlength="255">

<?php $catname=array('Страницы','Записи','Объявления');

if (!isset($comrow['page_category'])) $comrow['page_category']=1;

for ($i=0;$i<3;$i++){

echo '<option value="'.htmlspecialchars($i).'"';

if ($comrow['page_category']==$i) echo ' selected';

echo '>'.htmlspecialchars($catname[$i]).'</option>';

}?>

</select>

</li>

<li>

<label for="summary">Краткое содержание</label>

<textarea name="page_s_text" id="summary" placeholder="Краткое содержание для блога" required maxlength="1000" style="height: 5em;"><?php echo htmlspecialchars($comrow['page_s_text'])?></textarea>

</li>

<li>

<label for="content">Основное содержание</label>

<textarea name="page_content" id="content" placeholder="HTML содержание статьи" required maxlength="100000" style="height: 30em;"><?php echo htmlspecialchars($comrow['page_content'])?></textarea>

</li>

<li>

<label for="level">Уровень доступа</label>

<select name="page_level" id="level" required maxlength="1">

<?php $catname=array('Все','Пользователи','Редактор');

if (!isset($comrow['page_level'])) $comrow['page_level']=0;

for ($i=0;$i<3;$i++){

echo '<option value="'.htmlspecialchars($i).'"';

if ($comrow['page_level']==$i) echo ' selected';

echo '>'.htmlspecialchars($catname[$i]).'</option>';

}?>

</select>

</li>

<li>

<label for="publish">Опубликовать</label>

<input type="checkbox" id="publish" name="page_publish" value="Y" <?php if((isset($comrow['page_publish']) and ($comrow['page_publish']=='Y')) or ($_SESSION['action']=='newPage')){?> checked="checked" <?php }?> />

</li>

</ul>

</div>

<div class="buttons">

<?php

switch ($_SESSION['action']) {

case 'editPage':

case 'deletePage': ?>

<input type="submit" name="actionEdit" value="Сохранить" />

<input type="submit" name="actionEdit" value="Удалить" onclick="return confirm('Удалить эту статью?')"/>

<?php

break;

case 'newPage': ?>

<input type="submit" name="actionEdit" value="Сохранить" />

<?php

break;

}

?>

<input type="submit" formnovalidate name="actionEdit" value="Отмена" />

</div>

 

</form>

 

В браузере это будет выглядеть так (рис. 3):

pic03

 

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

 

Разберем код по строкам.

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

 

<h1><?php echo $_SESSION['page_act'].' '.$comrow['page_h1']?></h1>

Внутри тега h1 выводим две переменные через пробел: $_SESSION['page_act'] из куки — она будет хранить тип действия — редактирование, создание, удаление; $comrow['page_h1'] — заголовок из массива comrows, где будем хранить данные редактируемой строки таблицы.

 

<form method="post">

Определяем метод запроса, создаваемого формой. Как Вы видите, это будет post-запрос.

 

<input type="hidden" name="page_id" value="<?php echo $comrow['page_id'] ?>"/>

Скрытое поле, не отображаемое в браузере, служит для хранения id страницы. Оно не важно для пользователя, но необходимо для нас.

 

<?php if ( isset( $_SESSION['errorMessage'] ) ) { ?>

<div class="errorMessage"><?php echo $_SESSION['errorMessage'];

unset( $_SESSION['errorMessage'] ); ?></div>
<?php } ?>

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

 

<div class="editfrm">

Открываем блок, содержащий собственно форму. Каждому элементу будем присваивать класс для того, чтобы потом в css описать его отображение.

Компоненты формы расположим в иерархической структуре

 

<ul>

<li>

</li>

<li>

</li>

</ul>

<ul>

<li>

<label for="title">Название</label>

<input type="text" name="page_title" id="title" placeholder="Название для заголовка меню" required autofocus maxlength="255" value="<?php echo htmlspecialchars( $comrow['page_title'] )?>" />

</li>

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

 

<li>

<label for="p_alias">Алиас</label>

<input type="text" name="page_alias" id="p_alias" placeholder="Наименование анг. буквами для формирования меню" required maxlength="255" value="<?php echo htmlspecialchars($comrow['page_alias'])?>" />

</li>

Поле алиаса. Небольшое отступление. Ссылки на контент можно формировать и по номеру id, тогда в строке браузера пользователь увидит что-то типа <наш сайт>/pages/edititem1023.htm. Если же мы хотим видеть там хотя бы <наш сайт>/pages/editVvedenie.htm, так называемые человеко-понятные ссылки, нам нужен этот алиас. С этим алиасом связана одна проблема — он должен быть уникальным, что добавляет нам еще работу по обеспечению этой уникальности. Но тру-джедай не ищет простых путей.

 

<li>

<label for="ph1">Название для заголовка статьи</label>

<input type="text" name="page_h1" id="ph1" placeholder="Название для заголовка страницы" required maxlength="255" value="<?php echo htmlspecialchars($comrow['page_h1'])?>" />

</li>

Отображаемое название на странице в теге h1. Оно может отличаться и от заголовка в меню, и, несомненно, от ссылки-алиаса.

 

<li>

<label for="cat">Категория статьи</label>

<select name="page_category" id="cat" required placeholder="Выберите категорию статьи" maxlength="255">

<?php $catname=array('Страницы','Записи','Объявления');

if (!isset($comrow['page_category'])) $rows['page_category']=1;

for ($i=0;$i<3;$i++){

echo '<option value="'.htmlspecialchars($i).'"';

if ($comrow['page_category']==$i) echo ' selected';

echo '>'.htmlspecialchars($catname[$i]).'</option>';

}?>

</select>

</li>

Поле выбора категории. Определение метки нам уже знакомо, а вот комбобокс мы определяем впервые. Открываем тег select с именем "page_category", создаем массив $catname с данными, затем проверяем наличие переменной $rows['page_category'], если она не определена, устанавливаем её в 1 ('Записи'). Затем открываем цикл, где задаем значения комбобокса, одновременно устанавливая значение по умолчанию.

 

<li>

<label for="summary">Краткое содержание</label>

<textarea name="page_s_text" id="summary" placeholder="Краткое содержание для блога" required maxlength="1000" style="height: 5em;"><?php echo htmlspecialchars($comrow['page_s_text'])?></textarea>

</li>

Краткое содержание необходимо для вывода в разного рода списках и заголовках блогов. Является простым текстовым полем.

 

<li>

<label for="content">Основное содержание</label>

<textarea name="page_content" id="content" placeholder="HTML содержание статьи" required maxlength="100000" style="height: 30em;"><?php echo htmlspecialchars($comrow['page_content'])?></textarea>

</li>

Основное содержание — текстовое поле для ввода html-кода статьи, пока работаем без WYSIWYG редакторов, которые есть зло (поясню позже).

 

<li>

<label for="level">Уровень доступа</label>

<select name="page_level" id="level" required maxlength="1">

<?php $catname=array('Все','Пользователи','Редактор');

if (!isset($comrow['page_level'])) $comrow['page_level']=0;

for ($i=0;$i<3;$i++){

echo '<option value="'.htmlspecialchars($i).'"';

if ($comrow['page_level']==$i) echo ' selected';

echo '>'.htmlspecialchars($catname[$i]).'</option>';

}?>

</select>

</li>

Поле уровня доступа устанавливает группу, имеющую право просматривать эту статью. Право редактирования мы можем устанавливать программно. Определение комбобокса аналогично категориям.

 

<li>

<label for="publish">Опубликовать</label>

<input type="checkbox" id="publish" name="page_publish" value="Y" <?php if((isset($comrow['page_publish']) and ($rows['page_publish']=='Y')) or ($_SESSION['action']=='newPage')){?> checked="checked" <?php }?> />

</li>

</ul>

</div>

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

 

<div class="buttons">

Открываем блок кнопок.

 

<?php

switch ($_SESSION['action']) {

case 'editPage':

case 'deletePage': ?>

<input type="submit" name="actionEdit" value="Сохранить" />

<input type="submit" name="actionEdit" value="Удалить" onclick="return confirm('Удалить эту статью?')"/>

<?php

break;

case 'newPage': ?>

<input type="submit" name="actionEdit" value="Сохранить" />

<?php

break;

}

?>

В соответствии со значением переменной $_SESSION['action'] выводим на экран кнопки. Если происходит редактирование или удаление, показываем все кнопки, если же мы создаем новую страницу — тогда только "Сохранить".

 

<input type="submit" formnovalidate name="actionEdit" value="Отмена" />

</div>

 

</form>

Кнопку «Отмена» показываем всегда, закрываем блок кнопок и вообще заканчиваем форму.

Итак, нашей форме нужны переменные

// Входные данные -> $ comrow

$comrow['page_h1']

$comrow['page_title']

$comrow['page_alias']

$comrow['page_category']

$comrow['page_s_text']

$comrow['page_content']

$comrow['page_level']

$comrow['page_publish']

 

//$_SESSION['errorMessage']

//$_SESSION['page_act']

А результатом выполнения будет отправка post-запроса с переменной actionEdit. Пока отставим это в сторонку, перейдем к форме списка статей. Подготовку данных в формы разберем позже.

 

8. Форма списка статей.

 

Создаем в каталоге mod подкаталог mod_list, в котором еще два каталога data и view (рис. 4).

pic04

В каталоге view создадим файл mod_listForm.php со следующим содержанием:

 

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

?>

<form method="post">

<input type="hidden" name="listPages" value="true" />

<?php if ( isset( $_SESSION['errorMessage'] ) ) { ?>

<div class="errorMessage"><?php echo $_SESSION['errorMessage'];

unset( $_SESSION['errorMessage'] ); ?></div>

<?php }

?>

<filter class="filter">

<?php if(isset($_SESSION['filter_show']) and $_SESSION['filter_show']=='1') {?>

<ul>

<li>

<label for="filter_publish">Показывать статьи</label>

<select name="filter_publish" id="filter_publish" required maxlength="16">

<?php $pubname=array('Опубликованные','Неопубликованные','Все');

if (!isset($_SESSION['filter_publish'])) $_SESSION['filter_publish']='_';

$ordname=array('Y','N','_');

for ($i=0;$i<3;$i++){

echo '<option value="'.htmlspecialchars($ordname[$i]).'"';

if ($_SESSION['filter_publish']==$ordname[$i]) echo ' selected';

echo '>'.htmlspecialchars($pubname[$i]).'</option>';

}?>

</select>

</li>

<li>

<label for="order_field">Сортировать по</label>

<select name="order_field" id="order_field" required maxlength="16">

<?php $pubname=array('Название','Дата создания','Дата изменения');

if (!isset($_SESSION['order_field'])) $_SESSION['order_field']='page_created';

$ordname=array('page_title','page_created','page_modified');

for ($i=0;$i<3;$i++){

echo '<option value="'.htmlspecialchars($ordname[$i]).'"';

if ($_SESSION['order_field']==$ordname[$i]) echo ' selected';

echo '>'.htmlspecialchars($pubname[$i]).'</option>';

}?>

</select>

</li>

<li>

<label for="filter_order">Показывать статьи</label>

<select name="filter_order" id="filter_order" required maxlength="14">

<?php $pubname=array('По возрастанию','По убыванию');

if (!isset($_SESSION['filter_order'])) $_SESSION['filter_order']='DESC';

$ordname=array('ASC','DESC');

for ($i=0;$i<2;$i++){

echo '<option value="'.htmlspecialchars($ordname[$i]).'"';

if ($_SESSION['filter_order']==$ordname[$i]) echo ' selected';

echo '>'.htmlspecialchars($pubname[$i]).'</option>';

}?>

</select>

</li>

</ul>

<div class="buttons">

<input type="submit" id="filter_show" name="filter_show" value="Сбросить фильтр" />

<input type="submit" id="filter_show" name="filter_show" value="Применить фильтр" />

<input type="submit" id="filter_show" name="filter_show" value="Скрыть фильтр" />

</div>

 

<?php }

else {?>

<div class="buttons">

<input type="submit" name="filter_show" value="Показать фильтр" />

</div>

<?php }?>

 

 

</filter>

 

<table>

<tr>

<th></th>

<?php for ($j=1;$j<count($tabletitle)+1;$j++){?>

<th><?php echo htmlspecialchars($tabletitle[$j]);?></th>

<?php }?>

</tr>

 

<?php

$i=1;

if ($rows) {

foreach ( $rows as $row ) { ?>

<tr>

<td><input type="radio" name="page_id" value=<?php echo htmlspecialchars($row['page_alias']);?> required <?php if ($i=='1') {?>checked="checked"<?php }?>/></td>

<?php for ($j=1;$j<count($curfield);$j++){?>

<td><?php echo htmlspecialchars($row[$curfield[$j]]);?></td>

<?php }?>

<?php $i=$i+1; ?>

</tr>

 

<?php }}?>

</tr>

 

</table>

 

<p>Страница <?php echo $curpage;?> из <?php echo $_SESSION['pages'];?>.</p>

 

<div class="buttons">

<?php if ($curpage>1) { ?>

<input type="submit" name="browsePages" value="1" />

<?php }

if ($curpage>2) { ?>

<input type="submit" name="browsePages" value="Предыдущая" />

<?php }

if ($curpage<($_SESSION['pages'])-1) { ?>

<input type="submit" name="browsePages" value="Следующая" />

<?php }

if ($curpage<($_SESSION['pages'])) { ?>

<input type="submit" name="browsePages" value="Последняя" />

<?php } ?>

</div>

<?php if ($_SESSION['user_level']>1) {?>

<div class="buttons">

<input type="submit" name="actionPage" value="Создать" />

<?php if ($rows) {?>

<input type="submit" name="actionPage" value="Редактировать" />

<input type="submit" name="actionPage" value="Удалить" />

<?php } ?>

</div>

<?php } ?>

</form>

 

Перевожу на почти русский. Сначала традиционно проверяем константу INDEX и выводим сообщения, если они есть. Затем открываем блок <filter>, в котором показываем фильтр, если переменная $_SESSION['filter_show'] установлена и равна 1. То есть, по умолчанию, фильтр скрыт и есть только кнопка «Показать фильтр».

В фильтре три комбобокса и три кнопки, построены по тому же принципу, что и в предыдущей форме, поэтому подробно описывать не буду. Отмечу лишь, что они формируют POST-запрос с переменными filter_publish, order_field, filter_order и filter_show, а их значение определяется тремя переменными из куки $_SESSION['filter_publish'], $_SESSION['order_field'] и $_SESSION['filter_order'].

Далее идет блок-таблица, который открывается тэгом <table>.

 

<tr>

<th></th>

<?php for ($j=1;$j<count($tabletitle)+1;$j++){?>

<th><?php echo htmlspecialchars($tabletitle[$j]);?></th>

<?php }?>

</tr>

Это первая строка таблицы (тег <tr> ограничивает строки). Первый столбец без названия (пустой тег <th></th>). Следующие столбцы озаглавливаем в цикле из массива $tabletitle.

Далее проверяем наличие данных в массиве $rows, и, если этот массив непустой, выводим в цикле все его строки, выбирая названия полей по массиву $curfield. Обращаю Ваше внимание на тот факт, что строк в списке может быть любое количество. Чтобы не получить список из полутора тысяч строк, мы должны озаботиться о том, чтобы в этот массив попало нужное количество строк. Другими словами, надо будет разбить данные на страницы в соответствии с константой ROWSPERPAGE, определенной в core.php. Поэтому ниже таблицы мы и выводим номер страницы списка:

<p>Страница <?php echo $curpage;?> из <?php echo $_SESSION['pages'];?>.</p>

 

Далее выводим кнопки перехода по страницам browsePages, для красоты проверяя некоторые условия. Если номер страницы больше 1, тогда отображаем кнопку «1» (первая страница), если номер больше 2, тогда еще и кнопку «Предыдущая». Если номер страницы меньше количества страниц $_SESSION['pages'], покажем кнопку «Последняя», а если она и не предпоследняя, тогда покажем кнопку «Следующая».

В конце — группа кнопок actionPage, отправляющих выбранное действие.

 

Итак, для работы формы нужны переменные

$rows

$_SESSION['filter_show']

$_SESSION['pages']

$curpage

$_SESSION['filter_publish']

$_SESSION['order_field']

$_SESSION['filter_order']

$tabletitle

$curfield

Результатом может быть отправка значений:

filter_publish

order_field

filter_order

filter_show

browsePages

actionPage

 

Как мы видим, здесь необходимо подготовить данные, поэтому в каталоге data создаем файл mod_list.php со следующим содержанием:

 

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

if (isset($_SESSION['user_level']) and $_SESSION['user_level'] > 1) {

//$curtable=$pref;

 

switch ($option) {

case 'pages':

$cat_val='0';

break;

case 'records':

$cat_val='1';

break;

case 'ads':

$cat_val='2';

break;

}

$curfield=array (

0 => 'page_level',

1 => 'page_title',

2 => 'page_s_text',

3 => 'page_created',

4 => 'page_modified'

);

$tabletitle=array (

1 => 'Название',

2 => 'Краткое содержание',

3 => 'Создано',

4 => 'Изменено',

);

if (!isset($_SESSION['filter_publish'])) $_SESSION['filter_publish']='_';

if (!isset($_SESSION['order_field'])) $_SESSION['order_field']='page_created';

if (!isset($_SESSION['filter_order'])) $_SESSION['filter_order']='DESC';

if (!isset($_SESSION['firstrow'])) { // при выходе на другую таблицу (смена option) надо удалять переменную unset($_SESSION[firstrow])

$_SESSION['firstrow']='0';

$sql="SELECT * FROM pages WHERE ?n<=?i AND page_category=?i AND page_publish LIKE ?s";

$rows=$db->getAll($sql, $curfield[0], $_SESSION['user_level'], $cat_val, , $_SESSION['filter_publish']);

$pagecount=count($rows);

$_SESSION['pages']=intval(($pagecount-1)/ROWSPERPAGE)+1;

}

$curpage=intval($_SESSION['firstrow']/ROWSPERPAGE)+1;

$sql="SELECT * FROM pages WHERE ?n<=?i AND page_category=?i AND page_publish LIKE ?s ORDER BY ?n ".$_SESSION['filter_order']." LIMIT ?i,?i";

$rows=$db->getAll($sql, $curfield[0], $_SESSION['user_level'], $cat_val, $_SESSION['filter_publish'], $_SESSION['order_field'], $_SESSION['firstrow'], ROWSPERPAGE);

 

include(MODPATH."mod_list/view/mod_listForm.php");

 

}

?>

 

Сначала по переменной $alias устанавливаем значение переменной $cat_val(категория статьи), которая используется в запросе. Далее объявляем и определяем массивы $curfield (названия полей для формирования таблицы) и $tabletitle (заголовки). После этого устанавливаем значения по умолчанию, если они не были определены ранее. Переменная $_SESSION['firstrow'] ранее не встречалась, мы её вводим для запоминания номера первой строки в выводимом списке. Например, всего 40 записей, мы выводим по 10 на странице, и смотрим третью страницу, тогда первая строка в списке имеет номер 21.

Следующие строки требуют более подробного описания — это наше первое обращение к mysql с помощью библиотеки safеmysql (http://phpfaq.ru/safemysql). Прелесть этого инструмента в том, что он проверяет корректность подставляемых в запрос переменных, что значительно повышает безопасность кода и упрощает написание запросов. Описание самой библиотеки смотрите на сайте автора, мы же рассмотрим применение на конкретном примере. Здесь используются несколько плейсхолдеров («местодержателей» - если перевести буквально): ?n — имя, ?i — целое значение, ?s — строка (а также DATE, FLOAT и DECIMAL), и одна функция — getAll. Строка

$sql="SELECT * FROM pages WHERE ?n<=?i AND page_category=?i AND page_publish LIKE ?s;

формирует SQL-запрос, содержащий плесхолдеры (выделены жирным). Следующая строка

$rows=$db->getAll($sql, $curfield[0], $_SESSION['user_level'], $cat_val, $_SESSION['filter_publish']);

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

Обратите внимание, что $_SESSION['firstrow'] хранится в куке, поэтому при смене алиаса или фильтра необходимо уничтожать эту переменную, иначе мы получим неверное отображение. Поэтому переменная $_SESSION['alias'] для запоминания текущей категории статей должна храниться в куке.

Далее определяем количество строк, разбиваем на страницы, вычисляем текущую страницу и формируем массив соответствующих строк. Запрос аналогичен предыдущему, лишь в конце добавляется сортировка в соответствии с фильтром (ORDER BY ?n ".$_SESSION['filter_order'].") и выборка соответствующих строк (LIMIT ?i,?i). После формирования массива подключаем форму mod_listForm.php.

 

9. База данных.

Напомню, что данные будем хранить в mysql-базе dingo09, пользователь proba c паролем proba. Сейчас немного стала проясняться структура требуемой базы данных. Первая база — pages, где будут храниться наши статьи. Для этого создаем файл pages.sql со следующим содержимым:

-- Структура таблицы `pages`

 

DROP TABLE IF EXISTS `pages`;

CREATE TABLE IF NOT EXISTS `pages` (

`page_id` int(10) unsigned NOT NULL AUTO_INCREMENT,

`page_alias` varchar(255) NOT NULL COMMENT 'Наименование для меню',

`page_title` varchar(255) NOT NULL COMMENT 'Человеческое название',

`page_level` smallint(6) NOT NULL DEFAULT '0' COMMENT 'Уровень доступа',

`page_category` smallint(6) NOT NULL DEFAULT '1' COMMENT 'Категория',

`page_meta_d` varchar(255) DEFAULT NULL COMMENT 'метаописание',

`page_meta_k` varchar(255) DEFAULT NULL COMMENT 'мета ключи',

`page_h1` varchar(255) DEFAULT NULL COMMENT 'заголовок',

`page_s_text` text COMMENT 'краткое содержание',

`page_content` mediumtext NOT NULL COMMENT 'собсно текст',

`page_publish` char(1) DEFAULT NULL COMMENT 'опубликовать — 1',

`page_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Дата.время создания',

`page_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'дата время изменения',

PRIMARY KEY (`page_id`)

UNIQUE KEY `page_alias` (`page_alias`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1001 ;

 

Должно быть все понятно, специально добавил поле Comment, в боевой базе можно убрать.

Обратите внимание, значения page_id автоматически увеличивается на 1, начиная с 1001. Первые 1000 номеров зарезервируем для внутренних нужд (могут потребоваться разные технологические страницы). Обязательные для заполнения значения page_alias, page_title, page_content, остальные имеют определенные значения по умолчанию. Для категорий отдельной таблицы делать пока не будем, просто запомним, что 0 — страницы, 1 — записи, 2- объявления.

 

Следующая таблица — users:

-- Структура таблицы `users`

 

DROP TABLE IF EXISTS `users`;

CREATE TABLE IF NOT EXISTS `users` (

`user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,

`user_fam` varchar(32) NOT NULL COMMENT 'Фамилия',

`user_name` varchar(32) NOT NULL COMMENT 'Имя',

`user_otch` varchar(32) DEFAULT NULL COMMENT 'Отчество',

`user_log` varchar(32) DEFAULT NULL COMMENT 'Логин',

`user_pw` varchar(255) DEFAULT NULL COMMENT 'Пароль',

`user_level` smallint(6) NOT NULL DEFAULT '1' COMMENT 'Уровень доступа 1-юзер, 2-редактор, 3-админ',

`user_active` char(1) DEFAULT 'Y' COMMENT 'Активность',

`user_email` varchar(255) DEFAULT NULL COMMENT 'Почта',

`user_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Дата создания',

`user_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Дата модификации',

`user_last_vizit` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Последний вход',

PRIMARY KEY (`user_id`)

UNIQUE KEY `user_log` (`user_log`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;

 

-- Дамп данных таблицы `users`

 

INSERT INTO `users` (`user_id`, `user_fam`, `user_name`, `user_otch`, `user_log`, `user_pw`, `user_level`, `user_active`, `user_object`, `user_1Cid`, `user_created`, `user_modified`, `user_last_vizit`) VALUES

(1, 'Иванов', 'Иван', NULL, 'user1', 'user1', 3, 'Y', NULL, NULL, '2016-12-31 15:08:21', '0000-00-00 00:00:00', '0000-00-00 00:00:00');

 

Здесь все аналогично, только мы заводим одного пользователя — администратора, иначе нам не зайти. Имя – user1, пароль — user1.

 

Следующая таблица — modules, здесь указываем какие модули и в какой позиции будем выводить. Содержимое modules.sql:

-- модули

 

DROP TABLE IF EXISTS modules;

CREATE TABLE modules

(

module_id int unsigned NOT NULL auto_increment,

module_title varchar(255) NOT NULL COMMENT 'Имя',

module_mod varchar(255) NOT NULL COMMENT 'наименование файла',

module_text varchar(255) COMMENT 'Описание',

module_param text COMMENT 'Параметры',

module_pos varchar(32) COMMENT 'позиция',

module_order smallint NOT NULL COMMENT 'Порядок в позиции',

module_level smallint NOT NULL COMMENT 'Уровень доступа 0-анон, 1-юзер, 2-редактор, 3-админ',

module_publish char DEFAULT 'Y' COMMENT 'Y(default) - если опубликовано и N — скрыто',

module_created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,

module_modified timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',

PRIMARY KEY (module_id)

UNIQUE KEY `module_title` (`module_title`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

 

Ну и пока последняя таблица — menu, файл menu.sql:

-- --------------------------------------------------------

 

-- Структура таблицы `menu`

 

DROP TABLE IF EXISTS `menu`;

CREATE TABLE IF NOT EXISTS `menu` (

`item_id` int(10) unsigned NOT NULL AUTO_INCREMENT,

`item_alias` varchar(32) NOT NULL COMMENT 'Алиас для ссылки',

`item_name` varchar(32) NOT NULL COMMENT 'Человеческое имя',

`item_parent` varchar(32) DEFAULT NULL COMMENT 'Родительский пункт',

`item_level` smallint(6) NOT NULL DEFAULT '2' COMMENT 'уровень доступа',

`item_active` char(1) DEFAULT 'Y' COMMENT 'Опубликовать — Y',

`item_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'дата.время создания',

`item_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'дата время изменения',

PRIMARY KEY (`item_id`)

UNIQUE KEY `item_alias` (`item_alias`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=8 ;

 

-- Дамп данных таблицы `menu`

 

INSERT INTO `menu` (`item_id`, `item_alias`, `item_name`, `item_parent`, `item_path`, `item_level`, `item_active`) VALUES

(1, 'pages', 'Страницы', NULL, 2, 'Y'),

(2, 'records', 'Записи', NULL, 2, 'Y'),

(3, 'ads', 'Объявления', NULL, 2, 'Y'),

(4, 'users', 'Пользователи', NULL, 3, 'Y'),

(5, 'contacts', 'Контакты', NULL, 2, 'Y'),

(6, 'modules', 'Модули', NULL, 3, 'Y'),

(7, 'mainmenu', 'Меню', NULL, 3, 'Y');

Здесь обратите внимание на добавляемые данные. Во-первых, меню сформировано только для пользователей с уровнем доступа 2 и 3, то есть редактор и администратор. Для пользователей мы пока код не сделали, а для посетителей меню будет формироваться только по именам страниц.

Теперь создаем базу данных и импортируем в неё наши таблицы.

 

10. Формы для посетителей.

Форму списка изменим таким образом, чтобы выбор элемента происходил кликом по соответствующей строке. Для этого изменим тэг, открывающий строку таблицы:

<tr onclick="location='/<?php echo htmlspecialchars( $option ); ?>/<?php echo htmlspecialchars( $row['page_alias'] ); ?>.htm'">

Например, мы щелкнули по строке, содержащей запись с названием «Запись 17». Тогда мы получим ссылку «/records/record17.htm». Кроме этого, удалим кнопки редактирования. Мы не случайно создаем отдельную форму просмотра для посетителей. Конечно, можно добавить проверки уровня пользователя в форме, в зависимости от этого показывать или скрывать какие-то элементы. Вместо этого мы делаем лишь одну проверку и сразу включаем соответствующую форму, избегая лишних проверок и загрузки в память лишнего кода. Да, это увеличивает общий объем, но ускоряет выполнение кода.

Аналогично поступаем с формой редактирования, превращая её в mod_showForm.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

// Входные данные -> $rows

//$_SESSION['page_id']

//

//

?>

<h1><?php echo htmlspecialchars($rows['page_h1'])?></h1>

 

 

<?php if ( isset( $_SESSION['errorMessage'] ) ) { ?>

<div class="errorMessage"><?php echo htmlspecialchars($_SESSION['errorMessage']);

unset( $_SESSION['errorMessage'] ); ?></div>

<?php } ?>

 

<div class="content">

<?php echo htmlspecialchars($rows['page_content'])?>

</div>

 

Соответственно mod_edit.php приобретает вид:

 

11. Записи и объявления.

Модули, отображающие записи и объявления в виде бложиков, выглядят совсем просто.

Создаем в каталоге mod подкаталоги mod_ads и mod_rec с уже знакомой структурой (data и view), не забывая про заглушки index.html. Содержимое /mod/mod_ads/data/mod_ads.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

if (isset($_SESSION['user_level'])) {

$sql="SELECT * FROM pages WHERE page_level<=?i AND page_category=2 AND page_publish LIKE 'Y' ORDER BY page_modified DESC";

$rows=$db->getAll($sql, $_SESSION['user_level']);

 

include(MODPATH."mod_ads/view/mod_adview.php");

}

?>

 

Берем все актуальные объявления, без ограничения количества. Пусть его ограничивает редактор сайта. Содержимое /mod/mod_ads/view/mod_adview.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

if ($rows) {

echo '<h2>Объявления</h2>';

foreach ( $rows as $row ) { ?>

<div id="rec" class="wow1">

<h1><?php echo htmlspecialchars( $row['page_s_text'] ) ?></h1>

<p><?php echo htmlspecialchars( $row['page_content'] ) ?></p>

<p><?php echo htmlspecialchars( $row['page_modified'] ) ?></p>

</div>

<?php }

}

?>

Отображаем только три поля. Класс блока wow1 для возможности с помощью css выделить его на странице.

Содержимое /mod/mod_rec/data/mod_rec.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

if (isset($_SESSION['user_level'])) {

$sql="SELECT * FROM pages WHERE page_level<=?i AND page_category=1 AND page_publish LIKE 'Y' ORDER BY page_modified DESC LIMIT 0, 5";

$rows=$db->getAll($sql, $_SESSION['user_level']);

 

include(MODPATH."mod_rec/view/mod_recview.php");

 

}

?>

Здесь мы отображаем только 5 последних записей.

Содержимое /mod/mod_rec/view/mod_recview.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

if ($rows) {

echo '<a href="/records/list.htm" ><h2>Наш блог</h2></a>';

foreach ( $rows as $row ) { ?>

<div id="rec" class="wow2">

<a href="/records/<?php echo htmlspecialchars( $row['page_alias'] ); ?>.htm" ><h1><?php echo htmlspecialchars( $row['page_title'] ); ?></h1></a>

<p><?php echo htmlspecialchars( $row['page_s_text'] ); ?></p>

<p><?php echo htmlspecialchars( $row['page_modified'] ); ?></p>

</div>

<?php }

}

?>

Так как мы показываем только пять записей, в первой строке делаем ссылку на просмотр всех записей нашего блога. При этом каждая отображаемая запись имеет заголовок — ссылку на просмотр соответствующей записи целиком. Стиль указали wow2, чтоб отличался от объявлений.

 

12. Шаблон.

Я думаю, Вы уже заметили, что порядок создания CMS у нас какой-то странный. Возможно, даже беспорядочный порядок. На самом деле мы начинаем с интерфейса, затрагивая по пути некоторые «внутренние» элементы, и в самом конце подойдем к собственно устройству CMS.

Итак, шаблон. В нашем случае это будет один файл, в котором мы опишем расположение и порядок вывода модулей на нашем сайте. Для начала мы используем некоторые элементы, живущие в разных CMS (Joomla, Wordpress и т. д.), потом попробуем оптимизировать код в соответствии с нашей конкретной задачей. Итак, шаблон у нас будет простой, исходить мы будем из 12-тиколоночного дизайна страницы (рис. 5).

pic05

Содержимое файла /template/template.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

// Ширина контента

if ($cmodules[left] && $cmodules[right])

{

$span = "span6";

}

elseif ($cmodules[left] && !$cmodules[right])

{

$span = "span9";

}

elseif (!$cmodules[left] && $cmodules[right])

{

$span = "span9";

}

else

{

$span = "span12";

}

?>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">

<head>

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<meta charset="utf-8" />

<base href="/http://<?php echo ($_SERVER[HTTP_HOST])?>/" />

<meta name="keywords" content="<?php echo htmlspecialchars( $kwd );?>" />

<meta name="rights" content="&quot;Di N Go&quot;" />

<meta name="description" content="<?php echo htmlspecialchars( $dscrpt );?>" />

<link href="/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />

<!--выводим заголовок в браузер-->

<title>

<?php echo htmlspecialchars( $title );?>

</title>

<link rel="stylesheet" type="text/css" href="/css/style.css" />

<!--[if lt IE 9]>

<script src="/js/html5.js"></script>

<![endif]-->

</head>

 

<body>

<div class="body">

<p><a name="top"></a></p>

<div class="container">

<header class="header">

<a href="/."><img id="logo" src="/images/logo.png" alt="Основной сайт" /></a>

</header>

 

<!-- горизонтальное меню -->

<?php if ($cmodules[top]) : ?>

<!-- включаем top -->

<?php endif; ?>

<nav class="navigation" role="navigation">

<div>

<ul>

<?php include MODPATH."/mod_menu/data/mod_menu.php";?>

</ul>

</div>

</nav>

<!-- собсно содержимое-->

<div class="row">

<?php if ($cmodules[left]) : ?>

<!-- Начало Sidebar -->

<div id="sidebar" class="span3" type="modules" style="wow1">

<?php

$modpos='left';

include COMPATH."modules/com_modules.php";

?>

</div>

<!-- Конец Sidebar -->

<?php endif; ?>

<main id="content" role="main" class="<?php echo $span; ?>">

<!-- Начало Content -->

<?php if ($cmodules[breadcrumb]) : ?>

<!--Включаем breadcrumb -->

<?php endif; ?>

 

<!-- включаем message -->

<!-- включаем компонент -->

<?php

include COMPATH."/main/com_main.php";

?>

<?php if ($cmodules[descript]) : ?>

<!-- включаем descript -->

<?php endif; ?>

<!-- Конец Content -->

</main>

<?php if ($cmodules[right]) : ?>

<div id="aside" class="span3" type="modules" name="right" style="wow2">

<!-- Начало правой панели -->

<?php

$modpos='right';

include COMPATH."modules/com_modules.php";

?>

<!-- Конец правой панели -->

</div>

<?php endif; ?>

</div>

</div>

</div>

<!-- Футер -->

<footer class="footer">

<div>

<p>&copy; <?php echo htmlspecialchars( NAMEOFSITE ) ?> <?php echo date('Y'); ?>. </p>

<p class="pull-right">

<a href="#top" id="back-top">

<?php echo "Наверх"; ?>

</a>

</p>

</div>

</footer>

</body>

 

</html>

 

Сначала определяем ширину контента в зависимости от наличия левой и правой панели. Если окажется, что в какой-то панели выводить нечего, тогда контент должен будет занять эту ширину. Переменная $span будет хранить вычисленное значение и определять стиль отображения.

Далее начинается собственно html документ. Блок <head> содержит заголовок страницы.

 

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">

Указываем язык страницы, это помогает разным браузерам правильно отображать русские буквы.

 

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

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

 

<meta charset="utf-8" />

Указываем кодировку. Без этого также может неправильно отображаться кириллица.

 

<base href="/http://<?php echo ($_SERVER[HTTP_HOST])?>/" />

<meta name="keywords" content="ключевые слова" />

<meta name="rights" content="&quot;Di N Go&quot;" />

<meta name="description" content="Описание" />

Это различные метатеги. По сути, например, тег keywords должен содержать ключевые слова конкретной страницы. Позже мы это исправим, но пока оставим так.

 

<link href="/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />

Иконка сайта может располагаться где угодно, но для того, чтоб её обнаруживали все роботы и браузеры лучше разместить оную в корне сайта.

 

<title>

<?php echo htmlspecialchars( $title );?>

</title>

Выводим заголовок в браузер (название закладки в браузере).

 

<link rel="stylesheet" type="text/css" href="/css/style.css" />

Подключаем нашу таблицу стилей. Сейчас её нет, просто создадим по этому пути пустой файл.

 

<!--[if lt IE 9]>

<script src="/js/html5.js"></script>

<![endif]-->

Так как старые браузеры не знали ничего о html5, которого тогда просто не было, для совместимости используем java-скрипт (http://html5shiv.googlecode.com/svn/trunk/html5.js). Браузеры обычно обновляются до актуальной версии, но MS просто бросает старую линейку и создает новую, поэтому браузеры до ie9 абсолютно несовместимы с html5.

Далее открываем тег body и блок, которому мы присвоим класс body, первой строкой ставим метку:

<p><a name="top"></a></p>

Она позволит нам использовать ссылку «наверх» внизу страницы.

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

 

<header class="header">

<a href="/."><img id="logo" src="/images/logo.png" alt="Основной сайт" /></a>

</header>

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

 

<!-- горизонтальное меню -->

Это просто комментарий, в боевой версии лучше это убрать. Пользователь его не увидит, но браузер это закачает. Комментарии увеличивают объем сайта.

 

<?php if ($cmodules[top]) : ?>

<!-- включаем top -->

<?php endif; ?>

Если переменная $cmodules[top] определена, тогда выполнится код, которого пока нет. Если мы придумаем, что и как включить в этой позиции, тогда вместо комментария это напишем.

 

<nav class="navigation" role="navigation">

<div>

<ul>

<?php include MODPATH."/mod_menu/data/mod_menu.php";?>

</ul>

</div>

</nav>

В блоке навигации явно указываем только блок списка <ul>, а пункты меню будут формироваться в скрипте /mod/mod_menu/data/mod_menu.php. Горизонтальное меню безусловно включаем в соответствии с нашим шаблоном.

 

<div class="row">

Открываем блок записей.

 

<?php if ($cmodules[left]) : ?>

<!-- Начало Sidebar -->

<div id="sidebar" class="span3" type="modules" style="wow1">

<?php

$modpos='left';

include COMPATH."modules/com_modules.php";

?>

</div>

<!-- Конец Sidebar -->

<?php endif; ?>

Опять проверяем соответствующую переменную ($cmodules[left]), затем открываем блок sidebar. Мы включаем в код компоненту COMPATH."modules/com_modules.php", которая будет выводить модули в соответствии со значением переменной $modpos='left'. Далее аналогично включаем компоненты в остальных позициях.

В футере все совсем просто, обращаю внимание только на

 

<p class="pull-right">

<a href="#top" id="back-top">

<?php echo "Наверх"; ?>

</a>

</p>

Это ссылка к метке <p><a name="top"></a></p> в верхней части страницы, которую мы хотим видеть смещенной к правой части страницы.

 

Итак, перед включением этого кода нам нужно определить переменные $cmodules[], $kwd, $dscrpt, и написать компоненты для вывода соответствующих элементов.

 

13. Первый компонент.

Назовем его просто page.php. Напомню, что это первый файл, относящийся собственно к нашему сайту, который должен определить переменные, необходимые для отображения страницы в целом и основного контента в частности. Здесь же мы будем использовать много включений (инклудов) для того, чтобы разгрузить компилятор при выполнении кода. Конечно, сами эти инклуды замедляют выполнение программы, так как приходится подгружать файлы во время работы программы. На эту тему много сломано копий, прошло немало холиваров, но истина так и осталась непознанной. И мы не будем заморачиваться, а используем этот инструмент на полную. Итак, код:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

/* КОМПОНЕНТ СТРАНИЦЫ */

/* Здесь формируем те данные, который нужны для отображения в шаблоне */

if ($option === NULL) {

$option='pages';

}

elseif ($option=='logout') {

session_start();

session_destroy();

$_SESSION['user_id'] = '0';

$_SESSION['user_level'] = '0';

$alias = 'home';

$option='pages';

header("Location: http://".$_SERVER['HTTP_HOST']);

}

switch ($_SESSION['user_level']) {

case 0:

include(COMPATH."page/pageanon.php");

break;

case 1:

include(COMPATH."page/pagecommunity.php");

break;

case 2:

include(COMPATH."page/pageeditor.php");

break;

case 3:

include(COMPATH."page/pageadmin.php");

break;

default:

session_start();

session_destroy();

$_SESSION['user_id'] = '0';

$_SESSION['user_level'] = '0';

$alias = 'home';

$option='pages';

header("Location: http://".$_SERVER['HTTP_HOST']);

break;

} //switch userlevel

// ОПРЕДЕЛЯЕМ cmodules - НАЛИЧИЕ САЙДЕБАРОВ ДЛЯ ОПРЕДЕЛЕНИЯ SPAN И ВЫВОДА МОДУЛЕЙ

$sql="SELECT DISTINCT module_pos FROM modules WHERE module_level<=?i AND module_publish='Y'";

$positions = $db->getCol($sql, $_SESSION[user_level]);

if ($positions) {

foreach ( $positions as $position ) {

$cmodules[$position]='1';

}

}

 

?>

Здесь все просто.

Сначала проверяем переменную option и при необходимости устанавливаем ей значение по умолчанию. Затем идет блок выхода, реализованный просто как сброс всех сессионных переменных.

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

Теперь обратимся к com/page/pageanon.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

// дела анона

switch ( $option ) {

case 'pages':

$pcat='0';

if ($alias === NULL) {

$alias='home';

}

include(COMPATH."page/pagecon.php");

break;

case 'records':

$pcat='1';

if ($alias === NULL) {

$alias='list';

break;

}

include(COMPATH."page/pagecon.php");

break;

case 'ads':

$pcat='2';

if ($alias === NULL) {

$alias='list';

break;

}

include(COMPATH."page/pagecon.php");

break; case 'pricab':

break;

default:

$option='pages';

header("HTTP/1.1 404 Not Found");

$alias='page404';

include(COMPATH."page/pagecon.php");

}

?>

Здесь сразу бросается в глаза включение кода

include(COMPATH."page/pagecon.php");

Одинаковое для всех. Можно было сделать функцию, но тогда придется подавать в нее много переменных, мы же хотим простого решения — поэтому делаем инклуд. Еще обращаю Ваше внимание на обработку пустого $alias. Для страниц по умолчанию подставляем home — для всего остального — list. Для администратора же во всех случаях будет list. Остальные файлы здесь приводить не буду — в них все идентично устроено. Различаться они будут только в соответствии с доступным меню каждого уровня.

Переходим к файлу com/page/pagecon.php (как бы контент):

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

/* КОНТЕНТ СТРАНИЦЫ */

/* Здесь формируем те данные, который нужны для отображения в странице */

if ($alias==='list') {

switch ($option) {

case 'pages':

$dscrpt = 'Страницы';

$kwd = 'Страницы DINGO';

$title = 'Страницы';

break;

case 'records':

$dscrpt = 'Наш блог';

$kwd = 'записи, блог';

$title = 'Записи';

break;

case 'ads':

$dscrpt = 'Наш блог';

$kwd = 'записи, блог';

$title = 'Объявления';

break;

} //switch option

}

else {

if ( $alias ) {

$sql = "SELECT * FROM pages WHERE page_alias=?s AND page_publish='Y' AND page_category=?i AND page_level<=?i";

$comrow = $db->getrow($sql, $alias, $pcat, $_SESSION['user_level']);

// ПЕРЕМЕННЫЕ КОМПОНЕНТА

if ($comrow) {

$page_id = $comrow['page_id'];

$title = $comrow['page_title'];

$h1 = $comrow['page_h1'];

$dscrpt = $comrow['page_meta_d'];

$$kwd = $comrow['page_meta_k'];

$component = $comrow['page_content'];

}

else {

// error404

header("HTTP/1.1 404 Not Found");

$alias='page404';

$sql = "SELECT * FROM pages WHERE page_alias=?s AND page_level<=?i";

$comrow = $db->getrow($sql, $alias, $_SESSION['user_level']);

 

// ПЕРЕМЕННЫЕ КОМПОНЕНТА

if ($comrow) {

$page_id = $comrow['page_id'];

$alias = $comrow['page_alias'];

$title = $comrow['page_title'];

$h1 = $comrow['page_h1'];

$dscrpt = $comrow['page_meta_d'];

$$kwd = $comrow['page_meta_k'];

$component = $comrow['page_content'];

}

}

}

}

?>

Сразу откидываем алиас list, а для действительного алиаса получаем данные. Если запрос пришел пустым (нет страницы), показываем страницу 404 и в header выводим ошибку. Вторую проверку делаем на всякий случай. А страницу эту мы заранее вносим в базу.

В результате получаем все необходимые переменные, что позволит нам начинать вывод данных на страницу.

Также подставляем свою страницу ошибок. Эта страница должна быть в базе, мы её делаем скрытой, можем придать ей такой вид, какой нам нужен. Можно было просто все неправильные адреса менять на дефолтный, тогда на любой запрос пользователь получит какую-то страницу, но поисковики неправильно будут нас индексировать. Они будут считать произвольный адрес валидным. Мы же должны сообщить поисковику, что этот адрес ошибочный (header("HTTP/1.1 404 Not Found")).

Файлы для остальных пользователей формируем по тому же принципу с учетом их меню. Например, первый блок у администраторов в com/page/pageadmin.php будет указывать не на конкретную страницу, а на список:

case 'pages':

$pcat='0';

if ($alias === NULL) {

$alias='list';

break;

}

include(COMPATH."page/pageconad.php");

break;

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

 

14. Меню.

Итак, опорные переменные получены, далее для работы шаблона нужен mod/mod_menu/data/mod_menu.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

switch ($_SESSION['user_level']) {

case 0:

$sql = "SELECT page_title, page_alias FROM pages WHERE page_publish='Y' AND page_category='0' AND page_level<=?i";

$namepages = $db->getAll($sql, $_SESSION[user_level]);

if ($namepages) {

foreach ( $namepages as $namepage ) {

$itemclass=' class="item';

$pagetitle = $namepage['page_title'];

if ($namepage['page_alias']==$alias) {

$itemclass=$itemclass.' current active';

}

if ($namepage['page_alias']=='home') {

$itemclass=$itemclass.' default"';

}

else {

$itemclass=$itemclass.'"';

}

$itemclass=$itemclass.'>';

echo '<li'.$itemclass.'<a href="/pages/'.$namepage['page_alias'].'.htm" >'.$namepage['page_title'].'</a></li>';

unset($itemclass);

}

}

echo '<li><a href="/pricab/home.htm" >Личный кабинет</a></li>';

unset ($namepages);

unset ($namepage);

break;

case 1:

$sql = "SELECT * FROM menu WHERE item_active='Y' AND item_level=?i AND item_parent IS NULL";

$namepages = $db->getAll($sql, $_SESSION[user_level]);

if ($namepages) {

foreach ( $namepages as $namepage ) {

}

}

unset ($namepages);

unset ($namepage);

break;

case 2:

case 3:

$sql = "SELECT * FROM menu WHERE item_active='Y' AND item_level<=?i AND item_level>1 AND item_parent IS NULL";

$namepages = $db->getAll($sql, $_SESSION[user_level]);

if ($namepages) {

foreach ( $namepages as $row ) {

$itemclass=' class="item';

if ($row['item_alias']==$alias) {

$itemclass=$itemclass.' current active';

}

if ($row['item_alias']=='pages') {

$itemclass=$itemclass.' default"';

}

else {

$itemclass=$itemclass.'"';

}

$itemclass=$itemclass.'>';

echo '<li'.$itemclass.'<a href="/'.$row['item_alias'].'/list.htm" >'.$row['item_name'].'</a></li>';

unset($itemclass);

}

}

echo '<li><a href="/logout/home.htm" >Выход</a></li>';

unset ($namepages);

unset ($row);

break;

default: // неведома фигня - сбрасываем все и на главную

session_start();

session_destroy();

$_SESSION['user_id'] = '0';

$_SESSION['user_level'] = '0';

$alias = 'home';

$option='pages';

header("Location: http://".$_SERVER['HTTP_HOST']);

exit;

}

?>

Меню формируется в зависимости от уровня пользователя.

Для анонимусов меню формируется только по названиям страниц, после вывода (echo '<li'.$itemclass.'<a href="/'.$row['item_alias'].'/list.htm" >'.$row['item_name'].'</a></li>';) переменные уничтожаются (unset($itemclass)) для освобождения памяти. Это необязательно, уничтожая переменные, мы тратим на это процессорное время. Сомнительный выигрыш здесь используется просто для примера.

Для администраторов с редакторами меню формируется из базы.

$sql = "SELECT * FROM menu WHERE item_active='Y' AND item_level<=?i AND item_level>1 AND item_parent IS NULL";

$namepages = $db->getAll($sql, $_SESSION[user_level]);

Далее аналогично анонимусам, только в качестве пунктов меню используем полученные данные. Ну и вместо входа — кнопка выхода.

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

 

15. Вывод контента.

Теперь опишем вывод контента, файл com/main/com_main.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

/* Здесь формируется главный компонент */

switch ($_SESSION['user_level']) {

case '0':

switch ($option) {

case 'pages':

case 'records':

case 'ads':

switch ($alias) {

case 'list':

include MODPATH."mod_list/data/mod_list.php";

break;

default:

include MODPATH."mod_edit/data/mod_edit.php";

break;

}

break;

case 'pricab':

include MODPATH."mod_auth/view/mod_authform.php";

break;

default: // неведома фигня - пусто

} //option userlevel 0

case '1':

break;

case '2':

break;

case '3':

switch ($option) {

case 'pages':

case 'records':

case 'ads':

switch ($alias) {

case 'list':

include MODPATH."mod_list/data/mod_list.php";

break;

default:

include MODPATH."mod_edit/data/mod_edit.php";

break;

}

break;

default: // неведома фигня - пусто

 

} //option userlevel 3

 

break;

default: // неведома фигня - пусто

 

} //userlevel

?>

Пока у нас готовы только формы редактирования, поэтому блоки для анонимов и администраторов одинаковые, в реальности, они, конечно будут отличаться. Сейчас только у администраторов нет case ‘pricab’ - авторизации.

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

 

16. Аутентификация пользователей.

Аутентификация — это просто. Делаем простую форму на две строки — имя и пароль. В нашем случае файл /mod/mod_auth/view/mod_authform.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

// ПОКАЗЫВАЕМ ФОРМУ ВХОДА -

?>

<form method="post" style="width: 50%;">

<input type="hidden" name="login" value="true" />

 

<?php if ( isset( $_SESSION['authmsg'] ) ) { ?>

<div class="errorMessage"><?php echo $_SESSION['authmsg'];

unset( $_SESSION['authmsg'] ); ?></div>

<?php } ?>

<ul>

<li>

<label for="username">Имя</label>

<input type="text" name="auth_name" id="username" placeholder="Ваше имя" required autofocus maxlength="20" />

</li>

<li>

<label for="password">Пароль</label>

<input type="password" name="auth_pass" id="password" placeholder="Ваш пароль" required maxlength="20" />

</li>

</ul>

<div class="buttons">

<input type="submit" name="login" value="Вход" />

</div>

</form>

Формы мы уже видели, поэтому здесь вопросов нет. Но неужели мы собираемся отправлять пароль на сервер в открытом виде? Пока — да. Потом мы исправим это, и отправляться будет только хэш, который и будет храниться в базе, но для реализации этого механизма необходимо привлечение дополнительных сущностей, которые утяжелят и без того непростое повествование. Поэтому ни в коем случае не выкладывайте программу в таком виде в эти ваши интернеты. Такая реализация подходит только для внутреннего использования (для тестирования кода) на каком-нибудь локальном Денвере. Результат выполнения отправляется POST-запросом на сервер. Здесь его встречает /com/compost/com_auth.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

$sql = "SELECT * FROM users WHERE user_log=?s";

$row = $db->getRow($sql, $_POST['auth_name']);

if ($row && password_verify($_POST['auth_pass'], $row['pass'])) { // Только >php5.5

// if ($row && ($_POST['auth_pass'] === $row['user_pw'])) { // для старых php <5.3.7

$_SESSION['user_id'] = $row['user_id'];

$_SESSION['user_level'] = $row['user_level'];

$_SESSION['user_log'] = $row['user_log'];

$alias = 'home';

$option='pages';

header("Location: http://".$_SERVER['HTTP_HOST']);

exit;

}

else {

// Ошибка входа: выводим сообщение об ошибке для пользователя

if ($_SESSION[auth_attempts]<2) {

$_SESSION['authmsg'] = "Неправильное имя или пароль. Попробуйте еще раз.";

$_SESSION[auth_attempts]=$_SESSION[auth_attempts]+1;

}

else {

session_start();

session_destroy();

$_SESSION['user_id'] = '0';

$_SESSION['user_level'] = '0';

$alias = '';

$option='';

header("Location: http://".$_SERVER['HTTP_HOST']);

exit;

}

}

header("Location: http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);

exit;

?>

 

Первые две строки запрашивают из базы данные пользователя. Если данные по этому пользователю есть и хэш пароля совпадает, тогда сохраняем в куке уровень пользователя, а если же нет — считаем попытки. После третьей попытки сбрасываем все и на главную.

Рассмотрим более подробно проверку пароля. Мы используем функцию password_verify(). Она присутствует только в php версии >5.5. Для версий старше 5.3.7 есть библиотечка password_compat, реализующая совместимость по этой функции. Просто качаем файл password.php, кладем его в каталог /lib и включаем его в com_auth.php строкой:

require_once(LIBPATH."password.php"); //только для php от 5.3.7 до 5.5

Дальше нам необходимо как-то вписать в базу первого пользователя — суперадминистратора. Для этого мы создаем файл installsa.php:

<?php

if ($_POST) {

define("INDEX", ""); // УСТАНОВКА КОНСТАНТЫ ГЛАВНОГО КОНТРОЛЛЕРА

require_once($_SERVER[DOCUMENT_ROOT]."/cfg/core.php"); // ПОДКЛЮЧЕНИЕ ЯДРА

require_once(LIBPATH."password.php");

require_once(LIBPATH."sfms.php");

$db = new SafeMySQL();

$hash = password_hash($_POST['auth_pass'], PASSWORD_BCRYPT);

$table = 'users';

$data = array('user_fam'=>'Hero',

'user_name'=>'First',

'user_otch'=>'Admin',

'user_log'=>$_POST['auth_name'],

'user_pw'=>$hash,

'user_level'=>'3',

'user_active'=>'Y',

'user_email'=>Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript.',

'user_created'=>date("Y-m-d H:i:s"),

'user_modified'=>date("Y-m-d H:i:s"),

'user_last_vizit'=>'0000-00-00 00:00:00');

$sql = "INSERT INTO ?n SET ?u";

$res=$db->query($sql,$table,$data);

if ($res==1) {

echo 'Администратор установлен!';

}

else {

echo 'Что-то пошло не так! '.$res;

}

exit;

}

// ПОКАЗЫВАЕМ ФОРМУ ВВОДА ПАРОЛЯ -

?>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">

<head>

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<meta charset="utf-8" />

<form method="post">

<h1> Установка суперадминистратора </h1>

 

<ul>

<li>

<label for="username">Имя</label>

<input type="text" name="auth_name" id="username" placeholder="Ваше имя" required autofocus maxlength="20" />

</li>

 

<li>

<label for="password">Пароль</label>

<input type="password" name="auth_pass" id="password" placeholder="Ваш пароль" required maxlength="20" />

</li>

 

</ul>

 

<div class="buttons">

<input type="submit" name="actionEdit" value="Установить" />

</div>

 

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

Первое обращение к этому файлу вызовет форму, так как массив $_POST не определен. Форма содержит только два поля ввода и кнопку «Установить». Так как адрес не меняется, после отправки формы сервер снова запустит этот файл, но теперь уже $_POST существует, поэтому выполнится первая часть, а она заканчивается командой «exit;»,поэтому второй раз форму ввода мы не увидим. Первые строки — установка константы, включение конфигурации и библиотек. В седьмой строке мы видим функцию password_hash(), которая и вычислит хэш нашего пароля. Таким образом, в базе будет храниться хэш, а не сам пароль. Человек, получивший доступ к этой таблице, не получит пароли в чистом виде.

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

 

17. Обработка POST-запросов.

Вы уже видели, как мы обрабатываем такие запросы на примере аутентификации. Мы просто проверим наличие массива $_POST в index.php и проведем анализ этого массива в /com/compost/compost.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

// АУТЕНТИФИКАЦИЯ

if (isset($_POST['auth_name'])){

include(COMPATH."compost/com_auth.php");

}

// Данные форм

elseif (isset($_POST['actionPage'])){

include(COMPATH."compost/com_page.php");

}

elseif (isset($_POST['browsePages'])){

include(COMPATH."compost/com_browse.php");

}

elseif (isset($_POST['actionEdit'])){

include(COMPATH."compost/com_form.php");

}

elseif (isset($_POST['filter_show'])){

include(COMPATH."compost/com_filter.php");

}

else {

include(COMPATH."compost/restartsess.php");

}

?>

В этом файле мы подключаем соответствующую обработку или сбрасываем все, если мы получили непонятный POST. Напомню, что переменная $_POST['auth_name'] устанавливается формой авторизации, $_POST['actionPage'] - формой списка при нажатии кнопки какого-либо действия, $_POST['browsePages'] - при нажатии кнопок листания страниц, $_POST['filter_show'] - при нажатии кнопок в фильтре, и $_POST['actionEdit'] - при нажатии кнопок в форме редактирования.

Файл restartsess.php очень прост по содержанию:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

session_start();

session_destroy();

$_SESSION['user_id'] = '0';

$_SESSION['user_level'] = '0';

$alias = 'home';

$option='pages';

header("Location: http://".$_SERVER['HTTP_HOST']);

exit;

?>

session destroy() перезапускает сессию, потом мы устанавливаем начальные значения и показываем стартовую страницу.

Далее рассмотрим compost/com_page.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

if(isset($_POST['actionPage'])) {

switch ($_POST['actionPage']) {

case 'Создать':

$alias='new';

$_SESSION['action']='newPage';

$_SESSION['page_act']='Новая страница';

break;

case 'Редактировать':

$alias=$_POST['page_alias'];

$_SESSION['action']='editPage';

$_SESSION['page_act']='Редактирование статьи';

break;

case 'Удалить':

$alias=$_POST['page_alias'];

$_SESSION['action']='deletePage';

$_SESSION['page_act']='Удаление статьи';

break;

default:

include(COMPATH."compost/restartsess.php");

}

}

?>

Все очень просто: в зависимости от значения $_POST['actionPage'] выставляем значения переменных и идем дальше. Наша программа по этим данным покажет страничку редактирования или создания статьи с соответствующими кнопками.

Файл compost/com_browse.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

switch ($_POST['browsePages']) {

case "1":

$_SESSION[firstrow]='0';

break;

case "Предыдущая":

$_SESSION[firstrow]=$_SESSION[firstrow]-ROWSPERPAGE;

break;

case "Следующая":

$_SESSION[firstrow]=$_SESSION[firstrow]+ROWSPERPAGE;

break;

case "Последняя":

$_SESSION[firstrow]=($_SESSION['pages']-1)*ROWSPERPAGE;

break;

default:

}

?>

Здесь тоже понятно: если мы нажали кнопку «1» (первая страница) — переменной $_SESSION[firstrow] присваиваем значение «0»; следующая страница — добавляем количество записей на странице, а предыдущая — наоборот, вычитаем; последняя страница — количество страниц без единицы умножаем на количество строк.

Еще одна обработка результатов формы списка — compost/com_filter.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

switch ($_POST['filter_show']) {

case 'Показать фильтр':

$_SESSION['filter_show']=1;

break;

case 'Сбросить фильтр':

$_SESSION['filter_publish']='_';

$_SESSION['order_field']='page_created';

$_SESSION['filter_order']='DESC';

unset($_SESSION['firstrow']);

break;

case 'Применить фильтр':

$_SESSION['filter_publish']=$_POST['filter_publish'];

$_SESSION['order_field']=$_POST['order_field'];

$_SESSION['filter_order']=$_POST['filter_order'];

unset($_SESSION['firstrow']);

break;

default:

unset($_SESSION['filter_show']);

}

?>

С кнопками «показать фильтр» и «скрыть фильтр» все просто. С применением фильтра обязательно сбрасываем переменную $_SESSION['firstrow'], так как сортировка изменилась, это влечет за собой если и не изменение количества строк, то их порядок.

Самая непростая обработка формы редактирования статьи compost/com_form.php:

<?php defined('INDEX') OR die('Прямой доступ к странице запрещён!');

if(isset($_POST['actionEdit'])) {

switch ($_POST['actionEdit']) {

case 'Отмена':

unset($_POST);

switch ($_SESSION['user_level']) {

case 0:

case 1:

// надо вставить сброс всего нафик.

header("Location: http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);

break;

case 2:

case 3:

$alias='list';

header("Location: http://".$_SERVER['HTTP_HOST'].'/'.$option.'/list.htm');

break;

break;

}

exit;

case 'Сохранить':

if ($_SESSION['user_level']>1) {

switch ($_SESSION['action']) {

// при создании

case 'newPage':

switch ($option) {

case 'pages':

case 'records':

case 'ads':

$table = 'pages';

$allowed = array('page_alias','page_title','page_level','page_category','page_meta_d','page_meta_k','page_h1','page_s_text','page_content','page_publish');

$data = $db->filterArray($_POST,$allowed);

if (isset($data['page_id'])) unset($data['page_id']);

if (!isset($data['page_publish'])) $data['page_publish']='N';

$data['page_modified'] = date("Y-m-d H:i:s");

break;

default:

include(LIBPATH."restartsess.php");

}

$sql = "INSERT INTO ?n SET ?u";

$res=$db->query($sql,$table,$data);

if ($res==1) {

$_SESSION['errorMessage']='Данные успешно сохранены';

unset ($_SESSION['action']);

header("Location: http://".$_SERVER['HTTP_HOST'].'/'.$option.'/list.htm');

exit;

}

else {

$_SESSION['errorMessage']=$res;

}

break;

// при редактировании

case 'editPage':

switch ($option) {

case 'pages':

case 'records':

case 'ads':

$table = 'pages';

$allowed = array('page_alias','page_title','page_level','page_category','page_meta_d','page_meta_k','page_h1','page_s_text','page_content','page_publish');

$data = $db->filterArray($_POST,$allowed);

$field_id = 'page_id';

$field_val = $_POST['page_id'];

$data[page_modified] = date("Y-m-d H:i:s");

if (!isset($data['page_publish'])) $data['page_publish']='N';

break;

default:

include(LIBPATH."restartsess.php");

}

 

$sql = "UPDATE ?n SET ?u WHERE ?n=?i";

$res=$db->query($sql,$table,$data,$field_id,$field_val);

if ($res==1) {

$_SESSION['errorMessage']='Данные успешно сохранены';

unset ($_SESSION['action']);

header("Location: http://".$_SERVER['HTTP_HOST'].'/'.$option.'/list.htm');

exit;

}

else {

$_SESSION['errorMessage']=$res;

}

break;

default:

include(LIBPATH."restartsess.php");

}

} //userlevel >1

 

break;

case 'Удалить':

if ($_SESSION['user_level']>1) {

switch ($option) {

case 'pages':

case 'records':

case 'ads':

$table = 'pages';

$field_id = 'page_id';

$field_val = $_POST['page_id'];

break;

default:

include(LIBPATH."restartsess.php");

}

$sql = "DELETE FROM ?n WHERE ?n=?i";

$res=$db->query($sql,$table,$field_id,$field_val);

if ($res==1) {

$_SESSION['errorMessage']='Данные успешно удалены';

unset ($_SESSION['action']);

header("Location: http://".$_SERVER['HTTP_HOST'].'/'.$option.'/list.htm');

exit;

}

else {

$_SESSION['errorMessage']=$res;

}

break;

} //userlevel >1

default:

include(LIBPATH."restartsess.php");

}

}

?>

Во-первых, блоки редактирования и удаления включены внутри проверки уровня пользователя. Только пользователь с уровнем выше 1 (т. е. 2 или 3) имеет право изменять что-то в базе. А все остальное — элементарно.

Аналогичным образом организуем обработку других пунктов меню администраторов.

 

18. Стили.

 К сожалению, времени совсем не осталось на описание таблицы стилей. Готовые файлы можете скачать у нас в составе всего сайта. Скажу лишь, что основываются они на Bootstrap 3.0, который каждый интересующийся может скачать с сайта производителя

Xs – мобильные устройства

sm — планшеты

md — десктопы и лаптопы

lg — большие экраны

 cmsSht7xcfcmsSht8cmsSht5cmsSht6cmsSht3cmsSht4cmsSht1cmsSht2