$url, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 300, CURLOPT_CONNECTTIMEOUT => 30, CURLOPT_BUFFERSIZE => 65536, CURLOPT_HTTPHEADER => $curlHeaders, CURLOPT_HEADERFUNCTION => function($ch, $headerLine) use (&$respHeaders) { $len = strlen($headerLine); $parts = explode(':', $headerLine, 2); if (count($parts) == 2) { $respHeaders[strtolower(trim($parts[0]))] = trim($parts[1]); } return $len; }, CURLOPT_WRITEFUNCTION => function($ch, $data) use (&$browserHeadersSent, &$respHeaders, &$isError, &$errorBody, $forceDownload, $suggestedFilename) { $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // Buffer error responses if ($httpCode >= 400) { $isError = true; $errorBody .= $data; return strlen($data); } // Detect JSON error responses if (!$browserHeadersSent) { $ct = $respHeaders['content-type'] ?? curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?? ''; if (stripos($ct, 'application/json') !== false) { $isError = true; $errorBody .= $data; return strlen($data); } } // Send browser headers on first data chunk if (!$browserHeadersSent) { $browserHeadersSent = true; $contentType = $respHeaders['content-type'] ?? curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?? 'application/octet-stream'; $contentLength = $respHeaders['content-length'] ?? null; header_remove('Content-Type'); header('Cache-Control: public, max-age=86400'); header('Content-Type: ' . $contentType); if ($contentLength) header('Content-Length: ' . $contentLength); if ($forceDownload) { $filename = $suggestedFilename; if (!$filename) { $cd = $respHeaders['content-disposition'] ?? null; if ($cd) { if (preg_match("/filename\\*=(?:[^']*'')?(.+)$/i", $cd, $m)) { $filename = rawurldecode(trim($m[1], " \"'\r\n")); } elseif (preg_match('/filename="?([^";]+)"?/i', $cd, $m)) { $filename = trim($m[1]); } } } if (!$filename) { $path = parse_url(curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) ?: '', PHP_URL_PATH) ?: ''; $filename = basename($path) ?: 'download'; if (strpos($filename, '.') === false) { $mimeMap = ['application/pdf'=>'.pdf','image/jpeg'=>'.jpg','image/png'=>'.png','video/mp4'=>'.mp4','audio/mpeg'=>'.mp3','audio/ogg'=>'.ogg']; $baseCt = strtolower(trim(explode(';', $contentType)[0])); if (isset($mimeMap[$baseCt])) $filename .= $mimeMap[$baseCt]; } } header('Content-Disposition: attachment; filename="' . $filename . '"'); } } echo $data; flush(); return strlen($data); }, ]); curl_exec($ch); $err = curl_error($ch); $finalHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($err || $isError) { if (!$browserHeadersSent) { http_response_code(502); header('Content-Type: application/json; charset=utf-8'); if ($isError && $errorBody) { $decoded = json_decode($errorBody, true); echo json_encode(['success' => false, 'error' => 'Remote server error', 'http_code' => $finalHttpCode, 'remote' => $decoded]); } else { echo json_encode(['success' => false, 'error' => 'Error streaming file', 'detail' => $err]); } } media_log("streamRemoteFile failed: url={$url} err={$err} http={$finalHttpCode}"); return false; } return true; } // Log de petición inicial media_log("Request: " . ($_SERVER['REQUEST_METHOD'] ?? 'GET') . " " . ($_SERVER['REQUEST_URI'] ?? '') . " GET:" . json_encode($_GET) . " POST:" . json_encode($_POST)); // Manejador de excepciones no capturadas -> devolver JSON claro set_exception_handler(function($e){ media_log("Uncaught exception: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine()); http_response_code(500); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success'=>false,'error'=>'Uncaught exception','detail'=>$e->getMessage()]); exit; }); // Shutdown handler para capturar errores fatales (E_ERROR, E_PARSE, ...) register_shutdown_function(function(){ $err = error_get_last(); if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { media_log("Fatal error: " . ($err['message'] ?? '') . " in " . ($err['file'] ?? '') . ":" . ($err['line'] ?? '')); http_response_code(500); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success'=>false,'error'=>'Fatal error','detail'=>$err]); // no exit; (shutdown) } }); $isGet = $_SERVER['REQUEST_METHOD'] === 'GET'; $accept = $_SERVER['HTTP_ACCEPT'] ?? ''; $wantsJson = strpos($accept, 'application/json') !== false; // Requerir autenticación sólo para peticiones no-GET o cuando se solicita JSON (API/AJAX) if (!$isGet || $wantsJson) { requireAuthentication(); } header('Content-Type: application/json; charset=utf-8'); $mediaId = $_GET['id'] ?? $_POST['id'] ?? null; $providedUrl = $_GET['url'] ?? $_POST['url'] ?? null; $download = isset($_GET['download']) && $_GET['download'] === '1'; $debug = isset($_GET['debug']) && $_GET['debug'] === '1'; if (!$mediaId && !$providedUrl) { http_response_code(400); echo json_encode(['success' => false, 'error' => 'Se requiere id o url']); exit; } // ── PRIORIDAD: Buscar archivo local en BD antes de consultar Graph API ── // También buscar por URL completa (para videos/docs enviados como URL lookaside) if ($providedUrl) { try { $db = Database::getInstance(); $row = $db->fetch( "SELECT local_file, local_thumb, mime_type, filename FROM conversations WHERE media_url = :url AND local_file IS NOT NULL AND local_file != '' ORDER BY id DESC LIMIT 1", ['url' => $providedUrl] ); if ($row && !empty($row['local_file'])) { $localPath = realpath(__DIR__ . '/../../' . ltrim($row['local_file'], '/')); $base = realpath(__DIR__ . '/../../uploads'); if ($localPath && strpos($localPath, $base) === 0 && file_exists($localPath)) { media_log("Serving local file for URL {$providedUrl}: {$localPath}"); $finfo = finfo_open(FILEINFO_MIME_TYPE); $ctype = finfo_file($finfo, $localPath) ?: ($row['mime_type'] ?? 'application/octet-stream'); finfo_close($finfo); header_remove('Content-Type'); header('Cache-Control: public, max-age=86400'); header('Content-Type: ' . $ctype); header('Content-Length: ' . filesize($localPath)); if ($download) { $fname = $row['filename'] ?: basename($localPath); header('Content-Disposition: attachment; filename="' . $fname . '"'); } readfile($localPath); exit; } } } catch (Exception $e) { media_log("Error buscando local_file por URL: " . $e->getMessage()); } } if ($mediaId) { try { $db = Database::getInstance(); // Buscar por media_url (que almacena el media ID) o por whatsapp_media_id $row = $db->fetch( "SELECT local_file, local_thumb, mime_type, filename FROM conversations WHERE (media_url = :mid OR whatsapp_media_id = :mid2) AND local_file IS NOT NULL AND local_file != '' ORDER BY id DESC LIMIT 1", ['mid' => $mediaId, 'mid2' => $mediaId] ); if ($row && !empty($row['local_file'])) { $localPath = realpath(__DIR__ . '/../../' . ltrim($row['local_file'], '/')); $base = realpath(__DIR__ . '/../../uploads'); if ($localPath && strpos($localPath, $base) === 0 && file_exists($localPath)) { media_log("Serving local file for media ID {$mediaId}: {$localPath}"); $finfo = finfo_open(FILEINFO_MIME_TYPE); $ctype = finfo_file($finfo, $localPath) ?: ($row['mime_type'] ?? 'application/octet-stream'); finfo_close($finfo); header_remove('Content-Type'); header('Cache-Control: public, max-age=86400'); header('Content-Type: ' . $ctype); header('Content-Length: ' . filesize($localPath)); if ($download) { $fname = $row['filename'] ?: basename($localPath); header('Content-Disposition: attachment; filename="' . $fname . '"'); } readfile($localPath); exit; } } } catch (Exception $e) { media_log("Error buscando local_file en BD: " . $e->getMessage()); // Continuar con Graph API como fallback } } // ── Si no hay archivo local, descargar con MediaService y guardar como cache permanente ── if ($mediaId) { try { require_once __DIR__ . '/../../classes/MediaService.php'; $ms = new MediaService(); $subdir = date('Y/m'); $result = $ms->fetchAndStoreFromGraph($mediaId, $subdir); if ($result && !empty($result['local_file'])) { // Actualizar BD para que próximas peticiones se sirvan desde local try { $db = $db ?? Database::getInstance(); $db->query( "UPDATE conversations SET local_file = :lf, local_thumb = :lt, mime_type = :mt WHERE (media_url = :mid OR whatsapp_media_id = :mid2) AND (local_file IS NULL OR local_file = '')", [ 'lf' => $result['local_file'], 'lt' => $result['local_thumb'] ?? null, 'mt' => $result['mime_type'] ?? null, 'mid' => $mediaId, 'mid2' => $mediaId ] ); media_log("Auto-cached media {$mediaId} → {$result['local_file']}"); } catch (Exception $dbErr) { media_log("DB update failed after caching media {$mediaId}: " . $dbErr->getMessage()); } // Servir el archivo recién descargado $localPath = realpath(__DIR__ . '/../../' . ltrim($result['local_file'], '/')); if ($localPath && file_exists($localPath)) { $finfo = finfo_open(FILEINFO_MIME_TYPE); $ctype = finfo_file($finfo, $localPath) ?: ($result['mime_type'] ?? 'application/octet-stream'); finfo_close($finfo); header_remove('Content-Type'); header('Cache-Control: public, max-age=86400'); header('Content-Type: ' . $ctype); header('Content-Length: ' . filesize($localPath)); if ($download) { $fname = basename($localPath); header('Content-Disposition: attachment; filename="' . $fname . '"'); } readfile($localPath); exit; } } } catch (Exception $e) { media_log("Auto-download failed for media {$mediaId}: " . $e->getMessage()); // Continuar con Graph API proxy como fallback } } $mediaUrl = null; if ($providedUrl) { // Reconstruir la URL si fue pasada sin codificar y otros parámetros quedaron como GET separados $mediaUrl = $providedUrl; // Normalizar entidades HTML que puedan haber sido almacenadas (ej. & -> &) $mediaUrl = html_entity_decode($mediaUrl, ENT_QUOTES | ENT_HTML5); $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/'), '/'); // Validar que mediaId no esté vacío antes de construir el endpoint if (empty($mediaId) || !is_string($mediaId) || trim($mediaId) === '') { media_log("Invalid or empty mediaId provided: " . var_export($mediaId, true)); http_response_code(400); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'success' => false, 'error' => 'Invalid media ID', 'hint' => 'Media ID cannot be empty or invalid' ]); exit; } $endpoint = "{$apiUrl}/{$mediaId}"; if (empty($token)) { media_log("Missing whatsapp_token for Graph API request to {$endpoint}"); http_response_code(400); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'success' => false, 'error' => 'Missing whatsapp_token in configuration', 'hint' => 'Set the token using /configurar_whatsapp.php or insert into system_config (key: whatsapp_token). Run verificar_token.php to validate.' ]); 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; } } // === MODO DEBUG: muestra diagnóstico en lugar de redirigir/descargar === if ($debug) { header('Content-Type: text/html; charset=utf-8'); $tokenRaw = getConfigFromDB('whatsapp_token', ''); $tokenPreview = $tokenRaw ? substr($tokenRaw, 0, 15) . '...' . substr($tokenRaw, -10) : '(vacío)'; $apiUrlConfig = rtrim(getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'), '/'); echo '
';
echo "Media ID: " . htmlspecialchars($mediaId ?? '(no proporcionado)') . "\n";
echo "URL proporcionada: " . htmlspecialchars($providedUrl ?? '(no proporcionada)') . "\n";
echo "download: " . ($download ? '1' : '0') . "\n";
echo '';
echo '';
echo "Token: " . htmlspecialchars($tokenPreview) . "\n";
echo "API URL: " . htmlspecialchars($apiUrlConfig) . "\n";
if (empty($tokenRaw)) {
echo '⛔ ERROR: Token de WhatsApp vacío o no configurado' . "\n";
echo 'Solución: Configurar en /configurar_whatsapp.php o en system_config (key: whatsapp_token)' . "\n";
} else {
echo '✅ Token presente' . "\n";
}
echo '';
echo '';
if ($mediaUrl) {
echo '✅ URL resuelta: ' . htmlspecialchars($mediaUrl) . "\n";
echo "Resuelto desde Graph: " . ($resolvedFromGraph ? 'Sí' : 'No') . "\n";
// Validar que la URL sea accesible desde el servidor
echo "\nVerificando accesibilidad de la URL...\n";
$testCh = curl_init();
$testHeaders = ['User-Agent: WhatsAppMediaProxy/1.0', 'Accept: */*'];
if ($tokenRaw) $testHeaders[] = 'Authorization: Bearer ' . $tokenRaw;
curl_setopt_array($testCh, [
CURLOPT_URL => $mediaUrl,
CURLOPT_NOBODY => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => $testHeaders,
]);
curl_exec($testCh);
$testInfo = curl_getinfo($testCh);
$testErr = curl_error($testCh);
curl_close($testCh);
echo "HTTP Status: " . ($testInfo['http_code'] ?? 'N/A') . "\n";
echo "Content-Type: " . ($testInfo['content_type'] ?? 'N/A') . "\n";
echo "Content-Length: " . ($testInfo['download_content_length'] ?? 'N/A') . " bytes\n";
if ($testErr) {
echo '⛔ Error cURL: ' . htmlspecialchars($testErr) . '' . "\n";
}
if (($testInfo['http_code'] ?? 0) >= 400) {
echo '⛔ La URL del media NO es accesible (HTTP ' . $testInfo['http_code'] . ')' . "\n";
if (($testInfo['http_code'] ?? 0) == 404) {
echo '⚠️ El media probablemente expiró. Los archivos multimedia de WhatsApp expiran después de ~30 días.' . "\n";
} elseif (($testInfo['http_code'] ?? 0) == 401 || ($testInfo['http_code'] ?? 0) == 403) {
echo '⚠️ Error de autenticación. Verificar que el token sea válido y tenga permisos.' . "\n";
}
} else {
echo '✅ URL accesible correctamente' . "\n";
}
} else {
echo '⛔ No se pudo resolver la URL del media' . "\n";
if (!empty($decoded)) {
echo "Respuesta de Graph API:\n";
echo htmlspecialchars(json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) . "\n";
// Diagnóstico específico de errores comunes
$errCode = $decoded['error']['code'] ?? null;
$errSubcode = $decoded['error']['error_subcode'] ?? null;
$errMsg = $decoded['error']['message'] ?? '';
if ($errCode == 190) {
echo '⛔ Token expirado o inválido (code 190)' . "\n";
echo 'Solución: Renovar el token en /configurar_whatsapp.php' . "\n";
} elseif ($errCode == 100 && $errSubcode == 33) {
echo '⛔ Media ID no encontrado o expirado' . "\n";
echo 'Los archivos multimedia de WhatsApp Business API expiran aproximadamente 30 días después de ser recibidos.' . "\n";
} elseif ($errCode == 100) {
echo '⛔ Parámetro inválido (code 100)' . "\n";
echo 'El media ID puede ser incorrecto: ' . htmlspecialchars($mediaId ?? '') . '' . "\n";
}
}
}
echo '';
echo '';
echo htmlspecialchars(json_encode($decoded ?? null, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo '';
echo 'Generado: ' . date('Y-m-d H:i:s T') . '
'; echo ''; 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) { // Si la URL apunta al mismo servidor (localhost/127.0.0.1 o al host actual), no usar curl (evita deadlocks en PHP built-in server) $parsedUrl = parse_url($mediaUrl); $host = isset($parsedUrl['host']) ? strtolower($parsedUrl['host']) : ''; $port = isset($parsedUrl['port']) ? $parsedUrl['port'] : null; $isLocalHost = in_array($host, ['127.0.0.1', 'localhost']) || ($host && isset($_SERVER['HTTP_HOST']) && stripos($_SERVER['HTTP_HOST'], $host) !== false); if ($isLocalHost) { media_log("Serving local file directly for URL: {$mediaUrl}"); // Mapear path a archivo local seguro $path = $parsedUrl['path'] ?? ''; // Asumir que los archivos subidos están en uploads/ $localPath = realpath(__DIR__ . '/../../' . ltrim($path, '/')); $base = realpath(__DIR__ . '/../../uploads'); if (!$localPath || strpos($localPath, $base) !== 0 || !file_exists($localPath)) { http_response_code(404); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success' => false, 'error' => 'Archivo local no encontrado o ruta inválida', 'path' => $localPath]); exit; } // Determinar mime type $finfo = finfo_open(FILEINFO_MIME_TYPE); $contentType = finfo_file($finfo, $localPath); finfo_close($finfo); header_remove('Content-Type'); header('Cache-Control: public, max-age=3600'); if (!empty($contentType)) header('Content-Type: ' . $contentType); header('Content-Length: ' . filesize($localPath)); header('Content-Disposition: attachment; filename="' . basename($localPath) . '"'); readfile($localPath); exit; } // Streaming proxy para descargas (soporta archivos grandes sin buffering en memoria) $token = getConfigFromDB('whatsapp_token', ''); media_log("Streaming download for: {$mediaUrl}"); streamRemoteFile($mediaUrl, $token, true); exit; } // Por defecto: si la URL fue resuelta mediante Graph API, streaming proxy // (evita buffering en memoria para archivos grandes como videos) if (!$wantsJson && !empty($resolvedFromGraph) && $resolvedFromGraph) { $token = getConfigFromDB('whatsapp_token', ''); media_log("Streaming Graph-resolved media: {$mediaUrl}"); streamRemoteFile($mediaUrl, $token); exit; } // Por defecto: si la URL parece pertenecer a Facebook/lookaside y disponemos de token, proxificar con Authorization $parsed = parse_url($mediaUrl); $host = isset($parsed['host']) ? strtolower($parsed['host']) : ''; $token = getConfigFromDB('whatsapp_token', ''); $shouldProxy = false; if ($token && ($host === 'lookaside.fbsbx.com' || stripos($host, 'graph.facebook.com') !== false || stripos($host, 'facebook.com') !== false)) { $shouldProxy = true; } if ($shouldProxy) { media_log("Streaming Facebook media with Authorization: {$mediaUrl}"); streamRemoteFile($mediaUrl, $token); exit; } // Fallback: redirigir al URL (navegador hará la descarga o mostrará la imagen) header('Cache-Control: public, max-age=3600'); header('Location: ' . $mediaUrl, true, 302); exit;