El problema típico
Muchos proveedores ofrecen el CSV detrás de un área privada. En el navegador funciona, pero en automatización suele fallar por:
- Sesiones que dependen de cookies (si no las guardas, te devuelven la pantalla de login).
- Redirecciones (si no sigues Location , te quedas a medias).
- Descargas que “parecen” un CSV pero realmente devuelven HTML.
- Concurrencia: dos procesos descargando a la vez se pisan (cookies o ficheros temporales).
Qué hace este código en ShopinCloud
En ShopinCloud monté una acción que hace la descarga de forma robusta usando cURL y una carpeta temporal. La idea es: login → validar sesión → descargar CSV con la misma sesión → servir el CSV al usuario .
1) Login con cURL y persistencia de cookies
El script hace un POST al formulario de login y guarda la sesión en un cookie jar (fichero de cookies). Así, cuando se llama al endpoint real del CSV, Matacanaria ya reconoce la sesión.
- Se usa
CURLOPT_FOLLOWLOCATIONpara seguir redirecciones. - Se configura
CURLOPT_COOKIEJARpara persistir cookies en fichero. - Se valida que existan cookies (en memoria o en fichero) tras el login.
2) Carpeta temporal + lock para evitar concurrencia
Para que no haya dos descargas pisándose (especialmente con cookies), se crea un lock por email y se bloquea con flock(). Así garantizas que cada credencial mantiene su sesión limpia durante la descarga.
- Directorio temporal tipo:
cache/tmp/matacanaria. - Cookie file estable por usuario (hash del email).
- Lock file asociado a ese cookie file.
3) Descarga del CSV usando la misma sesión
Con el mismo handle de cURL ($ch), se cambia la URL al endpoint de descarga y se vuelca directamente a un fichero temporal con CURLOPT_FILE. Esto evita tener que cargar todo el CSV en memoria.
4) Validaciones “anti-login”
El fallo más común es que el endpoint devuelva HTML (login) en vez de CSV. Para detectarlo:
- Se leen los primeros bytes del fichero descargado y se busca
<html, “login” o “iniciar sesión”. - Se revisa el HTTP status (si no es 2xx/3xx, se elimina el fichero).
- Se calcula el tamaño real antes de servirlo (Content-Length).
5) Servir el CSV al navegador
Cuando el fichero ya está bien descargado, el script envía cabeceras de descarga (Content-Disposition) y hace readfile(). Además, limpia buffers con ob_end_clean() para evitar que se “ensucie” el CSV.
Recomendaciones rápidas
- No hardcodees credenciales en el código: usa variables de entorno o configuración cifrada en tu plataforma.
- Guarda logs solo si hace falta (y rota el fichero de verbose).
- Si el proveedor cambia el formulario o endpoint, centraliza esas URLs en config.
- Si hay varios usuarios/tiendas, genera cookie/lock por “cuenta” real del proveedor.
Código para compartir (sin credenciales)
Aquí tienes el código tal cual para que quien lo necesite lo adapte. Ojo: he eliminado email y password (y cualquier dato sensible). Configura tú las credenciales en tu sistema de configuración/entorno.
<?php
if (!defined('BASEPATH')) exit('No direct script access allowed');
class MY_matacanaria_action extends ED_Front_Actions
{
// ✅ NO poner credenciales aquí. Cárgalas desde config/ENV.
private $_email = '';
private $_password = '';
public function __construct()
{
parent::__construct();
}
public function downloadFile()
{
$loginUrl = 'https://www.matacanaria.com/iniciar-sesion?back=my-account';
$email = $this->_email;
$password = $this->_password;
if (empty($email) || empty($password)) {
$this->output
->set_status_header(500)
->set_content_type('application/json')
->set_output(json_encode(['ok' => false, 'error' => 'Faltan credenciales Matacanaria en config']));
return;
}
$catalogUrl = 'https://www.matacanaria.com/module/terranetpricelist/download'
. '?getCsv=1&email=' . rawurlencode($email);
$tmpDir = frontROOTPATH . 'cache/tmp/matacanaria';
if (!is_dir($tmpDir)) {
if (!mkdir($tmpDir, 0755, true)) {
$this->output
->set_status_header(500)
->set_content_type('application/json')
->set_output(json_encode(['ok' => false, 'error' => 'No se pudo crear el directorio temporal']));
return;
}
}
// Cookie estable + lock para evitar concurrencia pisándose
$cookieFile = $tmpDir . DIRECTORY_SEPARATOR . 'matacanaria_cookie_' . md5($email) . '.txt';
$lockFile = $cookieFile . '.lock';
$outFile = $tmpDir . DIRECTORY_SEPARATOR . 'matacanaria_catalog_' . date('Ymd_His') . '.csv';
$lockFp = null;
$fpOut = null;
$ch = null;
$verboseFp = null;
try {
$lockFp = @fopen($lockFile, 'c');
if (!$lockFp) throw new Exception("No se pudo abrir lock: {$lockFile}");
if (!flock($lockFp, LOCK_EX)) throw new Exception("No se pudo bloquear lock: {$lockFile}");
// Cookie file limpio (sin helpers)
@unlink($cookieFile);
@touch($cookieFile);
@chmod($cookieFile, 0666);
clearstatcache(true, $cookieFile);
// 1) LOGIN
$postFields = http_build_query([
'email' => $email,
'password' => $password,
'submitLogin' => '1',
]);
$ch = curl_init();
// Log opcional por si vuelve a fallar
$verboseFp = @fopen($tmpDir . DIRECTORY_SEPARATOR . 'matacanaria_curl_verbose.log', 'ab');
curl_setopt_array($ch, [
CURLOPT_URL => $loginUrl,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
// cookies en memoria + persistencia en fichero (debug)
CURLOPT_COOKIEFILE => '', // activa engine cookies en memoria
CURLOPT_COOKIEJAR => $cookieFile,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'Mozilla/5.0 (CI cURL Matacanaria)',
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_TIMEOUT => 60,
CURLOPT_VERBOSE => $verboseFp ? true : false,
CURLOPT_STDERR => $verboseFp ?: null,
]);
$loginBody = curl_exec($ch);
if ($loginBody === false) {
throw new Exception("Login falló (cURL): " . curl_error($ch));
}
$loginHttp = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($loginHttp < 200 || $loginHttp >= 400) {
throw new Exception("Login falló (HTTP {$loginHttp})");
}
// ✅ Validación robusta: cookies en memoria (no filesize)
$cookieList = curl_getinfo($ch, CURLINFO_COOKIELIST);
$hasCookies = is_array($cookieList) && count($cookieList) > 0;
// Fallback: contenido del cookieFile
clearstatcache(true, $cookieFile);
$cookieTxt = @file_get_contents($cookieFile);
$cookieLen = strlen((string)$cookieTxt);
if (!$hasCookies && $cookieLen === 0) {
throw new Exception("Login sin cookies (no se generó sesión).");
}
// 2) DESCARGAR CSV (mismo $ch => misma sesión)
$fpOut = fopen($outFile, 'wb');
if ($fpOut === false) {
throw new Exception("No se pudo crear el fichero de salida: {$outFile}");
}
curl_setopt_array($ch, [
CURLOPT_URL => $catalogUrl,
CURLOPT_HTTPGET => true,
CURLOPT_POST => false,
CURLOPT_POSTFIELDS => null,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FILE => $fpOut,
CURLOPT_TIMEOUT => 120,
// Por si el endpoint mira la cabecera Accept:
CURLOPT_HTTPHEADER => ['Accept: text/csv,*/*;q=0.8'],
]);
$ok = curl_exec($ch);
if ($ok === false) {
throw new Exception("Descarga CSV falló (cURL): " . curl_error($ch));
}
$http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
fflush($fpOut);
fclose($fpOut);
$fpOut = null;
if ($http < 200 || $http >= 400) {
@unlink($outFile);
throw new Exception("Descarga CSV falló (HTTP {$http})");
}
// Check anti-HTML (si te devuelve login)
clearstatcache(true, $outFile);
$head = $this->readFirstBytes($outFile, 1024);
if (stripos($head, '<html') !== false || stripos($head, 'iniciar sesión') !== false || stripos($head, 'login') !== false) {
@unlink($outFile);
throw new Exception("Devolvió HTML (probable no autenticado). Content-Type: {$contentType}");
}
// Tamaño robusto
clearstatcache(true, $outFile);
$outSize = @filesize($outFile);
if (!$outSize || $outSize <= 0) {
$outSize = strlen((string)@file_get_contents($outFile));
}
// 3) SERVIR CSV AL NAVEGADOR
$filename = 'catalog_matacanaria_' . date('Ymd_His') . '.csv';
header('Content-Description: File Transfer');
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Expires: 0');
if ($outSize > 0) {
header('Content-Length: ' . $outSize);
}
while (ob_get_level()) {
@ob_end_clean();
}
readfile($outFile);
exit;
} catch (Exception $e) {
while (ob_get_level()) {
@ob_end_clean();
}
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
exit;
} finally {
if (is_resource($fpOut)) @fclose($fpOut);
if (is_resource($ch)) @curl_close($ch);
if (is_resource($verboseFp)) @fclose($verboseFp);
if (is_resource($lockFp)) {
@flock($lockFp, LOCK_UN);
@fclose($lockFp);
}
// Si quieres conservar cookie para inspección, comenta esto:
// @unlink($cookieFile);
if (isset($outFile) && is_file($outFile)) {
register_shutdown_function(function() use ($outFile) {
@unlink($outFile);
});
}
}
}
private function readFirstBytes($file, $bytes = 512)
{
$fh = @fopen($file, 'rb');
if (!$fh) return '';
$data = fread($fh, $bytes);
fclose($fh);
return $data !== false ? $data : '';
}
}