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