Files
2026-03-05 11:12:24 +01:00

296 lines
9.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* clean_uploads.php
* Outil admin destructif : suppression des fichiers orphelins dans /public/uploads
*/
require_once __DIR__ . '/../../public/includes/init_admin.php'; // ✅ impose admin + strict super-admin + CSRF
// Anti-cache (évite de conserver un token / résultat en cache)
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
// Reauth (10 min) avant action destructive
$mustReauth = static function (int $ttl = 600): bool {
$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=clean_uploads'),
]);
header('Location: ?' . $qs);
exit;
}
// Durcir PDO
if (! isset($pdo) || ! ($pdo instanceof PDO)) {
http_response_code(500);
exit('PDO introuvable (DB non initialisée).');
}
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
// Dossier uploads (chez toi: htdocs/public/uploads/)
$uploadDir = rtrim(APP_HTDOCS, '/\\') . '/public/uploads/';
if (! is_dir($uploadDir)) {
http_response_code(500);
exit("Uploads introuvable: " . $uploadDir);
}
$uploadDir = rtrim($uploadDir, "/\\") . DIRECTORY_SEPARATOR;
// Assurer PDO
if (! isset($pdo) || ! ($pdo instanceof PDO)) {
http_response_code(500);
exit("PDO introuvable (DB non initialisée).");
}
// ⚙️ Sources DB : tu peux en ajouter plusieurs (plusieurs tables/colonnes)
$dbSources = [
[
'sql' => "SELECT photo FROM praticien WHERE photo IS NOT NULL AND photo <> ''",
'col' => 'photo',
],
// Exemple si tu as d'autres tables :
// ['sql' => "SELECT image FROM articles WHERE image IS NOT NULL AND image <> ''", 'col' => 'image'],
];
// --- CSRF token ---
if (empty($_SESSION['csrf_clean_uploads'])) {
$_SESSION['csrf_clean_uploads'] = bin2hex(random_bytes(32));
}
$csrf = $_SESSION['csrf_clean_uploads'];
// --- Paramètres ---
$limitList = 400; // pour éviter une page énorme si milliers de fichiers
$action = $_POST['action'] ?? '';
$confirmed = isset($_POST['confirm_delete']) && $_POST['confirm_delete'] === '1';
// --- 1) Charger noms d'images depuis la DB ---
$dbNames = [];
foreach ($dbSources as $src) {
$stmt = $pdo->query($src['sql']);
$rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
foreach ($rows as $name) {
$name = trim((string) $name);
if ($name !== '') {
$dbNames[$name] = true;
}
// set
}
}
// --- 2) Scanner le dossier uploads (fichiers seulement) ---
$files = @scandir($uploadDir);
if (! is_array($files)) {
http_response_code(500);
exit('Impossible de lire le dossier uploads.');
}
$orphans = []; // [ ['name'=>..., 'size'=>..., 'mtime'=>...] ]
$totalOrphanSize = 0;
foreach ($files as $f) {
if ($f === '.' || $f === '..') {
continue;
}
if ($f !== '' && $f[0] === '.') {
continue;
}
// ignore fichiers cachés (.htaccess, .DS_Store, etc.)
// ignore fichiers système fréquents (si tu veux garder explicite)
if ($f === 'Thumbs.db') {
continue;
}
$full = $uploadDir . $f;
if (! is_file($full)) {
continue;
}
// si le fichier n'est pas dans la base => orphelin
if (! isset($dbNames[$f])) {
$size = filesize($full);
$mtime = filemtime($full);
$orphans[] = [
'name' => $f,
'size' => is_int($size) ? $size : 0,
'mtime' => is_int($mtime) ? $mtime : 0,
];
$totalOrphanSize += (is_int($size) ? $size : 0);
}
}
// Tri : plus gros d'abord (pratique pour voir vite les gains)
usort($orphans, fn($a, $b) => ($b['size'] <=> $a['size']));
// --- 3) Suppression si demandé ---
$deleted = [];
$errors = [];
if ($action === 'delete') {
// CSRF
$postedCsrf = (string) ($_POST['csrf'] ?? '');
if (! hash_equals($csrf, $postedCsrf)) {
$errors[] = "CSRF invalide. Recharge la page.";
} elseif (! $confirmed) {
$errors[] = "Coche la confirmation avant de supprimer.";
} else {
// On supprime uniquement les orphelins trouvés à l’instant T
foreach ($orphans as $o) {
$name = $o['name'];
// Sécurité anti traversal
if (str_contains($name, '/') || str_contains($name, '\\') || $name === '') {
$errors[] = "Nom de fichier suspect ignoré: " . h($name);
continue;
}
$full = $uploadDir . $name;
// recheck réel
if (! is_file($full)) {
continue;
}
// s'assurer qu'on reste dans uploadDir
$real = realpath($full);
if ($real === false || strncmp($real, $uploadDir, strlen($uploadDir)) !== 0) {
$errors[] = "Chemin refusé: " . h($name);
continue;
}
if (@unlink($real)) {
$deleted[] = $name;
} else {
$errors[] = "Impossible de supprimer: " . h($name);
}
}
// Re-générer un nouveau token après action destructive
$_SESSION['csrf_clean_uploads'] = bin2hex(random_bytes(32));
$csrf = $_SESSION['csrf_clean_uploads'];
}
}
// Helpers
function format_bytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = 0;
$val = (float) $bytes;
while ($val >= 1024 && $i < count($units) - 1) {$val /= 1024;
$i++;}
return ($i === 0 ? (string) $bytes : number_format($val, 2, '.', ' ')) . ' ' . $units[$i];
}
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Nettoyage uploads (orphelins)</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:20px;line-height:1.35}
.box{padding:12px 14px;border:1px solid #ddd;border-radius:10px;margin:12px 0}
.ok{background:#f2fff5;border-color:#bde8c7}
.err{background:#fff4f4;border-color:#f0b9b9}
table{border-collapse:collapse;width:100%;margin-top:10px}
th,td{border-bottom:1px solid #eee;padding:8px 6px;text-align:left;font-size:14px}
th{background:#fafafa}
code{background:#f6f6f6;padding:2px 6px;border-radius:6px}
.muted{color:#666}
.row{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
.btn:disabled{opacity:.5;cursor:not-allowed}
</style>
</head>
<body>
<h2>Nettoyage du dossier uploads</h2>
<div class="box">
<div class="muted">Dossier :</div>
<div><code><?php echo h($uploadDir) ?></code></div>
</div>
<?php if (! empty($deleted)): ?>
<div class="box ok">
✅ Supprimés : <b><?php echo count($deleted) ?></b>
<div class="muted" style="margin-top:6px;">
Exemple : <?php echo h($deleted[0]) ?><?php echo count($deleted) > 1 ? ' …' : '' ?>
</div>
</div>
<?php endif; ?>
<?php if (! empty($errors)): ?>
<div class="box err">
❌ Erreurs :
<ul>
<?php foreach ($errors as $e): ?>
<li><?php echo $e ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="box">
<div class="row">
<div>Fichiers orphelins : <b><?php echo count($orphans) ?></b></div>
<div>Taille totale : <b><?php echo h(format_bytes((int) $totalOrphanSize)) ?></b></div>
<div class="muted">(triés par taille décroissante)</div>
</div>
<?php if (count($orphans) === 0): ?>
<p class="muted" style="margin-top:10px;">Aucun fichier orphelin trouvé ✅</p>
<?php else: ?>
<form method="post" onsubmit="return confirm('Confirmer la suppression des fichiers orphelins ?');" style="margin-top:12px;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="csrf" value="<?php echo h($csrf) ?>">
<label style="display:flex;gap:8px;align-items:center;margin:10px 0;">
<input type="checkbox" name="confirm_delete" value="1">
<span>Je confirme la suppression de <b><?php echo count($orphans) ?></b> fichiers orphelins (<?php echo h(format_bytes((int) $totalOrphanSize)) ?>).</span>
</label>
<button class="btn btn-danger" type="submit">Supprimer maintenant</button>
<span class="muted" style="margin-left:10px;">(Tu as déjà une copie sur ton PC, parfait.)</span>
</form>
<table>
<thead>
<tr>
<th>#</th>
<th>Fichier</th>
<th>Taille</th>
<th>Dernière modif</th>
</tr>
</thead>
<tbody>
<?php
$max = min(count($orphans), $limitList);
for ($i = 0; $i < $max; $i++):
$o = $orphans[$i];
?>
<tr>
<td><?php echo $i + 1 ?></td>
<td><?php echo h($o['name']) ?></td>
<td><?php echo h(format_bytes((int) $o['size'])) ?></td>
<td><?php echo $o['mtime'] ? h(date('Y-m-d H:i:s', (int) $o['mtime'])) : '-' ?></td>
</tr>
<?php endfor; ?>
</tbody>
</table>
<?php if (count($orphans) > $limitList): ?>
<p class="muted">Affichage limité à <?php echo (int) $limitList ?> fichiers. (Mais la suppression concerne tous les orphelins trouvés.)</p>
<?php endif; ?>
<?php endif; ?>
</div>
</body>
</html>