1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
14:
15: 16: 17: 18: 19: 20:
21: class Horde_Routes_Route
22: {
23: 24: 25: 26:
27: public $routePath;
28:
29: 30: 31: 32:
33: public $encoding = 'utf-8';
34:
35: 36: 37: 38:
39: public $decodeErrors = 'replace';
40:
41: 42: 43: 44:
45: public $static;
46:
47: 48: 49: 50:
51: public $filter;
52:
53: 54: 55: 56:
57: public $absolute;
58:
59: 60: 61: 62:
63: public $explicit;
64:
65: 66: 67: 68:
69: public $defaults = array();
70:
71: 72: 73: 74:
75: public $conditions;
76:
77: 78: 79: 80:
81: public $maxKeys;
82:
83: 84: 85: 86:
87: public $minKeys;
88:
89: 90: 91: 92:
93: public $hardCoded;
94:
95: 96: 97: 98:
99: public $reqs;
100:
101: 102: 103: 104:
105: public $regexp;
106:
107: 108: 109: 110:
111: protected $_routeList;
112:
113: 114: 115: 116:
117: protected $_routeBackwards;
118:
119: 120: 121: 122:
123: protected $_splitChars;
124:
125: 126: 127: 128:
129: protected $_prior;
130:
131: 132: 133: 134:
135: protected $_reqRegs;
136:
137: 138: 139: 140: 141:
142: protected $_memberName;
143:
144: 145: 146: 147: 148:
149: protected $_collectionName;
150:
151: 152: 153: 154: 155:
156: protected $_parentResource;
157:
158:
159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177:
178: public function __construct($routePath, $kargs = array())
179: {
180: $this->routePath = $routePath;
181:
182:
183: $this->static = isset($kargs['_static']) ? $kargs['_static'] : false;
184:
185: $this->filter = isset($kargs['_filter']) ? $kargs['_filter'] : null;
186: unset($kargs['_filter']);
187:
188: $this->absolute = isset($kargs['_absolute']) ? $kargs['_absolute'] : false;
189: unset($kargs['_absolute']);
190:
191:
192:
193: $this->_memberName = isset($kargs['_memberName']) ? $kargs['_memberName'] : null;
194: unset($kargs['_memberName']);
195:
196: $this->_collectionName = isset($kargs['_collectionName']) ? $kargs['_collectionName'] : null;
197: unset($kargs['_collectionName']);
198:
199: $this->_parentResource = isset($kargs['_parentResource']) ? $kargs['_parentResource'] : null;
200: unset($kargs['_parentResource']);
201:
202:
203: $this->conditions = isset($kargs['conditions']) ? $kargs['conditions'] : null;
204: unset($kargs['conditions']);
205:
206:
207: $this->explicit = isset($kargs['_explicit']) ? $kargs['_explicit'] : false;
208: unset($kargs['_explicit']);
209:
210:
211: $reservedKeys = array('requirements');
212:
213:
214:
215: $this->_splitChars = array('/', ',', ';', '.', '#');
216:
217:
218: if (substr($this->routePath, 0, 1) == '/') {
219: $routePath = substr($this->routePath, 1);
220: }
221:
222:
223: $this->_routeList = $this->_pathKeys($routePath);
224: $routeKeys = array();
225: foreach ($this->_routeList as $key) {
226: if (is_array($key)) { $routeKeys[] = $key['name']; }
227: }
228:
229:
230: $this->reqs = isset($kargs['requirements']) ? $kargs['requirements'] : array();
231: $this->_reqRegs = array();
232: foreach ($this->reqs as $key => $value) {
233: $this->_reqRegs[$key] = '@^' . str_replace('@', '\@', $value) . '$@';
234: }
235:
236:
237:
238: list($this->defaults, $defaultKeys) = $this->_defaults($routeKeys, $reservedKeys, $kargs);
239:
240:
241: $this->maxKeys = array_keys(array_flip(array_merge($defaultKeys, $routeKeys)));
242: list($this->minKeys, $this->_routeBackwards) = $this->_minKeys($this->_routeList);
243:
244:
245:
246: $this->hardCoded = array();
247: foreach ($this->maxKeys as $key) {
248: if (!in_array($key, $routeKeys) && $this->defaults[$key] != null) {
249: $this->hardCoded[] = $key;
250: }
251: }
252: }
253:
254: 255: 256: 257: 258: 259: 260:
261: protected function _pathKeys($routePath)
262: {
263: $collecting = false;
264: $current = '';
265: $doneOn = array();
266: $varType = '';
267: $justStarted = false;
268: $routeList = array();
269:
270: foreach (preg_split('//', $routePath, -1, PREG_SPLIT_NO_EMPTY) as $char) {
271: if (!$collecting && in_array($char, array(':', '*'))) {
272: $justStarted = true;
273: $collecting = true;
274: $varType = $char;
275: if (strlen($current) > 0) {
276: $routeList[] = $current;
277: $current = '';
278: }
279: } elseif ($collecting && $justStarted) {
280: $justStarted = false;
281: if ($char == '(') {
282: $doneOn = array(')');
283: } else {
284: $current = $char;
285:
286:
287: $doneOn = $this->_splitChars + array('-');
288: }
289: } elseif ($collecting && !in_array($char, $doneOn)) {
290: $current .= $char;
291: } elseif ($collecting) {
292: $collecting = false;
293: $routeList[] = array('type' => $varType, 'name' => $current);
294: if (in_array($char, $this->_splitChars)) {
295: $routeList[] = $char;
296: }
297: $doneOn = $varType = $current = '';
298: } else {
299: $current .= $char;
300: }
301: }
302: if ($collecting) {
303: $routeList[] = array('type' => $varType, 'name' => $current);
304: } elseif (!empty($current)) {
305: $routeList[] = $current;
306: }
307: return $routeList;
308: }
309:
310: 311: 312: 313: 314: 315: 316: 317: 318:
319: protected function _minKeys($routeList)
320: {
321: $minKeys = array();
322: $backCheck = array_reverse($routeList);
323: $gaps = false;
324: foreach ($backCheck as $part) {
325: if (!is_array($part) && !in_array($part, $this->_splitChars)) {
326: $gaps = true;
327: continue;
328: } elseif (!is_array($part)) {
329: continue;
330: }
331: $key = $part['name'];
332: if (array_key_exists($key, $this->defaults) && !$gaps)
333: continue;
334: $minKeys[] = $key;
335: $gaps = true;
336: }
337: return array($minKeys, $backCheck);
338: }
339:
340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352:
353: protected function _defaults($routeKeys, $reservedKeys, $kargs)
354: {
355: $defaults = array();
356:
357:
358: if ((!in_array('controller', $routeKeys)) &&
359: (!in_array('controller', array_keys($kargs))) &&
360: (!$this->explicit)) {
361: $kargs['controller'] = 'content';
362: }
363:
364: if (!in_array('action', $routeKeys) &&
365: (!in_array('action', array_keys($kargs))) &&
366: (!$this->explicit)) {
367: $kargs['action'] = 'index';
368: }
369:
370: $defaultKeys = array();
371: foreach (array_keys($kargs) as $key) {
372: if (!in_array($key, $reservedKeys)) {
373: $defaultKeys[] = $key;
374: }
375: }
376:
377: foreach ($defaultKeys as $key) {
378: if ($kargs[$key] !== null) {
379: $defaults[$key] = (string)$kargs[$key];
380: } else {
381: $defaults[$key] = null;
382: }
383: }
384:
385: if (in_array('action', $routeKeys) &&
386: (!array_key_exists('action', $defaults)) &&
387: (!$this->explicit)) {
388: $defaults['action'] = 'index';
389: }
390:
391: if (in_array('id', $routeKeys) &&
392: (!array_key_exists('id', $defaults)) &&
393: (!$this->explicit)) {
394: $defaults['id'] = null;
395: }
396:
397: $newDefaultKeys = array();
398: foreach (array_keys($defaults) as $key) {
399: if (!in_array($key, $reservedKeys)) {
400: $newDefaultKeys[] = $key;
401: }
402: }
403: return array($defaults, $newDefaultKeys);
404: }
405:
406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418:
419: public function makeRegexp($clist)
420: {
421: list($reg, $noreqs, $allblank) = $this->buildNextReg($this->_routeList, $clist);
422:
423: if (empty($reg)) {
424: $reg = '/';
425: }
426: $reg = $reg . '(/)?$';
427: if (substr($reg, 0, 1) != '/') {
428: $reg = '/' . $reg;
429: }
430: $reg = '^' . $reg;
431:
432: $this->regexp = $reg;
433: }
434:
435: 436: 437: 438: 439: 440: 441: 442: 443: 444:
445: public function buildNextReg($path, $clist)
446: {
447: if (!empty($path)) {
448: $part = $path[0];
449: } else {
450: $part = '';
451: }
452:
453:
454:
455:
456: list($rest, $noreqs, $allblank) = array('', true, true);
457:
458: if (count($path) > 1) {
459: $this->_prior = $part;
460: list($rest, $noreqs, $allblank) = $this->buildNextReg(array_slice($path, 1), $clist);
461: }
462:
463: if (is_array($part) && $part['type'] == ':') {
464: $var = $part['name'];
465: $partreg = '';
466:
467:
468: if (array_key_exists($var, $this->reqs)) {
469: $partreg = '(?P<' . $var . '>' . $this->reqs[$var] . ')';
470: } elseif ($var == 'controller') {
471: $partreg = '(?P<' . $var . '>' . implode('|', array_map('preg_quote', $clist)) . ')';
472: } elseif (in_array($this->_prior, array('/', '#'))) {
473: $partreg = '(?P<' . $var . '>[^' . $this->_prior . ']+?)';
474: } else {
475: if (empty($rest)) {
476: $partreg = '(?P<' . $var . '>[^/]+?)';
477: } else {
478: $partreg = '(?P<' . $var . '>[^' . implode('', $this->_splitChars) . ']+?)';
479: }
480: }
481:
482: if (array_key_exists($var, $this->reqs)) {
483: $noreqs = false;
484: }
485: if (!array_key_exists($var, $this->defaults)) {
486: $allblank = false;
487: $noreqs = false;
488: }
489:
490:
491:
492:
493:
494: if ($noreqs) {
495:
496:
497:
498:
499: if (array_key_exists($var, $this->reqs) && array_key_exists($var, $this->defaults)) {
500: $reg = '(' . $partreg . $rest . ')?';
501:
502:
503:
504: } elseif (array_key_exists($var, $this->reqs)) {
505: $allblank = false;
506: $reg = $partreg . $rest;
507:
508:
509:
510: } elseif (array_key_exists($var, $this->defaults) && in_array($this->_prior, array(',', ';', '.'))) {
511: $reg = $partreg . $rest;
512:
513:
514: } elseif (array_key_exists($var, $this->defaults)) {
515: $reg = $partreg . '?' . $rest;
516:
517:
518:
519: } else {
520: $allblank = false;
521: $reg = $partreg . $rest;
522: }
523:
524:
525:
526: } else {
527:
528:
529:
530:
531: if ($allblank && array_key_exists($var, $this->defaults)) {
532: $reg = '(' . $partreg . $rest . ')?';
533:
534:
535:
536: } else {
537: $reg = $partreg . $rest;
538: }
539: }
540: } elseif (is_array($part) && $part['type'] == '*') {
541: $var = $part['name'];
542: if ($noreqs) {
543: $reg = '(?P<' . $var . '>.*)' . $rest;
544: if (!array_key_exists($var, $this->defaults)) {
545: $allblank = false;
546: $noreqs = false;
547: }
548: } else {
549: if ($allblank && array_key_exists($var, $this->defaults)) {
550: $reg = '(?P<' . $var . '>.*)' . $rest;
551: } elseif (array_key_exists($var, $this->defaults)) {
552: $reg = '(?P<' . $var . '>.*)' . $rest;
553: } else {
554: $allblank = false;
555: $noreqs = false;
556: $reg = '(?P<' . $var . '>.*)' . $rest;
557: }
558: }
559: } elseif ($part && in_array(substr($part, -1), $this->_splitChars)) {
560: if ($allblank) {
561: $reg = preg_quote(substr($part, 0, -1)) . '(' . preg_quote(substr($part, -1)) . $rest . ')?';
562: } else {
563: $allblank = false;
564: $reg = preg_quote($part) . $rest;
565: }
566:
567:
568:
569: } else {
570: $noreqs = false;
571: $allblank = false;
572: $reg = preg_quote($part) . $rest;
573: }
574:
575: return array($reg, $noreqs, $allblank);
576: }
577:
578: 579: 580: 581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591: 592: 593:
594: public function match($url, $kargs = array())
595: {
596: $defaultKargs = array('environ' => array(),
597: 'subDomains' => false,
598: 'subDomainsIgnore' => array(),
599: 'domainMatch' => '');
600: $kargs = array_merge($defaultKargs, $kargs);
601:
602:
603: if ($this->static) {
604: return false;
605: }
606:
607: if (substr($url, -1) == '/' && strlen($url) > 1) {
608: $url = substr($url, 0, -1);
609: }
610:
611:
612: $match = preg_match('@' . str_replace('@', '\@', $this->regexp) . '@', $url, $matches);
613: if ($match == 0) {
614: return false;
615: }
616:
617: $host = isset($kargs['environ']['HTTP_HOST']) ? $kargs['environ']['HTTP_HOST'] : null;
618: if ($host !== null && !empty($kargs['subDomains'])) {
619: $host = substr($host, 0, strpos(':', $host));
620: $subMatch = '@^(.+?)\.' . $kargs['domainMatch'] . '$';
621: $subdomain = preg_replace($subMatch, '$1', $host);
622: if (!in_array($subdomain, $kargs['subDomainsIgnore']) && $host != $subdomain) {
623: $subDomain = $subdomain;
624: }
625: }
626:
627: if (!empty($this->conditions)) {
628: if (isset($this->conditions['method'])) {
629: if (empty($kargs['environ']['REQUEST_METHOD'])) { return false; }
630:
631: if (!in_array($kargs['environ']['REQUEST_METHOD'], $this->conditions['method'])) {
632: return false;
633: }
634: }
635:
636:
637: $use_sd = isset($this->conditions['subDomain']) ? $this->conditions['subDomain'] : null;
638: if (!empty($use_sd) && empty($subDomain)) {
639: return false;
640: }
641: if (is_array($use_sd) && !in_array($subDomain, $use_sd)) {
642: return false;
643: }
644: }
645: $matchDict = $matches;
646:
647:
648: foreach ($matchDict as $key => $val) {
649: if (is_int($key)) {
650: unset($matchDict[$key]);
651: }
652: }
653: $result = array();
654: $extras = Horde_Routes_Utils::arraySubtract(array_keys($this->defaults), array_keys($matchDict));
655:
656: foreach ($matchDict as $key => $val) {
657:
658: if ($key != 'path_info' && $this->encoding) {
659: $val = urldecode($val);
660: }
661:
662: if (empty($val) && array_key_exists($key, $this->defaults) && !empty($this->defaults[$key])) {
663: $result[$key] = $this->defaults[$key];
664: } else {
665: $result[$key] = $val;
666: }
667: }
668:
669: foreach ($extras as $key) {
670: $result[$key] = $this->defaults[$key];
671: }
672:
673:
674: if (!empty($kargs['subDomains'])) {
675: $result['subDomain'] = $subDomain;
676: }
677:
678:
679:
680: if (!empty($this->conditions) && array_key_exists('function', $this->conditions) &&
681: !call_user_func_array($this->conditions['function'], array($kargs['environ'], $result))) {
682: return false;
683: }
684:
685: return $result;
686: }
687:
688: 689: 690: 691: 692: 693:
694: public function generate($kargs)
695: {
696: $defaultKargs = array('_ignoreReqList' => false,
697: '_appendSlash' => false);
698: $kargs = array_merge($defaultKargs, $kargs);
699:
700: $_appendSlash = $kargs['_appendSlash'];
701: unset($kargs['_appendSlash']);
702:
703: $_ignoreReqList = $kargs['_ignoreReqList'];
704: unset($kargs['_ignoreReqList']);
705:
706:
707: if (!$_ignoreReqList) {
708: foreach ($this->reqs as $key => $v) {
709: $value = (isset($kargs[$key])) ? $kargs[$key] : null;
710:
711: if (!empty($value) && !preg_match($this->_reqRegs[$key], $value)) {
712: return null;
713: }
714: }
715: }
716:
717:
718:
719: $meth = (isset($kargs['method'])) ? $kargs['method'] : null;
720:
721: if ($meth) {
722: if ($this->conditions && isset($this->conditions['method']) &&
723: (!in_array(strtoupper($meth), $this->conditions['method']))) {
724:
725: return null;
726: }
727: unset($kargs['method']);
728: }
729:
730: $routeList = $this->_routeBackwards;
731: $urlList = array();
732: $gaps = false;
733: foreach ($routeList as $part) {
734: if (is_array($part) && $part['type'] == ':') {
735: $arg = $part['name'];
736:
737:
738: $hasArg = array_key_exists($arg, $kargs);
739: $hasDefault = array_key_exists($arg, $this->defaults);
740:
741:
742:
743:
744: if ($hasDefault && !$hasArg && !$gaps) {
745: continue;
746: }
747:
748:
749:
750: if (($hasDefault && $hasArg) && $kargs[$arg] == $this->defaults[$arg] && !$gaps) {
751: continue;
752: }
753:
754:
755:
756: if ($hasArg && $kargs[$arg] === null && $hasDefault && !$gaps) {
757: continue;
758:
759:
760: } elseif ($hasArg) {
761: $val = ($kargs[$arg] === null) ? 'null' : $kargs[$arg];
762: } elseif ($hasDefault && $this->defaults[$arg] != null) {
763: $val = $this->defaults[$arg];
764:
765:
766: } else {
767: return null;
768: }
769:
770: $urlList[] = Horde_Routes_Utils::urlQuote($val, $this->encoding);
771: if ($hasArg) {
772: unset($kargs[$arg]);
773: }
774: $gaps = true;
775: } elseif (is_array($part) && $part['type'] == '*') {
776: $arg = $part['name'];
777: $kar = (isset($kargs[$arg])) ? $kargs[$arg] : null;
778: if ($kar != null) {
779: $urlList[] = Horde_Routes_Utils::urlQuote($kar, $this->encoding);
780: $gaps = true;
781: }
782: } elseif (!empty($part) && in_array(substr($part, -1), $this->_splitChars)) {
783: if (!$gaps && in_array($part, $this->_splitChars)) {
784: continue;
785: } elseif (!$gaps) {
786: $gaps = true;
787: $urlList[] = substr($part, 0, -1);
788: } else {
789: $gaps = true;
790: $urlList[] = $part;
791: }
792: } else {
793: $gaps = true;
794: $urlList[] = $part;
795: }
796: }
797:
798: $urlList = array_reverse($urlList);
799: $url = implode('', $urlList);
800: if (substr($url, 0, 1) != '/') {
801: $url = '/' . $url;
802: }
803:
804: $extras = $kargs;
805: foreach ($this->maxKeys as $key) {
806: unset($extras[$key]);
807: }
808: $extras = array_keys($extras);
809:
810: if (!empty($extras)) {
811: if ($_appendSlash && substr($url, -1) != '/') {
812: $url .= '/';
813: }
814: $url .= '?';
815: $newExtras = array();
816: foreach ($kargs as $key => $value) {
817: if (in_array($key, $extras) && ($key != 'action' || $key != 'controller')) {
818: $newExtras[$key] = $value;
819: }
820: }
821: $url .= http_build_query($newExtras);
822: } elseif ($_appendSlash && substr($url, -1) != '/') {
823: $url .= '/';
824: }
825: return $url;
826: }
827:
828: }
829: