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