<?php
/**
 * @intranet-file instalacion/lib.php
 * @intranet-file-version 2026.03.13.1
 * @intranet-file-date 2026-03-13
 * @intranet-file-owner CSIF Intranet
 */
function is_https(): bool {
    if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') return true;
    if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') return true;
    return false;
}
function enforce_https(): void {
    $host = $_SERVER['HTTP_HOST'] ?? '';
    $is_local = in_array($host, ['localhost', '127.0.0.1'], true);
    if (!$is_local && !is_https()) {
        $uri = $_SERVER['REQUEST_URI'] ?? '/';
        header('Location: https://' . $host . $uri, true, 302);
        exit;
    }
}
function h($s): string { return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }

function app_selected(array $selected, string $app_id): bool {
    return in_array($app_id, $selected, true);
}

function check_requirements(): array {
    $req = [];
    $req[] = ['PHP >= 8.0', version_compare(PHP_VERSION, '8.0.0', '>='), PHP_VERSION];
    $req[] = ['Ext PDO + pdo_mysql', extension_loaded('pdo') && extension_loaded('pdo_mysql'), ''];
    $req[] = ['Ext LDAP (para login)', extension_loaded('ldap'), ''];
    $req[] = ['Ext JSON', extension_loaded('json'), ''];
    $commonWritable = is_writable(dirname(__DIR__) . '/common') || (file_exists(dirname(__DIR__) . '/common/config.php') && is_writable(dirname(__DIR__) . '/common/config.php'));
    $secretsWritable = is_writable(dirname(__DIR__) . '/config') || (file_exists(dirname(__DIR__) . '/config/secrets.local.php') && is_writable(dirname(__DIR__) . '/config/secrets.local.php'));
    $req[] = ['Escritura common/config.php y config/secrets.local.php', $commonWritable && $secretsWritable, ''];
    $req[] = ['SQL schema presente', file_exists(dirname(__DIR__) . '/config/intranet_schema.sql'), 'config/intranet_schema.sql'];
    $req[] = ['SQL seed presente', file_exists(dirname(__DIR__) . '/config/intranet_seed.sql'), 'config/intranet_seed.sql'];
    return $req;
}

