445 lines
16 KiB
PHP
445 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* Horde Routes package
|
|
*
|
|
* This package is heavily inspired by the Python "Routes" library
|
|
* by Ben Bangert (http://routes.groovie.org). Routes is based
|
|
* largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
|
|
*
|
|
* @author Maintainable Software, LLC. (http://www.maintainable.com)
|
|
* @author Mike Naberezny <mike@maintainable.com>
|
|
* @license http://www.horde.org/licenses/bsd BSD
|
|
* @package Routes
|
|
*/
|
|
|
|
/**
|
|
* Utility functions for use in templates and controllers
|
|
*
|
|
* @package Routes
|
|
*/
|
|
class Horde_Routes_Utils
|
|
{
|
|
/**
|
|
* @var Horde_Routes_Mapper
|
|
*/
|
|
public $mapper;
|
|
|
|
/**
|
|
* Match data from last match; implements for urlFor() route memory
|
|
* @var array
|
|
*/
|
|
public $mapperDict = array();
|
|
|
|
/**
|
|
* Callback function used for redirectTo()
|
|
* @var callback
|
|
*/
|
|
public $redirect;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param Horde_Routes_Mapper $mapper Mapper for these utilities
|
|
* @param callback $redirect Redirect callback for redirectTo()
|
|
*/
|
|
public function __construct(Horde_Routes_Mapper $mapper, $redirect = null)
|
|
{
|
|
$this->mapper = $mapper;
|
|
$this->redirect = $redirect;
|
|
}
|
|
|
|
/**
|
|
* Generates a URL.
|
|
*
|
|
* All keys given to urlFor are sent to the Routes Mapper instance for
|
|
* generation except for::
|
|
*
|
|
* anchor specified the anchor name to be appened to the path
|
|
* host overrides the default (current) host if provided
|
|
* protocol overrides the default (current) protocol if provided
|
|
* qualified creates the URL with the host/port information as
|
|
* needed
|
|
*
|
|
* The URL is generated based on the rest of the keys. When generating a new
|
|
* URL, values will be used from the current request's parameters (if
|
|
* present). The following rules are used to determine when and how to keep
|
|
* the current requests parameters:
|
|
*
|
|
* * If the controller is present and begins with '/', no defaults are used
|
|
* * If the controller is changed, action is set to 'index' unless otherwise
|
|
* specified
|
|
*
|
|
* For example, if the current request yielded a dict (associative array) of
|
|
* array('controller'=>'blog', 'action'=>'view', 'id'=>2), with the standard
|
|
* ':controller/:action/:id' route, you'd get the following results::
|
|
*
|
|
* urlFor(array('id'=>4)) => '/blog/view/4',
|
|
* urlFor(array('controller'=>'/admin')) => '/admin',
|
|
* urlFor(array('controller'=>'admin')) => '/admin/view/2'
|
|
* urlFor(array('action'=>'edit')) => '/blog/edit/2',
|
|
* urlFor(array('action'=>'list', id=NULL)) => '/blog/list'
|
|
*
|
|
* **Static and Named Routes**
|
|
*
|
|
* If there is a string present as the first argument, a lookup is done
|
|
* against the named routes table to see if there's any matching routes. The
|
|
* keyword defaults used with static routes will be sent in as GET query
|
|
* arg's if a route matches.
|
|
*
|
|
* If no route by that name is found, the string is assumed to be a raw URL.
|
|
* Should the raw URL begin with ``/`` then appropriate SCRIPT_NAME data will
|
|
* be added if present, otherwise the string will be used as the url with
|
|
* keyword args becoming GET query args.
|
|
*/
|
|
public function urlFor($first = array(), $second = array())
|
|
{
|
|
if (is_array($first)) {
|
|
// urlFor(array('controller' => 'foo', ...))
|
|
$routeName = null;
|
|
$kargs = $first;
|
|
} else {
|
|
// urlFor('named_route')
|
|
// urlFor('named_route', array('id' => 3, ...))
|
|
// urlFor('static_path')
|
|
$routeName = (string)$first;
|
|
$kargs = $second;
|
|
}
|
|
|
|
$anchor = isset($kargs['anchor']) ? $kargs['anchor'] : null;
|
|
$host = isset($kargs['host']) ? $kargs['host'] : null;
|
|
$protocol = isset($kargs['protocol']) ? $kargs['protocol'] : null;
|
|
$qualified = isset($kargs['qualified']) ? $kargs['qualified'] : null;
|
|
unset($kargs['qualified']);
|
|
|
|
// Remove special words from kargs, convert placeholders
|
|
foreach (array('anchor', 'host', 'protocol') as $key) {
|
|
if (array_key_exists($key, $kargs)) {
|
|
unset($kargs[$key]);
|
|
}
|
|
if (array_key_exists($key . '_', $kargs)) {
|
|
$kargs[$key] = $kargs[$key . '_'];
|
|
unset($kargs[$key . '_']);
|
|
}
|
|
}
|
|
|
|
$route = null;
|
|
$routeArgs = array();
|
|
$static = false;
|
|
$encoding = $this->mapper->encoding;
|
|
$environ = $this->mapper->environ;
|
|
$url = '';
|
|
|
|
if (isset($routeName)) {
|
|
if (isset($kargs['format']) && isset($this->mapper->routeNames['formatted_' . $routeName])) {
|
|
$route = $this->mapper->routeNames['formatted_' . $routeName];
|
|
} elseif (isset($this->mapper->routeNames[$routeName])) {
|
|
$route = $this->mapper->routeNames[$routeName];
|
|
}
|
|
|
|
if ($route && array_key_exists('_static', $route->defaults)) {
|
|
$static = true;
|
|
$url = $route->routePath;
|
|
}
|
|
|
|
// No named route found, assume the argument is a relative path
|
|
if ($route === null) {
|
|
$static = true;
|
|
$url = $routeName;
|
|
}
|
|
|
|
if ((substr($url, 0, 1) == '/') &&
|
|
isset($environ['SCRIPT_NAME'])) {
|
|
$url = $environ['SCRIPT_NAME'] . $url;
|
|
}
|
|
|
|
if ($static) {
|
|
if (!empty($kargs)) {
|
|
$url .= '?';
|
|
$query_args = array();
|
|
foreach ($kargs as $key => $val) {
|
|
$query_args[] = urlencode(utf8_decode($key)) . '=' .
|
|
urlencode(utf8_decode($val));
|
|
}
|
|
$url .= implode('&', $query_args);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! $static) {
|
|
if ($route) {
|
|
$routeArgs = array($route);
|
|
$newargs = $route->defaults;
|
|
foreach ($kargs as $key => $value) {
|
|
$newargs[$key] = $value;
|
|
}
|
|
|
|
// If this route has a filter, apply it
|
|
if (!empty($route->filter)) {
|
|
$newargs = call_user_func($route->filter, $newargs);
|
|
}
|
|
|
|
$newargs = $this->_subdomainCheck($newargs);
|
|
} else {
|
|
$newargs = $this->_screenArgs($kargs);
|
|
}
|
|
|
|
$anchor = (isset($newargs['_anchor'])) ? $newargs['_anchor'] : $anchor;
|
|
unset($newargs['_anchor']);
|
|
|
|
$host = (isset($newargs['_host'])) ? $newargs['_host'] : $host;
|
|
unset($newargs['_host']);
|
|
|
|
$protocol = (isset($newargs['_protocol'])) ? $newargs['_protocol'] : $protocol;
|
|
unset($newargs['_protocol']);
|
|
|
|
$url = $this->mapper->generate($routeArgs, $newargs);
|
|
}
|
|
|
|
if (!empty($anchor)) {
|
|
$url .= '#' . self::urlQuote($anchor, $encoding);
|
|
}
|
|
|
|
if (!empty($host) || !empty($qualified) || !empty($protocol)) {
|
|
$http_host = isset($environ['HTTP_HOST']) ? $environ['HTTP_HOST'] : null;
|
|
$server_name = isset($environ['SERVER_NAME']) ? $environ['SERVER_NAME'] : null;
|
|
$fullhost = !is_null($http_host) ? $http_host : $server_name;
|
|
|
|
if (empty($host) && empty($qualified)) {
|
|
$host = explode(':', $fullhost);
|
|
$host = $host[0];
|
|
} else if (empty($host)) {
|
|
$host = $fullhost;
|
|
}
|
|
if (empty($protocol)) {
|
|
if (!empty($environ['HTTPS']) && $environ['HTTPS'] != 'off') {
|
|
$protocol = 'https';
|
|
} else {
|
|
$protocol = 'http';
|
|
}
|
|
}
|
|
if ($url !== null) {
|
|
$url = $protocol . '://' . $host . $url;
|
|
}
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Issues a redirect based on the arguments.
|
|
*
|
|
* Redirects *should* occur as a "302 Moved" header, however the web
|
|
* framework may utilize a different method.
|
|
*
|
|
* All arguments are passed to urlFor() to retrieve the appropriate URL, then
|
|
* the resulting URL it sent to the redirect function as the URL.
|
|
*
|
|
* @param mixed $first First argument in varargs, same as urlFor()
|
|
* @param mixed $second Second argument in varargs
|
|
* @return mixed Result of redirect callback
|
|
*/
|
|
public function redirectTo($first = array(), $second = array())
|
|
{
|
|
$target = $this->urlFor($first, $second);
|
|
return call_user_func($this->redirect, $target);
|
|
}
|
|
|
|
/**
|
|
* Pretty-print a listing of the routes connected to the mapper.
|
|
*
|
|
* @param stream|null $stream Output stream for printing (optional)
|
|
* @param string|null $eol Line ending (optional)
|
|
* @return void
|
|
*/
|
|
public function printRoutes($stream = null, $eol = PHP_EOL)
|
|
{
|
|
$printer = new Horde_Routes_Printer($this->mapper);
|
|
$printer->printRoutes($stream, $eol);
|
|
}
|
|
|
|
/**
|
|
* Scan a directory for PHP files and use them as controllers. Used
|
|
* as the default scanner callback for Horde_Routes_Mapper. See the
|
|
* constructor of that class for more information.
|
|
*
|
|
* Given a directory with:
|
|
* foo.php, bar.php, baz.php
|
|
* Returns an array:
|
|
* foo, bar, baz
|
|
*
|
|
* @param string $dirname Directory to scan for controller files
|
|
* @param string $prefix Prefix controller names (optional)
|
|
* @return array Array of controller names
|
|
*/
|
|
public static function controllerScan($dirname = null, $prefix = '')
|
|
{
|
|
$controllers = array();
|
|
|
|
if ($dirname === null) {
|
|
return $controllers;
|
|
}
|
|
|
|
$baseregexp = preg_quote($dirname . DIRECTORY_SEPARATOR, '/');
|
|
|
|
foreach (new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($dirname)) as $entry) {
|
|
if (!$entry->isFile()) {
|
|
continue;
|
|
}
|
|
// Match .php files that don't start with an underscore
|
|
if (preg_match('/^[^_]{1,1}.*\.php$/', basename($entry->getFilename())) == 0) {
|
|
continue;
|
|
}
|
|
// Strip off base path: dirname/admin/users.php -> admin/users.php
|
|
$controller = preg_replace("/^$baseregexp(.*)\.php/", '\\1', $entry->getPathname());
|
|
|
|
// PrepareController -> prepare_controller -> prepare
|
|
$controller = Horde_String::lower(
|
|
preg_replace('/([a-z])([A-Z])/',
|
|
"\${1}_\${2}", $controller));
|
|
if (preg_match('/_controller$/', $controller)) {
|
|
$controller = substr($controller, 0, -(strlen('_controller')));
|
|
}
|
|
|
|
// Normalize directory separators.
|
|
$controller = str_replace(DIRECTORY_SEPARATOR, '/', $controller);
|
|
|
|
// Add to controller list.
|
|
$controllers[] = $prefix . $controller;
|
|
}
|
|
|
|
usort($controllers, array('Horde_Routes_Utils', 'longestFirst'));
|
|
|
|
return $controllers;
|
|
}
|
|
|
|
/**
|
|
* Private function that takes a dict, and screens it against the current
|
|
* request dict to determine what the dict should look like that is used.
|
|
* This is responsible for the requests "memory" of the current.
|
|
*/
|
|
private function _screenArgs($kargs)
|
|
{
|
|
if ($this->mapper->explicit && $this->mapper->subDomains) {
|
|
return $this->_subdomainCheck($kargs);
|
|
} else if ($this->mapper->explicit) {
|
|
return $kargs;
|
|
}
|
|
|
|
$controllerName = (isset($kargs['controller'])) ? $kargs['controller'] : null;
|
|
|
|
if (!empty($controllerName) && substr($controllerName, 0, 1) == '/') {
|
|
// If the controller name starts with '/', ignore route memory
|
|
$kargs['controller'] = substr($kargs['controller'], 1);
|
|
return $kargs;
|
|
} else if (!empty($controllerName) && !array_key_exists('action', $kargs)) {
|
|
// Fill in an action if we don't have one, but have a controller
|
|
$kargs['action'] = 'index';
|
|
}
|
|
|
|
$memoryKargs = $this->mapperDict;
|
|
|
|
// Remove keys from memory and kargs if kargs has them as null
|
|
foreach ($kargs as $key => $value) {
|
|
if ($value === null) {
|
|
unset($kargs[$key]);
|
|
if (array_key_exists($key, $memoryKargs)) {
|
|
unset($memoryKargs[$key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge the new args on top of the memory args
|
|
foreach ($kargs as $key => $value) {
|
|
$memoryKargs[$key] = $value;
|
|
}
|
|
|
|
// Setup a sub-domain if applicable
|
|
if (!empty($this->mapper->subDomains)) {
|
|
$memoryKargs = $this->_subdomainCheck($memoryKargs);
|
|
}
|
|
|
|
return $memoryKargs;
|
|
}
|
|
|
|
/**
|
|
* Screen the kargs for a subdomain and alter it appropriately depending
|
|
* on the current subdomain or lack therof.
|
|
*/
|
|
private function _subdomainCheck($kargs)
|
|
{
|
|
if ($this->mapper->subDomains) {
|
|
$subdomain = (isset($kargs['subDomain'])) ? $kargs['subDomain'] : null;
|
|
unset($kargs['subDomain']);
|
|
|
|
$environ = $this->mapper->environ;
|
|
$http_host = isset($environ['HTTP_HOST']) ? $environ['HTTP_HOST'] : null;
|
|
$server_name = isset($environ['SERVER_NAME']) ? $environ['SERVER_NAME'] : null;
|
|
$fullhost = !is_null($http_host) ? $http_host : $server_name;
|
|
|
|
$hostmatch = explode(':', $fullhost);
|
|
$host = $hostmatch[0];
|
|
$port = '';
|
|
if (count($hostmatch) > 1) {
|
|
$port .= ':' . $hostmatch[1];
|
|
}
|
|
|
|
$subMatch = '^.+?\.(' . $this->mapper->domainMatch . ')$';
|
|
$domain = preg_replace("@$subMatch@", '$1', $host);
|
|
|
|
if ($subdomain && (substr($host, 0, strlen($subdomain)) != $subdomain)
|
|
&& (! in_array($subdomain, $this->mapper->subDomainsIgnore))) {
|
|
$kargs['_host'] = $subdomain . '.' . $domain . $port;
|
|
} else if (($subdomain === null || in_array($subdomain, $this->mapper->subDomainsIgnore))
|
|
&& $domain != $host) {
|
|
$kargs['_host'] = $domain . $port;
|
|
}
|
|
return $kargs;
|
|
} else {
|
|
return $kargs;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Quote a string containing a URL in a given encoding.
|
|
*
|
|
* @todo This is a placeholder. Multiple encodings aren't yet supported.
|
|
*
|
|
* @param string $url URL to encode
|
|
* @param string $encoding Encoding to use
|
|
*/
|
|
public static function urlQuote($url, $encoding = null)
|
|
{
|
|
if ($encoding === null) {
|
|
return str_replace('%2F', '/', urlencode($url));
|
|
} else {
|
|
return str_replace('%2F', '/', urlencode(utf8_decode($url)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback used by usort() in controllerScan() to sort controller
|
|
* names by the longest first.
|
|
*
|
|
* @param string $fst First string to compare
|
|
* @param string $lst Last string to compare
|
|
* @return integer Difference of string length (first - last)
|
|
*/
|
|
public static function longestFirst($fst, $lst)
|
|
{
|
|
return strlen($lst) - strlen($fst);
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public static function arraySubtract($a1, $a2)
|
|
{
|
|
foreach ($a2 as $key) {
|
|
if (in_array($key, $a1)) {
|
|
unset($a1[array_search($key, $a1)]);
|
|
}
|
|
}
|
|
return $a1;
|
|
}
|
|
}
|