Saltar al contenido principal

Subir archivos

La subida de archivos es una funcionalidad común en aplicaciones web que permite a los usuarios cargar documentos, imágenes u otros tipos de ficheros al servidor. PHP proporciona un mecanismo robusto para gestionar esta operación de forma segura a través la variable superglobal $_FILES, la cual es un array asociativo con múltiples datos.

Para implementar la funcionalidad de subida de ficheros necesitamos dos elementos: el formulario y el script PHP que procese toda la información. Pueden estar en ficheros diferentes o todo en el mismo fichero.

Formulario HTML

Para permitir la subida de archivos, el formulario HTML debe cumplir tres requisitos esenciales:

  1. El atributo method debe ser POST.
  2. El atributo enctype debe establecerse como multipart/form-data.
  3. Debe incluir un campo <input type="file">.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Subida de Archivos</title>
</head>
<body>
<h2>Formulario de Subida</h2>
<form action="procesar_subida.php" method="POST" enctype="multipart/form-data">
<!-- Subir un único archivo -->
<label for="archivo">Selecciona un archivo:</label>
<input type="file" name="archivo" id="archivo" required>

<!-- Subir múltiples archivos -->
<label for="archivos">Selecciona archivos:</label>
<input type="file" name="archivos[]" multiple>

<button type="submit">Subir Archivo</button>
</form>
</body>
</html>

El atributo enctype="multipart/form-data" es crucial porque indica que el formulario enviará datos binarios, no solo texto plano.

Script PHP

El archivo PHP que procesa la subida accede a la información del fichero a través del array $_FILES:

procesar_subida.php
<?php
// Verificar que se ha enviado un archivo
if (isset($_FILES['archivo']) && $_FILES['archivo']['error'] === UPLOAD_ERR_OK) {

// Directorio de destino
$directorioDestino = 'uploads/';

// Crear el directorio si no existe
if (!file_exists($directorioDestino)) {
mkdir($directorioDestino, 0755, true);
}

// Información del archivo
$nombreTemporal = $_FILES['archivo']['tmp_name'];
$nombreOriginal = $_FILES['archivo']['name'];
$tamano = $_FILES['archivo']['size'];
$tipo = $_FILES['archivo']['type'];

// Generar un nombre único para evitar sobrescrituras
$nombreUnico = uniqid() . '_' . basename($nombreOriginal);
$rutaDestino = $directorioDestino . $nombreUnico;

// Validaciones adicionales
$extensionesPermitidas = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'];
$extension = strtolower(pathinfo($nombreOriginal, PATHINFO_EXTENSION));

if (!in_array($extension, $extensionesPermitidas)) {
die("Error: Tipo de archivo no permitido.");
}

// Mover el archivo del directorio temporal al destino final
if (move_uploaded_file($nombreTemporal, $rutaDestino)) {
echo "Archivo subido correctamente: " . htmlspecialchars($nombreUnico);
} else {
echo "Error al mover el archivo.";
}

} else {
echo "Error en la subida del archivo.";
if (isset($_FILES['archivo']['error'])) {
echo " Código de error: " . $_FILES['archivo']['error'];
}
}
?>

Parámetros del php.ini

PHP tiene varios parámetros de configuración en php.ini que controlan el comportamiento de la subida de archivos. Para comprobar cuáles son sus valores, podemos consular el php.ini o ejecutar el siguiente código PHP:

<?php
echo ini_get('file_uploads'), PHP_EOL;
echo ini_get('upload_max_filesize'), PHP_EOL;
echo ini_get('post_max_size'), PHP_EOL;
echo ini_get('max_file_uploads'), PHP_EOL;
echo ini_get('upload_tmp_dir'), PHP_EOL;
echo ini_get('max_input_time'), PHP_EOL;
echo ini_get('max_execution_time'), PHP_EOL;

file_uploads

file_uploads = On

Habilita o deshabilita la subida de archivos vía HTTP. Debe estar en On para permitir uploads.

upload_max_filesize

upload_max_filesize = 2M

Define el tamaño máximo permitido para un archivo individual. Valores comunes: 2M, 10M, 50M.

post_max_size

post_max_size = 8M

Establece el tamaño máximo de datos POST. Debe ser mayor que upload_max_filesize porque incluye el archivo más otros datos del formulario.

max_file_uploads

max_file_uploads = 20

Número máximo de archivos que se pueden subir simultáneamente en una sola petición.

upload_tmp_dir

upload_tmp_dir = "/tmp"

Directorio temporal donde PHP almacena los archivos antes de moverlos a su ubicación final. Si no se especifica, usa el directorio temporal del sistema.

max_input_time

max_input_time = 60

Tiempo máximo (en segundos) que un script puede dedicar a recibir datos de entrada.

max_execution_time

max_execution_time = 30

Tiempo máximo de ejecución de un script. Importante para archivos grandes que tardan en procesarse.

La variable $_FILES

Cuando se sube un archivo, PHP almacena su información en el array superglobal $_FILES. Si el campo del formulario se llama archivo, la estructura sería:

$_FILES['archivo'] = [
'name' => 'documento.pdf', // Nombre original del archivo
'type' => 'application/pdf', // Tipo MIME reportado por el navegador
'tmp_name' => '/tmp/phpXXXXXX', // Ruta temporal en el servidor
'error' => 0, // Código de error (0 = sin errores)
'size' => 1024000 // Tamaño en bytes
];

