Router.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <?php
  2. namespace ForkBB\Core;
  3. use InvalidArgumentException;
  4. class Router
  5. {
  6. const OK = 200;
  7. const NOT_FOUND = 404;
  8. const METHOD_NOT_ALLOWED = 405;
  9. const NOT_IMPLEMENTED = 501;
  10. /**
  11. * Массив постоянных маршрутов
  12. * @var array
  13. */
  14. protected $statical = [];
  15. /**
  16. * Массив динамических маршрутов
  17. * @var array
  18. */
  19. protected $dynamic = [];
  20. /**
  21. * Список методов доступа
  22. * @var array
  23. */
  24. protected $methods = [];
  25. /**
  26. * Массив для построения ссылок
  27. * @var array
  28. */
  29. protected $links = [];
  30. /**
  31. * Базовый url сайта
  32. * @var string
  33. */
  34. protected $baseUrl;
  35. /**
  36. * Host сайта
  37. * @var string
  38. */
  39. protected $host;
  40. /**
  41. * Префикс uri
  42. * @var string
  43. */
  44. protected $prefix;
  45. /**
  46. * Длина префикса в байтах
  47. * @var int
  48. */
  49. protected $length;
  50. protected $subSearch = [
  51. '/',
  52. '\\',
  53. ];
  54. protected $subRepl = [
  55. '(_slash_)',
  56. '(_backslash_)',
  57. ];
  58. /**
  59. * Конструктор
  60. *
  61. * @param string $base
  62. */
  63. public function __construct($base)
  64. {
  65. $this->baseUrl = $base;
  66. $this->host = \parse_url($base, PHP_URL_HOST);
  67. $this->prefix = \parse_url($base, PHP_URL_PATH);
  68. $this->length = \strlen($this->prefix);
  69. }
  70. /**
  71. * Проверка url на принадлежность форуму
  72. *
  73. * @param mixed $url
  74. * @param string $defMarker
  75. * @param array $defArgs
  76. *
  77. * @return string
  78. */
  79. public function validate($url, $defMarker, array $defArgs = [])
  80. {
  81. if (\is_string($url)
  82. && \parse_url($url, PHP_URL_HOST) === $this->host
  83. && ($route = $this->route('GET', \rawurldecode(\parse_url($url, \PHP_URL_PATH))))
  84. && $route[0] === self::OK
  85. ) {
  86. if (isset($route[3])) {
  87. return $this->link($route[3], $route[2]);
  88. } else {
  89. return $url;
  90. }
  91. } else {
  92. return $this->link($defMarker, $defArgs);
  93. }
  94. }
  95. /**
  96. * Возвращает ссылку на основании маркера
  97. *
  98. * @param string $marker
  99. * @param array $args
  100. *
  101. * @return string
  102. */
  103. public function link($marker = null, array $args = [])
  104. {
  105. $result = $this->baseUrl;
  106. $anchor = isset($args['#']) ? '#' . \rawurlencode($args['#']) : '';
  107. // маркер пустой
  108. if (null === $marker) {
  109. return $result . "/{$anchor}";
  110. // такой ссылки нет
  111. } elseif (! isset($this->links[$marker])) {
  112. return $result . '/';
  113. // ссылка статична
  114. } elseif (\is_string($data = $this->links[$marker])) {
  115. return $result . $data . $anchor;
  116. }
  117. list($link, $names, $request) = $data;
  118. $data = [];
  119. // перечисление имен переменных для построения ссылки
  120. foreach ($names as $name) {
  121. // значение есть
  122. if (isset($args[$name])) {
  123. // кроме page = 1
  124. if ($name !== 'page' || $args[$name] !== 1) {
  125. $data['{' . $name . '}'] = \rawurlencode(\str_replace($this->subSearch, $this->subRepl, $args[$name]));
  126. continue;
  127. }
  128. }
  129. // значения нет, но оно обязательно
  130. if ($request[$name]) {
  131. return $result . '/';
  132. // значение не обязательно
  133. } else {
  134. // $link = preg_replace('%\[[^\[\]{}]*{' . preg_quote($name, '%') . '}[^\[\]{}]*\]%', '', $link);
  135. $link = \preg_replace('%\[[^\[\]]*?{' . \preg_quote($name, '%') . '}[^\[\]]*+(\[((?>[^\[\]]*+)|(?1))+\])*?\]%', '', $link);
  136. }
  137. }
  138. $link = \str_replace(['[', ']'], '', $link);
  139. return $result . \strtr($link, $data) . $anchor;
  140. }
  141. /**
  142. * Метод определяет маршрут
  143. *
  144. * @param string $method
  145. * @param string $uri
  146. *
  147. * @return array
  148. */
  149. public function route($method, $uri)
  150. {
  151. $head = $method == 'HEAD';
  152. if (empty($this->methods[$method]) && (! $head || empty($this->methods['GET']))) {
  153. return [self::NOT_IMPLEMENTED];
  154. }
  155. if ($this->length) {
  156. if (0 === \strpos($uri, $this->prefix)) {
  157. $uri = \substr($uri, $this->length);
  158. } else {
  159. return [self::NOT_FOUND];
  160. }
  161. }
  162. $allowed = [];
  163. if (isset($this->statical[$uri])) {
  164. if (isset($this->statical[$uri][$method])) {
  165. list($handler, $marker) = $this->statical[$uri][$method];
  166. return [self::OK, $handler, [], $marker];
  167. } elseif ($head && isset($this->statical[$uri]['GET'])) {
  168. list($handler, $marker) = $this->statical[$uri]['GET'];
  169. return [self::OK, $handler, [], $marker];
  170. } else {
  171. $allowed = \array_keys($this->statical[$uri]);
  172. }
  173. }
  174. $pos = \strpos($uri, '/', 1);
  175. $base = false === $pos ? $uri : \substr($uri, 0, $pos);
  176. if (isset($this->dynamic[$base])) {
  177. foreach ($this->dynamic[$base] as $pattern => $data) {
  178. if (! \preg_match($pattern, $uri, $matches)) {
  179. continue;
  180. }
  181. if (isset($data[$method])) {
  182. list($handler, $keys, $marker) = $data[$method];
  183. } elseif ($head && isset($data['GET'])) {
  184. list($handler, $keys, $marker) = $data['GET'];
  185. } else {
  186. $allowed += \array_keys($data);
  187. continue;
  188. }
  189. $args = [];
  190. foreach ($keys as $key) {
  191. if (isset($matches[$key])) { // ???? может isset($matches[$key][0]) тут поставить?
  192. $args[$key] = isset($matches[$key][0]) ? \str_replace($this->subRepl, $this->subSearch, $matches[$key]) : null;
  193. }
  194. }
  195. return [self::OK, $handler, $args, $marker];
  196. }
  197. }
  198. if (empty($allowed)) {
  199. return [self::NOT_FOUND];
  200. } else {
  201. return [self::METHOD_NOT_ALLOWED, $allowed];
  202. }
  203. }
  204. /**
  205. * Метод добавдяет маршрут
  206. *
  207. * @param string|array $method
  208. * @param string $route
  209. * @param string $handler
  210. * @param string $marker
  211. */
  212. public function add($method, $route, $handler, $marker = null)
  213. {
  214. if (\is_array($method)) {
  215. foreach ($method as $m) {
  216. $this->methods[$m] = 1;
  217. }
  218. } else {
  219. $this->methods[$method] = 1;
  220. }
  221. $link = $route;
  222. $anchor = '';
  223. if (false !== \strpos($route, '#')) {
  224. list($route, $anchor) = \explode('#', $route, 2);
  225. $anchor = '#' . $anchor;
  226. }
  227. if (false === \strpbrk($route, '{}[]')) {
  228. $data = null;
  229. if (\is_array($method)) {
  230. foreach ($method as $m) {
  231. $this->statical[$route][$m] = [$handler, $marker];
  232. }
  233. } else {
  234. $this->statical[$route][$method] = [$handler, $marker];
  235. }
  236. } else {
  237. $data = $this->parse($route);
  238. if (false === $data) {
  239. throw new InvalidArgumentException('Route is incorrect');
  240. }
  241. if (\is_array($method)) {
  242. foreach ($method as $m) {
  243. $this->dynamic[$data[0]][$data[1]][$m] = [$handler, $data[2], $marker];
  244. }
  245. } else {
  246. $this->dynamic[$data[0]][$data[1]][$method] = [$handler, $data[2], $marker];
  247. }
  248. }
  249. if ($marker) {
  250. if ($data) {
  251. $this->links[$marker] = [$data[3] . $anchor, $data[2], $data[4]];
  252. } else {
  253. $this->links[$marker] = $link;
  254. }
  255. }
  256. }
  257. /**
  258. * Метод разбирает динамический маршрут
  259. *
  260. * @param string $route
  261. *
  262. * @return array|false
  263. */
  264. protected function parse($route)
  265. {
  266. $parts = \preg_split('%([\[\]{}/])%', $route, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
  267. $s = 1;
  268. $base = $parts[0];
  269. if ($parts[0] === '/') {
  270. $s = 2;
  271. $base .= $parts[1];
  272. }
  273. if (isset($parts[$s]) && $parts[$s] !== '/' && $parts[$s] !== '[') {
  274. $base = '/';
  275. }
  276. $pattern = '%^';
  277. $var = false;
  278. $first = false;
  279. $buffer = '';
  280. $args = [];
  281. $s = 0;
  282. $req = true;
  283. $argReq = [];
  284. $temp = '';
  285. foreach ($parts as $part) {
  286. if ($var) {
  287. switch ($part) {
  288. case '{':
  289. return false;
  290. case '}':
  291. $data = \explode(':', $buffer, 2);
  292. if (! isset($data[1])) {
  293. $data[1] = '[^/\x00-\x1f]+';
  294. }
  295. if ($data[0] === '' || $data[1] === '' || \is_numeric($data[0][0])) {
  296. return false;
  297. }
  298. $pattern .= '(?P<' . $data[0] . '>' . $data[1] . ')';
  299. $args[] = $data[0];
  300. $temp .= '{' . $data[0] . '}';
  301. $var = false;
  302. $buffer = '';
  303. $argsReq[$data[0]] = $req;
  304. break;
  305. default:
  306. $buffer .= $part;
  307. }
  308. } elseif ($first) {
  309. switch ($part) {
  310. case '/':
  311. $first = false;
  312. $pattern .= \preg_quote($part, '%');
  313. $temp .= $part;
  314. break;
  315. default:
  316. return false;
  317. }
  318. } else {
  319. switch ($part) {
  320. case '[':
  321. ++$s;
  322. $pattern .= '(?:';
  323. $first = true;
  324. $req = false;
  325. $temp .= '[';
  326. break;
  327. case ']':
  328. --$s;
  329. if ($s < 0) {
  330. return false;
  331. }
  332. $pattern .= ')?';
  333. $req = true;
  334. $temp .= ']';
  335. break;
  336. case '{':
  337. $var = true;
  338. break;
  339. case '}':
  340. return false;
  341. default:
  342. $pattern .= \preg_quote($part, '%');
  343. $temp .= $part;
  344. }
  345. }
  346. }
  347. if ($var || $s) {
  348. return false;
  349. }
  350. $pattern .= '$%D';
  351. return [$base, $pattern, $args, $temp, $argsReq];
  352. }
  353. }