eesyphp/src/Tpl.php
Benjamin Renard 4f47dc056d Tpl: Add stuff to handle static files
* Move example application in example sub-directory
* Widely use App::get() instead of Config::get()
2023-02-12 00:30:36 +01:00

547 lines
17 KiB
PHP

<?php
namespace EesyPHP;
use Exception;
use Smarty;
use Smarty_Security;
use League\MimeTypeDetection\ExtensionMimeTypeDetector;
class Tpl {
/**
* Smarty object
* @var \Smarty
*/
public static Smarty $smarty;
/**
* Smarty_Security object
* @var \Smarty_Security|null
*/
public static $smarty_security_policy = null;
/**
* Smarty templates directory path
* @var string
*/
public static string $templates_dir;
/**
* Smarty cache templates directory path
* @var string
*/
public static string $templates_c_dir;
/**
* Enable/disable AJAX returned data debugging in logs
* @var bool
*/
public static bool $_debug_ajax;
/**
* Static directories
* @var array
*/
private static array $static_directories = array();
/**
* CSS files to load in next displayed page
* @var array<string>
*/
private static array $css_files = array();
/**
* JavaScript files to load in next displayed page
* @var array<string>
*/
private static array $js_files = array();
/**
* MIME type detector object
* @var null|string
*/
private static $static_root_url;
/**
* MIME type detector object
* @var ExtensionMimeTypeDetector|null
*/
private static $mime_type_detector = null;
/**
* Initialization
* @param string $templates_dir Smarty templates directory path
* (optional, default: from template.directory config key)
* @param string $templates_c_dir Smarty cache templates directory path
* (optional, default: from template.cache_directory config key)
* @param bool $debug_ajax Enable/disable AJAX returned data debugging in logs
* (optional, default: from template.debug_ajax or debug_ajax config keys if set,
* false otherwise)
* @param bool $static_root_url Configure custom root URL path for static files
* (optional, default: from template.static_root_url config key if set,
* '/static' otherwise. Set to False to disable)
* @return void
*/
public static function init($templates_dir=null, $templates_c_dir=null, $debug_ajax=null,
$static_root_url=null) {
// Check templates/templates_c directories
if (is_null($templates_dir))
$templates_dir = App::get('template.directory', null, 'string');
if (is_null($templates_c_dir))
$templates_c_dir = App::get('template.cache_directory', null, 'string');
if (!$templates_dir || !is_dir($templates_dir)) {
Log :: fatal(
"Template directory not found (%s)",
$templates_dir?$templates_dir:'not set');
return;
}
if (!$templates_c_dir || !is_dir($templates_c_dir) || !is_writable($templates_c_dir)) {
Log :: fatal(
"Template cache directory not found or not writable (%s)",
$templates_c_dir?$templates_c_dir:'not set');
return;
}
self :: $smarty = new Smarty();
self :: $smarty->setTemplateDir($templates_dir);
self :: $smarty->setCompileDir($templates_c_dir);
if (is_null($debug_ajax))
$debug_ajax = App::get('template.debug_ajax', App::get('debug_ajax'));
self :: $_debug_ajax = boolval($debug_ajax);
Log :: register_fatal_error_handler(array('\\EesyPHP\\Tpl', 'fatal_error'));
if (is_null($static_root_url))
$static_root_url = App::get('template.static_root_url', 'static/', 'string');
if ($static_root_url) {
if (substr($static_root_url, 0, 1) == '/')
$static_root_url = substr($static_root_url, 1);
if (substr($static_root_url, -1) != '/')
$static_root_url = "$static_root_url/";
self :: $static_root_url = $static_root_url;
$default_static_directory = realpath(__DIR__."/../static");
self :: register_static_directory($default_static_directory, 100);
self :: register_function('static_url', array('EesyPHP\\Tpl', 'smarty_static_url'));
foreach(App :: get('templates.static_directories', array(), 'array') as $path)
self :: register_static_directory($path);
}
}
/**
* Enable security in mode to limit functions (in IF clauses) and modifiers usable from
* template files
* @param array<string>|null $functions List of function names granted in IF clauses
* @param array<string>|null $modifiers List of modifier names granted
* @return void
*/
public static function enable_security_mode($functions=null, $modifiers=null) {
// Define security policy
self :: $smarty_security_policy = new Smarty_Security(self :: $smarty);
// Allow functions in IF clauses
if (is_array($functions))
foreach($functions as $function)
self :: $smarty_security_policy->php_functions[] = $function;
// Allow modifier functions
if (is_array($modifiers))
foreach($modifiers as $modifier)
self :: $smarty_security_policy->php_modifiers[] = $modifier;
// Enable security
self :: $smarty -> enableSecurity(self :: $smarty_security_policy);
// Initialize errors & messages session variables
if (!isset($_SESSION['errors']))
$_SESSION['errors'] = array();
if (!isset($_SESSION['messages']))
$_SESSION['messages'] = array();
}
/**
* Register a function usable from template files
* @param string $name The function name
* @param callable $callable The function
* @return void
*/
public static function register_function($name, $callable) {
self :: $smarty -> registerPlugin("function", $name, $callable);
}
/**
* Assign template variable
* @param string $name The variable name
* @param mixed $value The variable value
* @return void
*/
public static function assign($name, $value) {
self :: $smarty -> assign($name, $value);
}
/**
* Add error message
* @param string $error The message
* @param array $extra_args Extra arguments to use to compute error message using sprintf
* @return void
*/
public static function add_error($error, ...$extra_args) {
// If extra arguments passed, format error message using sprintf
if ($extra_args) {
$error = call_user_func_array(
'sprintf',
array_merge(array($error), $extra_args)
);
}
$_SESSION['errors'][] = $error;
}
/**
* Add informational message
* @param string $message The message
* @param array $extra_args Extra arguments to use to compute message using sprintf
* @return void
*/
public static function add_message($message, ...$extra_args) {
// If extra arguments passed, format message using sprintf
if ($extra_args) {
$message = call_user_func_array(
'sprintf',
array_merge(array($message), $extra_args)
);
}
$_SESSION['messages'][] = $message;
}
/**
* Register CSS file(s) to load on next displayed page
* @param string|array<string> $args CSS files to load
* @return void
*/
public static function add_css_file(...$args) {
// Check if the first argument is a custom static root URL
$root_url = self :: $static_root_url;
if (
$args && is_string($args[0]) && array_key_exists(
self :: clean_static_root_url($args[0]),
self :: $static_directories
)
)
$root_url = self :: clean_static_root_url(array_shift($args));
foreach ($args as $files) {
if (!is_array($files)) $files = array($files);
foreach ($files as $file) {
$path = $root_url.$file;
if (!in_array($path, self :: $css_files))
self :: $css_files[] = $path;
}
}
}
/**
* Register JS file(s) to load on next displayed page
* @param string|array<string> $args JS files to load
* @return void
*/
public static function add_js_file(...$args) {
// Check if the first argument is a custom static root URL
$root_url = self :: $static_root_url;
if (
$args && is_string($args[0]) && array_key_exists(
self :: clean_static_root_url($args[0]),
self :: $static_directories
)
)
$root_url = self :: clean_static_root_url(array_shift($args));
foreach ($args as $files) {
if (!is_array($files)) $files = array($files);
foreach ($files as $file) {
$path = $root_url.$file;
if (!in_array($path, self :: $js_files))
self :: $js_files[] = $path;
}
}
}
/**
* Define common variables
* @param string|null $pagetitle The page title
* @return void
*/
protected static function define_common_variables($pagetitle=null) {
global $auth_user;
self :: assign('pagetitle', $pagetitle);
// Messages
self :: assign('errors', (isset($_SESSION['errors'])?$_SESSION['errors']:array()));
self :: assign('messages', (isset($_SESSION['messages'])?$_SESSION['messages']:array()));
// Files inclusions
self :: assign('css', self :: $css_files);
self :: assign('js', self :: $js_files);
// Authenticated user info
if (isset($auth_user))
self :: assign('auth_user', $auth_user);
}
/**
* Display the template
* @param string $template The template to display
* @param string|null $pagetitle The page title (optional)
* @param array $extra_args Extra arguments to use to compute the page title using sprintf
* @return void
*/
public static function display($template, $pagetitle=null, ...$extra_args) {
if (!$template) {
Log :: fatal(_("No template specified."));
return;
}
// If refresh parameter is present, remove it and redirect
if (isset($_GET['refresh'])) {
unset($_GET['refresh']);
$url = Url :: get_current_url();
if (!empty($_GET))
$url .= '?'.http_build_query($_GET);
Url :: redirect($url);
return;
}
$sentry_span = new SentrySpan('smarty.display_template', "Display Smarty template");
// If extra arguments passed, format pagetitle using sprintf
if ($pagetitle && $extra_args) {
$pagetitle = call_user_func_array(
'sprintf',
array_merge(array($pagetitle), $extra_args)
);
}
try {
Hook :: trigger('before_displaying_template');
self :: define_common_variables($pagetitle);
self :: $smarty->display($template);
}
catch (Exception $e) {
Log :: exception($e, "Smarty - An exception occured displaying template '$template'");
if ($template != 'fatal_error.tpl')
Log :: fatal(_("An error occurred while displaying this page."));
return;
}
unset($_SESSION['errors']);
unset($_SESSION['messages']);
Hook :: trigger('after_displaying_template');
$sentry_span->finish();
}
/**
* Display AJAX return
* @param array|null $data AJAX returned data (optional)
* @param bool $pretty AJAX returned data
* (optional, default: true if $_REQUEST['pretty'] is set, False otherwise)
* @return void
*/
public static function display_ajax_return($data=null, $pretty=false) {
if (!is_array($data))
$data = array();
// Adjust HTTP error code on unsuccessfull request
elseif (isset($data['success']) && !$data['success'] && http_response_code() == 200)
http_response_code(400);
if (isset($_SESSION['messages']) && !empty($_SESSION['messages'])) {
$data['messages'] = $_SESSION['messages'];
unset($_SESSION['messages']);
}
if (isset($_SESSION['errors']) && !empty($_SESSION['errors'])) {
$data['errors'] = $_SESSION['errors'];
unset($_SESSION['errors']);
}
if (self :: $_debug_ajax)
Log :: debug("AJAX Response : ".vardump($data));
header('Content-Type: application/json');
echo json_encode($data, (($pretty||isset($_REQUEST['pretty']))?JSON_PRETTY_PRINT:0));
exit();
}
/**
* Handle a fatal error
* @param string $error The error message
* @param array $extra_args Extra arguments to use to compute the error message using sprintf
* @return void
*/
public static function fatal_error($error, ...$extra_args) {
// If extra arguments passed, format error message using sprintf
if ($extra_args) {
$error = call_user_func_array(
'sprintf',
array_merge(array($error), $extra_args)
);
}
if (php_sapi_name() == "cli")
die("FATAL ERROR : $error\n");
// Set HTTP reponse code to 500
http_response_code(500);
// Handle API mode
if (Url :: api_mode()) {
self :: display_ajax_return(array('success' => false, 'error' => $error));
return;
}
self :: assign('fatal_error', $error);
self :: display('fatal_error.tpl');
exit();
}
/**
* Get/set AJAX debug mode
* @param bool|null $value If boolean, the current API mode will be changed
* @return bool Current API mode
*/
public static function debug_ajax($value=null) {
if (is_bool($value)) self :: $_debug_ajax = $value;
return self :: $_debug_ajax;
}
/**
* Check if initialized
* @return bool
*/
public static function initialized() {
return isset(self :: $smarty);
}
/**
* Get the templates directory path
* @return string|null
*/
public static function templates_directory() {
return isset(self :: $templates_directory)?self :: $templates_directory:null;
}
/**
* Clean static root URL helper
* @param string $value
* @return string
*/
public static function clean_static_root_url($value) {
if (substr($value, 0, 1) == '/')
$value = substr($value, 1);
if (substr($value, -1) != '/')
$value = "$value/";
return $value;
}
/**
* Register a static directory
* @param string $path The static directory path
* @param int|null $priority The priority of this static directory
* (optional, default: prior than all other registered directories)
* @return void
*/
public static function register_static_directory($path, $priority=null, $root_url=null) {
if (is_null($root_url)) {
if (!self :: $static_root_url)
Log :: fatal(
'register_static_directory(%s): no root URL provided and no default value configured',
$path);
$root_url = self :: $static_root_url;
}
if (!array_key_exists($root_url, self :: $static_directories)) {
self :: $static_directories[$root_url] = array();
if (is_null($priority)) $priority = 100;
$pattern = "#^(?P<root_url>$root_url)(?P<path>.*)#";
Log :: trace(
'Register static file URL handler for root URL "%s" with pattern "%s" and directory '.
'"%s" (priority: %d)', $root_url, $pattern, $path, $priority);
Url :: add_url_handler(
$pattern,
array('EesyPHP\\Tpl', 'handle_static_file'),
false, // authenticated
false, // override
false, // API mode
array('GET') // methods
);
if (is_null(self :: $mime_type_detector))
self :: $mime_type_detector = new ExtensionMimeTypeDetector();
}
else {
if (is_null($priority)) {
$priority = max(self :: $static_directories[$root_url]);
$priority++;
}
Log :: trace(
'Register additionnal static directory "%s" for root URL "%s" (priority: %d)',
$path, $root_url, $priority);
}
if (substr($path, -1) == PATH_SEPARATOR)
$path = substr($path, 0, -1);
self :: $static_directories[$root_url][$path] = $priority;
arsort(self :: $static_directories[$root_url]);
}
/**
* Resolve static path against registered static directories
* @param string $path
* @return string|false
*/
public static function resolve_static_path($root_url, $path) {
if (!array_key_exists($root_url, self :: $static_directories)) {
Log::error(
'No static directory registered for root URL "%s". Can no resolve static file "%s" path.',
$root_url, $path);
return false;
}
foreach(array_keys(self :: $static_directories[$root_url]) as $dir) {
$fullpath = "$dir/$path";
if (file_exists($fullpath))
return $fullpath;
}
Log::trace('Static file "%s%s" not found', $root_url, $path);
return false;
}
/**
* Handle URL request for static file
* Note: this URL handler is registered in EesyPHP\Url by self::init().
* @see self::init()
* @param UrlRequest $request
* @return void
*/
public static function handle_static_file($request) {
$path = self :: resolve_static_path($request->root_url, $request->path);
Log::trace('Resolved static file path for "%s": "%s"', $request->path, $path);
if (!$path)
Url :: trigger_error_404($request);
$mime_type = self :: $mime_type_detector->detectMimeTypeFromFile($path);
Log::trace('MIME type detected for "%s" is "%s"', $path, $mime_type);
dump_file($path, $mime_type);
}
/**
* Function to retrieve static file URL
* @param string $path The file path
* @return string|false
*/
public static function static_url($path) {
if (self :: $static_root_url)
return self :: $static_root_url.$path;
return false;
}
/**
* Smarty function to print static file URL
* @param array<string,mixed> $params Parameters from template file
* @param Smarty $smarty The smarty object
* @return void
*/
public static function smarty_static_url($params, $smarty) {
if (!isset($params['path'])) return;
$url = self :: static_url($params['path']);
if ($url) echo $url;
}
}