1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Application\Routers;
9:
10: use Nette;
11: use Nette\Application;
12: use Nette\Utils\Strings;
13:
14:
15: 16: 17: 18:
19: class Route implements Application\IRouter
20: {
21: use Nette\SmartObject;
22:
23: const PRESENTER_KEY = 'presenter';
24: const MODULE_KEY = 'module';
25:
26:
27: const HOST = 1,
28: PATH = 2,
29: RELATIVE = 3;
30:
31:
32: const VALUE = 'value';
33: const PATTERN = 'pattern';
34: const FILTER_IN = 'filterIn';
35: const FILTER_OUT = 'filterOut';
36: const FILTER_TABLE = 'filterTable';
37: const FILTER_STRICT = 'filterStrict';
38:
39:
40: const OPTIONAL = 0,
41: PATH_OPTIONAL = 1,
42: CONSTANT = 2;
43:
44:
45: public static $defaultFlags = 0;
46:
47:
48: public static $styles = [
49: '#' => [
50: self::PATTERN => '[^/]+',
51: self::FILTER_OUT => [__CLASS__, 'param2path'],
52: ],
53: '?#' => [
54: ],
55: 'module' => [
56: self::PATTERN => '[a-z][a-z0-9.-]*',
57: self::FILTER_IN => [__CLASS__, 'path2presenter'],
58: self::FILTER_OUT => [__CLASS__, 'presenter2path'],
59: ],
60: 'presenter' => [
61: self::PATTERN => '[a-z][a-z0-9.-]*',
62: self::FILTER_IN => [__CLASS__, 'path2presenter'],
63: self::FILTER_OUT => [__CLASS__, 'presenter2path'],
64: ],
65: 'action' => [
66: self::PATTERN => '[a-z][a-z0-9-]*',
67: self::FILTER_IN => [__CLASS__, 'path2action'],
68: self::FILTER_OUT => [__CLASS__, 'action2path'],
69: ],
70: '?module' => [
71: ],
72: '?presenter' => [
73: ],
74: '?action' => [
75: ],
76: ];
77:
78:
79: private $mask;
80:
81:
82: private $sequence;
83:
84:
85: private $re;
86:
87:
88: private $aliases;
89:
90:
91: private $metadata = [];
92:
93:
94: private $xlat;
95:
96:
97: private $type;
98:
99:
100: private $scheme;
101:
102:
103: private $flags;
104:
105:
106: private $lastRefUrl;
107:
108:
109: private $lastBaseUrl;
110:
111:
112: 113: 114: 115: 116:
117: public function __construct($mask, $metadata = [], $flags = 0)
118: {
119: if (is_string($metadata)) {
120: list($presenter, $action) = Nette\Application\Helpers::splitName($metadata);
121: if (!$presenter) {
122: throw new Nette\InvalidArgumentException("Second argument must be array or string in format Presenter:action, '$metadata' given.");
123: }
124: $metadata = [self::PRESENTER_KEY => $presenter];
125: if ($action !== '') {
126: $metadata['action'] = $action;
127: }
128: } elseif ($metadata instanceof \Closure || $metadata instanceof Nette\Callback) {
129: if ($metadata instanceof Nette\Callback) {
130: trigger_error('Nette\Callback is deprecated, use Nette\Utils\Callback::closure().', E_USER_DEPRECATED);
131: }
132: $metadata = [
133: self::PRESENTER_KEY => 'Nette:Micro',
134: 'callback' => $metadata,
135: ];
136: }
137:
138: $this->flags = $flags | static::$defaultFlags;
139: $this->setMask($mask, $metadata);
140: if (static::$defaultFlags) {
141: trigger_error('Route::$defaultFlags is deprecated, router by default keeps the used protocol.', E_USER_DEPRECATED);
142: } elseif ($flags & self::SECURED) {
143: trigger_error('Router::SECURED is deprecated, specify scheme in mask.', E_USER_DEPRECATED);
144: $this->scheme = 'https';
145: }
146: }
147:
148:
149: 150: 151: 152:
153: public function match(Nette\Http\IRequest $httpRequest)
154: {
155:
156:
157:
158: $url = $httpRequest->getUrl();
159: $re = $this->re;
160:
161: if ($this->type === self::HOST) {
162: $host = $url->getHost();
163: $path = '//' . $host . $url->getPath();
164: $parts = ip2long($host) ? [$host] : array_reverse(explode('.', $host));
165: $re = strtr($re, [
166: '/%basePath%/' => preg_quote($url->getBasePath(), '#'),
167: '%tld%' => preg_quote($parts[0], '#'),
168: '%domain%' => preg_quote(isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0], '#'),
169: '%sld%' => preg_quote(isset($parts[1]) ? $parts[1] : '', '#'),
170: '%host%' => preg_quote($host, '#'),
171: ]);
172:
173: } elseif ($this->type === self::RELATIVE) {
174: $basePath = $url->getBasePath();
175: if (strncmp($url->getPath(), $basePath, strlen($basePath)) !== 0) {
176: return NULL;
177: }
178: $path = (string) substr($url->getPath(), strlen($basePath));
179:
180: } else {
181: $path = $url->getPath();
182: }
183:
184: if ($path !== '') {
185: $path = rtrim(rawurldecode($path), '/') . '/';
186: }
187:
188: if (!$matches = Strings::match($path, $re)) {
189:
190: return NULL;
191: }
192:
193:
194: $params = [];
195: foreach ($matches as $k => $v) {
196: if (is_string($k) && $v !== '') {
197: $params[$this->aliases[$k]] = $v;
198: }
199: }
200:
201:
202:
203: foreach ($this->metadata as $name => $meta) {
204: if (!isset($params[$name]) && isset($meta['fixity']) && $meta['fixity'] !== self::OPTIONAL) {
205: $params[$name] = NULL;
206: }
207: }
208:
209:
210:
211: if ($this->xlat) {
212: $params += self::renameKeys($httpRequest->getQuery(), array_flip($this->xlat));
213: } else {
214: $params += $httpRequest->getQuery();
215: }
216:
217:
218:
219: foreach ($this->metadata as $name => $meta) {
220: if (isset($params[$name])) {
221: if (!is_scalar($params[$name])) {
222:
223: } elseif (isset($meta[self::FILTER_TABLE][$params[$name]])) {
224: $params[$name] = $meta[self::FILTER_TABLE][$params[$name]];
225:
226: } elseif (isset($meta[self::FILTER_TABLE]) && !empty($meta[self::FILTER_STRICT])) {
227: return NULL;
228:
229: } elseif (isset($meta[self::FILTER_IN])) {
230: $params[$name] = call_user_func($meta[self::FILTER_IN], (string) $params[$name]);
231: if ($params[$name] === NULL && !isset($meta['fixity'])) {
232: return NULL;
233: }
234: }
235:
236: } elseif (isset($meta['fixity'])) {
237: $params[$name] = $meta[self::VALUE];
238: }
239: }
240:
241: if (isset($this->metadata[NULL][self::FILTER_IN])) {
242: $params = call_user_func($this->metadata[NULL][self::FILTER_IN], $params);
243: if ($params === NULL) {
244: return NULL;
245: }
246: }
247:
248:
249: if (!isset($params[self::PRESENTER_KEY])) {
250: throw new Nette\InvalidStateException('Missing presenter in route definition.');
251: } elseif (!is_string($params[self::PRESENTER_KEY])) {
252: return NULL;
253: }
254: $presenter = $params[self::PRESENTER_KEY];
255: unset($params[self::PRESENTER_KEY]);
256:
257: if (isset($this->metadata[self::MODULE_KEY])) {
258: $presenter = (isset($params[self::MODULE_KEY]) ? $params[self::MODULE_KEY] . ':' : '') . $presenter;
259: unset($params[self::MODULE_KEY]);
260: }
261:
262: return new Application\Request(
263: $presenter,
264: $httpRequest->getMethod(),
265: $params,
266: $httpRequest->getPost(),
267: $httpRequest->getFiles(),
268: [Application\Request::SECURED => $httpRequest->isSecured()]
269: );
270: }
271:
272:
273: 274: 275: 276:
277: public function constructUrl(Application\Request $appRequest, Nette\Http\Url $refUrl)
278: {
279: if ($this->flags & self::ONE_WAY) {
280: return NULL;
281: }
282:
283: $params = $appRequest->getParameters();
284: $metadata = $this->metadata;
285:
286: $presenter = $appRequest->getPresenterName();
287: $params[self::PRESENTER_KEY] = $presenter;
288:
289: if (isset($metadata[NULL][self::FILTER_OUT])) {
290: $params = call_user_func($metadata[NULL][self::FILTER_OUT], $params);
291: if ($params === NULL) {
292: return NULL;
293: }
294: }
295:
296: if (isset($metadata[self::MODULE_KEY])) {
297: $module = $metadata[self::MODULE_KEY];
298: if (isset($module['fixity']) && strncmp($presenter, $module[self::VALUE] . ':', strlen($module[self::VALUE]) + 1) === 0) {
299: $a = strlen($module[self::VALUE]);
300: } else {
301: $a = strrpos($presenter, ':');
302: }
303: if ($a === FALSE) {
304: $params[self::MODULE_KEY] = isset($module[self::VALUE]) ? '' : NULL;
305: } else {
306: $params[self::MODULE_KEY] = substr($presenter, 0, $a);
307: $params[self::PRESENTER_KEY] = substr($presenter, $a + 1);
308: }
309: }
310:
311: foreach ($metadata as $name => $meta) {
312: if (!isset($params[$name])) {
313: continue;
314: }
315:
316: if (isset($meta['fixity'])) {
317: if ($params[$name] === FALSE) {
318: $params[$name] = '0';
319: } elseif (is_scalar($params[$name])) {
320: $params[$name] = (string) $params[$name];
321: }
322:
323: if ($params[$name] === $meta[self::VALUE]) {
324: unset($params[$name]);
325: continue;
326:
327: } elseif ($meta['fixity'] === self::CONSTANT) {
328: return NULL;
329: }
330: }
331:
332: if (is_scalar($params[$name]) && isset($meta['filterTable2'][$params[$name]])) {
333: $params[$name] = $meta['filterTable2'][$params[$name]];
334:
335: } elseif (isset($meta['filterTable2']) && !empty($meta[self::FILTER_STRICT])) {
336: return NULL;
337:
338: } elseif (isset($meta[self::FILTER_OUT])) {
339: $params[$name] = call_user_func($meta[self::FILTER_OUT], $params[$name]);
340: }
341:
342: if (isset($meta[self::PATTERN]) && !preg_match($meta[self::PATTERN], rawurldecode($params[$name]))) {
343: return NULL;
344: }
345: }
346:
347:
348: $sequence = $this->sequence;
349: $brackets = [];
350: $required = NULL;
351: $url = '';
352: $i = count($sequence) - 1;
353: do {
354: $url = $sequence[$i] . $url;
355: if ($i === 0) {
356: break;
357: }
358: $i--;
359:
360: $name = $sequence[$i]; $i--;
361:
362: if ($name === ']') {
363: $brackets[] = $url;
364:
365: } elseif ($name[0] === '[') {
366: $tmp = array_pop($brackets);
367: if ($required < count($brackets) + 1) {
368: if ($name !== '[!') {
369: $url = $tmp;
370: }
371: } else {
372: $required = count($brackets);
373: }
374:
375: } elseif ($name[0] === '?') {
376: continue;
377:
378: } elseif (isset($params[$name]) && $params[$name] != '') {
379: $required = count($brackets);
380: $url = $params[$name] . $url;
381: unset($params[$name]);
382:
383: } elseif (isset($metadata[$name]['fixity'])) {
384: if ($required === NULL && !$brackets) {
385: $url = '';
386: } else {
387: $url = $metadata[$name]['defOut'] . $url;
388: }
389:
390: } else {
391: return NULL;
392: }
393: } while (TRUE);
394:
395: $scheme = $this->scheme ?: $refUrl->getScheme();
396:
397: if ($this->type === self::HOST) {
398: $host = $refUrl->getHost();
399: $parts = ip2long($host) ? [$host] : array_reverse(explode('.', $host));
400: $url = strtr($url, [
401: '/%basePath%/' => $refUrl->getBasePath(),
402: '%tld%' => $parts[0],
403: '%domain%' => isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0],
404: '%sld%' => isset($parts[1]) ? $parts[1] : '',
405: '%host%' => $host,
406: ]);
407: $url = $scheme . ':' . $url;
408: } else {
409: if ($this->lastRefUrl !== $refUrl) {
410: $basePath = ($this->type === self::RELATIVE ? $refUrl->getBasePath() : '');
411: $this->lastBaseUrl = $scheme . '://' . $refUrl->getAuthority() . $basePath;
412: $this->lastRefUrl = $refUrl;
413: }
414: $url = $this->lastBaseUrl . $url;
415: }
416:
417: if (strpos($url, '//', strlen($scheme) + 3) !== FALSE) {
418: return NULL;
419: }
420:
421:
422: if ($this->xlat) {
423: $params = self::renameKeys($params, $this->xlat);
424: }
425:
426: $sep = ini_get('arg_separator.input');
427: $query = http_build_query($params, '', $sep ? $sep[0] : '&');
428: if ($query != '') {
429: $url .= '?' . $query;
430: }
431:
432: return $url;
433: }
434:
435:
436: 437: 438: 439: 440: 441:
442: private function setMask($mask, array $metadata)
443: {
444: $this->mask = $mask;
445:
446:
447: if (preg_match('#(?:(https?):)?(//.*)#A', $mask, $m)) {
448: $this->type = self::HOST;
449: list(, $this->scheme, $mask) = $m;
450:
451: } elseif (substr($mask, 0, 1) === '/') {
452: $this->type = self::PATH;
453:
454: } else {
455: $this->type = self::RELATIVE;
456: }
457:
458: foreach ($metadata as $name => $meta) {
459: if (!is_array($meta)) {
460: $metadata[$name] = $meta = [self::VALUE => $meta];
461: }
462:
463: if (array_key_exists(self::VALUE, $meta)) {
464: if (is_scalar($meta[self::VALUE])) {
465: $metadata[$name][self::VALUE] = (string) $meta[self::VALUE];
466: }
467: $metadata[$name]['fixity'] = self::CONSTANT;
468: }
469: }
470:
471: if (strpbrk($mask, '?<>[]') === FALSE) {
472: $this->re = '#' . preg_quote($mask, '#') . '/?\z#A';
473: $this->sequence = [$mask];
474: $this->metadata = $metadata;
475: return;
476: }
477:
478:
479:
480: $parts = Strings::split($mask, '/<([^<>= ]+)(=[^<> ]*)? *([^<>]*)>|(\[!?|\]|\s*\?.*)/');
481:
482: $this->xlat = [];
483: $i = count($parts) - 1;
484:
485:
486: if (isset($parts[$i - 1]) && substr(ltrim($parts[$i - 1]), 0, 1) === '?') {
487:
488: $matches = Strings::matchAll($parts[$i - 1], '/(?:([a-zA-Z0-9_.-]+)=)?<([^> ]+) *([^>]*)>/');
489:
490: foreach ($matches as list(, $param, $name, $pattern)) {
491: if (isset(static::$styles['?' . $name])) {
492: $meta = static::$styles['?' . $name];
493: } else {
494: $meta = static::$styles['?#'];
495: }
496:
497: if (isset($metadata[$name])) {
498: $meta = $metadata[$name] + $meta;
499: }
500:
501: if (array_key_exists(self::VALUE, $meta)) {
502: $meta['fixity'] = self::OPTIONAL;
503: }
504:
505: unset($meta['pattern']);
506: $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]);
507:
508: $metadata[$name] = $meta;
509: if ($param !== '') {
510: $this->xlat[$name] = $param;
511: }
512: }
513: $i -= 5;
514: }
515:
516:
517: $brackets = 0;
518: $re = '';
519: $sequence = [];
520: $autoOptional = TRUE;
521: $aliases = [];
522: do {
523: $part = $parts[$i];
524: if (strpbrk($part, '<>') !== FALSE) {
525: throw new Nette\InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
526: }
527: array_unshift($sequence, $part);
528: $re = preg_quote($part, '#') . $re;
529: if ($i === 0) {
530: break;
531: }
532: $i--;
533:
534: $part = $parts[$i];
535: if ($part === '[' || $part === ']' || $part === '[!') {
536: $brackets += $part[0] === '[' ? -1 : 1;
537: if ($brackets < 0) {
538: throw new Nette\InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
539: }
540: array_unshift($sequence, $part);
541: $re = ($part[0] === '[' ? '(?:' : ')?') . $re;
542: $i -= 4;
543: continue;
544: }
545:
546: $pattern = trim($parts[$i]); $i--;
547: $default = $parts[$i]; $i--;
548: $name = $parts[$i]; $i--;
549: array_unshift($sequence, $name);
550:
551: if ($name[0] === '?') {
552: $name = substr($name, 1);
553: $re = $pattern ? '(?:' . preg_quote($name, '#') . "|$pattern)$re" : preg_quote($name, '#') . $re;
554: $sequence[1] = $name . $sequence[1];
555: continue;
556: }
557:
558:
559: if (isset(static::$styles[$name])) {
560: $meta = static::$styles[$name];
561: } else {
562: $meta = static::$styles['#'];
563: }
564:
565: if (isset($metadata[$name])) {
566: $meta = $metadata[$name] + $meta;
567: }
568:
569: if ($pattern == '' && isset($meta[self::PATTERN])) {
570: $pattern = $meta[self::PATTERN];
571: }
572:
573: if ($default !== '') {
574: $meta[self::VALUE] = (string) substr($default, 1);
575: $meta['fixity'] = self::PATH_OPTIONAL;
576: }
577:
578: $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]);
579: if (array_key_exists(self::VALUE, $meta)) {
580: if (isset($meta['filterTable2'][$meta[self::VALUE]])) {
581: $meta['defOut'] = $meta['filterTable2'][$meta[self::VALUE]];
582:
583: } elseif (isset($meta[self::FILTER_OUT])) {
584: $meta['defOut'] = call_user_func($meta[self::FILTER_OUT], $meta[self::VALUE]);
585:
586: } else {
587: $meta['defOut'] = $meta[self::VALUE];
588: }
589: }
590: $meta[self::PATTERN] = "#(?:$pattern)\\z#A";
591:
592:
593: $aliases['p' . $i] = $name;
594: $re = '(?P<p' . $i . '>(?U)' . $pattern . ')' . $re;
595: if ($brackets) {
596: if (!isset($meta[self::VALUE])) {
597: $meta[self::VALUE] = $meta['defOut'] = NULL;
598: }
599: $meta['fixity'] = self::PATH_OPTIONAL;
600:
601: } elseif (!$autoOptional) {
602: unset($meta['fixity']);
603:
604: } elseif (isset($meta['fixity'])) {
605: $re = '(?:' . $re . ')?';
606: $meta['fixity'] = self::PATH_OPTIONAL;
607:
608: } else {
609: $autoOptional = FALSE;
610: }
611:
612: $metadata[$name] = $meta;
613: } while (TRUE);
614:
615: if ($brackets) {
616: throw new Nette\InvalidArgumentException("Missing '[' in mask '$mask'.");
617: }
618:
619: $this->aliases = $aliases;
620: $this->re = '#' . $re . '/?\z#A';
621: $this->metadata = $metadata;
622: $this->sequence = $sequence;
623: }
624:
625:
626: 627: 628: 629:
630: public function getMask()
631: {
632: return $this->mask;
633: }
634:
635:
636: 637: 638: 639:
640: public function getDefaults()
641: {
642: $defaults = [];
643: foreach ($this->metadata as $name => $meta) {
644: if (isset($meta['fixity'])) {
645: $defaults[$name] = $meta[self::VALUE];
646: }
647: }
648: return $defaults;
649: }
650:
651:
652: 653: 654: 655:
656: public function getFlags()
657: {
658: return $this->flags;
659: }
660:
661:
662:
663:
664:
665: 666: 667: 668: 669:
670: public function getTargetPresenters()
671: {
672: if ($this->flags & self::ONE_WAY) {
673: return [];
674: }
675:
676: $m = $this->metadata;
677: $module = '';
678:
679: if (isset($m[self::MODULE_KEY])) {
680: if (isset($m[self::MODULE_KEY]['fixity']) && $m[self::MODULE_KEY]['fixity'] === self::CONSTANT) {
681: $module = $m[self::MODULE_KEY][self::VALUE] . ':';
682: } else {
683: return NULL;
684: }
685: }
686:
687: if (isset($m[self::PRESENTER_KEY]['fixity']) && $m[self::PRESENTER_KEY]['fixity'] === self::CONSTANT) {
688: return [$module . $m[self::PRESENTER_KEY][self::VALUE]];
689: }
690: return NULL;
691: }
692:
693:
694: 695: 696: 697: 698: 699:
700: private static function renameKeys($arr, $xlat)
701: {
702: if (empty($xlat)) {
703: return $arr;
704: }
705:
706: $res = [];
707: $occupied = array_flip($xlat);
708: foreach ($arr as $k => $v) {
709: if (isset($xlat[$k])) {
710: $res[$xlat[$k]] = $v;
711:
712: } elseif (!isset($occupied[$k])) {
713: $res[$k] = $v;
714: }
715: }
716: return $res;
717: }
718:
719:
720:
721:
722:
723: 724: 725: 726: 727:
728: private static function action2path($s)
729: {
730: $s = preg_replace('#(.)(?=[A-Z])#', '$1-', $s);
731: $s = strtolower($s);
732: $s = rawurlencode($s);
733: return $s;
734: }
735:
736:
737: 738: 739: 740: 741:
742: private static function path2action($s)
743: {
744: $s = preg_replace('#-(?=[a-z])#', ' ', $s);
745: $s = lcfirst(ucwords($s));
746: $s = str_replace(' ', '', $s);
747: return $s;
748: }
749:
750:
751: 752: 753: 754: 755:
756: private static function presenter2path($s)
757: {
758: $s = strtr($s, ':', '.');
759: $s = preg_replace('#([^.])(?=[A-Z])#', '$1-', $s);
760: $s = strtolower($s);
761: $s = rawurlencode($s);
762: return $s;
763: }
764:
765:
766: 767: 768: 769: 770:
771: private static function path2presenter($s)
772: {
773: $s = preg_replace('#([.-])(?=[a-z])#', '$1 ', $s);
774: $s = ucwords($s);
775: $s = str_replace('. ', ':', $s);
776: $s = str_replace('- ', '', $s);
777: return $s;
778: }
779:
780:
781: 782: 783: 784: 785:
786: private static function param2path($s)
787: {
788: return str_replace('%2F', '/', rawurlencode($s));
789: }
790:
791: }
792: