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: 20: 21: 22:
23: class Route extends Nette\Object implements Application\IRouter
24: {
25: const PRESENTER_KEY = 'presenter';
26: const MODULE_KEY = 'module';
27:
28:
29: const CASE_SENSITIVE = 256;
30:
31:
32: const HOST = 1,
33: PATH = 2,
34: RELATIVE = 3;
35:
36:
37: const VALUE = 'value';
38: const PATTERN = 'pattern';
39: const FILTER_IN = 'filterIn';
40: const FILTER_OUT = 'filterOut';
41: const FILTER_TABLE = 'filterTable';
42: const FILTER_STRICT = 'filterStrict';
43:
44:
45: const OPTIONAL = 0,
46: PATH_OPTIONAL = 1,
47: CONSTANT = 2;
48:
49:
50: public static $defaultFlags = 0;
51:
52:
53: public static $styles = array(
54: '#' => array(
55: self::PATTERN => '[^/]+',
56: self::FILTER_OUT => array(__CLASS__, 'param2path'),
57: ),
58: '?#' => array(
59: ),
60: 'module' => array(
61: self::PATTERN => '[a-z][a-z0-9.-]*',
62: self::FILTER_IN => array(__CLASS__, 'path2presenter'),
63: self::FILTER_OUT => array(__CLASS__, 'presenter2path'),
64: ),
65: 'presenter' => array(
66: self::PATTERN => '[a-z][a-z0-9.-]*',
67: self::FILTER_IN => array(__CLASS__, 'path2presenter'),
68: self::FILTER_OUT => array(__CLASS__, 'presenter2path'),
69: ),
70: 'action' => array(
71: self::PATTERN => '[a-z][a-z0-9-]*',
72: self::FILTER_IN => array(__CLASS__, 'path2action'),
73: self::FILTER_OUT => array(__CLASS__, 'action2path'),
74: ),
75: '?module' => array(
76: ),
77: '?presenter' => array(
78: ),
79: '?action' => array(
80: ),
81: );
82:
83:
84: private $mask;
85:
86:
87: private $sequence;
88:
89:
90: private $re;
91:
92:
93: private $aliases;
94:
95:
96: private $metadata = array();
97:
98:
99: private $xlat;
100:
101:
102: private $type;
103:
104:
105: private $flags;
106:
107:
108: private $lastRefUrl;
109:
110:
111: private $lastBaseUrl;
112:
113:
114: 115: 116: 117: 118:
119: public function __construct($mask, $metadata = array(), $flags = 0)
120: {
121: if (is_string($metadata)) {
122: $a = strrpos($metadata, ':');
123: if (!$a) {
124: throw new Nette\InvalidArgumentException("Second argument must be array or string in format Presenter:action, '$metadata' given.");
125: }
126: $metadata = array(
127: self::PRESENTER_KEY => substr($metadata, 0, $a),
128: 'action' => $a === strlen($metadata) - 1 ? NULL : substr($metadata, $a + 1),
129: );
130: } elseif ($metadata instanceof \Closure || $metadata instanceof Nette\Callback) {
131: $metadata = array(
132: self::PRESENTER_KEY => 'Nette:Micro',
133: 'callback' => $metadata,
134: );
135: }
136:
137: $this->flags = $flags | static::$defaultFlags;
138: $this->setMask($mask, $metadata);
139: }
140:
141:
142: 143: 144: 145:
146: public function match(Nette\Http\IRequest $httpRequest)
147: {
148:
149:
150:
151: $url = $httpRequest->getUrl();
152: $re = $this->re;
153:
154: if ($this->type === self::HOST) {
155: $host = $url->getHost();
156: $path = '//' . $host . $url->getPath();
157: $host = ip2long($host) ? array($host) : array_reverse(explode('.', $host));
158: $re = strtr($re, array(
159: '/%basePath%/' => preg_quote($url->getBasePath(), '#'),
160: '%tld%' => preg_quote($host[0], '#'),
161: '%domain%' => preg_quote(isset($host[1]) ? "$host[1].$host[0]" : $host[0], '#'),
162: ));
163:
164: } elseif ($this->type === self::RELATIVE) {
165: $basePath = $url->getBasePath();
166: if (strncmp($url->getPath(), $basePath, strlen($basePath)) !== 0) {
167: return NULL;
168: }
169: $path = (string) substr($url->getPath(), strlen($basePath));
170:
171: } else {
172: $path = $url->getPath();
173: }
174:
175: if ($path !== '') {
176: $path = rtrim(rawurldecode($path), '/') . '/';
177: }
178:
179: if (!$matches = Strings::match($path, $re)) {
180:
181: return NULL;
182: }
183:
184:
185: $params = array();
186: foreach ($matches as $k => $v) {
187: if (is_string($k) && $v !== '') {
188: $params[$this->aliases[$k]] = $v;
189: }
190: }
191:
192:
193:
194: foreach ($this->metadata as $name => $meta) {
195: if (!isset($params[$name]) && 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']) && strncmp($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: } elseif (is_scalar($params[$name])) {
315: $params[$name] = (string) $params[$name];
316: }
317:
318: if ($params[$name] === $meta[self::VALUE]) {
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: if ($this->type !== self::HOST) {
392: if ($this->lastRefUrl !== $refUrl) {
393: $scheme = ($this->flags & self::SECURED ? 'https://' : 'http://');
394: $basePath = ($this->type === self::RELATIVE ? $refUrl->getBasePath() : '');
395: $this->lastBaseUrl = $scheme . $refUrl->getAuthority() . $basePath;
396: $this->lastRefUrl = $refUrl;
397: }
398: $url = $this->lastBaseUrl . $url;
399:
400: } else {
401: $host = $refUrl->getHost();
402: $host = ip2long($host) ? array($host) : array_reverse(explode('.', $host));
403: $url = strtr($url, array(
404: '/%basePath%/' => $refUrl->getBasePath(),
405: '%tld%' => $host[0],
406: '%domain%' => isset($host[1]) ? "$host[1].$host[0]" : $host[0],
407: ));
408: $url = ($this->flags & self::SECURED ? 'https:' : 'http:') . $url;
409: }
410:
411: if (strpos($url, '//', 7) !== FALSE) {
412: return NULL;
413: }
414:
415:
416: if ($this->xlat) {
417: $params = self::renameKeys($params, $this->xlat);
418: }
419:
420: $sep = ini_get('arg_separator.input');
421: $query = http_build_query($params, '', $sep ? $sep[0] : '&');
422: if ($query != '') {
423: $url .= '?' . $query;
424: }
425:
426: return $url;
427: }
428:
429:
430: 431: 432: 433: 434: 435:
436: private function setMask($mask, array $metadata)
437: {
438: $this->mask = $mask;
439:
440:
441: if (substr($mask, 0, 2) === '//') {
442: $this->type = self::HOST;
443:
444: } elseif (substr($mask, 0, 1) === '/') {
445: $this->type = self::PATH;
446:
447: } else {
448: $this->type = self::RELATIVE;
449: }
450:
451: foreach ($metadata as $name => $meta) {
452: if (!is_array($meta)) {
453: $metadata[$name] = $meta = array(self::VALUE => $meta);
454: }
455:
456: if (array_key_exists(self::VALUE, $meta)) {
457: if (is_scalar($meta[self::VALUE])) {
458: $metadata[$name][self::VALUE] = (string) $meta[self::VALUE];
459: }
460: $metadata[$name]['fixity'] = self::CONSTANT;
461: }
462: }
463:
464: if (strpbrk($mask, '?<[') === FALSE) {
465: $this->re = '#' . preg_quote($mask, '#') . '/?\z#A';
466: $this->sequence = array($mask);
467: $this->metadata = $metadata;
468: return;
469: }
470:
471:
472:
473: $parts = Strings::split($mask, '/<([^>#= ]+)(=[^># ]*)? *([^>#]*)(#?[^>\[\]]*)>|(\[!?|\]|\s*\?.*)/');
474:
475: $this->xlat = array();
476: $i = count($parts) - 1;
477:
478:
479: if (isset($parts[$i - 1]) && substr(ltrim($parts[$i - 1]), 0, 1) === '?') {
480:
481: $matches = Strings::matchAll($parts[$i - 1], '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/');
482:
483: foreach ($matches as $match) {
484: list(, $param, $name, $pattern, $class) = $match;
485:
486: if ($class !== '') {
487: if (!isset(static::$styles[$class])) {
488: throw new Nette\InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
489: }
490: $meta = static::$styles[$class];
491:
492: } elseif (isset(static::$styles['?' . $name])) {
493: $meta = static::$styles['?' . $name];
494:
495: } else {
496: $meta = static::$styles['?#'];
497: }
498:
499: if (isset($metadata[$name])) {
500: $meta = $metadata[$name] + $meta;
501: }
502:
503: if (array_key_exists(self::VALUE, $meta)) {
504: $meta['fixity'] = self::OPTIONAL;
505: }
506:
507: unset($meta['pattern']);
508: $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]);
509:
510: $metadata[$name] = $meta;
511: if ($param !== '') {
512: $this->xlat[$name] = $param;
513: }
514: }
515: $i -= 6;
516: }
517:
518:
519: $brackets = 0;
520: $re = '';
521: $sequence = array();
522: $autoOptional = TRUE;
523: $aliases = array();
524: do {
525: array_unshift($sequence, $parts[$i]);
526: $re = preg_quote($parts[$i], '#') . $re;
527: if ($i === 0) {
528: break;
529: }
530: $i--;
531:
532: $part = $parts[$i];
533: if ($part === '[' || $part === ']' || $part === '[!') {
534: $brackets += $part[0] === '[' ? -1 : 1;
535: if ($brackets < 0) {
536: throw new Nette\InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
537: }
538: array_unshift($sequence, $part);
539: $re = ($part[0] === '[' ? '(?:' : ')?') . $re;
540: $i -= 5;
541: continue;
542: }
543:
544: $class = $parts[$i]; $i--;
545: $pattern = trim($parts[$i]); $i--;
546: $default = $parts[$i]; $i--;
547: $name = $parts[$i]; $i--;
548: array_unshift($sequence, $name);
549:
550: if ($name[0] === '?') {
551: $name = substr($name, 1);
552: $re = $pattern ? '(?:' . preg_quote($name, '#') . "|$pattern)$re" : preg_quote($name, '#') . $re;
553: $sequence[1] = $name . $sequence[1];
554: continue;
555: }
556:
557:
558: if ($class !== '') {
559: if (!isset(static::$styles[$class])) {
560: throw new Nette\InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
561: }
562: $meta = static::$styles[$class];
563:
564: } elseif (isset(static::$styles[$name])) {
565: $meta = static::$styles[$name];
566:
567: } else {
568: $meta = static::$styles['#'];
569: }
570:
571: if (isset($metadata[$name])) {
572: $meta = $metadata[$name] + $meta;
573: }
574:
575: if ($pattern == '' && isset($meta[self::PATTERN])) {
576: $pattern = $meta[self::PATTERN];
577: }
578:
579: if ($default !== '') {
580: $meta[self::VALUE] = (string) substr($default, 1);
581: $meta['fixity'] = self::PATH_OPTIONAL;
582: }
583:
584: $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]);
585: if (array_key_exists(self::VALUE, $meta)) {
586: if (isset($meta['filterTable2'][$meta[self::VALUE]])) {
587: $meta['defOut'] = $meta['filterTable2'][$meta[self::VALUE]];
588:
589: } elseif (isset($meta[self::FILTER_OUT])) {
590: $meta['defOut'] = call_user_func($meta[self::FILTER_OUT], $meta[self::VALUE]);
591:
592: } else {
593: $meta['defOut'] = $meta[self::VALUE];
594: }
595: }
596: $meta[self::PATTERN] = "#(?:$pattern)\\z#A";
597:
598:
599: $aliases['p' . $i] = $name;
600: $re = '(?P<p' . $i . '>(?U)' . $pattern . ')' . $re;
601: if ($brackets) {
602: if (!isset($meta[self::VALUE])) {
603: $meta[self::VALUE] = $meta['defOut'] = NULL;
604: }
605: $meta['fixity'] = self::PATH_OPTIONAL;
606:
607: } elseif (!$autoOptional) {
608: unset($meta['fixity']);
609:
610: } elseif (isset($meta['fixity'])) {
611: $re = '(?:' . $re . ')?';
612: $meta['fixity'] = self::PATH_OPTIONAL;
613:
614: } else {
615: $autoOptional = FALSE;
616: }
617:
618: $metadata[$name] = $meta;
619: } while (TRUE);
620:
621: if ($brackets) {
622: throw new Nette\InvalidArgumentException("Missing closing ']' in mask '$mask'.");
623: }
624:
625: $this->aliases = $aliases;
626: $this->re = '#' . $re . '/?\z#A';
627: $this->metadata = $metadata;
628: $this->sequence = $sequence;
629: }
630:
631:
632: 633: 634: 635:
636: public function getMask()
637: {
638: return $this->mask;
639: }
640:
641:
642: 643: 644: 645:
646: public function getDefaults()
647: {
648: $defaults = array();
649: foreach ($this->metadata as $name => $meta) {
650: if (isset($meta['fixity'])) {
651: $defaults[$name] = $meta[self::VALUE];
652: }
653: }
654: return $defaults;
655: }
656:
657:
658: 659: 660: 661:
662: public function getFlags()
663: {
664: return $this->flags;
665: }
666:
667:
668:
669:
670:
671: 672: 673: 674: 675:
676: public function getTargetPresenters()
677: {
678: if ($this->flags & self::ONE_WAY) {
679: return array();
680: }
681:
682: $m = $this->metadata;
683: $module = '';
684:
685: if (isset($m[self::MODULE_KEY])) {
686: if (isset($m[self::MODULE_KEY]['fixity']) && $m[self::MODULE_KEY]['fixity'] === self::CONSTANT) {
687: $module = $m[self::MODULE_KEY][self::VALUE] . ':';
688: } else {
689: return NULL;
690: }
691: }
692:
693: if (isset($m[self::PRESENTER_KEY]['fixity']) && $m[self::PRESENTER_KEY]['fixity'] === self::CONSTANT) {
694: return array($module . $m[self::PRESENTER_KEY][self::VALUE]);
695: }
696: return NULL;
697: }
698:
699:
700: 701: 702: 703: 704: 705:
706: private static function renameKeys($arr, $xlat)
707: {
708: if (empty($xlat)) {
709: return $arr;
710: }
711:
712: $res = array();
713: $occupied = array_flip($xlat);
714: foreach ($arr as $k => $v) {
715: if (isset($xlat[$k])) {
716: $res[$xlat[$k]] = $v;
717:
718: } elseif (!isset($occupied[$k])) {
719: $res[$k] = $v;
720: }
721: }
722: return $res;
723: }
724:
725:
726:
727:
728:
729: 730: 731: 732: 733:
734: private static function action2path($s)
735: {
736: $s = preg_replace('#(.)(?=[A-Z])#', '$1-', $s);
737: $s = strtolower($s);
738: $s = rawurlencode($s);
739: return $s;
740: }
741:
742:
743: 744: 745: 746: 747:
748: private static function path2action($s)
749: {
750: $s = preg_replace('#-(?=[a-z])#', ' ', $s);
751: $s = lcfirst(ucwords($s));
752: $s = str_replace(' ', '', $s);
753: return $s;
754: }
755:
756:
757: 758: 759: 760: 761:
762: private static function presenter2path($s)
763: {
764: $s = strtr($s, ':', '.');
765: $s = preg_replace('#([^.])(?=[A-Z])#', '$1-', $s);
766: $s = strtolower($s);
767: $s = rawurlencode($s);
768: return $s;
769: }
770:
771:
772: 773: 774: 775: 776:
777: private static function path2presenter($s)
778: {
779: $s = preg_replace('#([.-])(?=[a-z])#', '$1 ', $s);
780: $s = ucwords($s);
781: $s = str_replace('. ', ':', $s);
782: $s = str_replace('- ', '', $s);
783: return $s;
784: }
785:
786:
787: 788: 789: 790: 791:
792: private static function param2path($s)
793: {
794: return str_replace('%2F', '/', rawurlencode($s));
795: }
796:
797:
798:
799:
800:
801: 802: 803:
804: public static function addStyle($style, $parent = '#')
805: {
806: trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
807: if (isset(static::$styles[$style])) {
808: throw new Nette\InvalidArgumentException("Style '$style' already exists.");
809: }
810:
811: if ($parent !== NULL) {
812: if (!isset(static::$styles[$parent])) {
813: throw new Nette\InvalidArgumentException("Parent style '$parent' doesn't exist.");
814: }
815: static::$styles[$style] = static::$styles[$parent];
816:
817: } else {
818: static::$styles[$style] = array();
819: }
820: }
821:
822:
823: 824: 825:
826: public static function setStyleProperty($style, $key, $value)
827: {
828: trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
829: if (!isset(static::$styles[$style])) {
830: throw new Nette\InvalidArgumentException("Style '$style' doesn't exist.");
831: }
832: static::$styles[$style][$key] = $value;
833: }
834:
835: }
836: