'csrf'], 419); } $role = $GLOBALS['MESSAGES_CTX']['role']; // 'praticien' | 'client' $meId = $GLOBALS['MESSAGES_CTX']['me_id']; $msgId = (int)($_POST['message_id'] ?? 0); if ($msgId <= 0) json_response(['error' => 'bad_params'], 400); // Récupère le message + conversation $sql = "SELECT m.id, m.conversation_id, m.sender_role, m.sender_id, m.created_at, m.attachment_path, m.attachment_name, m.retracted_for_all, c.praticien_id, c.client_id FROM messages m JOIN conversations c ON c.id = m.conversation_id WHERE m.id = ? LIMIT 1"; $stmt = $pdo->prepare($sql); $stmt->execute([$msgId]); $msg = $stmt->fetch(PDO::FETCH_ASSOC); if (!$msg) json_response(['error' => 'not_found'], 404); // Vérifie que je participe bien à la conversation (sécurité) assert_conversation_access($pdo, (int)$msg['conversation_id'], $role, $meId); // Vérifie que je suis bien l'expéditeur if (!($msg['sender_role'] === $role && (int)$msg['sender_id'] === $meId)) { json_response(['error' => 'not_sender'], 403); } // Déjà rétracté ? if (!empty($msg['retracted_for_all'])) { json_response(['error' => 'already_retracted'], 400); } // Vérifie fenêtre temps $win = (int)RETRACT_WINDOW_MIN; $ageSql = $pdo->prepare("SELECT TIMESTAMPDIFF(MINUTE, ?, NOW()) AS age_min"); $ageSql->execute([$msg['created_at']]); $ageMin = (int)$ageSql->fetchColumn(); if ($ageMin > $win) { json_response(['error' => 'too_late', 'window_min' => $win], 400); } // Vérifie lecture par l'autre (si lu → refus) $otherRole = ($role === 'praticien') ? 'client' : 'praticien'; $readChk = $pdo->prepare("SELECT 1 FROM message_reads WHERE message_id = ? AND user_role = ? LIMIT 1"); $readChk->execute([$msgId, $otherRole]); $alreadyRead = (bool)$readChk->fetchColumn(); if ($alreadyRead) { json_response(['error' => 'already_read'], 400); } // Si non lu par l'autre, il compte encore dans ses non-lus → on décrémentera $counterCol = ($role === 'praticien') ? 'c_unread' : 'p_unread'; // Prépare suppression de l'éventuelle pièce jointe $attPath = $msg['attachment_path'] ?? null; $fullPath = null; if (!empty($attPath) && is_string($attPath)) { // Par convention: messages/uploads/FILENAME if (strpos($attPath, 'messages/uploads/') === 0) { // filesystem: /path/to/site/ + messages/uploads/... $siteRoot = realpath(__DIR__ . '/../../'); // .../ (racine du site) if ($siteRoot) { $fullPath = $siteRoot . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $attPath); } } } try { $pdo->beginTransaction(); // 1) Marquer rétracté (+ effacer contenu & PJ) $upd = $pdo->prepare("UPDATE messages SET retracted_for_all = 1, body = NULL, attachment_path = NULL, attachment_name = NULL WHERE id = ?"); $upd->execute([$msgId]); // 2) Corriger le compteur de non-lus de l'autre si nécessaire (message encore non lu) // On vérifie à nouveau, mais on l'a déjà en mémoire (alreadyRead=false) : if (!$alreadyRead) { $pdo->prepare("UPDATE conversation_counters SET $counterCol = GREATEST($counterCol - 1, 0) WHERE conversation_id = ?") ->execute([(int)$msg['conversation_id']]); } $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } json_response(['error' => 'server_error'], 500); } // 3) Supprime physiquement la PJ (hors transaction DB) if ($fullPath && is_file($fullPath)) { // sécurité simple: s'assure que le chemin reste dans /messages/uploads/ $uploadsRoot = realpath(__DIR__ . '/../uploads/'); $fpReal = realpath($fullPath); if ($uploadsRoot && $fpReal && strpos($fpReal, $uploadsRoot) === 0) { @unlink($fpReal); } } json_response(['ok' => true, 'message_id' => $msgId]);