584 lines
19 KiB
PHP
584 lines
19 KiB
PHP
<?php
|
|
|
|
namespace EesyPHP;
|
|
|
|
use Exception;
|
|
|
|
class Url {
|
|
|
|
/**
|
|
* Current request
|
|
* @var UrlRequest|null
|
|
*/
|
|
public static $request = null;
|
|
|
|
/**
|
|
* Configured URL patterns :
|
|
*
|
|
* Example :
|
|
*
|
|
* array (
|
|
* '|get/(?P<name>[a-zA-Z0-9]+)$|' => array (
|
|
* 'handler' => 'get',
|
|
* 'additional_info' => array(),
|
|
* 'authenticated' => true,
|
|
* 'api_mode' => false,
|
|
* 'methods' => array('GET'),
|
|
* ),
|
|
* '|get/all$|' => => array (
|
|
* 'handler' => 'get_all',
|
|
* 'additional_info' => array(),
|
|
* 'authenticated' => true,
|
|
* 'api_mode' => false,
|
|
* 'methods' => array('GET', 'POST'),
|
|
* ),
|
|
* )
|
|
* @var array
|
|
*/
|
|
protected static $patterns = array();
|
|
|
|
/**
|
|
* Error 404 handler
|
|
* @var callable
|
|
*/
|
|
protected static $error_404_handler = array('\\EesyPHP\\Url', 'error_404');
|
|
|
|
/**
|
|
* Public root URL
|
|
* @var string|null
|
|
*/
|
|
protected static $public_root_url = null;
|
|
|
|
/**
|
|
* Enable/disable API mode
|
|
* @see self :: initialization()
|
|
* @var bool
|
|
*/
|
|
protected static $_api_mode;
|
|
|
|
/**
|
|
* Initialization
|
|
* @param string|null $public_root_url The application public root URL
|
|
* (optional, default: from public_root_url config key)
|
|
* @param bool $api_mode Enable/disable API mode
|
|
* @return void
|
|
*/
|
|
public static function init($public_root_url=null, $api_mode=false) {
|
|
if (php_sapi_name() == 'cli-server' && getenv('EESYPHP_SERVE_URL'))
|
|
$public_root_url = getenv('EESYPHP_SERVE_URL');
|
|
else if (is_null($public_root_url))
|
|
$public_root_url = App::get('public_root_url', null, 'string');
|
|
if (is_string($public_root_url) && $public_root_url) {
|
|
// Remove trailing slash
|
|
$public_root_url = rtrim($public_root_url, '/');
|
|
self :: $public_root_url = $public_root_url?$public_root_url:null;
|
|
}
|
|
self :: $_api_mode = boolval($api_mode);
|
|
}
|
|
|
|
/**
|
|
* Add an URL pattern
|
|
*
|
|
* @param string|array|null $pattern The URL pattern, an array of patterns or null to
|
|
* match to the root of the application
|
|
* @param callable $handler The URL pattern handler (must be callable, required)
|
|
* @param array|null $additional_info Array of information to pass to the URL handler
|
|
* @param boolean $authenticated Permit to define if this URL is accessible only for
|
|
* authenticated users (optional, default: true if the
|
|
* EesyPHP Authentication feature is enabled, false
|
|
* otherwise)
|
|
* @param boolean $overwrite Allow overwrite if a command already exists with the
|
|
* same name (optional, default: false)
|
|
* @param boolean $api_mode Enable API mode (optional, default: false)
|
|
* @param array|string|null $http_methods HTTP method (optional, default: array('GET', 'POST'))
|
|
* @return bool
|
|
**/
|
|
public static function add_url_handler($pattern, $handler=null, $additional_info=null,
|
|
$authenticated=null, $overwrite=true, $api_mode=false,
|
|
$http_methods=null) {
|
|
// Check HTTP methods parameter
|
|
if (is_null($http_methods))
|
|
$http_methods = array('GET', 'POST');
|
|
elseif (!is_array($http_methods))
|
|
$http_methods = array($http_methods);
|
|
|
|
// If multiple patterns specify using an array, add each of them
|
|
if (is_array($pattern)) {
|
|
$error = false;
|
|
if (is_null($handler))
|
|
foreach($pattern as $p => $h)
|
|
if (
|
|
!self :: add_url_handler(
|
|
$p, $h, $additional_info, $authenticated, $overwrite, $api_mode, $http_methods)
|
|
) $error = true;
|
|
else
|
|
foreach($pattern as $p)
|
|
if (
|
|
!self :: add_url_handler(
|
|
$p, $handler, $additional_info, $authenticated, $overwrite, $api_mode, $http_methods)
|
|
) $error = true;
|
|
return !$error;
|
|
}
|
|
else if (is_null($pattern))
|
|
$pattern = '#^$#';
|
|
|
|
$error = false;
|
|
foreach($http_methods as $http_method) {
|
|
if (!isset(self :: $patterns[$http_method]))
|
|
self :: $patterns[$http_method] = array();
|
|
// Check overwrite
|
|
if (isset(self :: $patterns[$http_method][$pattern])) {
|
|
if (!$overwrite) {
|
|
Log :: error(
|
|
"URL : pattern '%s' already defined for HTTP method %s: do not overwrite.".
|
|
$pattern, $http_method);
|
|
$error = true;
|
|
continue;
|
|
}
|
|
Log :: debug(
|
|
"URL : overwrite pattern '%s' for HTTP method %s with handler '%s' ".
|
|
"(old handler = '%s')",
|
|
$pattern, $http_method, format_callable($handler),
|
|
vardump(self :: $patterns[$http_method][$pattern])
|
|
);
|
|
}
|
|
|
|
// Register URL pattern info
|
|
self :: $patterns[$http_method][$pattern] = array(
|
|
'handler' => $handler,
|
|
'additional_info' => (
|
|
is_array($additional_info)?
|
|
$additional_info:array()
|
|
),
|
|
'authenticated' => $authenticated,
|
|
'api_mode' => $api_mode,
|
|
);
|
|
Log :: trace(
|
|
"URL: Register pattern '%s' on HTTP %s with :\n%s",
|
|
$pattern, $http_method, vardump(self :: $patterns[$http_method][$pattern])
|
|
);
|
|
}
|
|
|
|
return !$error;
|
|
}
|
|
|
|
/**
|
|
* Show error page
|
|
*
|
|
* @param $request UrlRequest|null The request (optional, default: null)
|
|
* @param $error_code int|null The HTTP error code (optional, default: 400)
|
|
*
|
|
* @return void
|
|
**/
|
|
public static function error_page($request=null, $error_code=null) {
|
|
$http_errors = array(
|
|
400 => array(
|
|
'pagetitle' => I18n::_("Bad request"),
|
|
'message' => I18n::_("Invalid request."),
|
|
),
|
|
401 => array(
|
|
'pagetitle' => I18n::_("Authentication required"),
|
|
'message' => I18n::_("You have to be authenticated to access to this page."),
|
|
),
|
|
403 => array(
|
|
'pagetitle' => I18n::_("Access denied"),
|
|
'message' => I18n::_("You do not have access to this application. If you think this is an error, please contact support."),
|
|
),
|
|
404 => array(
|
|
'pagetitle' => I18n::_("Whoops ! Page not found"),
|
|
'message' => I18n::_("The requested page can not be found."),
|
|
),
|
|
);
|
|
$error_code = ($error_code?intval($error_code):400);
|
|
if (array_key_exists($error_code, $http_errors))
|
|
$error = $http_errors[intval($error_code)];
|
|
else
|
|
$error = array(
|
|
'pagetitle' => I18n::_('Error'),
|
|
'message' => I18n::_('An unknown error occurred. If problem persist, please contact support.'),
|
|
);
|
|
http_response_code($error_code);
|
|
|
|
if (Tpl :: initialized()) {
|
|
Tpl :: assign('message', $error['message']);
|
|
Tpl :: display('error_page.tpl', $error['pagetitle']);
|
|
exit();
|
|
}
|
|
die($error['message']);
|
|
}
|
|
|
|
/**
|
|
* Error 404 handler
|
|
*
|
|
* @param UrlRequest|null $request The request (optional, default: null)
|
|
*
|
|
* @return void
|
|
**/
|
|
public static function error_404($request=null) {
|
|
self :: error_page($request, 404);
|
|
}
|
|
|
|
/**
|
|
* Set unknown URL handler
|
|
* @param callable|null $handler
|
|
* @return bool
|
|
*/
|
|
public static function set_unknown_url_handler($handler=null) {
|
|
// @phpstan-ignore-next-line
|
|
if (is_callable($handler) || is_null($handler)) {
|
|
self :: $error_404_handler = $handler;
|
|
return true;
|
|
}
|
|
// @phpstan-ignore-next-line
|
|
Log :: warning(
|
|
'Url::set_unknown_url_handler(): Invalid URL handler provided: %s',
|
|
vardump($handler));
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Trigger a 404 HTTP error
|
|
* @param UrlRequest|null $request Current UrlRequest object (optional, default: null)
|
|
* @return void
|
|
*/
|
|
public static function trigger_error_404($request=null) {
|
|
call_user_func_array(self :: $error_404_handler, array($request));
|
|
}
|
|
|
|
|
|
/**
|
|
* Interprets the requested URL and return the corresponding UrlRequest object
|
|
*
|
|
* @param $default_url string|null The default URL if current one does not
|
|
* match with any configured pattern.
|
|
*
|
|
* @return UrlRequest The UrlRequest object corresponding to the the requested URL.
|
|
*/
|
|
protected static function get_request($default_url=null) {
|
|
$current_url = self :: get_current_url();
|
|
if ($current_url === false) {
|
|
Log :: fatal(
|
|
I18n::_('Unable to determine the requested page. '.
|
|
'If the problem persists, please contact support.')
|
|
);
|
|
exit();
|
|
}
|
|
if (!is_array(self :: $patterns)) {
|
|
Log :: fatal('URL : No URL patterns configured !');
|
|
exit();
|
|
}
|
|
|
|
Log :: debug("URL : current url = '$current_url'");
|
|
if (array_key_exists($_SERVER['REQUEST_METHOD'], self :: $patterns)) {
|
|
Log :: trace(
|
|
"URL : check current url with the following URL patterns :\n - ".
|
|
implode("\n - ", array_keys(self :: $patterns))
|
|
);
|
|
foreach (self :: $patterns[$_SERVER['REQUEST_METHOD']] as $pattern => $handler_infos) {
|
|
$m = self :: url_match($pattern, $current_url);
|
|
if (is_array($m)) {
|
|
$request = new UrlRequest($current_url, $handler_infos, $m);
|
|
// Reset last redirect
|
|
if (isset($_SESSION['last_redirect']))
|
|
unset($_SESSION['last_redirect']);
|
|
Log :: trace("URL : result :\n".vardump($request));
|
|
return $request;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
Log :: debug(
|
|
'URL: no URL pattern registered for %s HTTP method',
|
|
$_SERVER['REQUEST_METHOD']);
|
|
}
|
|
if ($default_url !== false) {
|
|
Log :: debug("Current url match with no pattern. Redirect to default url ('$default_url')");
|
|
self :: redirect($default_url);
|
|
exit();
|
|
}
|
|
// Error 404
|
|
$api_mode = self :: $_api_mode || (strpos($current_url, 'api/') === 0);
|
|
Log :: debug(
|
|
"Current URL match with no pattern. Use error 404 handler (API mode=$api_mode).");
|
|
return new UrlRequest(
|
|
$current_url,
|
|
array(
|
|
'handler' => self :: $error_404_handler,
|
|
'authenticated' => false,
|
|
'api_mode' => $api_mode,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the current requested URL match with a specific pattern
|
|
*
|
|
* @param string $pattern The URL pattern
|
|
* @param string|false $current_url The current URL (optional)
|
|
*
|
|
* @return array|false The URL info if pattern matched, false otherwise.
|
|
**/
|
|
public static function url_match($pattern, $current_url=false) {
|
|
if ($current_url === false) {
|
|
$current_url = self :: get_current_url();
|
|
if (!$current_url) return False;
|
|
}
|
|
if (preg_match($pattern, $current_url, $m)) {
|
|
Log :: debug(
|
|
"URL : Match found with pattern '$pattern' :\n\t".
|
|
str_replace("\n", "\n\t", print_r($m, true)));
|
|
return $m;
|
|
}
|
|
return False;
|
|
}
|
|
|
|
/**
|
|
* Retreive current requested URL and return it
|
|
*
|
|
* @return string|false The current request URL or false if fail
|
|
**/
|
|
public static function get_current_url() {
|
|
Log :: trace("URL : request URI = '".$_SERVER['REQUEST_URI']."'");
|
|
|
|
$base = self :: get_rewrite_base();
|
|
Log :: trace("URL : rewrite base = '$base'");
|
|
|
|
if ($_SERVER['REQUEST_URI'] == $base)
|
|
return '';
|
|
|
|
if (substr($_SERVER['REQUEST_URI'], 0, strlen($base)) != $base) {
|
|
Log :: error(
|
|
"URL : request URI (".$_SERVER['REQUEST_URI'].") does not start with rewrite base ($base)");
|
|
return False;
|
|
}
|
|
|
|
$current_url = substr($_SERVER['REQUEST_URI'], strlen($base));
|
|
|
|
// URL contain params ?
|
|
$params_start = strpos($current_url, '?');
|
|
if ($params_start !== false)
|
|
// Params detected, remove it
|
|
|
|
// No url / currrent url start by '?' ?
|
|
if ($params_start == 0)
|
|
return '';
|
|
else
|
|
return substr($current_url, 0, $params_start);
|
|
|
|
return $current_url;
|
|
}
|
|
|
|
/**
|
|
* Try to detect rewrite base from public root URL
|
|
*
|
|
* @return string The detected rewrite base
|
|
**/
|
|
public static function get_rewrite_base() {
|
|
if (!self :: $public_root_url) return '/';
|
|
if (preg_match('|^https?://[^/]+/(.*)$|', self :: $public_root_url, $m))
|
|
return '/'.self :: remove_trailing_slash($m[1]).'/';
|
|
if (preg_match('|^/(.*)$|', self :: $public_root_url, $m))
|
|
return '/'.self :: remove_trailing_slash($m[1]).'/';
|
|
return '/';
|
|
}
|
|
|
|
/**
|
|
* Trigger redirect to specified URL (or homepage if omited)
|
|
*
|
|
* @param string|false $go The destination URL
|
|
*
|
|
* @return void
|
|
**/
|
|
public static function redirect($go=false) {
|
|
if ($go===false)
|
|
$go = "";
|
|
// If more than one argument passed, format URL using sprintf & urlencode parameters
|
|
elseif (func_num_args() > 1)
|
|
$go = call_user_func_array(
|
|
'sprintf',
|
|
array_merge(
|
|
array($go),
|
|
array_map('urlencode', array_slice(func_get_args(), 1))
|
|
)
|
|
);
|
|
|
|
if (self :: is_absolute_url($go))
|
|
$url = $go;
|
|
elseif (self :: $public_root_url)
|
|
$url = self :: $public_root_url."/$go";
|
|
else
|
|
$url = "/$go";
|
|
|
|
// Prevent loop
|
|
if (isset($_SESSION['last_redirect']) && $_SESSION['last_redirect'] == $url)
|
|
Log :: fatal(
|
|
I18n::_('Unable to determine the requested page (loop detected). '.
|
|
'If the problem persists, please contact support.'));
|
|
else
|
|
$_SESSION['last_redirect'] = $url;
|
|
|
|
Log :: debug("redirect($go) => Redirect to : <$url>");
|
|
header("Location: $url");
|
|
exit();
|
|
}
|
|
|
|
/**
|
|
* Handle the current requested URL
|
|
*
|
|
* Note: if the route required that user is authenticated, this method will
|
|
* invoke Auth::login() in force mode (or trigger a fatal error if fail).
|
|
*
|
|
* @param string|null $default_url The default URL if current one does not
|
|
* match with any configured pattern.
|
|
*
|
|
* @return void
|
|
**/
|
|
public static function handle_request($default_url=null) {
|
|
$sentry_span = new SentrySpan('http.handle_request', 'Handle the HTTP request');
|
|
|
|
self :: $request = self :: get_request($default_url);
|
|
|
|
if (!is_callable(self :: $request -> handler)) {
|
|
Log :: error(
|
|
"URL handler function %s does not exists !",
|
|
format_callable(self :: $request -> handler));
|
|
Log :: fatal(I18n::_("This request cannot be processed."));
|
|
}
|
|
|
|
if (self :: $request -> api_mode)
|
|
self :: $_api_mode = true;
|
|
if (Tpl :: initialized())
|
|
Tpl :: assign('request', self :: $request );
|
|
|
|
// Check authentication (if need)
|
|
if(self :: $request -> authenticated && !Auth::login(true))
|
|
Log :: fatal(I18n::_("Authentication required but fail to authenticate you."));
|
|
|
|
Hook::trigger('before_handling_request', array('request' => self :: $request));
|
|
$success = false;
|
|
try {
|
|
call_user_func(self :: $request -> handler, self :: $request);
|
|
$success = true;
|
|
}
|
|
catch (Exception $e) {
|
|
Log :: exception(
|
|
$e, "An exception occured running URL handler %s",
|
|
format_callable(self :: $request -> handler));
|
|
}
|
|
Hook::trigger(
|
|
'after_handling_request',
|
|
array('request' => self :: $request, 'success' => $success)
|
|
);
|
|
$sentry_span->finish();
|
|
if (!$success)
|
|
Log :: fatal(I18n::_("This request could not be processed correctly."));
|
|
}
|
|
|
|
/**
|
|
* Remove trailing slash in specified URL
|
|
*
|
|
* @param string $url The URL
|
|
*
|
|
* @return string The specified URL without trailing slash
|
|
**/
|
|
public static function remove_trailing_slash($url) {
|
|
if ($url == '/')
|
|
return $url;
|
|
return rtrim($url, '/');
|
|
}
|
|
|
|
/**
|
|
* Get the public absolute URL
|
|
*
|
|
* @param string|null $relative_url Relative URL to convert (Default: current URL)
|
|
*
|
|
* @return string The public absolute URL
|
|
**/
|
|
public static function get_absolute_url($relative_url=null) {
|
|
if (!is_string($relative_url))
|
|
$relative_url = self :: get_current_url();
|
|
if (self :: $public_root_url[0] == '/') {
|
|
Log :: debug(
|
|
"URL :: get_absolute_url($relative_url): configured public root URL is relative ".
|
|
"(%s) => try to detect it from current request infos.",
|
|
self :: $public_root_url);
|
|
self :: $public_root_url = (
|
|
'http'.(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'?'s':'').'://'.
|
|
$_SERVER['HTTP_HOST'].self :: $public_root_url);
|
|
Log :: debug(
|
|
"URL :: get_absolute_url(%s): detected public_root_url: %s",
|
|
$relative_url, self :: $public_root_url);
|
|
}
|
|
|
|
if (substr($relative_url, 0, 1) == '/')
|
|
$relative_url = substr($relative_url, 1);
|
|
$url = self :: remove_trailing_slash(self :: $public_root_url)."/$relative_url";
|
|
Log :: debug("URL :: get_absolute_url($relative_url): result = $url");
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Check if specified URL is absolute
|
|
*
|
|
* @param string $url The URL to check
|
|
*
|
|
* @return boolean True if specified URL is absolute, False otherwise
|
|
**/
|
|
public static function is_absolute_url($url) {
|
|
return boolval(preg_match('#^https?://#', $url));
|
|
}
|
|
|
|
/**
|
|
* Add parameter in specified URL
|
|
*
|
|
* @param string &$url The reference of the URL
|
|
* @param string $parameter The parameter name
|
|
* @param string $value The parameter value
|
|
* @param boolean $encode Set if parameter value must be URL encoded (optional, default: true)
|
|
*
|
|
* @return string The completed URL
|
|
*/
|
|
public static function add_url_parameter(&$url, $parameter, $value, $encode=true) {
|
|
if (strpos($url, '?') === false)
|
|
$url .= '?';
|
|
else
|
|
$url .= '&';
|
|
$url .= "$parameter=".($encode?urlencode($value):$value);
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Add parameters in specified URL
|
|
*
|
|
* @param string &$url The reference of the URL
|
|
* @param array<string,string> $parameters The parameters as an associative array
|
|
* @param boolean $encode Set if parameters values must be URL encoded (optional, default: true)
|
|
*
|
|
* @return string The completed URL
|
|
*/
|
|
public static function add_url_parameters(&$url, $parameters, $encode=true) {
|
|
foreach($parameters as $parameter => $value)
|
|
self :: add_url_parameter($url, $parameter, $value, $encode);
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Get/set API mode
|
|
* @param bool|null $value If boolean, the current API mode will be changed
|
|
* @return bool Current API mode
|
|
*/
|
|
public static function api_mode($value=null) {
|
|
if (is_bool($value)) self :: $_api_mode = $value;
|
|
return self :: $_api_mode;
|
|
}
|
|
|
|
/**
|
|
* Get public root URL
|
|
* @return string
|
|
*/
|
|
public static function public_root_url() {
|
|
return (self :: $public_root_url?self :: $public_root_url:'/');
|
|
}
|
|
}
|
|
|
|
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab
|