Организация пространства адресов страниц веб-сайта средствами PHP и MySQL/MariaDB
В статье излагается подход, при котором запросы к сайту обрабатываются централизованно (через один PHP-файл), а адреса и содержимое страниц хранятся в базе данных. При этом адрес страницы может быть динамическим, а содержимое — включать в себя метки для подстановки переменных. Странице также может быть назначен исполняемый PHP-код.
Настоящий подход опирается на два программных компонента: маршрутизатор и обработчик шаблонов. Они будут описаны в статье далее.
Виртуальное пространство адресов и централизация обработки запросов
При данном подходе адреса страниц не привязаны к файловой системе, поэтому такое адресное пространство можно называть виртуальным.
Для организации виртуального пространства адресов прежде всего нужно изменить обычное поведение веб-сервера, при котором он интерпретирует адреса запрашиваемых страниц как пути к физическим файлам и каталогам. Вместо этого от веб-сервера требуется, чтобы при получении HTTP-запроса он исполнял некоторый PHP-код и использовал результат работы этого кода для формирования HTTP-ответа.
Исключение составляют реально существующие физические файлы (например, изображения, и др.), которые серверу следует продолжать отдавать напрямую, не задействуя PHP.
Настройка Apache
Описанная задача решается с помощью модуля mod_rewrite. Один из способов — разместить в корневом каталоге веб-сервера файл .htaccess примерно следующего содержания1,2,3:
RewriteCond %{REQUEST_FILENAME} !-f [OR]
RewriteCond %{REQUEST_FILENAME} \.php$
RewriteRule .* _showpage.php
Настройка nginx
Директивы для nginx выглядят следующим образом:
set $php_socket unix:/run/php/php5.6-fpm.sock;
location / {
index $vertex;
error_page 404 = @vertex;
location ~ \.php$ {
return 404;
}
}
location @vertex {
fastcgi_pass $php_socket;
fastcgi_param SCRIPT_FILENAME $document_root$vertex;
include fastcgi_params;
}
Маршрутизатор
Любой сайт есть совокупность страниц, имеющих адреса. Механизм сайта должен уметь анализировать приходящий HTTP-запрос и определять, к какой конкретно странице он относится.
Средство, позволяющее задать пространство адресов страниц и сопоставляющее адрес приходящего запроса этому пространству, называется маршрутизатором (или роутером).
В рамках настоящего подхода результатом работы маршрутизатора является HTTP-код ответа и данные для генерации HTML-кода страницы. Эти данные затем передаются обработчику шаблонов, который подставляет их в шаблон, формируя таким образом конечный HTML:
require_once 'router.php'; // подключаем код маршрутизатора
$router_cfg = array( ... ); // перечисляем настройки маршрутизатора (см. ниже)
$R = new Router($router_cfg); // создаем объект маршрутизатора
$R->analyzeURL(); // проводим анализ адреса; в результате объект
// содержит данные страницы - свойство pageData
// и путь к файлу с шаблоном - свойство template
require_once 'websun.php'; // подключаем код шаблонизатора
$HTML = websun_parse_template_path( // формируем HTML-код страницы
$R->pageData, // данные страницы для подстановки
$R->template // путь к файлу с шаблоном
);
echo $HTML;
Код маршрутизатора можно скачать одним файлом (требуется также библиотека для работы с MySQL), код шаблонизатора можно скачать здесь. Техника использования будет подробно описана ниже.
Организация хранения страниц
По большому счету, страница сайта — это HTML-код, имеющий адрес.
HTML-код страницы всегда имеет определенную структуру: каркас остается неизменным, а меняются лишь некоторые части. Обычно его можно описать следующим шаблоном:
<html>
<head>
<title>{* заголовок *}</title>
<meta name="description" content="{* описание для поисковых систем *}">
<meta name="keywords" content="{* ключевые слова *}">
<!-- далее может следовать подключение CSS и javascript-файлов и др. -->
</head>
<body>
{* содержимое *}
</body>
</html>
С этой точки зрения страницу сайта можно считать некой сущностью, уникально идентифицируемой адресом, которая имеет заголовок и содержимое, а также ключевые слова и описание для поисковых систем. Такие сущности удобно хранить в виде записей (строк) в таблице базы данных, а универсальный шаблон для подстановки — в виде отдельного файла.
Рассмотрим конфигурацию маршрутизатора в самом простом случае:
'table' => 'pages', // имя таблицы со страницами
'template' => '$/templates/_page.tpl', // путь к файлу шаблона
); // ($ - корневой каталог веб-сервера)
Таблица pages должна иметь следующий минимальный набор колонок:
url VARCHAR(255) NOT NULL,
title TEXT,
description TEXT,
keywords TEXT,
content MEDIUMTEXT,
code TEXT, -- назначение полей
template VARCHAR(255), -- code, template и headers
headers TEXT, -- разъясняется далее
UNIQUE KEY (url)
);
Шаблон страницы содержит метки для подстановки данных:
<html>
<head>
<title>{* + >*title* *}</title>
<meta name="description" content="{* + >*description* *}">
<meta name="keywords" content="{* + >*keywords* *}">
<!-- далее может следовать подключение CSS и javascript-файлов и др. -->
</head>
<body>
{* + >*content* *}
</body>
</html>
Более подробно языковые конструкции шаблона обсуждаются ниже.
Страницы со статическим адресом
Перейдем теперь непосредственно к технике создания страниц и рассмотрим самый простой случай — страницу со статическим адресом.
Для иллюстрации этого примера как нельзя лучше подходит главная страница, которая есть у любого сайта. Чтобы создать главную страницу, необходимо вставить в таблицу pages вот такую строку:
url = '/',
title = 'webew.ru — портал о веб-технологиях',
description = 'Портал посвящен веб-технологиям.
Здесь можно задать вопросы или поделиться своими идеями',
keywords = 'веб-технологии, php, mysql, html, css, javascript',
content = '
<h1>Вас приветствует webew.ru</h1>
<h2>Последние статьи</h2>
<ul>
...
</ul>
<h2>Новые темы и комментарии</h2>
<ul>
...
</ul>
...
<footer>2014 © webew.ru</footer>
';
Кириллические (и другие нелатинские) символы в адресах страниц следует указывать без url-кодирования: /мы, а не /%D0%BC%D1%8B, и т.п.
GET-паметры не считаются частью адреса страницы. Другими словами, адреса /page.html, /page.html?a=1 и /page.html?b=2&c=3 с точки зрения маршрутизатора соответствуют одной и той же странице. (Это, однако, вовсе не означает, что GET-параметры нельзя использовать: с ними следует работать, назначив странице исполняемый PHP-код.)
Переменные в данных страницы. Назначение странице исполняемого кода.
Вообще говоря, title, description, keywords и content страниц сами по себе обрабатываются как полноценные шаблоны и могут содержать метки для подстановки переменных, наряду с прочими инструкциями. Например:
url = '/',
title = '{*SITENAME*} — портал о веб-технологиях',
description = 'Портал посвящен веб-технологиям.
Здесь можно задать вопросы или поделиться своими идеями',
keywords = 'веб-технологии, php, mysql, html, css, javascript',
content = '
<h1>Вас приветствует {*SITENAME*}</h1>
<h2>Последние статьи</h2>
<ul>
{* + $/mainpage/newarticles.tpl *}
</ul>
<h2>Новые темы и комментарии</h2>
<ul>
{* + $/mainpage/newposts.tpl *}
</ul>
...
<footer>{*year*} © {*SITENAME*}</footer>
';
Инструкция вида {* + $/mainpage/newarticles.tpl *} предписывает вызвать вложенный шаблон, находящийся по адресу mainpage/newarticles.tpl относительно корневого каталога веб-сервера (сокращение "$")4. Также поддерживаются условия, циклы и многое другое5; подробнее эти возможности описаны в уже упоминавшейся выше статье про обработчик шаблонов.6
Теперь о том, как же задавать значения для подстановки.
Посмотрим еще раз, как выглядит вызов обработки шаблона:
$R->pageData, // данные страницы для подстановки
$R->template // путь к шаблону
);
Массивом для подстановки служит свойство pageData объекта маршрутизатора.
Каждой странице можно назначить исполняемый PHP-код. Этот код может выполнять совершенно любые действия (никакие ограничения не накладываются), но прежде всего он используется как раз для заполнения массива pageData.
Назначить исполняемый код можно несколькими способами.
Первый способ — записать в поле code страницы путь к некоторому PHP-файлу. Например:
url = '/',
...
code = '$/mainpage/get-data.php';
Этот файл будет подключен маршрутизатором с помощью require.7
Предположим, на главной странице нужно показать общее количество опубликованных статей и сообщений:
url = '/',
...
content = '<h1>...</h1>
...
<p>Статьи: {*articles_total*}, сообщения: {*posts_total*}.</p>
<footer>...</footer>
';
Тогда содержимое mainpage/get-data.php будет примерно таким:
$arts = ...; // тут некоторым образом получаем
$posts = ...; // общее количество статей и сообщений
// а теперь записываем полученную информацию в объект маршрутизатора
$this->pageData["articles_total"] = $arts;
$this->pageData["posts_total"] = $posts;
$this->pageData["SITENAME"] = "webew.ru"; // добавляем в массив также
$this->pageData["year"] = date('Y'); // элементы с ключами SITENAME и year
?>
Как видно из примера, обращение к объекту маршрутизатора осуществляется через ссылку $this8.
Следует отметить, что title, description, keywords и content страниц также содержатся в массиве pageData, но изменять эти элементы непосредственно в исполняемом коде не рекомендуется.
Глобальные переменные, при необходимости, делаются доступными с помощью инструкции global (аналогично тому, как это делается в функциях и классах).
Второй способ назначить странице исполняемый код — это записать его прямо в поле code, заключив в <?php ?>:
url = '/',
...
content = '<h1>...</h1>
...
<p>Статьи: {*articles_total*}, сообщения: {*posts_total*}.</p>
<footer>...</footer>
',
code = '<?php
$arts = ...;
$posts = ...;
$this->pageData["articles_total"] = $arts;
$this->pageData["posts_total"] = $posts;
$this->pageData["SITENAME"] = "webew.ru";
$this->pageData["year"] = date(Y);
?>' ;
Есть и третий способ. О нем будет рассказано в следующем разделе.
Страницы с динамическим адресом
Страница может обслуживать набор однородных сущностей.
Предположим, сообщения, публикуемые на сайте, имеют адрес вида /posts/(id сообщения).html. Тогда соответствующая запись в таблице будет выглядеть примерно так:9
url = '/posts/(\d+).html',
code = 'Posts->getData(1) [post]',
title = '{*post.title*}',
description = '{*post.snippet*}',
keywords = '{*post.tags*}',
content = '<h1>{*post.title*}</h1>
<p class="date">{*post.date*}</p>
<div class="text">{*post.text*}</div>
...
';
Здесь в качестве url страницы указано Perl-совместимое регулярное выражение (PCRE).
Следует отметить, однако, что в данном случае по одному лишь виду пути запроса нельзя определить, что отдать в ответ — нужно еще как минимум убедиться в том, что имеется конкретная сущность (в данном случае сообщение), а также получить ее данные. Для этого нужно назначить странице исполняемый PHP-код.
Запись Posts->getData(1) [post], содержащаяся в code, реализует вышеупомянутый третий способ назначения странице исполняемого кода и в совокупности с url = '/posts/(\d+).html' означает буквально следующее: «проанализировать адрес запроса регулярным выраженем ^/posts/(\d+).html$10, в случае совпадения создать объект класса Posts и вызвать его метод getData, передав методу в качестве аргумента элемент массива вхождений с индексом 1 (то есть, строку, соответствующую первой паре скобок регулярного выражения); если результат работы getData не пуст — записать его в pageData['post'] объекта маршрутизатора».
Можно также использовать запись вида Posts->getData(1) [post] $/posts/single.php, которая, в дополнение к вышеперечисленному, предписывает в случае успеха подключить файл posts/single.php11.
Нулевой элемент массива вхождений всегда содержит полный путь запроса без ведущего слэша, поэтому при необходимости анализа полного пути можно обращаться к нулевому элементу: Posts->getData(0). Если индекс не указан — Posts->getData() — в качестве аргумента методу будет передан весь массив вхождений целиком.
Альтернативные шаблоны страниц
Отдельным страницам можно назначать альтернативные шаблоны для каркаса HTML-разметки.
Представим себе, что на всех страницах сайта, кроме главной, присутствует левая колонка, незименная от страницы к странице:
<head>...</head>
<body>
<!-- header одинаковый у всех страниц без исключения,
поэтому его вынесем в отдельный шаблон -->
{* + $/templates/header.tpl *}
<div id="left-column">
<!-- левая колонка не обязательно статична по своему содержимому
здесь вполне могут быть переменные, например: -->
{*left_menu*}
</div>
<div id="center-column">
<!-- а здесь собственно содержимое страницы -
то, что в ячейке content таблицы в БД -->
{* +> *content* *}
</div>
<!-- footer - аналогично header -->
{* + $/templates/footer.tpl *}
</body>
В таком случае следует сделать для главной страницы отдельный шаблон, в котором левой колонки нет:
<head>...</head>
<body>
{* + $/templates/header.tpl *}
<!-- а тут левой колонки нет -->
<div id="center-for-mainpage">
{* +> *content* *}
</div>
{* + $/templates/footer.tpl *}
</body>
Сохраним этот шаблон, например, в файле mainpage/_page.tpl и укажем это в записи для главной страницы, заполнив поле template:
url = '/',
...
template = '$/mainpage/_page.tpl' ;
У остальных страниц поле template будет пустым, и это послужит инструкцией использовать для них общий шаблон, указанный при создании объекта маршрутизатора — $/templates/_page.tpl.
Постраничное подключение javascript- и css-файлов
Помимо общих файлов, которые можно перечислить в секции <head> прямо в шаблоне, для отдельных страниц могут потребоваться специальные файлы. Чтобы обеспечить их подключение, нужно выполнить два действия:
1. Заполнить для страницы поле headers, перечислив файлы через перевод строки. Например:
url = '/',
...
headers = '/mainpage/style.css
http://code.jquery.com/jquery.min.js
/mainpage/intro.js
/mainpage/document-ready.js' ;
Пути к файлам будут вставлены в HTML-код страницы ровно в том виде, в котором они указаны.
При отсутствии расширения в адресе тип файла следует явно указать в начале:
js https://api-maps.yandex.ru/2.1/?load=package.full&lang=ru-RU
2. Отредактировать шаблон страниц, поместив внутрь тега <head> следующую конструкцию:
{%*headers.js*} <script src="{*headers.js:*}"></script> {*headers.js*%}
В результате шаблон примет приблизительно такой вид:
<html>
<head>
<title>{* + >*title* *}</title>
<meta name="description" content="{* + >*description* *}">
<meta name="keywords" content="{* + >*keywords* *}">
<!-- тут можно без всяких условий подключить файлы, необходимые для всех страниц -->
<link rel="stylesheet" href="/css/common.css" />
<script src="/js/jquery.min.js"></script>
<!-- а вот указание на постраничные файлы: -->
{%*headers.css*} <link rel="stylesheet" href="{*headers.css:*}" /> {*headers.css*%}
{%*headers.js*} <script src="{*headers.js:*}"></script> {*headers.js*%}
</head>
<body>
...
</body>
</html>
Разделение однородного пространства адресов
Встречаются случаи, когда разнородные сущности имеют одинаковые по виду адреса страниц.
Рассмотрим пример. Предположим, речь идет об интернет-магазине, где товары разбиты на категории, у каждой из которых должна быть своя страница с адресом вида /название категории. У каждого производителя товаров тоже должна быть своя страница с адресом вида /название производителя.
Оба этих случая описываются регулярным выражением /[^/]+12, которое не позволяет отличить один от другого. При этом составить более точные выражения, подходящие для каждого из этих случаев в отдельности, нельзя.13
В такой ситуации потребуется выполнить следующие действия.
Для начала нужно добавить в таблицу pages запись, соответсвующую такому адресу, при этом снабдив её специальными инструкциями:
url = '/[^/]+',
code = 'Categories->getData(1) [cat] category
Manufacturers->getData(1) [manufacturer] manufacturer' ;
Формально эта запись является страницей, но фактически она представляет собой «развилку»14, от которой идут две «дорожки» — к категориям (category) и к производителям (manufacturer).
Разберем эти инструкции подробнее.
Вспомним инструкцию Posts->getData() [post] $/posts/single.php, которая предписывала проверить адрес и в случае успеха записать данные в pageData['post'], передав затем управление файлу posts/single.php.
Похожим образом действует и инструкция Categories->getData(1) [cat] category, однако в третьей ее части стоит не путь к PHP-файлу, а указатель на другую запись в таблице pages15,16, которой и будет передано управление. Этот указатель представляет собой значение поля url целевой записи.
Вот как примерно выглядит эта запись:
url = 'category',
title = '{*cat.title*}',
description = '{*cat.description*}',
keywords = '...',
content = '<h1>{*cat.title*}</h1>
...
',
code = '(здесь можно пользоваться $this->pageData[cat])';
Это обычная страница17, которой уже доступны данные категории в pageData['cat']. Они используются в полях страницы в виде переменных {*cat.ключ*} и, при необходимости, могут быть задействованы в назначенном ей PHP-коде.
Если проверка адреса с помощью Categories->getData() потерпела неудачу, маршрутизатор немедленно переходит к следующей инструкции — Manufacturers->getData(1) [manufacturer] manufacturer (которой соответствует страница с url = 'manufacturer') и так далее до тех пор, пока проверка адреса не завершится успешно или не закончатся инструкции. В последнем случае код HTTP-ответа будет установлен маршрутизатором в 404.
Выделение подпространства адресов
Бывает, что адреса некоторой группы страниц попадают в другую, более широкую, группу.
Допустим, имеется интернет-магазин, в котором страницы товаров имеют адреса вида /производитель/модель.html, а страницы точек розничной продажи — /shops/(id точки).html. Первые при этом описываются регулярным выражением /[^/]+/[^/]+\.html, а вторые — /shops/\d+\.html. Тогда адрес страницы конкретной розничной точки, например /shops/1.html, совпадет с обоими выражениями.
В таком случае маршрутизатору нужно сообщить, что более строгое выражение должно обрабатываться первым. Для этого его нужно предварить числом. Например:
10 /shops/\d+\.html.
Это число маршрутизатор рассматривает как приоритет, обрабатывая первыми страницы с б́ольшим приоритетом.18
Аналогичного эффекта можно достичь, если менее строгому выражению указать отрицательный приоритет — -10 /[^/]+/[^/]+, а более строгое оставить без изменений.
Ошибка 404
Маршрутизатор считает, что запрошенная страница не существует на сайте, в следующих случаях:
- не обнаружена запись в таблице pages
- проверка всех инструкций Класс->метод() [ключ] указатель закончилась неудачей
В такой ситуации маршрутизатор установит HTTP-код ответа в 404.
Можно создать специальную страницу, которую маршрутизатор автоматически будет отдавать в таких случаях19. Её url должен быть равным '404':
url = '404',
title = 'Ошибка 404 - страница не найдена.',
content = '<h1>Ошибка: страница не найдена</h1>
<p>Страницы с адресом {*$_SERVER.REQUEST_URI*} на сайте нет.</p>
<!-- в шаблонах можно использовать суперглобальные переменные -->
',
code = '(и в этом случае тоже можно назначать исполняемый код)';
Об адресах для AJAX-запросов
Ответы на AJAX-запросы не являются полноценными страницами сайта, поэтому включать их в общее виртуальное пространство адресов под управлением маршрутизатора не рекомендуется.
Следует принять соглашение, по которому адреса таких запросов содержат некоторый фрагмент (например, "ajax"), дополнив соответствующим образом настройки веб-серверов.
Apache
Нужная директива записывается так: RewriteCond %{REQUEST_URI} !ajax. Весь набор директив примет вид20:
RewriteCond %{REQUEST_FILENAME} !-f [OR]
RewriteCond %{REQUEST_FILENAME} \.php$
RewriteCond %{REQUEST_URI} !ajax
RewriteRule .* _showpage.php
nginx
Директиву location для *.php потребуется дополнить, и она примет вот такой вид:
if ($uri !~ ajax) {
return 404;
}
fastcgi_pass $php_socket;
fastcgi_param SCRIPT_FILENAME $document_root$uri;
include fastcgi_params;
}
1.▲ Подробнее об использовании mod_rewrite и .htaccess можно прочитать в этой немного устаревшей статье.
2.▲ Директива RewriteCond %{REQUEST_FILENAME} \.php$ воспрепятствует исполнению файлов *.php в ответ на прямой запрос через веб-сервер.
3.▲ Менее строгим, но и менее ресурсоемким (засчет отсутствия проверки существования физического файла) является явное перечисление фрагментов имен файлов (обычно расширений), встречая которые, веб-серверу не следует задействовать PHP:
RewriteCond %{REQUEST_FILENAME} !\.(css|js|png|jpg|gif)$
RewriteRule .* _showpage.php
4.▲ При желании можно поместить содержимое страницы во вложенный шаблон полностью. Например:
url = '/',
...
content = '{* + $/mainpage/content.tpl *}';
5.▲ В том числе инструкция вида {* + >*имя* *}, предписывающая не просто подставить значение переменной, но еще и обработать само это значение как шаблона: искать в содержимом переменной ссылки на другие переменные, а также все остальные синтаксические выражения обычных шаблонов. Именно использование таких инструкций ({* + >*title* *}, {* + >*content* *} и т.п.) в общих шаблонах для страниц обеспечивает динамическую обработку полей title, description, keywords и content.
6.▲ Вообще говоря, в связке с маршрутизатором может быть использован любой схожий по возможностям с websun обработчик шаблонов. Websun является вариантом, к использованию рекомендуемым, но не обязательным. Никаких специальных взаимных требований друг к другу маршрутизатор и обработчик не имеют.
7.▲ Сокращение "$", как и в случае путей к шаблонам, обозначает корневой каталог веб-сервера.
8.▲ Технически файл подключается внутри одного из методов маршрутизатора, поэтому код файла выполняется в контексте объекта.
9.▲ Нотация с точкой соответствует в языке шаблонизатора следующему уровню вложенности массива: {*post.title*} — pageData['post']['title'], {*post.date*} — pageData['post']['date'] и т.п.
10.▲
Механизм маршрутизатора автоматически добавляет символы начала и конца строки перед анализом пути, т.е. запись /posts/(\d+).html на самом деле приведет к сопоставлению с выражением ^/posts/(\d+).html$. Поэтому ошибочное совпадение с частью пути запроса (например, /something/posts/13.html или /posts/13.html/something) исключено.
Сопоставление проводится в режиме регистронезависимости (флаг i).
11.▲ Запись Posts->getData(1) [post] $/posts/single.php в code страницы равносильна следующей:
// Код будет выполняться только в случае успешного сопоставления
// пути запроса регулярному выражению; при этом маршрутизатор
// записывает массив с результатами этого сопоставления в свойство matches.
$Obj = new Posts;
$data = $Obj->getData($this->matches[1]);
if ($data) {
http_response_code(200);
$this->pageData['post'] = $data;
require_once "$_SERVER[DOCUMENT_ROOT]/posts/single.php";
// а точнее - $this->requireFile('$/posts/single.php');
}
else
http_response_code(404);
?>
12.▲ Круглые скобки нужны в данном случае лишь для того, чтобы дать маршрутизатору указание рассматривать адрес этой страницы как динамический и использовать его в качестве регулярного выражения, а не для простой проверки на равенство (как это делается для страниц со статическим адресом).
13.▲ В подобных случаях из положения обычно выходят, выделяя каждому роду сущностей свое подпространство, что приводит к адресам вида /cat/категория вместо /категория. Такое решение, как правило, обсуловлено именно техническими ограничениями механизма сайта, поскольку более короткие и наглядные адреса обычно являются более предпочтительными.
14.▲ Именно поэтому у неё не указаны никакие поля, кроме url и code.
15.▲ Если третья часть инструкции заканчивается фрагментом .php, она считается путем к PHP-файлу, в противном случае — указателем на страницу.
16.▲ В случае, если указатель ссылается на несуществующую запись, маршрутизатор будет считать, что страница с запрошенным адресом не существует. Подробнее см. соответствующий раздел статьи.
17.▲ Нужно сказать, однако, что доступ к этой странице напрямую невозможен в принципе: любой адрес начинается со слэша, а здесь в поле url ведущего слэша нет. Потому такая страница может быть задействована только через описанное указание-«развилку».
18.▲ Число-приоритет должно быть целым. Его можно отделять от регулярного выражения пробелом — для удобочитаемости.
19.▲ Если специальную страницу не заводить, то маршрутизатор все равно будет отдавать код 404, но пользователь увидит либо пустой экран, либо результат замены меток для подстановки данных в основном шаблоне на пустые строки.
20.▲ Или
RewriteCond %{REQUEST_FILENAME} !\.(css|js|png|jpg|gif)$
RewriteCond %{REQUEST_URI} !ajax
RewriteRule .* _showpage.php
© Все права на данную статью принадлежат порталу webew.ru. Перепечатка в интернет-изданиях разрешается только с указанием автора и прямой ссылки на оригинальную статью. Перепечатка в печатных изданиях допускается только с разрешения редакции.