Files
whatsapp/config/config.php
T
Lizandro GuarnizoandClaude Opus 5 018fb13332 Módulo Soporte: visor de documentación con renderizado Markdown y buscador
Nuevo módulo `soporte` con la documentación del proyecto en cuatro secciones:
manual de usuario (visible para todos), y documentación técnica, arquitectura
y operación (solo administradores).

- Markdown.php: renderizador propio del subconjunto que usa la documentación
  (encabezados, listas anidadas, tablas, código, citas). Escapa todo el texto
  antes de aplicar formato, así que los .md no pueden inyectar HTML. Se
  prefirió un archivo auditable a incorporar una dependencia externa.
- DocIndex.php: descubre los .md, arma el árbol, resuelve acceso por sección
  y construye el índice del buscador.
- Generadores.php: expande marcadores {{modulos}}, {{endpoints}}, {{tablas}},
  {{roles}} y {{servicios}} leyendo el código y la base en cada carga, para
  que los inventarios no puedan quedar desactualizados.

Se registra en SYSTEM_MODULES y se concede a los 12 roles con permission=read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:07:06 -05:00

1065 lines
37 KiB
PHP

<?php
/**
* Configuración del sistema WhatsApp Bot Manager
* Desarrollado por U-Site.app
* Fecha: 4 de enero de 2026 - Sistema de autenticación unificado
*/
// ── Cargar .env ANTES de definir cualquier constante ──────────────────────────
// Si no se hace aquí, getenv() devuelve vacío y los define() usan el fallback
// 'mysql' (nombre del contenedor Docker) en lugar de la IP real de la BD.
if (!function_exists('loadEnvFile')) {
function loadEnvFile($path) {
if (!file_exists($path)) {
return false;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos($line, '#') === 0 || strpos($line, '=') === false) {
continue;
}
[$name, $value] = explode('=', $line, 2);
$name = trim($name);
$value = trim($value, " \t\n\r\0\x0B\"'");
if (!array_key_exists($name, $_ENV)) {
$_ENV[$name] = $value;
putenv("$name=$value");
}
}
return true;
}
}
loadEnvFile(__DIR__ . '/../.env');
// Configuración de la base de datos - 100% desde variables de entorno (o .env)
define('DB_HOST', getenv('DB_HOST') ?: 'mysql');
define('DB_PORT', getenv('DB_PORT') ?: '3306');
define('DB_NAME', getenv('DB_NAME') ?: 'usite_whatsapp_bot');
define('DB_USER', getenv('DB_USER') ?: 'usite_whatsapp_user');
define('DB_PASS', getenv('DB_PASS') ?: '');
define('DB_CHARSET', 'utf8mb4');
// Configuración de autenticación (LEGACY - migrado a BD)
// NOTA: Esta constante se mantiene para retrocompatibilidad durante la migración
// El sistema ahora usa autenticación basada en base de datos (admin_users table)
define('ADMIN_PASSWORD', '$2y$10$IXCY8Sm1xFkfhC6Y67Ahn.QLHxE.sjWfmTEOKFZdCN2a9s9YLVuGW'); // password123
// ── Módulos del sistema ──────────────────────────────────────────────────────
// Catálogo de todos los módulos disponibles para asignar a los roles.
// Clave: slug interno · Valor: etiqueta legible para la UI.
define('SYSTEM_MODULES', [
// ── Bot ──────────────────────────────────────────────────────────────────
'whatsapp' => 'WhatsApp Bot',
// ── Laboratorio (Oleada 0) ───────────────────────────────────────────────
'lab_dashboard' => 'Dashboard Lab',
'lab_ordenes' => 'Órdenes Médicas',
'lab_pacientes' => 'Pacientes',
'lab_domicilios' => 'Domicilios',
'lab_enfermeras' => 'Enfermeras',
'lab_formularios' => 'Formularios',
'lab_reportes' => 'Reportes',
'lab_configuracion' => 'Configuración Lab',
// ── Sistema ──────────────────────────────────────────────────────────────
'usuarios' => 'Gestión de Usuarios',
'enfermero_portal' => 'Portal Enfermero',
'soporte' => 'Soporte y Documentación',
// ── Oleada 1 — Turnero ───────────────────────────────────────────────────
'turnero' => 'Turnero',
// ── Oleada 2 — pendiente ─────────────────────────────────────────────────
'registro_exams' => 'Registro de Exámenes',
'facturacion' => 'Facturación',
'inventario' => 'Inventario',
'citas' => 'Citas',
'resultados' => 'Resultados',
]);
// loadEnvFile ya fue definida y ejecutada al inicio del archivo.
// Cargar autoloader de Composer si existe (solo una vez)
$composerAutoload = __DIR__ . '/../vendor/autoload.php';
if (file_exists($composerAutoload)) {
// Verificar que no se haya cargado ya usando una clase del autoloader
if (!class_exists('Predis\Client', false)) {
require_once $composerAutoload;
}
}
// Función helper para obtener variables de entorno
if (!function_exists('env')) {
function env($key, $default = null) {
return $_ENV[$key] ?? getenv($key) ?: $default;
}
}
// Incluir clase Database
require_once __DIR__ . '/../classes/Database.php';
// Función para verificar si la instalación está completada
if (!function_exists('isInstallationCompleted')) {
function isInstallationCompleted() {
return file_exists(dirname(__DIR__) . '/.installation_completed');
}
}
// Función para obtener configuración desde base de datos
if (!function_exists('getConfigFromDB')) {
function getConfigFromDB($key, $default = null) {
static $configs = null;
global $config_cache_cleared;
// Asegurar que la bandera exista y tenga valor booleano
if (!isset($config_cache_cleared)) {
$config_cache_cleared = false;
}
if ($configs === null || $config_cache_cleared) {
$config_cache_cleared = false;
$configs = [];
if (isInstallationCompleted()) {
try {
// Usar la función centralizada para crear la conexión
$pdo = createDbConnection();
// Verificar si existe la tabla system_config
$tableExistsStmt = $pdo->query("SHOW TABLES LIKE 'system_config'");
$tableExists = $tableExistsStmt && $tableExistsStmt->rowCount() > 0;
if ($tableExists) {
// Cargar todas las configuraciones en caché de una sola vez
$stmt = $pdo->query("SELECT config_key, config_value FROM system_config");
while ($row = $stmt->fetch()) {
$configs[$row['config_key']] = $row['config_value'];
}
} else {
// Si no existe la tabla, usar valores por defecto
$configs = [];
}
} catch (PDOException $e) {
error_log("Error loading config: " . $e->getMessage());
$configs = [];
}
} else {
$configs = [];
}
}
return $configs[$key] ?? $default;
}
} // end if !function_exists('getConfigFromDB')
/**
* Guarda una configuración en la base de datos
* @param string $key Clave de configuración
* @param mixed $value Valor de configuración
* @param string $description Descripción opcional
* @return bool True si se guardó correctamente
*/
function saveConfigToDB($key, $value, $description = null) {
try {
if (!isInstallationCompleted()) {
return false;
}
$pdo = createDbConnection();
// Crear tabla si no existe
createSystemConfigTable($pdo);
// Verificar si ya existe la configuración
$stmt = $pdo->prepare("SELECT id FROM system_config WHERE config_key = ?");
$stmt->execute([$key]);
$exists = $stmt->fetch();
if ($exists) {
// Actualizar configuración existente
$sql = "UPDATE system_config SET config_value = ?, updated_at = NOW()";
$params = [$value];
if ($description !== null) {
$sql .= ", description = ?";
$params[] = $description;
}
$sql .= " WHERE config_key = ?";
$params[] = $key;
$stmt = $pdo->prepare($sql);
$result = $stmt->execute($params);
} else {
// Insertar nueva configuración
$stmt = $pdo->prepare("
INSERT INTO system_config (config_key, config_value, description, created_at, updated_at)
VALUES (?, ?, ?, NOW(), NOW())
");
$result = $stmt->execute([$key, $value, $description]);
}
// Limpiar cache estático
if ($result) {
clearConfigCache();
}
return $result;
} catch (PDOException $e) {
error_log("Error guardando configuración '$key': " . $e->getMessage());
return false;
}
}
/**
* Guarda múltiples configuraciones en una sola transacción
* @param array $configs Array asociativo [key => value]
* @return bool True si todas se guardaron correctamente
*/
function saveMultipleConfigsToDB($configs) {
try {
if (!isInstallationCompleted() || empty($configs)) {
return false;
}
$pdo = createDbConnection();
$pdo->beginTransaction();
// Crear tabla si no existe
createSystemConfigTable($pdo);
$success = true;
foreach ($configs as $key => $value) {
// Verificar si ya existe
$stmt = $pdo->prepare("SELECT id FROM system_config WHERE config_key = ?");
$stmt->execute([$key]);
$exists = $stmt->fetch();
if ($exists) {
// Actualizar
$stmt = $pdo->prepare("UPDATE system_config SET config_value = ?, updated_at = NOW() WHERE config_key = ?");
$result = $stmt->execute([$value, $key]);
} else {
// Insertar
$stmt = $pdo->prepare("
INSERT INTO system_config (config_key, config_value, created_at, updated_at)
VALUES (?, ?, NOW(), NOW())
");
$result = $stmt->execute([$key, $value]);
}
if (!$result) {
$success = false;
break;
}
}
if ($success) {
$pdo->commit();
clearConfigCache();
return true;
} else {
$pdo->rollback();
return false;
}
} catch (PDOException $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollback();
}
error_log("Error guardando configuraciones múltiples: " . $e->getMessage());
return false;
}
}
/**
* Obtiene todas las configuraciones de la base de datos
* @return array Array asociativo con todas las configuraciones
*/
function getAllConfigsFromDB() {
try {
if (!isInstallationCompleted()) {
return [];
}
$pdo = createDbConnection();
// Verificar si existe la tabla
$tableExists = $pdo->query("SHOW TABLES LIKE 'system_config'")->rowCount() > 0;
if (!$tableExists) {
return [];
}
$stmt = $pdo->query("
SELECT config_key, config_value, description, created_at, updated_at
FROM system_config
ORDER BY config_key
");
$configs = [];
while ($row = $stmt->fetch()) {
$configs[$row['config_key']] = [
'value' => $row['config_value'],
'description' => $row['description'],
'created_at' => $row['created_at'],
'updated_at' => $row['updated_at']
];
}
return $configs;
} catch (PDOException $e) {
error_log("Error obteniendo todas las configuraciones: " . $e->getMessage());
return [];
}
}
/**
* Elimina una configuración de la base de datos
* @param string $key Clave de configuración a eliminar
* @return bool True si se eliminó correctamente
*/
function deleteConfigFromDB($key) {
try {
if (!isInstallationCompleted()) {
return false;
}
$pdo = createDbConnection();
$stmt = $pdo->prepare("DELETE FROM system_config WHERE config_key = ?");
$result = $stmt->execute([$key]);
if ($result) {
clearConfigCache();
}
return $result;
} catch (PDOException $e) {
error_log("Error eliminando configuración '$key': " . $e->getMessage());
return false;
}
}
/**
* Limpia el cache estático de configuraciones
*/
function clearConfigCache() {
// Necesitamos acceder a la variable estática de getConfigFromDB
// Como no podemos modificar directamente variables estáticas de otra función,
// establecemos una flag para que getConfigFromDB reinicie el cache
global $config_cache_cleared;
$config_cache_cleared = true;
}
/**
* Crea la tabla system_config si no existe
* @param PDO $pdo Conexión a la base de datos
* @return bool True si se creó o ya existía
*/
function createSystemConfigTable($pdo) {
try {
$sql = "CREATE TABLE IF NOT EXISTS system_config (
id INT AUTO_INCREMENT PRIMARY KEY,
config_key VARCHAR(255) NOT NULL UNIQUE,
config_value TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_config_key (config_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
return $pdo->exec($sql) !== false;
} catch (PDOException $e) {
error_log("Error creando tabla system_config: " . $e->getMessage());
return false;
}
}
// WhatsApp Business API (prioridad: BD > .env > default)
define('WHATSAPP_TOKEN', getConfigFromDB('whatsapp_token', env('WHATSAPP_TOKEN', 'TU_TOKEN_DE_WHATSAPP_AQUI')));
define('WHATSAPP_PHONE_NUMBER_ID', getConfigFromDB('whatsapp_phone_number_id', env('WHATSAPP_PHONE_NUMBER_ID', 'TU_PHONE_ID_AQUI')));
define('WHATSAPP_API_URL', getConfigFromDB('whatsapp_api_url', env('WHATSAPP_API_URL', 'https://graph.facebook.com/v22.0/')));
define('WEBHOOK_VERIFY_TOKEN', getConfigFromDB('webhook_verify_token', env('WEBHOOK_VERIFY_TOKEN', 'mi_token_secreto_123')));
// Configuración general
define('APP_NAME', 'WhatsApp Bot System');
define('APP_VERSION', '2.0.0');
// URL auto-detectada según protocolo y host actuales
if (!defined('APP_URL')) {
// Detectar HTTPS correctamente detrás de proxy reverso (Coolify, nginx, etc.)
$__isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'
|| ($_SERVER['HTTP_X_FORWARDED_SSL'] ?? '') === 'on'
|| ($_SERVER['HTTP_FRONT_END_HTTPS'] ?? '') === 'on';
$__proto = $__isHttps ? 'https' : 'http';
$__host = $_SERVER['HTTP_HOST'] ?? 'localhost';
define('APP_URL', $__proto . '://' . $__host);
}
// BASE_URL: alias de APP_URL con barra final (usado en vistas de módulos ERP)
if (!defined('BASE_URL')) {
define('BASE_URL', rtrim(APP_URL, '/') . '/');
}
// Token para runners de migración accesibles por web
if (!defined('MIGRATION_TOKEN')) {
define('MIGRATION_TOKEN', 'lab2026migrate');
}
// API key server-to-server para ingesta de pacientes desde RIPS Manager
if (!defined('LAB_SYNC_KEY')) {
define('LAB_SYNC_KEY', getenv('LAB_SYNC_KEY') ?: 'rips-lab-sync-2026');
}
// URL base del RIPS Manager (para consultas server-to-server)
if (!defined('RIPS_MANAGER_URL')) {
define('RIPS_MANAGER_URL', getenv('RIPS_MANAGER_URL') ?: '');
}
define('TIMEZONE', 'America/Bogota');
// Información del desarrollador
define('DEVELOPER_NAME', 'U-Site.app');
define('DEVELOPER_URL', 'https://u-site.app');
define('DEVELOPER_EMAIL', 'support@u-site.app');
define('DEVELOPER_SUPPORT', 'https://u-site.app/support');
// Configuración de seguridad
define('INSTALLATION_LOCK', '.installation_completed'); // Archivo de bloqueo
define('SECRET_KEY', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
define('ADMIN_USERNAME', 'admin'); // Usuario administrador por defecto
// ADMIN_PASSWORD movido arriba como parte del sistema unificado (línea 18)
define('SESSION_TIMEOUT', (int)(getenv('SESSION_TIMEOUT') ?: 1800)); // 30 minutos
define('MAX_LOGIN_ATTEMPTS', (int)(getenv('MAX_LOGIN_ATTEMPTS') ?: 3));
define('LOGIN_LOCKOUT_TIME', (int)(getenv('LOGIN_LOCKOUT_TIME') ?: 900)); // 15 minutos
// Configuración de logs (LOG_LEVEL sobreescribible desde env)
define('ENABLE_LOGGING', true);
define('LOG_LEVEL', getenv('LOG_LEVEL') ?: 'INFO'); // DEBUG, INFO, WARNING, ERROR
define('LOG_FILE', 'logs/system.log');
// Configuración de archivos
define('UPLOAD_MAX_SIZE', 10485760); // 10MB
define('ALLOWED_FILE_TYPES', ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx']);
// Configurar zona horaria
date_default_timezone_set(TIMEZONE);
// Configurar errores para desarrollo (se desactivará en producción)
if (!file_exists(dirname(__DIR__) . '/' . 'installation_completed')) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
ini_set('log_errors', 1);
// Headers de seguridad
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
// Auto-detectar URL base si no está configurada
if (!defined('AUTO_DETECTED_URL')) {
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$path = dirname($_SERVER['SCRIPT_NAME'] ?? '');
define('AUTO_DETECTED_URL', $protocol . $host . $path);
}
// ========================================
// SISTEMA DE AUTENTICACIÓN UNIFICADO
// ========================================
/**
* Crea conexión a la base de datos
* @return PDO
*/
function createDbConnection() {
try {
$pdo = new PDO(
"mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET,
DB_USER,
DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
return $pdo;
} catch (PDOException $e) {
error_log("Error de conexión BD: " . $e->getMessage());
throw $e;
}
}
/**
* Autentica un usuario usando la base de datos
* @param string $username
* @param string $password
* @return array|false Datos del usuario o false si falla
*/
function authenticateUser($username, $password) {
try {
$pdo = createDbConnection();
// Primero verificar si existe la tabla admin_users
$checkTable = $pdo->query("SHOW TABLES LIKE 'admin_users'");
$tableExists = $checkTable->rowCount() > 0;
if ($tableExists) {
// Sistema moderno: autenticación por BD
$stmt = $pdo->prepare("SELECT * FROM admin_users WHERE username = ? AND is_active = 1");
$stmt->execute([$username]);
$admin = $stmt->fetch();
if ($admin && password_verify($password, $admin['password_hash'])) {
// Actualizar último login
$updateLogin = $pdo->prepare("UPDATE admin_users SET last_login = NOW() WHERE id = ?");
$updateLogin->execute([$admin['id']]);
// Cargar módulos del rol (slug y permiso)
$modules = [];
$modulePermissions = [];
$roleSlug = $admin['role'] ?? 'admin';
$homePage = null;
if (!empty($admin['role_id'])) {
$modStmt = $pdo->prepare(
"SELECT module_slug, COALESCE(permission,'write') AS permission
FROM role_modules WHERE role_id = ?"
);
$modStmt->execute([$admin['role_id']]);
foreach ($modStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$modules[] = $row['module_slug'];
$modulePermissions[$row['module_slug']] = $row['permission'];
}
// Leer home_page del rol
$rpStmt = $pdo->prepare("SELECT home_page FROM roles WHERE id = ?");
$rpStmt->execute([$admin['role_id']]);
$rp = $rpStmt->fetch(PDO::FETCH_ASSOC);
$homePage = $rp['home_page'] ?? null;
} elseif ($roleSlug === 'admin') {
// Fallback: admin sin role_id tiene todos los módulos
$modules = array_keys(SYSTEM_MODULES);
} elseif ($roleSlug === 'enfermero') {
$modules = ['enfermero_portal', 'lab_formularios'];
}
return [
'id' => $admin['id'],
'username' => $admin['username'],
'full_name' => $admin['full_name'],
'email' => $admin['email'],
'role' => $roleSlug,
'role_id' => $admin['role_id'] ?? null,
'home_page' => $homePage,
'enfermera_id' => $admin['enfermera_id'] ?? null,
'turnero_lugar_id' => $admin['turnero_lugar_id'] ?? null,
'modules' => $modules,
'module_permissions' => $modulePermissions,
];
}
} else {
// Sistema legacy: usar constante ADMIN_PASSWORD
if ($username === 'admin' && password_verify($password, ADMIN_PASSWORD)) {
return [
'id' => 1,
'username' => 'admin',
'full_name' => 'Administrador',
'email' => 'admin@sistema.local'
];
}
}
} catch (Exception $e) {
error_log("Error en autenticación: " . $e->getMessage());
return false;
}
return false;
}
/**
* Verifica si el sistema está migrado a BD
* @return bool
*/
function isAuthSystemMigrated() {
try {
$pdo = createDbConnection();
$checkTable = $pdo->query("SHOW TABLES LIKE 'admin_users'");
return $checkTable->rowCount() > 0;
} catch (Exception $e) {
return false;
}
}
/**
* Obtiene todos los administradores del sistema
* @return array
*/
function getAdminUsers() {
try {
$pdo = createDbConnection();
if (isAuthSystemMigrated()) {
$stmt = $pdo->query("SELECT * FROM admin_users ORDER BY created_at DESC");
return $stmt->fetchAll();
} else {
// Sistema legacy: retornar admin por defecto
return [[
'id' => 1,
'username' => 'admin',
'full_name' => 'Administrador',
'email' => 'admin@sistema.local',
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'last_login' => null
]];
}
} catch (Exception $e) {
error_log("Error obteniendo usuarios admin: " . $e->getMessage());
return [];
}
}
// Función para verificar login
if (!function_exists('isUserLoggedIn')) {
/**
* Verifica si el usuario autenticado tiene el rol indicado.
*/
function userHasRole(string $role): bool {
return isset($_SESSION['admin_user']['role']) && $_SESSION['admin_user']['role'] === $role;
}
/**
* Verdadero si el usuario es enfermero.
*/
function isEnfermero(): bool {
return userHasRole('enfermero');
}
/**
* Devuelve el ID de enfermera del usuario en sesión, o null si no aplica.
*/
function enfermeraId(): ?int {
$id = $_SESSION['admin_user']['enfermera_id'] ?? null;
return $id ? (int)$id : null;
}
/**
* Comprueba si el usuario en sesión tiene acceso al módulo indicado.
* Los administradores sin modules cargados tienen acceso total (retrocompat.).
*/
function hasModule(string $slug): bool {
$modules = $_SESSION['admin_user']['modules'] ?? null;
// Si no hay módulos en sesión (usuario legacy) y es admin → acceso total
if ($modules === null) {
return userHasRole('admin');
}
return in_array($slug, $modules, true);
}
/**
* Comprueba si el usuario tiene permiso de escritura (write) en el módulo.
* Los administradores tienen acceso de escritura total.
* Un usuario con permission='read' solo puede ver, no modificar.
*/
function hasModuleWrite(string $slug): bool {
// Admins siempre pueden escribir
if (userHasRole('admin')) return true;
$permissions = $_SESSION['admin_user']['module_permissions'] ?? null;
if ($permissions === null) {
// Sesión legacy sin 'module_permissions': si tiene el módulo, se asume write
return hasModule($slug);
}
return ($permissions[$slug] ?? '') === 'write';
}
/**
* Detiene la ejecución si el usuario no tiene el rol requerido.
*/
function requireRole(string $role): void {
if (!isUserLoggedIn()) {
header('Location: login.php');
exit;
}
if (!userHasRole($role)) {
http_response_code(403);
// Redirigir al portal correcto
$destino = userHasRole('enfermero') ? 'enfermero_portal.php' : 'index.php';
header("Location: $destino");
exit;
}
}
function isUserLoggedIn() {
return isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true;
}
}
// Función para verificar intentos de login
if (!function_exists('checkLoginAttempts')) {
function checkLoginAttempts($ip) {
$attemptsFile = dirname(__DIR__) . '/.login_attempts.json';
if (!file_exists($attemptsFile)) {
return true;
}
$attempts = json_decode(file_get_contents($attemptsFile), true);
if (!isset($attempts[$ip])) {
return true;
}
$ipData = $attempts[$ip];
if (time() > $ipData['locked_until']) {
unset($attempts[$ip]);
file_put_contents($attemptsFile, json_encode($attempts));
return true;
}
return $ipData['attempts'] < MAX_LOGIN_ATTEMPTS;
}
}
// Función para registrar intento de login fallido
if (!function_exists('recordFailedLogin')) {
function recordFailedLogin($ip) {
$attemptsFile = dirname(__DIR__) . '/.login_attempts.json';
$attempts = [];
if (file_exists($attemptsFile)) {
$attempts = json_decode(file_get_contents($attemptsFile), true);
}
if (!isset($attempts[$ip])) {
$attempts[$ip] = ['attempts' => 0, 'locked_until' => 0];
}
$attempts[$ip]['attempts']++;
if ($attempts[$ip]['attempts'] >= MAX_LOGIN_ATTEMPTS) {
$attempts[$ip]['locked_until'] = time() + LOGIN_LOCKOUT_TIME;
}
file_put_contents($attemptsFile, json_encode($attempts));
}
}
// Función para limpiar intentos de login exitoso
if (!function_exists('clearLoginAttempts')) {
function clearLoginAttempts($ip) {
$attemptsFile = dirname(__DIR__) . '/.login_attempts.json';
if (file_exists($attemptsFile)) {
$attempts = json_decode(file_get_contents($attemptsFile), true);
unset($attempts[$ip]);
file_put_contents($attemptsFile, json_encode($attempts));
}
}
}
// Autoloader
spl_autoload_register(function ($class) {
$directories = [
dirname(__DIR__) . '/classes/',
dirname(__DIR__) . '/services/',
dirname(__DIR__) . '/controllers/'
];
foreach ($directories as $directory) {
$file = $directory . $class . '.php';
if (file_exists($file)) {
require_once $file;
return;
}
}
});
/**
* Guarda la configuración completa de WhatsApp
* @param array $whatsappConfig Array con las configuraciones de WhatsApp
* @return bool True si se guardó correctamente
*/
function saveWhatsAppConfigToDB($whatsappConfig) {
$success = true;
// Mapear configuraciones de WhatsApp y guardar individualmente
if (isset($whatsappConfig['token'])) {
$success &= saveConfigToDB('whatsapp_token', $whatsappConfig['token']);
}
if (isset($whatsappConfig['phone_number_id'])) {
$success &= saveConfigToDB('whatsapp_phone_number_id', $whatsappConfig['phone_number_id']);
}
if (isset($whatsappConfig['business_account_id'])) {
$success &= saveConfigToDB('whatsapp_business_account_id', $whatsappConfig['business_account_id']);
}
if (isset($whatsappConfig['api_url'])) {
$success &= saveConfigToDB('whatsapp_api_url', $whatsappConfig['api_url']);
}
if (isset($whatsappConfig['webhook_verify_token'])) {
$success &= saveConfigToDB('webhook_verify_token', $whatsappConfig['webhook_verify_token']);
}
if (isset($whatsappConfig['business_name'])) {
$success &= saveConfigToDB('business_name', $whatsappConfig['business_name']);
}
if (isset($whatsappConfig['welcome_message'])) {
$success &= saveConfigToDB('welcome_message', $whatsappConfig['welcome_message']);
}
if (isset($whatsappConfig['terms_message'])) {
$success &= saveConfigToDB('terms_message', $whatsappConfig['terms_message']);
}
if (isset($whatsappConfig['terms_rejected_message'])) {
$success &= saveConfigToDB('terms_rejected_message', $whatsappConfig['terms_rejected_message']);
}
return $success;
}
/**
* Obtiene toda la configuración de WhatsApp desde BD
* @return array Configuración completa de WhatsApp
*/
function getWhatsAppConfigFromDB() {
// Usar constantes como fallback (ya tienen cascada BD > env > default)
$token = getConfigFromDB('whatsapp_token', '');
if (empty($token) && defined('WHATSAPP_TOKEN')) $token = WHATSAPP_TOKEN;
if (empty($token)) $token = getenv('WHATSAPP_TOKEN') ?: '';
$phoneId = getConfigFromDB('whatsapp_phone_number_id', '');
if (empty($phoneId) && defined('WHATSAPP_PHONE_NUMBER_ID')) $phoneId = WHATSAPP_PHONE_NUMBER_ID;
if (empty($phoneId)) $phoneId = getenv('WHATSAPP_PHONE_NUMBER_ID') ?: '';
$apiUrl = getConfigFromDB('whatsapp_api_url', '');
if (empty($apiUrl) && defined('WHATSAPP_API_URL')) $apiUrl = WHATSAPP_API_URL;
if (empty($apiUrl)) $apiUrl = 'https://graph.facebook.com/v22.0/';
$webhookToken = getConfigFromDB('webhook_verify_token', '');
if (empty($webhookToken) && defined('WEBHOOK_VERIFY_TOKEN')) $webhookToken = WEBHOOK_VERIFY_TOKEN;
return [
// Mapear a nombres esperados por el código
'token' => $token,
'whatsapp_token' => $token,
'phone_number_id' => $phoneId,
'business_account_id' => getConfigFromDB('whatsapp_business_account_id', ''),
'api_url' => $apiUrl,
'whatsapp_api_url' => $apiUrl,
'webhook_verify_token' => $webhookToken,
'business_name' => getConfigFromDB('business_name', ''),
'welcome_message' => getConfigFromDB('welcome_message', ''),
'terms_message' => getConfigFromDB('terms_message', ''),
'terms_rejected_message' => getConfigFromDB('terms_rejected_message', ''),
'status' => validateWhatsAppConfigFromDB()
];
}
/**
* Valida la configuración de WhatsApp desde BD
* @return array Estado de validación
*/
function validateWhatsAppConfigFromDB() {
$token = getConfigFromDB('whatsapp_token', '');
$phoneId = getConfigFromDB('whatsapp_phone_number_id', '');
$webhookToken = getConfigFromDB('webhook_verify_token', '');
$errors = [];
$warnings = [];
if (empty($token) || $token === 'TU_TOKEN_DE_WHATSAPP_AQUI') {
$errors[] = 'Token de WhatsApp no configurado';
}
if (empty($phoneId) || $phoneId === 'TU_PHONE_ID_AQUI') {
$errors[] = 'Phone Number ID no configurado';
} elseif ($phoneId === '858157464051987') {
$warnings[] = 'Phone Number ID problemático detectado (858157464051987)';
}
if (empty($webhookToken)) {
$errors[] = 'Token de verificación del webhook no configurado';
}
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings,
'configured_fields' => [
'token' => !empty($token) && $token !== 'TU_TOKEN_DE_WHATSAPP_AQUI',
'phone_id' => !empty($phoneId) && $phoneId !== 'TU_PHONE_ID_AQUI',
'webhook_token' => !empty($webhookToken)
]
];
}
/**
* Inicializa configuraciones por defecto en la base de datos
* @return bool True si se inicializaron correctamente
*/
function initializeDefaultConfigs() {
$defaultConfigs = [
// Configuración de WhatsApp
'whatsapp_token' => env('WHATSAPP_TOKEN', 'TU_TOKEN_DE_WHATSAPP_AQUI'),
'whatsapp_phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID', 'TU_PHONE_ID_AQUI'),
'whatsapp_api_url' => env('WHATSAPP_API_URL', 'https://graph.facebook.com/v22.0/'),
'webhook_verify_token' => env('WEBHOOK_VERIFY_TOKEN', 'mi_token_secreto_123'),
// Configuración de la aplicación
'app_name' => 'WhatsApp Bot System',
'app_version' => '1.0.0',
'timezone' => 'America/Bogota',
'developer_name' => 'U-Site.app',
'developer_url' => 'https://u-site.app',
'developer_email' => 'support@u-site.app',
// Configuración de logs y archivos
'enable_logging' => '1',
'log_level' => 'INFO',
'upload_max_size' => '10485760',
'allowed_file_types' => 'jpg,jpeg,png,gif,pdf,doc,docx',
// Configuración de seguridad
'session_timeout' => '1800',
'max_login_attempts' => '3',
'login_lockout_time' => '900'
];
return saveMultipleConfigsToDB($defaultConfigs);
}
/**
* Migra configuraciones desde constantes/archivos a la base de datos
* @return bool True si la migración fue exitosa
*/
function migrateConfigsToDatabase() {
try {
writeLog('INFO', 'Iniciando migración de configuraciones a BD');
// Crear tabla si no existe
$pdo = createDbConnection();
if (!createSystemConfigTable($pdo)) {
writeLog('ERROR', 'No se pudo crear la tabla system_config');
return false;
}
// Migrar configuraciones actuales
$success = initializeDefaultConfigs();
if ($success) {
writeLog('INFO', 'Migración de configuraciones completada exitosamente');
} else {
writeLog('ERROR', 'Error durante la migración de configuraciones');
}
return $success;
} catch (Exception $e) {
writeLog('ERROR', 'Error crítico en migración de configuraciones: ' . $e->getMessage());
return false;
}
}
// Función para validar configuración de WhatsApp
function validateWhatsAppConfig() {
$errors = [];
if (WHATSAPP_TOKEN === 'TU_TOKEN_DE_WHATSAPP_AQUI' || empty(WHATSAPP_TOKEN)) {
$errors[] = 'Token de WhatsApp no configurado';
}
if (WHATSAPP_PHONE_NUMBER_ID === 'TU_PHONE_ID_AQUI' || empty(WHATSAPP_PHONE_NUMBER_ID)) {
$errors[] = 'Phone Number ID de WhatsApp no configurado';
}
// Detectar Phone Number ID problemático conocido
if (WHATSAPP_PHONE_NUMBER_ID === '858157464051987') {
$errors[] = '⚠️ PHONE NUMBER ID PROBLEMÁTICO: El ID 858157464051987 genera errores. <a href="fix_phone_number_id.php" class="alert-link"><strong>Corregir ahora</strong></a>';
}
if (empty(WEBHOOK_VERIFY_TOKEN)) {
$errors[] = 'Token de verificación del webhook no configurado';
}
return $errors;
}
// Función para logging centralizado
if (!function_exists('writeLog')) {
function writeLog($level, $message, $context = []) {
if (!ENABLE_LOGGING) return;
$logLevels = ['DEBUG' => 0, 'INFO' => 1, 'WARNING' => 2, 'ERROR' => 3];
$currentLevel = $logLevels[LOG_LEVEL] ?? 1;
if (($logLevels[$level] ?? 1) < $currentLevel) {
return;
}
$timestamp = date('Y-m-d H:i:s');
$contextStr = $context ? ' ' . json_encode($context) : '';
$logMessage = "[$timestamp] [$level] $message$contextStr" . PHP_EOL;
$logDir = dirname(__DIR__) . '/logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
file_put_contents($logDir . '/system.log', $logMessage, FILE_APPEND | LOCK_EX);
}
}
// Crear directorios necesarios
$requiredDirs = [
dirname(__DIR__) . '/logs',
dirname(__DIR__) . '/uploads'
];
foreach ($requiredDirs as $dir) {
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
// Inicializar sesión si no está activa
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Verificar timeout de sesión
if (isUserLoggedIn() && isset($_SESSION['last_activity'])) {
if (time() - $_SESSION['last_activity'] > SESSION_TIMEOUT) {
session_destroy();
session_start();
}
}
// Actualizar última actividad
if (isUserLoggedIn()) {
$_SESSION['last_activity'] = time();
}
// Función para verificar autenticación en APIs
function requireAuthentication() {
if (!isUserLoggedIn()) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'error' => 'No autorizado. Inicie sesión.',
'redirect' => 'login.php'
]);
exit;
}
}
?>