settings = $config;
}
function analyzeURL($url = FALSE) {
$this->url = ($url)
? $url
: $_SERVER['REQUEST_URI'];
$this->url = urldecode( strval(parse_url($this->url, PHP_URL_PATH)) );
$this->pageData = $this->getPage($this->url);
if ($this->pageData) {
http_response_code(200);
$this->runCode($this->pageData['code']);
$this->checkURLduplicates();
}
else
http_response_code(404);
# Обрабатываем 404
// (надо проверить еще раз, т.к. он мог установиться
// в результате исполнения кода, назначенного найденной странице)
if (http_response_code() == 404 AND $this->pageData = $this->getPage('404') )
$this->runCode($this->pageData['code']); // у 404 может быть свой код - выполняем
}
function getPage($url) {
# Ищем адрес в таблице: сначала среди страниц со статическими,
# потом - среди страниц с динамическими адресами
$table = $this->settings['table'];
$page = mysql_getrow("
SELECT *
FROM $table
WHERE url LIKE " . mysql_escape($url) . "
");
// Именно LIKE, а не =, т.к. = сравнивает строки без учета пробелов на конце
if (!$page) { # Признак страницы с динамическим адресом -
# наличие ( или + в поле url
// С версии 1.1.2 для url можно указывать
// целочисленный приоритет в виде '-10 (выражение)' и т.д.
$sql = "
SELECT url
FROM $table
WHERE url REGEXP '[+(]'
ORDER BY 1 * url DESC -- преобразование в число
";
foreach (mysql_getcolumn($sql) as $u) {
// В url точно будут слэши, поэтому разделителем назначаем
// двойную кавычку. Для порядка её нужно экранировать,
// чтоб не сработала как закрывающий ограничитель
// (хотя вряд ли она встретится в адресах страниц).
$pattern = '"^'
. str_replace(
'"',
'\\"', // Удаляем число-приоритет (на стороне MySQL
trim(preg_replace('/^-?\d+/', '', $u)) // не получается)
)
. '$"iu' ;
if (preg_match($pattern, $url, $this->matches)) {
// Получаем полные данные записи из pages
$page = mysql_getrow("
SELECT *
FROM $table
WHERE url = " . mysql_escape($u) . "
");
break;
}
}
}
if ($page) { # Если страницу нашли - проводим дополнительную обработку
# Устанавливаем основной шаблон
$this->template = trim((string) $page['template'])
?: $this->settings['template'];
# Превращаем строку headers в массив [css,js]
$tmp = array();
$lines = array_filter(explode("\n", trim($page['headers'])));
foreach ($lines as $line) {
$line = trim($line);
if (substr($line, 0, 1) === '#') { // комментарий
continue;
}
if (preg_match('/^(\w+)\s+(.+)$/', $line, $matches)) {
// явное указание расширения файла вида
// js http://api-maps.yandex.ru/...
$ext = $matches[1];
$address = $matches[2];
}
else {
// обычный случай - расширение получаем из пути
// при определении расширения отбрасываем GET-параметры,
// иначе будет ошибка
$ext = pathinfo(
parse_url($line, PHP_URL_PATH),
PATHINFO_EXTENSION
);
$address = $line;
}
$tmp[$ext][] = $address;
}
$page['headers'] = $tmp;
unset($tmp);
}
return $page;
}
function runCode($code) {
$code = trim($code);
if (!$code)
return FALSE;
$pattern = '/
(\w+) -> (\w+) # 1 - класс, 2 - метод
\( ([^)]*) \) # 3 - ключ $this->matches
\s* \[(\w+)\] # 4 - ключ массива pageData
(\s*\S.*)? # 5 - файл или метка
/x';
if (substr($code, 0, 5) == '' )
eval( substr($code, 5, -2) );
elseif (preg_match_all($pattern, $code, $matches_all, PREG_SET_ORDER))
foreach ($matches_all as $matches) {
$classname = $matches[1];
$method = $matches[2];
// Не запутаться:
// $this->matches - разбор пути запроса regexp'ом (url) страницы;
// $matches - разбор выражения для исполняемого кода страницы.
// Аргументом передается один из элементов $this->matches -
// тот, чей ключ указан в выражении для исполняемого кода;
// если ключ не указан, методу передается весь массив полностью.
$arg = ($matches[3] !== '') // строгое равенство, иначе вместо
? $this->matches[$matches[3]] // нулевого элемента $matches
: $this->matches ; // передастся весь массив
$key = $matches[4]; // имя ключа для записи в pageData
$extra = (isset($matches[5])) // подключаемый файл
? trim($matches[5]) // или указатель на другую страницу
: '' ;
$Obj = new $classname;
$data = $Obj->$method($arg);
if ($data) {
http_response_code(200);
$this->pageData[$key] = $data;
if ($extra) {
if (pathinfo($extra, PATHINFO_EXTENSION) == 'php')
$this->requireFile($extra); // файл
else { // указатель на страницу
$page = $this->getPage($extra);
if ($page) { // здесь данные не заменяем полностью, а дописываем
http_response_code(200);
$this->pageData = $page + $this->pageData;
$this->runCode($this->pageData['code']);
}
else // если страницу, на которую ссылается указатель,
http_response_code(404); // не нашли - отдаем 404
}
}
break; // т.к. "нашли", дальше указания не разбираем
}
else
http_response_code(404);
}
elseif ( strpos($code, "\n") === FALSE ) // просто файл
$this->requireFile($code);
else // указания не распознаны
trigger_error(
'Unrecognized code instruction "' . htmlspecialchars($code) . '"',
E_USER_ERROR
);
}
function requireFile($filename) {
$filename = trim($filename);
$filename = preg_replace('/^\$/', $_SERVER['DOCUMENT_ROOT'], $filename);
$this->filename = $filename;
unset($filename); // чтобы полностью очистить область видимости файла
require $this->filename; // В файле будет виден объект маршрутизатора
// в виде переменной $this
}
/**
* Проверка уникальности обрабатываемого адреса.
*
* Если установлено, что данный адрес является дубликатом для другого, происходит перенаправление на него с кодом 301.
*
* В соответствии с настройками (передаются при создании объекта):
*
* 1) Проверка GET-параметров (если check_get_vars = 1).
* Допустимые параметры для каждой страницы берутся из колонки get_vars_allowed (если её нет, генерируется ошибка). Там они перечисляются через пробел. Можно использовать маски со звездочкой. Если указана просто звездочка, разрешены любые GET-параметры.
* Общие для всех страниц допустимые параметры (например, utm*) можно указать
в массиве get_vars_allowed при создании объекта.
*
*/
function checkURLduplicates() {
if (isset($this->settings['check_get_vars']) AND $this->settings['check_get_vars']) {
if ( !array_key_exists('get_vars_allowed', $this->pageData) )
trigger_error(
"Unable to check GET vars: no get_vars_allowed column in " .
$this->settings['table'] . " table.",
E_USER_ERROR
);
else {
$allowed = array_filter( array_map('trim', explode(' ', $this->pageData['get_vars_allowed'] ) ) );
if (isset($this->settings['get_vars_allowed']))
$allowed = array_merge($allowed, $this->settings['get_vars_allowed']);
$duplicate_of = self::checkGETvars($allowed);
if ($duplicate_of) {
http_response_code(301);
header("Location: $duplicate_of");
exit;
}
}
}
}
/**
* Проверка GET-параметров.
*
* Если запрещенных GET-параметов нет, функция возвращает пустую строку.
*
* Если же они есть, то функция возвращает адрес, из которого все запрещенные
исключены. Он в дальнейшем используется для перенаправления.
* Адрес состоит из пути и GET-параметров, без хоста и протокола.
*
* @param array $get_vars_allowed разрешенные имена GET-параметров; в именах
можно использовать звёздочку (*).
* @param void|string $uri адрес для проверки; если не указан, используется
$_SERVER[REQUEST_URI]
* @return string
*/
static function checkGETvars($get_vars_allowed, $uri = FALSE) {
// Первый случай, тривиальный: всё разрешено
if (in_array('*', $get_vars_allowed))
return '';
if (!$uri)
$uri = $_SERVER['REQUEST_URI'];
$parts = parse_url($uri);
if (!isset($parts['query']))
return ''; // нет GET-параметров: нечего проверять
parse_str($parts['query'], $params);
// Второй случай, простой: всё запрещено, но GET-параметры есть
if (!$get_vars_allowed AND !isset($params['query']) )
return $parts['path'];
$discard = [];
foreach ($params as $key => $value) {
$allowed = FALSE;
foreach ($get_vars_allowed as $name) {
if (strpos($name, '*') === FALSE)
$allowed = ($key == $name);
else {
$regexp = '/^' . str_replace('*', '.*?', $name) . '$/';
$allowed = preg_match($regexp, $key);
}
if ($allowed)
break;
}
if (!$allowed)
$discard[] = $key;
}
// Третий случай: запрещенных не обнаружено
if (!$discard)
return '';
// Четвертый случай, последний: исключаем запрещенные
$leave = array_diff_key( $params, array_fill_keys($discard, TRUE) );
$fixed_url = $parts['path'];
if ($leave)
$fixed_url .= '?' . http_build_query($leave);
return $fixed_url;
}
}
?>