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