feat: log plugin runtime errors

This commit is contained in:
Marek Miklewicz
2026-06-02 22:57:23 +02:00
parent 7561077eee
commit 86eb1dddbd
8 changed files with 110 additions and 12 deletions
+4
View File
@@ -10,6 +10,9 @@ if (!defined('IN_DA_PLUGIN') || IN_DA_PLUGIN !== true) {
define('PLUGIN_ROOT', dirname(__DIR__)); define('PLUGIN_ROOT', dirname(__DIR__));
define('PLUGIN_EXEC_DIR', __DIR__); define('PLUGIN_EXEC_DIR', __DIR__);
require_once PLUGIN_EXEC_DIR . '/lib/PluginLogger.php';
PluginLogger::init(PLUGIN_ROOT . '/plugin.log');
foreach ([ foreach ([
'Settings.php', 'Settings.php',
'BackendRuntimeConfig.php', 'BackendRuntimeConfig.php',
@@ -58,6 +61,7 @@ try {
} }
$handler($ctx); $handler($ctx);
} catch (Throwable $e) { } catch (Throwable $e) {
PluginLogger::exception($e, 'BOOTSTRAP');
http_response_code(500); http_response_code(500);
header('Content-Type: text/html; charset=UTF-8'); header('Content-Type: text/html; charset=UTF-8');
echo '<!doctype html><html><head><meta charset="utf-8"><title>Błąd</title></head><body>'; echo '<!doctype html><html><head><meta charset="utf-8"><title>Błąd</title></head><body>';
+1
View File
@@ -18,6 +18,7 @@ return static function (AppContext $ctx): void {
} }
Http::redirect($ctx->url('index.html', ['status' => 'created'])); Http::redirect($ctx->url('index.html', ['status' => 'created']));
} catch (Throwable $e) { } catch (Throwable $e) {
PluginLogger::exception($e, 'CREATE');
$errors[] = $e->getMessage(); $errors[] = $e->getMessage();
} }
} }
+1
View File
@@ -16,6 +16,7 @@ return static function (AppContext $ctx): void {
$ctx->audit->record('delete', $ctx->daUser->username(), ['rule_id' => $id, 'subject_prefix' => (string)($rule['subject_prefix'] ?? $rule['subject'] ?? '')]); $ctx->audit->record('delete', $ctx->daUser->username(), ['rule_id' => $id, 'subject_prefix' => (string)($rule['subject_prefix'] ?? $rule['subject'] ?? '')]);
Http::redirect($ctx->url('index.html', ['status' => 'deleted'])); Http::redirect($ctx->url('index.html', ['status' => 'deleted']));
} catch (Throwable $e) { } catch (Throwable $e) {
PluginLogger::exception($e, 'DELETE');
$ctx->render('Confirm deletion', '<div class="alert alert-err">' . AppContext::e($e->getMessage()) . '</div>'); $ctx->render('Confirm deletion', '<div class="alert alert-err">' . AppContext::e($e->getMessage()) . '</div>');
return; return;
} }
+5
View File
@@ -2,6 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
return static function (AppContext $ctx): void { return static function (AppContext $ctx): void {
try {
$ctx->requirePost(); $ctx->requirePost();
$id = $ctx->post('id'); $id = $ctx->post('id');
$ctx->requireCsrf('toggle:' . $id); $ctx->requireCsrf('toggle:' . $id);
@@ -14,4 +15,8 @@ return static function (AppContext $ctx): void {
$ctx->rules->update($ctx->daUser->username(), $id, $next); $ctx->rules->update($ctx->daUser->username(), $id, $next);
$ctx->syncActiveBackend(); $ctx->syncActiveBackend();
Http::redirect($ctx->url('index.html', ['status' => 'toggled'])); Http::redirect($ctx->url('index.html', ['status' => 'toggled']));
} catch (Throwable $e) {
PluginLogger::exception($e, 'TOGGLE');
throw $e;
}
}; };
+1
View File
@@ -19,6 +19,7 @@ return static function (AppContext $ctx): void {
$ctx->syncActiveBackend(); $ctx->syncActiveBackend();
Http::redirect($ctx->url('index.html', ['status' => 'updated'])); Http::redirect($ctx->url('index.html', ['status' => 'updated']));
} catch (Throwable $e) { } catch (Throwable $e) {
PluginLogger::exception($e, 'UPDATE');
$errors[] = $e->getMessage(); $errors[] = $e->getMessage();
} }
} }
+70
View File
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
final class PluginLogger
{
private static string $path = '';
public static function init(string $path): void
{
self::$path = $path;
@ini_set('log_errors', '1');
@ini_set('error_log', $path);
set_error_handler(static function (int $severity, string $message, string $file, int $line): bool {
if ((error_reporting() & $severity) === 0) {
return false;
}
self::log('PHP_ERROR', self::severityName($severity) . ': ' . $message . ' in ' . $file . ':' . $line);
return false;
});
register_shutdown_function(static function (): void {
$error = error_get_last();
if (!is_array($error)) {
return;
}
$fatal = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR];
if (in_array((int)($error['type'] ?? 0), $fatal, true)) {
self::log('PHP_FATAL', self::severityName((int)$error['type']) . ': ' . (string)$error['message'] . ' in ' . (string)$error['file'] . ':' . (string)$error['line']);
}
});
}
public static function exception(Throwable $e, string $context = 'UNCAUGHT'): void
{
self::log($context, get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() . "\n" . $e->getTraceAsString());
}
public static function log(string $level, string $message): void
{
$path = self::$path !== '' ? self::$path : (defined('PLUGIN_ROOT') ? PLUGIN_ROOT . '/plugin.log' : __DIR__ . '/../../plugin.log');
$line = '[' . gmdate('c') . '] ' . $level . ' ' . str_replace(["\r\n", "\r"], "\n", $message) . "\n";
$ok = @file_put_contents($path, $line, FILE_APPEND | LOCK_EX);
if ($ok !== false && is_file($path)) {
@chmod($path, 0600);
}
}
private static function severityName(int $severity): string
{
return match ($severity) {
E_ERROR => 'E_ERROR',
E_WARNING => 'E_WARNING',
E_PARSE => 'E_PARSE',
E_NOTICE => 'E_NOTICE',
E_CORE_ERROR => 'E_CORE_ERROR',
E_CORE_WARNING => 'E_CORE_WARNING',
E_COMPILE_ERROR => 'E_COMPILE_ERROR',
E_COMPILE_WARNING => 'E_COMPILE_WARNING',
E_USER_ERROR => 'E_USER_ERROR',
E_USER_WARNING => 'E_USER_WARNING',
E_USER_NOTICE => 'E_USER_NOTICE',
E_STRICT => 'E_STRICT',
E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
E_DEPRECATED => 'E_DEPRECATED',
E_USER_DEPRECATED => 'E_USER_DEPRECATED',
default => 'E_' . $severity,
};
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ name=global-autoresponder
id=global-autoresponder id=global-autoresponder
type=user type=user
author=HITME.PL author=HITME.PL
version=1.1.1 version=1.1.2
active=no active=no
installed=no installed=no
user_run_as=root user_run_as=root
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
require_once PLUGIN_ROOT . '/exec/lib/PluginLogger.php';
test('plugin logger writes exceptions to plugin log file', function (): void {
$path = TEST_TMP . '/plugin.log';
PluginLogger::init($path);
PluginLogger::exception(new RuntimeException('DirectAdmin API command failed'), 'CREATE');
$log = file_get_contents($path) ?: '';
assert_contains('CREATE RuntimeException: DirectAdmin API command failed', $log);
assert_contains('plugin_logger_test.php', $log);
assert_same('0600', substr(sprintf('%o', fileperms($path)), -4));
});