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