Files
admin-netmonster-db/admin/backup.php
T

184 lines
7.7 KiB
PHP

<?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'] ?? ''));
// Correction : Accepte les grands nombres (BigInt) pour les gros backups
$rawSize = $_POST['size_bytes'] ?? '';
$sizeBytes = (ctype_digit((string)$rawSize)) ? $rawSize : 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>