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