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 NRoute extends NObject 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 ? NPresenter::DEFAULT_ACTION : substr($metadata, $a + 1),
126: );
127: } elseif ($metadata instanceof Closure || $metadata instanceof NCallback) {
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 = NStrings::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 NPresenterRequest(
244: $presenter,
245: $httpRequest->getMethod(),
246: $params,
247: $httpRequest->getPost(),
248: $httpRequest->getFiles(),
249: array(NPresenterRequest::SECURED => $httpRequest->isSecured())
250: );
251: }
252:
253:
254:
255: 256: 257: 258: 259: 260:
261: public function constructUrl(NPresenterRequest $appRequest, NUrl $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 (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 = 0;
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: $url = $metadata[$name]['defOut'] . $url;
360:
361: } else {
362: return NULL;
363: }
364: } while (TRUE);
365:
366:
367:
368: if ($this->xlat) {
369: $params = self::renameKeys($params, $this->xlat);
370: }
371:
372: $sep = ini_get('arg_separator.input');
373: $query = http_build_query($params, '', $sep ? $sep[0] : '&');
374: if ($query != '') {
375: $url .= '?' . $query;
376: }
377:
378:
379: if ($this->type === self::RELATIVE) {
380: $url = '//' . $refUrl->getAuthority() . $refUrl->getBasePath() . $url;
381:
382: } elseif ($this->type === self::PATH) {
383: $url = '//' . $refUrl->getAuthority() . $url;
384: }
385:
386: if (strpos($url, '//', 2) !== FALSE) {
387: return NULL;
388: }
389:
390: $url = ($this->flags & self::SECURED ? 'https:' : 'http:') . $url;
391:
392: return $url;
393: }
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 = NStrings::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 = NStrings::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 = array(0, 0);
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)$#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 (isset($meta['fixity'])) {
566: $re = '(?:' . substr_replace($re, ')?', strlen($re) - $autoOptional[0], 0);
567: array_splice($sequence, count($sequence) - $autoOptional[1], 0, array(']', ''));
568: array_unshift($sequence, '[', '');
569: $meta['fixity'] = self::PATH_OPTIONAL;
570:
571: } else {
572: $autoOptional = array(strlen($re), count($sequence));
573: }
574:
575: $metadata[$name] = $meta;
576: } while (TRUE);
577:
578: if ($brackets) {
579: throw new InvalidArgumentException("Missing closing ']' in mask '$mask'.");
580: }
581:
582: $this->re = '#' . $re . '/?$#A' . ($this->flags & self::CASE_SENSITIVE ? '' : 'iu');
583: $this->metadata = $metadata;
584: $this->sequence = $sequence;
585: }
586:
587:
588:
589: 590: 591: 592:
593: public function getMask()
594: {
595: return $this->mask;
596: }
597:
598:
599:
600: 601: 602: 603:
604: public function getDefaults()
605: {
606: $defaults = array();
607: foreach ($this->metadata as $name => $meta) {
608: if (isset($meta['fixity'])) {
609: $defaults[$name] = $meta[self::VALUE];
610: }
611: }
612: return $defaults;
613: }
614:
615:
616:
617: 618: 619: 620:
621: public function getFlags()
622: {
623: return $this->flags;
624: }
625:
626:
627:
628:
629:
630:
631:
632: 633: 634: 635:
636: public function getTargetPresenter()
637: {
638: if ($this->flags & self::ONE_WAY) {
639: return FALSE;
640: }
641:
642: $m = $this->metadata;
643: $module = '';
644:
645: if (isset($m[self::MODULE_KEY])) {
646: if (isset($m[self::MODULE_KEY]['fixity']) && $m[self::MODULE_KEY]['fixity'] === self::CONSTANT) {
647: $module = $m[self::MODULE_KEY][self::VALUE] . ':';
648: } else {
649: return NULL;
650: }
651: }
652:
653: if (isset($m[self::PRESENTER_KEY]['fixity']) && $m[self::PRESENTER_KEY]['fixity'] === self::CONSTANT) {
654: return $module . $m[self::PRESENTER_KEY][self::VALUE];
655: }
656: return NULL;
657: }
658:
659:
660:
661: 662: 663: 664: 665: 666:
667: private static function renameKeys($arr, $xlat)
668: {
669: if (empty($xlat)) {
670: return $arr;
671: }
672:
673: $res = array();
674: $occupied = array_flip($xlat);
675: foreach ($arr as $k => $v) {
676: if (isset($xlat[$k])) {
677: $res[$xlat[$k]] = $v;
678:
679: } elseif (!isset($occupied[$k])) {
680: $res[$k] = $v;
681: }
682: }
683: return $res;
684: }
685:
686:
687:
688:
689:
690:
691:
692: 693: 694: 695: 696:
697: private static function action2path($s)
698: {
699: $s = preg_replace('#(.)(?=[A-Z])#', '$1-', $s);
700: $s = strtolower($s);
701: $s = rawurlencode($s);
702: return $s;
703: }
704:
705:
706:
707: 708: 709: 710: 711:
712: private static function path2action($s)
713: {
714: $s = strtolower($s);
715: $s = preg_replace('#-(?=[a-z])#', ' ', $s);
716: $s = substr(ucwords('x' . $s), 1);
717:
718: $s = str_replace(' ', '', $s);
719: return $s;
720: }
721:
722:
723:
724: 725: 726: 727: 728:
729: private static function presenter2path($s)
730: {
731: $s = strtr($s, ':', '.');
732: $s = preg_replace('#([^.])(?=[A-Z])#', '$1-', $s);
733: $s = strtolower($s);
734: $s = rawurlencode($s);
735: return $s;
736: }
737:
738:
739:
740: 741: 742: 743: 744:
745: private static function path2presenter($s)
746: {
747: $s = strtolower($s);
748: $s = preg_replace('#([.-])(?=[a-z])#', '$1 ', $s);
749: $s = ucwords($s);
750: $s = str_replace('. ', ':', $s);
751: $s = str_replace('- ', '', $s);
752: return $s;
753: }
754:
755:
756:
757:
758:
759:
760:
761: 762: 763: 764: 765: 766:
767: public static function addStyle($style, $parent = '#')
768: {
769: if (isset(self::$styles[$style])) {
770: throw new InvalidArgumentException("Style '$style' already exists.");
771: }
772:
773: if ($parent !== NULL) {
774: if (!isset(self::$styles[$parent])) {
775: throw new InvalidArgumentException("Parent style '$parent' doesn't exist.");
776: }
777: self::$styles[$style] = self::$styles[$parent];
778:
779: } else {
780: self::$styles[$style] = array();
781: }
782: }
783:
784:
785:
786: 787: 788: 789: 790: 791: 792:
793: public static function setStyleProperty($style, $key, $value)
794: {
795: if (!isset(self::$styles[$style])) {
796: throw new InvalidArgumentException("Style '$style' doesn't exist.");
797: }
798: self::$styles[$style][$key] = $value;
799: }
800:
801: }
802: