<?php
/**
 * 炎酷CMS - 在线升级核心库
 * 职责：本地版本读写（config/version.php）、HTTP 下载、路径防穿越、
 *       数据库/全站文件备份、SQL 白名单校验、异步任务存取与唤醒
 * 依赖：需先加载 inc/functions.php 与 admin/inc/auth.php（提供 db()、DB_PREFIX、SITE_PATH 等）
 */
defined('YKCMS') or exit('Access Denied');

/* ---------------- 本地版本（config/version.php） ---------------- */

/** 读取当前版本号：优先 config/version.php，兼容旧版数据库配置 */
function yk_version_get() {
    $file = SITE_PATH . '/config/version.php';
    if (is_file($file)) {
        $v = include $file;
        if (is_string($v) && $v !== '') return $v;
    }
    return site_config('version', '1.0.0');
}

/** 写入版本号到 config/version.php，并同步数据库配置（兼容旧逻辑） */
function yk_version_set($version) {
    $dir = SITE_PATH . '/config';
    if (!is_dir($dir)) @mkdir($dir, 0755, true);
    file_put_contents($dir . '/version.php', "<?php\nreturn " . var_export($version, true) . ";\n");
    config_set('version', $version);
}

/* ---------------- HTTP ---------------- */

/** URL 安全校验：仅允许 http/https，防 file:// 等协议注入 */
function yk_url_safe($url) {
    if (!is_string($url) || strlen($url) > 2000) return false;
    $scheme = parse_url($url, PHP_URL_SCHEME);
    return in_array($scheme, array('http', 'https'), true);
}

/** 带超时的 HTTP GET（优先 curl，退化 file_get_contents） */
function yk_http_get($url, $timeout = 5) {
    if (!yk_url_safe($url)) return false;
    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        curl_setopt_array($ch, array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => $timeout,
            CURLOPT_TIMEOUT        => $timeout,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => 0,
            CURLOPT_USERAGENT      => 'YkCMS-Upgrade/1.1',
        ));
        $body = curl_exec($ch);
        $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($body === false || $code >= 400) return false;
        return $body;
    }
    $ctx = stream_context_create(array(
        'http'  => array('timeout' => $timeout, 'user_agent' => 'YkCMS-Upgrade/1.1'),
        'https' => array('timeout' => $timeout, 'user_agent' => 'YkCMS-Upgrade/1.1'),
    ));
    return @file_get_contents($url, false, $ctx);
}

/* ---------------- 路径防穿越 ---------------- */

/**
 * 升级文件路径安全校验：仅允许站点根内相对路径，拒绝 ../、绝对路径、伪协议
 * @return string 目标绝对路径；非法返回空串
 */
function yk_upgrade_safe_path($path) {
    $path = str_replace('\\', '/', (string)$path);
    if ($path === '' || $path[0] === '/' || strpos($path, '..') !== false
        || preg_match('#^[a-zA-Z]:#', $path) || strpos($path, '://') !== false) {
        return '';
    }
    $target = SITE_PATH . '/' . $path;
    $real = realpath($target);
    if ($real !== false && strpos($real, SITE_PATH) !== 0) return '';
    $realDir = realpath(dirname($target));
    if ($realDir !== false && strpos($realDir, SITE_PATH) !== 0) return '';
    return $target;
}

/* ---------------- SQL 白名单（防注入/防破坏） ---------------- */

/**
 * 仅允许结构性/数据性语句，拒绝 DROP/TRUNCATE/GRANT/FILE 等高危操作；
 * 支持 {pre} 占位符，执行前替换为实际表前缀
 */
function yk_sql_allowed($sql) {
    $sql = trim((string)$sql);
    if ($sql === '') return false;
    if (strpos($sql, ';') !== false) return false; // 拒绝多语句串联
    if (!preg_match('/^(ALTER|CREATE|INSERT|UPDATE|REPLACE)\s+/i', $sql)) return false;
    if (preg_match('/\b(DROP|TRUNCATE|DELETE|GRANT|REVOKE|OUTFILE|INFILE|INTO\s+DUMPFILE|EXECUTE|PREPARE)\b/i', $sql)) return false;
    return true;
}

/* ---------------- 数据库备份（PHP 导出整库 SQL） ---------------- */

/**
 * 导出全部数据表（结构+数据）到指定文件
 * @throws Exception
 */
