Benjamin Renard
6fdc5447f1
* Code cleaning and fix some small errors using Phpstan * Configure pre-commit to run Phpstan before each commit * Some little improvments and logging, mail, smarty & URL libs * Add Sentry integration * Add Webstat JS code inclusion * Install Smarty dependency using composer Breaking changes: * Rename Event class as HookEvent to avoid conflict with PECL event * URL with refresh GET parameter now automatically trigger redirection without it after page loading to avoid to keep it in URL
613 lines
17 KiB
PHP
613 lines
17 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Configured URL patterns :
|
|
*
|
|
* Example :
|
|
*
|
|
* array (
|
|
* '|get/(?P<name>[a-zA-Z0-9]+)$|' => array (
|
|
* 'handler' => 'get',
|
|
* 'authenticated' => true,
|
|
* 'api_mode' => false,
|
|
* 'methods' => array('GET'),
|
|
* ),
|
|
* '|get/all$|' => => array (
|
|
* 'handler' => 'get_all',
|
|
* 'authenticated' => true,
|
|
* 'api_mode' => false,
|
|
* 'methods' => array('GET', 'POST'),
|
|
* ),
|
|
* )
|
|
*
|
|
*/
|
|
$url_patterns =array();
|
|
|
|
/**
|
|
* Add an URL pattern
|
|
*
|
|
* @param string|array $pattern The URL pattern or an array of patterns (required)
|
|
* @param callable $handler The URL pattern handler (must be callable, required)
|
|
* @param boolean $authenticated Permit to define if this URL is accessible only for
|
|
* authenticated users (optional, default: true if the
|
|
* special force_authentication function is defined,
|
|
* false otherwise)
|
|
* @param boolean $override Allow override 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 $methods HTTP method (optional, default: array('GET', 'POST'))
|
|
**/
|
|
function add_url_handler($pattern, $handler=null, $authenticated=null, $override=true,
|
|
$api_mode=false, $methods=null) {
|
|
$authenticated = (
|
|
is_null($authenticated)?
|
|
function_exists('force_authentication'):
|
|
(bool)$authenticated
|
|
);
|
|
if (is_null($methods))
|
|
$methods = array('GET', 'POST');
|
|
elseif (!is_array($methods))
|
|
$methods = array($methods);
|
|
global $url_patterns;
|
|
if (is_array($pattern)) {
|
|
if (is_null($handler))
|
|
foreach($pattern as $p => $h)
|
|
add_url_handler($p, $h, $authenticated, $override, $api_mode, $methods);
|
|
else
|
|
foreach($pattern as $p)
|
|
add_url_handler($p, $handler, $authenticated, $override, $api_mode, $methods);
|
|
}
|
|
else {
|
|
if (!isset($url_patterns[$pattern])) {
|
|
$url_patterns[$pattern] = array(
|
|
'handler' => $handler,
|
|
'authenticated' => $authenticated,
|
|
'api_mode' => $api_mode,
|
|
'methods' => $methods,
|
|
);
|
|
}
|
|
elseif ($override) {
|
|
logging(
|
|
'DEBUG', "URL : override pattern '%s' with handler '%s' (old handler = '%s')".
|
|
$pattern, format_callable($handler), vardump($url_patterns[$pattern])
|
|
);
|
|
$url_patterns[$pattern] = array(
|
|
'handler' => $handler,
|
|
'authenticated' => $authenticated,
|
|
'api_mode' => $api_mode,
|
|
'methods' => $methods,
|
|
);
|
|
}
|
|
else {
|
|
logging('DEBUG', "URL : pattern '$pattern' already defined : do not override.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
**/
|
|
function error_page($request=null, $error_code=null) {
|
|
global $smarty;
|
|
$http_errors = array(
|
|
400 => array(
|
|
'pagetitle' => _("Bad request"),
|
|
'message' => _("Invalid request."),
|
|
),
|
|
401 => array(
|
|
'pagetitle' => _("Authentication required"),
|
|
'message' => _("You have to be authenticated to access to this page."),
|
|
),
|
|
403 => array(
|
|
'pagetitle' => _("Access denied"),
|
|
'message' => _("You do not have access to this application. If you think this is an error, please contact support."),
|
|
),
|
|
404 => array(
|
|
'pagetitle' => _("Whoops ! Page not found"),
|
|
'message' => _("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' => _('Error'),
|
|
'message' => _('An unknown error occurred. If problem persist, please contact support.'),
|
|
);
|
|
http_response_code($error_code);
|
|
|
|
$smarty -> assign('message', $error['message']);
|
|
display_template('error_page.tpl', $error['pagetitle']);
|
|
exit();
|
|
}
|
|
|
|
/*
|
|
* Error 404 page
|
|
*/
|
|
|
|
/**
|
|
* Error 404 handler
|
|
*
|
|
* @param UrlRequest|null $request The request (optional, default: null)
|
|
*
|
|
* @return void
|
|
**/
|
|
function error_404($request=null) {
|
|
error_page($request, 404);
|
|
}
|
|
|
|
$_404_url_handler = 'error_404';
|
|
function set_404_url_handler($handler=null) {
|
|
global $_404_url_handler;
|
|
$_404_url_handler = $handler;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
function get_request($default_url=null) {
|
|
global $url_patterns, $_404_url_handler;
|
|
$current_url = get_current_url();
|
|
if ($current_url === false) {
|
|
logging(
|
|
'FATAL',
|
|
_('Unable to determine the requested page. '.
|
|
'If the problem persists, please contact support.')
|
|
);
|
|
exit();
|
|
}
|
|
if (!is_array($url_patterns)) {
|
|
logging('FATAL', 'URL : No URL patterns configured !');
|
|
exit();
|
|
}
|
|
|
|
logging('DEBUG', "URL : current url = '$current_url'");
|
|
logging(
|
|
'TRACE',
|
|
"URL : check current url with the following URL patterns :\n - ".
|
|
implode("\n - ", array_keys($url_patterns))
|
|
);
|
|
foreach ($url_patterns as $pattern => $handler_infos) {
|
|
$m = url_match($pattern, $current_url, $handler_infos['methods']);
|
|
if (is_array($m)) {
|
|
$request = new UrlRequest($current_url, $handler_infos, $m);
|
|
// Reset last redirect
|
|
if (isset($_SESSION['last_redirect']))
|
|
unset($_SESSION['last_redirect']);
|
|
logging('TRACE', "URL : result :\n".vardump($request));
|
|
return $request;
|
|
}
|
|
}
|
|
if ($default_url !== false) {
|
|
logging('DEBUG', "Current url match with no pattern. Redirect to default url ('$default_url')");
|
|
redirect($default_url);
|
|
exit();
|
|
}
|
|
// Error 404
|
|
$api_mode = (strpos($current_url, 'api/') === 0);
|
|
logging(
|
|
'DEBUG',
|
|
"Current URL match with no pattern. Use error 404 handler (API mode=$api_mode).");
|
|
return new UrlRequest(
|
|
$current_url,
|
|
array(
|
|
'handler' => $_404_url_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)
|
|
* @param array|null $methods HTTP method (optional, default: no check)
|
|
*
|
|
* @return array|false The URL info if pattern matched, false otherwise.
|
|
**/
|
|
function url_match($pattern, $current_url=false, $methods=null) {
|
|
if ($methods && !in_array($_SERVER['REQUEST_METHOD'], $methods))
|
|
return false;
|
|
if ($current_url === false) {
|
|
$current_url = get_current_url();
|
|
if (!$current_url) return False;
|
|
}
|
|
if (preg_match($pattern, $current_url, $m)) {
|
|
logging(
|
|
'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
|
|
**/
|
|
function get_current_url() {
|
|
logging('TRACE', "URL : request URI = '".$_SERVER['REQUEST_URI']."'");
|
|
|
|
$base = get_rewrite_base();
|
|
logging('TRACE', "URL : rewrite base = '$base'");
|
|
|
|
if ($_SERVER['REQUEST_URI'] == $base)
|
|
return '';
|
|
|
|
if (substr($_SERVER['REQUEST_URI'], 0, strlen($base)) != $base) {
|
|
logging(
|
|
'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
|
|
**/
|
|
function get_rewrite_base() {
|
|
global $public_root_url;
|
|
if (preg_match('|^https?://[^/]+/(.*)$|', $public_root_url, $m))
|
|
return '/'.remove_trailing_slash($m[1]).'/';
|
|
elseif (preg_match('|^/(.*)$|', $public_root_url, $m))
|
|
return '/'.remove_trailing_slash($m[1]).'/';
|
|
return '/';
|
|
}
|
|
|
|
/**
|
|
* Trigger redirect to specified URL (or homepage if omited)
|
|
*
|
|
* @param string|false $go The destination URL
|
|
*
|
|
* @return void
|
|
**/
|
|
function redirect($go=false) {
|
|
global $public_root_url;
|
|
|
|
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 (is_absolute_url($go))
|
|
$url = $go;
|
|
elseif (isset($public_root_url) && $public_root_url) {
|
|
// Check $public_root_url end
|
|
if (substr($public_root_url, -1)=='/') {
|
|
$public_root_url=substr($public_root_url, 0, -1);
|
|
}
|
|
$url="$public_root_url/$go";
|
|
}
|
|
else
|
|
$url="/$go";
|
|
|
|
// Prevent loop
|
|
if (isset($_SESSION['last_redirect']) && $_SESSION['last_redirect'] == $url)
|
|
logging(
|
|
'FATAL',
|
|
_('Unable to determine the requested page (loop detected). '.
|
|
'If the problem persists, please contact support.'));
|
|
else
|
|
$_SESSION['last_redirect'] = $url;
|
|
|
|
logging('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 the force_authentication() special function (or trigger a fatal error
|
|
* if it's not defined).
|
|
*
|
|
* @param string|null $default_url The default URL if current one does not
|
|
* match with any configured pattern.
|
|
*
|
|
* @return void
|
|
**/
|
|
function handle_request($default_url=null) {
|
|
global $smarty, $api_mode;
|
|
|
|
$sentry_span = new SentrySpan('http.handle_request', 'Handle the HTTP request');
|
|
|
|
$request = get_request($default_url);
|
|
|
|
if (!is_callable($request -> handler)) {
|
|
logging(
|
|
'ERROR', "URL handler function %s does not exists !",
|
|
format_callable($request -> handler));
|
|
logging('FATAL', _("This request cannot be processed."));
|
|
}
|
|
|
|
if ($request -> api_mode)
|
|
$api_mode = true;
|
|
if (isset($smarty) && $smarty)
|
|
$smarty -> assign('request', $request);
|
|
|
|
// Check authentication (if need)
|
|
if($request -> authenticated)
|
|
if (function_exists('force_authentication'))
|
|
force_authentication();
|
|
else
|
|
logging('FATAL', _("Authentication required but force_authentication function is not defined."));
|
|
|
|
try {
|
|
call_user_func($request -> handler, $request);
|
|
}
|
|
catch (Exception $e) {
|
|
log_exception(
|
|
$e, "An exception occured running URL handler function ".$request -> handler."()");
|
|
logging('FATAL', _("This request could not be processed correctly."));
|
|
}
|
|
$sentry_span->finish();
|
|
}
|
|
|
|
/**
|
|
* Remove trailing slash in specified URL
|
|
*
|
|
* @param string $url The URL
|
|
*
|
|
* @return string The specified URL without trailing slash
|
|
**/
|
|
function remove_trailing_slash($url) {
|
|
if ($url == '/')
|
|
return $url;
|
|
elseif (substr($url, -1) == '/')
|
|
return substr($url, 0, -1);
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Check an AJAX request and trigger a fatal error on fail
|
|
*
|
|
* Check if session key is present and valid and set AJAX
|
|
* mode.
|
|
*
|
|
* @param string|null $session_key string The current session key (optional)
|
|
*
|
|
* @return void
|
|
**/
|
|
function check_ajax_request($session_key=null) {
|
|
global $ajax, $debug_ajax;
|
|
$ajax = true;
|
|
|
|
if (check_session_key($session_key))
|
|
fatal_error('Invalid request');
|
|
|
|
if ($debug_ajax)
|
|
logging('DEBUG',"Ajax Request : ".vardump($_REQUEST));
|
|
}
|
|
|
|
/**
|
|
* Get the public absolute URL
|
|
*
|
|
* @param string|null $relative_url Relative URL to convert (Default: current URL)
|
|
*
|
|
* @return string The public absolute URL
|
|
**/
|
|
function get_absolute_url($relative_url=null) {
|
|
global $public_root_url;
|
|
if (!is_string($relative_url))
|
|
$relative_url = get_current_url();
|
|
if ($public_root_url[0] == '/') {
|
|
logging(
|
|
'DEBUG',
|
|
"URL :: get_absolute_url($relative_url): configured public root URL is relative ".
|
|
"($public_root_url) => try to detect it from current request infos.");
|
|
$public_root_url = (
|
|
'http'.(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'?'s':'').'://'.
|
|
$_SERVER['HTTP_HOST'].$public_root_url);
|
|
logging(
|
|
'DEBUG',
|
|
"URL :: get_absolute_url($relative_url): detected public_root_url: $public_root_url");
|
|
}
|
|
|
|
if (substr($relative_url, 0, 1) == '/')
|
|
$relative_url = substr($relative_url, 1);
|
|
$url = remove_trailing_slash($public_root_url)."/$relative_url";
|
|
logging('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
|
|
**/
|
|
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 $param 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
|
|
*/
|
|
function add_url_parameter(&$url, $param, $value, $encode=true) {
|
|
if (strpos($url, '?') === false)
|
|
$url .= '?';
|
|
else
|
|
$url .= '&';
|
|
$url .= "$param=".($encode?urlencode($value):$value);
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* URL request abstraction
|
|
*
|
|
* @author Benjamin Renard <brenard@easter-eggs.com>
|
|
*/
|
|
class UrlRequest {
|
|
|
|
// The URL requested handler
|
|
private $current_url = null;
|
|
|
|
// The URL requested handler
|
|
private $handler = null;
|
|
|
|
// Request need authentication ?
|
|
private $authenticated = true;
|
|
|
|
// API mode enabled ?
|
|
private $api_mode = false;
|
|
|
|
// Parameters detected on requested URL
|
|
private $url_params = array();
|
|
|
|
public function __construct($current_url, $handler_infos, $url_params=array()) {
|
|
$this -> current_url = $current_url;
|
|
$this -> handler = $handler_infos['handler'];
|
|
$this -> authenticated = (
|
|
isset($handler_infos['authenticated'])?
|
|
boolval($handler_infos['authenticated']):true);
|
|
$this -> api_mode = (
|
|
isset($handler_infos['api_mode'])?
|
|
boolval($handler_infos['api_mode']):false);
|
|
$this -> url_params = $url_params;
|
|
}
|
|
|
|
/**
|
|
* Get request info
|
|
*
|
|
* @param string $key The name of the info
|
|
*
|
|
* @return mixed The value
|
|
**/
|
|
public function __get($key) {
|
|
if ($key == 'current_url')
|
|
return $this -> current_url;
|
|
if ($key == 'handler')
|
|
return $this -> handler;
|
|
if ($key == 'authenticated')
|
|
return $this -> authenticated;
|
|
if ($key == 'api_mode')
|
|
return $this -> api_mode;
|
|
if ($key == 'referer')
|
|
return $this -> get_referer();
|
|
if ($key == 'http_method')
|
|
return $_SERVER['REQUEST_METHOD'];
|
|
if (array_key_exists($key, $this->url_params)) {
|
|
return urldecode($this->url_params[$key]);
|
|
}
|
|
// Unknown key, log warning
|
|
logging('WARNING', "__get($key): invalid property requested\n".get_debug_backtrace_context());
|
|
}
|
|
|
|
/**
|
|
* Set request info
|
|
*
|
|
* @param string $key The name of the info
|
|
* @param mixed $value The value of the info
|
|
*
|
|
* @return void
|
|
**/
|
|
public function __set($key, $value) {
|
|
if ($key == 'referer')
|
|
$_SERVER['HTTP_REFERER'] = $value;
|
|
elseif ($key == 'http_method')
|
|
$_SERVER['REQUEST_METHOD'] = $value;
|
|
else
|
|
$this->url_params[$key] = $value;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Check is request info is set
|
|
*
|
|
* @param string $key The name of the info
|
|
*
|
|
* @return bool True is info is set, False otherwise
|
|
**/
|
|
public function __isset($key) {
|
|
if (
|
|
in_array(
|
|
$key, array('current_url', 'handler', 'authenticated',
|
|
'api_mode', 'referer', 'http_method')
|
|
)
|
|
)
|
|
return True;
|
|
return array_key_exists($key, $this->url_params);
|
|
}
|
|
|
|
/**
|
|
* Get request parameter
|
|
*
|
|
* @param string $parameter The name of the parameter
|
|
* @param bool $decode If true, the parameter value will be urldecoded
|
|
* (optional, default: true)
|
|
*
|
|
* @return mixed The value or false if parameter does not exists
|
|
**/
|
|
public function get_param($parameter, $decode=true) {
|
|
if (array_key_exists($parameter, $this->url_params)) {
|
|
if ($decode)
|
|
return urldecode($this->url_params[$parameter]);
|
|
return $this->url_params[$parameter];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get request referer (if known)
|
|
*
|
|
* @return string|null The request referer URL if known, null otherwise
|
|
*/
|
|
public function get_referer() {
|
|
if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'])
|
|
return $_SERVER['HTTP_REFERER'];
|
|
return null;
|
|
}
|
|
|
|
}
|
|
|
|
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab
|