false, 'error' => 'Se requiere id o url']); exit; } $mediaUrl = null; if ($providedUrl) { // Reconstruir la URL si fue pasada sin codificar y otros parámetros quedaron como GET separados $mediaUrl = $providedUrl; $ignoredKeys = ['url','download','id','local']; $extraParts = []; foreach ($_GET as $k => $v) { if (in_array($k, $ignoredKeys, true)) continue; // Reconstruir sólo si el parámetro no está ya presente en la URL if (strpos($mediaUrl, $k . '=') === false) { $extraParts[] = $k . ($v !== '' ? '=' . rawurlencode($v) : ''); } } if (!empty($extraParts)) { $mediaUrl .= (strpos($mediaUrl, '?') === false ? '?' : '&') . implode('&', $extraParts); } // Intentar detectar un media id embebido en la URL (mid=... o URL hacia graph.facebook.com/v.../{id}) $resolvedFromGraph = false; $mid = null; // Buscar mid en query string (lookaside or other URLs) if (preg_match('/[?&]mid=([^&]+)/i', $mediaUrl, $m)) { $mid = rawurldecode($m[1]); } // Buscar patrón directo hacia graph.facebook.com/.../{mediaId} if (!$mid && preg_match('#https?://[^/]*graph\.facebook\.com(?:/v[0-9]+\.[0-9]+)?/([^/?#&]+)#i', $mediaUrl, $m)) { // El primer segmento después de la versión suele ser el media id $candidate = $m[1]; // Aceptar si parece un id (numérico o alfanum corto) if (preg_match('/^[a-zA-Z0-9_-]+$/', $candidate)) { $mid = $candidate; } } // Si detectamos un mid, consultamos Graph API desde el servidor para obtener la URL (autenticada) if ($mid) { $token = getConfigFromDB('whatsapp_token', ''); $apiUrl = rtrim(getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'), '/'); $endpoint = "{$apiUrl}/{$mid}"; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $endpoint, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $token, 'Accept: application/json' ], CURLOPT_TIMEOUT => 15, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($err || $httpCode >= 400) { media_log("Graph API fetch failed for mid {$mid} - http: {$httpCode} - err: {$err} - resp: " . substr($response ?? '', 0, 1000)); } else { $decoded = json_decode($response, true); $resolvedUrl = $decoded['url'] ?? ($decoded['data'][0]['url'] ?? null); if ($resolvedUrl) { $mediaUrl = $resolvedUrl; $resolvedFromGraph = true; media_log("Resolved media URL from Graph for mid {$mid}: {$mediaUrl}"); } else { media_log("Graph returned no media URL for mid {$mid} - resp: " . substr($response ?? '', 0, 1000)); } } // Si hubo error o no devolvió URL, seguimos usando la URL provista como fallback } } else { $token = getConfigFromDB('whatsapp_token', ''); $apiUrl = rtrim(getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'), '/'); $endpoint = "{$apiUrl}/{$mediaId}"; if (empty($token)) { media_log("Missing whatsapp_token for Graph API request to {$endpoint}"); http_response_code(500); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success' => false, 'error' => 'Missing whatsapp_token in configuration']); exit; } media_log("Graph API request to {$endpoint}"); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $endpoint, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $token, 'Accept: application/json' ], CURLOPT_TIMEOUT => 15, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($err) { http_response_code(502); echo json_encode(['success' => false, 'error' => 'Error comunicando con Graph API', 'detail' => $err]); exit; } $decoded = json_decode($response, true); if ($httpCode >= 400 || !$decoded) { http_response_code($httpCode ?: 500); echo json_encode(['success' => false, 'error' => 'Graph API error', 'response' => $decoded]); exit; } $mediaUrl = $decoded['url'] ?? ($decoded['data'][0]['url'] ?? null); if (!$mediaUrl) { http_response_code(404); echo json_encode(['success' => false, 'error' => 'No se recibió URL del media', 'raw' => $decoded]); exit; } } // Soporte para archivos locales (local=uploads/media/...) $local = $_GET['local'] ?? null; if ($local) { $localPath = realpath(__DIR__ . '/../../' . ltrim($local, '/')); $base = realpath(__DIR__ . '/../../uploads/media'); // Prevención simple de path traversal if (!$localPath || strpos($localPath, $base) !== 0) { http_response_code(400); echo json_encode(['success' => false, 'error' => 'Ruta local inválida']); exit; } if (!file_exists($localPath)) { http_response_code(404); echo json_encode(['success' => false, 'error' => 'Archivo no encontrado']); exit; } if ($wantsJson) { // devolver URL pública directa (relative) echo json_encode(['success' => true, 'url' => '/' . ltrim($local, '/')]); exit; } if ($download) { // Forzar descarga del archivo local header('Cache-Control: public, max-age=3600'); $finfo = finfo_open(FILEINFO_MIME_TYPE); $ctype = finfo_file($finfo, $localPath); finfo_close($finfo); header('Content-Type: ' . $ctype); header('Content-Length: ' . filesize($localPath)); header('Content-Disposition: attachment; filename="' . basename($localPath) . '"'); readfile($localPath); exit; } // Redirigir al archivo local (servidor web lo servirá) header('Cache-Control: public, max-age=3600'); header('Location: /' . ltrim($local, '/'), true, 302); exit; } // Si se solicitó JSON, devolver la URL y metadata si está disponible if ($wantsJson) { $result = ['success' => true, 'url' => $mediaUrl]; // Incluir metadata retornada por Graph API si existe en $decoded if (!empty($decoded) && is_array($decoded)) { // Campos esperados directamente foreach (['url','mime_type','sha256','file_size','id','messaging_product'] as $k) { if (isset($decoded[$k])) $result[$k] = $decoded[$k]; } // También comprobar 'data'[0] (caso de respuestas anidadas) if (isset($decoded['data']) && is_array($decoded['data']) && isset($decoded['data'][0]) && is_array($decoded['data'][0])) { foreach (['url','mime_type','sha256','file_size','id','messaging_product'] as $k) { if (isset($decoded['data'][0][$k])) $result[$k] = $decoded['data'][0][$k]; } } } echo json_encode($result); exit; } // Si se pidió proxy (download=1), traer el contenido y retornarlo como descarga if ($download) { $ch = curl_init(); // Añadir Authorization si existe token en configuración $token = getConfigFromDB('whatsapp_token', ''); $headers = ['User-Agent: WhatsAppMediaProxy/1.0', 'Accept: */*']; if ($token) $headers[] = 'Authorization: Bearer ' . $token; // Pedimos cabeceras + body para poder extraer Content-Disposition curl_setopt_array($ch, [ CURLOPT_URL => $mediaUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 60, CURLOPT_HTTPHEADER => $headers, CURLOPT_HEADER => true, ]); $response = curl_exec($ch); $info = curl_getinfo($ch); $err = curl_error($ch); curl_close($ch); if ($err) { http_response_code(502); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success' => false, 'error' => 'Error al descargar el archivo', 'detail' => $err]); exit; } // Si el servidor remoto devolvió un código HTTP de error, devolverlo al cliente $remoteHttp = isset($info['http_code']) ? (int)$info['http_code'] : 0; if ($remoteHttp >= 400) { http_response_code(502); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success' => false, 'error' => 'Remote server returned error status', 'http_code' => $remoteHttp, 'url' => $mediaUrl]); exit; } $headerSize = isset($info['header_size']) ? (int)$info['header_size'] : 0; $headersText = $headerSize ? substr($response, 0, $headerSize) : ''; $body = $headerSize ? substr($response, $headerSize) : $response; // Parsear cabeceras simples $parsed = []; if ($headersText) { $lines = preg_split('/\r?\n/', $headersText); foreach ($lines as $line) { if (strpos($line, ':') !== false) { list($n, $v) = explode(':', $line, 2); $parsed[strtolower(trim($n))] = trim($v); } } } $contentType = $parsed['content-type'] ?? ($info['content_type'] ?? null); // Si la respuesta es JSON, retornar el error en JSON (evitar descargar JSON como archivo) if ($contentType && stripos($contentType, 'application/json') !== false) { $decoded = json_decode($body, true); http_response_code($decoded && isset($decoded['error']) ? 502 : 500); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success' => false, 'error' => 'Remote server returned JSON instead of file', 'remote' => $decoded]); exit; } // Determinar filename desde Content-Disposition o URL como fallback $filename = null; $cd = $parsed['content-disposition'] ?? null; if ($cd) { if (preg_match('/filename\*=(?:[^']*'')?(.+)$/i', $cd, $m)) { $filename = trim($m[1], " \"'\r\n"); $filename = rawurldecode($filename); } elseif (preg_match('/filename="?([^";]+)"?/i', $cd, $m)) { $filename = trim($m[1]); } } if (!$filename) { $path = parse_url($mediaUrl, PHP_URL_PATH) ?: ''; $basename = basename($path); if ($basename && $basename !== '/') { $filename = $basename; } } // Si aún no hay filename, intentar deducir extensión desde content-type if (!$filename) { $ext = ''; $mimeMap = [ 'application/pdf' => '.pdf', 'text/csv' => '.csv', 'image/jpeg' => '.jpg', 'image/png' => '.png', 'application/zip' => '.zip', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => '.xlsx', 'application/vnd.ms-excel' => '.xls', ]; if ($contentType && isset($mimeMap[strtolower($contentType)])) { $ext = $mimeMap[strtolower($contentType)]; } $filename = 'download' . $ext; } if ($body === null || $body === '') { http_response_code(502); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success' => false, 'error' => 'Empty response body from remote']); exit; } // Enviar body como descarga header_remove('Content-Type'); header('Cache-Control: public, max-age=3600'); if (!empty($contentType)) header('Content-Type: ' . $contentType); header('Content-Length: ' . strlen($body)); header('Content-Disposition: attachment; filename="' . $filename . '"'); echo $body; exit; } // Por defecto: si la URL fue resuelta mediante Graph API, intentar proxificar // (hacer la petición desde el servidor añadiendo Authorization si existe el token) // Esto evita problemas cuando la URL requiere autorización y un 302 directo no funciona. if (!$wantsJson && !empty($resolvedFromGraph) && $resolvedFromGraph) { $ch = curl_init(); $token = getConfigFromDB('whatsapp_token', ''); $headers = ['User-Agent: WhatsAppMediaProxy/1.0', 'Accept: */*']; if ($token) $headers[] = 'Authorization: Bearer ' . $token; curl_setopt_array($ch, [ CURLOPT_URL => $mediaUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 60, CURLOPT_HTTPHEADER => $headers, ]); $body = curl_exec($ch); $info = curl_getinfo($ch); $err = curl_error($ch); curl_close($ch); if ($err) { http_response_code(502); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success' => false, 'error' => 'Error al descargar el archivo (proxy)', 'detail' => $err]); exit; } $remoteHttp = isset($info['http_code']) ? (int)$info['http_code'] : 0; if ($remoteHttp >= 400) { http_response_code(502); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success' => false, 'error' => 'Remote server returned error status', 'http_code' => $remoteHttp, 'url' => $mediaUrl]); exit; } $contentType = $info['content_type'] ?? null; header_remove('Content-Type'); header('Cache-Control: public, max-age=3600'); if (!empty($contentType)) header('Content-Type: ' . $contentType); header('Content-Length: ' . strlen($body)); echo $body; exit; } // Por defecto redirigir al URL (navegador hará la descarga o mostrará la imagen) header('Cache-Control: public, max-age=3600'); header('Location: ' . $mediaUrl, true, 302); exit;