diff --git a/includes/core.php b/includes/core.php index a0962b1..7cb8c6e 100644 --- a/includes/core.php +++ b/includes/core.php @@ -4,6 +4,7 @@ use EesyPHP\Log; use EesyPHP\SentryIntegration; use EesyPHP\SentrySpan; use EesyPHP\SentryTransaction; +use EesyPHP\Url; error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED); @@ -27,9 +28,6 @@ set_include_path($root_dir_path.'/includes' . PATH_SEPARATOR . get_include_path( // Load composer autoload.php require("$root_dir_path/vendor/autoload.php"); -// API mode -$api_mode = false; - // Load configuration require_once('translation.php'); require_once('config.inc.php'); @@ -74,7 +72,7 @@ require_once('hooks.php'); require_once('cli.php'); require_once('translation-cli.php'); require_once('smarty.php'); -require_once('url.php'); +Url::init(isset($public_root_url)?$public_root_url:null); require_once('url-helpers.php'); require_once('db.php'); require_once('mail.php'); diff --git a/includes/functions.php b/includes/functions.php index 2a86c88..0acf130 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -299,4 +299,25 @@ function run_external_command($command, $data_stdin=null, $escape_command_args=t return array($return_value, $stdout, $stderr); } +/** + * 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) + Log :: debug("Ajax Request : ".vardump($_REQUEST)); +} + # vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab diff --git a/includes/smarty.php b/includes/smarty.php index 020e1ec..0e8e3c8 100644 --- a/includes/smarty.php +++ b/includes/smarty.php @@ -3,6 +3,7 @@ use EesyPHP\Log; use EesyPHP\SentrySpan; use EesyPHP\SentryTransaction; +use EesyPHP\Url; if (php_sapi_name() == "cli") return true; @@ -188,10 +189,10 @@ function display_template($template, $pagetitle=false) { // If refresh parameter is present, remove it and redirect if (isset($_GET['refresh'])) { unset($_GET['refresh']); - $url = get_current_url(); + $url = Url :: get_current_url(); if (!empty($_GET)) $url .= '?'.http_build_query($_GET); - redirect($url); + Url :: redirect($url); return; } @@ -246,7 +247,7 @@ function display_ajax_return($data=null, $pretty=false) { $ajax=false; function fatal_error($error) { - global $smarty, $ajax, $api_mode; + global $smarty, $ajax; // If more than one arguments passed, format error message using sprintf if (func_num_args() > 1) { @@ -262,7 +263,7 @@ function fatal_error($error) { // Set HTTP reponse code to 500 http_response_code(500); - if ($ajax || $api_mode) + if ($ajax || Url :: api_mode()) display_ajax_return(array('success' => false, 'error' => $error)); $smarty->assign('fatal_error', $error); diff --git a/includes/url-public.php b/includes/url-public.php index 4a34e07..bbfc7e1 100644 --- a/includes/url-public.php +++ b/includes/url-public.php @@ -2,6 +2,7 @@ use EesyPHP\Check; use EesyPHP\Log; +use EesyPHP\Url; use function EesyPHP\vardump; @@ -11,7 +12,7 @@ if (php_sapi_name() == "cli") function handle_homepage($request) { display_template("homepage.tpl", _("Hello world !")); } -add_url_handler('#^$#', 'handle_homepage'); +Url :: add_url_handler('#^$#', 'handle_homepage'); function handle_search($request) { global $smarty, $status_list; @@ -29,7 +30,7 @@ function handle_search($request) { 'order_direction' => 'ASC', ); if (isset($_REQUEST['clear']) && $_REQUEST['clear']=='true') - redirect($request -> current_url); + Url :: redirect($request -> current_url); } Log :: debug('Request params : '.vardump($_REQUEST)); @@ -108,7 +109,7 @@ function handle_search($request) { display_template("search.tpl", _("Search")); } -add_url_handler('|^item/?$|', 'handle_search'); +Url :: add_url_handler('|^item/?$|', 'handle_search'); /* * One item pages @@ -119,7 +120,7 @@ function handle_show($request) { $item = get_item_from_url($request -> id); if (!$item) - error_404(); + Url :: error_404(); $smarty->assign('item', $item); @@ -134,7 +135,7 @@ function handle_show($request) { (is_array($item)?$item['name']:"#".$request -> id) ); } -add_url_handler('|^item/(?P[0-9]+)$|', 'handle_show'); +Url :: add_url_handler('|^item/(?P[0-9]+)$|', 'handle_show'); function handle_create($request) { global $smarty, $status_list; @@ -145,7 +146,7 @@ function handle_create($request) { $item = add_item($info); if (is_array($item)) { add_message(_("The element '% s' has been created."), $item['name']); - redirect('item/'.$item['id']); + Url :: redirect('item/'.$item['id']); } else { add_error(_("An error occurred while saving this item.")); @@ -164,7 +165,7 @@ function handle_create($request) { display_template("form.tpl", _("New")); } -add_url_handler('|^item/new$|', 'handle_create'); +Url :: add_url_handler('|^item/new$|', 'handle_create'); function handle_modify($request) { global $smarty, $status_list; @@ -173,7 +174,7 @@ function handle_modify($request) { if(is_array($item)) { if (!can_modify($item)) { add_error(_('You cannot edit this item.')); - redirect('item/'.$item['id']); + Url :: redirect('item/'.$item['id']); } $info = array(); $field_errors = handle_item_post_data($info); @@ -186,11 +187,11 @@ function handle_modify($request) { Log :: debug('Changes : '.vardump($changes)); if (empty($changes)) { add_message(_("You have not made any changes to element '% s'."), $item['name']); - redirect('item/'.$item['id']); + Url :: redirect('item/'.$item['id']); } else if (update_item($item['id'], $changes) === true) { add_message(_("The element '% s' has been updated successfully."), $item['name']); - redirect('item/'.$item['id']); + Url :: redirect('item/'.$item['id']); } else { add_error(_("An error occurred while updating this item.")); @@ -209,7 +210,7 @@ function handle_modify($request) { $smarty -> assign('status_list', $status_list); } else { - error_404(); + Url :: error_404(); } display_template( @@ -217,7 +218,7 @@ function handle_modify($request) { (is_array($item)?$item['name']:"#".$request -> id) ); } -add_url_handler('|^item/(?P[0-9]+)/modify$|', 'handle_modify'); +Url :: add_url_handler('|^item/(?P[0-9]+)/modify$|', 'handle_modify'); function handle_archive($request) { global $smarty; @@ -225,7 +226,7 @@ function handle_archive($request) { $item = get_item_from_url($request -> id); if(!is_array($item)) { add_error(_("Item #% s not found."), $request -> id); - redirect('item'); + Url :: redirect('item'); } elseif ($item['status'] == 'archived') { add_message(_("This item is already archived.")); @@ -239,9 +240,9 @@ function handle_archive($request) { else { add_error(_('An error occurred while archiving this item.')); } - redirect('item/'.$item['id']); + Url :: redirect('item/'.$item['id']); } -add_url_handler('|^item/(?P[0-9]+)/archive$|', 'handle_archive'); +Url :: add_url_handler('|^item/(?P[0-9]+)/archive$|', 'handle_archive'); function handle_delete($request) { global $smarty; @@ -258,10 +259,10 @@ function handle_delete($request) { } else { add_error(_('An error occurred while deleting this item.')); - redirect('item/'.$item['id']); + Url :: redirect('item/'.$item['id']); } - redirect('item'); + Url :: redirect('item'); } -add_url_handler('|^item/(?P[0-9]+)/delete$|', 'handle_delete'); +Url :: add_url_handler('|^item/(?P[0-9]+)/delete$|', 'handle_delete'); # vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab diff --git a/includes/url.php b/includes/url.php deleted file mode 100644 index 5a18b66..0000000 --- a/includes/url.php +++ /dev/null @@ -1,614 +0,0 @@ -[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) { - Log :: 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 { - Log :: 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) { - Log :: fatal( - _('Unable to determine the requested page. '. - 'If the problem persists, please contact support.') - ); - exit(); - } - if (!is_array($url_patterns)) { - Log :: fatal('URL : No URL patterns configured !'); - exit(); - } - - Log :: debug("URL : current url = '$current_url'"); - Log :: 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']); - Log :: trace("URL : result :\n".vardump($request)); - return $request; - } - } - if ($default_url !== false) { - Log :: 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); - Log :: 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)) { - 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 - **/ -function get_current_url() { - Log :: trace("URL : request URI = '".$_SERVER['REQUEST_URI']."'"); - - $base = 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 - **/ -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) - Log :: fatal( - _('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 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)) { - Log :: error( - "URL handler function %s does not exists !", - format_callable($request -> handler)); - Log :: 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 - Log :: 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."()"); - Log :: 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) - Log :: 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] == '/') { - Log :: 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); - Log :: 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"; - 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 - **/ -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 - */ -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 - Log :: warning( - "__get($key): invalid property requested\n%s", - Log :: 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 diff --git a/phpstan.neon b/phpstan.neon index 5f76736..7a73eff 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -24,7 +24,6 @@ parameters: message: "#Variable \\$root_dir_path might not be defined\\.#" paths: - includes/config.inc.php - - "#Access to private property UrlRequest::\\$(handler|api_mode|authenticated)\\.#" - message: "#Variable \\$status_list might not be defined\\.#" paths: diff --git a/public_html/index.php b/public_html/index.php index ce2db4e..e42c9ce 100644 --- a/public_html/index.php +++ b/public_html/index.php @@ -3,7 +3,9 @@ include '../includes/core.php'; include 'url-public.php'; +use EesyPHP\Url; + $default_url=''; -handle_request(); +Url :: handle_request(); # vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab diff --git a/src/Url.php b/src/Url.php new file mode 100644 index 0000000..a5bfdbc --- /dev/null +++ b/src/Url.php @@ -0,0 +1,517 @@ +[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'), + * ), + * ) + * @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 bool $_api_mode; + + /** + * Initialization + * @param string|null $public_root_url The application public root URL + * @param bool $api_mode Enable/disable API mode + * @return void + */ + public static function init($public_root_url = null, $api_mode=false) { + if ($public_root_url) { + // Check URL end + if (substr(self :: $public_root_url, -1) == '/') + $public_root_url = substr($public_root_url, 0, -1); + self :: $public_root_url = $public_root_url?$public_root_url:null; + } + self :: $_api_mode = boolval($api_mode); + } + + /** + * 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')) + **/ + public static 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); + if (is_array($pattern)) { + if (is_null($handler)) + foreach($pattern as $p => $h) + self :: add_url_handler($p, $h, $authenticated, $override, $api_mode, $methods); + else + foreach($pattern as $p) + self :: add_url_handler($p, $handler, $authenticated, $override, $api_mode, $methods); + } + else { + if (!isset(self :: $patterns[$pattern])) { + self :: $patterns[$pattern] = array( + 'handler' => $handler, + 'authenticated' => $authenticated, + 'api_mode' => $api_mode, + 'methods' => $methods, + ); + } + elseif ($override) { + Log :: debug( + "URL : override pattern '%s' with handler '%s' (old handler = '%s')". + $pattern, format_callable($handler), vardump(self :: $patterns[$pattern]) + ); + self :: $patterns[$pattern] = array( + 'handler' => $handler, + 'authenticated' => $authenticated, + 'api_mode' => $api_mode, + 'methods' => $methods, + ); + } + else { + Log :: 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 + **/ + public static 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); + + if (isset($smarty) && $smarty) + $smarty -> assign('message', $error['message']); + display_template('error_page.tpl', $error['pagetitle']); + exit(); + } + + /** + * 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; + } + + + /** + * 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( + _('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'"); + Log :: trace( + "URL : check current url with the following URL patterns :\n - ". + implode("\n - ", array_keys(self :: $patterns)) + ); + foreach (self :: $patterns as $pattern => $handler_infos) { + $m = self :: 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']); + Log :: trace("URL : result :\n".vardump($request)); + return $request; + } + } + 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) + * @param array|null $methods HTTP method (optional, default: no check) + * + * @return array|false The URL info if pattern matched, false otherwise. + **/ + public static 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 = 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( + _('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 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 + **/ + public static function handle_request($default_url=null) { + global $smarty; + + $sentry_span = new SentrySpan('http.handle_request', 'Handle the HTTP request'); + + $request = self :: get_request($default_url); + + if (!is_callable($request -> handler)) { + Log :: error( + "URL handler function %s does not exists !", + format_callable($request -> handler)); + Log :: fatal(_("This request cannot be processed.")); + } + + if ($request -> api_mode) + self :: $_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 + Log :: 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 %s()", + format_callable($request -> handler)); + Log :: 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 + **/ + public static function remove_trailing_slash($url) { + if ($url == '/') + return $url; + elseif (substr($url, -1) == '/') + return substr($url, 0, -1); + return $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 $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 + */ + public static function add_url_parameter(&$url, $param, $value, $encode=true) { + if (strpos($url, '?') === false) + $url .= '?'; + else + $url .= '&'; + $url .= "$param=".($encode?urlencode($value):$value); + 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; + } +} + +# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab diff --git a/src/UrlRequest.php b/src/UrlRequest.php new file mode 100644 index 0000000..8769267 --- /dev/null +++ b/src/UrlRequest.php @@ -0,0 +1,143 @@ + + * @property-read string|null $current_url; + * @property-read array|null $handler; + * @property-read bool $authenticated; + * @property-read bool $api_mode; + */ +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 + Log :: warning( + "__get($key): invalid property requested\n%s", + Log :: 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