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