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