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