Descripción de cada campo:

  • name: Nombre original del archivo en el ordenador del usuario.
  • type: Tipo MIME del archivo según el navegador (no es totalmente fiable, debe validarse en el servidor).
  • tmp_name: Ruta completa del archivo temporal en el servidor donde PHP almacena inicialmente el archivo subido.
  • error: Código de error que indica el estado de la subida.
  • size: Tamaño del archivo en bytes.

Múltiples archivos

Si el campo permite múltiples archivos (name="archivos[]"), $_FILES tiene una estructura diferente:

$_FILES['archivos'] = [
'name' => ['archivo1.jpg', 'archivo2.pdf'],
'type' => ['image/jpeg', 'application/pdf'],
'tmp_name' => ['/tmp/phpXXXXX1', '/tmp/phpXXXXX2'],
'error' => [0, 0],
'size' => [256000, 512000]
];

Códigos de error

El campo $_FILES['archivo']['error'] contiene un código que indica el resultado de la operación. Las constantes predefinidas son:

UPLOAD_ERR_OK (0)

No hay error, el archivo se subió correctamente.

UPLOAD_ERR_INI_SIZE (1)

El archivo excede el tamaño definido en upload_max_filesize del php.ini.

if ($_FILES['archivo']['error'] === UPLOAD_ERR_INI_SIZE) {
echo "El archivo es demasiado grande (límite del servidor).";
}

UPLOAD_ERR_FORM_SIZE (2)

El archivo excede el tamaño especificado en el campo MAX_FILE_SIZE del formulario HTML.

<input type="hidden" name="MAX_FILE_SIZE" value="1000000">
<input type="file" name="archivo">

UPLOAD_ERR_PARTIAL (3)

El archivo se subió parcialmente (la transferencia se interrumpió).

UPLOAD_ERR_NO_FILE (4)

No se subió ningún archivo. Útil para campos opcionales.

UPLOAD_ERR_NO_TMP_DIR (6)

Falta el directorio temporal en el servidor.

UPLOAD_ERR_CANT_WRITE (7)

Error al escribir el archivo en disco (problemas de permisos).

UPLOAD_ERR_EXTENSION (8)

Una extensión de PHP detuvo la subida del archivo.

Ejemplo completo con manejo de errores

<?php
function obtenerMensajeError($codigoError) {
$mensajes = [
UPLOAD_ERR_OK => 'Subida exitosa',
UPLOAD_ERR_INI_SIZE => 'El archivo excede upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'El archivo excede MAX_FILE_SIZE del formulario',
UPLOAD_ERR_PARTIAL => 'El archivo se subió parcialmente',
UPLOAD_ERR_NO_FILE => 'No se seleccionó ningún archivo',
UPLOAD_ERR_NO_TMP_DIR => 'Falta el directorio temporal',
UPLOAD_ERR_CANT_WRITE => 'Error al escribir en disco',
UPLOAD_ERR_EXTENSION => 'Subida detenida por una extensión PHP'
];

return $mensajes[$codigoError] ?? 'Error desconocido';
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_FILES['archivo'])) {
die("No se recibió ningún archivo.");
}

$error = $_FILES['archivo']['error'];

if ($error !== UPLOAD_ERR_OK) {
die("Error en la subida: " . obtenerMensajeError($error));
}

// Validaciones de seguridad
$tamanoMaximo = 5 * 1024 * 1024; // 5 MB
if ($_FILES['archivo']['size'] > $tamanoMaximo) {
die("El archivo es demasiado grande (máximo 5 MB).");
}

// Validar tipo MIME real (no confiar solo en $_FILES['archivo']['type'])
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$tipoMime = finfo_file($finfo, $_FILES['archivo']['tmp_name']);
finfo_close($finfo);

$tiposPermitidos = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (!in_array($tipoMime, $tiposPermitidos)) {
die("Tipo de archivo no permitido.");
}

// Procesar el archivo
$directorioDestino = 'uploads/';
$nombreArchivo = uniqid() . '_' . basename($_FILES['archivo']['name']);
$rutaCompleta = $directorioDestino . $nombreArchivo;

if (move_uploaded_file($_FILES['archivo']['tmp_name'], $rutaCompleta)) {
echo "Archivo subido exitosamente: " . htmlspecialchars($nombreArchivo);
} else {
echo "Error al mover el archivo al destino final.";
}
}
?>

Consideraciones de seguridad

  1. Validar siempre el tipo de archivo: No confiar únicamente en la extensión o en $_FILES['archivo']['type']. Usar finfo_file() o mime_content_type().
  2. Limitar el tamaño: Establecer límites razonables tanto en php.ini como en la lógica de la aplicación.
  3. Renombrar archivos: Nunca usar directamente el nombre original del archivo para evitar ataques de path traversal.
  4. Almacenar fuera del document root: Idealmente, guardar los archivos en directorios no accesibles directamente por HTTP.
  5. Validar permisos: Asegurar que el directorio de destino tiene los permisos correctos (normalmente 755 para directorios, 644 para archivos).
  6. Sanitizar nombres: Eliminar caracteres especiales y espacios de los nombres de archivo.
  7. Verificar is_uploaded_file(): Antes de move_uploaded_file(), se puede usar is_uploaded_file() para confirmar que el archivo fue realmente subido vía HTTP POST.