/* ============================================================ CONEXIÓN CON ERRMODE_EXCEPTION (reemplaza la función original al final del archivo) ============================================================ */ /* ============================================================ SUBIDA SEGURA DE ARCHIVOS ============================================================ */ function subirArchivoSeguro($archivo, $carpeta_destino, $extensionesPermitidas, $mimesPermitidos, $tamanoMaximo, $validarComoImagen = false){ if (!isset($archivo['error']) || is_array($archivo['error'])) { return ['ok' => false, 'nombre' => null, 'error' => 'Parámetros de subida inválidos.']; } if ($archivo['error'] !== UPLOAD_ERR_OK) { return ['ok' => false, 'nombre' => null, 'error' => 'Error al subir el archivo (código ' . $archivo['error'] . ').']; } if ($archivo['size'] > $tamanoMaximo) { $mb = round($tamanoMaximo / 1024 / 1024, 1); return ['ok' => false, 'nombre' => null, 'error' => "El archivo supera el tamaño máximo permitido ({$mb} MB)."]; } $extension = strtolower(pathinfo($archivo['name'], PATHINFO_EXTENSION)); if (!in_array($extension, $extensionesPermitidas, true)) { return ['ok' => false, 'nombre' => null, 'error' => 'Extensión no permitida. Solo: ' . implode(', ', $extensionesPermitidas) . '.']; } $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeReal = finfo_file($finfo, $archivo['tmp_name']); finfo_close($finfo); if (!in_array($mimeReal, $mimesPermitidos, true)) { return ['ok' => false, 'nombre' => null, 'error' => 'El contenido del archivo no corresponde a un tipo permitido.']; } if ($validarComoImagen && @getimagesize($archivo['tmp_name']) === false) { return ['ok' => false, 'nombre' => null, 'error' => 'El archivo no es una imagen válida o está dañado.']; } if (!is_dir($carpeta_destino)) { @mkdir($carpeta_destino, 0755, true); } $nombreUnico = bin2hex(random_bytes(8)) . '_' . time() . '.' . $extension; $rutaDestino = rtrim($carpeta_destino, '/') . '/' . $nombreUnico; if (!move_uploaded_file($archivo['tmp_name'], $rutaDestino)) { return ['ok' => false, 'nombre' => null, 'error' => 'No se pudo guardar el archivo en el servidor.']; } @chmod($rutaDestino, 0644); return ['ok' => true, 'nombre' => $nombreUnico, 'error' => null]; } function subirImagenSegura($archivo, $carpeta_destino){ return subirArchivoSeguro($archivo, $carpeta_destino, ['jpg','jpeg','png','gif','webp'], ['image/jpeg','image/png','image/gif','image/webp'], 5 * 1024 * 1024, true); } function subirPdfSeguro($archivo, $carpeta_destino){ return subirArchivoSeguro($archivo, $carpeta_destino, ['pdf'], ['application/pdf'], 100 * 1024 * 1024, false); } function subirVideoSeguro($archivo, $carpeta_destino){ return subirArchivoSeguro($archivo, $carpeta_destino, ['mp4','webm','ogg','mov'], ['video/mp4','video/webm','video/ogg','video/quicktime'], 100 * 1024 * 1024, false); } function subirMediaExtra($archivo, $carpeta_destino){ $extension = strtolower(pathinfo($archivo['name'], PATHINFO_EXTENSION)); if (in_array($extension, ['jpg','jpeg','png','gif','webp'], true)) { $r = subirImagenSegura($archivo, $carpeta_destino); $r['tipo'] = 'imagen'; return $r; } if (in_array($extension, ['mp4','webm','ogg','mov'], true)) { $r = subirVideoSeguro($archivo, $carpeta_destino); $r['tipo'] = 'video'; return $r; } return ['ok' => false, 'nombre' => null, 'error' => 'Formato no permitido. Usa imagen (jpg, png, gif, webp) o video (mp4, webm, ogg, mov).', 'tipo' => null]; } /* ============================================================ BANNERS UNIFICADOS ============================================================ */ function posiciones_banners(){ return [ 'principal' => 'Banner principal', 'superior' => 'Banner superior', 'inferior' => 'Banner inferior', 'lateral3' => 'Banner lateral 3', 'lateral4' => 'Banner lateral 4', 'lateral5' => 'Banner lateral 5', 'lateral6' => 'Banner lateral 6', ]; } function obtener_banner_posicion($conexion, $posicion){ $sentencia = $conexion->prepare("SELECT * FROM banners_unificados WHERE posicion = :posicion AND visible = 1 ORDER BY id_banner DESC LIMIT 1"); $sentencia->execute([':posicion' => $posicion]); return $sentencia->fetchAll(); } function obtener_banners_todos_posicion($conexion, $posicion){ $sentencia = $conexion->prepare("SELECT * FROM banners_unificados WHERE posicion = :posicion ORDER BY id_banner DESC"); $sentencia->execute([':posicion' => $posicion]); return $sentencia->fetchAll(); } function obtener_banner_por_id($conexion, $id, $posicion){ $sentencia = $conexion->prepare("SELECT * FROM banners_unificados WHERE id_banner = :id AND posicion = :posicion LIMIT 1"); $sentencia->execute([':id' => $id, ':posicion' => $posicion]); return $sentencia->fetch(); } /* ============================================================ HASHING SEGURO DE CONTRASEÑAS ============================================================ */ function hashPassword($passwordPlano){ return password_hash($passwordPlano, PASSWORD_DEFAULT); } function verificarPassword($passwordPlano, $hashGuardado){ $esHashLegado = (bool) preg_match('/^[a-f0-9]{128}$/i', $hashGuardado); if ($esHashLegado) { $valido = hash_equals($hashGuardado, hash('sha512', $passwordPlano)); return ['valido' => $valido, 'necesita_migracion' => $valido]; } return [ 'valido' => password_verify($passwordPlano, $hashGuardado), 'necesita_migracion' => password_needs_rehash($hashGuardado, PASSWORD_DEFAULT), ]; } function actualizarPasswordUsuario($conexion, $id_usuario, $nuevoHash){ $sentencia = $conexion->prepare('UPDATE usuarios SET password = :password WHERE id_usuario = :id'); $sentencia->execute([':password' => $nuevoHash, ':id' => $id_usuario]); } /* ============================================================ REGISTRO DE VISITAS ============================================================ */ function obtenerGeolocalizacionIp($ip){ $resultado = ['pais' => 'Desconocido', 'estado' => 'Desconocido', 'ciudad' => 'Desconocido']; if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $resultado; } if (!function_exists('curl_init')) return $resultado; $ch = curl_init("http://ip-api.com/json/{$ip}?fields=status,country,regionName,city"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 2); $respuesta = curl_exec($ch); $error = curl_errno($ch); curl_close($ch); if ($error || !$respuesta) return $resultado; $datos = json_decode($respuesta, true); if (!is_array($datos) || ($datos['status'] ?? '') !== 'success') return $resultado; $resultado['pais'] = $datos['country'] ?? 'Desconocido'; $resultado['estado'] = $datos['regionName'] ?? 'Desconocido'; $resultado['ciudad'] = $datos['city'] ?? 'Desconocido'; return $resultado; } function registrarVisitaArticulo($conexion, $id_articulo, $ip){ try { $s = $conexion->prepare('INSERT INTO visitas_log (id_articulo, ip) VALUES (:id_articulo, :ip)'); $s->execute([':id_articulo' => $id_articulo, ':ip' => $ip]); } catch (Exception $e) {} $s = $conexion->prepare('SELECT id FROM articulos_visitas WHERE id_articulo = :id AND ip = :ip LIMIT 1'); $s->execute([':id' => $id_articulo, ':ip' => $ip]); $existe = $s->fetch(); if ($existe) { $s = $conexion->prepare('UPDATE articulos_visitas SET visitas = visitas + 1 WHERE id = :id'); $s->execute([':id' => $existe['id']]); return; } $geo = obtenerGeolocalizacionIp($ip); $s = $conexion->prepare('INSERT INTO articulos_visitas (id_articulo, ip, pais, estado, ciudad, visitas) VALUES (:id, :ip, :pais, :estado, :ciudad, 1)'); $s->execute([':id' => $id_articulo, ':ip' => $ip, ':pais' => $geo['pais'], ':estado' => $geo['estado'], ':ciudad' => $geo['ciudad']]); } function obtener_visitas_rango($conexion, $fecha_inicio, $fecha_fin, $id_articulo = null){ if ($id_articulo) { $s = $conexion->prepare('SELECT COUNT(*) AS total FROM visitas_log WHERE id_articulo = :id AND fecha_hora BETWEEN :inicio AND :fin'); $s->execute([':id' => $id_articulo, ':inicio' => $fecha_inicio, ':fin' => $fecha_fin]); } else { $s = $conexion->prepare('SELECT COUNT(*) AS total FROM visitas_log WHERE fecha_hora BETWEEN :inicio AND :fin'); $s->execute([':inicio' => $fecha_inicio, ':fin' => $fecha_fin]); } $res = $s->fetch(); return $res ? (int)$res['total'] : 0; } function obtener_top_articulos_rango($conexion, $fecha_inicio, $fecha_fin, $limite = 10){ $limite = (int)$limite; $s = $conexion->prepare("SELECT v.id_articulo, a.titulo, COUNT(*) AS total FROM visitas_log v LEFT JOIN articulos a ON a.id_articulo = v.id_articulo WHERE v.fecha_hora BETWEEN :inicio AND :fin GROUP BY v.id_articulo ORDER BY total DESC LIMIT $limite"); $s->execute([':inicio' => $fecha_inicio, ':fin' => $fecha_fin]); return $s->fetchAll(); }