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