Merge branch 'globalErrorHandling'

* globalErrorHandling:
  fixed plugin name output on load error
  no need to convert Errors to Exceptions
  FatalException and proper plugin detection
  add shutdown handler to even manage fatal errors
  reflow overlong line
  better exception handling on plugin loading
  log stacktrace to error log
  guess which plugin was the source of an exception
  introduce a global error handler
This commit is contained in:
Andreas Gohr 2020-12-03 20:01:33 +01:00
commit b102b83e6d
5 changed files with 213 additions and 33 deletions

145
inc/ErrorHandler.php Normal file
View File

@ -0,0 +1,145 @@
<?php
namespace dokuwiki;
use dokuwiki\Exception\FatalException;
/**
* Manage the global handling of errors and exceptions
*
* Developer may use this to log and display exceptions themselves
*/
class ErrorHandler
{
/**
* Register the default error handling
*/
public static function register()
{
if (!defined('DOKU_UNITTEST')) {
set_exception_handler([ErrorHandler::class, 'fatalException']);
register_shutdown_function([ErrorHandler::class, 'fatalShutdown']);
}
}
/**
* Default Exception handler to show a nice user message before dieing
*
* The exception is logged to the error log
*
* @param \Throwable $e
*/
public static function fatalException($e)
{
$plugin = self::guessPlugin($e);
$title = hsc(get_class($e) . ': ' . $e->getMessage());
$msg = 'An unforeseen error has occured. This is most likely a bug somewhere.';
if ($plugin) $msg .= ' It might be a problem in the ' . $plugin . ' plugin.';
$logged = self::logException($e)
? 'More info has been written to the DokuWiki _error.log'
: $e->getFile() . ':' . $e->getLine();
echo <<<EOT
<!DOCTYPE html>
<html>
<head><title>$title</title></head>
<body style="font-family: Arial, sans-serif">
<div style="width:60%; margin: auto; background-color: #fcc;
border: 1px solid #faa; padding: 0.5em 1em;">
<h1 style="font-size: 120%">$title</h1>
<p>$msg</p>
<p>$logged</p>
</div>
</body>
</html>
EOT;
}
/**
* Convenience method to display an error message for the given Exception
*
* @param \Throwable $e
* @param string $intro
*/
public static function showExceptionMsg($e, $intro = 'Error!')
{
$msg = hsc($intro) . '<br />' . hsc(get_class($e) . ': ' . $e->getMessage());
if (self::logException($e)) $msg .= '<br />More info is available in the _error.log';
msg($msg, -1);
}
/**
* Last resort to handle fatal errors that still can't be caught
*/
public static function fatalShutdown()
{
$error = error_get_last();
// Check if it's a core/fatal error, otherwise it's a normal shutdown
if (
$error !== null &&
in_array(
$error['type'],
[
E_ERROR,
E_CORE_ERROR,
E_COMPILE_ERROR,
]
)
) {
self::fatalException(
new FatalException($error['message'], 0, $error['type'], $error['file'], $error['line'])
);
}
}
/**
* Log the given exception to the error log
*
* @param \Throwable $e
* @return bool false if the logging failed
*/
public static function logException($e)
{
global $conf;
$log = join("\t", [
gmdate('c'),
get_class($e),
$e->getFile() . '(' . $e->getLine() . ')',
$e->getMessage(),
]) . "\n";
$log .= $e->getTraceAsString() . "\n";
return io_saveFile($conf['cachedir'] . '/_error.log', $log, true);
}
/**
* Checks the the stacktrace for plugin files
*
* @param \Throwable $e
* @return false|string
*/
protected static function guessPlugin($e)
{
if (preg_match('/lib\/plugins\/(\w+)\//', str_replace('\\', '/', $e->getFile()), $match)) {
return $match[1];
}
foreach ($e->getTrace() as $line) {
if (
isset($line['class']) &&
preg_match('/\w+?_plugin_(\w+)/', $line['class'], $match)
) {
return $match[1];
}
if (
isset($line['file']) &&
preg_match('/lib\/plugins\/(\w+)\//', str_replace('\\', '/', $line['file']), $match)
) {
return $match[1];
}
}
return false;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace dokuwiki\Exception;
/**
* Fatal Errors are converted into this Exception in out Shutdown handler
*/
class FatalException extends \ErrorException
{
}

View File

@ -2,6 +2,8 @@
namespace dokuwiki\Extension;
use dokuwiki\ErrorHandler;
/**
* Class to encapsulate access to dokuwiki plugins
*
@ -90,9 +92,11 @@ class PluginController
$class = $type . '_plugin_' . $name;
try {
//plugin already loaded?
if (!empty($DOKU_PLUGINS[$type][$name])) {
if ($new || !$DOKU_PLUGINS[$type][$name]->isSingleton()) {
return class_exists($class, true) ? new $class : null;
}
@ -101,7 +105,6 @@ class PluginController
//construct class and instantiate
if (!class_exists($class, true)) {
# the plugin might be in the wrong directory
$inf = confToHash(DOKU_PLUGIN . "$plugin/plugin.info.txt");
if ($inf['base'] && $inf['base'] != $plugin) {
@ -117,15 +120,21 @@ class PluginController
} elseif (preg_match('/^' . DOKU_PLUGIN_NAME_REGEX . '$/', $plugin) !== 1) {
msg(
sprintf(
"Plugin name '%s' is not a valid plugin name, only the characters a-z and 0-9 are allowed. " .
"Plugin name '%s' is not a valid plugin name, only the characters a-z ".
"and 0-9 are allowed. " .
'Maybe the plugin has been installed in the wrong directory?', hsc($plugin)
), -1
);
}
return null;
}
$DOKU_PLUGINS[$type][$name] = new $class;
} catch (\Throwable $e) {
ErrorHandler::showExceptionMsg($e, sprintf('Failed to load plugin %s', $plugin));
return null;
}
return $DOKU_PLUGINS[$type][$name];
}

View File

@ -199,6 +199,9 @@ if (empty($plugin_controller_class)) $plugin_controller_class = dokuwiki\Extensi
require_once(DOKU_INC.'vendor/autoload.php');
require_once(DOKU_INC.'inc/load.php');
// from now on everything is an exception
\dokuwiki\ErrorHandler::register();
// disable gzip if not available
define('DOKU_HAS_BZIP', function_exists('bzopen'));
define('DOKU_HAS_GZIP', function_exists('gzopen'));

View File

@ -108,7 +108,11 @@ function load_autoload($name){
$name = str_replace('/test/', '/_test/', $name); // no underscore in test namespace
$file = DOKU_PLUGIN . substr($name, 16) . '.php';
if(file_exists($file)) {
try {
require $file;
} catch (\Throwable $e) {
\dokuwiki\ErrorHandler::showExceptionMsg($e, "Error loading plugin $name");
}
return true;
}
}
@ -118,7 +122,11 @@ function load_autoload($name){
$name = str_replace('/test/', '/_test/', $name); // no underscore in test namespace
$file = DOKU_INC.'lib/tpl/' . substr($name, 18) . '.php';
if(file_exists($file)) {
try {
require $file;
} catch (\Throwable $e) {
\dokuwiki\ErrorHandler::showExceptionMsg($e, "Error loading template $name");
}
return true;
}
}
@ -144,7 +152,11 @@ function load_autoload($name){
$c = ((count($m) === 4) ? "/{$m[3]}" : '');
$plg = DOKU_PLUGIN . "{$m[2]}/{$m[1]}$c.php";
if(file_exists($plg)){
try {
require $plg;
} catch (\Throwable $e) {
\dokuwiki\ErrorHandler::showExceptionMsg($e, "Error loading plugin {$m[2]}");
}
}
return true;
}