Some improvments from recent works on apps based on its "framework"

* Code cleaning and fix some small errors using Phpstan
* Configure pre-commit to run Phpstan before each commit
* Some little improvments and logging, mail, smarty & URL libs
* Add Sentry integration
* Add Webstat JS code inclusion
* Install Smarty dependency using composer

Breaking changes:
* Rename Event class as HookEvent to avoid conflict with PECL event
* URL with refresh GET parameter now automatically trigger redirection without it
 after page loading to avoid to keep it in URL
This commit is contained in:
Benjamin Renard 2023-01-29 11:51:41 +01:00
parent 01759fb4c2
commit 6fdc5447f1
24 changed files with 2363 additions and 510 deletions

9
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,9 @@
# Pre-commit hooks to run tests and ensure code is cleaned.
# See https://pre-commit.com for more information
repos:
- repo: https://github.com/digitalpulp/pre-commit-php.git
rev: 1.4.0
hooks:
- id: php-stan
files: \.(php)$
args: ['--configuration=phpstan.neon']

View file

@ -1,13 +1,23 @@
{
"name": "ee/eesyphp",
"description": "Easter-eggs easyphp PHP framework for simple web apps",
"authors": [
{
"name": "Easter-eggs",
"email": "info@easter-eggs.com"
}
],
"require": {
"envms/fluentpdo": "^1.1",
"pear/console_table": "^1.3",
"brenard/php-unidecode": "dev-master",
"smarty/smarty": "3.1.34",
"smarty-gettext/smarty-gettext": "^1.6",
"smarty-gettext/tsmarty2c": "^0.2.1",
"sepia/po-parser": "^6.0"
"sepia/po-parser": "^6.0",
"sentry/sdk": "^3.3"
},
"require-dev": {
"overtrue/phplint": "^3.0"
"phpstan/phpstan": "^1.9"
}
}

