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