350 lines
9.8 KiB
PHP
350 lines
9.8 KiB
PHP
<?php
|
|
|
|
namespace ForkBB\Core;
|
|
|
|
class Router
|
|
{
|
|
const OK = 200;
|
|
const NOT_FOUND = 404;
|
|
const METHOD_NOT_ALLOWED = 405;
|
|
const NOT_IMPLEMENTED = 501;
|
|
|
|
/**
|
|
* Массив постоянных маршрутов
|
|
* @var array
|
|
*/
|
|
protected $statical = [];
|
|
|
|
/**
|
|
* Массив динамических маршрутов
|
|
* @var array
|
|
*/
|
|
protected $dynamic = [];
|
|
|
|
/**
|
|
* Список методов доступа
|
|
* @var array
|
|
*/
|
|
protected $methods = [];
|
|
|
|
/**
|
|
* Массив для построения ссылок
|
|
* @var array
|
|
*/
|
|
protected $links = [];
|
|
|
|
/**
|
|
* Базовый url сайта
|
|
* @var string
|
|
*/
|
|
protected $baseUrl;
|
|
|
|
/**
|
|
* Host сайта
|
|
* @var string
|
|
*/
|
|
protected $host;
|
|
|
|
/**
|
|
* Префикс uri
|
|
* @var string
|
|
*/
|
|
protected $prefix;
|
|
|
|
/**
|
|
* Длина префикса в байтах
|
|
* @var int
|
|
*/
|
|
protected $length;
|
|
|
|
/**
|
|
* Конструктор
|
|
*
|
|
* @param string $base
|
|
*/
|
|
public function __construct($base)
|
|
{
|
|
$this->baseUrl = $base;
|
|
$this->host = parse_url($base, PHP_URL_HOST);
|
|
$this->prefix = parse_url($base, PHP_URL_PATH);
|
|
$this->length = strlen($this->prefix);
|
|
}
|
|
|
|
/**
|
|
* Проверка url на принадлежность форуму
|
|
*
|
|
* @param mixed $url
|
|
* @param string $defMarker
|
|
* @param array $defArgs
|
|
*
|
|
* @return string
|
|
*/
|
|
public function validate($url, $defMarker, array $defArgs = [])
|
|
{
|
|
if (is_string($url)
|
|
&& parse_url($url, PHP_URL_HOST) === $this->host
|
|
&& ($route = $this->route('GET', rawurldecode(parse_url($url, PHP_URL_PATH))))
|
|
&& $route[0] === self::OK
|
|
) {
|
|
return $url;
|
|
} else {
|
|
return $this->link($defMarker, $defArgs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Возвращает ссылку на основании маркера
|
|
*
|
|
* @param string $marker
|
|
* @param array $args
|
|
*
|
|
* @return string
|
|
*/
|
|
public function link($marker = null, array $args = [])
|
|
{
|
|
$result = $this->baseUrl;
|
|
if (is_string($marker) && isset($this->links[$marker])) {
|
|
$s = $this->links[$marker];
|
|
foreach ($args as $key => $val) {
|
|
if ($key == '#') {
|
|
$s .= '#' . rawurlencode($val);
|
|
continue;
|
|
} elseif ($key == 'page' && $val === 1) {
|
|
continue;
|
|
}
|
|
$s = preg_replace_callback( //????
|
|
'%\{' . preg_quote($key, '%') . '(?::[^{}]+)?\}%',
|
|
function($match) use ($val) {
|
|
if (is_string($val)) {
|
|
$val = trim(preg_replace('%[^\p{L}\p{N}_]+%u', '-', $val), '_-');
|
|
} elseif (is_numeric($val)) { //????
|
|
$val = (string) $val;
|
|
} else {
|
|
$val = null;
|
|
}
|
|
return isset($val[0]) ? rawurlencode($val) : '-';
|
|
},
|
|
$s
|
|
);
|
|
}
|
|
$s = preg_replace('%\[[^{}\[\]]*\{[^}]+\}[^{}\[\]]*\]%', '', $s);
|
|
if (strpos($s, '{') === false) {
|
|
$result .= str_replace(['[', ']'], '', $s);
|
|
} else {
|
|
$result .= '/';
|
|
}
|
|
} else {
|
|
$result .= '/';
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Метод определяет маршрут
|
|
*
|
|
* @param string $method
|
|
* @param string $uri
|
|
*
|
|
* @return array
|
|
*/
|
|
public function route($method, $uri)
|
|
{
|
|
$head = $method == 'HEAD';
|
|
|
|
if (empty($this->methods[$method]) && (! $head || empty($this->methods['GET']))) {
|
|
return [self::NOT_IMPLEMENTED];
|
|
}
|
|
|
|
if ($this->length) {
|
|
if (0 === strpos($uri, $this->prefix)) {
|
|
$uri = substr($uri, $this->length);
|
|
} else {
|
|
return [self::NOT_FOUND];
|
|
}
|
|
}
|
|
|
|
$allowed = [];
|
|
|
|
if (isset($this->statical[$uri])) {
|
|
if (isset($this->statical[$uri][$method])) {
|
|
return [self::OK, $this->statical[$uri][$method], []];
|
|
} elseif ($head && isset($this->statical[$uri]['GET'])) {
|
|
return [self::OK, $this->statical[$uri]['GET'], []];
|
|
} else {
|
|
$allowed = array_keys($this->statical[$uri]);
|
|
}
|
|
}
|
|
|
|
$pos = strpos(substr($uri, 1), '/');
|
|
$base = false === $pos ? $uri : substr($uri, 0, ++$pos);
|
|
|
|
if (isset($this->dynamic[$base])) {
|
|
foreach ($this->dynamic[$base] as $pattern => $data) {
|
|
if (! preg_match($pattern, $uri, $matches)) {
|
|
continue;
|
|
}
|
|
|
|
if (isset($data[$method])) {
|
|
$data = $data[$method];
|
|
} elseif ($head && isset($data['GET'])) {
|
|
$data = $data['GET'];
|
|
} else {
|
|
$allowed += array_keys($data);
|
|
continue;
|
|
}
|
|
|
|
$args = [];
|
|
foreach ($data[1] as $key) {
|
|
if (isset($matches[$key])) {
|
|
$args[$key] = $matches[$key];
|
|
}
|
|
}
|
|
return [self::OK, $data[0], $args];
|
|
}
|
|
}
|
|
if (empty($allowed)) {
|
|
return [self::NOT_FOUND];
|
|
} else {
|
|
return [self::METHOD_NOT_ALLOWED, $allowed];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Метод добавдяет маршрут
|
|
*
|
|
* @param string|array $method
|
|
* @param string $route
|
|
* @param string $handler
|
|
* @param string $marker
|
|
*/
|
|
public function add($method, $route, $handler, $marker = null)
|
|
{
|
|
if (is_array($method)) {
|
|
foreach ($method as $m) {
|
|
$this->methods[$m] = 1;
|
|
}
|
|
} else {
|
|
$this->methods[$method] = 1;
|
|
}
|
|
|
|
$link = $route;
|
|
if (($pos = strpos($route, '#')) !== false) {
|
|
$route = substr($route, 0, $pos);
|
|
}
|
|
|
|
if (false === strpbrk($route, '{}[]')) {
|
|
if (is_array($method)) {
|
|
foreach ($method as $m) {
|
|
$this->statical[$route][$m] = $handler;
|
|
}
|
|
} else {
|
|
$this->statical[$route][$method] = $handler;
|
|
}
|
|
} else {
|
|
$data = $this->parse($route);
|
|
if (false === $data) {
|
|
throw new \Exception('Route is incorrect');
|
|
}
|
|
if (is_array($method)) {
|
|
foreach ($method as $m) {
|
|
$this->dynamic[$data[0]][$data[1]][$m] = [$handler, $data[2]];
|
|
}
|
|
} else {
|
|
$this->dynamic[$data[0]][$data[1]][$method] = [$handler, $data[2]];
|
|
}
|
|
}
|
|
|
|
if ($marker) {
|
|
$this->links[$marker] = $link;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Метод разбирает динамический маршрут
|
|
*
|
|
* @param string $route
|
|
*
|
|
* @return array|false
|
|
*/
|
|
protected function parse($route)
|
|
{
|
|
$parts = preg_split('%([\[\]{}/])%', $route, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
|
|
|
|
$s = 1;
|
|
$base = $parts[0];
|
|
if ($parts[0] === '/') {
|
|
$s = 2;
|
|
$base .= $parts[1];
|
|
}
|
|
if (isset($parts[$s]) && $parts[$s] !== '/' && $parts[$s] !== '[') {
|
|
$base = '/';
|
|
}
|
|
|
|
$pattern = '%^';
|
|
$var = false;
|
|
$first = false;
|
|
$buffer = '';
|
|
$args = [];
|
|
$s = 0;
|
|
|
|
foreach ($parts as $part) {
|
|
if ($var) {
|
|
switch ($part) {
|
|
case '{':
|
|
return false;
|
|
case '}':
|
|
$data = explode(':', $buffer, 2);
|
|
if (! isset($data[1])) {
|
|
$data[1] = '[^/\x00-\x1f]+';
|
|
}
|
|
if ($data[0] === '' || $data[1] === '' || is_numeric($data[0]{0})) {
|
|
return false;
|
|
}
|
|
$pattern .= '(?P<' . $data[0] . '>' . $data[1] . ')';
|
|
$args[] = $data[0];
|
|
$var = false;
|
|
$buffer = '';
|
|
break;
|
|
default:
|
|
$buffer .= $part;
|
|
}
|
|
} elseif ($first) {
|
|
switch ($part) {
|
|
case '/':
|
|
$first = false;
|
|
$pattern .= preg_quote($part, '%');
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
} else {
|
|
switch ($part) {
|
|
case '[':
|
|
++$s;
|
|
$pattern .= '(?:';
|
|
$first = true;
|
|
break;
|
|
case ']':
|
|
--$s;
|
|
if ($s < 0) {
|
|
return false;
|
|
}
|
|
$pattern .= ')?';
|
|
break;
|
|
case '{':
|
|
$var = true;
|
|
break;
|
|
case '}':
|
|
return false;
|
|
default:
|
|
$pattern .= preg_quote($part, '%');
|
|
}
|
|
}
|
|
}
|
|
if ($var || $s) {
|
|
return false;
|
|
}
|
|
$pattern .= '$%D';
|
|
return [$base, $pattern, $args];
|
|
}
|
|
}
|