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