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