pas de collision avec index.php */ require_once __DIR__ . '/../../public/includes/init_admin.php'; if (($_GET['page'] ?? '') !== 'purge_conversations') { return; } // Sécurité (l'admin/index.php a normalement déjà vérifié ça) if (session_status() !== PHP_SESSION_ACTIVE) session_start(); // Lang (pour $translations) $roles = (array)($_SESSION['role'] ?? []); if (!in_array('admin', $roles, true)) { echo '
' . htmlspecialchars((string)($translations['admin_access_denied'] ?? 'Accès refusé.'), ENT_QUOTES, 'UTF-8') . '
'; 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; } // PDO si absent if (!isset($pdo)) { $mb = realpath(__DIR__ . '/../../messages/_bootstrap.php'); if ($mb && is_file($mb)) { require_once $mb; } } if (!isset($pdo) || !($pdo instanceof PDO)) { echo '
' . htmlspecialchars((string)($translations['admin_db_unavailable'] ?? 'Connexion DB indisponible.'), ENT_QUOTES, 'UTF-8') . '
'; return; } try { $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (\Throwable $e) {} // CSRF if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } $CSRF = $_SESSION['csrf_token']; // -------- Helpers en closures (pas de fonctions globales !) ---------- $e = static fn($s) => htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); $logs = []; $log = static function(string $m) use (&$logs) { $logs[] = $m; }; $siteRoot = realpath(__DIR__ . '/../../'); $safeWithin = static function(string $path, string $base) { $rp = realpath($path); $rb = realpath($base); return $rp && $rb && strpos($rp, $rb) === 0; }; $tableExists = static function(PDO $pdo, string $table): bool { $sql = "SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?"; $st = $pdo->prepare($sql); $st->execute([$table]); return (bool)$st->fetchColumn(); }; $placeholders = static function(int $n): string { return implode(',', array_fill(0,$n,'?')); }; $chunked = static function(array $ids, int $size=500): array { $out=[]; $chunk=[]; foreach ($ids as $id) { $chunk[]=(int)$id; if (count($chunk)>=$size){$out[]=$chunk;$chunk=[];} } if ($chunk) $out[]=$chunk; return $out; }; // ------------------------------------------------------------------- $retentionDays = isset($_REQUEST['days']) ? max(0, (int)$_REQUEST['days']) : 0; // ex 30 $doLive = false; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'purge') { if (!hash_equals($CSRF, $_POST['csrf'] ?? '')) { echo '
' . $e($translations['csrf_invalid'] ?? 'Token CSRF invalide.') . '
'; return; } $doLive = true; } $dry = !$doLive; $errors = []; $stats = ['conversations'=>0,'messages'=>0,'attachments'=>0,'counters'=>0,'read_markers'=>0,'duration'=>0.0]; $candidates = []; // [id, updated_at] $attachList = []; // max 50 $start = microtime(true); try { // Tables foreach (['conversations','messages'] as $t) { if (!$tableExists($pdo, $t)) { $tpl = (string)($translations['purge_table_missing'] ?? "Table {TABLE} introuvable."); $errors[] = str_replace('{TABLE}', $e($t), $tpl); } } $hasCounters = $tableExists($pdo, 'conversation_counters'); $hasReadMk = $tableExists($pdo, 'read_markers'); if (empty($errors)) { // 1) Candidats $lastSql = "SELECT id, updated_at FROM conversations WHERE deleted_for_praticien=1 AND deleted_for_client=1"; if ($retentionDays > 0) { $lastSql .= " AND updated_at < (NOW() - INTERVAL {$retentionDays} DAY)"; } $rows = $pdo->query($lastSql)->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $r) { $candidates[] = [(int)$r['id'], (string)$r['updated_at']]; } $stats['conversations'] = count($candidates); if ($stats['conversations'] > 0) { $ids = array_column($candidates, 0); // 2) Stats pré-purge foreach ($chunked($ids, 500) as $batch) { $ph = $placeholders(count($batch)); // messages $st = $pdo->prepare("SELECT COUNT(*) FROM messages WHERE conversation_id IN ($ph)"); $st->execute($batch); $stats['messages'] += (int)$st->fetchColumn(); // attachments (sample) $st = $pdo->prepare("SELECT attachment_path FROM messages WHERE conversation_id IN ($ph) AND attachment_path IS NOT NULL AND attachment_path<>''"); $st->execute($batch); while ($row = $st->fetch(PDO::FETCH_ASSOC)) { $p = (string)$row['attachment_path']; if ($p !== '') { $stats['attachments']++; if (count($attachList) < 50) { $attachList[] = $p; } } } if ($hasCounters) { $st = $pdo->prepare("SELECT COUNT(*) FROM conversation_counters WHERE conversation_id IN ($ph)"); $st->execute($batch); $stats['counters'] += (int)$st->fetchColumn(); } if ($hasReadMk) { $st = $pdo->prepare("SELECT COUNT(*) FROM read_markers WHERE conversation_id IN ($ph)"); $st->execute($batch); $stats['read_markers'] += (int)$st->fetchColumn(); } } // 3) Purge LIVE if ($doLive) { $pdo->beginTransaction(); try { // supprimer fichiers de pièces jointes foreach ($chunked($ids, 500) as $batch) { $ph = $placeholders(count($batch)); $st = $pdo->prepare("SELECT attachment_path FROM messages WHERE conversation_id IN ($ph) AND attachment_path IS NOT NULL AND attachment_path<>''"); $st->execute($batch); while ($row = $st->fetch(PDO::FETCH_ASSOC)) { $rel = (string)$row['attachment_path']; // ex: messages/uploads/xxx $abs = $siteRoot . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $rel); if ($safeWithin($abs, $siteRoot) && is_file($abs)) { if (@unlink($abs)) { $log("[OK] ".$e($translations['purge_log_file_deleted'] ?? 'Fichier supprimé').": {$rel}"); } else { $log("[WARN] ".$e($translations['purge_log_file_delete_failed'] ?? 'Échec suppression fichier').": {$rel}"); } } else { $log("[INFO] ".$e($translations['purge_log_file_missing_or_outside'] ?? 'Fichier introuvable/hors périmètre').": {$rel}"); } } } // dépendances foreach ($chunked($ids, 500) as $batch) { $ph = $placeholders(count($batch)); $st = $pdo->prepare("DELETE FROM messages WHERE conversation_id IN ($ph)"); $st->execute($batch); $log("[OK] ".$e($translations['purge_log_messages_deleted_batch'] ?? 'messages supprimés (batch)').": ".$st->rowCount()); if ($hasCounters) { $st = $pdo->prepare("DELETE FROM conversation_counters WHERE conversation_id IN ($ph)"); $st->execute($batch); $log("[OK] ".$e($translations['purge_log_counters_deleted_batch'] ?? 'conversation_counters supprimés (batch)').": ".$st->rowCount()); } if ($hasReadMk) { $st = $pdo->prepare("DELETE FROM read_markers WHERE conversation_id IN ($ph)"); $st->execute($batch); $log("[OK] ".$e($translations['purge_log_read_markers_deleted_batch'] ?? 'read_markers supprimés (batch)').": ".$st->rowCount()); } } // parent foreach ($chunked($ids, 500) as $batch) { $ph = $placeholders(count($batch)); $st = $pdo->prepare("DELETE FROM conversations WHERE id IN ($ph)"); $st->execute($batch); $log("[OK] ".$e($translations['purge_log_conversations_deleted_batch'] ?? 'conversations supprimées (batch)').": ".$st->rowCount()); } $pdo->commit(); $log($translations['purge_done_ok'] ?? 'Purge effectuée avec succès.'); } catch (\Throwable $ex) { $pdo->rollBack(); $tpl = (string)($translations['purge_error'] ?? 'Erreur purge: {ERR}'); $errors[] = str_replace('{ERR}', $e($ex->getMessage()), $tpl); } } else { $log($translations['purge_dry_run_hint'] ?? 'DRY-RUN: aucune suppression; utilisez le bouton Purger en LIVE pour exécuter.'); } } else { $log($translations['purge_none'] ?? 'Aucune conversation à purger.'); } } } catch (\Throwable $ex) { $tpl = (string)($translations['purge_exception'] ?? 'Exception: {ERR}'); $errors[] = str_replace('{ERR}', $e($ex->getMessage()), $tpl); } $stats['duration'] = microtime(true) - $start; ?>

  • s
100): ?>