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