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