false, 'error' => 'Método no permitido']); exit; } try { $token = $_POST['token'] ?? ''; // Validar token if (empty($token) || !preg_match('/^[a-zA-Z0-9]{32,64}$/', $token)) { http_response_code(400); echo json_encode(['success' => false, 'error' => 'Token no válido']); exit; } // Validar archivo if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { $errorMsg = 'No se recibió el archivo'; if (isset($_FILES['file'])) { $uploadErrors = [ UPLOAD_ERR_INI_SIZE => 'El archivo excede el tamaño máximo del servidor', UPLOAD_ERR_FORM_SIZE => 'El archivo excede el tamaño máximo del formulario', UPLOAD_ERR_PARTIAL => 'El archivo se subió parcialmente', UPLOAD_ERR_NO_FILE => 'No se envió ningún archivo', UPLOAD_ERR_NO_TMP_DIR => 'Falta la carpeta temporal del servidor', UPLOAD_ERR_CANT_WRITE => 'No se pudo escribir el archivo en disco', ]; $errorMsg = $uploadErrors[$_FILES['file']['error']] ?? 'Error de subida desconocido'; } http_response_code(400); echo json_encode(['success' => false, 'error' => $errorMsg]); exit; } $db = Database::getInstance(); // Buscar solicitud por token $request = $db->fetch( "SELECT fr.*, u.name as user_name, u.phone_number as user_phone FROM file_requests fr LEFT JOIN users u ON fr.user_id = u.id WHERE fr.token = ?", [$token] ); if (!$request) { http_response_code(404); echo json_encode(['success' => false, 'error' => 'Solicitud no encontrada']); exit; } // Verificar expiración if (strtotime($request['expires_at']) < time()) { http_response_code(410); echo json_encode(['success' => false, 'error' => 'El enlace ha expirado']); exit; } // Verificar estado if ($request['status'] === 'cancelled') { http_response_code(410); echo json_encode(['success' => false, 'error' => 'La solicitud fue cancelada']); exit; } $file = $_FILES['file']; $maxSize = intval($request['max_file_size']); // Validar tamaño if ($file['size'] > $maxSize) { $maxMB = round($maxSize / (1024 * 1024)); http_response_code(413); echo json_encode(['success' => false, 'error' => "El archivo excede el límite de {$maxMB} MB"]); exit; } // Detectar MIME type $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); if (!$mimeType) { $mimeType = $file['type'] ?: 'application/octet-stream'; } // Clasificar tipo de media $mediaType = 'other'; if (strpos($mimeType, 'image/') === 0) $mediaType = 'image'; elseif (strpos($mimeType, 'video/') === 0) $mediaType = 'video'; elseif (strpos($mimeType, 'audio/') === 0) $mediaType = 'audio'; elseif (strpos($mimeType, 'application/') === 0 || strpos($mimeType, 'text/') === 0) $mediaType = 'document'; // Validar tipo permitido $allowedTypes = explode(',', $request['allowed_types']); $typeAllowed = in_array($mediaType, $allowedTypes) || in_array('other', $allowedTypes); if (!$typeAllowed && $mediaType === 'other') { // Dar paso a "other" si document está permitido $typeAllowed = in_array('document', $allowedTypes); } if (!$typeAllowed) { http_response_code(400); echo json_encode(['success' => false, 'error' => 'Tipo de archivo no permitido']); exit; } // Crear directorio de destino $uploadDir = __DIR__ . '/../uploads/file_requests/' . date('Y') . '/' . date('m'); if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } // Generar nombre único $ext = pathinfo($file['name'], PATHINFO_EXTENSION) ?: 'bin'; $ext = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $ext)); $storedName = 'fr_' . $request['id'] . '_' . uniqid() . '.' . $ext; $destPath = $uploadDir . '/' . $storedName; $relativePath = 'uploads/file_requests/' . date('Y') . '/' . date('m') . '/' . $storedName; // Mover archivo if (!move_uploaded_file($file['tmp_name'], $destPath)) { http_response_code(500); echo json_encode(['success' => false, 'error' => 'No se pudo guardar el archivo en disco']); exit; } // Generar thumbnail para imágenes $thumbnailPath = null; if ($mediaType === 'image' && class_exists('GdImage') || function_exists('imagecreatefromjpeg')) { try { $thumbName = 'thumb_' . $storedName; $thumbDest = $uploadDir . '/' . $thumbName; if (createThumbnail($destPath, $thumbDest, 200, 200)) { $thumbnailPath = 'uploads/file_requests/' . date('Y') . '/' . date('m') . '/' . $thumbName; } } catch (Exception $e) { // Thumbnail falla silenciosamente error_log('Thumbnail error: ' . $e->getMessage()); } } // Guardar en BD - tabla file_request_uploads $uploadId = $db->insert('file_request_uploads', [ 'request_id' => $request['id'], 'original_filename' => $file['name'], 'stored_filename' => $storedName, 'file_path' => $relativePath, 'file_size' => $file['size'], 'mime_type' => $mimeType, 'media_type' => $mediaType, 'thumbnail_path' => $thumbnailPath, ]); // Actualizar estado de la solicitud a "uploaded" $db->update('file_requests', ['status' => 'uploaded'], 'id = ?', [$request['id']]); // Generar URL pública del archivo $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $appUrl = $protocol . '://' . $_SERVER['HTTP_HOST']; $fileUrl = $appUrl . '/' . $relativePath; $thumbUrl = $thumbnailPath ? ($appUrl . '/' . $thumbnailPath) : null; // Insertar como mensaje en la conversación (para que el operador lo vea en el chat) $db->insert('conversations', [ 'user_id' => $request['user_id'], 'direction' => 'incoming', 'message_type' => $mediaType === 'other' ? 'document' : $mediaType, 'content' => '📎 Archivo recibido via enlace: ' . $file['name'], 'media_url' => $fileUrl, 'local_file' => $relativePath, 'local_thumb' => $thumbnailPath, 'status' => 'received', 'is_read' => 0, 'filename' => $file['name'], 'mime_type' => $mimeType, 'created_at' => date('Y-m-d H:i:s'), ]); // Crear notificación para el operador $userName = $request['user_name'] ?: $request['phone_number']; $db->insert('notifications', [ 'user_id' => $request['user_id'], 'type' => 'file_uploaded', 'message' => "📎 {$userName} subió un archivo: {$file['name']}", 'data' => json_encode([ 'request_id' => $request['id'], 'upload_id' => $uploadId, 'filename' => $file['name'], 'file_url' => $fileUrl, 'media_type' => $mediaType, 'file_size' => $file['size'], ]), 'is_read' => 0, ]); // Escribir evento SSE para actualización en tiempo real try { $eventsFile = __DIR__ . '/../uploads/events_global.json'; $events = []; if (file_exists($eventsFile)) { $raw = file_get_contents($eventsFile); $events = json_decode($raw, true) ?: []; } $events[] = [ 'type' => 'file_uploaded', 'user_id' => $request['user_id'], 'data' => [ 'request_id' => $request['id'], 'upload_id' => $uploadId, 'filename' => $file['name'], 'file_url' => $fileUrl, 'thumb_url' => $thumbUrl, 'media_type' => $mediaType, 'file_size' => $file['size'], 'user_name' => $userName, ], 'timestamp' => time(), ]; file_put_contents($eventsFile, json_encode($events)); } catch (Exception $e) { error_log('Error escribiendo evento SSE: ' . $e->getMessage()); } echo json_encode([ 'success' => true, 'file_id' => $uploadId, 'filename' => $file['name'], 'file_url' => $fileUrl, 'media_type' => $mediaType, 'file_size' => $file['size'], ]); } catch (Exception $e) { $errorDetail = date('[Y-m-d H:i:s] ') . 'file_request_upload ERROR: ' . $e->getMessage() . "\n" . $e->getTraceAsString() . "\n\n"; error_log('Error en file_request_upload: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString()); @file_put_contents(__DIR__ . '/../logs/app_errors.log', $errorDetail, FILE_APPEND); http_response_code(500); echo json_encode(['success' => false, 'error' => 'Error interno del servidor']); } /** * Crear thumbnail de una imagen usando GD */ function createThumbnail($source, $dest, $maxW = 200, $maxH = 200) { $info = @getimagesize($source); if (!$info) return false; $mime = $info['mime']; $origW = $info[0]; $origH = $info[1]; switch ($mime) { case 'image/jpeg': $img = @imagecreatefromjpeg($source); break; case 'image/png': $img = @imagecreatefrompng($source); break; case 'image/gif': $img = @imagecreatefromgif($source); break; case 'image/webp': $img = @imagecreatefromwebp($source); break; default: return false; } if (!$img) return false; // Calcular dimensiones $ratio = min($maxW / $origW, $maxH / $origH); if ($ratio >= 1) { // La imagen ya es más pequeña que el thumb imagedestroy($img); return copy($source, $dest); } $newW = intval($origW * $ratio); $newH = intval($origH * $ratio); $thumb = imagecreatetruecolor($newW, $newH); // Preservar transparencia para PNG/GIF if ($mime === 'image/png' || $mime === 'image/gif') { imagealphablending($thumb, false); imagesavealpha($thumb, true); $transparent = imagecolorallocatealpha($thumb, 0, 0, 0, 127); imagefilledrectangle($thumb, 0, 0, $newW, $newH, $transparent); } imagecopyresampled($thumb, $img, 0, 0, 0, 0, $newW, $newH, $origW, $origH); // Guardar como JPEG (más ligero para thumbs) $result = imagejpeg($thumb, $dest, 85); imagedestroy($img); imagedestroy($thumb); return $result; }