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; namespace dokuwiki\Extension;
use dokuwiki\ErrorHandler;
/** /**
* Class to encapsulate access to dokuwiki plugins * Class to encapsulate access to dokuwiki plugins
* *
@ -90,42 +92,49 @@ class PluginController
$class = $type . '_plugin_' . $name; $class = $type . '_plugin_' . $name;
//plugin already loaded? try {
if (!empty($DOKU_PLUGINS[$type][$name])) { //plugin already loaded?
if ($new || !$DOKU_PLUGINS[$type][$name]->isSingleton()) { if (!empty($DOKU_PLUGINS[$type][$name])) {
return class_exists($class, true) ? new $class : null; if ($new || !$DOKU_PLUGINS[$type][$name]->isSingleton()) {
return class_exists($class, true) ? new $class : null;
}
return $DOKU_PLUGINS[$type][$name];
} }
return $DOKU_PLUGINS[$type][$name]; //construct class and instantiate
} if (!class_exists($class, true)) {
# the plugin might be in the wrong directory
//construct class and instantiate $inf = confToHash(DOKU_PLUGIN . "$plugin/plugin.info.txt");
if (!class_exists($class, true)) { if ($inf['base'] && $inf['base'] != $plugin) {
msg(
# the plugin might be in the wrong directory sprintf(
$inf = confToHash(DOKU_PLUGIN . "$plugin/plugin.info.txt"); "Plugin installed incorrectly. Rename plugin directory '%s' to '%s'.",
if ($inf['base'] && $inf['base'] != $plugin) { hsc($plugin),
msg( hsc(
sprintf( $inf['base']
"Plugin installed incorrectly. Rename plugin directory '%s' to '%s'.", )
hsc($plugin), ), -1
hsc( );
$inf['base'] } elseif (preg_match('/^' . DOKU_PLUGIN_NAME_REGEX . '$/', $plugin) !== 1) {
) msg(
), -1 sprintf(
); "Plugin name '%s' is not a valid plugin name, only the characters a-z ".
} elseif (preg_match('/^' . DOKU_PLUGIN_NAME_REGEX . '$/', $plugin) !== 1) { "and 0-9 are allowed. " .
msg( 'Maybe the plugin has been installed in the wrong directory?', hsc($plugin)
sprintf( ), -1
"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 null;
} }
$DOKU_PLUGINS[$type][$name] = new $class;
return $DOKU_PLUGINS[$type][$name]; 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.'vendor/autoload.php');
require_once(DOKU_INC.'inc/load.php'); require_once(DOKU_INC.'inc/load.php');
// from now on everything is an exception
\dokuwiki\ErrorHandler::register();
// disable gzip if not available // disable gzip if not available
define('DOKU_HAS_BZIP', function_exists('bzopen')); define('DOKU_HAS_BZIP', function_exists('bzopen'));
define('DOKU_HAS_GZIP', function_exists('gzopen')); 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 $name = str_replace('/test/', '/_test/', $name); // no underscore in test namespace
$file = DOKU_PLUGIN . substr($name, 16) . '.php'; $file = DOKU_PLUGIN . substr($name, 16) . '.php';
if(file_exists($file)) { if(file_exists($file)) {
require $file; try {
require $file;
} catch (\Throwable $e) {
\dokuwiki\ErrorHandler::showExceptionMsg($e, "Error loading plugin $name");
}
return true; return true;
} }
} }
@ -118,7 +122,11 @@ function load_autoload($name){
$name = str_replace('/test/', '/_test/', $name); // no underscore in test namespace $name = str_replace('/test/', '/_test/', $name); // no underscore in test namespace
$file = DOKU_INC.'lib/tpl/' . substr($name, 18) . '.php'; $file = DOKU_INC.'lib/tpl/' . substr($name, 18) . '.php';
if(file_exists($file)) { if(file_exists($file)) {
require $file; try {
require $file;
} catch (\Throwable $e) {
\dokuwiki\ErrorHandler::showExceptionMsg($e, "Error loading template $name");
}
return true; return true;
} }
} }
@ -144,7 +152,11 @@ function load_autoload($name){
$c = ((count($m) === 4) ? "/{$m[3]}" : ''); $c = ((count($m) === 4) ? "/{$m[3]}" : '');
$plg = DOKU_PLUGIN . "{$m[2]}/{$m[1]}$c.php"; $plg = DOKU_PLUGIN . "{$m[2]}/{$m[1]}$c.php";
if(file_exists($plg)){ if(file_exists($plg)){
require $plg; try {
require $plg;
} catch (\Throwable $e) {
\dokuwiki\ErrorHandler::showExceptionMsg($e, "Error loading plugin {$m[2]}");
}
} }
return true; return true;
} }