Files
whatsapp/api/file_request_upload.php
2026-02-21 07:37:36 -05:00

328 lines
12 KiB
PHP

<?php
/**
* API: Recibir archivos del cliente (público — sin auth de panel)
*
* El cliente sube archivos desde la página upload.php usando un token.
* Los archivos se validan, guardan en disco e insertan en la BD.
* Además se crea un mensaje en la conversación y una notificación para el operador.
*
* POST /api/file_request_upload.php
* Body: multipart/form-data { token: string, file: File }
*
* Respuesta: { success: bool, file_id: int }
*/
error_reporting(E_ERROR | E_PARSE);
ini_set('display_errors', 0);
// Permitir uploads grandes
ini_set('upload_max_filesize', '55M');
ini_set('post_max_size', '60M');
ini_set('max_execution_time', 120);
require_once __DIR__ . '/../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => 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;
}