560 lines
21 KiB
PHP
560 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* Endpoint: /api/version/media-url.php
|
|
* Parámetros: id (media id) o url (direct url), opcional download=1 para proxy y forzar descarga
|
|
* - Si se solicita JSON (Accept: application/json) la endpoint requiere autenticación y devuelve {success:true,url:...}
|
|
* - Si se accede mediante GET desde navegador, redirige (302) al URL del media (no requiere sesión)
|
|
*/
|
|
require_once __DIR__ . '/../../config/config.php';
|
|
|
|
// Debugging helpers: log errors to a file and initialize control vars
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', '0');
|
|
ini_set('log_errors', '1');
|
|
ini_set('error_log', __DIR__ . '/media-url_error.log');
|
|
|
|
function media_log($msg) {
|
|
static $triedInit = false;
|
|
$path = __DIR__ . '/media-url_debug.log';
|
|
$dir = dirname($path);
|
|
$entry = date('[Y-m-d H:i:s] ') . $msg . PHP_EOL;
|
|
$ok = false;
|
|
|
|
// Intentar crear directorio si es necesario
|
|
if (!is_dir($dir) && !file_exists($dir)) {
|
|
@mkdir($dir, 0755, true);
|
|
}
|
|
|
|
// Intentar abrir y escribir
|
|
$fp = @fopen($path, 'a');
|
|
if ($fp) {
|
|
if (@fwrite($fp, $entry) !== false) {
|
|
$ok = true;
|
|
}
|
|
@fclose($fp);
|
|
}
|
|
|
|
// Si falla, fallback a error_log y marcar bandera
|
|
if (!$ok) {
|
|
@error_log("media-url (fallback): " . $msg);
|
|
global $log_write_failed;
|
|
$log_write_failed = true;
|
|
}
|
|
|
|
// Intento inicial de escritura para detectar permisos temprano
|
|
if (!$triedInit) {
|
|
$triedInit = true;
|
|
if (isset($log_write_failed) && $log_write_failed) {
|
|
// escribir un pequeño aviso también en error_log
|
|
@error_log('media-url: debug log write failed, check permissions on ' . $dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
$log_write_failed = false;
|
|
$resolvedFromGraph = false;
|
|
$decoded = null;
|
|
|
|
// 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';
|
|
|
|
if (!$mediaId && !$providedUrl) {
|
|
http_response_code(400);
|
|
echo json_encode(['success' => 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;
|
|
// 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/'), '/');
|
|
$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;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Si no es local, hacer curl normal (incluye Authorization si aplica)
|
|
$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) {
|
|
// Usar comillas dobles para el regex que contiene apóstrofes
|
|
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: 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("Proxying Facebook media URL with Authorization to {$mediaUrl}");
|
|
$ch = curl_init();
|
|
$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,
|
|
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 (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;
|
|
}
|
|
|
|
$headerSize = isset($info['header_size']) ? (int)$info['header_size'] : 0;
|
|
$headersText = $headerSize ? substr($response, 0, $headerSize) : '';
|
|
$body = $headerSize ? substr($response, $headerSize) : $response;
|
|
|
|
// Parsear content-type desde headers devueltos
|
|
$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;
|
|
}
|
|
|
|
// 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;
|