Commit 7cc8303d authored by Cédric Anne's avatar Cédric Anne Committed by Johan Cwiklinski
Browse files

Add log and debug display for exception and fatal errors (#6726)

* Add log and debug display for exception and fatal errors

* Add unit tests and fixes @ operator usage

* Use Monolog TestHandler to simplify tests

* Remove useless imports

* Make tests fails if log is not empty
parent 001a421f
......@@ -26,6 +26,7 @@ The present file will list all changes made to the project; according to the
### Changed
- PHP error_reporting and display_errors configuration directives are no longer overrided by GLPI, unless in debug mode (which forces reporting and display of all errors).
- `scripts/migrations/racks_plugin.php` has been replaced by `glpi:migration:racks_plugin_to_core` command available using `bin/console`
### API changes
......@@ -70,6 +71,8 @@ The present file will list all changes made to the project; according to the
- `Config::checkWriteAccessToDirs()`
- `Config::displayCheckExtensions()`
- `Toolbox::checkSELinux()`
- `Toolbox::userErrorHandlerDebug()`
- `Toolbox::userErrorHandlerNormal()`
#### Removed
......
......@@ -109,7 +109,6 @@ abstract class API extends CommonGLPI {
self::$api_url = trim($CFG_GLPI['url_base_api'], "/");
// Don't display error in result
set_error_handler(['Toolbox', 'userErrorHandlerNormal']);
ini_set('display_errors', 'Off');
// Avoid keeping messages between api calls
......
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2018 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;
/**
* @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
];
/**
* 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 array
*/
private $last_fatal_trace;
/**
* 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;
}
/**
* 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;
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;
}
$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 = $trace;
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
);
$this->logErrorMessage(
$error_type,
$error_description,
$error_trace,
self::ERROR_LEVEL_MAP[$error_code]
);
$this->outputDebugMessage($error_type, $error_description);
return $return;
}
/**
* 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) {
if (0 === error_reporting()) {
// Do not handle exception if '@' operator is used on errored expression
// see https://www.php.net/manual/en/language.operators.errorcontrol.php
return;
}
$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());
$this->logErrorMessage(
$error_type,
$error_description,
$error_trace,
self::ERROR_LEVEL_MAP[E_ERROR]
);
$this->outputDebugMessage($error_type, $error_description);
}
/**
* Handle fatal errors.
*
* @retun void
*/
public function handleFatalError() {
if (0 === error_reporting()) {
// Do not handle exception if '@' operator is used on errored expression
// see https://www.php.net/manual/en/language.operators.errorcontrol.php
return;
}
// 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)) {
$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
$trace = $this->last_fatal_trace ?? [];
$error_trace = $this->getTraceAsString($trace);
$this->logErrorMessage(
$error_type,
$error_description,
$error_trace,
self::ERROR_LEVEL_MAP[$error['type']]
);
$this->outputDebugMessage($error_type, $error_description);
}
}
/**
* 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
*
* @return void
*/
private function outputDebugMessage(string $error_type, string $message) {
if (!isset($_SESSION['glpi_use_mode']) || $_SESSION['glpi_use_mode'] != \Session::DEBUG_MODE) {
return;
}
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;
}
}
......@@ -51,6 +51,7 @@ if (!isset($_SESSION['glpi_use_mode'])) {
$GLPI = new GLPI();
$GLPI->initLogger();
$GLPI->initErrorHandler();
//init cache
$GLPI_CACHE = Config::getCache('cache_db');
......
......@@ -252,6 +252,7 @@ class Application extends BaseApplication {
global $GLPI;
$GLPI = new GLPI();
$GLPI->initLogger();
$GLPI->initErrorHandler();
Config::detectRootDoc();
}
......
......@@ -30,6 +30,7 @@
* ---------------------------------------------------------------------
*/
use Glpi\Application\ErrorHandler;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\TestHandler;
......@@ -44,6 +45,7 @@ if (!defined('GLPI_ROOT')) {
**/
class GLPI {
private $error_handler;
private $loggers;
private $log_level;
......@@ -93,4 +95,16 @@ class GLPI {
public function getLogLevel() {
return $this->log_level;
}
/**
* Init and register error handler.
*
* @return void
*/
public function initErrorHandler() {
global $PHPLOGGER;
$this->error_handler = new ErrorHandler($PHPLOGGER);
$this->error_handler->register();
}
}
......@@ -423,6 +423,10 @@ class Toolbox {
}
}
if (defined('TU_USER') && $level >= Logger::NOTICE) {
throw new \RuntimeException($msg);
}
$tps = microtime(true);
if ($logger === null) {
......@@ -437,9 +441,7 @@ class Toolbox {
error_log($e);
}
if (defined('TU_USER') && $level >= Logger::NOTICE) {
throw new \RuntimeException($msg);
} else if (isCommandLine() && $level >= Logger::WARNING) {
if (isCommandLine() && $level >= Logger::WARNING) {
echo $msg;
}
}
......@@ -612,9 +614,13 @@ class Toolbox {
* @param integer $linenum line number the error was raised at.
*
* @return string Error type
*
* @deprecated 9.5.0
**/
static function userErrorHandlerNormal($errno, $errmsg, $filename, $linenum) {
Toolbox::deprecated();
$errortype = [E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parsing Error',
......@@ -676,9 +682,13 @@ class Toolbox {
* @param integer $linenum line number the error was raised at.
*
* @return void
*
* @deprecated 9.5.0
**/
static function userErrorHandlerDebug($errno, $errmsg, $filename, $linenum) {
Toolbox::deprecated();
// For file record
$type = self::userErrorHandlerNormal($errno, $errmsg, $filename, $linenum);
......@@ -729,15 +739,10 @@ class Toolbox {
// If debug mode activated : display some information
if ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE) {
// Recommended development settings
ini_set('display_errors', 'On');
// Force reporting of all errors
error_reporting(E_ALL);
set_error_handler(['Toolbox','userErrorHandlerDebug']);
} else if (!defined('TU_USER')) {
// Recommended production settings
// Disable native error displaying as it will be done by custom handler
ini_set('display_errors', 'Off');
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
set_error_handler(['Toolbox', 'userErrorHandlerNormal']);
}
}
......
......@@ -37,6 +37,7 @@ include_once (GLPI_ROOT . "/inc/db.function.php");
$GLPI = new GLPI();
$GLPI->initLogger();
$GLPI->initErrorHandler();
Config::detectRootDoc();
......
......@@ -40,6 +40,7 @@ include_once (GLPI_CONFIG_DIR . "/config_db.php");
$GLPI = new GLPI();
$GLPI->initLogger();
$GLPI->initErrorHandler();
$GLPI_CACHE = Config::getCache('cache_db');
$GLPI_CACHE->clear(); // Force cache cleaning to prevent usage of outdated cache data
......
......@@ -75,6 +75,23 @@ class GLPITestCase extends atoum {
}
$GLPI_CACHE = false;
}
global $PHPLOGGER;
$handlers = $PHPLOGGER->getHandlers();
foreach ($handlers as $handler) {
$records = $handler->getRecords();
$messages = array_column($records, 'message');
$this->integer(count($records))
->isEqualTo(
0,
sprintf(
'Unexpected logs records found in %s::%s() test: %s',
static::class,
$method,
"\n" . implode("\n", $messages)
)
);
}
}
/**
......
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2018 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 tests\units\Glpi\Application;
use Psr\Log\LogLevel;
class ErrorHandler extends \GLPITestCase {
public function afterTestMethod($method) {
switch ($method) {
case 'testRegisteredHandleError':
// Restore previous handlers
restore_error_handler();
restore_exception_handler();
break;
}
parent::afterTestMethod($method);
}
/**
* @return array