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