Overview

Packages

  • Horde
    • Routes
  • Routes

Classes

  • Horde_Routes_Exception
  • Horde_Routes_Mapper
  • Horde_Routes_Printer
  • Horde_Routes_Route
  • Horde_Routes_Utils
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Horde Routes package
  4:  *
  5:  * This package is heavily inspired by the Python "Routes" library
  6:  * by Ben Bangert (http://routes.groovie.org).  Routes is based
  7:  * largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
  8:  *
  9:  * @author  Maintainable Software, LLC. (http://www.maintainable.com)
 10:  * @author  Mike Naberezny <mike@maintainable.com>
 11:  * @license http://www.horde.org/licenses/bsd BSD
 12:  * @package Routes
 13:  */
 14: 
 15: /**
 16:  * The Route object holds a route recognition and generation routine.
 17:  * See __construct() docs for usage.
 18:  *
 19:  * @package Routes
 20:  */
 21: class Horde_Routes_Route
 22: {
 23:     /**
 24:      * The path for this route, such as ':controller/:action/:id'
 25:      * @var string
 26:      */
 27:     public $routePath;
 28: 
 29:     /**
 30:      * Encoding of this route (not yet supported)
 31:      * @var string
 32:      */
 33:     public $encoding = 'utf-8';
 34: 
 35:     /**
 36:      * What to do on decoding errors?  'ignore' or 'replace'
 37:      * @var string
 38:      */
 39:     public $decodeErrors = 'replace';
 40: 
 41:     /**
 42:      * Is this a static route?
 43:      * @var string
 44:      */
 45:     public $static;
 46: 
 47:     /**
 48:      * Filter function to operate on arguments before generation
 49:      * @var callback
 50:      */
 51:     public $filter;
 52: 
 53:     /**
 54:      * Is this an absolute path?  (Mapper will not prepend SCRIPT_NAME)
 55:      * @var boolean
 56:      */
 57:     public $absolute;
 58: 
 59:     /**
 60:      * Does this route use explicit mode (no implicit defaults)?
 61:      * @var boolean
 62:      */
 63:     public $explicit;
 64: 
 65:     /**
 66:      * Default keyword arguments for this route
 67:      * @var array
 68:      */
 69:     public $defaults = array();
 70: 
 71:     /**
 72:      * Array of keyword args for special conditions (method, subDomain, function)
 73:      * @var array
 74:      */
 75:     public $conditions;
 76: 
 77:     /**
 78:      * Maximum keys that this route could utilize.
 79:      * @var array
 80:      */
 81:     public $maxKeys;
 82: 
 83:     /**
 84:      * Minimum keys required to generate this route
 85:      * @var array
 86:      */
 87:     public $minKeys;
 88: 
 89:     /**
 90:      * Default keywords that don't exist in the path; can't be changed by an incomng URL.
 91:      * @var array
 92:      */
 93:     public $hardCoded;
 94: 
 95:     /**
 96:      * Requirements for this route
 97:      * @var array
 98:      */
 99:     public $reqs;
100: 
101:     /**
102:      * Regular expression for matching this route
103:      * @var string
104:      */
105:     public $regexp;
106: 
107:     /**
108:      * Route path split by '/'
109:      * @var array
110:      */
111:     protected $_routeList;
112: 
113:     /**
114:      * Reverse of $routeList
115:      * @var array
116:      */
117:     protected $_routeBackwards;
118: 
119:     /**
120:      * Characters that split the parts of a URL
121:      * @var array
122:      */
123:     protected $_splitChars;
124: 
125:     /**
126:      * Last path part used by buildNextReg()
127:      * @var string
128:      */
129:     protected $_prior;
130: 
131:     /**
132:      * Requirements formatted as regexps suitable for preg_match()
133:      * @var array
134:      */
135:     protected $_reqRegs;
136: 
137:     /**
138:      * Member name if this is a RESTful route
139:      * @see resource()
140:      * @var null|string
141:      */
142:     protected $_memberName;
143: 
144:     /**
145:      * Collection name if this is a RESTful route
146:      * @see resource()
147:      * @var null|string
148:      */
149:     protected $_collectionName;
150: 
151:     /**
152:      * Name of the parent resource, if this is a RESTful route & has a parent
153:      * @see resource
154:      * @var string
155:      */
156:     protected $_parentResource;
157: 
158: 
159:     /**
160:      *  Initialize a route, with a given routepath for matching/generation
161:      *
162:      *  The set of keyword args will be used as defaults.
163:      *
164:      *  Usage:
165:      *      $route = new Horde_Routes_Route(':controller/:action/:id');
166:      *
167:      *      $route = new Horde_Routes_Route('date/:year/:month/:day',
168:      *                      array('controller'=>'blog', 'action'=>'view'));
169:      *
170:      *      $route = new Horde_Routes_Route('archives/:page',
171:      *                      array('controller'=>'blog', 'action'=>'by_page',
172:      *                            'requirements' => array('page'=>'\d{1,2}'));
173:      *
174:      *  Note:
175:      *      Route is generally not called directly, a Mapper instance connect()
176:      *      method should be used to add routes.
177:      */
178:     public function __construct($routePath, $kargs = array())
179:     {
180:         $this->routePath = $routePath;
181: 
182:         // Don't bother forming stuff we don't need if its a static route
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:         // Pull out the member/collection name if present, this applies only to
192:         // map.resource
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:         // Pull out route conditions
203:         $this->conditions = isset($kargs['conditions']) ? $kargs['conditions'] : null;
204:         unset($kargs['conditions']);
205: 
206:         // Determine if explicit behavior should be used
207:         $this->explicit = isset($kargs['_explicit']) ? $kargs['_explicit'] : false;
208:         unset($kargs['_explicit']);
209: 
210:         // Reserved keys that don't count
211:         $reservedKeys = array('requirements');
212: 
213:         // Name has been changed from the Python version
214:         // This is a list of characters natural splitters in a URL
215:         $this->_splitChars = array('/', ',', ';', '.', '#');
216: 
217:         // trim preceding '/' if present
218:         if (substr($this->routePath, 0, 1) == '/') {
219:             $routePath = substr($this->routePath, 1);
220:         }
221: 
222:         // Build our routelist, and the keys used in the route
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:         // Build a req list with all the regexp requirements for our args
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:         // Update our defaults and set new default keys if needed. defaults
237:         // needs to be saved
238:         list($this->defaults, $defaultKeys) = $this->_defaults($routeKeys, $reservedKeys, $kargs);
239: 
240:         // Save the maximum keys we could utilize
241:         $this->maxKeys = array_keys(array_flip(array_merge($defaultKeys, $routeKeys)));
242:         list($this->minKeys, $this->_routeBackwards) = $this->_minKeys($this->_routeList);
243: 
244:         // Populate our hardcoded keys, these are ones that are set and don't
245:         // exist in the route
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:      * Utility method to walk the route, and pull out the valid
256:      * dynamic/wildcard keys
257:      *
258:      * @param  string  $routePath  Route path
259:      * @return array               Route list
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:                     // Basically appends '-' to _splitChars
286:                     // Helps it fall in line with the Python idioms.
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:      * Utility function to walk the route backwards
312:      *
313:      * Will determine the minimum keys we must have to generate a
314:      * working route.
315:      *
316:      * @param  array  $routeList  Route path split by '/'
317:      * @return array              [minimum keys for route, route list reversed]
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:      * Creates a default array of strings
342:      *
343:      * Puts together the array of defaults, turns non-null values to strings,
344:      * and add in our action/id default if they use and do not specify it
345:      *
346:      * Precondition: $this->_defaultKeys is an array of the currently assumed default keys
347:      *
348:      * @param  array  $routekeys     All the keys found in the route path
349:      * @param  array  $reservedKeys  Array of keys not in the route path
350:      * @param  array  $kargs         Keyword args passed to the Route constructor
351:      * @return array                 [defaults, new default keys]
352:      */
353:     protected function _defaults($routeKeys, $reservedKeys, $kargs)
354:     {
355:         $defaults = array();
356: 
357:         // Add in a controller/action default if they don't exist
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:      * Create the regular expression for matching.
408:      *
409:      * Note: This MUST be called before match can function properly.
410:      *
411:      * clist should be a list of valid controller strings that can be
412:      * matched, for this reason makeregexp should be called by the web
413:      * framework after it knows all available controllers that can be
414:      * utilized.
415:      *
416:      * @param  array  $clist  List of all possible controllers
417:      * @return void
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:      * Recursively build a regexp given a path, and a controller list.
437:      *
438:      * Returns the regular expression string, and two booleans that can be
439:      * ignored as they're only used internally by buildnextreg.
440:      *
441:      * @param  array  $path   The RouteList for the path
442:      * @param  array  $clist  List of all possible controllers
443:      * @return array          [array, boolean, boolean]
444:      */
445:     public function buildNextReg($path, $clist)
446:     {
447:         if (!empty($path)) {
448:             $part = $path[0];
449:         } else {
450:             $part = '';
451:         }
452: 
453:         // noreqs will remember whether the remainder has either a string
454:         // match, or a non-defaulted regexp match on a key, allblank remembers
455:         // if the rest could possible be completely empty
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:             // First we plug in the proper part matcher
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:             // Now we determine if its optional, or required. This changes
491:             // depending on what is in the rest of the match. If noreqs is
492:             // true, then its possible the entire thing is optional as there's
493:             // no reqs or string matches.
494:             if ($noreqs) {
495:                 // The rest is optional, but now we have an optional with a
496:                 // regexp. Wrap to ensure that if we match anything, we match
497:                 // our regexp first. It's still possible we could be completely
498:                 // blank as we have a default
499:                 if (array_key_exists($var, $this->reqs) && array_key_exists($var, $this->defaults)) {
500:                     $reg = '(' . $partreg . $rest . ')?';
501: 
502:                 // Or we have a regexp match with no default, so now being
503:                 // completely blank form here on out isn't possible
504:                 } elseif (array_key_exists($var, $this->reqs)) {
505:                     $allblank = false;
506:                     $reg = $partreg . $rest;
507: 
508:                 // If the character before this is a special char, it has to be
509:                 // followed by this
510:                 } elseif (array_key_exists($var, $this->defaults) && in_array($this->_prior, array(',', ';', '.'))) {
511:                     $reg = $partreg . $rest;
512: 
513:                 // Or we have a default with no regexp, don't touch the allblank
514:                 } elseif (array_key_exists($var, $this->defaults)) {
515:                     $reg = $partreg . '?' . $rest;
516: 
517:                 // Or we have a key with no default, and no reqs. Not possible
518:                 // to be all blank from here
519:                 } else {
520:                     $allblank = false;
521:                     $reg = $partreg . $rest;
522:                 }
523: 
524:             // In this case, we have something dangling that might need to be
525:             // matched
526:             } else {
527:                 // If they can all be blank, and we have a default here, we know
528:                 // its safe to make everything from here optional. Since
529:                 // something else in the chain does have req's though, we have
530:                 // to make the partreg here required to continue matching
531:                 if ($allblank && array_key_exists($var, $this->defaults)) {
532:                     $reg = '(' . $partreg . $rest . ')?';
533: 
534:                 // Same as before, but they can't all be blank, so we have to
535:                 // require it all to ensure our matches line up right
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:         // We have a normal string here, this is a req, and it prevents us from
568:         // being all blank
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:      * Match a url to our regexp.
580:      *
581:      * While the regexp might match, this operation isn't
582:      * guaranteed as there's other factors that can cause a match to fail
583:      * even though the regexp succeeds (Default that was relied on wasn't
584:      * given, requirement regexp doesn't pass, etc.).
585:      *
586:      * Therefore the calling function shouldn't assume this will return a
587:      * valid dict, the other possible return is False if a match doesn't work
588:      * out.
589:      *
590:      * @param  string  $url  URL to match
591:      * @param  array         Keyword arguments
592:      * @return null|array    Array of match data if matched, Null otherwise
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:         // Static routes don't match, they generate only
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:         // Match the regexps we generated
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:             // Check sub-domains?
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:         // Clear out int keys as PHP gives us both the named subgroups and numbered subgroups
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:             // TODO: character set decoding
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:         // Add the sub-domain if there is one
674:         if (!empty($kargs['subDomains'])) {
675:             $result['subDomain'] = $subDomain;
676:         }
677: 
678:         // If there's a function, call it with environ and expire if it
679:         // returns False
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:      * Generate a URL from ourself given a set of keyword arguments
690:      *
691:      * @param  array  $kargs   Keyword arguments
692:      * @param  null|string     Null if generation failed, URL otherwise
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:         // Verify that our args pass any regexp requirements
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:         // Verify that if we have a method arg, it's in the method accept list.
718:         // Also, method will be changed to _method for route generation.
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:                 // For efficiency, check these just once
738:                 $hasArg = array_key_exists($arg, $kargs);
739:                 $hasDefault = array_key_exists($arg, $this->defaults);
740: 
741:                 // Determine if we can leave this part off
742:                 // First check if the default exists and wasn't provided in the
743:                 // call (also no gaps)
744:                 if ($hasDefault && !$hasArg && !$gaps) {
745:                     continue;
746:                 }
747: 
748:                 // Now check to see if there's a default and it matches the
749:                 // incoming call arg
750:                 if (($hasDefault && $hasArg) && $kargs[$arg] == $this->defaults[$arg] && !$gaps) {
751:                     continue;
752:                 }
753: 
754:                 // We need to pull the value to append, if the arg is NULL and
755:                 // we have a default, use that
756:                 if ($hasArg && $kargs[$arg] === null && $hasDefault && !$gaps) {
757:                     continue;
758: 
759:                 // Otherwise if we do have an arg, use that
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:                 // No arg at all? This won't work
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: 
API documentation generated by ApiGen