1833
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -49,3 +49,11 @@ Some other variables exist to manage the email sending by the application :
- `$mail_headers` : an PHP array of default headers add to mail sent by the application
- `$mail_sender` : the email address of the sender of all emails sent by the application
- `$mail_catch_all` : for debugging purpose, you can using this variable to specify an email address that will received all email sent by the application in place of the original recipient
## Web stats JS code inclusion
If you use a Web stats tool that need to include a piece of JS code in all page like [Matomo](https://matomo.org/), you could defined the `$webstats_js_code` variable.
## Sentry integration
If you want to enable the [Sentry](https://sentry.io) integration, you have to define the `$sentry_dsn`. This integration permit to report PHP errors (see `$sentry_php_error_types`) and exception as issues and to monitor performance of the application (see `$sentry_traces_sample_rate`).

View file

@ -5,7 +5,7 @@
Some Debian packages have to be installed :
```bash
apt install git composer php-cli php-mail php-mail-mine php-net-smtp php-auth-sasl php-json php-mbstring php-intl smarty3
apt install git composer php-cli php-mail php-mail-mine php-net-smtp php-auth-sasl php-json php-mbstring php-intl
# for PostgreSQL DB backend
apt install php-pgsql
# for MySQL/MariaDB DB backend

View file

@ -5,7 +5,7 @@ function add_cli_command($command, $handler, $short_desc, $usage_args=false, $lo
$override=false) {
global $cli_commands;
if (array_key_exists($command, $cli_commands) && !$override) {
logging('ERROR', _("The CLI command '%s' already exists.", $command));
logging('ERROR', _("The CLI command '%s' already exists."), $command);
return False;
}
@ -331,10 +331,10 @@ function cli_restore($command_args) {
$fd = fopen((count($command_args) >= 1?$command_args[0]:'php://stdin'), 'r');
restore_items($fd);
fclose($fd);
logging('INFO', sprint(
"Items restored from '%s'",
logging(
'INFO', "Items restored from '%s'",
(count($command_args) >= 1?$command_args[0]:'STDIN')
));
);
}
add_cli_command(
'restore',
@ -389,7 +389,6 @@ function cli_cron($command_args) {
logging('INFO', 'Item #%s (%s) deleted (creation date: %s)',
$item['id'], $item['name'], format_time($item['date'])
);
remove_item_attachments($item['id']);
}
else {
logging('ERROR', 'Fail to delete item "%s" (%s, creation date: %s)',
@ -416,3 +415,5 @@ add_cli_command(
___("-m/--max-age Item expiration limit (in days, optional)"),
)
);
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -35,8 +35,39 @@ $log_level = 'INFO';
// Debug Ajax request/response
$debug_ajax = false;
// Smarty class path
$smarty_path = 'smarty3/Smarty.class.php';
// Log PHP errors levels (as specified to set_error_handler())
// Default:
// - In TRACE or DEBUG: E_ALL & ~E_STRICT
// - Otherwise: E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
// $log_php_errors_levels = E_ALL & ~E_STRICT;
/*
* Sentry configuration
*/
// Sentry DSN
$sentry_dsn = null;
// Log PHP errors in Sentry: list of errors types to logs
// Note: must also match with $log_php_errors_levels.
// See: https://www.php.net/manual/fr/errorfunc.constants.php
$sentry_php_error_types = array(
E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR,
E_RECOVERABLE_ERROR,E_DEPRECATED,
);
// Traces sample rate (between 0 and 1)
// Note: this parameter permit to determine how many transactions (=~ access) are traced and
// sent to Sentry, for instance, 0.2 meen that 20% of the transactions will be traced. In dev
// mode, set to 1 if you want all transactions are sent to Sentry.
// Default: 0.2
$sentry_traces_sample_rate = 0.2;
/*
* Smarty template configuration
*/
// Smarty directories
$smarty_templates_dir = "$root_dir_path/templates";
$smarty_templates_c_dir = "$tmp_root_dir/templates_c";
@ -116,7 +147,9 @@ $mail_sender = "noreply@example.org";
// Catch all e-mails sent to a configured e-mail address
$mail_catch_all = false;
// Load local configuration file is present
if (is_file("$root_dir_path/includes/config.local.php")) {
require "$root_dir_path/includes/config.local.php";
}
/**
* Web Stats JS code
*/
$webstats_js_code = '';
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -3,7 +3,8 @@
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
// Root directory path
if (__FILE__ != "") {
$script = null;
if (defined('__FILE__') && constant('__FILE__')) {
$script = __FILE__;
}
else {
@ -12,7 +13,8 @@ else {
if (basename($script) == 'core.php')
break;
}
$root_dir_path=realpath(dirname($script).'/../');
if (!$script) die('Fail to detect root directory path');
$root_dir_path = realpath(dirname($script).'/../');
// Include App's includes and vendor directories to PHP include paths
set_include_path($root_dir_path.'/includes' . PATH_SEPARATOR . get_include_path());
@ -27,13 +29,19 @@ $api_mode = false;
require_once('translation.php');
require_once('config.inc.php');
// Check $public_root_url end
if (substr($public_root_url, -1)=='/') {
$public_root_url=substr($public_root_url, 0, -1);
// Load local configuration file is present
if (is_file("$root_dir_path/includes/config.local.php")) {
require "$root_dir_path/includes/config.local.php";
}
require_once 'sentry.php';
$sentry_transaction = new SentryTransaction();
$sentry_span = new SentrySpan('core.init', 'Core initialization');
// Define upload_tmp_dir
ini_set('upload_tmp_dir',$upload_tmp_dir);
if (isset($upload_tmp_dir))
ini_set('upload_tmp_dir', $upload_tmp_dir);
require_once('logging.php');
require_once('functions.php');
@ -60,3 +68,6 @@ require_once('mail.php');
init_translation();
foreach($status_list as $key => $value)
$status_list[$key] = _($value);
$sentry_span->finish();
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -2,8 +2,18 @@
use Unidecode\Unidecode;
if (!isset($db_dsn)) {
logging('FATAL', 'Database DSN not configured');
exit(1);
}
try {
$pdo = new PDO($db_dsn, $db_user, $db_pwd, $db_options);
$pdo = new PDO(
$db_dsn,
isset($db_user)?$db_user:null,
isset($db_pwd)?$db_pwd:null,
isset($db_options)?$db_options:null
);
$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$fpdo = new FluentPDO($pdo);
@ -266,18 +276,18 @@ function search_items($params) {
$orderby="$order $order_direction";
$limit = "";
$page = 1;
$nb_by_page = 10;
$offset = 0;
if (!isset($params['all'])) {
$page=1;
$nb_by_page=10;
if (isset($params['page']) && $params['page']>0) {
if (isset($params['nb_by_page']) && $params['nb_by_page']>0) {
$nb_by_page=intval($params['nb_by_page']);
$nb_by_page = intval($params['nb_by_page']);
}
$page=intval($params['page']);
$page = intval($params['page']);
}
$offset=($page-1)*$nb_by_page;
$limit=$nb_by_page;
$offset = ($page-1)*$nb_by_page;
$limit = $nb_by_page;
}
try {
@ -297,14 +307,17 @@ function search_items($params) {
-> offset($offset)
-> execute();
if ($result !== false) {
if ($result === false) {
logging('ERROR', 'search_items() : search in DB return false');
return false;
}
$rows = $result -> fetchAll();
$items = array();
foreach ($rows as $row) {
$items[] = _format_row_info($row, array('date'));
}
if (is_array($items)) {
if (empty($limit)) {
if (isset($params['all'])) {
return array(
'count' => count($items),
'first' => 1,
@ -314,7 +327,6 @@ function search_items($params) {
'items' => $items
);
}
else {
$query_count = $fpdo -> from('item')
-> select(null)
-> select('count(*) as count');
@ -347,11 +359,6 @@ function search_items($params) {
'items' => $items,
);
}
}
}
else
logging('ERROR', 'search_items() : search in DB return false');
}
catch (Exception $e) {
log_exception(
$e, "An exception occured searching items with params %s infos from database : ",
@ -461,3 +468,5 @@ function restore_items($fd=null) {
return !$error;
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -232,6 +232,25 @@ function vardump($data) {
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":
@ -322,7 +341,7 @@ function delete_directory($dir, $recursive=true) {
* error
**/
function run_external_command($command, $data_stdin=null, $escape_command_args=true) {
if (array($command))
if (is_array($command))
$command = implode(' ', $command);
if ($escape_command_args)
$command = escapeshellcmd($command);
@ -365,3 +384,5 @@ function run_external_command($command, $data_stdin=null, $escape_command_args=t
return array($return_value, $stdout, $stderr);
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -25,7 +25,7 @@ function register_hook($event, $callable, $param=NULL) {
/**
* Run triggered actions on specific event
*
* @param $event string Event name
* @param $event string HookEvent name
*
* @return boolean True if all triggered actions succefully runned, false otherwise
*/
@ -35,16 +35,16 @@ function trigger_hook($event_name, $event_data=null) {
if (isset($hooks[$event_name]) && is_array($hooks[$event_name])) {
if ($event_name == 'all')
$event = new Event($event_data['event_name'], $event_data['event_data']);
$event = new HookEvent($event_data['event_name'], $event_data['event_data']);
else
$event = new Event($event_name, $event_data);
$event = new HookEvent($event_name, $event_data);
foreach ($hooks[$event_name] as $e) {
if (is_callable($e['callable'])) {
try {
call_user_func_array($e['callable'],array($event, &$e['param']));
}
catch(Exception $e) {
logException(
log_exception(
$e, "An exception occured running hook ".format_callable($e['callable']).
" on event $event_name");
$return = false;
@ -75,7 +75,7 @@ function trigger_hook($event_name, $event_data=null) {
return $return;
}
class Event implements JsonSerializable {
class HookEvent implements JsonSerializable {
private $name;
private $data;
@ -101,3 +101,5 @@ class Event implements JsonSerializable {
);
}
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -7,14 +7,23 @@
*
* // Log level (DEBUG / INFO / WARNING / ERROR / FATAL)
* $log_level='INFO';
*
* // Log PHP errors levels (as specified to set_error_handler())
* // $log_php_errors_levels = E_ALL & ~E_STRICT;
* // Log PHP errors levels (as specified to set_error_handler())
* // Default:
* // - In TRACE or DEBUG: E_ALL & ~E_STRICT
* // - Otherwise: E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
* // $log_php_errors_levels = E_ALL & ~E_STRICT;
*/
// Log file descriptor (Do not change !!!)
$_log_file_fd=null;
$_log_file_fd = null;
// Log Levels
$_log_levels=array(
$_log_levels = array(
'TRACE' => 0,
'DEBUG' => 1,
'INFO' => 2,
@ -23,13 +32,22 @@ $_log_levels=array(
'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, $auth_user;
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];
if (!array_key_exists($log_level, $_log_levels)) $log_level = 'INFO';
$log_level_id = $_log_levels[$log_level];
if ($level_id < $log_level_id) return true;
@ -66,10 +84,12 @@ function logging($level, $message) {
$msg = implode(' - ', $msg)."\n";
}
fwrite($_log_file_fd , $msg);
fwrite($_log_file_fd, $msg);
if ($level == 'FATAL')
if (function_exists('fatal_error'))
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");
@ -79,6 +99,38 @@ function logging($level, $message) {
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;
@ -90,7 +142,17 @@ function change_log_file($file) {
return True;
}
// Handle exception logging
/*
*******************************************************************************
* 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();
@ -105,6 +167,7 @@ function get_debug_backtrace_context($ignore_last=0) {
$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'],
@ -118,7 +181,16 @@ function get_debug_backtrace_context($ignore_last=0) {
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(
@ -135,13 +207,50 @@ function log_exception($exception, $prefix=null) {
}
set_exception_handler('log_exception');
// Handle PHP error logging
function log_php_eror($errno, $errstr, $errfile, $errline) {
logging("ERROR", "A PHP error occured : [%d] %s\nFile : %s (line : %d)",
$errno, $errstr, $errfile, $errline);
/*
*******************************************************************************
* 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 ($log_level == 'DEBUG')
set_error_handler('log_php_eror', E_ALL & ~E_STRICT);
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_eror', E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
set_error_handler('log_php_error', E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -1,24 +1,72 @@
<?php
// Load PHP PEAR Mail and Mail_mime libs
require_once($php_mail_path);
require_once($php_mail_mime_path);
require_once(isset($php_mail_path)?$php_mail_path:"Mail.php");
require_once(isset($php_mail_mime_path)?$php_mail_mime_path:"Mail/mime.php");
function send_mail($from, $to, $subject, $msg, $headers=array(), $attachments=array(),
$crlf="\r\n") {
global $mail_send_method, $mail_headers, $mail_send_params, $mail_catch_all, $mail_sender;
$mail_obj = Mail::factory($mail_send_method, $mail_send_params);
/**
* Send an email
*
* @param string|null $from Email sender
* @param string|array<string> $to Email recipient(s)
* @param string $subject Email subject
* @param string $msg Email body
* @param boolean $html Set to true to send an HTML email (default: false)
* @param array<string,string>|null $attachments Email attachments as an array with
* filepath as key and filename as value
* @param array<string,string>|null $headers Email headers
* @param string|null $encoding Email encoding (default: utf8)
* @param string|null $eol End of line string (default : \n)
*
* @return boolean true If mail was sent, false otherwise
*/
function send_mail($from, $to, $subject, $msg, $html=false, $attachments=null, $headers=null,
$encoding=null, $eol=null) {
global $mail_send_method, $mail_headers, $mail_send_params, $mail_sender, $mail_catch_all;
$mail_obj = & Mail::factory($mail_send_method, $mail_send_params);
if ($mail_catch_all) {
if (!$headers) $headers = array();
if(isset($mail_headers) && is_array($mail_headers)) {
$headers = array_merge($headers, $mail_headers);
}
logging(
'TRACE', 'Mail catch all: %s',
isset($mail_catch_all) && $mail_catch_all?
vardump($mail_catch_all):'not set'
);
if (isset($mail_catch_all) && $mail_catch_all) {
logging(
'DEBUG', 'Mail catch to %s',
is_array($mail_catch_all)?implode(',', $mail_catch_all):$mail_catch_all
);
$msg .= sprintf(
_("\n\n\nMail initialy intended for %s."),
(
$html?
_("</hr><p><small>Mail initialy intended for %s.</small></p>"):
_("\n\n\nMail initialy intended for %s.")
),
(is_array($to)?implode(',', $to):$to));
$to = $mail_catch_all;
$headers["X-Orig-To"] = $to;
$to = (
is_array($mail_catch_all)?
implode(',', $mail_catch_all):$mail_catch_all
);
}
if(is_array($mail_headers)) {
$headers = array_merge($headers,$mail_headers);
if ($subject) {
$headers["Subject"] = $subject;
}
if (isset($headers['From'])) {
if (!$from)
$from = $headers['From'];
unset($headers['From']);
}
elseif (!$from) {
$from = $mail_sender;
}
$headers["To"] = $to;
$to = array (
@ -26,33 +74,43 @@ function send_mail($from, $to, $subject, $msg, $headers=array(), $attachments=ar
);
foreach(array_keys($headers) as $header) {
if(strtoupper($header) == 'BCC') {
$to['BCC'] = $headers[$header];
if(in_array(strtoupper($header), array('BCC', 'CC'))) {
if (isset($mail_catch_all) && $mail_catch_all) {
logging('DEBUG', "Mail catched: remove $header header");
$msg .= sprintf(
(
$html?
_("<p><small>%s: %s</small></p>"):
_("\n%s: %s")
),
strtoupper($header),
(is_array($headers[$header])?implode(',', $headers[$header]):$headers[$header]));
unset($headers[$header]);
continue;
}
elseif(strtoupper($header) == 'CC') {
$to['CC'] = $headers[$header];
$to[strtoupper($header)] = $headers[$header];
}
}
if (!$encoding) $encoding = "utf8";
$mime = new Mail_mime(
array(
'eol' => $crlf,
'text_charset' => 'utf8',
'head_charset' => 'utf8',
'eol' => ($eol?$eol:"\n"),
($html?'html_charset':'text_charset') => $encoding,
'head_charset' => $encoding,
)
);
if ($from) {
if ($from)
$mime->setFrom($from);
}
elseif ($mail_sender) {
$mime->setFrom($mail_sender);
}
if ($subject) {
if ($subject)
$mime->setSubject($subject);
}
$mime->setTXTBody($msg);
if ($html)
$mime->setHTMLBody($msg);
else
$mime->setTXTBody($msg);
if (is_array($attachments) && !empty($attachments)) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
@ -64,11 +122,14 @@ function send_mail($from, $to, $subject, $msg, $headers=array(), $attachments=ar
$body = $mime->get();
$headers = $mime->headers($headers);
$ret = $mail_obj -> send($to,$headers,$body);
$ret = $mail_obj -> send($to, $headers, $body);
if ($ret instanceof PEAR_Error) {
logging('ERROR',"Error sending email to $to : ".$ret -> getMessage());
return False;
if (PEAR::isError($ret)) {
$msg = "Error sending email: ".$ret -> getMessage();
logging('ERROR', $msg);
return false;
}
return true;
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

213
includes/sentry.php Normal file
View file

@ -0,0 +1,213 @@
<?php
/*
* Configuration :
*
* // Log PHP errors in Sentry: list of errors types to logs
* // Note: must also match with $log_php_errors_levels.
* // See: https://www.php.net/manual/fr/errorfunc.constants.php
* $sentry_php_error_types = array(
* E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR,
* E_RECOVERABLE_ERROR,E_DEPRECATED,
* );
*/
// Init Sentry (if its DSN is configured)
if (isset($sentry_dsn) && $sentry_dsn) {
\Sentry\init([
'dsn' => $sentry_dsn,
'traces_sample_rate' => (
isset($sentry_traces_sample_rate) ?
$sentry_traces_sample_rate : 0.2
),
]);
\Sentry\configureScope(function (\Sentry\State\Scope $scope): void {
global $auth_user;
$scope->setUser([
'id' => isset($auth_user) && $auth_user?$auth_user['uid']:null,
'email' => isset($auth_user) && $auth_user?$auth_user['mail']:null,
'segment' => isset($auth_user) && $auth_user?$auth_user['type']:null,
'ip_address' => $_SERVER['REMOTE_ADDR'],
]);
});
}
/**
* Log an exception or a message in Sentry
* @param string|Exception $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');
return;
}
if (is_string($msg)) {
logging('DEBUG', 'Error logged in Sentry');
\Sentry\captureMessage($msg);
}
elseif ($msg instanceof Exception) {
logging('DEBUG', 'Exception logged in Sentry');
\Sentry\captureException($msg);
}
}
/**
* Log a PHP error in Sentry
* @param int $errno The error number
* @param string $msg The error message
* @return void
*/
function log_php_error_in_sentry($errno, $msg) {
global $sentry_php_error_types;
if (
isset($sentry_php_error_types)
&& is_array($sentry_php_error_types)
&& in_array($errno, $sentry_php_error_types)
)
log_in_sentry($msg);
}
/*
* Performance monitoring
*/
class SentryTransaction {
/**
* The Sentry transaction object
* @var \Sentry\Tracing\Transaction
*/
private $transaction;
/**
* The Sentry transaction context object
* @var \Sentry\Tracing\TransactionContext
*/
private $context;
/**
* Constructor: start a Sentry transaction
* @param string|null $op The operation name
* @param string|null $name The transaction name
* @return void
*/
public function __construct($op=null, $name=null) {
// Setup context for the full transaction
$this->context = new \Sentry\Tracing\TransactionContext();
$this->context->setName(
$name?$name:
(php_sapi_name()=='cli'?'CLI execution':'HTTP request')
);
$this->context->setOp(
$op?$op:
(php_sapi_name()=='cli'?'cli.command':'http.request')
);
// Start the transaction
$this->transaction = \Sentry\startTransaction($this->context);
// Set the current transaction as the current span so we can retrieve it later
\Sentry\SentrySdk::getCurrentHub()->setSpan($this->transaction);
}
/**
* Destructor: Stop the current Sentry transaction
* @return void
*/
public function __destruct() {
SentrySpan :: finishAll();
$this->transaction->finish();
}
}
/**
* Internal Sentry Span object implementation
* This internal implementation principally permit to keep trace of new span parent
* and list of started spans.
*/
class SentrySpan {
/**
* Keep trace of started Sentry spans
* @var array<int,mixed>
*/
private static $_started_spans = array();
/**
* The unique ID of the Sentry span
* Note: internal ID used as key in self::$_started_spans
* @var int|null
*/
private $id = null;
/**
* The parent of the Sentry span
* @var mixed
*/
private $parent = null;
/**
* The context of the Sentry span
* @var null|\Sentry\Tracing\SpanContext
*/
private $context = null;
/**
* The Sentry span object
* @var mixed
*/
private $span = null;
/**
* Sentry span constructor
* @param string|null $op The operation name
* @param string|null $name The span name
* @return void
*/
public function __construct($op, $name) {
$this -> parent = \Sentry\SentrySdk::getCurrentHub()->getSpan();
// Check if we have a parent span (this is the case if we started a transaction earlier)
if (is_null($this -> parent)) return;
while (is_null($this -> id)) {
$this -> id = rand();
if (isset(self :: $_started_spans[$this -> id]))
$this -> id = null;
}
$this -> context = new \Sentry\Tracing\SpanContext();
$this -> context->setOp($op);
$this -> context->setDescription($name);
$this -> span = $this->parent->startChild($this -> context);
// Set the current span to the span we just started
\Sentry\SentrySdk::getCurrentHub()->setSpan($this -> span);
self :: $_started_spans[$this -> id] = $this;
}
/**
* Finish the span (if started)
* @return void
*/
public function finish() {
if (!$this -> span) return;
$this -> span -> finish();
unset(self::$_started_spans[$this -> id]);
\Sentry\SentrySdk::getCurrentHub()->setSpan($this -> parent);
}
/**
* Finish all started spans
* @see SentryTransaction::__destruct()
* @return void
*/
public static function finishAll() {
foreach (array_reverse(self :: $_started_spans) as $id => $span)
$span -> finish();
}
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -17,7 +17,7 @@ if (!isset($_SESSION['session_key'])) {
}
// Handle session timeout
if ($session_timeout) {
if (isset($session_timeout) && $session_timeout) {
if (!isset($_SESSION['session_last_access'])) {
logging('DEBUG', 'Set initial session last access');
$_SESSION['session_last_access'] = time();
@ -40,3 +40,5 @@ function check_session_key($value=null) {
$value = $_REQUEST['session_key'];
return ($value && $_SESSION['session_key'] == $value);
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -3,7 +3,6 @@
if (php_sapi_name() == "cli")
return true;
require_once($smarty_path);
$smarty = new Smarty();
/*
@ -62,8 +61,26 @@ else {
}
// Configure templates/templates_c directories
$smarty->setTemplateDir($smarty_templates_dir);
$smarty->setCompileDir($smarty_templates_c_dir);
if (
!isset($smarty_templates_dir)
|| !is_dir($smarty_templates_dir)
)
logging(
'FATAL', "Template directory not found (%s)",
isset($smarty_templates_dir)?$smarty_templates_dir:'not set');
else
$smarty->setTemplateDir($smarty_templates_dir);
if (
!isset($smarty_templates_c_dir)
|| !is_dir($smarty_templates_c_dir)
|| !is_writable($smarty_templates_c_dir)
)
logging(
'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);
// Enable Smarty security
smarty_enable_security_mode(
@ -77,13 +94,13 @@ smarty_enable_security_mode(
);
// Defined some global template variables
$smarty->assign('public_root_url', $public_root_url);
$smarty->assign('main_pagetitle', $main_pagetitle);
$smarty->assign('public_root_url', isset($public_root_url)?$public_root_url:'/');
$smarty->assign('main_pagetitle', isset($main_pagetitle)?$main_pagetitle:null);
$smarty->assign('session_key', $_SESSION['session_key']);
// Handle in-page errors & messages
if (!isset($_SESSION['errors']))
$_SESSION['errors']=array();
$_SESSION['errors'] = array();
function add_error($error) {
// If more than one arguments passed, format error message using sprintf
if (func_num_args() > 1) {
@ -96,7 +113,7 @@ function add_error($error) {
}
if (!isset($_SESSION['messages']))
$_SESSION['messages']=array();
$_SESSION['messages'] = array();
function add_message($message) {
// If more than one arguments passed, format message using sprintf
if (func_num_args() > 1) {
@ -113,42 +130,72 @@ if (isset($included_css_files) && is_array($included_css_files)) {
$_css = $included_css_files;
}
else {
$_css=array();
$_css = array();
}
function add_css_file($files) {
function add_css_file() {
global $_css;
foreach (func_get_args() as $files) {
if (!is_array($files)) $files=array($files);
foreach ($files as $file)
foreach ($files as $file) {
if (!in_array($file, $_css))
$_css[]=$file;
}
}
}
$_js=array();
function add_js_file($files) {
$_js = array();
function add_js_file() {
global $_js;
if (!is_array($files)) $files=array($files);
foreach ($files as $file)
foreach (func_get_args() as $files) {
if (!is_array($files)) $files = array($files);
foreach ($files as $file) {
if (!in_array($file, $_js))
$_js[]=$file;
$_js[] = $file;
}
}
}
function _defineCommonTemplateVariables($template, $pagetitle) {
global $smarty, $_css, $_js, $status_list, $auth_user, $admin;
global $smarty, $_css, $_js, $status_list, $auth_user, $admin, $webstats_js_code;
$smarty->assign('pagetitle', $pagetitle);
// Messages
$smarty -> assign('errors', (isset($_SESSION['errors'])?$_SESSION['errors']:array()));
$smarty -> assign('messages', (isset($_SESSION['messages'])?$_SESSION['messages']:array()));
// Files inclusions
$smarty -> assign('css', $_css);
$smarty -> assign('js', $_js);
// Authenticated user info
if (isset($auth_user))
$smarty->assign('auth_user', $auth_user);
$smarty->assign('errors', $_SESSION['errors']);
$smarty->assign('messages', $_SESSION['messages']);
$smarty->assign('css', $_css);
$smarty->assign('js', $_js);
// Webstats JS code
$smarty->assign(
'webstats_js_code',
isset($webstats_js_code)?$webstats_js_code:null);
}
function display_template($template, $pagetitle=false) {
if (!$template)
logging("FATAL", _("No template specified."));
// If refresh parameter is present, remove it and redirect
if (isset($_GET['refresh'])) {
unset($_GET['refresh']);
$url = get_current_url();
if (!empty($_GET))
$url .= '?'.http_build_query($_GET);
redirect($url);
return;
}
$sentry_span = new SentrySpan('smarty.display_template', "Display Smarty template");
global $smarty;
// If more than 2 arguments passed, format pagetitle using sprintf
if ($pagetitle & func_num_args() > 2) {
if ($pagetitle && func_num_args() > 2) {
$pagetitle = call_user_func_array(
'sprintf',
array_merge(array($pagetitle), array_slice(func_get_args(), 2))
@ -163,8 +210,11 @@ function display_template($template, $pagetitle=false) {
catch (Exception $e) {
log_exception($e, "Smarty - An exception occured displaying template '$template'");
if ($template != 'fatal_error.tpl')
logging("FATAL", _("An error occurred while viewing this page."));
logging("FATAL", _("An error occurred while displaying this page."));
}
$sentry_span->finish();
}
function display_ajax_return($data=null, $pretty=false) {
@ -269,3 +319,5 @@ function smarty_encodeJsonBase64($params, $smarty) {
echo base64_encode(json_encode($params['data']));
}
smarty_register_function('encodeJsonBase64','smarty_encodeJsonBase64');
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -154,7 +154,7 @@ add_cli_command(
* all PO file in lang/[lang]/LC_MESSAGES.
*
* @param array $command_args The command arguments
* @return void
* @return bool
*/
function cli_update_messages($command_args) {
global $root_dir_path, $root_lang_dir, $smarty_templates_dir;
@ -234,6 +234,7 @@ function cli_update_messages($command_args) {
}
logging('FATAL', _("Fail to open root lang directory (%s)."), $root_dir_path);
return false;
}
add_cli_command(
'update_messages',
@ -248,7 +249,7 @@ add_cli_command(
* to corresponding MO files and as JSON catalog (for translation in JS).
*
* @param array $command_args The command arguments
* @return void
* @return bool
*/
function cli_compile_messages($command_args) {
global $root_dir_path, $root_lang_dir, $smarty_templates_dir;
@ -354,6 +355,7 @@ function cli_compile_messages($command_args) {
return !$error;
}
logging('FATAL', _("Fail to open root lang directory (%s)."), $root_dir_path);
return false;
}
add_cli_command(
'compile_messages',
@ -368,3 +370,5 @@ add_cli_command(
"directories to MO files and as JSON catalogs in public_html/translations."
)
);
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -145,3 +145,5 @@ function init_translation() {
add_js_file(array("lib/babel.js", "js/translation.js", $js_translation_file));
}
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -14,3 +14,5 @@ function get_item_from_url($id, $fatal=false) {
}
return $item;
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -258,3 +258,5 @@ function handle_delete($request) {
redirect('item');
}
add_url_handler('|^item/(?P<id>[0-9]+)/delete$|', 'handle_delete');
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -26,15 +26,16 @@ $url_patterns =array();
/**
* Add an URL pattern
*
* @param $pattern string The URL pattern (required)
* @param $handler callable The URL pattern handler (must be callable, required)
* @param $authenticated boolean 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 $override boolean Allow override if a command already exists with the
* @param string|array $pattern The URL pattern or an array of patterns (required)
* @param callable $handler The URL pattern handler (must be callable, required)
* @param boolean $authenticated Permit to define if this URL is accessible only for
* authenticated users (optional, default: true if the
* special force_authentication function is defined,
* false otherwise)
* @param boolean $override Allow override if a command already exists with the
* same name (optional, default: false)
* @param $api_mode boolean Enable API mode (optional, default: false)
* @param $methods array|null HTTP method (optional, default: array('GET', 'POST'))
* @param boolean $api_mode Enable API mode (optional, default: false)
* @param array|string|null $methods HTTP method (optional, default: array('GET', 'POST'))
**/
function add_url_handler($pattern, $handler=null, $authenticated=null, $override=true,
$api_mode=false, $methods=null) {
@ -67,9 +68,9 @@ function add_url_handler($pattern, $handler=null, $authenticated=null, $override
}
elseif ($override) {
logging(
'DEBUG',
"URL : override pattern '$pattern' with handler '$handler' ".
"(old handler = '".$url_patterns[$pattern]."')");
'DEBUG', "URL : override pattern '%s' with handler '%s' (old handler = '%s')".
$pattern, format_callable($handler), vardump($url_patterns[$pattern])
);
$url_patterns[$pattern] = array(
'handler' => $handler,
'authenticated' => $authenticated,
@ -133,9 +134,9 @@ function error_page($request=null, $error_code=null) {
/**
* Error 404 handler
*
* @param[in] $request UrlRequest|null The request (optional, default: null)
* @param UrlRequest|null $request The request (optional, default: null)
*
* @retval void
* @return void
**/
function error_404($request=null) {
error_page($request, 404);
@ -174,7 +175,7 @@ function get_request($default_url=null) {
logging('DEBUG', "URL : current url = '$current_url'");
logging(
'DEBUG',
'TRACE',
"URL : check current url with the following URL patterns :\n - ".
implode("\n - ", array_keys($url_patterns))
);
@ -185,7 +186,7 @@ function get_request($default_url=null) {
// Reset last redirect
if (isset($_SESSION['last_redirect']))
unset($_SESSION['last_redirect']);
logging('DEBUG', "URL : result :\n".varDump($request, 1));
logging('TRACE', "URL : result :\n".vardump($request));
return $request;
}
}
@ -212,9 +213,9 @@ function get_request($default_url=null) {
/**
* Check if the current requested URL match with a specific pattern
*
* @param $pattern string The URL pattern
* @param $current_url string|false The current URL (optional)
* @param $methods array|null HTTP method (optional, default: no check)
* @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.
**/
@ -229,7 +230,7 @@ function url_match($pattern, $current_url=false, $methods=null) {
logging(
'DEBUG',
"URL : Match found with pattern '$pattern' :\n\t".
str_replace("\n", "\n\t", print_r($m, 1)));
str_replace("\n", "\n\t", print_r($m, true)));
return $m;
}
return False;
@ -241,10 +242,10 @@ function url_match($pattern, $current_url=false, $methods=null) {
* @return string|false The current request URL or false if fail
**/
function get_current_url() {
logging('DEBUG', "URL : request URI = '".$_SERVER['REQUEST_URI']."'");
logging('TRACE', "URL : request URI = '".$_SERVER['REQUEST_URI']."'");
$base = get_rewrite_base();
logging('DEBUG', "URL : rewrite base = '$base'");
logging('TRACE', "URL : rewrite base = '$base'");
if ($_SERVER['REQUEST_URI'] == $base)
return '';
@ -289,7 +290,7 @@ function get_rewrite_base() {
/**
* Trigger redirect to specified URL (or homepage if omited)
*
* @param $go string|false The destination URL
* @param string|false $go The destination URL
*
* @return void
**/
@ -298,6 +299,15 @@ 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 (is_absolute_url($go))
$url = $go;
@ -332,7 +342,7 @@ function redirect($go=false) {
* invoke the force_authentication() special function (or trigger a fatal error
* if it's not defined).
*
* @param $default_url string|null The default URL if current one does not
* @param string|null $default_url The default URL if current one does not
* match with any configured pattern.
*
* @return void
@ -340,10 +350,14 @@ function redirect($go=false) {
function handle_request($default_url=null) {
global $smarty, $api_mode;
$sentry_span = new SentrySpan('http.handle_request', 'Handle the HTTP request');
$request = get_request($default_url);
if (!is_callable($request -> handler)) {
logging('ERROR', "URL handler function ".$request -> handler."() does not exists !");
logging(
'ERROR', "URL handler function %s does not exists !",
format_callable($request -> handler));
logging('FATAL', _("This request cannot be processed."));
}
@ -360,19 +374,20 @@ function handle_request($default_url=null) {
logging('FATAL', _("Authentication required but force_authentication function is not defined."));
try {
return call_user_func($request -> handler, $request);
call_user_func($request -> handler, $request);
}
catch (Exception $e) {
log_exception(
$e, "An exception occured running URL handler function ".$request -> handler."()");
logging('FATAL', _("This request could not be processed correctly."));
}
$sentry_span->finish();
}
/**
* Remove trailing slash in specified URL
*
* @param $url string The URL
* @param string $url The URL
*
* @return string The specified URL without trailing slash
**/
@ -390,13 +405,13 @@ function remove_trailing_slash($url) {
* Check if session key is present and valid and set AJAX
* mode.
*
* @param $session_key string The current session key (optional)
* @param string|null $session_key string The current session key (optional)
*
* @return void
**/
function check_ajax_request($session_key=null) {
global $ajax, $debug_ajax;
$ajax=true;
$ajax = true;
if (check_session_key($session_key))
fatal_error('Invalid request');
@ -408,7 +423,7 @@ function check_ajax_request($session_key=null) {
/**
* Get the public absolute URL
*
* @param $relative_url string|null Relative URL to convert (Default: current URL)
* @param string|null $relative_url Relative URL to convert (Default: current URL)
*
* @return string The public absolute URL
**/
@ -430,7 +445,7 @@ function get_absolute_url($relative_url=null) {
}
if (substr($relative_url, 0, 1) == '/')
$relative_url = substr($url, 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");
return $url;
@ -439,7 +454,7 @@ function get_absolute_url($relative_url=null) {
/**
* Check if specified URL is absolute
*
* @param $url string The URL to check
* @param string $url The URL to check
*
* @return boolean True if specified URL is absolute, False otherwise
**/
@ -450,12 +465,12 @@ function is_absolute_url($url) {
/**
* Add parameter in specified URL
*
* @param &$url string The reference of the URL
* @param $param string The parameter name
* @param $value string The parameter value
* @param $encode boolean Set if parameter value must be URL encoded (optional, default: true)
* @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|null The completed URL
* @return string The completed URL
*/
function add_url_parameter(&$url, $param, $value, $encode=true) {
if (strpos($url, '?') === false)
@ -503,7 +518,7 @@ class UrlRequest {
/**
* Get request info
*
* @param $key string The name of the info
* @param string $key The name of the info
*
* @return mixed The value
**/
@ -527,15 +542,39 @@ class UrlRequest {
logging('WARNING', "__get($key): invalid property requested\n".get_debug_backtrace_context());
}
/**
* Set request info
*
* @param string $key The name of the info
* @param mixed $value The value of the info
*
* @return void
**/
public function __set($key, $value) {
if ($key == 'referer')
$_SERVER['HTTP_REFERER'] = $value;
elseif ($key == 'http_method')
$_SERVER['REQUEST_METHOD'] = $value;
else
$this->url_params[$key] = $value;
}
/**
* Check is request info is set
*
* @param $key string The name of the info
* @param string $key The name of the info
*
* @return boolval True is info is set, False otherwise
* @return bool True is info is set, False otherwise
**/
public function __isset($key) {
if (in_array($key, array('current_url', 'handler', 'authenticated')))
if (
in_array(
$key, array('current_url', 'handler', 'authenticated',
'api_mode', 'referer', 'http_method')
)
)
return True;
return array_key_exists($key, $this->url_params);
}
@ -543,8 +582,8 @@ class UrlRequest {
/**
* Get request parameter
*
* @param $parameter string The name of the parameter
* @param $decode string If true, the parameter value will be urldecoded
* @param string $parameter The name of the parameter
* @param bool $decode If true, the parameter value will be urldecoded
* (optional, default: true)
*
* @return mixed The value or false if parameter does not exists
@ -570,3 +609,5 @@ class UrlRequest {
}
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

31
phpstan.neon Normal file
View file

@ -0,0 +1,31 @@
parameters:
level: 5
paths:
- includes
- public_html
- bin
excludePaths:
- includes/config.local.php
ignoreErrors:
-
message: "#Instantiated class Mail_mime not found\\.#"
path: includes/mail.php
-
message: "#Call to method (setFrom|setSubject|setHTMLBody|setTXTBody|get|headers|addAttachment)\\(\\) on an unknown class Mail_mime\\.#"
path: includes/mail.php
-
message: "#Call to static method factory\\(\\) on an unknown class Mail\\.#"
path: includes/mail.php
-
message: "#Call to static method isError\\(\\) on an unknown class PEAR\\.#"
path: includes/mail.php
-
message: "#Variable \\$root_dir_path might not be defined\\.#"
paths:
- includes/config.inc.php
- "#Access to private property UrlRequest::\\$(handler|api_mode|authenticated)\\.#"
-
message: "#Variable \\$status_list might not be defined\\.#"
paths:
- includes/cli.php

View file

@ -5,3 +5,5 @@ include 'url-public.php';
$default_url='';
handle_request();
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -97,5 +97,6 @@
<script language="javascript" src="{$file}"></script>
{/foreach}
{if $webstats_js_code}{$webstats_js_code}{/if}
</body>
</html>