276 lines
8.9 KiB
PHP
276 lines
8.9 KiB
PHP
<?php
|
||
declare (strict_types = 1);
|
||
/**
|
||
* clean_uploads.php
|
||
* Compatible:
|
||
* - accès direct /admin/tools/clean_uploads.php
|
||
* - inclusion via /admin/index.php?page=clean_uploads
|
||
*/
|
||
|
||
// Si APP_HTDOCS n'existe pas, c'est qu'on n'est pas passé par le bootstrap admin
|
||
if (! defined('APP_HTDOCS')) {
|
||
require_once __DIR__ . '/../../public/includes/init_admin.php';
|
||
}
|
||
|
||
// 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[0] === '.') {
|
||
continue;
|
||
}
|
||
|
||
}
|
||
|
||
// ignore fichiers système fréquents
|
||
if ($f === '.DS_Store' || $f === 'Thumbs.db' || $f === '.htaccess') {
|
||
continue;
|
||
}
|
||
|
||
$full = $uploadDir . $f;
|
||
|
||
// uniquement les fichiers (pas sous-dossiers)
|
||
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{display:inline-block;padding:10px 14px;border-radius:10px;border:1px solid #ccc;background:#fff;cursor:pointer}
|
||
.btn-danger{border-color:#e3a4a4;background:#fff1f1}
|
||
.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>
|