settings = $config; } function analyzeURL($url = FALSE) { $this->url = ($url) ? $url : $_SERVER['REQUEST_URI']; $this->url = urldecode( 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)) // не получается) ) . '$"i' ; 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($page['template'])) ? trim($page['template']) : $this->settings['template']; # Превращаем строку headers в массив [css,js] $tmp = array(); foreach (explode("\n", trim($page['headers'])) as $line) { $line = trim($line); if (preg_match('/^(\w+)\s+(.+)$/', $line, $matches)) { // явное указание расширения файла вида // js http://api-maps.yandex.ru/... $ext = $matches[1]; $address = $matches[2]; } else { // обычный случай - расширение получаем из пути $ext = pathinfo($line, 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; } } ?>