184 lines
7.7 KiB
PHP
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>
|