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