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:  * Utility functions for use in templates and controllers
 17:  *
 18:  * @package Routes
 19:  */
 20: class Horde_Routes_Utils
 21: {
 22:     /**
 23:      * @var Horde_Routes_Mapper
 24:      */
 25:     public $mapper;
 26: 
 27:     /**
 28:      * Match data from last match; implements for urlFor() route memory
 29:      * @var array
 30:      */
 31:     public $mapperDict = array();
 32: 
 33:     /**
 34:      * Callback function used for redirectTo()
 35:      * @var callback
 36:      */
 37:     public $redirect;
 38: 
 39:     /**
 40:      * Constructor
 41:      *
 42:      * @param  Horde_Routes_Mapper  $mapper    Mapper for these utilities
 43:      * @param  callback             $redirect  Redirect callback for redirectTo()
 44:      */
 45:     public function __construct(Horde_Routes_Mapper $mapper, $redirect = null)
 46:     {
 47:         $this->mapper   = $mapper;
 48:         $this->redirect = $redirect;
 49:     }
 50: 
 51:     /**
 52:      * Generates a URL.
 53:      *
 54:      * All keys given to urlFor are sent to the Routes Mapper instance for
 55:      * generation except for::
 56:      *
 57:      *     anchor          specified the anchor name to be appened to the path
 58:      *     host            overrides the default (current) host if provided
 59:      *     protocol        overrides the default (current) protocol if provided
 60:      *     qualified       creates the URL with the host/port information as
 61:      *                     needed
 62:      *
 63:      * The URL is generated based on the rest of the keys. When generating a new
 64:      * URL, values will be used from the current request's parameters (if
 65:      * present). The following rules are used to determine when and how to keep
 66:      * the current requests parameters:
 67:      *
 68:      * * If the controller is present and begins with '/', no defaults are used
 69:      * * If the controller is changed, action is set to 'index' unless otherwise
 70:      *   specified
 71:      *
 72:      * For example, if the current request yielded a dict (associative array) of
 73:      * array('controller'=>'blog', 'action'=>'view', 'id'=>2), with the standard
 74:      * ':controller/:action/:id' route, you'd get the following results::
 75:      *
 76:      *     urlFor(array('id'=>4))                    =>  '/blog/view/4',
 77:      *     urlFor(array('controller'=>'/admin'))     =>  '/admin',
 78:      *     urlFor(array('controller'=>'admin'))      =>  '/admin/view/2'
 79:      *     urlFor(array('action'=>'edit'))           =>  '/blog/edit/2',
 80:      *     urlFor(array('action'=>'list', id=NULL))  =>  '/blog/list'
 81:      *
 82:      * **Static and Named Routes**
 83:      *
 84:      * If there is a string present as the first argument, a lookup is done
 85:      * against the named routes table to see if there's any matching routes. The
 86:      * keyword defaults used with static routes will be sent in as GET query
 87:      * arg's if a route matches.
 88:      *
 89:      * If no route by that name is found, the string is assumed to be a raw URL.
 90:      * Should the raw URL begin with ``/`` then appropriate SCRIPT_NAME data will
 91:      * be added if present, otherwise the string will be used as the url with
 92:      * keyword args becoming GET query args.
 93:      */
 94:     public function urlFor($first = array(), $second = array())
 95:     {
 96:         if (is_array($first)) {
 97:             // urlFor(array('controller' => 'foo', ...))
 98:             $routeName = null;
 99:             $kargs = $first;
100:         } else {
101:             // urlFor('named_route')
102:             // urlFor('named_route', array('id' => 3, ...))
103:             // urlFor('static_path')
104:             $routeName = (string)$first;
105:             $kargs = $second;
106:         }
107: 
108:         $anchor    = isset($kargs['anchor'])    ? $kargs['anchor']    : null;
109:         $host      = isset($kargs['host'])      ? $kargs['host']      : null;
110:         $protocol  = isset($kargs['protocol'])  ? $kargs['protocol']  : null;
111:         $qualified = isset($kargs['qualified']) ? $kargs['qualified'] : null;
112:         unset($kargs['qualified']);
113: 
114:         // Remove special words from kargs, convert placeholders
115:         foreach (array('anchor', 'host', 'protocol') as $key) {
116:             if (array_key_exists($key, $kargs)) {
117:                 unset($kargs[$key]);
118:             }
119:             if (array_key_exists($key . '_', $kargs)) {
120:                 $kargs[$key] = $kargs[$key . '_'];
121:                 unset($kargs[$key . '_']);
122:             }
123:         }
124: 
125:         $route = null;
126:         $routeArgs = array();
127:         $static = false;
128:         $encoding = $this->mapper->encoding;
129:         $environ = $this->mapper->environ;
130:         $url = '';
131: 
132:         if (isset($routeName)) {
133:             if (isset($kargs['format']) && isset($this->mapper->routeNames['formatted_' . $routeName])) {
134:                 $route = $this->mapper->routeNames['formatted_' . $routeName];
135:             } elseif (isset($this->mapper->routeNames[$routeName])) {
136:                 $route = $this->mapper->routeNames[$routeName];
137:             }
138: 
139:             if ($route && array_key_exists('_static', $route->defaults)) {
140:                 $static = true;
141:                 $url = $route->routePath;
142:             }
143: 
144:             // No named route found, assume the argument is a relative path
145:             if ($route === null) {
146:                 $static = true;
147:                 $url = $routeName;
148:             }
149: 
150:             if ((substr($url, 0, 1) == '/') &&
151:                 isset($environ['SCRIPT_NAME'])) {
152:                 $url = $environ['SCRIPT_NAME'] . $url;
153:             }
154: 
155:             if ($static) {
156:                 if (!empty($kargs)) {
157:                     $url .= '?';
158:                     $query_args = array();
159:                     foreach ($kargs as $key => $val) {
160:                         $query_args[] = urlencode(utf8_decode($key)) . '=' .
161:                             urlencode(utf8_decode($val));
162:                     }
163:                     $url .= implode('&', $query_args);
164:                 }
165:             }
166:         }
167: 
168:         if (! $static) {
169:             if ($route) {
170:                 $routeArgs = array($route);
171:                 $newargs = $route->defaults;
172:                 foreach ($kargs as $key => $value) {
173:                     $newargs[$key] = $value;
174:                 }
175: 
176:                 // If this route has a filter, apply it
177:                 if (!empty($route->filter)) {
178:                     $newargs = call_user_func($route->filter, $newargs);
179:                 }
180: 
181:                 $newargs = $this->_subdomainCheck($newargs);
182:             } else {
183:                 $newargs = $this->_screenArgs($kargs);
184:             }
185: 
186:             $anchor = (isset($newargs['_anchor'])) ? $newargs['_anchor'] : $anchor;
187:             unset($newargs['_anchor']);
188: 
189:             $host = (isset($newargs['_host'])) ? $newargs['_host'] : $host;
190:             unset($newargs['_host']);
191: 
192:             $protocol = (isset($newargs['_protocol'])) ? $newargs['_protocol'] : $protocol;
193:             unset($newargs['_protocol']);
194: 
195:             $url = $this->mapper->generate($routeArgs, $newargs);
196:         }
197: 
198:         if (!empty($anchor)) {
199:             $url .= '#' . self::urlQuote($anchor, $encoding);
200:         }
201: 
202:         if (!empty($host) || !empty($qualified) || !empty($protocol)) {
203:             $http_host   = isset($environ['HTTP_HOST']) ? $environ['HTTP_HOST'] : null;
204:             $server_name = isset($environ['SERVER_NAME']) ? $environ['SERVER_NAME'] : null;
205:             $fullhost = !is_null($http_host) ? $http_host : $server_name;
206: 
207:             if (empty($host) && empty($qualified)) {
208:                 $host = explode(':', $fullhost);
209:                 $host = $host[0];
210:             } else if (empty($host)) {
211:                 $host = $fullhost;
212:             }
213:             if (empty($protocol)) {
214:                 if (!empty($environ['HTTPS']) && $environ['HTTPS'] != 'off') {
215:                     $protocol = 'https';
216:                 } else {
217:                     $protocol = 'http';
218:                 }
219:             }
220:             if ($url !== null) {
221:                 $url = $protocol . '://' . $host . $url;
222:             }
223:         }
224: 
225:         return $url;
226:     }
227: 
228:     /**
229:      * Issues a redirect based on the arguments.
230:      *
231:      * Redirects *should* occur as a "302 Moved" header, however the web
232:      * framework may utilize a different method.
233:      *
234:      * All arguments are passed to urlFor() to retrieve the appropriate URL, then
235:      * the resulting URL it sent to the redirect function as the URL.
236:      *
237:      * @param   mixed  $first   First argument in varargs, same as urlFor()
238:      * @param   mixed  $second  Second argument in varargs
239:      * @return  mixed           Result of redirect callback
240:      */
241:     public function redirectTo($first = array(), $second = array())
242:     {
243:         $target = $this->urlFor($first, $second);
244:         return call_user_func($this->redirect, $target);
245:     }
246: 
247:     /**
248:      * Pretty-print a listing of the routes connected to the mapper.
249:      *
250:      * @param  stream|null  $stream  Output stream for printing (optional)
251:      * @param  string|null  $eol     Line ending (optional)
252:      * @return void
253:      */
254:     public function printRoutes($stream = null, $eol = PHP_EOL)
255:     {
256:         $printer = new Horde_Routes_Printer($this->mapper);
257:         $printer->printRoutes($stream, $eol);
258:     }
259: 
260:     /**
261:      * Scan a directory for PHP files and use them as controllers.  Used
262:      * as the default scanner callback for Horde_Routes_Mapper.  See the
263:      * constructor of that class for more information.
264:      *
265:      * Given a directory with:
266:      *   foo.php, bar.php, baz.php
267:      * Returns an array:
268:      *   foo, bar, baz
269:      *
270:      * @param  string  $dirname  Directory to scan for controller files
271:      * @param  string  $prefix   Prefix controller names (optional)
272:      * @return array             Array of controller names
273:      */
274:     public static function controllerScan($dirname = null, $prefix = '')
275:     {
276:         $controllers = array();
277: 
278:         if ($dirname === null) {
279:             return $controllers;
280:         }
281: 
282:         $baseregexp = preg_quote($dirname . DIRECTORY_SEPARATOR, '/');
283: 
284:         foreach (new RecursiveIteratorIterator(
285:                  new RecursiveDirectoryIterator($dirname)) as $entry) {
286:             if (!$entry->isFile()) {
287:                 continue;
288:             }
289:             // Match .php files that don't start with an underscore
290:             if (preg_match('/^[^_]{1,1}.*\.php$/', basename($entry->getFilename())) == 0) {
291:                 continue;
292:             }
293:             // Strip off base path: dirname/admin/users.php -> admin/users.php
294:             $controller = preg_replace("/^$baseregexp(.*)\.php/", '\\1', $entry->getPathname());
295: 
296:             // PrepareController -> prepare_controller -> prepare
297:             $controller = Horde_String::lower(
298:                 preg_replace('/([a-z])([A-Z])/',
299:                              "\${1}_\${2}", $controller));
300:             if (preg_match('/_controller$/', $controller)) {
301:                 $controller = substr($controller, 0, -(strlen('_controller')));
302:             }
303: 
304:             // Normalize directory separators.
305:             $controller = str_replace(DIRECTORY_SEPARATOR, '/', $controller);
306: 
307:             // Add to controller list.
308:             $controllers[] = $prefix . $controller;
309:         }
310: 
311:         usort($controllers, array('Horde_Routes_Utils', 'longestFirst'));
312: 
313:         return $controllers;
314:     }
315: 
316:     /**
317:      * Private function that takes a dict, and screens it against the current
318:      * request dict to determine what the dict should look like that is used.
319:      * This is responsible for the requests "memory" of the current.
320:      */
321:     private function _screenArgs($kargs)
322:     {
323:         if ($this->mapper->explicit && $this->mapper->subDomains) {
324:             return $this->_subdomainCheck($kargs);
325:         } else if ($this->mapper->explicit) {
326:             return $kargs;
327:         }
328: 
329:         $controllerName = (isset($kargs['controller'])) ? $kargs['controller'] : null;
330: 
331:         if (!empty($controllerName) && substr($controllerName, 0, 1) == '/') {
332:             // If the controller name starts with '/', ignore route memory
333:             $kargs['controller'] = substr($kargs['controller'], 1);
334:             return $kargs;
335:         } else if (!empty($controllerName) && !array_key_exists('action', $kargs)) {
336:             // Fill in an action if we don't have one, but have a controller
337:             $kargs['action'] = 'index';
338:         }
339: 
340:         $memoryKargs = $this->mapperDict;
341: 
342:         // Remove keys from memory and kargs if kargs has them as null
343:         foreach ($kargs as $key => $value) {
344:              if ($value === null) {
345:                  unset($kargs[$key]);
346:                  if (array_key_exists($key, $memoryKargs)) {
347:                      unset($memoryKargs[$key]);
348:                  }
349:              }
350:         }
351: 
352:         // Merge the new args on top of the memory args
353:         foreach ($kargs as $key => $value) {
354:             $memoryKargs[$key] = $value;
355:         }
356: 
357:         // Setup a sub-domain if applicable
358:         if (!empty($this->mapper->subDomains)) {
359:             $memoryKargs = $this->_subdomainCheck($memoryKargs);
360:         }
361: 
362:         return $memoryKargs;
363:     }
364: 
365:     /**
366:      * Screen the kargs for a subdomain and alter it appropriately depending
367:      * on the current subdomain or lack therof.
368:      */
369:     private function _subdomainCheck($kargs)
370:     {
371:         if ($this->mapper->subDomains) {
372:             $subdomain = (isset($kargs['subDomain'])) ? $kargs['subDomain'] : null;
373:             unset($kargs['subDomain']);
374: 
375:             $environ = $this->mapper->environ;
376:             $http_host   = isset($environ['HTTP_HOST']) ? $environ['HTTP_HOST'] : null;
377:             $server_name = isset($environ['SERVER_NAME']) ? $environ['SERVER_NAME'] : null;
378:             $fullhost = !is_null($http_host) ? $http_host : $server_name;
379: 
380:             $hostmatch = explode(':', $fullhost);
381:             $host = $hostmatch[0];
382:             $port = '';
383:             if (count($hostmatch) > 1) {
384:                 $port .= ':' . $hostmatch[1];
385:             }
386: 
387:             $subMatch = '^.+?\.(' . $this->mapper->domainMatch . ')$';
388:             $domain = preg_replace("@$subMatch@", '$1', $host);
389: 
390:             if ($subdomain && (substr($host, 0, strlen($subdomain)) != $subdomain)
391:                     && (! in_array($subdomain, $this->mapper->subDomainsIgnore))) {
392:                 $kargs['_host'] = $subdomain . '.' . $domain . $port;
393:             } else if (($subdomain === null || in_array($subdomain, $this->mapper->subDomainsIgnore))
394:                     && $domain != $host) {
395:                 $kargs['_host'] = $domain . $port;
396:             }
397:             return $kargs;
398:         } else {
399:             return $kargs;
400:         }
401:     }
402: 
403:     /**
404:      * Quote a string containing a URL in a given encoding.
405:      *
406:      * @todo This is a placeholder.  Multiple encodings aren't yet supported.
407:      *
408:      * @param  string  $url       URL to encode
409:      * @param  string  $encoding  Encoding to use
410:      */
411:     public static function urlQuote($url, $encoding = null)
412:     {
413:         if ($encoding === null) {
414:             return str_replace('%2F', '/', urlencode($url));
415:         } else {
416:             return str_replace('%2F', '/', urlencode(utf8_decode($url)));
417:         }
418:     }
419: 
420:     /**
421:      * Callback used by usort() in controllerScan() to sort controller
422:      * names by the longest first.
423:      *
424:      * @param   string  $fst  First string to compare
425:      * @param   string  $lst  Last string to compare
426:      * @return  integer       Difference of string length (first - last)
427:      */
428:     public static function longestFirst($fst, $lst)
429:     {
430:         return strlen($lst) - strlen($fst);
431:     }
432: 
433:     /**
434:      */
435:     public static function arraySubtract($a1, $a2)
436:     {
437:         foreach ($a2 as $key) {
438:             if (in_array($key, $a1)) {
439:                 unset($a1[array_search($key, $a1)]);
440:             }
441:         }
442:         return $a1;
443:     }
444: }
445: 
API documentation generated by ApiGen