Import
This commit is contained in:
1 parent
038a01ccb8
commit
eee6418ea4
5 files changed
+1232
No files matched your search
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
/**
|
||||
* admin/backup.php
|
||||
*
|
||||
* Preparation des sauvegardes de la base. Cette page NE stocke jamais
|
||||
* d'identifiants de base en clair dans le HTML ni dans les logs, et ne
|
||||
* lance aucune commande shell directement depuis une requete HTTP pour
|
||||
* eviter tout risque d'injection de commande : elle genere uniquement
|
||||
* une commande mysqldump PRETE A COPIER, a executer manuellement ou via
|
||||
* une tache cron/systemd timer sur le serveur.
|
||||
*
|
||||
* Regle de securite appliquee ici :
|
||||
* - Pas de shell_exec()/exec()/proc_open() sur une entree utilisateur.
|
||||
* - Pas de mot de passe DB affiche : on reference une variable
|
||||
* d'environnement ou un fichier .my.cnf externe.
|
||||
* - Seule action en base : enregistrement de metadonnees de sauvegarde
|
||||
* (date, taille, note) dans une table de suivi, jamais le contenu.
|
||||
*
|
||||
* CORRIGE (20/09/2026, suite a erreur 500 en usage reel) :
|
||||
* - require_once _nav.php manquait, provoquant une erreur fatale
|
||||
* "Call to undefined function render_admin_nav()".
|
||||
* - La table backup_log necessaire a cette page n'existait pas :
|
||||
* voir admin/sql/migration_add_missing_tables.sql.
|
||||
*
|
||||
* Corrige suite a l'audit du 20/09/2026 : le detail technique des
|
||||
* exceptions n'est plus affiche a l'ecran ; il est journalise via
|
||||
* error_log().
|
||||
*
|
||||
* Hypothese de schema : backup_log(id, created_at, admin_username,
|
||||
* label, note, size_bytes_reported)
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/admin_auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_once __DIR__ . '/_nav.php';
|
||||
|
||||
require_admin_auth();
|
||||
|
||||
$db = pdo();
|
||||
$feedback = null;
|
||||
$feedbackType = 'success';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
require_csrf_token();
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$confirmed = isset($_POST['confirm']) && $_POST['confirm'] === '1';
|
||||
|
||||
if ($action === 'log_backup') {
|
||||
if (!$confirmed) {
|
||||
$feedback = 'Enregistrement refuse : confirmation manquante.';
|
||||
$feedbackType = 'error';
|
||||
} else {
|
||||
$label = trim((string)($_POST['label'] ?? ''));
|
||||
$note = trim((string)($_POST['note'] ?? ''));
|
||||
$sizeBytes = filter_input(INPUT_POST, 'size_bytes', FILTER_VALIDATE_INT) ?: null;
|
||||
|
||||
if ($label === '') {
|
||||
$feedback = 'Le libelle de la sauvegarde est obligatoire.';
|
||||
$feedbackType = 'error';
|
||||
} else {
|
||||
try {
|
||||
$stmt = $db->prepare(
|
||||
'INSERT INTO backup_log (created_at, admin_username, label, note, size_bytes_reported)
|
||||
VALUES (NOW(), ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([current_admin_username(), $label, $note, $sizeBytes]);
|
||||
$feedback = 'Sauvegarde enregistree dans le journal.';
|
||||
} catch (Throwable $e) {
|
||||
error_log('[backup] log_backup: ' . $e->getMessage());
|
||||
$feedback = 'Une erreur est survenue lors de l\'enregistrement.';
|
||||
$feedbackType = 'error';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$history = [];
|
||||
try {
|
||||
$history = $db->query(
|
||||
'SELECT id, created_at, admin_username, label, note, size_bytes_reported
|
||||
FROM backup_log
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50'
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[backup] history query: ' . $e->getMessage());
|
||||
$feedback = 'La table backup_log est introuvable ou inaccessible. Consultez les journaux du serveur.';
|
||||
$feedbackType = 'error';
|
||||
}
|
||||
|
||||
$user = current_admin_username();
|
||||
$today = date('Ymd_His');
|
||||
$dumpCommand = "mysqldump --defaults-extra-file=/etc/netmonster/.my.cnf --single-transaction --routines --triggers netmonster | gzip > /var/backups/netmonster/netmonster_{$today}.sql.gz";
|
||||
$restoreCommand = "gunzip -c /var/backups/netmonster/netmonster_{$today}.sql.gz | mysql --defaults-extra-file=/etc/netmonster/.my.cnf netmonster";
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Sauvegardes — NetMonster Admin</title>
|
||||
<link rel="stylesheet" href="assets/theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<?php render_admin_nav('backup.php', $user); ?>
|
||||
|
||||
<main class="page-wrap">
|
||||
<h1>Sauvegardes de la base</h1>
|
||||
<p class="page-subtitle">Cette page n'execute aucune commande sur le serveur. Elle fournit une commande prete a l'emploi et journalise les sauvegardes realisees manuellement ou par cron.</p>
|
||||
|
||||
<?php if ($feedback !== null): ?>
|
||||
<p class="<?= $feedbackType === 'error' ? 'msg-err' : 'msg-ok' ?>">
|
||||
<?= htmlspecialchars($feedback, ENT_QUOTES, 'UTF-8') ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="card">
|
||||
<h2>Commande de sauvegarde recommandee</h2>
|
||||
<p>A executer sur le serveur, avec un fichier <code>.my.cnf</code> contenant les identifiants (jamais dans ce fichier PHP ni dans un depot Git) :</p>
|
||||
<pre><?= htmlspecialchars($dumpCommand, ENT_QUOTES, 'UTF-8') ?></pre>
|
||||
|
||||
<h2>Commande de restauration</h2>
|
||||
<pre><?= htmlspecialchars($restoreCommand, ENT_QUOTES, 'UTF-8') ?></pre>
|
||||
|
||||
<p>Recommandations :</p>
|
||||
<ul>
|
||||
<li>Stockez <code>.my.cnf</code> avec les permissions <code>600</code>, proprietaire du compte de service uniquement.</li>
|
||||
<li>Chiffrez l'archive si elle est copiee hors du serveur (ex. <code>gpg --symmetric</code>).</li>
|
||||
<li>Automatisez via un timer systemd ou une tache cron dediee, jamais depuis cette interface web.</li>
|
||||
<li>Testez regulierement la restauration sur un environnement isole.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Journaliser une sauvegarde effectuee</h2>
|
||||
<form method="post" onsubmit="return confirm('Confirmer l\'enregistrement de cette sauvegarde dans le journal ?');">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="log_backup">
|
||||
<input type="hidden" name="confirm" value="1">
|
||||
<label>Libelle
|
||||
<input type="text" name="label" required placeholder="ex: backup_hebdo_2026_09_20">
|
||||
</label>
|
||||
<label>Taille rapportee (octets, optionnel)
|
||||
<input type="number" name="size_bytes" min="0">
|
||||
</label>
|
||||
<label>Note
|
||||
<textarea name="note" rows="2" placeholder="Contexte, emplacement de stockage..."></textarea>
|
||||
</label>
|
||||
<button type="submit" class="primary">Enregistrer dans le journal</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Historique des sauvegardes journalisees</h2>
|
||||
<table class="data-table">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Utilisateur</th>
|
||||
<th>Libelle</th>
|
||||
<th>Taille</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
<?php foreach ($history as $row): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars((string)$row['created_at'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars((string)$row['admin_username'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars((string)$row['label'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><?= $row['size_bytes_reported'] !== null ? number_format((float)$row['size_bytes_reported'], 0, ',', ' ') . ' o' : '—' ?></td>
|
||||
<td><?= htmlspecialchars((string)$row['note'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($history)): ?>
|
||||
<tr><td colspan="5" class="no-cadastre">Aucune sauvegarde journalisee.</td></tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user