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