mirror of
https://github.com/Shadowss/TravianZ.git
synced 2026-06-27 16:14:30 +00:00
feat(admin): add transparent debug error-log mode
Add an admin-controlled debug mode that captures PHP errors of all players into var/log/debug-players.log, to hunt remaining PHP 8.3 bugs from real play sessions. Fully transparent to players: no redirect, no gameplay change, errors are never displayed. - DB: new debug_log table (one row), mirroring the maintenance pattern. - Database: getDebugMode()/setDebugMode()/setDebugSettings(), defensive when the table is absent (no blank page). - Session: register a custom error + shutdown handler when enabled; the handler runs even when php.ini error_reporting masks warnings/notices, so capture is complete without a Docker rebuild. Auto-disables after a configurable window. - DebugErrorLogger: size-capped file with a single .log.1 rotation, honours the @ operator, never throws. - Admin: new "Debug Error Log" page (levels, size cap, auto-off, on-page viewer, clear, download) + debugLog action mod. - Menu: admin-only quick on/off widget (TZ_DEBUG_ON/OFF, EN/FR/RO). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
Catalin Novgorodschi
parent
63c94fa961
commit
827354a622
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<head>
|
||||
<link rel="shortcut icon" href="favicon.ico"/>
|
||||
<title><?php echo ($_SESSION['access'] == ADMIN ? 'Admin Control Panel' : 'Multihunter Control Panel'); ?> - TravianZ</title>
|
||||
<link rel="stylesheet" type="text/css" href="../img/admin/admin.css">
|
||||
<link rel="stylesheet" type="text/css" href="../img/admin/acp.css">
|
||||
<link rel="stylesheet" type="text/css" href="../img/../img.css">
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta http-equiv="imagetoolbar" content="no">
|
||||
<style>
|
||||
.dbg-wrap{max-width:100%;margin:12px;font-family:Tahoma,Verdana,Arial,sans-serif;color:#222}
|
||||
.dbg-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
|
||||
.dbg-head h2{margin:0;font-size:16px}
|
||||
.dbg-state{font-weight:700;padding:3px 10px;border-radius:14px;color:#fff;font-size:11px}
|
||||
.dbg-on{background:#16a34a}.dbg-off{background:#dc2626}
|
||||
.dbg-card{background:#fff;border:1px solid #bbb;border-radius:6px;padding:12px;margin-bottom:12px}
|
||||
.dbg-card h3{margin:0 0 10px;font-size:13px;color:#0f172a;border-bottom:1px solid #eee;padding-bottom:6px}
|
||||
.dbg-row{display:flex;flex-wrap:wrap;gap:18px;align-items:center;margin-bottom:8px;font-size:12px}
|
||||
.dbg-row label{display:flex;align-items:center;gap:5px;cursor:pointer}
|
||||
.dbg-row input[type=number]{width:70px;padding:3px 5px;border:1px solid #bbb;border-radius:4px}
|
||||
.dbg-btn{padding:6px 14px;font-size:12px;border:1px solid #bbb;border-radius:6px;background:#f5f5f5;cursor:pointer;text-decoration:none;color:#222;display:inline-block}
|
||||
.dbg-btn.primary{background:#2563eb;color:#fff;border-color:#2563eb}
|
||||
.dbg-btn.green{background:#16a34a;color:#fff;border-color:#16a34a}
|
||||
.dbg-btn.red{background:#dc2626;color:#fff;border-color:#dc2626}
|
||||
.dbg-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
|
||||
.dbg-log{background:#0f172a;color:#d1fae5;font-family:Consolas,Monaco,monospace;font-size:11px;line-height:15px;
|
||||
padding:10px;border-radius:6px;max-height:480px;overflow:auto;white-space:pre-wrap;word-break:break-word}
|
||||
.dbg-note{font-size:11px;color:#64748b;margin-top:6px}
|
||||
</style>
|
||||
</head>
|
||||
<?php
|
||||
#################################################################################
|
||||
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Filename : debug_log.tpl ##
|
||||
## Type : Admin Panel Frontend (Debug Error Log) ##
|
||||
## Project : TravianZ ##
|
||||
## GitHub : https://github.com/Shadowss/TravianZ ##
|
||||
## License : TravianZ Project ##
|
||||
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
#################################################################################
|
||||
|
||||
if($_SESSION['access'] < ADMIN) die("Access Denied: You are not Admin!");
|
||||
|
||||
$cfg = $database->getDebugMode();
|
||||
$isOn = !empty($cfg['active']);
|
||||
|
||||
// Resolve project root (max 5 levels up) to find the log file.
|
||||
$autoprefix = '';
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$autoprefix = str_repeat('../', $i);
|
||||
if (file_exists($autoprefix . 'autoloader.php')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$logFile = $autoprefix . 'var/log/debug-players.log';
|
||||
|
||||
// Read the last lines for the on-screen viewer.
|
||||
$maxLines = 400;
|
||||
$lines = [];
|
||||
$logSize = 0;
|
||||
if (is_file($logFile)) {
|
||||
$logSize = filesize($logFile);
|
||||
$all = file($logFile, FILE_IGNORE_NEW_LINES);
|
||||
if ($all !== false) {
|
||||
$lines = array_slice($all, -$maxLines);
|
||||
}
|
||||
}
|
||||
|
||||
// Active-since label + auto-off info.
|
||||
$since = !empty($cfg['started_at']) ? date('d.m.Y H:i', $cfg['started_at']) : '-';
|
||||
$autoOff = (int)($cfg['auto_off_hours'] ?? 0);
|
||||
?>
|
||||
<div class="dbg-wrap">
|
||||
|
||||
<div class="dbg-head">
|
||||
<h2>🐞 Debug Error Log</h2>
|
||||
<span class="dbg-state <?php echo $isOn ? 'dbg-on' : 'dbg-off'; ?>">
|
||||
<?php echo $isOn ? 'CAPTURE ON' : 'CAPTURE OFF'; ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="dbg-card">
|
||||
<h3>Status</h3>
|
||||
<div class="dbg-row">
|
||||
<span>Capture is <b><?php echo $isOn ? 'ON' : 'OFF'; ?></b><?php echo $isOn ? ' since '.$since : ''; ?>.</span>
|
||||
<span>Auto-off: <b><?php echo $autoOff > 0 ? $autoOff.' h' : 'never'; ?></b></span>
|
||||
<span>Log size: <b><?php echo number_format($logSize / 1024, 1); ?> KB</b></span>
|
||||
</div>
|
||||
<form action="../GameEngine/Admin/Mods/debugLog.php" method="POST" style="display:inline">
|
||||
<input type="hidden" name="do" value="toggle">
|
||||
<input type="hidden" name="active" value="<?php echo $isOn ? 0 : 1; ?>">
|
||||
<button type="submit" class="dbg-btn <?php echo $isOn ? 'red' : 'green'; ?>">
|
||||
<?php echo $isOn ? 'Turn capture OFF' : 'Turn capture ON'; ?>
|
||||
</button>
|
||||
</form>
|
||||
<p class="dbg-note">Transparent to players: errors are only written to the log file, never shown in-game and gameplay is unaffected.</p>
|
||||
</div>
|
||||
|
||||
<div class="dbg-card">
|
||||
<h3>Capture settings</h3>
|
||||
<form action="../GameEngine/Admin/Mods/debugLog.php" method="POST">
|
||||
<input type="hidden" name="do" value="save">
|
||||
<div class="dbg-row">
|
||||
<label><input type="checkbox" name="lvl_warning" <?php echo !empty($cfg['lvl_warning']) ? 'checked' : ''; ?>> Warnings</label>
|
||||
<label><input type="checkbox" name="lvl_notice" <?php echo !empty($cfg['lvl_notice']) ? 'checked' : ''; ?>> Notices</label>
|
||||
<label><input type="checkbox" name="lvl_deprecated" <?php echo !empty($cfg['lvl_deprecated']) ? 'checked' : ''; ?>> Deprecated</label>
|
||||
<label><input type="checkbox" name="lvl_fatal" <?php echo !empty($cfg['lvl_fatal']) ? 'checked' : ''; ?>> Fatal errors</label>
|
||||
</div>
|
||||
<div class="dbg-row">
|
||||
<label>Max file size (MB):
|
||||
<input type="number" name="max_size_mb" min="1" max="200" value="<?php echo (int)($cfg['max_size_mb'] ?? 5); ?>">
|
||||
</label>
|
||||
<label>Auto-off after (hours, 0 = never):
|
||||
<input type="number" name="auto_off_hours" min="0" max="168" value="<?php echo (int)($cfg['auto_off_hours'] ?? 6); ?>">
|
||||
</label>
|
||||
<button type="submit" class="dbg-btn primary">Save settings</button>
|
||||
</div>
|
||||
<p class="dbg-note">Beyond the size cap the file is rotated to a single <code>.log.1</code> backup, so the total volume stays bounded.</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="dbg-card">
|
||||
<h3>Last <?php echo $maxLines; ?> lines</h3>
|
||||
<div class="dbg-actions">
|
||||
<a class="dbg-btn" href="../GameEngine/Admin/Mods/debugLog.php?do=download">⬇ Download full log</a>
|
||||
<form action="../GameEngine/Admin/Mods/debugLog.php" method="POST" style="display:inline"
|
||||
onsubmit="return confirm('Clear the debug log file?');">
|
||||
<input type="hidden" name="do" value="clear">
|
||||
<button type="submit" class="dbg-btn red">🗑 Clear log</button>
|
||||
</form>
|
||||
<a class="dbg-btn" href="?p=debug_log">↻ Refresh</a>
|
||||
</div>
|
||||
<div class="dbg-log" style="margin-top:8px"><?php
|
||||
if (empty($lines)) {
|
||||
echo "(log is empty)";
|
||||
} else {
|
||||
foreach ($lines as $l) {
|
||||
echo htmlspecialchars($l) . "\n";
|
||||
}
|
||||
}
|
||||
?></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -147,6 +147,10 @@ if (!empty($_GET['p'])) {
|
||||
$subpage = 'Server Settings';
|
||||
break;
|
||||
|
||||
case 'debug_log':
|
||||
$subpage = 'Debug Error Log';
|
||||
break;
|
||||
|
||||
case 'editServerSet':
|
||||
$subpage = 'Server Configuration';
|
||||
break;
|
||||
@@ -622,6 +626,7 @@ body.app #lmid3 a{color:#15803d !important;font-weight:600 !important;}
|
||||
<li class="sub"><a href="#">Admin</a>
|
||||
<ul>
|
||||
<li><a href="?p=admin_log"><font color="Red"><b>Admin Log</b></font></a></li>
|
||||
<li><a href="?p=debug_log">Debug Error Log</a></li>
|
||||
<li><a href="?p=config">Server Settings</a></li>
|
||||
<li><a href="?p=maintenance">Server Maintenance</a></li>
|
||||
<li><a href="?p=resetServer">Server Resetting</a></li>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
#################################################################################
|
||||
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Filename debugLog.php ##
|
||||
## Type : Admin action (Debug Error Log) ##
|
||||
## License: TravianZ Project ##
|
||||
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
|
||||
## ##
|
||||
## Handles the admin actions of the Debug Error Log page: ##
|
||||
## do=save -> persist capture settings (levels, size cap, auto-off) ##
|
||||
## do=toggle -> turn the debug capture on/off ##
|
||||
## do=clear -> empty the log file(s) ##
|
||||
## do=download -> stream the log file as a download ##
|
||||
#################################################################################
|
||||
|
||||
if(!isset($_SESSION)) session_start();
|
||||
if(($_SESSION['access'] ?? 0) < 9) die("Access denied: You are not Admin!");
|
||||
|
||||
include_once("../../Database.php");
|
||||
|
||||
// Resolve project root (max 5 levels up), like the rest of the codebase.
|
||||
$autoprefix = '';
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$autoprefix = str_repeat('../', $i);
|
||||
if (file_exists($autoprefix . 'autoloader.php')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$logFile = $autoprefix . 'var/log/debug-players.log';
|
||||
|
||||
$uid = (int)($_SESSION['id_user'] ?? 0);
|
||||
$do = $_REQUEST['do'] ?? '';
|
||||
|
||||
switch ($do) {
|
||||
|
||||
case 'save':
|
||||
$database->setDebugSettings(
|
||||
isset($_POST['lvl_warning']),
|
||||
isset($_POST['lvl_notice']),
|
||||
isset($_POST['lvl_deprecated']),
|
||||
isset($_POST['lvl_fatal']),
|
||||
$_POST['max_size_mb'] ?? 5,
|
||||
$_POST['auto_off_hours'] ?? 6
|
||||
);
|
||||
$database->query("Insert into ".TB_PREFIX."admin_log values (0,".$uid.",'Changed Debug Error Log settings',".time().")");
|
||||
break;
|
||||
|
||||
case 'toggle':
|
||||
$active = (int)($_POST['active'] ?? 0);
|
||||
$database->setDebugMode($active, $uid);
|
||||
$database->query("Insert into ".TB_PREFIX."admin_log values (0,".$uid.",'".($active ? 'Enabled' : 'Disabled')." Debug Error Log',".time().")");
|
||||
break;
|
||||
|
||||
case 'clear':
|
||||
@file_put_contents($logFile, '');
|
||||
@unlink($logFile . '.1');
|
||||
$database->query("Insert into ".TB_PREFIX."admin_log values (0,".$uid.",'Cleared Debug Error Log',".time().")");
|
||||
break;
|
||||
|
||||
case 'download':
|
||||
if (is_file($logFile) && filesize($logFile) > 0) {
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
header('Content-Disposition: attachment; filename="debug-players-'.date('Ymd-His').'.log"');
|
||||
header('Content-Length: ' . filesize($logFile));
|
||||
readfile($logFile);
|
||||
} else {
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo "The debug log is empty.";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
header("Location: ../../../Admin/admin.php?p=debug_log");
|
||||
exit;
|
||||
+61
-3
@@ -8878,12 +8878,70 @@ public function setMaintenance($active, $uid=0) {
|
||||
$active = (int)$active;
|
||||
$uid = (int)$uid;
|
||||
// REPLACE creează rândul dacă nu există
|
||||
return $this->query("REPLACE INTO ".TB_PREFIX."maintenance
|
||||
(id, active, started_by, started_at)
|
||||
return $this->query("REPLACE INTO ".TB_PREFIX."maintenance
|
||||
(id, active, started_by, started_at)
|
||||
VALUES (1, $active, $uid, $time)");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Debug error-log mode (admin-controlled, transparent to players).
|
||||
* Returns the single config row, falling back to safe defaults when the
|
||||
* table does not exist yet (so deploying the code before creating the table
|
||||
* never produces a blank page).
|
||||
*/
|
||||
public function getDebugMode() {
|
||||
$default = [
|
||||
'active' => 0,
|
||||
'lvl_warning' => 1,
|
||||
'lvl_notice' => 1,
|
||||
'lvl_deprecated' => 1,
|
||||
'lvl_fatal' => 1,
|
||||
'max_size_mb' => 5,
|
||||
'auto_off_hours' => 6,
|
||||
'started_by' => null,
|
||||
'started_at' => null,
|
||||
];
|
||||
try {
|
||||
$res = @mysqli_query($this->dblink, "SELECT * FROM ".TB_PREFIX."debug_log WHERE id=1 LIMIT 1");
|
||||
if (!$res) {
|
||||
return $default;
|
||||
}
|
||||
$row = mysqli_fetch_assoc($res);
|
||||
return $row ?: $default;
|
||||
} catch (\Throwable $e) {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the debug mode on/off (stamps who/when on activation).
|
||||
*/
|
||||
public function setDebugMode($active, $uid = 0) {
|
||||
$active = (int)$active;
|
||||
$uid = (int)$uid;
|
||||
$time = time();
|
||||
return $this->query("UPDATE ".TB_PREFIX."debug_log
|
||||
SET active = $active, started_by = $uid, started_at = $time
|
||||
WHERE id = 1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the debug capture parameters (levels, size cap, auto-off window).
|
||||
*/
|
||||
public function setDebugSettings($warning, $notice, $deprecated, $fatal, $maxSizeMb, $autoOffHours) {
|
||||
$warning = $warning ? 1 : 0;
|
||||
$notice = $notice ? 1 : 0;
|
||||
$deprecated = $deprecated ? 1 : 0;
|
||||
$fatal = $fatal ? 1 : 0;
|
||||
$maxSizeMb = max(1, (int)$maxSizeMb);
|
||||
$autoOffHours = max(0, (int)$autoOffHours);
|
||||
return $this->query("UPDATE ".TB_PREFIX."debug_log
|
||||
SET lvl_warning = $warning, lvl_notice = $notice, lvl_deprecated = $deprecated,
|
||||
lvl_fatal = $fatal, max_size_mb = $maxSizeMb, auto_off_hours = $autoOffHours
|
||||
WHERE id = 1");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Changed the actual capital with a new one
|
||||
*
|
||||
|
||||
@@ -1932,6 +1932,8 @@ tz_def('TZ_CONSTRUCT_WAREHOUSE', 'Construct Warehouse.');
|
||||
tz_def('TZ_CP_DAY', 'CP/day');
|
||||
tz_def('TZ_CROP_25_5_GOLD', 'Crop +25% (5 gold)');
|
||||
tz_def('TZ_DATE_AND_TIME', 'Date and time');
|
||||
tz_def('TZ_DEBUG_OFF', 'Debug log OFF');
|
||||
tz_def('TZ_DEBUG_ON', 'Debug log ON');
|
||||
tz_def('TZ_DECLARE_WAR', 'declare war');
|
||||
tz_def('TZ_DEFAULT', 'Default:');
|
||||
tz_def('TZ_DELETE_ACCOUNT', 'Delete account?');
|
||||
|
||||
@@ -1931,6 +1931,8 @@ define('TZ_CONSTRUCT_WAREHOUSE', 'Construisez un entrepôt.');
|
||||
define('TZ_CP_DAY', 'PC/jour');
|
||||
define('TZ_CROP_25_5_GOLD', 'Céréales +25% (5 or)');
|
||||
define('TZ_DATE_AND_TIME', 'Date et heure');
|
||||
define('TZ_DEBUG_OFF', 'Journal debug DÉSACTIVÉ');
|
||||
define('TZ_DEBUG_ON', 'Journal debug ACTIVÉ');
|
||||
define('TZ_DECLARE_WAR', 'déclarer la guerre');
|
||||
define('TZ_DEFAULT', 'Par défaut :');
|
||||
define('TZ_DELETE_ACCOUNT', 'Supprimer le compte ?');
|
||||
|
||||
@@ -1926,6 +1926,8 @@ define('TZ_CONSTRUCT_WAREHOUSE', 'Construiește un depozit.');
|
||||
define('TZ_CP_DAY', 'PC/zi');
|
||||
define('TZ_CROP_25_5_GOLD', 'Grâne +25% (5 aur)');
|
||||
define('TZ_DATE_AND_TIME', 'Data și ora');
|
||||
define('TZ_DEBUG_OFF', 'Jurnal debug OPRIT');
|
||||
define('TZ_DEBUG_ON', 'Jurnal debug PORNIT');
|
||||
define('TZ_DECLARE_WAR', 'declară război');
|
||||
define('TZ_DEFAULT', 'Implicit:');
|
||||
define('TZ_DELETE_ACCOUNT', 'Ștergi contul?');
|
||||
|
||||
@@ -129,6 +129,21 @@ function __construct() {
|
||||
}
|
||||
}
|
||||
|
||||
// === DEBUG ERROR LOG (admin-controlled, transparent to players) ===
|
||||
// When enabled from the admin panel, capture the selected PHP errors of
|
||||
// every player into var/log/debug-players.log. Auto-disables itself after
|
||||
// the configured window so a forgotten debug session cannot run forever.
|
||||
$dbg = $database->getDebugMode();
|
||||
if (!empty($dbg['active'])) {
|
||||
$autoOff = (int)($dbg['auto_off_hours'] ?? 0);
|
||||
if ($autoOff > 0 && !empty($dbg['started_at'])
|
||||
&& ($dbg['started_at'] + $autoOff * 3600) < $this->time) {
|
||||
$database->setDebugMode(0, $this->uid ?? 0);
|
||||
} else {
|
||||
\App\Utils\DebugErrorLogger::enable($dbg, $this->uid ?? 0, $this->username ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$this->referrer = $_SESSION['url'] ?? "/";
|
||||
$this->url = $_SESSION['url'] = $_SERVER['PHP_SELF'];
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
#################################################################################
|
||||
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Filename : debug_status.tpl ##
|
||||
## Type : Left Menu Widget (admin only) ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Project : TravianZ ##
|
||||
## Quick on/off toggle for the Debug Error Log, handy to flip while playing ##
|
||||
## to reproduce a bug. Transparent to players; visible to admins only. ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
#################################################################################
|
||||
|
||||
global $database, $session;
|
||||
|
||||
if($isAdmin) {
|
||||
|
||||
// === QUICK TOGGLE from the menu (?dbg=on / ?dbg=off) ===
|
||||
if(isset($_GET['dbg']) && ($_GET['dbg'] == 'on' || $_GET['dbg'] == 'off')) {
|
||||
$newState = ($_GET['dbg'] == 'on') ? 1 : 0;
|
||||
$database->setDebugMode($newState, $session->uid);
|
||||
// redirect to clean the URL
|
||||
$cleanUrl = strtok($_SERVER["REQUEST_URI"], '?');
|
||||
header("Location: $cleanUrl");
|
||||
exit;
|
||||
}
|
||||
|
||||
$dbg = $database->getDebugMode();
|
||||
|
||||
if(!empty($dbg['active'])) {
|
||||
$started = !empty($dbg['started_at']) ? date('H:i d.m.Y', $dbg['started_at']) : '-';
|
||||
?>
|
||||
<a href="?dbg=off"
|
||||
title="Debug capture ON since <?=$started?> - Click to STOP"
|
||||
style="color:#dc2626; font-weight:700;">
|
||||
<?php echo TZ_DEBUG_ON; ?>
|
||||
</a>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<a href="?dbg=on"
|
||||
title="Debug capture OFF - Click to START"
|
||||
style="color:#16a34a; font-weight:700;">
|
||||
<?php echo TZ_DEBUG_OFF; ?>
|
||||
</a>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -195,6 +195,11 @@ $idUser = isset($_SESSION['id_user']) ? (int)$_SESSION['id_user'] : 0;
|
||||
*/
|
||||
include("Templates/maintenance_status.tpl");
|
||||
|
||||
/**
|
||||
* Debug Error Log quick toggle for admins
|
||||
*/
|
||||
include("Templates/debug_status.tpl");
|
||||
|
||||
?>
|
||||
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
#################################################################################
|
||||
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Project: TravianZ ##
|
||||
## Filename DebugErrorLogger.php ##
|
||||
## License: TravianZ Project ##
|
||||
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
|
||||
## Source code: https://github.com/Shadowss/TravianZ ##
|
||||
## ##
|
||||
#################################################################################
|
||||
|
||||
namespace App\Utils;
|
||||
|
||||
/**
|
||||
* Admin-controlled PHP error capture (issue: in-game debug log).
|
||||
*
|
||||
* When the admin turns the debug mode on, this logger registers a custom
|
||||
* error handler (plus a shutdown handler for fatals) that writes every
|
||||
* selected PHP error into a dedicated file. It is completely transparent to
|
||||
* players: nothing is ever displayed, the game behaviour is unchanged, and a
|
||||
* custom error handler is invoked even when the php.ini `error_reporting`
|
||||
* masks warnings/notices — so we capture everything without rebuilding Docker.
|
||||
*
|
||||
* The file is size-capped: once it exceeds the configured limit it is rotated
|
||||
* to a single ".1" backup, so the total volume stays bounded (~2x the cap).
|
||||
*/
|
||||
class DebugErrorLogger {
|
||||
|
||||
/** @var string Absolute/relative path to the active log file. */
|
||||
private static $file;
|
||||
|
||||
/** @var int Size cap in bytes before rotation. */
|
||||
private static $maxBytes;
|
||||
|
||||
/** @var array<int,bool> Which severities to capture. */
|
||||
private static $capture = [];
|
||||
|
||||
/** @var string Per-request context appended to every line. */
|
||||
private static $context = '';
|
||||
|
||||
/** @var bool Guards against double registration. */
|
||||
private static $registered = false;
|
||||
|
||||
/**
|
||||
* Turn the capture on for the current request.
|
||||
*
|
||||
* @param array $cfg The debug_log config row (levels + max_size_mb).
|
||||
* @param int $uid Current player id (0 when not logged in).
|
||||
* @param string $name Current player username (for context).
|
||||
*/
|
||||
public static function enable(array $cfg, $uid = 0, $name = '') {
|
||||
if (self::$registered) {
|
||||
return;
|
||||
}
|
||||
self::$registered = true;
|
||||
|
||||
// Resolve the project root (max 5 levels up), like AccessLogger.
|
||||
$autoprefix = '';
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$autoprefix = str_repeat('../', $i);
|
||||
if (file_exists($autoprefix . 'autoloader.php')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self::$file = $autoprefix . 'var/log/debug-players.log';
|
||||
self::$maxBytes = max(1, (int)($cfg['max_size_mb'] ?? 5)) * 1024 * 1024;
|
||||
|
||||
self::$capture = [
|
||||
'WARNING' => !empty($cfg['lvl_warning']),
|
||||
'NOTICE' => !empty($cfg['lvl_notice']),
|
||||
'DEPRECATED' => !empty($cfg['lvl_deprecated']),
|
||||
'FATAL' => !empty($cfg['lvl_fatal']),
|
||||
];
|
||||
|
||||
$page = $_SERVER['PHP_SELF'] ?? 'cli';
|
||||
self::$context = 'uid=' . (int)$uid
|
||||
. ($name !== '' ? ' (' . $name . ')' : '')
|
||||
. ' | page=' . $page;
|
||||
|
||||
set_error_handler([self::class, 'handleError']);
|
||||
register_shutdown_function([self::class, 'handleShutdown']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a PHP error constant to one of our four severity buckets.
|
||||
*/
|
||||
private static function bucket($errno) {
|
||||
switch ($errno) {
|
||||
case E_WARNING:
|
||||
case E_USER_WARNING:
|
||||
case E_CORE_WARNING:
|
||||
case E_COMPILE_WARNING:
|
||||
return 'WARNING';
|
||||
case E_NOTICE:
|
||||
case E_USER_NOTICE:
|
||||
return 'NOTICE';
|
||||
case E_DEPRECATED:
|
||||
case E_USER_DEPRECATED:
|
||||
return 'DEPRECATED';
|
||||
default:
|
||||
// E_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR, ...
|
||||
return 'FATAL';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime error handler. Returns false so PHP's default handling still
|
||||
* runs (respecting display_errors/error_reporting), keeping behaviour
|
||||
* unchanged. Suppressed errors (the @ operator) are ignored on purpose.
|
||||
*/
|
||||
public static function handleError($errno, $errstr, $errfile = '', $errline = 0) {
|
||||
// Respect the @ operator: in PHP 8 error_reporting() returns 0 inside
|
||||
// the handler for a suppressed error.
|
||||
if (error_reporting() === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bucket = self::bucket($errno);
|
||||
if (!empty(self::$capture[$bucket])) {
|
||||
self::write($bucket, $errstr, $errfile, $errline);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown handler: catches fatal errors that the error handler cannot
|
||||
* (E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR).
|
||||
*/
|
||||
public static function handleShutdown() {
|
||||
if (empty(self::$capture['FATAL'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$err = error_get_last();
|
||||
if ($err === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fatal = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR;
|
||||
if (($err['type'] & $fatal) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::write('FATAL', $err['message'], $err['file'], $err['line']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one formatted line, rotating the file first if it grew past the
|
||||
* size cap. Never throws — logging must not break the game.
|
||||
*/
|
||||
private static function write($severity, $message, $file, $line) {
|
||||
try {
|
||||
if (self::$file === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Size-cap rotation: keep one ".1" backup, then start fresh.
|
||||
if (@file_exists(self::$file) && @filesize(self::$file) >= self::$maxBytes) {
|
||||
@rename(self::$file, self::$file . '.1');
|
||||
}
|
||||
|
||||
$message = str_replace(["\r", "\n"], ' ', (string)$message);
|
||||
$entry = '[' . date('Y-m-d H:i:s') . '] '
|
||||
. $severity . ': ' . $message
|
||||
. ' in ' . $file . ':' . $line
|
||||
. ' | ' . self::$context . "\n";
|
||||
|
||||
@file_put_contents(self::$file, $entry, FILE_APPEND | LOCK_EX);
|
||||
} catch (\Throwable $e) {
|
||||
// swallow: a logging failure must never surface to players
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1828,3 +1828,28 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%maintenance` (
|
||||
--
|
||||
INSERT INTO `%PREFIX%maintenance` (`id`, `active`, `message`, `started_by`, `started_at`) VALUES
|
||||
(1, 0, 'Server in maintenance', NULL, NULL);
|
||||
|
||||
--
|
||||
-- Table structure for table `%PREFIX%debug_log`
|
||||
-- Admin-controlled PHP error capture (transparent to players). One row (id=1).
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `%PREFIX%debug_log` (
|
||||
`id` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`active` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`lvl_warning` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`lvl_notice` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`lvl_deprecated` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`lvl_fatal` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`max_size_mb` int(11) NOT NULL DEFAULT 5,
|
||||
`auto_off_hours` int(11) NOT NULL DEFAULT 6,
|
||||
`started_by` int(11) DEFAULT NULL,
|
||||
`started_at` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
--
|
||||
-- Dumping data for table `%PREFIX%debug_log`
|
||||
--
|
||||
INSERT INTO `%PREFIX%debug_log` (`id`, `active`, `lvl_warning`, `lvl_notice`, `lvl_deprecated`, `lvl_fatal`, `max_size_mb`, `auto_off_hours`, `started_by`, `started_at`) VALUES
|
||||
(1, 0, 1, 1, 1, 1, 5, 6, NULL, NULL);
|
||||
|
||||
Reference in New Issue
Block a user