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