1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\Routing\Route;
16:
17: use Cake\Http\ServerRequestFactory;
18: use Cake\Routing\Router;
19: use InvalidArgumentException;
20: use Psr\Http\Message\ServerRequestInterface;
21:
22: 23: 24: 25: 26: 27: 28:
29: class Route
30: {
31:
32: 33: 34: 35: 36: 37:
38: public $keys = [];
39:
40: 41: 42: 43: 44:
45: public $options = [];
46:
47: 48: 49: 50: 51:
52: public $defaults = [];
53:
54: 55: 56: 57: 58:
59: public $template;
60:
61: 62: 63: 64: 65: 66:
67: protected $_greedy = false;
68:
69: 70: 71: 72: 73:
74: protected $_compiledRoute;
75:
76: 77: 78: 79: 80:
81: protected $_name;
82:
83: 84: 85: 86: 87:
88: protected $_extensions = [];
89:
90: 91: 92: 93: 94:
95: protected $middleware = [];
96:
97: 98: 99: 100: 101:
102: protected $braceKeys = false;
103:
104: 105: 106: 107: 108:
109: const VALID_METHODS = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
110:
111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126:
127: public function __construct($template, $defaults = [], array $options = [])
128: {
129: $this->template = $template;
130: if (isset($defaults['[method]'])) {
131: deprecationWarning('The `[method]` option is deprecated. Use `_method` instead.');
132: $defaults['_method'] = $defaults['[method]'];
133: unset($defaults['[method]']);
134: }
135: $this->defaults = (array)$defaults;
136: $this->options = $options + ['_ext' => [], '_middleware' => []];
137: $this->setExtensions((array)$this->options['_ext']);
138: $this->setMiddleware((array)$this->options['_middleware']);
139: unset($this->options['_middleware']);
140: }
141:
142: 143: 144: 145: 146: 147: 148:
149: public function extensions($extensions = null)
150: {
151: deprecationWarning(
152: 'Route::extensions() is deprecated. ' .
153: 'Use Route::setExtensions()/getExtensions() instead.'
154: );
155: if ($extensions === null) {
156: return $this->_extensions;
157: }
158: $this->_extensions = (array)$extensions;
159: }
160:
161: 162: 163: 164: 165: 166:
167: public function setExtensions(array $extensions)
168: {
169: $this->_extensions = [];
170: foreach ($extensions as $ext) {
171: $this->_extensions[] = strtolower($ext);
172: }
173:
174: return $this;
175: }
176:
177: 178: 179: 180: 181:
182: public function getExtensions()
183: {
184: return $this->_extensions;
185: }
186:
187: 188: 189: 190: 191: 192: 193:
194: public function setMethods(array $methods)
195: {
196: $methods = array_map('strtoupper', $methods);
197: $diff = array_diff($methods, static::VALID_METHODS);
198: if ($diff !== []) {
199: throw new InvalidArgumentException(
200: sprintf('Invalid HTTP method received. %s is invalid.', implode(', ', $diff))
201: );
202: }
203: $this->defaults['_method'] = $methods;
204:
205: return $this;
206: }
207:
208: 209: 210: 211: 212: 213: 214: 215: 216:
217: public function setPatterns(array $patterns)
218: {
219: $patternValues = implode("", $patterns);
220: if (mb_strlen($patternValues) < strlen($patternValues)) {
221: $this->options['multibytePattern'] = true;
222: }
223: $this->options = array_merge($this->options, $patterns);
224:
225: return $this;
226: }
227:
228: 229: 230: 231: 232: 233:
234: public function setHost($host)
235: {
236: $this->options['_host'] = $host;
237:
238: return $this;
239: }
240:
241: 242: 243: 244: 245: 246:
247: public function setPass(array $names)
248: {
249: $this->options['pass'] = $names;
250:
251: return $this;
252: }
253:
254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268:
269: public function setPersist(array $names)
270: {
271: $this->options['persist'] = $names;
272:
273: return $this;
274: }
275:
276: 277: 278: 279: 280:
281: public function compiled()
282: {
283: return !empty($this->_compiledRoute);
284: }
285:
286: 287: 288: 289: 290: 291: 292: 293:
294: public function compile()
295: {
296: if ($this->_compiledRoute) {
297: return $this->_compiledRoute;
298: }
299: $this->_writeRoute();
300:
301: return $this->_compiledRoute;
302: }
303:
304: 305: 306: 307: 308: 309: 310: 311:
312: protected function _writeRoute()
313: {
314: if (empty($this->template) || ($this->template === '/')) {
315: $this->_compiledRoute = '#^/*$#';
316: $this->keys = [];
317:
318: return;
319: }
320: $route = $this->template;
321: $names = $routeParams = [];
322: $parsed = preg_quote($this->template, '#');
323:
324: if (strpos($route, '{') !== false && strpos($route, '}') !== false) {
325: preg_match_all('/\{([a-z][a-z0-9-_]*)\}/i', $route, $namedElements);
326: $this->braceKeys = true;
327: } else {
328: preg_match_all('/:([a-z0-9-_]+(?<![-_]))/i', $route, $namedElements);
329: $this->braceKeys = false;
330: }
331: foreach ($namedElements[1] as $i => $name) {
332: $search = preg_quote($namedElements[0][$i]);
333: if (isset($this->options[$name])) {
334: $option = null;
335: if ($name !== 'plugin' && array_key_exists($name, $this->defaults)) {
336: $option = '?';
337: }
338: $slashParam = '/' . $search;
339: if (strpos($parsed, $slashParam) !== false) {
340: $routeParams[$slashParam] = '(?:/(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option;
341: } else {
342: $routeParams[$search] = '(?:(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option;
343: }
344: } else {
345: $routeParams[$search] = '(?:(?P<' . $name . '>[^/]+))';
346: }
347: $names[] = $name;
348: }
349: if (preg_match('#\/\*\*$#', $route)) {
350: $parsed = preg_replace('#/\\\\\*\\\\\*$#', '(?:/(?P<_trailing_>.*))?', $parsed);
351: $this->_greedy = true;
352: }
353: if (preg_match('#\/\*$#', $route)) {
354: $parsed = preg_replace('#/\\\\\*$#', '(?:/(?P<_args_>.*))?', $parsed);
355: $this->_greedy = true;
356: }
357: $mode = '';
358: if (!empty($this->options['multibytePattern'])) {
359: $mode = 'u';
360: }
361: krsort($routeParams);
362: $parsed = str_replace(array_keys($routeParams), $routeParams, $parsed);
363: $this->_compiledRoute = '#^' . $parsed . '[/]*$#' . $mode;
364: $this->keys = $names;
365:
366:
367: foreach ($this->keys as $key) {
368: unset($this->defaults[$key]);
369: }
370:
371: $keys = $this->keys;
372: sort($keys);
373: $this->keys = array_reverse($keys);
374: }
375:
376: 377: 378: 379: 380:
381: public function getName()
382: {
383: if (!empty($this->_name)) {
384: return $this->_name;
385: }
386: $name = '';
387: $keys = [
388: 'prefix' => ':',
389: 'plugin' => '.',
390: 'controller' => ':',
391: 'action' => ''
392: ];
393: foreach ($keys as $key => $glue) {
394: $value = null;
395: if (strpos($this->template, ':' . $key) !== false) {
396: $value = '_' . $key;
397: } elseif (isset($this->defaults[$key])) {
398: $value = $this->defaults[$key];
399: }
400:
401: if ($value === null) {
402: continue;
403: }
404: if ($value === true || $value === false) {
405: $value = $value ? '1' : '0';
406: }
407: $name .= $value . $glue;
408: }
409:
410: return $this->_name = strtolower($name);
411: }
412:
413: 414: 415: 416: 417: 418: 419: 420: 421:
422: public function parseRequest(ServerRequestInterface $request)
423: {
424: $uri = $request->getUri();
425: if (isset($this->options['_host']) && !$this->hostMatches($uri->getHost())) {
426: return false;
427: }
428:
429: return $this->parse($uri->getPath(), $request->getMethod());
430: }
431:
432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442:
443: public function parse($url, $method = '')
444: {
445: if (empty($this->_compiledRoute)) {
446: $this->compile();
447: }
448: list($url, $ext) = $this->_parseExtension($url);
449:
450: if (!preg_match($this->_compiledRoute, urldecode($url), $route)) {
451: return false;
452: }
453:
454: if (isset($this->defaults['_method'])) {
455: if (empty($method)) {
456: deprecationWarning(
457: 'Extracting the request method from global state when parsing routes is deprecated. ' .
458: 'Instead adopt Route::parseRequest() which extracts the method from the passed request.'
459: );
460:
461: $request = Router::getRequest(true) ?: ServerRequestFactory::fromGlobals();
462: $method = $request->getMethod();
463: }
464: if (!in_array($method, (array)$this->defaults['_method'], true)) {
465: return false;
466: }
467: }
468:
469: array_shift($route);
470: $count = count($this->keys);
471: for ($i = 0; $i <= $count; $i++) {
472: unset($route[$i]);
473: }
474: $route['pass'] = [];
475:
476:
477: foreach ($this->defaults as $key => $value) {
478: if (isset($route[$key])) {
479: continue;
480: }
481: if (is_int($key)) {
482: $route['pass'][] = $value;
483: continue;
484: }
485: $route[$key] = $value;
486: }
487:
488: if (isset($route['_args_'])) {
489: $pass = $this->_parseArgs($route['_args_'], $route);
490: $route['pass'] = array_merge($route['pass'], $pass);
491: unset($route['_args_']);
492: }
493:
494: if (isset($route['_trailing_'])) {
495: $route['pass'][] = $route['_trailing_'];
496: unset($route['_trailing_']);
497: }
498:
499: if (!empty($ext)) {
500: $route['_ext'] = $ext;
501: }
502:
503:
504: if (isset($this->options['_name'])) {
505: $route['_name'] = $this->options['_name'];
506: }
507:
508:
509: if (isset($this->options['pass'])) {
510: $j = count($this->options['pass']);
511: while ($j--) {
512: if (isset($route[$this->options['pass'][$j]])) {
513: array_unshift($route['pass'], $route[$this->options['pass'][$j]]);
514: }
515: }
516: }
517: $route['_matchedRoute'] = $this->template;
518: if (count($this->middleware) > 0) {
519: $route['_middleware'] = $this->middleware;
520: }
521:
522: return $route;
523: }
524:
525: 526: 527: 528: 529: 530:
531: public function hostMatches($host)
532: {
533: $pattern = '@^' . str_replace('\*', '.*', preg_quote($this->options['_host'], '@')) . '$@';
534:
535: return preg_match($pattern, $host) !== 0;
536: }
537:
538: 539: 540: 541: 542: 543: 544:
545: protected function _parseExtension($url)
546: {
547: if (count($this->_extensions) && strpos($url, '.') !== false) {
548: foreach ($this->_extensions as $ext) {
549: $len = strlen($ext) + 1;
550: if (substr($url, -$len) === '.' . $ext) {
551: return [substr($url, 0, $len * -1), $ext];
552: }
553: }
554: }
555:
556: return [$url, null];
557: }
558:
559: 560: 561: 562: 563: 564: 565: 566: 567: 568:
569: protected function _parseArgs($args, $context)
570: {
571: $pass = [];
572: $args = explode('/', $args);
573:
574: foreach ($args as $param) {
575: if (empty($param) && $param !== '0' && $param !== 0) {
576: continue;
577: }
578: $pass[] = rawurldecode($param);
579: }
580:
581: return $pass;
582: }
583:
584: 585: 586: 587: 588: 589: 590: 591: 592:
593: protected function _persistParams(array $url, array $params)
594: {
595: foreach ($this->options['persist'] as $persistKey) {
596: if (array_key_exists($persistKey, $params) && !isset($url[$persistKey])) {
597: $url[$persistKey] = $params[$persistKey];
598: }
599: }
600:
601: return $url;
602: }
603:
604: 605: 606: 607: 608: 609: 610: 611: 612: 613: 614: 615: 616:
617: public function match(array $url, array $context = [])
618: {
619: if (empty($this->_compiledRoute)) {
620: $this->compile();
621: }
622: $defaults = $this->defaults;
623: $context += ['params' => [], '_port' => null, '_scheme' => null, '_host' => null];
624:
625: if (!empty($this->options['persist']) &&
626: is_array($this->options['persist'])
627: ) {
628: $url = $this->_persistParams($url, $context['params']);
629: }
630: unset($context['params']);
631: $hostOptions = array_intersect_key($url, $context);
632:
633:
634: if (isset($this->options['_host'])) {
635: if (!isset($hostOptions['_host']) && strpos($this->options['_host'], '*') === false) {
636: $hostOptions['_host'] = $this->options['_host'];
637: }
638: if (!isset($hostOptions['_host'])) {
639: $hostOptions['_host'] = $context['_host'];
640: }
641:
642:
643: if (!$this->hostMatches($hostOptions['_host'])) {
644: return false;
645: }
646: }
647:
648:
649:
650: if (isset($hostOptions['_scheme']) ||
651: isset($hostOptions['_port']) ||
652: isset($hostOptions['_host'])
653: ) {
654: $hostOptions += $context;
655:
656: if (getservbyname($hostOptions['_scheme'], 'tcp') === $hostOptions['_port']) {
657: unset($hostOptions['_port']);
658: }
659: }
660:
661:
662: if (!isset($hostOptions['_base']) && isset($context['_base'])) {
663: $hostOptions['_base'] = $context['_base'];
664: }
665:
666: $query = !empty($url['?']) ? (array)$url['?'] : [];
667: unset($url['_host'], $url['_scheme'], $url['_port'], $url['_base'], $url['?']);
668:
669:
670:
671: if (isset($url['_ext'])) {
672: $hostOptions['_ext'] = $url['_ext'];
673: unset($url['_ext']);
674: }
675:
676:
677: if (!$this->_matchMethod($url)) {
678: return false;
679: }
680: unset($url['_method'], $url['[method]'], $defaults['_method']);
681:
682:
683: if (array_diff_key($defaults, $url) !== []) {
684: return false;
685: }
686:
687:
688: if (array_intersect_key($url, $defaults) != $defaults) {
689: return false;
690: }
691:
692:
693:
694: if (isset($this->options['pass'])) {
695: foreach ($this->options['pass'] as $i => $name) {
696: if (isset($url[$i]) && !isset($url[$name])) {
697: $url[$name] = $url[$i];
698: unset($url[$i]);
699: }
700: }
701: }
702:
703:
704: $keyNames = array_flip($this->keys);
705: if (array_intersect_key($keyNames, $url) !== $keyNames) {
706: return false;
707: }
708:
709: $pass = [];
710: foreach ($url as $key => $value) {
711:
712: $defaultExists = array_key_exists($key, $defaults);
713:
714:
715: if (array_key_exists($key, $keyNames)) {
716: continue;
717: }
718:
719:
720: $numeric = is_numeric($key);
721: if ($numeric && isset($defaults[$key]) && $defaults[$key] == $value) {
722: continue;
723: }
724: if ($numeric) {
725: $pass[] = $value;
726: unset($url[$key]);
727: continue;
728: }
729:
730:
731: if (!$defaultExists && ($value !== null && $value !== false && $value !== '')) {
732: $query[$key] = $value;
733: unset($url[$key]);
734: }
735: }
736:
737:
738: if (!$this->_greedy && !empty($pass)) {
739: return false;
740: }
741:
742:
743: if (!empty($this->options)) {
744: foreach ($this->options as $key => $pattern) {
745: if (isset($url[$key]) && !preg_match('#^' . $pattern . '$#u', $url[$key])) {
746: return false;
747: }
748: }
749: }
750: $url += $hostOptions;
751:
752: return $this->_writeUrl($url, $pass, $query);
753: }
754:
755: 756: 757: 758: 759: 760:
761: protected function _matchMethod($url)
762: {
763: if (empty($this->defaults['_method'])) {
764: return true;
765: }
766:
767: if (isset($url['[method]'])) {
768: deprecationWarning('The `[method]` key is deprecated. Use `_method` instead.');
769: $url['_method'] = $url['[method]'];
770: }
771: if (empty($url['_method'])) {
772: $url['_method'] = 'GET';
773: }
774: $methods = array_map('strtoupper', (array)$url['_method']);
775: foreach ($methods as $value) {
776: if (in_array($value, (array)$this->defaults['_method'])) {
777: return true;
778: }
779: }
780:
781: return false;
782: }
783:
784: 785: 786: 787: 788: 789: 790: 791: 792: 793: 794:
795: protected function _writeUrl($params, $pass = [], $query = [])
796: {
797: $pass = implode('/', array_map('rawurlencode', $pass));
798: $out = $this->template;
799:
800: $search = $replace = [];
801: foreach ($this->keys as $key) {
802: $string = null;
803: if (isset($params[$key])) {
804: $string = $params[$key];
805: } elseif (strpos($out, $key) != strlen($out) - strlen($key)) {
806: $key .= '/';
807: }
808: if ($this->braceKeys) {
809: $search[] = "{{$key}}";
810: } else {
811: $search[] = ':' . $key;
812: }
813: $replace[] = $string;
814: }
815:
816: if (strpos($this->template, '**') !== false) {
817: array_push($search, '**', '%2F');
818: array_push($replace, $pass, '/');
819: } elseif (strpos($this->template, '*') !== false) {
820: $search[] = '*';
821: $replace[] = $pass;
822: }
823: $out = str_replace($search, $replace, $out);
824:
825:
826: if (isset($params['_base'])) {
827: $out = $params['_base'] . $out;
828: unset($params['_base']);
829: }
830:
831: $out = str_replace('//', '/', $out);
832: if (isset($params['_scheme']) ||
833: isset($params['_host']) ||
834: isset($params['_port'])
835: ) {
836: $host = $params['_host'];
837:
838:
839: if (isset($params['_port'])) {
840: $host .= ':' . $params['_port'];
841: }
842: $scheme = isset($params['_scheme']) ? $params['_scheme'] : 'http';
843: $out = "{$scheme}://{$host}{$out}";
844: }
845: if (!empty($params['_ext']) || !empty($query)) {
846: $out = rtrim($out, '/');
847: }
848: if (!empty($params['_ext'])) {
849: $out .= '.' . $params['_ext'];
850: }
851: if (!empty($query)) {
852: $out .= rtrim('?' . http_build_query($query), '?');
853: }
854:
855: return $out;
856: }
857:
858: 859: 860: 861: 862:
863: public function staticPath()
864: {
865: $routeKey = strpos($this->template, ':');
866: if ($routeKey !== false) {
867: return substr($this->template, 0, $routeKey);
868: }
869: $routeKey = strpos($this->template, '{');
870: if ($routeKey !== false && strpos($this->template, '}') !== false) {
871: return substr($this->template, 0, $routeKey);
872: }
873: $star = strpos($this->template, '*');
874: if ($star !== false) {
875: $path = rtrim(substr($this->template, 0, $star), '/');
876:
877: return $path === '' ? '/' : $path;
878: }
879:
880: return $this->template;
881: }
882:
883: 884: 885: 886: 887: 888: 889:
890: public function setMiddleware(array $middleware)
891: {
892: $this->middleware = $middleware;
893:
894: return $this;
895: }
896:
897: 898: 899: 900: 901:
902: public function getMiddleware()
903: {
904: return $this->middleware;
905: }
906:
907: 908: 909: 910: 911: 912: 913: 914: 915:
916: public static function __set_state($fields)
917: {
918: $class = get_called_class();
919: $obj = new $class('');
920: foreach ($fields as $field => $value) {
921: $obj->$field = $value;
922: }
923:
924: return $obj;
925: }
926: }
927: