Files
MYSOPHAL/inc/application/errorhandler.class.php
2025-08-07 13:15:31 +01:00

454 lines
13 KiB
PHP

<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Application;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @since 9.5.0
*/
class ErrorHandler {
/**
* Map between error codes and log levels.
*
* @var array
*/
const ERROR_LEVEL_MAP = [
E_ERROR => LogLevel::CRITICAL,
E_WARNING => LogLevel::WARNING,
E_PARSE => LogLevel::ALERT,
E_NOTICE => LogLevel::NOTICE,
E_CORE_ERROR => LogLevel::CRITICAL,
E_CORE_WARNING => LogLevel::WARNING,
E_COMPILE_ERROR => LogLevel::ALERT,
E_COMPILE_WARNING => LogLevel::WARNING,
E_USER_ERROR => LogLevel::ERROR,
E_USER_WARNING => LogLevel::WARNING,
E_USER_NOTICE => LogLevel::NOTICE,
E_STRICT => LogLevel::NOTICE,
E_RECOVERABLE_ERROR => LogLevel::ERROR,
E_DEPRECATED => LogLevel::NOTICE,
E_USER_DEPRECATED => LogLevel::NOTICE,
];
/**
* Fatal errors list.
*
* @var array
*/
const FATAL_ERRORS = [
E_ERROR,
E_PARSE,
E_CORE_ERROR,
E_COMPILE_ERROR,
E_USER_ERROR
];
/**
* Exit code to use on shutdown.
*
* @var int|null
*/
private $exit_code = null;
/**
* Flag to indicate if error should be forwarded to PHP internal error handler.
*
* @var boolean
*/
private $forward_to_internal_handler = true;
/**
* Logger instance.
*
* @var LoggerInterface
*/
private $logger;
/**
* Last fatal error trace.
*
* @var string
*/
private $last_fatal_trace;
/**
* Output handler to use. If not set, output will be directly echoed on a format depending on
* execution context (Web VS CLI).
*
* @var OutputInterface|null
*/
private $output_handler;
/**
* Reserved memory that will be used in case of an "out of memory" error.
*
* @var string
*/
private $reserved_memory;
/**
* @param LoggerInterface|null $logger
*/
public function __construct(LoggerInterface $logger = null) {
$this->logger = $logger;
}
/**
* Defines output handler.
*
* @param OutputInterface $output_handler
*
* @return void
*/
public function setOutputHandler(OutputInterface $output_handler) {
$this->output_handler = $output_handler;
}
/**
* Register error handler callbacks.
*
* @return void
*/
public function register() {
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleFatalError']);
$this->reserved_memory = str_repeat('x', 50 * 1024); // reserve 50 kB of memory space
}
/**
* Error handler.
*
* @param integer $error_code
* @param string $error_message
* @param string $filename
* @param integer $line_number
*
* @return boolean
*/
public function handleError($error_code, $error_message, $filename, $line_number) {
// Have to false to forward to PHP internal error handler.
$return = !$this->forward_to_internal_handler;
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
array_shift($trace); // Remove current method from trace
$error_trace = $this->getTraceAsString($trace);
if (in_array($error_code, self::FATAL_ERRORS)) {
// Fatal errors are handled by shutdown function
// (as some are not recoverable and cannot be handled here).
// Store backtrace to be able to use it there.
$this->last_fatal_trace = $error_trace;
return $return;
}
if (0 === error_reporting()) {
// Do not handle error if '@' operator is used on errored expression
// see https://www.php.net/manual/en/language.operators.errorcontrol.php
return $return;
}
$error_type = sprintf(
'PHP %s (%s)',
$this->codeToString($error_code),
$error_code
);
$error_description = sprintf(
'%s in %s at line %s',
$error_message,
$filename,
$line_number
);
$log_level = self::ERROR_LEVEL_MAP[$error_code];
$this->logErrorMessage($error_type, $error_description, $error_trace, $log_level);
$this->outputDebugMessage($error_type, $error_description, $log_level);
return $return;
}
/**
* SQL error handler.
*
* This handler is manually by application when a SQL error occured.
*
* @param integer $error_code
* @param string $error_message
* @param string $query
*
* @return void
*/
public function handleSqlError($error_code, $error_message, $query) {
$this->outputDebugMessage(
sprintf('SQL Error "%s"', $error_code),
sprintf('%s in query "%s"', $error_message, $query),
self::ERROR_LEVEL_MAP[E_USER_ERROR],
isCommandLine()
);
}
/**
* Exception handler.
*
* This handler is called by PHP prior to exiting, when an Exception is not catched.
*
* @param \Throwable $exception
*
* @return void
*/
public function handleException(\Throwable $exception) {
$this->exit_code = 255;
$error_type = sprintf(
'Uncaught Exception %s',
get_class($exception)
);
$error_description = sprintf(
'%s in %s at line %s',
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
);
$error_trace = $this->getTraceAsString($exception->getTrace());
$log_level = self::ERROR_LEVEL_MAP[E_ERROR];
$this->logErrorMessage($error_type, $error_description, $error_trace, $log_level);
$this->outputDebugMessage($error_type, $error_description, $log_level, isCommandLine());
}
/**
* Handle fatal errors.
*
* @retun void
*/
public function handleFatalError() {
// Free reserved memory to be able to handle "out of memory" errors
$this->reserved_memory = null;
$error = error_get_last();
if ($error && in_array($error['type'], self::FATAL_ERRORS)) {
$this->exit_code = 255;
$error_type = sprintf(
'PHP %s (%s)',
$this->codeToString($error['type']),
$error['type']
);
$error_description = sprintf(
'%s in %s at line %s',
$error['message'],
$error['file'],
$error['line']
);
// debug_backtrace is not available in shutdown function
// so get stored trace if any exists
$error_trace = $this->last_fatal_trace ?? '';
$log_level = self::ERROR_LEVEL_MAP[$error['type']];
$this->logErrorMessage($error_type, $error_description, $error_trace, $log_level);
$this->outputDebugMessage($error_type, $error_description, $log_level);
}
if ($this->exit_code !== null) {
// If an exit code is defined, register a shutdown function that will be called after
// thoose that are already defined, in order to exit the script with the correct code.
$exit_code = $this->exit_code;
register_shutdown_function(
'register_shutdown_function',
function () use ($exit_code) { exit($exit_code); }
);
}
}
/**
* Defines if errors should be forward to PHP internal error handler.
*
* @param bool $forward_to_internal_handler
*
* @return void
*/
public function setForwardToInternalHandler(bool $forward_to_internal_handler) {
$this->forward_to_internal_handler = $forward_to_internal_handler;
}
/**
* Log message related to error.
*
* @param string $type
* @param string $description
* @param string $trace
* @param string $log_level
*
* @return void
*/
private function logErrorMessage(string $type, string $description, string $trace, string $log_level) {
if (!($this->logger instanceof LoggerInterface)) {
return;
}
$this->logger->log(
$log_level,
' *** ' . $type . ': ' . $description . (!empty($trace) ? "\n" . $trace : '')
);
}
/**
* Output debug message related to error.
*
* @param string $error_type
* @param string $message
* @param string $log_level
* @param boolean $force
*
* @return void
*/
private function outputDebugMessage(string $error_type, string $message, string $log_level, bool $force = false) {
if ((!$force
&& (!isset($_SESSION['glpi_use_mode']) || $_SESSION['glpi_use_mode'] != \Session::DEBUG_MODE)) || isAPI()) {
return;
}
if ($this->output_handler instanceof OutputInterface) {
$format = 'comment';
switch ($log_level) {
case LogLevel::EMERGENCY:
case LogLevel::ALERT:
case LogLevel::CRITICAL:
case LogLevel::ERROR:
$format = 'error';
$verbosity = OutputInterface::VERBOSITY_QUIET;
break;
case LogLevel::WARNING:
$verbosity = OutputInterface::VERBOSITY_NORMAL;
break;
case LogLevel::NOTICE:
case LogLevel::INFO:
default:
$verbosity = OutputInterface::VERBOSITY_VERBOSE;
break;
case LogLevel::DEBUG:
$verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE;
break;
}
$message = $error_type . ': ' . $message;
if (null !== $format) {
$message = sprintf('<%1$s>%2$s</%1$s>', $format, $message);
}
$this->output_handler->writeln($message, $verbosity);
} else if (!isCommandLine()) {
echo '<div style="position:float-left; background-color:red; z-index:10000">'
. '<span class="b">' . $error_type . ': </span>' . $message . '</div>';
} else {
echo $error_type . ': ' . $message . "\n";
}
}
/**
* Get error type as string from error code.
*
* @param int $error_code
*
* @return string
*/
private function codeToString(int $error_code): string {
$map = [
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parsing Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User Notice',
E_STRICT => 'Runtime Notice',
E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
E_DEPRECATED => 'Deprecated function',
E_USER_DEPRECATED => 'User deprecated function',
];
return $map[$error_code] ?? 'Unknown error';
}
/**
* Get trace as string.
*
* @param array $trace
*
* @return string
*/
private function getTraceAsString(array $trace): string {
if (empty($trace)) {
return '';
}
$message = " Backtrace :\n";
foreach ($trace as $item) {
$script = ($item['file'] ?? '') . ':' . ($item['line'] ?? '');
if (strpos($script, GLPI_ROOT) === 0) {
$script = substr($script, strlen(GLPI_ROOT) + 1);
}
if (strlen($script) > 50) {
$script = '...' . substr($script, -47);
} else {
$script = str_pad($script, 50);
}
$call = ($item['class'] ?? '') . ($item['type'] ?? '') . ($item['function'] ?? '');
if (!empty($call)) {
$call .= '()';
}
$message .= " $script $call\n";
}
return $message;
}
}