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