eesyphp/src/Url.php
Benjamin Renard 83f1445799
Make core libs set their default config values in App
This permit to homogenize the method to store it and permit an access to 
all config default values.
Futhermore, core libs init() methods now does not handle parameters: all 
are taken from config.
2023-03-01 16:22:11 +01:00

586 lines
18 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
* @return void
*/
public static function init() {
// Set default config values
App :: set_default('public_root_url', null);
App :: set_default('api_mode', false);
if (php_sapi_name() == 'cli-server' && getenv('EESYPHP_SERVE_URL'))
$public_root_url = getenv('EESYPHP_SERVE_URL');
else
$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(App::get('api_mode', null, 'bool'));
}
/**
* 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 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 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)
$url = 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