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