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 */ 21class 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(Horde_String::upper($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