Files
maria_uslugi/admin/tools/logs.php
T
2026-03-05 10:25:39 +01:00

206 lines
8.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* /admin/tools/logs.php
* Viewer de logs "admin-friendly" :
* - Affiche /logs/php_errors.log (et /logs/app.log si présent)
* - Tail (dernieres N lignes), Download, Clear (truncate)
* - CSRF + garde-fous; pas de fonctions globales (closures only)
* - S'affiche uniquement sur ?page=logs
*/
if (($_GET['page'] ?? '') !== 'logs') { return; }
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
// Lang (pour $translations)
require_once __DIR__ . '/../../public/includes/lang.php';
$roles = (array)($_SESSION['role'] ?? []);
if (!in_array('admin', $roles, true)) {
echo '<div class="container my-4"><div class="alert alert-danger">'
. htmlspecialchars((string)($translations['purge_admin_access_denied'] ?? 'Accès refusé.'), ENT_QUOTES, 'UTF-8')
. '</div></div>';
return;
}
// reauth (10 min)
$mustReauth=function(int $ttl=600){$t=(int)($_SESSION['admin_reauth_at']??0);return ($t===0||(time()-$t)>$ttl);};
if ($mustReauth()){ $qs=http_build_query(['page'=>'reauth','redir'=>($_SERVER['REQUEST_URI']??'index.php?page=maintenance')]); header('Location:?'.$qs); exit; }
if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); }
$CSRF = $_SESSION['csrf_token'];
$e = static fn($s) => htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
$siteRoot = realpath(__DIR__ . '/../../');
$logsDir = $siteRoot . '/logs';
@is_dir($logsDir) || @mkdir($logsDir, 0755, true);
// Fichiers autorisés
$files = [
'php_errors' => $logsDir . '/php_errors.log',
'app' => $logsDir . '/app.log',
];
// Sélection
$key = $_GET['file'] ?? 'php_errors';
if (!isset($files[$key])) { $key = 'php_errors'; }
$path = $files[$key];
// Actions pré-dispatch
$op = $_GET['op'] ?? '';
if ($op === 'download' && hash_equals($CSRF, $_GET['csrf'] ?? '')) {
if (is_file($path) && is_readable($path)) {
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="'.$key.'.log"');
readfile($path);
} else {
header('Content-Type: text/plain; charset=utf-8');
echo (string)($translations['purge_logs_file_unavailable_plain'] ?? 'Fichier indisponible.');
}
return;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'truncate') {
if (!hash_equals($CSRF, $_POST['csrf'] ?? '')) {
echo '<div class="container my-4"><div class="alert alert-danger">'
. $e($translations['purge_csrf_invalid'] ?? 'Token CSRF invalide.')
. '</div></div>';
return;
}
if (is_file($path) && is_writable($path)) {
$fp = @fopen($path, 'w'); if ($fp) { fclose($fp); }
} else if (!file_exists($path)) {
// crée le fichier si absent
@touch($path);
}
// redirection douce (évite le repost)
$qs = http_build_query(['page'=>'logs','file'=>$key]);
header('Location: ?'.$qs);
return;
}
// Helpers
$humanSize = static function(int $bytes): string {
$units = ['B','KB','MB','GB','TB']; $i=0;
while ($bytes >= 1024 && $i < count($units)-1) { $bytes/=1024; $i++; }
return number_format($bytes, ($i?1:0)).' '.$units[$i];
};
$tail = static function(string $file, int $lines=300, int $maxBytes=1048576): string {
if (!is_file($file) || !is_readable($file)) return '';
$size = filesize($file);
if ($size === 0) return '';
$bytesToRead = min($size, $maxBytes);
$fh = fopen($file, 'rb');
if (!$fh) return '';
// positionne le pointeur en fin-moins-bytesToRead
fseek($fh, -$bytesToRead, SEEK_END);
$data = fread($fh, $bytesToRead);
fclose($fh);
// Normalise fin de fichier + découpe
$data = ltrim($data); // évite premier morceau tronqué
$linesArr = preg_split('/\R/', $data);
$linesArr = array_slice($linesArr, -$lines);
return implode("\n", $linesArr);
};
$exists = is_file($path);
$readable = $exists && is_readable($path);
$writable = $exists ? is_writable($path) : is_writable(dirname($path));
$size = $exists ? filesize($path) : 0;
$mtime = $exists ? date('Y-m-d H:i:s', filemtime($path)) : '—';
$content = $readable ? $tail($path, (int)($_GET['n'] ?? 300)) : '';
?>
<div class="shadow p-4 rounded bg-light">
<div class="container my-4">
<div class="d-flex align-items-center flex-wrap gap-2 mb-3">
<h3 class="m-0"><?= $e($translations['purge_logs_title'] ?? 'Journaux &amp; erreurs') ?></h3>
<span class="badge text-bg-<?= $readable ? 'success':'secondary' ?>">
<?= $readable ? $e($translations['purge_logs_readable'] ?? 'Lisible') : $e($translations['purge_logs_unavailable'] ?? 'Indisponible') ?>
</span>
<span class="badge text-bg-info">
<?php
$tpl = (string)($translations['purge_logs_file_badge'] ?? 'Fichier: {KEY}');
echo $e(str_replace('{KEY}', (string)$key, $tpl));
?>
</span>
</div>
<div class="card mb-3">
<div class="card-body">
<form method="get" class="row g-3">
<input type="hidden" name="page" value="logs">
<div class="col-12 col-md-4">
<label class="form-label"><?= $e($translations['purge_logs_select_label'] ?? 'Sélection du log') ?></label>
<select class="form-select" name="file" onchange="this.form.submit()">
<?php foreach ($files as $k=>$p): ?>
<option value="<?= $e($k) ?>" <?= $k===$key?'selected':'' ?>>
<?= $e($k) ?> (
<?= is_file($p)
? $e($humanSize(filesize($p)))
: $e($translations['purge_logs_absent'] ?? 'absent')
?>
)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-2">
<label class="form-label"><?= $e($translations['purge_logs_last_lines_label'] ?? 'Dernières lignes') ?></label>
<input type="number" min="50" max="5000" step="50" name="n" class="form-control" value="<?= (int)($_GET['n'] ?? 300) ?>">
</div>
<div class="col-12 col-md-6 d-flex align-items-end gap-2">
<a class="btn btn-outline-primary"
href="/admin/tools/logs_download.php?file=<?= $e($key) ?>&csrf=<?= $e($CSRF) ?>">
<?= $e($translations['purge_logs_btn_download_short'] ?? 'Téléchar.') ?>
</a>
<button type="submit" class="btn btn-outline-secondary">
<?= $e($translations['purge_logs_btn_refresh'] ?? 'Actualiser') ?>
</button>
<form method="post" class="d-inline">
<input type="hidden" name="csrf" value="<?= $e($CSRF) ?>">
<button formaction="?page=logs&file=<?= $e($key) ?>" name="action" value="truncate"
class="btn btn-danger"
onclick="return confirm('<?= $e($translations['purge_logs_confirm_truncate'] ?? 'Vider le contenu de ce log ?') ?>');"
<?= $writable ? '' : 'disabled' ?>>
<?= $e($translations['purge_logs_btn_truncate'] ?? 'Vider') ?>
</button>
</form>
</div>
</form>
</div>
<div class="card-footer small text-muted">
<?php
$tpl = (string)($translations['purge_logs_footer_path'] ?? 'Emplacement: {PATH}');
echo $e(str_replace('{PATH}', (string)$path, $tpl));
?>
—
<?php
$tpl = (string)($translations['purge_logs_footer_size'] ?? 'Taille: {SIZE}');
echo $e(str_replace('{SIZE}', (string)$humanSize($size), $tpl));
?>
—
<?php
$tpl = (string)($translations['purge_logs_footer_mtime'] ?? 'Dernière modif: {TIME}');
echo $e(str_replace('{TIME}', (string)$mtime, $tpl));
?>
<?php if (!$exists): ?>
— <em><?= $e($translations['purge_logs_footer_missing_note'] ?? 'Le fichier n’existe pas (il sera créé au premier log).') ?></em>
<?php endif; ?>
</div>
</div>
<div class="card">
<div class="card-header"><?= $e($translations['purge_logs_preview_header'] ?? 'Aperçu (tail)') ?></div>
<div class="card-body">
<?php if (!$readable): ?>
<div class="text-muted">
<?php
$tpl = (string)($translations['purge_logs_no_content_rights_hint'] ?? 'Aucun contenu disponible. Vérifie les droits sur <code>{DIR}</code> (0755) et le fichier (0644).');
echo str_replace('{DIR}', $e($logsDir), $tpl);
?>
</div>
<?php else: ?>
<pre class="small mb-0" style="white-space:pre-wrap;"><?= $e($content ?: ($translations['purge_logs_empty'] ?? '(vide)')) ?></pre>
<?php endif; ?>
</div>
</div>
</div>
</div>