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