function pdo_connect(array $db): PDO {
    $dsn = "mysql:host={$db['host']};port={$db['port']};dbname={$db['name']};charset=utf8mb4";
    $pdo = new PDO($dsn, $db['user'], $db['pass'], [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
    return $pdo;
}

function mysql_exec_admin(array $admin, string $sql): void {
    $dsn = "mysql:host={$admin['host']};port={$admin['port']};charset=utf8mb4";
    $pdo = new PDO($dsn, $admin['user'], $admin['pass'], [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
    $pdo->exec($sql);
}

function ensure_db_and_user(array $admin, array $db, string $userHost): void {
    // Create DB
    mysql_exec_admin($admin, "CREATE DATABASE IF NOT EXISTS `{$db['name']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;");
    // Create user (host-specific)
    $u = $db['user']; $p = str_replace("'", "\\'", $db['pass']);
    mysql_exec_admin($admin, "CREATE USER IF NOT EXISTS '{$u}'@'{$userHost}' IDENTIFIED BY '{$p}';");
    mysql_exec_admin($admin, "GRANT ALL PRIVILEGES ON `{$db['name']}`.* TO '{$u}'@'{$userHost}';");
    // Common pitfall: client connects as 127.0.0.1
    if ($userHost === 'localhost') {
        mysql_exec_admin($admin, "CREATE USER IF NOT EXISTS '{$u}'@'127.0.0.1' IDENTIFIED BY '{$p}';");
        mysql_exec_admin($admin, "GRANT ALL PRIVILEGES ON `{$db['name']}`.* TO '{$u}'@'127.0.0.1';");
    }
    mysql_exec_admin($admin, "FLUSH PRIVILEGES;");
}

function run_sql_file(PDO $pdo, string $filePath): void {
    $sql = file_get_contents($filePath);
    if ($sql === false) throw new RuntimeException("No se puede leer $filePath");
    $pdo->exec("SET FOREIGN_KEY_CHECKS=0;");
    // Ensure USE db is not needed (pdo has db selected)
    $pdo->exec($sql);
    $pdo->exec("SET FOREIGN_KEY_CHECKS=1;");
}

function ldap_parse_server_url(string $server): array {
    $server = trim($server);
    if ($server === '') {
        return [null, null, null, 'Servidor LDAP vacÃ­o'];
    }

    $parts = @parse_url($server);
    if ($parts === false || empty($parts['host'])) {
        return [null, null, null, 'Servidor LDAP invÃ¡lido. Usa por ejemplo ldap://servidor:389 o ldaps://servidor:636'];
    }

    $scheme = strtolower((string)($parts['scheme'] ?? 'ldap'));
    $host = (string)$parts['host'];
    $port = (int)($parts['port'] ?? ($scheme === 'ldaps' ? 636 : 389));

    return [$scheme, $host, $port, null];
}

function ldap_tcp_probe(string $server, float $timeout = 5.0): array {
    [$scheme, $host, $port, $parseError] = ldap_parse_server_url($server);
    if ($parseError !== null) {
        return [false, $parseError];
    }

    $errno = 0;
    $errstr = '';
    $target = ($scheme === 'ldaps' ? 'ssl' : 'tcp') . '://' . $host . ':' . $port;
    $fp = @stream_socket_client($target, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT);
    if (!$fp) {
        $msg = $errstr !== '' ? $errstr : 'error desconocido';
        return [false, "No se pudo abrir {$host}:{$port} ({$msg})"];
    }
    fclose($fp);
    return [true, "Conectividad TCP OK con {$host}:{$port}"];
}

function ldap_last_diagnostic($conn): string {
    $diag = '';
    if (function_exists('ldap_get_option') && defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
        @ldap_get_option($conn, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diag);
    }
    return trim((string)$diag);
}


function ldap_is_ldaps(string $server): bool {
    [$scheme, $host, $port, $parseError] = ldap_parse_server_url($server);
    return $parseError === null && $scheme === 'ldaps';
}

function ldap_host_is_ip(string $server): bool {
    [$scheme, $host, $port, $parseError] = ldap_parse_server_url($server);
    if ($parseError !== null || $host === null) return false;
    return filter_var($host, FILTER_VALIDATE_IP) !== false;
}

/**
 * Normaliza el nombre del certificado que se guardarÃ¡ en Linux para evitar
 * rutas arbitrarias, extensiones extraÃ±as y caracteres no seguros.
 */
function sanitize_ca_cert_filename(string $filename, string $default = 'intranet-ldaps-ca.crt'): string {
    $filename = trim(basename(str_replace('\\', '/', $filename)));
    if ($filename === '') {
        $filename = $default;
    }

    $filename = preg_replace('/[^A-Za-z0-9._-]+/', '-', $filename) ?: $default;
    if (!preg_match('/\.crt$/i', $filename)) {
        $filename = preg_replace('/\.(cer|pem)$/i', '', $filename);
        $filename = rtrim((string)$filename, '.-');
        $filename .= '.crt';
    }

    return $filename;
}

/**
 * Valida un certificado X.509 subido por el administrador y lo convierte a PEM.
 * AsÃ­ reutilizamos la misma validaciÃ³n tanto para la copia temporal como para
 * la instalaciÃ³n final en el almacÃ©n del sistema.
 */
function certificate_upload_to_pem(array $file): string {
    if (!isset($file['tmp_name']) || !isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Debes subir un certificado de CA vÃ¡lido (.cer, .crt o .pem).');
    }
    if (!is_uploaded_file($file['tmp_name'])) {
        throw new RuntimeException('La subida del certificado no es vÃ¡lida.');
    }

    $raw = @file_get_contents($file['tmp_name']);
    if ($raw === false || trim($raw) === '') {
        throw new RuntimeException('No se pudo leer el certificado subido.');
    }

    $x509 = @openssl_x509_read($raw);
    if ($x509 === false) {
        throw new RuntimeException('El archivo subido no contiene un certificado X.509 vÃ¡lido.');
    }

    $pem = '';
    if (!@openssl_x509_export($x509, $pem)) {
        throw new RuntimeException('No se pudo convertir el certificado al formato PEM.');
    }

    return $pem;
}

/**
 * Guarda una copia temporal del certificado dentro del instalador para que el
 * administrador pueda instalarlo manualmente si el usuario web no tiene sudo.
 */
function cache_uploaded_ca_certificate(array $file, string $targetName = 'intranet-ldaps-ca.crt'): array {
    $pem = certificate_upload_to_pem($file);
    $cacheDir = __DIR__ . '/tmp';
    if (!is_dir($cacheDir) && !@mkdir($cacheDir, 0750, true) && !is_dir($cacheDir)) {
        throw new RuntimeException('No se pudo crear la carpeta temporal del instalador para el certificado LDAPS.');
    }
    if (!is_writable($cacheDir)) {
        throw new RuntimeException('La carpeta temporal del instalador no tiene permisos de escritura.');
    }

    $targetPath = $cacheDir . DIRECTORY_SEPARATOR . sanitize_ca_cert_filename($targetName);
    if (@file_put_contents($targetPath, $pem, LOCK_EX) === false) {
        throw new RuntimeException('No se pudo guardar la copia temporal del certificado LDAPS.');
    }
    @chmod($targetPath, 0640);

    return [$targetPath, $pem];
}

/**
 * Instala el certificado en el almacÃ©n del sistema y actualiza la confianza CA.
 * Si el usuario web no tiene permisos, el instalador mostrarÃ¡ la alternativa manual.
 */
function install_uploaded_ca_certificate(array $file, string $targetName = 'intranet-ldaps-ca.crt'): array {
    $pem = certificate_upload_to_pem($file);

    $installDir = '/usr/local/share/ca-certificates';
    if (!is_dir($installDir) || !is_writable($installDir)) {
        throw new RuntimeException('La carpeta ' . $installDir . ' no existe o no tiene permisos de escritura para el usuario web.');
    }

    $targetPath = rtrim($installDir, '/\\') . DIRECTORY_SEPARATOR . basename($targetName);
    if (@file_put_contents($targetPath, $pem) === false) {
        throw new RuntimeException('No se pudo guardar el certificado en ' . $targetPath . '.');
    }

    @chmod($targetPath, 0644);

    $output = [];
    $code = 0;
    @exec('update-ca-certificates 2>&1', $output, $code);
    if ($code !== 0) {
        @unlink($targetPath);
        throw new RuntimeException('Se guardÃ³ el certificado, pero fallÃ³ update-ca-certificates: ' . trim(implode("\n", $output)));
    }

    return [$targetPath, trim(implode("\n", $output))];
}
function ldap_test(array $ldap): array {
    if (!extension_loaded('ldap')) return [false, 'ExtensiÃ³n LDAP no disponible en PHP.'];

    [$scheme, $host, $port, $parseError] = ldap_parse_server_url((string)($ldap['server'] ?? ''));
    if ($parseError !== null) {
        return [false, $parseError];
    }

    [$tcpOk, $tcpMsg] = ldap_tcp_probe((string)$ldap['server']);
    if (!$tcpOk) {
        return [false, $tcpMsg];
    }

    $conn = @ldap_connect((string)$ldap['server']);
    if (!$conn) return [false, 'No se pudo inicializar la conexiÃ³n LDAP.'];

    ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);
    ldap_set_option($conn, LDAP_OPT_REFERRALS, 0);
    if (defined('LDAP_OPT_NETWORK_TIMEOUT')) {
        @ldap_set_option($conn, LDAP_OPT_NETWORK_TIMEOUT, 5);
    }
    if (defined('LDAP_OPT_TIMEOUT')) {
        @ldap_set_option($conn, LDAP_OPT_TIMEOUT, 10);
    }

    $bindDn = (string)($ldap['bind_dn'] ?? '');
    $bindPass = (string)($ldap['bind_pass'] ?? '');
    $baseDn = (string)($ldap['base_dn'] ?? '');

    $ok = @ldap_bind($conn, $bindDn, $bindPass);
    if (!$ok) {
        $err = ldap_error($conn);
        $diag = ldap_last_diagnostic($conn);
        @ldap_unbind($conn);
        $extra = $diag !== '' ? " | DiagnÃ³stico: {$diag}" : '';
        if ($scheme === 'ldaps' && stripos($err . ' ' . $diag, 'certificate') !== false) {
            $extra .= ' | Revisa la cadena de certificados CA del servidor LDAPS.';
        }
        return [false, "Bind fallido en {$host}:{$port}: {$err}{$extra}"];
    }

    $sr = @ldap_search($conn, $baseDn, '(objectClass=*)', ['dn'], 0, 1);
    if (!$sr) {
        $err = ldap_error($conn);
        $diag = ldap_last_diagnostic($conn);
        @ldap_unbind($conn);
        $extra = $diag !== '' ? " | DiagnÃ³stico: {$diag}" : '';
        return [false, "BÃºsqueda en base DN fallida: {$err}{$extra}"];
    }

    @ldap_unbind($conn);
    return [true, "OK. {$tcpMsg}. Bind y bÃºsqueda base correctos."];
}

function write_common_config(array $ldap, array $db, string $siteName): void {
    $path = dirname(__DIR__) . '/common/config.php';
    if (file_exists($path)) {
        @copy($path, $path . '.bak.' . date('Ymd_His'));
    }

    $secretsPath = dirname(__DIR__) . '/config/secrets.local.php';
    if (file_exists($secretsPath)) {
        @copy($secretsPath, $secretsPath . '.bak.' . date('Ymd_His'));
    }

    $content = "<?php\n";
    $content .= "// Auto-generado por /instalacion\n";
    $content .= "$intranetConfigOverrides = [\n";
    $content .= "    '/etc/csif/intranet-config.php',\n";
    $content .= "    dirname(__DIR__) . '/config/secrets.local.php',\n";
    $content .= "];\n\n";
    $content .= "foreach ($intranetConfigOverrides as $overridePath) {\n";
    $content .= "    if (is_file($overridePath) && is_readable($overridePath)) {\n";
    $content .= "        require_once $overridePath;\n";
    $content .= "    }\n";
    $content .= "}\n\n";
    $content .= "if (!function_exists('intranet_config_env_value')) {\n";
    $content .= "    function intranet_config_env_value(string $name, string $default = ''): string\n";
    $content .= "    {\n";
    $content .= "        $value = getenv($name);\n";
    $content .= "        if ($value !== false && $value !== '') {\n";
    $content .= "            return (string)$value;\n";
    $content .= "        }\n";
    $content .= "        if (isset($_ENV[$name]) && $_ENV[$name] !== '') {\n";
    $content .= "            return (string)$_ENV[$name];\n";
    $content .= "        }\n";
    $content .= "        return $default;\n";
    $content .= "    }\n";
    $content .= "}\n\n";
    $content .= "if (!function_exists('intranet_config_define')) {\n";
    $content .= "    function intranet_config_define(string $name, $value): void\n";
    $content .= "    {\n";
    $content .= "        if (!defined($name)) {\n";
    $content .= "            define($name, $value);\n";
    $content .= "        }\n";
    $content .= "    }\n";
    $content .= "}\n\n";
    $content .= "define('INTRANET_SITE_NAME', " . var_export($siteName, true) . ");\n\n";
    $content .= "intranet_config_define('LDAP_SERVER', intranet_config_env_value('LDAP_SERVER', " . var_export($ldap['server'], true) . "));\n";
    $content .= "intranet_config_define('LDAP_DOMAIN', intranet_config_env_value('LDAP_DOMAIN', " . var_export($ldap['domain'], true) . "));\n";
    $content .= "intranet_config_define('LDAP_BASE_DN', intranet_config_env_value('LDAP_BASE_DN', " . var_export($ldap['base_dn'], true) . "));\n";
    $content .= "intranet_config_define('LDAP_ADMIN_DN', intranet_config_env_value('LDAP_ADMIN_DN', " . var_export($ldap['bind_dn'], true) . "));\n";
    $content .= "intranet_config_define('LDAP_ADMIN_PASS', intranet_config_env_value('LDAP_ADMIN_PASS', ''));\n\n";
    $content .= "intranet_config_define('DB_HOST', intranet_config_env_value('DB_HOST', " . var_export($db['host'], true) . "));\n";
    $content .= "intranet_config_define('DB_PORT', (int) intranet_config_env_value('DB_PORT', " . var_export((string)((int)$db['port']), true) . "));\n";
    $content .= "intranet_config_define('DB_NAME', intranet_config_env_value('DB_NAME', " . var_export($db['name'], true) . "));\n";
    $content .= "intranet_config_define('DB_USER', intranet_config_env_value('DB_USER', " . var_export($db['user'], true) . "));\n";
    $content .= "intranet_config_define('DB_PASS', intranet_config_env_value('DB_PASS', ''));\n";
    $content .= "?>\n";
    file_put_contents($path, $content);

    $secretContent = "<?php\n";
    $secretContent .= "define('LDAP_ADMIN_PASS', " . var_export($ldap['bind_pass'], true) . ");\n";
    $secretContent .= "define('DB_PASS', " . var_export($db['pass'], true) . ");\n";
    file_put_contents($secretsPath, $secretContent);
}
function save_logo(array $file, string $destPath): void {
    if (!isset($file['tmp_name']) || $file['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException("Error subiendo fichero.");
    }
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($file['tmp_name']);
    if ($mime !== 'image/png') throw new RuntimeException("El fichero debe ser PNG. Detectado: $mime");
    $dir = dirname($destPath);
    if (!is_dir($dir)) @mkdir($dir, 0775, true);
    if (!move_uploaded_file($file['tmp_name'], $destPath)) {
        throw new RuntimeException("No se pudo guardar en $destPath");
    }
}

function update_apps(PDO $pdo, array $selectedExtras): void {
    $extras = ['entregas','flota','compras','listin','permisos'];
    // Delete non-selected extras completely
    foreach ($extras as $app_id) {
        if (!in_array($app_id, $selectedExtras, true)) {
            $st = $pdo->prepare("DELETE FROM intranet_apps WHERE app_id = ?");
            $st->execute([$app_id]);
        }
    }
    // Ensure selected extras are enabled
    if (!empty($selectedExtras)) {
        $in = implode(',', array_fill(0, count($selectedExtras), '?'));
        $pdo->prepare("UPDATE intranet_apps SET is_enabled=1 WHERE app_id IN ($in)")->execute($selectedExtras);
    }
}

function rrmdir(string $dir): bool {
    if (!is_dir($dir)) return true;
    $items = scandir($dir);
    if ($items === false) return false;
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        $path = $dir . DIRECTORY_SEPARATOR . $item;
        if (is_dir($path)) rrmdir($path);
        else @unlink($path);
    }
    return @rmdir($dir);
}
?>
