Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+205
View File
@@ -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 &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>