diff --git a/composer.json b/composer.json index 11049bd..3f17caa 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,10 @@ "smarty-gettext/smarty-gettext": "^1.6", "smarty-gettext/tsmarty2c": "^0.2.1", "sepia/po-parser": "^6.0", - "sentry/sdk": "^3.3" + "sentry/sdk": "^3.3", + "ext-pdo": "^7.3", + "ext-json": "^7.3", + "ext-yaml": "^2.0" }, "require-dev": { "phpstan/phpstan": "^1.9" diff --git a/composer.lock b/composer.lock index 3c13090..434f63f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "372e3ce1ce40fd466379b8709b0f7c5a", + "content-hash": "bbb5f6bafa8928dce90eb5d63dbc0376", "packages": [ { "name": "brenard/php-unidecode", @@ -2856,7 +2856,11 @@ }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], + "platform": { + "ext-pdo": "^7.4", + "ext-yaml": "^2.1", + "ext-json": "^7.4" + }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.0.0" } diff --git a/includes/.gitignore b/includes/.gitignore index 529bf01..8cf7685 100644 --- a/includes/.gitignore +++ b/includes/.gitignore @@ -1 +1 @@ -config.local.php +config.local.yml diff --git a/includes/config.inc.php b/includes/config.inc.php deleted file mode 100644 index 3339ce1..0000000 --- a/includes/config.inc.php +++ /dev/null @@ -1,155 +0,0 @@ - array( + "$root_dir_path/includes/config.local.yml", + ), + ), + $root_dir_path ); -$sentry_transaction = new SentryTransaction(); $sentry_span = new SentrySpan('core.init', 'Core initialization'); -// Define upload_tmp_dir -if (isset($upload_tmp_dir)) - ini_set('upload_tmp_dir', $upload_tmp_dir); - -if (!isset($log_file)) - die('Log file path not configured'); -Log::init( - $log_file, - isset($log_level)?$log_level:null, - isset($log_php_errors_levels)?$log_php_errors_levels:null -); require_once('functions.php'); -Session :: init( - isset($session_max_duration)?$session_max_duration:null, - isset($session_timeout)?$session_timeout:null -); // Nomenclatures $status_list = array ( @@ -72,29 +49,14 @@ $status_list = array ( 'refused' => I18n :: ___('Refused'), 'archived' => I18n :: ___('Archived'), ); +foreach($status_list as $key => $value) + $status_list[$key] = _($value); require_once('cli.php'); require_once('templates.php'); -Url::init(isset($public_root_url)?$public_root_url:null); require_once('url-helpers.php'); require_once('db.php'); -Email :: init( - isset($mail_sender)?$mail_sender:null, - isset($mail_send_method)?$mail_send_method:null, - isset($mail_send_params)?$mail_send_params:null, - isset($mail_catch_all)?$mail_catch_all:null, - isset($mail_headers)?$mail_headers:null, - isset($php_mail_path)?$php_mail_path:null, - isset($php_mail_mime_path)?$php_mail_mime_path:null, -); -// Initialize translation -I18n::init( - "$root_dir_path/locales", - isset($default_locale)?$default_locale:null); - -foreach($status_list as $key => $value) - $status_list[$key] = _($value); $sentry_span->finish(); # vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab diff --git a/includes/db.php b/includes/db.php index 9f9a3c0..f5d67cc 100644 --- a/includes/db.php +++ b/includes/db.php @@ -1,5 +1,6 @@ finish(); + } + + /** + * Get a specific option value + * + * @param string $key The configuration variable key + * @param mixed $default The default value to return if configuration variable + * is not set (Default : null) + * @param string $cast The type of expected value. The configuration variable + * value will be cast as this type. Could be : bool, int, + * float or string. (Optional, default : raw value) + * @param bool $split If true, $cast is 'array' and value retreived from configuration + * is a string, split the value by comma (optional, default: true) + * @return mixed The configuration variable value + **/ + public static function get_option($key, $default=null, $cast=null, $split=true) { + return Config::get( + $key, + Config::loaded()?Config::get($key, $default, $cast, $split):$default, + $cast, + $split, + self :: $options + ); + } + + /** + * Retreive application root directory path + * @return string|null + */ + public static function root_directory_path() { + return self :: $root_directory_path?self :: $root_directory_path:'.'; + } + +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..4e7efbc --- /dev/null +++ b/src/Config.php @@ -0,0 +1,209 @@ + 20) { + Log::fatal('Config::replace_variables(%s): max iteration reached'); + return $value; + } + $value = str_replace($m[0], self :: get($m[1], '', 'string'), $value); + $iteration++; + } + } + return $value; + } + + /** + * Get list of keys of a specific configuration variable + * + * @param string $key The configuration variable key + * + * @return array An array of the keys of a specific configuration variable + **/ + public static function keys($key) { + $value = self :: get($key); + return (is_array($value)?array_keys($value):array()); + } + + /** + * Set a configuration variable + * + * @param string $key The configuration variable key + * @param mixed $value The configuration variable value + * @param array|null &$config Optional configuration to use instead of current loaded configuration + * + * @return boolean + **/ + public static function set($key, $value, &$config=null) { + $exploded_key = explode('.', $key); + if (!is_array($exploded_key)) return false; + if (is_array($config)) { + $parent = &$config; + } + else { + if (!is_array(self :: $config)) + self :: $config = array(); + $parent = &self :: $config; + } + for ($i=0; $i < count($exploded_key) - 1; $i++) { + $k = $exploded_key[$i]; + if (!array_key_exists($k, $config)) + $config[$k] = array(); + $config = &$config[$k]; + } + $config[array_pop($exploded_key)] = $value; + return true; + } + +} + +# vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab diff --git a/src/Email.php b/src/Email.php index d5c2bec..ff86e5a 100644 --- a/src/Email.php +++ b/src/Email.php @@ -59,18 +59,40 @@ class Email { /** * Initialization - * @param string|null $php_mail_path PHP PEAR Mail lib path (optional, default: Mail.php) - * @param string|null $php_mail_mime_path PHP PEAR Mail lib path (optional, default: Mail/mime.php) + * @param string|null $php_mail_path PHP PEAR Mail lib path (optional, default: from + * email.php_mail_path config key if set, 'Mail.php' otherwise) + * @param string|null $php_mail_mime_path PHP PEAR Mail lib path (optional, default: from + * email.php_mail_mime_path config key if set, 'Mail/mime.php' otherwise) * @return void */ public static function init($sender=null, $send_method=null, $send_params=null, $catch_all=null, $headers=null, $php_mail_path=null, $php_mail_mime_path=null) { + if (is_null($sender)) + $sender = Config::get('email.sender', null, 'string'); if ($sender) self :: $sender = $sender; + + if (is_null($send_method)) + $send_method = Config::get('email.send_method', null, 'string'); if ($send_method) self :: $send_method = $send_method; + + if (is_null($send_params)) + $send_params = Config::get('email.send_params', null, 'array'); if ($send_params) self :: $send_params = $send_params; + + if (is_null($catch_all)) + $catch_all = Config::get('email.catch_all'); if ($catch_all) self :: $catch_all = $catch_all; + + if (is_null($headers)) + $headers = Config::get('email.headers', null, 'array'); if ($headers) self :: $headers = $headers; + + if (is_null($php_mail_path)) + $php_mail_path = Config::get('email.php_mail_path', null, 'string'); if ($php_mail_path) self :: $php_mail_path = $php_mail_path; + + if (is_null($php_mail_mime_path)) + $php_mail_mime_path = Config::get('email.php_mail_mime_path', null, 'string'); if ($php_mail_mime_path) self :: $php_mail_mime_path = $php_mail_mime_path; } diff --git a/src/I18n.php b/src/I18n.php index 66b8dfe..83b06d9 100644 --- a/src/I18n.php +++ b/src/I18n.php @@ -27,15 +27,22 @@ class I18n { * system. * * @param string|null $root_path The root directory path of translation files - * (optional, default: ./locales) + * (optional, default: from i18n.root_directory config key if set, + * '${root_directory_path}/locales' otherwise) * @param string|null $default_locale The default locale - * (optional, default: en_US.UTF8) + * (optional, default: from i18n.default_locale config key if set, + * 'en_US.UTF8' otherwise) * @return void */ public static function init($root_path=null, $default_locale=null) { - global $root_dir_path; + if (is_null($root_path)) + self :: $root_path = Config::get( + 'i18n.root_directory', '${root_directory_path}/locales', 'string'); if (!is_null($root_path)) self :: $root_path = $root_path; + + if (is_null($default_locale)) + self :: $default_locale = Config::get('i18n.default_locale', null, 'string'); if (!is_null($default_locale)) self :: $default_locale = $default_locale; @@ -50,7 +57,11 @@ class I18n { $lang = $_REQUEST['lang']; Log :: trace("Select lang from request parameter: '$lang'"); } - elseif (isset($_SESSION['lang']) && in_array($_SESSION['lang'], $available_langs) && !isset($_REQUEST['reset_lang'])) { + elseif ( + isset($_SESSION['lang']) + && in_array($_SESSION['lang'], $available_langs) + && !isset($_REQUEST['reset_lang']) + ) { $lang = $_SESSION['lang']; Log :: trace("Restore lang from session: '$lang'"); } @@ -103,8 +114,7 @@ class I18n { $js_translation_file = "translations/$lang.js"; if ( php_sapi_name() != "cli" - && isset($root_dir_path) && $root_dir_path - && is_file("$root_dir_path/public_html/$js_translation_file") + && is_file(App :: root_directory_path()."/public_html/$js_translation_file") && Tpl :: initialized() ) { Tpl :: add_js_file(array("lib/babel.js", "js/translation.js", $js_translation_file)); @@ -250,14 +260,12 @@ class I18n { * @return void */ public static function cli_extract_messages($command_args) { - global $root_dir_path; - // Store list of generated POT files $pot_files = array(); // List PHP files to parse $php_files = run_external_command( - array('find', escapeshellarg($root_dir_path), '-name', "'*.php'"), + array('find', escapeshellarg(App :: root_directory_path()), '-name', "'*.php'"), null, // no STDIN data false // do not escape command args (already done) ); @@ -284,7 +292,7 @@ class I18n { // List JS files to parse $js_files = run_external_command( - array('find', escapeshellarg("$root_dir_path/public_html/js"), '-name', "'*.js'"), + array('find', escapeshellarg(App :: root_directory_path()."/public_html/js"), '-name', "'*.js'"), null, // no STDIN data false // do not escape command args (already done) ); @@ -313,7 +321,7 @@ class I18n { // Extract messages from templates files using tsmarty2c.php $result = run_external_command( array ( - "$root_dir_path/vendor/bin/tsmarty2c.php", + App :: root_directory_path()."/vendor/bin/tsmarty2c.php", "-o", self :: $root_path."/templates-messages.pot", Tpl :: templates_directory(), ) @@ -359,8 +367,6 @@ class I18n { * @return bool */ public static function cli_update_messages($command_args) { - global $root_dir_path; - $compendium_args = array(); foreach ($command_args as $path) { if (!file_exists($path)) @@ -435,7 +441,7 @@ class I18n { return !$error; } - Log :: fatal(_("Fail to open root lang directory (%s)."), $root_dir_path); + Log :: fatal(_("Fail to open root lang directory (%s)."), App :: root_directory_path()); return false; } @@ -447,8 +453,6 @@ class I18n { * @return bool */ public static function cli_compile_messages($command_args) { - global $root_dir_path; - if ($dh = opendir(self :: $root_path)) { $error = False; while (($file = readdir($dh)) !== false) { @@ -466,7 +470,7 @@ class I18n { Log :: debug(_("Lang alias symlink found: %s -> %s"), $lang, $real_lang_dir); // Create JSON catalog symlink (if not exists) - $js_link = "$root_dir_path/public_html/translations/$lang.js"; + $js_link = App :: root_directory_path()."/public_html/translations/$lang.js"; $link_target = "$real_lang_dir.js"; if (!file_exists($js_link)) { if (symlink($link_target, $js_link)) { @@ -528,7 +532,7 @@ class I18n { // Compile messages from PO file to JSON catalog file $json_catalog = self :: po2json($lang, $po_file); - $js_file = "$root_dir_path/public_html/translations/$lang.js"; + $js_file = App :: root_directory_path()."/public_html/translations/$lang.js"; if(!$fd = fopen($js_file, 'w')) { Log :: error(_("Fail to open %s JSON catalog file in write mode (%s)."), $lang, $js_file); @@ -547,7 +551,7 @@ class I18n { return !$error; } - Log :: fatal(_("Fail to open root lang directory (%s)."), $root_dir_path); + Log :: fatal(_("Fail to open root lang directory (%s)."), App :: root_directory_path()); return false; } diff --git a/src/Log.php b/src/Log.php index 249f5c0..a463dce 100644 --- a/src/Log.php +++ b/src/Log.php @@ -55,26 +55,69 @@ class Log { // Custom fatal error handler protected static $fatal_error_handler = null; - /* + /** * Initialization - * @param string $filepath - * @param string|null $level - * @param int|null $php_errors_levels + * @param string $filepath The log file path + * (optional, default: from log.file_path or log.cli_file_path is set) + * @param string|null $level The log level + * (optional, default: from log.level config key if set, otherwise, + * see self :: $default_level) + * @param int|null $php_errors_levels PHP errors level as expected by set_error_handler() + * (optional, default: from log.php_errors_levels if set, E_ALL & ~E_STRICT + * if level is TRACE or DEBUG, and E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED + * otherwise) * @return void */ - public static function init($filepath, $level=null, $php_errors_levels=null) { - self :: $filepath = $filepath; + public static function init($filepath=null, $level=null, $php_errors_levels=null) { + if ($filepath) + self :: $filepath = $filepath; + elseif (php_sapi_name() == 'cli') + self :: $filepath = Config::get( + 'log.cli_logfile_path', Config::get('log.cli_file_path')); + else + self :: $filepath = Config::get('log.file_path'); // Set log level - self :: set_level($level); + self :: set_level($level?$level:Config::get('log.level')); // Log PHP errors - if (!is_null($php_errors_levels)) + if (!is_null($php_errors_levels)) { self :: $php_errors_levels = $php_errors_levels; - elseif (in_array(self :: $level, array('DEBUG', 'TRACE'))) + } + elseif ($levels = Config::get('log.php_errors_levels', array(), 'array')) { + $code = 'self :: $php_errors_levels = '; + while($level = array_shift($levels)) { + if (!is_string($level)) continue; + if (!defined($level) || !is_int(constant($level))) continue; + $code .= $level; + break; + } + foreach($levels as $level) { + if (!is_string($level)) continue; + $combine_operator = '&'; + if (in_array($level[0], array('|', '^', '&'))) { + $combine_operator = $level[0]; + $level = substr($level, 1); + } + $not = false; + if (in_array($level[0], array('!', '~'))) { + $not = $level[0]; + $level = substr($level, 1); + } + if (!defined($level) || !is_int(constant($level))) continue; + $code .= " $combine_operator "; + if ($not) $code .= "$not"; + $code .= $level; + } + $code .= ";"; + eval($code); + } + elseif (in_array(self :: $level, array('DEBUG', 'TRACE'))) { self :: $php_errors_levels = E_ALL & ~E_STRICT; - else + } + else { self :: $php_errors_levels = E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED; + } set_error_handler(array('EesyPHP\\Log', 'on_php_error'), self :: $php_errors_levels); // Log uncatched exceptions @@ -93,7 +136,7 @@ class Log { if (!array_key_exists($level, self :: $levels)) $level = self :: $default_level; if (self :: $levels[$level] < self :: $levels[self :: $level]) return true; - if(is_null(self :: $file_fd)) { + if(self :: $filepath && is_null(self :: $file_fd)) { self :: $file_fd = fopen(self :: $filepath, 'a'); } @@ -125,8 +168,8 @@ class Log { $msg[] = $message; $msg = implode(' - ', $msg)."\n"; } - - fwrite(self :: $file_fd, $msg); + if (self :: $file_fd) + fwrite(self :: $file_fd, $msg); if ($level == 'FATAL') if (!is_null(self :: $fatal_error_handler)) @@ -260,7 +303,7 @@ class Log { self :: $level = self :: $default_level; self :: warning( "Invalid log level value found in configuration (%s). ". - "Set as default (%s).", $level, self :: $default_level); + "Set as default (%s).", vardump($level), self :: $default_level); } else { self :: $level = $level; diff --git a/src/SentryIntegration.php b/src/SentryIntegration.php index 7f84739..8eea5eb 100644 --- a/src/SentryIntegration.php +++ b/src/SentryIntegration.php @@ -26,22 +26,23 @@ class SentryIntegration { /** * Initialization * @param string|null $dsn Sentry DSN + * (optional, default: from sentry.dsn config key if set, null otherwise) * @param float|null $traces_sample_rate Sentry traces sample rate - * (optional, default: 0.2) + * (optional, default: from sentry.traces_sample_rate config key if set, + * 0.2 otherwise) * @param array|null $php_error_types Types of PHP error to log in Sentry - * (optional, default: see self::$php_error_types) + * (optional, default: from sentry.php_error_types config key if set, + * otherwise, see self::$php_error_types) * @return void */ public static function init($dsn=null, $traces_sample_rate=null, $php_error_types=null) { - // Init Sentry (if its DSN is configured) - if (!$dsn) return; - \Sentry\init([ - 'dsn' => $dsn, + 'dsn' => $dsn?$dsn:Config::get('sentry.dsn'), 'traces_sample_rate' => ( $traces_sample_rate? - $traces_sample_rate:0.2 + $traces_sample_rate: + Config::get('sentry.traces_sample_rate', 0.2, 'float') ), ]); @@ -55,8 +56,18 @@ class SentryIntegration { ]); }); - if (is_array($php_error_types)) - self :: $php_error_types = $php_error_types; + if (!is_array($php_error_types)) + $php_error_types = Config::get( + 'sentry.php_error_types', self :: $php_error_types, 'array' + ); + self :: $php_error_types = array(); + foreach($php_error_types as $php_error_type) { + if (is_string($php_error_type) && defined($php_error_type)) + $php_error_type = constant($php_error_type); + if (!is_int($php_error_type)) continue; + if (in_array($php_error_type, self :: $php_error_types)) continue; + self :: $php_error_types[] = $php_error_type; + } set_error_handler( array('EesyPHP\\SentryIntegration', 'on_php_error'), E_ALL diff --git a/src/Session.php b/src/Session.php index d8cc5c1..026e7d0 100644 --- a/src/Session.php +++ b/src/Session.php @@ -16,9 +16,9 @@ class Session { /** * Initialization * @param int|null $max_duration Session max duration in second - * (optional, default: 12h) + * (optional, default: from session.max_duration config key if set, 12h otherwise) * @param int|null $timeout Session inactivity timeout in second - * (optional, default: no timeout) + * (optional, default: from session.timeout config key if set, no timeout otherwise) * @return void */ public static function init($max_duration=null, $timeout=null) { @@ -26,6 +26,8 @@ class Session { return; // Define session max duration + if (is_null($max_duration)) + $max_duration = Config::get('session.max_duration', null, 'int'); if (is_int($max_duration)) self :: $max_duration = $max_duration; @@ -41,7 +43,9 @@ class Session { } // Handle session timeout - if ($timeout) { + if (is_null($timeout)) + $timeout = Config::get('session.timeout', null, 'int'); + if (is_int($timeout) && $timeout) { if (!isset($_SESSION['session_last_access'])) { Log :: debug('Set initial session last access'); $_SESSION['session_last_access'] = time(); diff --git a/src/Tpl.php b/src/Tpl.php index 73b3c1b..26f97e0 100644 --- a/src/Tpl.php +++ b/src/Tpl.php @@ -53,13 +53,20 @@ class Tpl { /** * 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: false) + * (optional, default: from template.debug_ajax or debug_ajax config keys if set, + * false otherwise) * @return void */ - public static function init($templates_dir, $templates_c_dir, $debug_ajax=false) { + public static function init($templates_dir=null, $templates_c_dir=null, $debug_ajax=null) { // Check templates/templates_c directories + if (is_null($templates_dir)) + $templates_dir = Config::get('template.directory', null, 'string'); + if (is_null($templates_c_dir)) + $templates_c_dir = Config::get('template.cache_directory', null, 'string'); if (!$templates_dir || !is_dir($templates_dir)) { Log :: fatal( "Template directory not found (%s)", @@ -75,6 +82,8 @@ class Tpl { self :: $smarty = new Smarty(); self :: $smarty->setTemplateDir($templates_dir); self :: $smarty->setCompileDir($templates_c_dir); + if (is_null($debug_ajax)) + $debug_ajax = Config::get('template.debug_ajax', Config::get('debug_ajax')); self :: $_debug_ajax = boolval($debug_ajax); Log :: register_fatal_error_handler(array('\\EesyPHP\\Tpl', 'fatal_error')); } diff --git a/src/Url.php b/src/Url.php index e806a2d..5161959 100644 --- a/src/Url.php +++ b/src/Url.php @@ -51,11 +51,14 @@ class Url { /** * 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 ($public_root_url) { + public static function init($public_root_url=null, $api_mode=false) { + if (is_null($public_root_url)) + $public_root_url = Config::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); diff --git a/src/functions.php b/src/functions.php index 7d186eb..68eb2e7 100644 --- a/src/functions.php +++ b/src/functions.php @@ -49,8 +49,13 @@ function format_callable($callable) { return vardump($callable); } -function check_is_empty($val) { - switch(gettype($val)) { +/** + * Check if given value is empty + * @param mixed $value + * @return bool + */ +function check_is_empty($value) { + switch(gettype($value)) { case "boolean": case "integer": case "double": @@ -59,11 +64,57 @@ function check_is_empty($val) { return False; case "array": case "string": - if ($val == "0") return false; - return empty($val); + if ($value == "0") return false; + return empty($value); case "NULL": return True; } + return empty($value); +} + +/** + * Ensure the given value is an array and return an array with this value if not + * @param mixed $value + * @return array + */ +function ensure_is_array($value) { + if (is_array($value)) + return $value; + if (check_is_empty($value)) + return array(); + return array($value); +} + +/** + * Get a specific configuration variable value + * + * @param mixed $value The value to cast + * @param string $type The type of expected value. The configuration variable + * value will be cast as this type. Could be : bool, int, + * float or string. + * @param bool $split If true, $type=='array' and $value is a string, split + * the value by comma (optional, default: false) + * @return mixed The cast value + **/ +function cast($value, $type, $split=false) { + switch($type) { + case 'bool': + case 'boolean': + return boolval($value); + case 'int': + case 'integer': + return intval($value); + case 'float': + return floatval($value); + case 'str': + case 'string': + return strval($value); + case 'array': + if ($split && is_string($value)) + $value = preg_split('/ *, */', $value); + return ensure_is_array($value); + } + return $value; } /*