Построение системы управления содержимым
Системы управления содержимым очень эффективны для Web-сайтов, где содержимое поддерживается не только одним автором, либо сопровождение осуществляет не технический персонал, либо содержимое и графическое оформление разрабатывается различными людьми или отделами.
Мы создадим приложение, помогающее уполномоченным пользователям управлять интеллектуальной собственностью организации в электронном виде.
Требования к проекту.Необходимо создать систему, которая:
· Увеличивает продуктивность работы, позволяя авторам сконцентрироваться на статьях, а дизайнерам — на оформлении
· Позволяет редактору просматривать статьи и выбирать их для публикации
· Создает единообразное восприятие сайта с помощью шаблонов страниц
· Предоставляет авторам доступ только к предназначенным для них областям сайта
· Позволяет легко изменять оформление любого раздела либо всего сайта
· Предотвращает изменение нового содержимого
Редактирование содержимого.Во-первых, необходимо продумать способ ввода содержимого в систему, а также методы его хранения и редактирования. Требуется выбрать метод передачи компонентов статей и оформления. Существует три возможности.
1. FTP. Авторам и дизайнерам можно предоставить FTP-доступ к областям Web-сервера. Это позволит им загружать на сервер файлы со своих локальных компьютеров. Для загружаемых файлов потребуется выработать строгий стандарт именования (для идентификации принадлежности изображений к статьям). Вместо этого можно применить основанную на Web систему, которая будет осуществлять идентификацию независимо от FTP-загрузки. Использование FTP создает проблему допусков. Необходимая для данного примера гибкость не позволяет использовать FTP для предоставления пользователям возможности загружать файлы.
2. Метод загрузки файлов. HTTP-протокол предоставляет метод загрузки файлов при помощи Web-браузера. Язык РНР позволяет решать эту задачу очень просто. Кроме того, метод загрузки файлов дает возможность хранить текст в базе данных вместо файлов. Для этого выполняется чтение во временный файл и сохранение его содержимого в базе данных, а не копирование в другую область файловой системы. Метод загрузки файлов в этом проекте не используется.
3. Интерактивное редактирование. Пользователи могут создавать и редактировать документы без участия FTP либо другого метода загрузки файлов. Вместо этого авторам можно предоставить в окне большое текстовое поле, в котором они смогут редактировать содержимое статей. Этот метод прост, но часто эффективен. Web-браузер не предоставляет каких-либо возможностей редактирования текста, кроме функций копирования и вставки, присущих операционной системе. Однако, когда требуется внести лишь небольшое изменение, например, исправить орфографическую ошибку, это можно осуществить очень быстро. Как и для метода загрузки файлов, данные формы можно записать в файл либо сохранить в базе данных.
Преимущество баз данных перед файлами для хранения содержимого.На начальном этапе необходимо принять важное решение относительно метода хранения содержимого после его загрузки в систему.
Поскольку совместно с текстом хранятся метаданные, мы решили поместить текстовую часть содержимого в базу данных. Хотя MySQL способен хранить мультимедийные данные, принято решение хранить загружаемые изображения в файловой системе. Использование большого двоичного объекта (BLOB) в базе данных MySQL может снизить быстродействие.
В базе данных будут храниться лишь имена файлов изображений. Дескриптор <IMG SRC> может прямо ссылаться на каталог графических файлов обычным образом.
Когда объем данных велик, важно оптимизировать их хранение. Подобно тому, как эффективность базы данных зависит от правильной индексации, файловая система существенно выигрывает от хорошо продуманной системы каталогов.
Рис. 6.1. Структура каталогов для загрузки файлов
В данном случае файловая система содержит каталоги, представляющие первую букву каждого имени файла. Таким образом, файлы распределены по 26 каталогам. Это существенно ускоряет доступ по сравнению с ситуацией, когда все файлы хранятся в одном каталоге. Важно отметить, что имена файлов должны генерироваться сценарием обработки загрузки таким образом, чтобы гарантировалась их уникальность.
Структура документов.В качестве примеров статей будет использоваться краткий текст новостей, включающий один-два абзаца и единственное изображение. Он предназначен для тех, кто спешит. Подобные документы с заголовком, абзацами и иллюстрацией можно считать структурированными. Чем выше степень структурирования документа, тем проще его разбить на составляющие, хранимые в базе данных. Преимущество такого подхода состоит в возможности единообразного структурированного представления документов.
В качестве примера возьмем статью новостей. Заголовок будет храниться в своем поле отдельно от текста. Изображение по своей природе является отдельным компонентом документа. Поскольку заголовок является отдельным элементом, для его отображения можно задать стандартный шрифт и стиль, а также легко отделить заголовок от остальной части статьи, сформировав главную страницу заголовков.
Для крупных документов можно применить к отдельным абзацам отношение один ко многим. Другими словами, каждый абзац будет храниться в отдельной строке базы данных и иметь связь с идентификатором главного документа. Этот вид динамической структуры документа позволит представлять страницу содержания для каждого документа и отображать каждый раздел независимо либо отображать документ целиком.
Использование метаданных.Уже решено, что запись для каждой статьи содержит заголовок, текст и изображение. Однако ничто не мешает хранить в той же записи другие данные. Система автоматически вставляет значения, описывающие авторство статьи и время ее последней модификации. Они могут автоматически отображаться в нижней части статьи и служить подписью и меткой времени, избавляя автора от необходимости добавления этой информации. Кроме того, иногда полезно добавлять неотображаемые данные, называемые метаданными. Хорошим примером служит хранение ключевых слов, которые будут использоваться в качестве индексов поискового механизма.
Поисковый механизм будет извлекать ключевое слово для каждой статьи и на его основе определять соответствие критериям поиска. Это устраняет необходимость сканирования всего текста каждой статьи. Администратор сайта сможет управлять соответствием ключевых слов и фраз определенным документам.
В нашем примере допускается связывать со статьей любое количество ключевых слов, а также присваивать каждому ключевому слову "весовое" значение, указывающее степень его значимости в диапазоне от 1 до 10.
Затем можно разработать алгоритм поискового механизма, который располагает соответствия статьям согласно человеческой шкале значимости. Это заменит сложный алгоритм, который интерпретирует английский текст и принимает решения в зависимости от своего ограниченного понимания, а также управляется фиксированными правилами.
Метаданные должны храниться в базе данных. Можно использовать HTML-дескриптор <МЕТА> либо даже применять XML для создания документов. Однако лучше при любой возможности воспользоваться преимуществами базы данных для управления документами.
Форматирование вывода.В нашем примере сайта новостей страницы отображаются в простом, но структурированном формате. Каждая страница содержит ряд статей, сформатированных одинаково. Прежде всего, заголовок выводится крупным шрифтом, внизу слева отображает фотография, а справа — текст статьи. Страница целиком содержится в стандартном шаблоне страниц, что создает последовательность в оформлении сайта.
<TABLE> | <TR><TD COLSPAN=2>TOP ВАR</ТD></TR> |
<TR><TD> Side menu </TD><ТD> | MAIN CONTENT CELL </TD></TR> </TAВLЕ> |
Рис. 6.2. Логическая структура страницы.
Этот вид компоновки крайне популярен — сколько сайтов вы регулярно посещаете, где панель меню расположена слева, а ссылки быстрого доступа вверху? При этом средства навигации остаются неизменными независимо от просматриваемой страницы.
Реализация структуры шаблонов, подобной той, что используется для оформления страниц, очень проста. В HTML-коде можно найти дескриптор <TD>, начинающий ячейку главного содержимого. В этом месте HTML-код расщепляется на два файла. Первая половина составляет файл заголовка, а вторая — нижний колонтитул. Когда отображается страница, сначала выводится заголовок, затем текст и, наконец, нижний колонтитул.
Реализация сайта с шаблоном заголовка и нижнего колонтитула позволяет модифицировать оформление сайта, изменяя файлы шаблонов.
РНР предоставляет две опции конфигурации, которые удобны в данной ситуации. Можно определить для каждого каталога директивы auto_prepend_file и auto_append_file, которые указывают файлы, подключаемые до или после выполнения любого сценария.
Однако существуют некоторые ограничения. Если существуют сценарии, которые не генерируют вывод и отправляют заголовок переадресации, такой как
<? header("Location: destination.php"); ?>
файлы заголовка и нижнего колонтитула будут отображаться, а переадресация не произойдет, поскольку вывод уже отправлен в Web-браузер. Это также создает проблемы с cookie-наборам и встроенными функциями управления сеансами РНР 4, поскольку cookie не будет правильно функционировать после отправки вывода Web-браузеру.
Управление изображениями.Авторы могут дополнять статьи своими фотографиями. Нам необходимо единообразие оформления, но что произойдет, когда один автор загрузит крупное изображение высокого качества, а другой — свернутую в пиктограмму картинку?
Исходя из предположения, что изображения в основном представляют собой фотографии, можно ограничиться лишь форматом JPEG и воспользоваться РНР-функциями для управления графикой.
Напишем небольшой сценарий resize_image.php, который на лету изменяет размер изображений, в результате чего они могут отображаться с помощью дескриптор <IMG SRC>. Сценарий приводится в листинге 6.21.
Листинг 6.21. Сценарий resize_image.php изменяет размеры JPEG-изображения "на лету"
<?
if (!$max_width) $max_width = 150;
if (!$max_height) $max_height = 100;
$size = GetImageSize($image); $width = $size[0]; $height=$size[l];
$x_ratio = $max_width / $width; $y_ratio = $max_height / $height;
if ( ($width <= $max_width) && ($height <= $max_height) )
{ $tn_width = $width; $tn_height = $height; }
else if (($x_ratio * $height) < $max_height)
{ $tn_height = ceil($x_ratio * $height); $tn_width = $max_width; }
else {$tn_width=ceil($y_ratio * $width); $tn_height=$max_height; }
$src = ImageCreateFromJpeg($image);
$dst = ImageCreate($tn_width,$tn_height);
ImageCopyResized($dst, $src, 0, 0, 0, 0,
$tn_width,$tn_height,$width,$height);
header("Content-type: image/jpeg"); ImageJpeg($dst, null, -1);
ImageDestroy($src); ImageDestroy($dst);
?>
Этот сценарий принимает три параметра — имя файла изображения, максимальную ширину и высоту в точках. Не стоит полагать, что если указан максимальный размер 200 х 200, изображение будет масштабировано в соответствии с этими значениями. Его масштаб будет уменьшен пропорционально таким образом, чтобы указанные максимальные размеры не превышались. Например, изображение размером 400 х 300 будет уменьшено до размера 200x150. Таким образом достигается сохранение пропорций изображения.
Изменение размера изображения на сервере предпочтительнее, чем простое задание атрибутов HEIGHT и WIDTH в дескрипторе <IMG SRC>. Размер большого изображения с высоким разрешением может составлять несколько мегабайт. Если же картинку уменьшить до приемлемых размеров, ее размер может не превышать 100 Кб. Нет надобности загружать файл большого размера, а затем указывать браузеру изменить размер изображения.
Здесь используется функция ImageCopyResized() для масштабирования изображений на лету. Суть операции изменения размера состоит в вычислении новых параметров ширины и высоты. Определяется соотношение между реальными и максимальными размерами. Параметры $max_width и $max_height могут быть переданы в одной строке запроса. Иначе будут использованы стандартные значения, указанные в верхней части листинга.
$x_ratio = $max_width / $width; $y_ratio = $max_height / $height;
Если изображение уже меньше указанных максимальных размеров, его ширина и высота остаются неизменными. В противном случае будет использован коэффициент X или Y для равного масштабирования обоих размеров, чтобы изображение уменьшенного размера не оказалось растянутым либо сплющенным:
if ( ($width <= $max_width) && ($height <= $max_height) )
{ $tn_width = $width; $tn_height = $height; }
else if (($x_ratio * $height) < $max_height)
{$tn_height = ceil($x_ratio * $height); $tn_width = $max_width; }
else {$tn_width=ceil($y_ratio * $width); $tn_height=$max_height; }
Обзор проекта.В табл. 6.9 содержится перечень файлов данного приложения.
Таблица 6.9 файлы приложения управления содержимым
Имя | Тип | Описание |
create_database.sql | SQL | SQL-запрос для создания базы данных и образцовых данных |
include_fns.php | Функции | Набор подключаемых файлов для данного приложения |
do_fns.php | Функции | Набор функций для подключения к базе данных содержимого |
select_fns.php | Функции | Набор вспомогательных функций для создания списков SELECT |
user_auth_fns.php | Функции | Набор функций для аутентификации пользователей |
header.php | Шаблон | Отображается в верхней части каждой страницы содержимого |
footer.php | Шаблон | Отображается в нижней части каждой страницы содержимого |
logo.gjf | Изображение | Файл логотипа, отображаемого сценарием header. php |
htadlines.php | Приложение | Отображает новейший заголовок каждой страницы сайта |
page.php | Приложение | Выводит список заголовков и текст для определенной страницы |
resize_image.php | Приложение | Изменяет на лету размеры изображений для сценария headlines. php |
search_form.php | Приложение | Форма ввода ключевых слов для ведения поиска в содержимом сайта |
search.php | Приложение | Отображает заголовки содержимого, соответствующего ключевым словам |
login.php | Приложение | Выполняет аутентификацию пароля пользователя и вводит его в систему |
logout.php | Приложение | Выводит пользователя из системы |
stories. php | Приложение | Перечисляет статьи, написанные вошедшим в систему пользователем. Ему предоставляются опции добавления, модификации и удаления статей |
Имя | Тип | Описание |
story.php | Приложение | Окно информации о статьях для редактирования, либо добавления новых статей |
story _submit.php | Приложение | Добавляет новую статью либо передает изменения на основе данных, введенных в окно story.php |
delete_story.php | Приложение | Обрабатывает запрос на удаление статьи, переданный из сценария stories. php |
keywords.php | Приложение | Выводит список ключевых слов для статьи с опцией их добавления либо удаления |
keyword_add.php | Приложение | Обрабатывает запрос на добавление ключевого слова, переданный из сценария keywords.php |
keyword_delete.php | Приложение | Обрабатывает запрос на удаление ключевого слова, переданный из сценария keywords. php |
publish.php | Приложение | Список статей для редактора с указанием, какие из них опубликованы, а также опцией переключения статуса каждой из них |
publish_story.php | Приложение | Обрабатывает запрос на публикацию, переданный из сценария publish. php |
unpublish_story.php | Приложение | Обрабатывает запрос на отмену публикации, переданный из сценария publish. php |
Создание базы данных.Листинг 6. 22 отображает SQL-запросы, используемые для создания базы данных содержимого системы. Этот листинг демонстрирует часть файла create_database.sql. Файл в приложении кроме этого содержит запросы для заполнения базы данных примерами пользователей и статей.
Листинг 6. 22. Фрагмент create_database.sql —создание содержимого базы данных
DROP DATABASE IF EXISTS content;
CREATE DATABASE content;
USE content;
DROP TABLE IF EXISTS writers;
CREATE TABLE writers (
username varchar(16) PRIMARY KEY,
password varchar(16) NOT NULL,
full_name text);
DROP TABLE IF EXISTS stories;
CREATE TABLE stories (
id int PRIMARY KEY auto_increment,
writer varchar(16) NOT NULL, # FOREIGN KEY writers.username
page varchar(16) NOT NULL, # FOREIGN KEY pages.code
headline text,
story_text text,
picture text,
created int,
modified int,
published int);
DROP TABLE IF EXISTS pages;
CREATE TABLE pages (
code varchar(16) PRIMARY KEY,
description text);
DROP TABLE IF EXISTS writer_permissions;
CREATE TABLE writer_permissions (
writer varchar(16) NOT NULL, # FOREIGN KEY writers.username
page varchar(16) NOT NULL # FOREIGN KEY pages.code
);
DROP TABLE IF EXISTS keywords;
CREATE TABLE keywords (
story int NOT NULL, # FOREIGN KEY stories.id
keyword varchar(32) NOT NULL,
weight int NOT NULL);
GRANT select, insert, update, delete ON content.*
TO content@localhost identified by 'password';
GRANT all ON content.* to root@localhost;
Необходимо сохранить краткие сведения о каждом авторе, включая входное имя и пароль, в таблице writers. Полные имена авторов будут храниться для вывода после каждой статьи, а также приветствия авторов после входа в систему.
Таблица pages содержит заголовки каждой страницы, в которых будут отображаться статьи. Таблица writer_permissions реализует отношение многие ко многим с указанием, для каких страниц автор может передавать статьи.
Таблица stories содержит отдельные поля для компонентов headline (заголовок), story_text (текст статьи) и picture (изображение), о чем речь шла выше. Поля created (создана), modified (модифицирована) и published (опубликована) имеют целочисленный тип данных и хранят метки времени UNIX соответствующих событий.
Для создания базы данных необходимо выполнить следующую команду:
mysql -u root < create_database.sql
Реализация.Теперь, когда база данных и функция изменения размеров созданы, приступим к построению основной части системы.
Интерфейсная часть.Начнем с обзора сценария headlines.php, показанного в листинге 6. 23. Он выводит первую страницу, которая будет отображаться посетителям сайта. Необходимо отобразить заголовки последних статей из каждой страницы.
Листинг 6. 23. Сценарий headlines.php отображает новейшие заголовки каждой страницы
<?php
include "include_fns.php";
include "header.php";
$conn = db_connect();
$pages_sql = "select * from pages order by code";
$pages_result = mysql_query($pages_sql, $conn);
while ($pages = mysql_fetch_array($pages_result)) {
$story_sql = "select * from stories where page = '$pages[code]'
and published is not null order by published desc";
$story_result = mysql_query($story_sql, $conn);
if (mysql_num_rows($story_result)) {
$story = mysql_fetch_array($story_result);
print "<TABLE BORDER=0 WIDTH=400>";
print "<TR><TD ROWSPAN=2 WIDTH=100>";
if ($story[picture])
print "<IMG SRC=\"resize_image.php?image=$story[picture]\"></TD>";
print "<TD><H3>$pages[description]</H3>";
print $story[headline];
print "</TD></TR>";
print "<TR><TD ALIGN=RIGHT>";
print "<A HREF=\"page.php?page=$pages[code]\">";
print "<FONT SIZE=1>Read more $pages[code] ...</FONT>";
print "</A></TABLE>";
}
}
include "footer.php";
?>
Этот сценарий, как и все общедоступные сценарии, подключает файл header.php в начале и файл footer.php в конце. В результате любой вывод, генерируемый сценарием, отображается в ячейке основного содержимого страницы.
Основную нагрузку несут два запроса базы данных. Первый
select * from pages order by code
извлекает список страниц, содержащихся в базе данных Затем выполняется тело цикла
select * from stories where page = ' $pages [code] '
and published is not null order by published desc
для поиска статей данной страницы в обратном порядке дат публикации.
Рядом с каждым заголовком генерируется ссылка в следующей форме:
<А HREF="page.php?page=l"><FONT SIZE=1>Read more news..</FONT></A>
Рассмотренный выше цикл обеспечивает вывод значения строки запроса $page и имени страницы рядом с соответствующим заголовком. В результате щелчка на ссылке выводится страница, генерируемая сценарием page.php. Она содержит полный список статей определенной страницы. Код сценария page.php приводится в листинге 6. 24.
Листинг 6. 24. Сценарий page.php отображает на странице все опубликованные статьи
<?php
include "include_fns.php";
include "header.php";
$conn = db_connect();
if (!$page) { header("Location: headlines.php"); exit; }
$sql = "select * from stories where page = '$page'
and published is not null order by published desc";
$result = mysql_query($sql, $conn);
while ($story = mysql_fetch_array($result)) {
print "<H2>".$story[headline]."</H2>";
if ($story[picture]) {
$size = getImageSize($story[picture]);
$width = $size[0]; $height = $size[1];
print "<IMG SRC=\"$story[picture]\"HEIGHT=$height WIDTH=$width ALIGN=LEFT>"; }
print $story[story_text];
$w = get_writer_record($story[writer]);
print "<br><FONT SIZE=1>";
print $w[full_name].", ";
print date("M d, H:i", $story[modified]);
print "</FONT>";
}
include "footer.php";
?>
Обратите внимание, что сценарий page.php требует наличия значения у переменной $page, чтобы правильно сформировать первый запрос. Когда сценарий page.php вызывается непосредственно, без строки запроса, первый условный оператор
if (!$раgе) { header("Location: headlines.php"); exit; }
отсылает посетителя обратно к странице заголовков, чтобы отсутствие значения $page не привело к ошибке. Первый запрос
select * from stories where раgе = ' $раgе'
and published is not null order by published desc
выбирает все статьи, опубликованные на указанной странице, располагая их в обратном хронологическом порядке (самые свежие статьи следуют в начале). Внутри каждого цикла загруженное изображение и текст отображаются на экране, затем следует имя автора и дата последнего изменения.
Прикладная часть.Теперь рассмотрим метод добавления статей в систему. Сначала для авторов запускается сценарий stories.php. Этот сценарий после аутентификации автора отображает список написанных им статей и дату публикации и либо опции добавления новой статьи, редактирования или удаления существующей, либо установки ключевых слов поиска.
Эти окна не форматируются внутри файлов заголовка и нижнего колонтитула, хотя такое и возможно. Поскольку данные сценарии применяют только авторы и редактор, в нашем примере использован лишь необходимый объем форматирования для создания работоспособной системы. Код сценария stories.php показан в листинге 6. 25.
Листинг 6. 25. Сценарий stories.php - интерфейс авторов для управления своими статьями
<?php
include "include_fns.php";
session_register("auth_user");
if (!check_auth_user()) {
?>
<FORM ACTION="login.php" METHOD=POST><TABLE BORDER=0>
<TR><TD>Username</TD><TD><INPUT SIZE=16 NAME="username"></TD></TR>
<TR><TD>Password</TD><TD><INPUT SIZE=16 TYPE="PASSWORD"
NAME="password"></TD></TR></TABLE>
<INPUT TYPE=SUBMIT VALUE="Log in"></FORM>
<?php
}
else {
$conn = db_connect();
$w = get_writer_record($auth_user);
print "Welcome, ".$w[full_name];
print " (<A HREF=\"logout.php\">Logout</A>)<p>";
$sql = "select * from stories where writer = '$auth_user'
order by created desc";
$result = mysql_query($sql, $conn);
print "Your stories: ";
print mysql_num_rows($result);
print " (<A HREF=\"story.php\">Add new</A>)<br><br>";
if (mysql_num_rows($result)) {
print "<TABLE><TR><TH>Headline</TH><TH>Page</TH>";
print "<TH>Created</TH><TH>Last modified</TH></TR>";
while ($qry = mysql_fetch_array($result)) {
print "<TR><TD>";
print $qry[headline];
print "</TD><TD>";
print $qry[page];
print "</TD><TD>";
print date("M d, H:i", $qry[created]);
print "</TD><TD>";
print date("M d, H:i", $qry[modified]);
print "</TD><TD>";
if ($qry[published])
print "[Published ".date("M d, H:i", $qry[published])."]";
else {
print "[<A HREF=\"story.php?story=".$qry[id]."\">edit</A>] ";
print"[<A HREF=\"delete_story.php?story=".$qry[id]."\">delete</A>] ";
print"[<A HREF=\"keywords.php?story=".$qry[id]."\">keywords</A>]";
}
print "</TD></TR>";
}
print "</TABLE>";
}
}
?>
На первом этапе проверяется, выполнена ли аутентификация пользователя. Если нет, отображается только форма входной регистрации. После входа автора в систему переменной сеанса $auth_user присваивается значение. Используемая здесь аутентификация не очень надежна. В реальной ситуации необходимо обеспечить, чтобы аутентификация пользователей выполнялась должным образом.
Форма входной регистрации передает данные сценарию login.php, который сравнивает имя пользователя и пароль с соответствующими значениями базы данных. В случае успешности входной регистрации пользователю возвращается предыдущая страница с помощью значения $HTTP_REFERER. Это означает, что сценарий регистрации может вызываться из любой страницы системы. Затем автор приветствуется по имени и предоставляется возможность выхода из системы. Эта ссылка всегда отображается в верхней части страницы stories.php, что позволяет легко выйти из системы в любой момент.
$w = get_writer_record($auth_user);
echo "Welcome, ".$w[full_name];
echo " (<A HREF=\"logout.php\">Logout</A>)";
Функция get_writer_record() описана в библиотеке db_fns.php и возвращает массив полей таблицы автора на основе переданного имени пользователя. Сценарий logout.php просто сбрасывает значение переменной $auth_user.
SQL-запрос выбирает все статьи автора, начиная с добавленных в последнее время:
select * from stories where writer = '$auth_user'
order by created desc
Для каждой записи, связанной со статьей, хранятся метки времени добавления, модификации и публикации. Когда добавляется новая статья, меткам создания и модификации присваивается текущее системное время. Каждое последующее изменение статьи будет вызывать обновление лишь метки модификации.
Вся эта информация выводится в окне статей сначала с помощью кода:
echo date("M d, H:i", $qry[created]);
затем:
echo date("M d, H:i", $qry[modified]);
и, наконец:
if ($qry[published])
echo "[Published ".date("M d, H:i", $qry[published])."]";
else {
echo "[<A HREF=\"story.php?story=".$qry[id]."\">edit</A>]";
echo"[<A HREF=\"delete_story.php?story=".$qry[id]."\"> delete</A> ]";
echo "[<A HREF=\"keywords.php?story=".$qry[id]."\">keywords</A>]";}
Последний фрагмент отображает лишь дату публикации, если она имеет смысл. В противном случае выводятся ссылки для редактирования или удаления статьи, а также установки ключевых слов поискового механизма.
Сценарий ввода или редактирования статьи содержится в файле story.php.
Листинг 6. 26. Сценарий story.php служит для создания или редактирования статьи
<?
include "include_fns.php";
if (isset($story))
$s = get_story_record($story);
?>
<FORM ACTION="story_submit.php" METHOD=POST
ENCTYPE="multipart/form-data">
<INPUT TYPE=HIDDEN NAME="story" VALUE="<?php print $story;?>">
<INPUT TYPE=HIDDEN NAME="destination" VALUE="<? print $HTTP_REFERER;?>">
<TABLE><TR><TD ALIGN=CENTER>Headline<TD></TR>
<TR><TD><INPUT SIZE=80 NAME="headline"
VALUE="<? print $s[headline];?>"></TD>
</TR><TR><TD ALIGN=CENTER>Page<TD></TR><TR>
<TD ALIGN=CENTER><? print query_select("page", "select p.code,
p.description from pages p,
writer_permissions w where p.code=w.page
and w.writer = '$auth_user'", $s[page]); ?>
</TD></TR><TR><TD ALIGN=CENTER>Story text (can contain HTML)</TD>
</TR><TR><TD><TEXTAREA COLS=80 ROWS=7 NAME="story_text"
WRAP=VIRTUAL><?php print $s[story_text];?></TEXTAREA>
</TD></TR><TR><TD ALIGN=CENTER>Or upload HTML file</TD></TR>
<TR><TD ALIGN=CENTER><INPUT TYPE=FILE NAME="html" SIZE=40></TD>
</TR><TR><TD ALIGN=CENTER>Picture</TD></TR>
<TR><TD ALIGN=CENTER><INPUT TYPE=FILE NAME="picture" SIZE=40></TD></TR>
<?php
if ($s[picture]) {
$size = getImageSize($s[picture]);
$width = $size[0];
$height = $size[1];
?>
<TR><TD ALIGN=CENTER><IMG SRC="<?php print $s[picture];?>"
WIDTH=<? print $width;?> HEIGHT=<?php print $height;?>>
</TD></TR>
<?php } ?>
<TR><TD ALIGN=CENTER><INPUT TYPE=SUBMIT VALUE="Submit"></TD></TR>
</TABLE></FORM>
Для добавления статей и их редактирования может использоваться один и тот же сценарий. Выполняемое действие зависит от того, установлено ли значение переменой $story при вызове сценария.
if (isset($story))
$s = get_story_record($story);
Функция get_story_record() описана в db_fns.php. Она возвращает массив всех полей таблицы статей для указанного идентификатора статьи. Если идентификатор не передан, переменная $story имеет значение NULL, а переменная $s не содержит элементов массива.
<INPUT SIZE=80 NAME="headline" VALUE="<? echo $s[headline];?>">
Если значение переменной $story не установлено, предыдущий фрагмент кода не создаст значения из оператора РНР, поэтому поле ввода заголовка будет пустым. Когда значение переменной $story установлено, она содержит текст заголовка для редактируемой статьи.
echo query_select("page", "select p.code, p.description
from pages p, writer_permissions w
where p.code = w.page and w.writer = ' $auth_user'", $s[page]); "
Функция query_select() описана в select_fns.php. Она возвращает HTML-код вывода списка SELECT на основе данного SQL-запроса. Первый параметр представляет собой атрибут NAME для оператора SELECT. SQL-запрос из второго параметра выбирает два столбца, где первый является составляющей VALUE каждой опции, а второй следует после дескриптора OPTION и представляет собой текст, отображаемый в списке. Третий параметр необязателен. Он добавляет атрибут SELECTED к опции, значение которой совпадает с указанным.
<INPUT TYPE=HIDDEN NAME="story" VALUE="<?echo $story;?>">
Здесь создается переменная-заполнитель путем установки нового значения для статьи из переданного в переменной $story. После передачи формы сценарий story_submit.php проверяет, существует ли значение переменной $story, и генерирует в соответствии с этим SQL-оператор UPDATE либо INSERT.
Код сценария story_submit.php показан в листинге 6. 27.
Листинг 6. 27. Сценарий story_submit.php - вставка или обновление статей в базе данных
<?php
include "include_fns.php";
$conn = db_connect();
$time = time();
if ( ($html) && (dirname($html_type) == "text") ) {
$fp = fopen($html, "r");
$story_text = addslashes(fread($fp, filesize($html)));
fclose($fp);
}
if ($story) { // It's an update
$sql = "update stories set headline = '$headline',
story_text = '$story_text', page = '$page',
modified = $time where id = $story";
}
else { // It's a new story
$sql = "insert into stories (headline, story_text, page, writer, created, modified) values ('$headline', '$story_text', '$page', '$auth_user', $time, $time)";
}
$result = mysql_query($sql, $conn);
if (!$result) {
print "There was a database error when executing <PRE>$sql</PRE>";
print mysql_error();
exit;
}
if ( ($picture) && ($picture != "none") ) {
if (!$story) $story = mysql_insert_id();
$type = basename($picture_type);
switch ($type) {
case "jpeg": $filename = "pictures/$story.jpg";
copy ($picture, $filename);
$sql = "update stories set picture='$filename'
where id = $story";
$result = mysql_query($sql, $conn); break;
default: print "Invalid picture format: $picture_type";
}
}
header("Location: $destination");
?>
Ссылка удаления статьи вызывает сценарий delete_story.php. Он просто реализует оператор DELETE и возвращает автора к вызывающей странице. Код сценария delete_story.php показан в листинге 6. 28.
Листинг 6. 28. Сценарий delete story.php удаляет статью из базы данных
<?php
include "include_fns.php";
$conn = db_connect(); $now = time();
$sql = "delete from stories where id = $story";
$result = mysql_query($sql, $conn);
header("Location: $HTTP_REFERER"); ?>
Поиск статей.В результате щелчка на ссылке ключевых слов в списке статей вызывается новая форма для ввода ключевых слов, связанных со статьей. Количество ключевых слов не ограничено. Каждому из них присваивается значение весомости. Более высокие значения соответствуют более важным ключевым словам.
Сценарий keywords.php довольно прост, поэтому мы не будем его подробно рассматривать. Он содержится в приложении. Он запускает сценарии keyword_add.php и keyword_delete.php. Они также просты и здесь не рассматриваются.
Для ввода новых ключевых слов в базу данных сценарий keyword_add.php использует следующий запрос:
insert into keywords (story, keyword, weight)
values ($story, '$keyword', $weight)
Подобным же образом, сценарий keyword_delete.php для удаления ключевого слова использует следующий запрос:
delete from keywords where story = $story and keyword = '$keyword'
Интерес представляет способ использования показателей весомости для вычисления процентного отношения значимости в процессе поиска.
Форма поиска, генерируемая сценарием search_form.php, содержит поле для ключевых слов и передается в сценарий search.php, который запрашивает базу данных статей для поиска соответствующего содержимого. Код сценария search.php показан в листинге 6.29.
Листинг 6. 29. Сценарий search.php - поиск статей по ключевым словам
<?php
include "include_fns.php";
include "header.php";
$conn = db_connect();
if ($keyword) {
$k = split(" ", $keyword); $num_keywords = count($k);
for ($i=0; $i<$num_keywords; $i++) {
if ($i) $k_string .= "or k.keyword = '".$k[$i]."' ";
else $k_string .= "k.keyword = '".$k[$i]."' "; }
$and .= "and ($k_string) ";
}
$sql = "select s.id, s.headline, 10*sum(k.weight)/$num_keywords
as score from stories s, keywords k where s.id = k.story $and
group by s.id, s.headline order by score desc, s.id desc";
$result = mysql_query($sql, $conn);
print "<H2>Search results</H2>";
if (mysql_num_rows($result)) {
print "<TABLE>";
while ($qry = mysql_fetch_array($result)) {
print "<TR><TD>";
print $qry[headline];
print "</TD><TD>";
print floor($qry[score])."%";
print "</TD></TR>";
}
print "</TABLE>";
}
else { print "No matching stories found"; }
include "footer.php";
?>
Сначала строка ключевого слова, передаваемая в сценарий, разбивается на отдельные слова для поиска. В этом примере не используются какие-либо расширенные методы поиска, такие как возможность применения операторов AND и OR, либо объединение слов в фразу.
if ($keyword) {
$k = split(" ", $keyword); $num_keywords = count($k);
for ($i=0; $i<$num_keywords; $i++)
{ if ($i)
$k_string .= "or k.keyword = '".$k[$i]."' ";
else $k_string .= "k.keyword = '".$k[$i]."'";
}
$and .= "and ($k_string)";
}
В этом коде используется РНР-функция split() для создания массива, содержащего каждое слово строки $keyword, разделенное пробелами. Если указано только одно слово, возвращается массив с единственным элементом, и последующий цикл выполняется один раз. В результате условие, хранимое в переменной Sand, будет иметь следующий вид:
and (k.keyword = 'keyword1' or k.keyword = 'keyword2' or
k.keyword = ' keyword3')
Ниже приводится запрос поиска, основанный на рассмотренном ранее коде:
select s.id, s.headline, 10 * sum(k.weight)/$num_keywords as score
from stories s, keywords k
where s.id = k.story and (k.keyword = 'keyword1'
or k.keyword = ' keyword2' or k.keyword = 'keyword3' )
group by s.id, s.headline order by score desc, s.id desc
Общий показатель значимости определяется как сумма весовых значений всех соответствующих ключевых слов, деленная на количество искомых ключевых слов и умноженная на десять. Эта формула пригодна для поисков, когда все введенные ключевые слова соответствуют ключевым словам базы данных.
Поскольку показатели весомости находятся в диапазоне от 1 до 10, максимальное значение общего показателя значимости составляет 100. Эта величина для поиска по трем ключевым словам достигается, когда все три слова найдены и каждое из них имеет весовое значение 10.
Окно редактора.Осталась нерассмотренной одна составляющая проекта, отвечающая за публикацию статьи после ее написания. Это осуществляет сценарий publish.php, показанный в листинге 6. 20.
Листинг 6. 20. Сценарий publish.php - выбор из списка документов публикации
<?php
include "include_fns.php";
$conn = db_connect();
$sql = "select * from stories order by modified desc";
$result = mysql_query($sql, $conn);
print "<H2>Editor admin</H2><TABLE>";
print "<TR><TH>Headline</TH><TH>Last modified</TH></TR>";
while ($story = mysql_fetch_array($result)) {
print "<TR><TD>".$story[headline];
print "</TD><TD>.date("M d, H:i", $story[modified])."</TD><TD>";
if ($story[published]) { print "[<A HREF=\"unpublish_story.php? story=$story[id]\"> unpublish</A>]"; }
else {print "[<A HREF=\"publish_story.php?story=$story[id]\"> publish</A>] ";
print"[<A HREF=\"delete_story.php?story=$story[id]\">delete</A>]";
}
print "[<A HREF=\"story.php?story=$story[id]\">edit</A>] ";
print "</TD></TR>";
}
print "</TABLE>";
?>
Данный сценарий должен быть доступным только для лиц, уполномоченных публиковать статьи на сайте. В нашем приложении это редактор сайта. Однако в целях простоты здесь не реализовано управление доступом. В реальной ситуации сценарий должен быть защищен.
Он очень похож на сценарий stories.php с той разницей, что для редактора отображаются статьи всех авторов, а не только его собственные. Оператор if обеспечивает предоставление для каждой статьи необходимых опций. Публикация статей может отменяться, а неопубликованные статьи могут публиковаться либо удаляться.
Связанные с этим оператором три ссылки вызывают, соответственно, сценарии unpublish_story.php, publish_story.php и delete_story.php. Сценарий publish_story.php использует следующий SQL-запрос:
update stories set published = $now where id = $story
Он помечает статью как опубликованную и делает ее общедоступной для просмотра.
Подобно этому, сценарий unpublish_story.php использует следующий запрос, чтобы пометить статью как неопубликованную и прекратить ее отображение для всеобщего просмотра:
update stories set published = null where id = $story
Ссылка редактирования отображается независимо от факта публикации статьи, поэтому редактор всегда может внести в нее изменения. В этом состоит отличие от уровня доступа автора, который может изменять статью лишь до ее публикации.
Дата добавления: 2015-11-04; просмотров: 1166;