Files
server/usr/share/psa-pear/pear/php/Horde/Routes/Utils.php
2026-01-07 20:52:11 +01:00

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;
}
}