. * * @copyright (c) Visman * @license The MIT License (MIT) */ declare(strict_types=1); namespace ForkBB\Core; use ForkBB\Core\Csrf; use InvalidArgumentException; class Router { const OK = 200; const NOT_FOUND = 404; const METHOD_NOT_ALLOWED = 405; const NOT_IMPLEMENTED = 501; const DUO = ['GET', 'POST']; const GET = 'GET'; const PST = 'POST'; /** * Массив постоянных маршрутов * @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; protected $subSearch = [ '/', '\\', ]; protected $subRepl = [ '(_slash_)', '(_backslash_)', ]; /** * @var Csrf */ protected $csrf; public function __construct(string $base, Csrf $csrf) { $this->baseUrl = $base; $this->csrf = $csrf; $this->host = \parse_url($base, \PHP_URL_HOST); $this->prefix = \parse_url($base, \PHP_URL_PATH) ?? ''; $this->length = \strlen($this->prefix); } /** * Проверка url на принадлежность форуму */ public function validate(/* mixed */ $url, string $defMarker, array $defArgs = []): string { if ( \is_string($url) && \parse_url($url, \PHP_URL_HOST) === $this->host && ($uri = \rawurldecode(\parse_url($url, \PHP_URL_PATH))) && ($route = $this->route(self::GET, $uri)) && ( self::OK === $route[0] || ( self::METHOD_NOT_ALLOWED === $route[0] && ($route = $this->route(self::PST, $uri)) && self::OK === $route[0] ) ) ) { if (isset($route[3])) { return $this->link($route[3], $route[2]); } else { return $url; } } else { return $this->link($defMarker, $defArgs); } } /** * Возвращает ссылку на основании маркера */ public function link(string $marker = null, array $args = []): string { $result = $this->baseUrl; $anchor = isset($args['#']) ? '#' . \rawurlencode($args['#']) : ''; // маркер пустой if (null === $marker) { return $result . "/{$anchor}"; // такой ссылки нет } elseif (! isset($this->links[$marker])) { return $result . '/'; // ссылка статична } elseif (\is_string($data = $this->links[$marker])) { return $result . $data . $anchor; } // автоматическое вычисление токена if ( \array_key_exists('token', $args) && ! isset($args['token']) ) { $args['token'] = $this->csrf->create($marker, $args); } list($link, $names, $request) = $data; $data = []; // перечисление имен переменных для построения ссылки foreach ($names as $name) { // значение есть if (isset($args[$name])) { // кроме page = 1 if ( 'page' !== $name || 1 !== $args[$name] ) { $data['{' . $name . '}'] = \rawurlencode(\str_replace($this->subSearch, $this->subRepl, (string) $args[$name])); continue; } } // значения нет, но оно обязательно if ($request[$name]) { return $result . '/'; // значение не обязательно } else { // $link = preg_replace('%\[[^\[\]{}]*{' . preg_quote($name, '%') . '}[^\[\]{}]*\]%', '', $link); $link = \preg_replace( '%\[[^\[\]]*?{' . \preg_quote($name, '%') . '}[^\[\]]*+(\[((?>[^\[\]]*+)|(?1))+\])*?\]%', '', $link ); } } $link = \str_replace(['[', ']'], '', $link); return $result . \strtr($link, $data) . $anchor; } /** * Метод определяет маршрут */ public function route(string $method, string $uri): array { $head = 'HEAD' == $method; 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])) { list($handler, $marker) = $this->statical[$uri][$method]; return [ self::OK, $handler, [], $marker, ]; } elseif ( $head && isset($this->statical[$uri]['GET']) ) { list($handler, $marker) = $this->statical[$uri]['GET']; return [ self::OK, $handler, [], $marker, ]; } else { $allowed = \array_keys($this->statical[$uri]); } } $pos = \strpos($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])) { list($handler, $keys, $marker) = $data[$method]; } elseif ( $head && isset($data['GET']) ) { list($handler, $keys, $marker) = $data['GET']; } else { $allowed += \array_keys($data); continue; } $args = []; foreach ($keys as $key) { if (isset($matches[$key])) { // ???? может isset($matches[$key][0]) тут поставить? $args[$key] = isset($matches[$key][0]) ? \str_replace($this->subRepl, $this->subSearch, $matches[$key]) : null; } } return [ self::OK, $handler, $args, $marker, ]; } } if (empty($allowed)) { return [ self::NOT_FOUND, ]; } else { return [ self::METHOD_NOT_ALLOWED, $allowed, ]; } } /** * Метод добавляет маршрут */ public function add(/* array|string */ $method, string $route, string $handler, string $marker = null): void { if (\is_array($method)) { foreach ($method as $m) { $this->methods[$m] = 1; } } else { $this->methods[$method] = 1; } $link = $route; $anchor = ''; if (false !== \strpos($route, '#')) { list($route, $anchor) = \explode('#', $route, 2); $anchor = '#' . $anchor; } if (false === \strpbrk($route, '{}[]')) { $data = null; if (\is_array($method)) { foreach ($method as $m) { $this->statical[$route][$m] = [$handler, $marker]; } } else { $this->statical[$route][$method] = [$handler, $marker]; } } else { $data = $this->parse($route); if (false === $data) { throw new InvalidArgumentException('Route is incorrect'); } if (\is_array($method)) { foreach ($method as $m) { $this->dynamic[$data[0]][$data[1]][$m] = [$handler, $data[2], $marker]; } } else { $this->dynamic[$data[0]][$data[1]][$method] = [$handler, $data[2], $marker]; } } if ($marker) { if ($data) { $this->links[$marker] = [$data[3] . $anchor, $data[2], $data[4]]; } else { $this->links[$marker] = $link; } } } /** * Метод разбирает динамический маршрут */ protected function parse(string $route): ?array { $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; $req = true; $argReq = []; $temp = ''; foreach ($parts as $part) { if ($var) { switch ($part) { case '{': return null; case '}': $data = \explode(':', $buffer, 2); if (! isset($data[1])) { $data[1] = '[^/\x00-\x1f]+'; } if ( '' === $data[0] || '' === $data[1] || \is_numeric($data[0][0]) ) { return null; } $pattern .= '(?P<' . $data[0] . '>' . $data[1] . ')'; $args[] = $data[0]; $temp .= '{' . $data[0] . '}'; $var = false; $buffer = ''; $argsReq[$data[0]] = $req; break; default: $buffer .= $part; } } elseif ($first) { switch ($part) { case '/': $first = false; $pattern .= \preg_quote($part, '%'); $temp .= $part; break; default: return null; } } else { switch ($part) { case '[': ++$s; $pattern .= '(?:'; $first = true; $req = false; $temp .= '['; break; case ']': --$s; if ($s < 0) { return null; } $pattern .= ')?'; $req = true; $temp .= ']'; break; case '{': $var = true; break; case '}': return null; default: $pattern .= \preg_quote($part, '%'); $temp .= $part; } } } if ( $var || $s ) { return null; } $pattern .= '$%D'; return [ $base, $pattern, $args, $temp, $argsReq, ]; } }