function yk_db_backup($file) {
    $pdo = db();
    $tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN);
    $out = "-- 炎酷CMS 升级前自动备份\n-- " . date('Y-m-d H:i:s') . "\n\n";
    foreach ($tables as $t) {
        $create = $pdo->query("SHOW CREATE TABLE `$t`")->fetch(PDO::FETCH_ASSOC);
        $ddl = end($create);
        // 统一为 IF NOT EXISTS，恢复时不冲突
        $ddl = preg_replace('/^CREATE TABLE `/i', 'CREATE TABLE IF NOT EXISTS `', $ddl);
        $out .= "DROP TABLE IF EXISTS `$t`;\n" . $ddl . ";\n\n";
        $count = (int)$pdo->query("SELECT COUNT(*) FROM `$t`")->fetchColumn();
        for ($offset = 0; $offset < $count; $offset += 500) {
            $rows = $pdo->query("SELECT * FROM `$t` LIMIT $offset, 500")->fetchAll(PDO::FETCH_ASSOC);
            foreach ($rows as $r) {
                $vals = array();
                foreach ($r as $v) {
                    $vals[] = $v === null ? 'NULL' : $pdo->quote((string)$v);
                }
                $out .= "INSERT INTO `$t` VALUES (" . implode(',', $vals) . ");\n";
            }
        }
        $out .= "\n";
    }
    if (@file_put_contents($file, $out) === false) {
        throw new Exception('数据库备份写入失败：' . basename($file));
    }
}

/* ---------------- 全站文件备份 ---------------- */

/**
 * 整站打包（排除 data/backup 与 data/tasks 自身，防止递归膨胀）
 * @throws Exception
 */
function yk_site_backup($zipFile) {
    if (!class_exists('ZipArchive')) {
        throw new Exception('服务器未启用 ZipArchive 扩展，无法打包全站文件');
    }
    $zip = new ZipArchive();
    if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        throw new Exception('无法创建全站备份压缩包');
    }
    $baseLen = strlen(SITE_PATH) + 1;
    $it = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator(SITE_PATH, FilesystemIterator::SKIP_DOTS)
    );
    foreach ($it as $f) {
        if (!$f->isFile()) continue;
        $abs = str_replace('\\', '/', $f->getPathname());
        $rel = substr($abs, $baseLen);
        // 排除备份目录、任务目录，避免递归打包
        if (strpos($rel, 'data/backup/') === 0 || strpos($rel, 'data/tasks/') === 0) continue;
        if (!$zip->addFile($abs, $rel)) continue; // 个别不可读文件跳过，不中断
    }
    $zip->close();
    if (!is_file($zipFile)) {
        throw new Exception('全站备份压缩包生成失败');
    }
}

/* ---------------- 异步任务（文件存储，data/tasks/<id>.json） ---------------- */

function yk_task_dir() {
    $dir = SITE_PATH . '/data/tasks';
    if (!is_dir($dir)) @mkdir($dir, 0755, true);
    return $dir;
}

/** 创建升级任务：快照 manifest、生成一次性执行令牌 */
function yk_task_new($manifest) {
    $id = date('YmdHis') . '_' . substr(md5(uniqid(mt_rand(), true)), 0, 8);
    $task = array(
        'id'       => $id,
        'token'    => md5(uniqid(mt_rand(), true) . $id),
        'status'   => 'pending',   // pending|running|done|error
        'step'     => '等待执行',
        'progress' => 0,
        'log'      => array('任务已创建，目标版本 ' . (isset($manifest['version']) ? $manifest['version'] : '')),
        'manifest' => $manifest,
        'created'  => time(),
        'updated'  => time(),
    );
    yk_task_save($task);
    return $task;
}

function yk_task_path($id) {
    // 防穿越：任务 ID 只允许字母数字下划线
    $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string)$id);
    return yk_task_dir() . '/' . $id . '.json';
}

function yk_task_get($id) {
    $file = yk_task_path($id);
    if (!is_file($file)) return null;
    $t = json_decode((string)file_get_contents($file), true);
    return is_array($t) ? $t : null;
}

function yk_task_save($task) {
    $task['updated'] = time();
    file_put_contents(yk_task_path($task['id']), json_encode($task, JSON_UNESCAPED_UNICODE), LOCK_EX);
}

/** 更新任务进度并追加日志 */
function yk_task_step(&$task, $progress, $step, $log = '') {
    $task['status']   = 'running';
    $task['progress'] = (int)$progress;
    $task['step']     = $step;
    if ($log !== '') $task['log'][] = '[' . date('H:i:s') . '] ' . $log;
    yk_task_save($task);
}

/* ---------------- 异步唤醒 worker ---------------- */

/**
 * 向自身发起非阻塞请求触发后台执行（连接成功即返回，不等响应）
 */
function yk_spawn_worker($url) {
    $parts = parse_url($url);
    if (!isset($parts['host'])) return false;
    $ssl  = (isset($parts['scheme']) && $parts['scheme'] === 'https');
    $host = $parts['host'];
    $port = isset($parts['port']) ? (int)$parts['port'] : ($ssl ? 443 : 80);
    $path = (isset($parts['path']) ? $parts['path'] : '/') . (isset($parts['query']) ? '?' . $parts['query'] : '');
    $fp = @fsockopen(($ssl ? 'ssl://' : '') . $host, $port, $errno, $errstr, 5);
    if (!$fp) return false;
    fwrite($fp, "GET $path HTTP/1.1\r\nHost: $host\r\nUser-Agent: YkCMS-Worker/1.1\r\nConnection: Close\r\n\r\n");
    // 读取响应头首行即关闭，确保请求已被服务器接收
    stream_set_timeout($fp, 3);
    @fgets($fp);
    fclose($fp);
    return true;
}
