[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 bool $_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 (is_null($public_root_url)) $public_root_url = App::get('public_root_url', null, 'string'); if (is_string($public_root_url) && $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 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 * 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, $additional_info=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, $additional_info, $authenticated, $override, $api_mode, $methods); else foreach($pattern as $p) self :: add_url_handler( $p, $handler, $additional_info, $authenticated, $override, $api_mode, $methods); } else { if (!isset(self :: $patterns[$pattern])) { self :: $patterns[$pattern] = array( 'handler' => $handler, 'additional_info' => ( is_array($additional_info)? $additional_info:array() ), '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, 'additional_info' => ( is_array($additional_info)? $additional_info:array() ), '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) { $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'"); 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( 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 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) { $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(I18n::_("This request cannot be processed.")); } if ($request -> api_mode) self :: $_api_mode = true; if (Tpl :: initialized()) Tpl :: assign('request', $request); // Check authentication (if need) if($request -> authenticated) if (function_exists('force_authentication')) force_authentication(); else Log :: fatal(I18n::_("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(I18n::_("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; } /** * 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