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