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