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