diff --git a/bin/eesyphp b/bin/eesyphp index 57f7316..cea5a5a 100755 --- a/bin/eesyphp +++ b/bin/eesyphp @@ -5,4 +5,4 @@ require realpath(dirname(__FILE__).'/..')."/includes/core.php"; if (is_callable('handle_cli_args')) handle_cli_args(); else - logging('FATAL', "An error occured initializing CLI : handle_cli_args() function not found."); + Log :: fatal("An error occured initializing CLI : handle_cli_args() function not found."); diff --git a/composer.json b/composer.json index acd05e5..ec2dec3 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,14 @@ "email": "info@easter-eggs.com" } ], + "autoload": { + "psr-4": { + "EesyPHP\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, "require": { "envms/fluentpdo": "^1.1", "pear/console_table": "^1.3", diff --git a/includes/cli.php b/includes/cli.php index 8ada725..bfad4ed 100644 --- a/includes/cli.php +++ b/includes/cli.php @@ -1,16 +1,19 @@ = 1?$command_args[0]:'php://output'), 'w'); export_items($fd); fclose($fd); - logging('INFO', "Items export to '".(count($command_args) >= 1?$command_args[0]:'STDOUT')."'."); + Log :: info("Items export to '".(count($command_args) >= 1?$command_args[0]:'STDOUT')."'."); } add_cli_command( 'export', @@ -331,8 +334,8 @@ function cli_restore($command_args) { $fd = fopen((count($command_args) >= 1?$command_args[0]:'php://stdin'), 'r'); restore_items($fd); fclose($fd); - logging( - 'INFO', "Items restored from '%s'", + Log :: info( + "Items restored from '%s'", (count($command_args) >= 1?$command_args[0]:'STDIN') ); } @@ -354,7 +357,7 @@ function cli_cron($command_args) { case '-m': case '--max-age': $i++; - if(!check_id($command_args[$i])) + if(!Check :: id($command_args[$i])) usage('Invalid -m|--max-age clause'); $item_max_age = $command_args[$i]; break; @@ -368,37 +371,36 @@ function cli_cron($command_args) { } if (!is_int($item_max_age) || $item_max_age <= 0) - logging( - 'FATAL', + Log :: fatal( 'Invalid $item_max_age value set in configuration: it\'s must be a positive integer.'); - logging('DEBUG', "cli_cron(): item max age = $item_max_age day(s)"); + Log :: debug("cli_cron(): item max age = $item_max_age day(s)"); $limit = time() - ($item_max_age * 86400); - logging('DEBUG', "Handle items expiration with creation date limit ".format_time($limit)."."); + Log :: debug("Handle items expiration with creation date limit ".format_time($limit)."."); $items = search_items(array('all' => true)); $error = false; foreach($items['items'] as $item) { if ($item['date'] < $limit) { if ($just_try) { - logging('DEBUG', 'Just-try mode: do not really delete item #%s (%s, creation date: %s)', + Log :: debug('Just-try mode: do not really delete item #%s (%s, creation date: %s)', $item['id'], $item['name'], format_time($item['date']) ); } else if (delete_item($item['id'])) { - logging('INFO', 'Item #%s (%s) deleted (creation date: %s)', + Log :: info('Item #%s (%s) deleted (creation date: %s)', $item['id'], $item['name'], format_time($item['date']) ); } else { - logging('ERROR', 'Fail to delete item "%s" (%s, creation date: %s)', + Log :: error('Fail to delete item "%s" (%s, creation date: %s)', $item['id'], $item['name'], format_time($item['date']) ); $error = true; } } else { - logging('DEBUG', 'Item "%s" (%s) still valid (creation date: %s)', + Log :: debug('Item "%s" (%s) still valid (creation date: %s)', $item['id'], $item['name'], format_time($item['date']) ); } diff --git a/includes/core.php b/includes/core.php index b0c77df..70affbb 100644 --- a/includes/core.php +++ b/includes/core.php @@ -1,5 +1,7 @@ getMessage()); - logging("FATAL", _('Unable to connect to the database.')); + Log :: error("Fail to connect to DB (DSN : '$db_dsn') : ".$e->getMessage()); + Log :: fatal(_('Unable to connect to the database.')); } /* @@ -111,7 +112,7 @@ function get_items($orderby='id', $raw_values=false) { } } catch (Exception $e) { - logging('ERROR', "Error retreiving items info from database : ".$e->getMessage()); + Log :: error("Error retreiving items info from database : ".$e->getMessage()); } return false; } @@ -134,7 +135,7 @@ function get_item($id, $raw_values=false) { } } catch (Exception $e) { - logging('ERROR', "Error retreiving item #$id info from database : ".$e->getMessage()); + Log :: error("Error retreiving item #$id info from database : ".$e->getMessage()); } return false; } @@ -149,13 +150,13 @@ function add_item($values) { if ($result !== false) { $item = get_item($result); - logging('INFO', "New item #$result added"); + Log :: info("New item #$result added"); trigger_hook('item_added', $item); return $item; } } catch (Exception $e) { - logging('ERROR', "Error creating item in database : ".$e->getMessage()); + Log :: error("Error creating item in database : ".$e->getMessage()); } return false; } @@ -180,20 +181,20 @@ function update_item($id, $changes) { -> execute(); if ($result !== false) { - logging('INFO', "Item #$id updated"); + Log :: info("Item #$id updated"); trigger_hook('item_updated', $item); return true; } } catch (Exception $e) { - logging('ERROR', "Error updating item #$id in database : ".$e->getMessage()); + Log :: error("Error updating item #$id in database : ".$e->getMessage()); } return false; } function change_item_status($id, $status) { if (update_item($id, array('status' => $status))) { - logging('INFO', "Status of item #$id changed to $status."); + Log :: info("Status of item #$id changed to $status."); return true; } return false; @@ -211,12 +212,12 @@ function delete_item($id) { -> execute(); if ($result !== false) { - logging('INFO', "Item #$id deleted"); + Log :: info("Item #$id deleted"); return True; } } catch (Exception $e) { - logging('ERROR', "Error deleting item #$id from database : ".$e->getMessage()); + Log :: error("Error deleting item #$id from database : ".$e->getMessage()); } return false; } @@ -308,7 +309,7 @@ function search_items($params) { -> execute(); if ($result === false) { - logging('ERROR', 'search_items() : search in DB return false'); + Log :: error('search_items() : search in DB return false'); return false; } @@ -344,7 +345,7 @@ function search_items($params) { $result_count = $query_count -> execute(); if ($result_count === false) { - logging('DEBUG', 'search_items() : search for count in DB return false'); + Log :: debug('search_items() : search for count in DB return false'); return False; } $count = $result_count -> fetch(); @@ -360,7 +361,7 @@ function search_items($params) { ); } catch (Exception $e) { - log_exception( + Log :: exception( $e, "An exception occured searching items with params %s infos from database : ", preg_replace("/\n[ \t]*/", " ", print_r($params, true)) ); @@ -403,12 +404,12 @@ function restore_items($fd=null) { $result = $fpdo -> deleteFrom('item') -> execute(); if ($result === false) { - logging('ERROR', "An unknown error occured truncating item table in database."); + Log :: error("An unknown error occured truncating item table in database."); return false; } } catch (Exception $e) { - logging('ERROR', "Error truncating item table in database : ".$e->getMessage()); + Log :: error("Error truncating item table in database : ".$e->getMessage()); return false; } @@ -450,18 +451,17 @@ function restore_items($fd=null) { $restored++; } else { - logging('ERROR', "Unkwown error occured restoring item from line #$line :\n".print_r($values, true)); + Log :: error("Unkwown error occured restoring item from line #$line :\n".print_r($values, true)); $error = true; } } catch (Exception $e) { - logging( - 'ERROR', + Log :: error( "Error restoring item from line #$line : ".$e->getMessage()."\n".print_r($values, true)); $error = true; } } - logging('INFO', "$restored items restored"); + Log :: info("$restored items restored"); // Trigger hooks trigger_hook('items_restored'); diff --git a/includes/functions.php b/includes/functions.php index 1056df1..2a86c88 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1,85 +1,16 @@ = 1577833200); // 2020-01-01 - date of birth of this soft -} - function check_status($status) { global $status_list; return array_key_exists($status, $status_list); } -function check_description($comment) { - if (preg_match("/^[\p{L}0-9\p{P}\p{Zs}\p{Zl}\p{Sc}\=\+]+$/uim", $comment)) - return true; - return false; -} - -function check_email($value, $domain=NULL, $checkDns=true) { - $regex = '/^((\"[^\"\f\n\r\t\v\b]+\")|([\w\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]+(\.[\w\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]+)*))@((\[(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))\])|(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))|((([A-Za-z0-9\-])+\.)+[A-Za-z\-]+))$/'; - - if (!preg_match($regex, $value)) { - return false; - } - - $nd = explode('@', $value); - $nd=$nd[1]; - - if ($domain) { - if(is_array($domain)) { - if (!in_array($nd,$domain)) { - return false; - } - } - else { - if($nd!=$domain) { - return false; - } - } - } - - if ($checkDns && function_exists('checkdnsrr')) { - if (!(checkdnsrr($nd, 'MX') || checkdnsrr($nd, 'A'))) { - return false; - } - } - - return true; -} - /* * Handling item POST data */ @@ -87,11 +18,11 @@ function handle_item_post_data(&$info, $enabled_fields=null, $required_fields=nu &$changes=null) { $field_errors=array(); if (isset($_POST['submit'])) { - logging('DEBUG', 'POST data : '.vardump($_POST)); + Log :: debug('POST data : '.vardump($_POST)); // Name if (!$enabled_fields || in_array('name', $enabled_fields)) { if (isset($_POST['name'])) { - if (check_name($_POST['name'])) { + if (Check :: name($_POST['name'])) { $info['name'] = $_POST['name']; } else { @@ -118,10 +49,10 @@ function handle_item_post_data(&$info, $enabled_fields=null, $required_fields=nu isset($_POST['description']) && (!$enabled_fields || in_array('description', $enabled_fields)) ) { - if (check_is_empty(trim($_POST['description']))) { + if (Check :: is_empty(trim($_POST['description']))) { $info['description'] = null; } - else if (check_description($_POST['description'])) { + else if (Check :: description($_POST['description'])) { $info['description'] = $_POST['description']; } else { @@ -135,7 +66,7 @@ function handle_item_post_data(&$info, $enabled_fields=null, $required_fields=nu foreach ($required_fields as $field) { if (array_key_exists($field, $field_errors)) continue; - if (array_key_exists($field, $info) && !is_null($info[$field]) && !check_is_empty($info)) + if (array_key_exists($field, $info) && !is_null($info[$field]) && !Check :: is_empty($info)) continue; $field_errors[$field] = "Cette information est obligatoire."; } @@ -251,23 +182,6 @@ function format_callable($callable) { return vardump($callable); } -function check_is_empty($val) { - switch(gettype($val)) { - case "boolean": - case "integer": - case "double": - case "object": - case "resource": - return False; - case "array": - case "string": - if ($val == "0") return false; - return empty($val); - case "NULL": - return True; - } -} - /* * Generic file/directory helpers */ @@ -308,18 +222,18 @@ function delete_directory($dir, $recursive=true) { foreach ($files as $file) { if (is_dir("$dir/$file")) { if (!delete_directory("$dir/$file", true)) { - logging('ERROR', "delete_directory($dir) : Fail to delete sub-directory '$dir/$file'."); + Log :: error("delete_directory($dir) : Fail to delete sub-directory '$dir/$file'."); return false; } } else if (!unlink("$dir/$file")) { - logging('ERROR', "delete_directory($dir) : Fail to delete '$dir/$file'."); + Log :: error("delete_directory($dir) : Fail to delete '$dir/$file'."); return false; } } } else if (!empty($files)) { - logging('ERROR', "delete_directory($dir) : Directory is not empty."); + Log :: error("delete_directory($dir) : Directory is not empty."); return false; } return rmdir($dir); @@ -345,7 +259,7 @@ function run_external_command($command, $data_stdin=null, $escape_command_args=t $command = implode(' ', $command); if ($escape_command_args) $command = escapeshellcmd($command); - logging('DEBUG', "Run external command: '$command'"); + Log :: debug("Run external command: '$command'"); $descriptorspec = array( 0 => array("pipe", "r"), // stdin 1 => array("pipe", "w"), // stdout @@ -354,7 +268,7 @@ function run_external_command($command, $data_stdin=null, $escape_command_args=t $process = proc_open($command, $descriptorspec, $pipes); if (!is_resource($process)) { - logging('ERROR', "Fail to run external command: '$command'"); + Log :: error("Fail to run external command: '$command'"); return false; } @@ -372,7 +286,7 @@ function run_external_command($command, $data_stdin=null, $escape_command_args=t $return_value = proc_close($process); $error = (!empty($stderr) || $return_value != 0); - logging( + Log :: log( ($error?'ERROR':'DEBUG'), "External command ".($error?"error":"result").":\n". "\tCommand : $command\n". diff --git a/includes/hooks.php b/includes/hooks.php index 1ac062c..26ed509 100644 --- a/includes/hooks.php +++ b/includes/hooks.php @@ -1,5 +1,7 @@ 0, - 'DEBUG' => 1, - 'INFO' => 2, - 'WARNING' => 3, - 'ERROR' => 4, - 'FATAL' => 5, -); - -// Custom fatal error handler -$_fatal_error_handler = null; - -/** - * Log a message - * @param string $level The message level (key of $_log_levels) - * @param string $message The message to log - * @return true - */ -function logging($level, $message) { - global $log_file, $_log_file_fd, $_log_levels, $log_level, $argv, - $_fatal_error_handler, $auth_user; - - if (!array_key_exists($level, $_log_levels)) $level = 'INFO'; - $level_id = $_log_levels[$level]; - - $log_level_id = $_log_levels[$log_level]; - - if ($level_id < $log_level_id) return true; - if(is_null($_log_file_fd)) { - $_log_file_fd = fopen($log_file, 'a'); - } - - // If more than 2 arguments passed, format message using sprintf - if (func_num_args() > 2) { - $message = call_user_func_array( - 'sprintf', - array_merge(array($message), array_slice(func_get_args(), 2)) - ); - } - - if (php_sapi_name() == "cli") { - $msg = implode(' - ', array( - date('Y/m/d H:i:s'), - basename($argv[0]), - $level, - $message - ))."\n"; - } - else { - $msg = array( - date('Y/m/d H:i:s'), - $_SERVER['REQUEST_URI'], - $_SERVER['REMOTE_ADDR'], - ); - if (isset($auth_user)) - $msg[] = ($auth_user['username']?$auth_user['username']:'anonymous'); - $msg[] = $level; - $msg[] = $message; - $msg = implode(' - ', $msg)."\n"; - } - - fwrite($_log_file_fd, $msg); - - if ($level == 'FATAL') - if (!is_null($_fatal_error_handler)) - call_user_func($_fatal_error_handler, $message); - elseif (function_exists('fatal_error')) - fatal_error($message); - else - die("\n$message\n\n"); - elseif (php_sapi_name() == "cli") - echo $msg; - - return true; -} - -// Set default log level (if not defined or invalid) -$default_log_level = 'WARNING'; -if (!isset($log_level)) { - $log_level = $default_log_level; -} -elseif (!array_key_exists($log_level, $_log_levels)) { - $invalid_value = $log_level; - $log_level = $default_log_level; - logging( - $log_level, "Invalid log level value found in configuration (%s). ". - "Set as default (%s).", $invalid_value, $log_level); -} - -/** - * Register a contextual fatal error handler - * @param null|callable $handler The fatal error handler (set as null to reset) - * @return void - */ -function register_fatal_error_handler($handler) { - // @phpstan-ignore-next-line - if ($handler && !is_callable($handler)) - logging('FATAL', 'Fatal handler provided is not callable !'); - global $_fatal_error_handler; - $_fatal_error_handler = ($handler?$handler:null); -} - - -/** - * Change of current log file - * @param string $file The new log file path - * @return bool - */ -function change_log_file($file) { - global $log_file, $_log_file_fd; - if ($file == $log_file) return True; - if ($_log_file_fd) { - fclose($_log_file_fd); - $_log_file_fd = false; - } - $log_file = $file; - return True; -} - -/* - ******************************************************************************* - * Handle exception logging - ******************************************************************************* - */ - -/** - * Get the current backtrace - * @param int $ignore_last The number of last levels to ignore - * @return string - */ -function get_debug_backtrace_context($ignore_last=0) { - $traces = debug_backtrace(); - - // Also ignore this function it self - $ignore_last++; - - if (!is_array($traces) || count($traces) <= $ignore_last) - return ""; - - $msg = array(); - for ($i=$ignore_last; $i < count($traces); $i++) { - $trace = array("#$i"); - if (isset($traces[$i]['file'])) - $trace[] = $traces[$i]['file'].(isset($traces[$i]['line'])?":".$traces[$i]['line']:""); - // @phpstan-ignore-next-line - if (isset($traces[$i]['class']) && isset($traces[$i]['function'])) - $trace[] = implode(" ", array( - $traces[$i]['class'], - $traces[$i]['type'], - $traces[$i]['function']. "()")); - elseif (isset($traces[$i]['function'])) - $trace[] = $traces[$i]['function']. "()"; - $msg[] = implode(" - ", $trace); - } - - return implode("\n", $msg); -} - -/** - * Log an exception - * @param Throwable $exception - * @param string|null $prefix The prefix of the log message - * (optional, default: "An exception occured") - * @return void - */ -function log_exception($exception, $prefix=null) { - if (function_exists('log_in_sentry')) - log_in_sentry($exception); - // If more than 2 arguments passed, format prefix message using sprintf - if ($prefix && func_num_args() > 2) { - $prefix = call_user_func_array( - 'sprintf', - array_merge(array($prefix), array_slice(func_get_args(), 2)) - ); - } - logging( - "ERROR", "%s:\n%s\n## %s:%d : %s", - ($prefix?$prefix:"An exception occured"), - get_debug_backtrace_context(1), - $exception->getFile(), $exception->getLine(), - $exception->getMessage()); -} -set_exception_handler('log_exception'); - -/* - ******************************************************************************* - * Handle PHP error logging - ******************************************************************************* - */ - -/** - * Convert PHP error number to the corresponding label - * @param int $errno - * @return string - */ -function errno2type($errno) { - $constants = get_defined_constants(); - if (is_array($constants)) - foreach($constants as $label => $value) - if ($value == $errno && preg_match('/^E_(.*)$/', $label, $m)) - return $m[1]; - return 'UNKNOWN ERROR #'.$errno; -} - -/** - * Log a PHP error - * Note: method design to be used as callable by set_error_handler() - * @param int $errno The error number - * @param string $errstr The error message - * @param string $errfile The filename that the error was raised in - * @param int $errline The line number where the error was raised - * @return false Return false to let the normal error handler continues. - */ -function log_php_error($errno, $errstr, $errfile, $errline) { - $msg = sprintf( - "A PHP error occured : [%s] %s\nFile : %s (line : %d)", - errno2type($errno), $errstr, $errfile, $errline - ); - logging("ERROR", $msg); - if (function_exists('log_php_error_in_sentry')) - log_php_error_in_sentry($errno, $msg); - return False; -} -if (isset($log_php_errors_levels)) - set_error_handler('log_php_error', $log_php_errors_levels); -elseif (in_array($log_level, array('DEBUG', 'TRACE'))) - set_error_handler('log_php_error', E_ALL & ~E_STRICT); -else - set_error_handler('log_php_error', E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED); - -# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab diff --git a/includes/mail.php b/includes/mail.php index fd6c405..ebced4b 100644 --- a/includes/mail.php +++ b/includes/mail.php @@ -1,5 +1,7 @@ getMessage(); - logging('ERROR', $msg); + Log :: error($msg); return false; } return true; diff --git a/includes/sentry.php b/includes/sentry.php index b0c3fc6..08df89f 100644 --- a/includes/sentry.php +++ b/includes/sentry.php @@ -1,5 +1,7 @@ $sentry_dsn, 'traces_sample_rate' => ( - isset($sentry_traces_sample_rate) ? + isset($sentry_traces_sample_rate) ? $sentry_traces_sample_rate : 0.2 ), ]); @@ -35,21 +37,21 @@ if (isset($sentry_dsn) && $sentry_dsn) { /** * Log an exception or a message in Sentry - * @param string|Exception $msg + * @param string|Throwable $msg * @return void */ function log_in_sentry($msg) { global $sentry_dsn; if (!isset($sentry_dsn) || !$sentry_dsn) { - logging('TRACE', 'Sentry DSN not configured, do not log this error'); + Log :: trace('Sentry DSN not configured, do not log this error'); return; } if (is_string($msg)) { - logging('DEBUG', 'Error logged in Sentry'); + Log :: debug('Error logged in Sentry'); \Sentry\captureMessage($msg); } elseif ($msg instanceof Exception) { - logging('DEBUG', 'Exception logged in Sentry'); + Log :: debug('Exception logged in Sentry'); \Sentry\captureException($msg); } } diff --git a/includes/session.php b/includes/session.php index 8a1812e..4c359e1 100644 --- a/includes/session.php +++ b/includes/session.php @@ -1,4 +1,7 @@ (time() - $session_timeout)) { - logging( - 'DEBUG', + Log :: debug( 'Session timeout not expired, update session last access '. '(Previous value : '.$_SESSION['session_last_access'].')'); $_SESSION['session_last_access'] = time(); } else { - logging('INFO', 'Session destroyed due to inactivity'); + Log :: info('Session destroyed due to inactivity'); session_destroy(); } } diff --git a/includes/smarty.php b/includes/smarty.php index fc37919..7a97fcd 100644 --- a/includes/smarty.php +++ b/includes/smarty.php @@ -1,5 +1,7 @@ setTemplateDir($smarty_templates_dir); @@ -76,8 +78,8 @@ if ( || !is_dir($smarty_templates_c_dir) || !is_writable($smarty_templates_c_dir) ) - logging( - 'FATAL', "Template cache directory not found or not writable (%s)", + Log :: fatal( + "Template cache directory not found or not writable (%s)", isset($smarty_templates_c_dir)?$smarty_templates_c_dir:'not set'); else $smarty->setCompileDir($smarty_templates_c_dir); @@ -179,7 +181,7 @@ function _defineCommonTemplateVariables($template, $pagetitle) { function display_template($template, $pagetitle=false) { if (!$template) - logging("FATAL", _("No template specified.")); + Log :: fatal(_("No template specified.")); // If refresh parameter is present, remove it and redirect if (isset($_GET['refresh'])) { @@ -208,9 +210,9 @@ function display_template($template, $pagetitle=false) { unset($_SESSION['messages']); } catch (Exception $e) { - log_exception($e, "Smarty - An exception occured displaying template '$template'"); + Log :: exception($e, "Smarty - An exception occured displaying template '$template'"); if ($template != 'fatal_error.tpl') - logging("FATAL", _("An error occurred while displaying this page.")); + Log :: fatal(_("An error occurred while displaying this page.")); } $sentry_span->finish(); @@ -234,7 +236,7 @@ function display_ajax_return($data=null, $pretty=false) { unset($_SESSION['errors']); } if ($debug_ajax) - logging('DEBUG',"Ajax Response : ".vardump($data)); + Log :: debug("Ajax Response : ".vardump($data)); header('Content-Type: application/json'); echo json_encode($data, (($pretty||isset($_REQUEST['pretty']))?JSON_PRETTY_PRINT:0)); exit(); diff --git a/includes/translation-cli.php b/includes/translation-cli.php index 796b09b..b9f2daf 100644 --- a/includes/translation-cli.php +++ b/includes/translation-cli.php @@ -1,5 +1,7 @@ %s"), $lang, $real_lang_dir); + 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"; $link_target = "$real_lang_dir.js"; if (!file_exists($js_link)) { if (symlink($link_target, $js_link)) { - logging('INFO', _("JSON catalog symlink for %s -> %s created (%s)"), + Log :: info(_("JSON catalog symlink for %s -> %s created (%s)"), $lang, $real_lang_dir, $js_link); } else { - logging('ERROR', _("Fail to create JSON catalog symlink for %s -> %s (%s)"), + Log :: error(_("Fail to create JSON catalog symlink for %s -> %s (%s)"), $lang, $real_lang_dir, $js_link); $error = True; } } elseif (readlink($js_link) == $link_target) { - logging('DEBUG', _("JSON catalog symlink for %s -> %s already exist (%s)"), + Log :: debug(_("JSON catalog symlink for %s -> %s already exist (%s)"), $lang, $real_lang_dir, $js_link); } else { - logging( - 'WARNING', + Log :: warning( _("JSON catalog file for %s already exist, but it's not a symlink to %s (%s)"), $lang, $real_lang_dir, $js_link ); @@ -299,13 +299,13 @@ function cli_compile_messages($command_args) { continue; } - logging('DEBUG', _("Lang directory '%s' found"), $file); + Log :: debug(_("Lang directory '%s' found"), $file); // Check LC_MESSAGES directory exists $lang = $file; $lang_dir = $root_lang_dir . '/' . $file . '/LC_MESSAGES' ; if (!is_dir($lang_dir)) { - logging('DEBUG', _("LC_MESSAGES directory not found in lang '%s' directory, ignore it."), + Log :: debug(_("LC_MESSAGES directory not found in lang '%s' directory, ignore it."), $lang); continue; } @@ -313,7 +313,7 @@ function cli_compile_messages($command_args) { // Test .PO file is present $po_file = $lang_dir . '/' . TEXT_DOMAIN . '.po'; if (!is_file($po_file)) { - logging('DEBUG', _("PO file not found in lang '%s' directory, ignore it."), + Log :: debug(_("PO file not found in lang '%s' directory, ignore it."), $lang); continue; } @@ -325,8 +325,7 @@ function cli_compile_messages($command_args) { array("msgfmt", "-o", $mo_file, $po_file) ); if (!is_array($result) || $result[0] != 0) { - logging( - 'ERROR', + Log :: error( _("Fail to compile messages from %s PO file as MO file using msgfmt (%s)."), $lang, $po_file ); @@ -337,24 +336,24 @@ function cli_compile_messages($command_args) { $json_catalog = po2json($lang, $po_file); $js_file = "$root_dir_path/public_html/translations/$lang.js"; if(!$fd = fopen($js_file, 'w')) { - logging('ERROR', _("Fail to open %s JSON catalog file in write mode (%s)."), + Log :: error(_("Fail to open %s JSON catalog file in write mode (%s)."), $lang, $js_file); $error = True; } elseif (fwrite($fd, sprintf("translations_data = %s;", $json_catalog)) === false) { - logging('ERROR', _("Fail to write %s JSON catalog in file (%s)."), + Log :: error(_("Fail to write %s JSON catalog in file (%s)."), $lang, $js_file); $error = True; } else { - logging('INFO', _("%s JSON catalog writed (%s)."), $lang, $js_file); + Log :: info(_("%s JSON catalog writed (%s)."), $lang, $js_file); } } closedir($dh); return !$error; } - logging('FATAL', _("Fail to open root lang directory (%s)."), $root_dir_path); + Log :: fatal(_("Fail to open root lang directory (%s)."), $root_dir_path); return false; } add_cli_command( diff --git a/includes/translation.php b/includes/translation.php index 93043d9..1f4d381 100644 --- a/includes/translation.php +++ b/includes/translation.php @@ -1,5 +1,7 @@ current_url); } - logging('DEBUG', 'Request params : '.vardump($_REQUEST)); + Log :: debug('Request params : '.vardump($_REQUEST)); $status_list['all'] = _('Any'); if (isset($_REQUEST['status'])) { @@ -39,7 +44,7 @@ function handle_search($request) { if (isset($_REQUEST['pattern'])) { if (trim($_REQUEST['pattern']) == '') $_SESSION['search']['pattern'] = false; - else if (check_search_pattern($_REQUEST['pattern'])) + else if (Check :: search_pattern($_REQUEST['pattern'])) $_SESSION['search']['pattern'] = $_REQUEST['pattern']; else $smarty -> assign('pattern_error', true); @@ -82,7 +87,7 @@ function handle_search($request) { $_SESSION['search']['nb_by_page']=$nbs_by_page[0]; } - logging('DEBUG', 'Search params : '.vardump($_SESSION['search'])); + Log :: debug('Search params : '.vardump($_SESSION['search'])); $result = search_items($_SESSION['search']); if (!is_array($result)) fatal_error( @@ -146,8 +151,8 @@ function handle_create($request) { add_error(_("An error occurred while saving this item.")); } } - logging('DEBUG', 'Validated data : '.vardump($info)); - logging('DEBUG', 'Fields errors : '.vardump($field_errors)); + Log :: debug('Validated data : '.vardump($info)); + Log :: debug('Fields errors : '.vardump($field_errors)); if (isset($_POST['submit']) && !empty($field_errors)) add_error( _("There are errors preventing this item from being saved. ". @@ -178,7 +183,7 @@ function handle_modify($request) { if ($value != $item[$key]) $changes[$key] = $value; } - logging('DEBUG', 'Changes : '.vardump($changes)); + 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']); @@ -191,8 +196,8 @@ function handle_modify($request) { add_error(_("An error occurred while updating this item.")); } } - logging('DEBUG', 'Validated data : '.vardump($info)); - logging('DEBUG', 'Fields errors : '.vardump($field_errors)); + Log :: debug('Validated data : '.vardump($info)); + Log :: debug('Fields errors : '.vardump($field_errors)); $smarty->assign('submited', isset($_POST['submit'])); if (isset($_POST['submit']) && !empty($field_errors)) add_error( diff --git a/includes/url.php b/includes/url.php index 0dfe1d0..e124be3 100644 --- a/includes/url.php +++ b/includes/url.php @@ -1,5 +1,10 @@ Redirect to : <$url>"); + Log :: debug("redirect($go) => Redirect to : <$url>"); header("Location: $url"); exit(); } @@ -355,10 +354,10 @@ function handle_request($default_url=null) { $request = get_request($default_url); if (!is_callable($request -> handler)) { - logging( - 'ERROR', "URL handler function %s does not exists !", + Log :: error( + "URL handler function %s does not exists !", format_callable($request -> handler)); - logging('FATAL', _("This request cannot be processed.")); + Log :: fatal(_("This request cannot be processed.")); } if ($request -> api_mode) @@ -371,15 +370,15 @@ function handle_request($default_url=null) { if (function_exists('force_authentication')) force_authentication(); else - logging('FATAL', _("Authentication required but force_authentication function is not defined.")); + Log :: fatal(_("Authentication required but force_authentication function is not defined.")); try { call_user_func($request -> handler, $request); } catch (Exception $e) { - log_exception( + Log :: exception( $e, "An exception occured running URL handler function ".$request -> handler."()"); - logging('FATAL', _("This request could not be processed correctly.")); + Log :: fatal(_("This request could not be processed correctly.")); } $sentry_span->finish(); } @@ -417,7 +416,7 @@ function check_ajax_request($session_key=null) { fatal_error('Invalid request'); if ($debug_ajax) - logging('DEBUG',"Ajax Request : ".vardump($_REQUEST)); + Log :: debug("Ajax Request : ".vardump($_REQUEST)); } /** @@ -432,22 +431,20 @@ function get_absolute_url($relative_url=null) { if (!is_string($relative_url)) $relative_url = get_current_url(); if ($public_root_url[0] == '/') { - logging( - 'DEBUG', + 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); - logging( - 'DEBUG', + 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"; - logging('DEBUG', "URL :: get_absolute_url($relative_url): result = $url"); + Log :: debug("URL :: get_absolute_url($relative_url): result = $url"); return $url; } @@ -539,7 +536,9 @@ class UrlRequest { return urldecode($this->url_params[$key]); } // Unknown key, log warning - logging('WARNING', "__get($key): invalid property requested\n".get_debug_backtrace_context()); + Log :: warning( + "__get($key): invalid property requested\n%s", + Log :: get_debug_backtrace_context()); } /** diff --git a/phpstan.neon b/phpstan.neon index dc7379c..5f76736 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,7 @@ parameters: level: 5 paths: + - src - includes - public_html - bin diff --git a/src/Check.php b/src/Check.php new file mode 100644 index 0000000..73374c8 --- /dev/null +++ b/src/Check.php @@ -0,0 +1,97 @@ += 1577833200); // 2020-01-01 - date of birth of this soft + } + + public static function description($comment) { + if (preg_match("/^[\p{L}0-9\p{P}\p{Zs}\p{Zl}\p{Sc}\=\+]+$/uim", $comment)) + return true; + return false; + } + + public static function email($value, $domain=NULL, $checkDns=true) { + $regex = '/^((\"[^\"\f\n\r\t\v\b]+\")|([\w\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]+(\.[\w\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]+)*))@((\[(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))\])|(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))|((([A-Za-z0-9\-])+\.)+[A-Za-z\-]+))$/'; + + if (!preg_match($regex, $value)) { + return false; + } + + $nd = explode('@', $value); + $nd=$nd[1]; + + if ($domain) { + if(is_array($domain)) { + if (!in_array($nd,$domain)) { + return false; + } + } + else { + if($nd!=$domain) { + return false; + } + } + } + + if ($checkDns && function_exists('checkdnsrr')) { + if (!(checkdnsrr($nd, 'MX') || checkdnsrr($nd, 'A'))) { + return false; + } + } + + return true; + } + + public static function is_empty($val) { + switch(gettype($val)) { + case "boolean": + case "integer": + case "double": + case "object": + case "resource": + return False; + case "array": + case "string": + if ($val == "0") return false; + return empty($val); + case "NULL": + return True; + } + } + +} + +# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab diff --git a/src/Log.php b/src/Log.php new file mode 100644 index 0000000..c94cb28 --- /dev/null +++ b/src/Log.php @@ -0,0 +1,365 @@ + 0, + 'DEBUG' => 1, + 'INFO' => 2, + 'WARNING' => 3, + 'ERROR' => 4, + 'FATAL' => 5, + ); + + /* + * Default log level + * @var string + */ + protected static string $default_level = 'WARNING'; + + /* + * Current log level + * @var string|null + */ + protected static $level = null; + + /* + * Log PHP errors levels (as specified to set_error_handler()) + * Default (set in self::init() method): + * - In TRACE or DEBUG: E_ALL & ~E_STRICT + * - Otherwise: E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED + */ + protected static $php_errors_levels = null; + + // Custom fatal error handler + protected static $fatal_error_handler = null; + + /* + * Initialization + * @param string $filepath + * @param string|null $level + * @param int|null $php_errors_levels + * @return void + */ + public static function init($filepath, $level=null, $php_errors_levels=null) { + self :: $filepath = $filepath; + + // Set default log level (if not defined or invalid) + if (is_null($level)) { + self :: $level = self :: $default_level; + } + elseif (!array_key_exists($level, self :: $levels)) { + self :: $level = self :: $default_level; + self :: warning( + "Invalid log level value found in configuration (%s). ". + "Set as default (%s).", $level, self :: $default_level); + } + else { + self :: $level = $level; + } + + // Log PHP errors + if (!is_null($php_errors_levels)) + self :: $php_errors_levels = $php_errors_levels; + elseif (in_array(self :: $level, array('DEBUG', 'TRACE'))) + self :: $php_errors_levels = E_ALL & ~E_STRICT; + 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 + set_exception_handler(array('EesyPHP\\Log', 'exception')); + } + + /** + * Log a message + * @param string $level The message level (key of self :: $levels) + * @param string $message The message to log + * @param array $extra_args Extra arguments to use to compute message using sprintf + * @return true + */ + public static function log($level, $message, ...$extra_args) { + global $auth_user, $argv; + + 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)) { + self :: $file_fd = fopen(self :: $filepath, 'a'); + } + + // Extra arguments passed, format message using sprintf + if ($extra_args) { + $message = call_user_func_array( + 'sprintf', + array_merge(array($message), $extra_args) + ); + } + + if (php_sapi_name() == "cli") { + $msg = implode(' - ', array( + date('Y/m/d H:i:s'), + basename($argv[0]), + $level, + $message + ))."\n"; + } + else { + $msg = array( + date('Y/m/d H:i:s'), + $_SERVER['REQUEST_URI'], + $_SERVER['REMOTE_ADDR'], + ); + if (isset($auth_user)) + $msg[] = ($auth_user['username']?$auth_user['username']:'anonymous'); + $msg[] = $level; + $msg[] = $message; + $msg = implode(' - ', $msg)."\n"; + } + + fwrite(self :: $file_fd, $msg); + + if ($level == 'FATAL') + if (!is_null(self :: $fatal_error_handler)) + call_user_func(self :: $fatal_error_handler, $message); + elseif (function_exists('fatal_error')) + fatal_error($message); + else + die("\n$message\n\n"); + elseif (php_sapi_name() == "cli") + echo $msg; + + return true; + } + + /** + * Log a trace message + * @param string $message The message to log + * @param array $extra_args Extra arguments to use to compute message using sprintf + * @return true + */ + public static function trace($message, ...$extra_args) { + return call_user_func_array( + array('EesyPHP\\Log', 'log'), + array_merge(array('TRACE', $message), $extra_args) + ); + } + + /** + * Log a debug message + * @param string $message The message to log + * @param array $extra_args Extra arguments to use to compute message using sprintf + * @return true + */ + public static function debug($message, ...$extra_args) { + return call_user_func_array( + array('EesyPHP\\Log', 'log'), + array_merge(array('DEBUG', $message), $extra_args) + ); + } + + /** + * Log an info message + * @param string $message The message to log + * @param array $extra_args Extra arguments to use to compute message using sprintf + * @return true + */ + public static function info($message, ...$extra_args) { + return call_user_func_array( + array('EesyPHP\\Log', 'log'), + array_merge(array('INFO', $message), $extra_args) + ); + } + + /** + * Log an warning message + * @param string $message The message to log + * @param array $extra_args Extra arguments to use to compute message using sprintf + * @return true + */ + public static function warning($message, ...$extra_args) { + return call_user_func_array( + array('EesyPHP\\Log', 'log'), + array_merge(array('WARNING', $message), $extra_args) + ); + } + + /** + * Log an error message + * @param string $message The message to log + * @param array $extra_args Extra arguments to use to compute message using sprintf + * @return true + */ + public static function error($message, ...$extra_args) { + return call_user_func_array( + array('EesyPHP\\Log', 'log'), + array_merge(array('ERROR', $message), $extra_args) + ); + } + + /** + * Log an fatal message + * @param string $message The message to log + * @param array $extra_args Extra arguments to use to compute message using sprintf + * @return true + */ + public static function fatal($message, ...$extra_args) { + return call_user_func_array( + array('EesyPHP\\Log', 'log'), + array_merge(array('FATAL', $message), $extra_args) + ); + } + + /** + * Register a contextual fatal error handler + * @param null|callable $handler The fatal error handler (set as null to reset) + * @return void + */ + public static function register_fatal_error_handler($handler) { + // @phpstan-ignore-next-line + if ($handler && !is_callable($handler)) + self :: fatal('Invalid fatal error handler provided: it is not callable !'); + self :: $fatal_error_handler = ($handler?$handler:null); + } + + /** + * Change of current log file + * @param string $file The new log file path + * @return bool + */ + public static function change_filepath($file) { + if ($file == self :: $filepath) return True; + if (self :: $file_fd) { + fclose(self :: $file_fd); + self :: $file_fd = null; + } + self :: $filepath = $file; + return True; + } + + /* + ******************************************************************************* + * Handle exception logging + ******************************************************************************* + */ + + /** + * Get the current backtrace + * @param int $ignore_last The number of last levels to ignore + * @return string + */ + public static function get_debug_backtrace_context($ignore_last=0) { + $traces = debug_backtrace(); + + // Also ignore this function it self + $ignore_last++; + + if (!is_array($traces) || count($traces) <= $ignore_last) + return ""; + + $msg = array(); + for ($i=$ignore_last; $i < count($traces); $i++) { + $trace = array("#$i"); + if (isset($traces[$i]['file'])) + $trace[] = $traces[$i]['file'].(isset($traces[$i]['line'])?":".$traces[$i]['line']:""); + // @phpstan-ignore-next-line + if (isset($traces[$i]['class']) && isset($traces[$i]['function'])) + $trace[] = implode(" ", array( + $traces[$i]['class'], + $traces[$i]['type'], + $traces[$i]['function']. "()")); + elseif (isset($traces[$i]['function'])) + $trace[] = $traces[$i]['function']. "()"; + $msg[] = implode(" - ", $trace); + } + + return implode("\n", $msg); + } + + /** + * Log an exception + * @param Throwable $exception + * @param string|null $prefix The prefix of the log message + * (optional, default: "An exception occured") + * @return void + */ + public static function exception($exception, $prefix=null) { + if (function_exists('log_in_sentry')) + log_in_sentry($exception); + // If more than 2 arguments passed, format prefix message using sprintf + if ($prefix && func_num_args() > 2) { + $prefix = call_user_func_array( + 'sprintf', + array_merge(array($prefix), array_slice(func_get_args(), 2)) + ); + } + self :: error( + "%s:\n%s\n## %s:%d : %s", + ($prefix?$prefix:"An exception occured"), + self::get_debug_backtrace_context(1), + $exception->getFile(), $exception->getLine(), + $exception->getMessage()); + } + + /* + ******************************************************************************* + * Handle PHP error logging + ******************************************************************************* + */ + + /** + * Convert PHP error number to the corresponding label + * @param int $errno + * @return string + */ + public static function errno2type($errno) { + $constants = get_defined_constants(); + if (is_array($constants)) + foreach($constants as $label => $value) + if ($value == $errno && preg_match('/^E_(.*)$/', $label, $m)) + return $m[1]; + return 'UNKNOWN ERROR #'.$errno; + } + + /** + * Log a PHP error + * Note: method design to be used as callable by set_error_handler() + * @param int $errno The error number + * @param string $errstr The error message + * @param string $errfile The filename that the error was raised in + * @param int $errline The line number where the error was raised + * @return false Return false to let the normal error handler continues. + */ + public static function on_php_error($errno, $errstr, $errfile, $errline) { + $msg = sprintf( + "A PHP error occured : [%s] %s\nFile : %s (line : %d)", + self :: errno2type($errno), $errstr, $errfile, $errline + ); + self :: error($msg); + if (function_exists('log_php_error_in_sentry')) + log_php_error_in_sentry($errno, $msg); + return False; + } +} + +# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 0000000..3b8f2fe --- /dev/null +++ b/src/functions.php @@ -0,0 +1,216 @@ +=1099511627776) + return number_format($size/1099511627776,$digit)."To"; + elseif ($size>=1073741824) + return number_format($size/1073741824,$digit)."Go"; + else if ($size>=1048576) + return number_format($size/1048576,$digit)."Mo"; + else if ($size>=1024) + return number_format($size/1024,$digit)."Ko"; + else + return $size."o"; +} + +/* + * Generic Data/value helpers + */ +function vardump($data) { + ob_start(); + var_dump($data); + $data = ob_get_contents(); + ob_end_clean(); + return $data; +} + +/** + * Format a callable object for logging + * @param string|array $callable The callable object + * @return string The callable object string representation + */ +function format_callable($callable) { + if (is_string($callable)) + return $callable."()"; + if (is_array($callable)) + if (is_string($callable[0])) + return $callable[0]."::".$callable[1]."()"; + elseif (is_object($callable[0])) + return get_class($callable[0])."->".$callable[1]."()"; + else + return "Unkown->".$callable[1]."()"; + // @phpstan-ignore-next-line + return vardump($callable); +} + +function check_is_empty($val) { + switch(gettype($val)) { + case "boolean": + case "integer": + case "double": + case "object": + case "resource": + return False; + case "array": + case "string": + if ($val == "0") return false; + return empty($val); + case "NULL": + return True; + } +} + +/* + * Generic file/directory helpers + */ +function dump_file($file_path, $max_age=3600) { + if (is_file($file_path)) { + header('Content-Type: '.mime_content_type($file_path)); + $last_modified_time = filemtime($file_path); + $etag = md5_file($file_path); + header("Cache-Control: max-age=$max_age, must-revalidate"); + header("Last-Modified: ".gmdate("D, d M Y H:i:s", $last_modified_time)." GMT"); + header("Etag: $etag"); + + if ( + ( + isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && + @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_modified_time + ) || ( + isset($_SERVER['HTTP_IF_NONE_MATCH']) && + trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag + ) + ) { + header("HTTP/1.1 304 Not Modified"); + exit(); + } + + header('Pragma: public'); + header('Content-Length: ' . filesize($file_path)); + readfile($file_path); + exit(); + } + header("HTTP/1.1 404 Not found"); + exit(); +} + +function delete_directory($dir, $recursive=true) { + $files = array_diff(scandir($dir), array('.','..')); + if ($recursive) { + foreach ($files as $file) { + if (is_dir("$dir/$file")) { + if (!delete_directory("$dir/$file", true)) { + Log :: error("delete_directory($dir) : Fail to delete sub-directory '$dir/$file'."); + return false; + } + } + else if (!unlink("$dir/$file")) { + Log :: error("delete_directory($dir) : Fail to delete '$dir/$file'."); + return false; + } + } + } + else if (!empty($files)) { + Log :: error("delete_directory($dir) : Directory is not empty."); + return false; + } + return rmdir($dir); +} + +/* + * Run external command helper + */ +/** + * Run external command + * + * @param $command string|array The command. It's could be an array of the command with its + * arguments. + * @param $data_stdin string|null The command arguments (optional, default: null) + * @param $escape_command_args boolean If true, the command will be escaped + * (optional, default: true) + * + * @return false|array An array of return code, stdout and stderr result or False in case of fatal + * error + **/ +function run_external_command($command, $data_stdin=null, $escape_command_args=true) { + if (is_array($command)) + $command = implode(' ', $command); + if ($escape_command_args) + $command = escapeshellcmd($command); + Log :: debug("Run external command: '$command'"); + $descriptorspec = array( + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w"), // stderr + ); + $process = proc_open($command, $descriptorspec, $pipes); + + if (!is_resource($process)) { + Log :: error("Fail to run external command: '$command'"); + return false; + } + + if (!is_null($data_stdin)) { + fwrite($pipes[0], $data_stdin); + } + fclose($pipes[0]); + + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + $return_value = proc_close($process); + + $error = (!empty($stderr) || $return_value != 0); + Log :: log( + ($error?'ERROR':'DEBUG'), + "External command ".($error?"error":"result").":\n". + "\tCommand : $command\n". + "\tReturn code: $return_value\n". + "\tOutput:\n". + "\t\t- Stdout :\n$stdout\n\n". + "\t\t- Stderr :\n$stderr" + ); + + return array($return_value, $stdout, $stderr); +} + +# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab