'No autorizado']); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'GET') { http_response_code(405); exit; } $fileParam = trim($_GET['file'] ?? ''); // Rechazar rutas con traversal if (!$fileParam || strpos($fileParam, '..') !== false) { http_response_code(400); header('Content-Type: application/json'); echo json_encode(['error' => 'Parámetro inválido']); exit; } $projectRoot = realpath(__DIR__ . '/../../'); $uploadsBase = realpath($projectRoot . '/uploads'); // Resolver ruta física if (preg_match('#^uploads/#', $fileParam)) { $path = realpath($projectRoot . '/' . $fileParam); } elseif (preg_match('/^[a-zA-Z0-9_\-\.]+$/', $fileParam)) { $path = realpath($projectRoot . '/uploads/media/' . $fileParam); } else { http_response_code(400); header('Content-Type: application/json'); echo json_encode(['error' => 'Nombre de archivo inválido']); exit; } // Verificar que existe y está dentro de uploads/ if (!$path || !$uploadsBase || strpos($path, $uploadsBase) !== 0 || !is_file($path)) { http_response_code(404); header('Content-Type: application/json'); echo json_encode(['error' => 'Archivo no encontrado']); exit; } // ── Detección MIME en 3 capas ───────────────────────────────────────── $mime = ''; // Capa 1: finfo if (function_exists('finfo_open')) { $fi = finfo_open(FILEINFO_MIME_TYPE); $mime = $fi ? (string)finfo_file($fi, $path) : ''; if ($fi) finfo_close($fi); // Quitar parámetros extras (ej: "image/jpeg; charset=binary") if ($mime && strpos($mime, ';') !== false) { $mime = trim(explode(';', $mime)[0]); } } // Capa 2: magic bytes manuales (más fiable que finfo para archivos renombrados como .bin) if (!$mime || $mime === 'application/octet-stream') { $fh = fopen($path, 'rb'); $bytes = $fh ? fread($fh, 16) : ''; if ($fh) fclose($fh); if (substr($bytes, 0, 3) === "\xFF\xD8\xFF") { $mime = 'image/jpeg'; } elseif (substr($bytes, 0, 8) === "\x89PNG\r\n\x1a\n") { $mime = 'image/png'; } elseif (substr($bytes, 0, 5) === '%PDF-') { $mime = 'application/pdf'; } elseif (substr($bytes, 0, 6) === 'GIF89a' || substr($bytes, 0, 6) === 'GIF87a') { $mime = 'image/gif'; } elseif (substr($bytes, 0, 4) === 'RIFF' && substr($bytes, 8, 4) === 'WEBP') { $mime = 'image/webp'; } elseif (substr($bytes, 0, 4) === "\xD0\xCF\x11\xE0") { $mime = 'application/msword'; // DOC clásico } elseif (substr($bytes, 0, 2) === 'PK') { $mime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; // DOCX/ZIP } } // Capa 3: fallback por extensión del nombre original $ext = strtolower(pathinfo($fileParam, PATHINFO_EXTENSION)); if (!$mime || $mime === 'application/octet-stream') { $mimeByExt = [ 'pdf' => 'application/pdf', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ]; $mime = $mimeByExt[$ext] ?? 'application/octet-stream'; } // ── Nombre de descarga: si era .bin, usar la extensión real ────────── $extByMime = [ 'application/pdf' => 'pdf', 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'application/msword' => 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', ]; $basename = basename($fileParam); $downloadName = $basename; if ($ext === 'bin' && isset($extByMime[$mime])) { $stem = substr($basename, 0, strrpos($basename, '.')); $downloadName = $stem . '.' . $extByMime[$mime]; } // ── Disposición: inline para imágenes/PDF, attachment para el resto ── $inlineMimes = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif', 'image/webp']; $disposition = in_array($mime, $inlineMimes, true) ? 'inline' : 'attachment'; // ── Enviar archivo ─────────────────────────────────────────────────── header('Content-Type: ' . $mime); header('Content-Disposition: ' . $disposition . '; filename="' . addslashes($downloadName) . '"'); header('Content-Length: ' . filesize($path)); header('Cache-Control: private, max-age=3600'); header('X-Content-Type-Options: nosniff'); readfile($path); exit; $fileParam = $_GET['file'] ?? ''; // Rechazar inmediatamente rutas con .. if (!$fileParam || strpos($fileParam, '..') !== false) { http_response_code(400); echo json_encode(['error' => 'Archivo inválido']); exit; } $projectRoot = realpath(__DIR__ . '/../../'); $uploadsBase = realpath($projectRoot . '/uploads'); // Resolver la ruta completa: // - Si ya empieza con uploads/ → resolver desde raíz del proyecto // - Si es solo un nombre de archivo → buscar en uploads/media/ if (preg_match('#^uploads/#', $fileParam)) { $path = realpath($projectRoot . '/' . $fileParam); } else { // Solo nombre: solo aceptar alfanumérico + guiones/puntos, sin slashes if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $fileParam)) { http_response_code(400); echo json_encode(['error' => 'Archivo inválido']); exit; } $path = realpath($projectRoot . '/uploads/media/' . $fileParam); } $base = $uploadsBase; // Verificar que el archivo esté dentro del directorio permitido if (!$path || !$base || strpos($path, $base) !== 0 || !is_file($path)) { http_response_code(404); echo json_encode(['error' => 'Archivo no encontrado']); exit; } // Detectar MIME real — 3 capas para máxima fiabilidad con archivos .bin // Capa 1: finfo (usa magic bytes internamente) $mime = ''; if (function_exists('finfo_open')) { $fi = finfo_open(FILEINFO_MIME_TYPE); $mime = $fi ? (string)finfo_file($fi, $path) : ''; if ($fi) finfo_close($fi); } // Capa 2: Lectura manual de magic bytes (funciona aunque finfo falle o devuelva octet-stream) if (!$mime || $mime === 'application/octet-stream') { $handle = fopen($path, 'rb'); $bytes = $handle ? fread($handle, 12) : ''; if ($handle) fclose($handle); if (substr($bytes, 0, 3) === "\xFF\xD8\xFF") { $mime = 'image/jpeg'; } elseif (substr($bytes, 0, 8) === "\x89PNG\r\n\x1a\n") { $mime = 'image/png'; } elseif (substr($bytes, 0, 5) === '%PDF-') { $mime = 'application/pdf'; } elseif (substr($bytes, 0, 6) === 'GIF89a' || substr($bytes, 0, 6) === 'GIF87a') { $mime = 'image/gif'; } elseif (substr($bytes, 0, 4) === 'RIFF' && substr($bytes, 8, 4) === 'WEBP') { $mime = 'image/webp'; } elseif (substr($bytes, 0, 2) === 'PK') { $mime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; } } // Capa 3: Fallback por extensión declarada $ext = strtolower(pathinfo($fileParam, PATHINFO_EXTENSION)); $mimeMap = [ 'pdf' => 'application/pdf', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', ]; if (!$mime || $mime === 'application/octet-stream') { $mime = $mimeMap[$ext] ?? 'application/octet-stream'; } // Tipos que el navegador puede mostrar inline $inlineTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif', 'image/webp']; $disposition = in_array($mime, $inlineTypes) ? 'inline' : 'attachment'; // Si el archivo fue guardado con extensión .bin, usar la correcta según MIME $mimeToExt = [ 'application/pdf' => 'pdf', 'application/msword' => 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', ]; $downloadExt = ($ext === 'bin' && isset($mimeToExt[$mime])) ? $mimeToExt[$mime] : $ext; $downloadName = basename($fileParam); if ($ext === 'bin' && isset($mimeToExt[$mime])) { $downloadName = substr($downloadName, 0, strrpos($downloadName, '.') + 1) . $downloadExt; } header('Content-Type: ' . $mime); header('Content-Disposition: ' . $disposition . '; filename="' . rawurlencode($downloadName) . '"'); header('Content-Length: ' . filesize($path)); header('Cache-Control: private, max-age=3600'); header('X-Content-Type-Options: nosniff'); readfile($path); exit;