prepare($sql); $stmt->execute([$messageId]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { http_response_code(404); echo "Message not found"; exit; } // Accès: dois être participant de la conversation assert_conversation_access($pdo, (int)$row['conversation_id'], $role, (int)$meId); // Visibilité: pas rétracté, pas supprimé pour moi, et PJ existante if (!empty($row['retracted_for_all']) || !empty($row['hidden_for_me'])) { http_response_code(403); echo "Forbidden"; exit; } $webRel = (string)($row['attachment_path'] ?? ''); if ($webRel === '') { http_response_code(404); echo "No attachment"; exit; } // Sécurité chemin: doit commencer par messages/uploads/ if (strpos($webRel, 'messages/uploads/') !== 0) { http_response_code(400); echo "Invalid path"; exit; } // Résolution chemin disque $siteRoot = realpath(__DIR__ . '/../../'); // racine site $uploadsAbs = $siteRoot . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $webRel); $real = realpath($uploadsAbs); $uploadsRoot = realpath(__DIR__ . '/../uploads/'); if (!$real || !$uploadsRoot || strpos($real, $uploadsRoot) !== 0 || !is_file($real)) { http_response_code(404); echo "File not found"; exit; } // Détection MIME + nom fichier $finfo = function_exists('finfo_open') ? finfo_open(FILEINFO_MIME_TYPE) : false; $mime = ($finfo && is_file($real)) ? finfo_file($finfo, $real) : null; if ($finfo) { @finfo_close($finfo); } if (!$mime) $mime = 'application/octet-stream'; $name = (string)($row['attachment_name'] ?? basename($real)); $size = filesize($real); // En-têtes (évite le sniffing, force téléchargement) header('Content-Type: ' . $mime); header('Content-Length: ' . (string)$size); header('X-Content-Type-Options: nosniff'); header('Content-Security-Policy: default-src \'none\'; frame-ancestors \'none\'; sandbox'); header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($name)); // Lecture par chunks (robuste) $fp = @fopen($real, 'rb'); if (!$fp) { http_response_code(500); echo "Cannot open file"; exit; } while (!feof($fp)) { $buf = fread($fp, 8192); if ($buf === false) break; echo $buf; // facultatif: flush pour gros fichiers if (function_exists('fastcgi_finish_request')) { /* noop here */ } } fclose($fp); exit;