Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,205 @@
|
||||
<?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 & 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>
|
||||
Reference in new issue
Block a user