Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
/**
|
||||
* /admin/tools/purger_mess_upload.php
|
||||
* Affichage Bootstrap pour purger messages/uploads (orphans + fix DB).
|
||||
* - S'affiche seulement si ?page=purger_mess_upload
|
||||
* - DRY-RUN par défaut (scan), purge LIVE via POST + CSRF.
|
||||
*/
|
||||
require_once __DIR__ . '/../../public/includes/init_admin.php';
|
||||
if (($_GET['page'] ?? '') !== 'purger_mess_upload') {
|
||||
return; // ne rien afficher hors de la page ciblée
|
||||
}
|
||||
|
||||
// -- Sécurité: on suppose que admin/index.php a déjà fait require_login_or_redirect(['admin'])
|
||||
// Mais on ajoute une ceinture si besoin :
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
||||
|
||||
|
||||
if (empty($_SESSION['role']) || !in_array('admin', (array)$_SESSION['role'], true)) {
|
||||
echo '<div class="container my-4"><div class="alert alert-danger">'
|
||||
. htmlspecialchars((string)($translations['admin_access_denied'] ?? 'Accès refusé.'), ENT_QUOTES, 'UTF-8')
|
||||
. '</div></div>';
|
||||
return;
|
||||
}
|
||||
// reauth (10 min)
|
||||
$mustReauth=function(int $ttl=600){$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=maintenance')]); header('Location:?'.$qs); exit; }
|
||||
|
||||
|
||||
// -- CSRF
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
$CSRF = $_SESSION['csrf_token'];
|
||||
|
||||
// -----------------------
|
||||
// Paramètres utilisateur
|
||||
// -----------------------
|
||||
$uploadsSubdir = 'messages/uploads'; // relatif à la racine du site
|
||||
$minAgeDays = isset($_REQUEST['min_age']) ? max(0, (int)$_REQUEST['min_age']) : 2;
|
||||
$fixDbRefs = isset($_REQUEST['fix_db']) ? ($_REQUEST['fix_db'] === '1') : true;
|
||||
|
||||
$doLive = false;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'purge') {
|
||||
// Vérif CSRF
|
||||
if (!hash_equals($CSRF, $_POST['csrf'] ?? '')) {
|
||||
echo '<div class="container my-4"><div class="alert alert-danger">'
|
||||
. htmlspecialchars((string)($translations['csrf_invalid'] ?? 'Token CSRF invalide.'), ENT_QUOTES, 'UTF-8')
|
||||
. '</div></div>';
|
||||
return;
|
||||
}
|
||||
$doLive = true;
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Résolution des chemins
|
||||
// -----------------------
|
||||
$siteRoot = realpath(__DIR__ . '/../../'); // racine du site
|
||||
$uploadsDir = $siteRoot . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $uploadsSubdir);
|
||||
|
||||
// -----------------------
|
||||
// Garde-fous fichiers
|
||||
// -----------------------
|
||||
$errors = [];
|
||||
if (!$siteRoot || !is_dir($siteRoot)) {
|
||||
$errors[] = htmlspecialchars((string)($translations['purge_mess_root_missing'] ?? 'Racine du site introuvable.'), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
if (!is_dir($uploadsDir)) {
|
||||
$tpl = (string)($translations['purge_mess_uploads_dir_missing'] ?? 'Dossier uploads introuvable: <code>{PATH}</code>');
|
||||
$errors[] = str_replace('{PATH}', htmlspecialchars((string)$uploadsDir, ENT_QUOTES, 'UTF-8'), $tpl);
|
||||
}
|
||||
if (!is_readable($uploadsDir)) {
|
||||
$errors[] = htmlspecialchars((string)($translations['purge_mess_uploads_not_readable'] ?? 'Dossier uploads non lisible.'), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
if ($doLive && !is_writable($uploadsDir)) {
|
||||
$errors[] = htmlspecialchars((string)($translations['purge_mess_uploads_not_writable_live'] ?? 'Dossier uploads non inscriptible alors que purge LIVE demandée.'), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Utilitaires
|
||||
// -----------------------
|
||||
$logs = [];
|
||||
function logln(string $msg){ global $logs; $logs[] = $msg; }
|
||||
function ageDays(int $mtime): float { return (time() - $mtime) / 86400.0; }
|
||||
function safeWithinUploads(string $path, string $uploadsRoot): bool {
|
||||
$rp = realpath($path);
|
||||
$ur = realpath($uploadsRoot);
|
||||
return $rp && $ur && strpos($rp, $ur) === 0;
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Exécution (si OK)
|
||||
// -----------------------
|
||||
$start = microtime(true);
|
||||
$stats = [
|
||||
'seen' => 0,
|
||||
'deleted' => 0,
|
||||
'skipped' => 0,
|
||||
'fixed' => 0,
|
||||
];
|
||||
$deletedList = [];
|
||||
$dry = !$doLive;
|
||||
|
||||
if (empty($errors)) {
|
||||
// 1) Chemins référencés en DB
|
||||
$dbPaths = [];
|
||||
if (isset($pdo) && $pdo instanceof PDO) {
|
||||
$stmt = $pdo->query("SELECT attachment_path FROM messages WHERE attachment_path IS NOT NULL");
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$p = trim((string)$row['attachment_path']);
|
||||
if ($p === '') continue;
|
||||
if (strpos($p, $uploadsSubdir . '/') === 0 || $p === $uploadsSubdir) {
|
||||
$dbPaths[$p] = true;
|
||||
}
|
||||
}
|
||||
$tpl = (string)($translations['purge_mess_db_refs_loaded'] ?? 'Références DB chargées: {N}');
|
||||
logln(str_replace('{N}', (string)count($dbPaths), $tpl));
|
||||
} else {
|
||||
logln($translations['purge_mess_pdo_unavailable_info'] ?? "[INFO] PDO indisponible: l'étape DB sera limitée.");
|
||||
}
|
||||
|
||||
// 2) Balayage disque
|
||||
$rii = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($uploadsDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::CURRENT_AS_FILEINFO)
|
||||
);
|
||||
foreach ($rii as $fi) {
|
||||
/** @var SplFileInfo $fi */
|
||||
if (!$fi->isFile()) continue;
|
||||
$stats['seen']++;
|
||||
|
||||
$basename = $fi->getFilename();
|
||||
if ($basename === '.htaccess' || $basename[0] === '.') continue;
|
||||
|
||||
$absPath = $fi->getRealPath();
|
||||
if (!$absPath) continue;
|
||||
|
||||
$relFromUploads = ltrim(str_replace($uploadsDir, '', $absPath), DIRECTORY_SEPARATOR);
|
||||
$webRel = rtrim($uploadsSubdir, '/').'/'.str_replace(DIRECTORY_SEPARATOR, '/', $relFromUploads);
|
||||
|
||||
// référencé ?
|
||||
if (isset($dbPaths[$webRel])) continue;
|
||||
|
||||
// âge
|
||||
$age = ageDays($fi->getMTime());
|
||||
if ($age < $minAgeDays) {
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// garde-fou
|
||||
if (!safeWithinUploads($absPath, $uploadsDir)) {
|
||||
$tpl = (string)($translations['purge_mess_alert_outside_scope_skip'] ?? '[ALERTE] Hors périmètre uploads (skip): {PATH}');
|
||||
logln(str_replace('{PATH}', $absPath, $tpl));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dry) {
|
||||
$deletedList[] = [$webRel, $age, 'simulation'];
|
||||
$tpl = (string)($translations['purge_mess_log_dry_orphan_sim'] ?? '[DRY] Orphelin: {FILE} ({DAYS} j) -> suppression simulée');
|
||||
logln(str_replace(
|
||||
['{FILE}','{DAYS}'],
|
||||
[$webRel, number_format($age,1)],
|
||||
$tpl
|
||||
));
|
||||
} else {
|
||||
if (@unlink($absPath)) {
|
||||
$stats['deleted']++;
|
||||
$deletedList[] = [$webRel, $age, 'supprimé'];
|
||||
$tpl = (string)($translations['purge_mess_log_ok_deleted'] ?? '[OK] Supprimé: {FILE} ({DAYS} j)');
|
||||
logln(str_replace(
|
||||
['{FILE}','{DAYS}'],
|
||||
[$webRel, number_format($age,1)],
|
||||
$tpl
|
||||
));
|
||||
} else {
|
||||
$tpl = (string)($translations['purge_mess_log_warn_delete_failed'] ?? '[WARN] Échec suppression: {FILE}');
|
||||
logln(str_replace('{FILE}', $webRel, $tpl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Normaliser DB si fichier manquant
|
||||
if ($fixDbRefs && isset($pdo) && $pdo instanceof PDO) {
|
||||
$stmt = $pdo->query("SELECT id, attachment_path FROM messages WHERE attachment_path IS NOT NULL");
|
||||
$update = $pdo->prepare("UPDATE messages SET attachment_path = NULL, attachment_name = NULL WHERE id = ?");
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$id = (int)$row['id'];
|
||||
$p = (string)$row['attachment_path'];
|
||||
if ($p === '' || strpos($p, $uploadsSubdir . '/') !== 0) continue;
|
||||
|
||||
$abs = $siteRoot . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $p);
|
||||
$absReal = realpath($abs);
|
||||
if (!$absReal || !is_file($absReal)) {
|
||||
if ($dry) {
|
||||
$tpl = (string)($translations['purge_mess_log_dry_db_fix'] ?? '[DRY] DB FIX id={ID} : {PATH} -> NULL (fichier manquant)');
|
||||
logln(str_replace(['{ID}','{PATH}'], [(string)$id, $p], $tpl));
|
||||
} else {
|
||||
$update->execute([$id]);
|
||||
$stats['fixed'] += $update->rowCount();
|
||||
$tpl = (string)($translations['purge_mess_log_ok_db_fix'] ?? '[OK] DB FIX id={ID} : {PATH} -> NULL');
|
||||
logln(str_replace(['{ID}','{PATH}'], [(string)$id, $p], $tpl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dur = microtime(true) - $start;
|
||||
|
||||
// -----------------------
|
||||
// Rendu Bootstrap
|
||||
// -----------------------
|
||||
?>
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<div class="container my-4">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2 mb-3">
|
||||
<h3 class="m-0"><?= htmlspecialchars((string)($translations['purge_mess_title'] ?? 'Purger PJ messagerie'), ENT_QUOTES, 'UTF-8') ?></h3>
|
||||
<span class="badge text-bg-<?= $dry ? 'secondary' : 'danger' ?>">
|
||||
<?= $dry
|
||||
? htmlspecialchars((string)($translations['purge_mess_mode_dry'] ?? 'DRY-RUN'), ENT_QUOTES, 'UTF-8')
|
||||
: htmlspecialchars((string)($translations['purge_mess_mode_live'] ?? 'LIVE (suppression activée)'), ENT_QUOTES, 'UTF-8')
|
||||
?>
|
||||
</span>
|
||||
<span class="badge text-bg-info">
|
||||
<?php
|
||||
$tpl = (string)($translations['purge_mess_age_threshold_badge'] ?? 'Seuil d’âge ≥ {N} j');
|
||||
echo htmlspecialchars(str_replace('{N}', (string)((int)$minAgeDays), $tpl), ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
</span>
|
||||
<span class="badge text-bg-<?= $fixDbRefs ? 'primary':'secondary' ?>">
|
||||
<?php
|
||||
$tpl = (string)($translations['purge_mess_fix_db_badge'] ?? 'Fix refs DB : {VAL}');
|
||||
$val = $fixDbRefs ? ($translations['yes'] ?? 'OUI') : ($translations['no'] ?? 'NON');
|
||||
echo htmlspecialchars(str_replace('{VAL}', (string)$val, $tpl), ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="alert alert-danger mb-3">
|
||||
<strong><?= htmlspecialchars((string)($translations['errors_title'] ?? 'Erreurs'), ENT_QUOTES, 'UTF-8') ?></strong><br>
|
||||
<ul class="mb-0">
|
||||
<?php foreach ($errors as $e): ?>
|
||||
<li><?= $e ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<form method="post" class="row g-3">
|
||||
<input type="hidden" name="csrf" value="<?= htmlspecialchars($CSRF, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label"><?= htmlspecialchars((string)($translations['purge_mess_min_age_label'] ?? 'Âge minimum (jours)'), ENT_QUOTES, 'UTF-8') ?></label>
|
||||
<input type="number" class="form-control" name="min_age" min="0" value="<?= (int)$minAgeDays ?>">
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label"><?= htmlspecialchars((string)($translations['purge_mess_fix_db_label'] ?? 'Fixer les refs DB manquantes'), ENT_QUOTES, 'UTF-8') ?></label>
|
||||
<select class="form-select" name="fix_db">
|
||||
<option value="1" <?= $fixDbRefs ? 'selected':'' ?>><?= htmlspecialchars((string)($translations['yes'] ?? 'Oui'), ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<option value="0" <?= !$fixDbRefs ? 'selected':'' ?>><?= htmlspecialchars((string)($translations['no'] ?? 'Non'), ENT_QUOTES, 'UTF-8') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 d-flex gap-2">
|
||||
<button type="submit" name="action" value="scan" class="btn btn-outline-secondary">
|
||||
<?= htmlspecialchars((string)($translations['btn_scan'] ?? 'Scanner'), ENT_QUOTES, 'UTF-8') ?>
|
||||
</button>
|
||||
<button type="submit" name="action" value="purge" class="btn btn-danger"
|
||||
onclick="return confirm('<?= htmlspecialchars((string)($translations['confirm_purge_orphan_files'] ?? 'Confirmer la purge réelle des fichiers orphelins ?'), ENT_QUOTES, 'UTF-8') ?>');">
|
||||
<?= htmlspecialchars((string)($translations['btn_purge'] ?? 'Purger'), ENT_QUOTES, 'UTF-8') ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($errors)): ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><?= htmlspecialchars((string)($translations['purge_mess_summary'] ?? 'Résumé'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<?= htmlspecialchars((string)($translations['purge_mess_uploads_dir'] ?? 'Dossier uploads'), ENT_QUOTES, 'UTF-8') ?>
|
||||
<code class="ms-2"><?= htmlspecialchars($uploadsDir, ENT_QUOTES, 'UTF-8') ?></code>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<?= htmlspecialchars((string)($translations['purge_mess_files_scanned'] ?? 'Fichiers scannés'), ENT_QUOTES, 'UTF-8') ?>
|
||||
<span class="badge text-bg-secondary ms-2"><?= (int)$stats['seen'] ?></span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<?php
|
||||
$tpl = (string)($translations['purge_mess_orphans_line'] ?? 'Orphelins {STATE}');
|
||||
$state = $dry ? ($translations['purge_mess_orphans_detected'] ?? 'détectés') : ($translations['purge_mess_orphans_deleted'] ?? 'supprimés');
|
||||
echo htmlspecialchars(str_replace('{STATE}', (string)$state, $tpl), ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
<span class="badge text-bg-<?= $dry ? 'warning' : 'danger' ?> ms-2"><?= (int)$stats['deleted'] ?></span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<?= htmlspecialchars((string)($translations['purge_mess_orphans_too_recent'] ?? 'Orphelins trop récents (ignorés)'), ENT_QUOTES, 'UTF-8') ?>
|
||||
<span class="badge text-bg-info ms-2"><?= (int)$stats['skipped'] ?></span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<?= htmlspecialchars((string)($translations['purge_mess_db_rows_fixed'] ?? 'Lignes DB normalisées'), ENT_QUOTES, 'UTF-8') ?>
|
||||
<span class="badge text-bg-primary ms-2"><?= (int)$stats['fixed'] ?></span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<?= htmlspecialchars((string)($translations['purge_mess_duration'] ?? 'Durée'), ENT_QUOTES, 'UTF-8') ?>
|
||||
<span class="badge text-bg-secondary ms-2"><?= number_format($dur, 3) ?> s</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<?php
|
||||
$tpl = (string)($translations['purge_mess_details_header'] ?? 'Détails {MODE}');
|
||||
$mode = $dry ? ($translations['purge_mess_details_sim'] ?? '(simulation)') : ($translations['purge_mess_details_live'] ?? '(LIVE)');
|
||||
echo htmlspecialchars(str_replace('{MODE}', (string)$mode, $tpl), ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<?php if (empty($deletedList)): ?>
|
||||
<div class="p-3 text-muted"><?= htmlspecialchars((string)($translations['purge_mess_no_orphans_found'] ?? 'Aucun fichier orphelin ancien trouvé.'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= htmlspecialchars((string)($translations['th_file'] ?? 'Fichier'), ENT_QUOTES, 'UTF-8') ?></th>
|
||||
<th><?= htmlspecialchars((string)($translations['th_age_days'] ?? 'Âge (j)'), ENT_QUOTES, 'UTF-8') ?></th>
|
||||
<th><?= htmlspecialchars((string)($translations['th_action'] ?? 'Action'), ENT_QUOTES, 'UTF-8') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($deletedList as [$webRel, $age, $act]): ?>
|
||||
<tr>
|
||||
<td><code><?= htmlspecialchars($webRel, ENT_QUOTES, 'UTF-8') ?></code></td>
|
||||
<td><?= number_format($age, 1) ?></td>
|
||||
<td>
|
||||
<span class="badge text-bg-<?= $act === 'supprimé' ? 'danger' : 'secondary' ?>">
|
||||
<?= htmlspecialchars((string)($translations[$act === 'supprimé' ? 'status_deleted' : 'status_simulation'] ?? $act), ENT_QUOTES, 'UTF-8') ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><?= htmlspecialchars((string)($translations['purge_mess_log_title'] ?? 'Journal'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div class="card-body">
|
||||
<pre class="small mb-0" style="white-space:pre-wrap;"><?= htmlspecialchars(implode("\n", $logs), ENT_QUOTES, 'UTF-8') ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
Reference in new issue
Block a user