<?php
if(!defined('__KIMS__')) exit;
checkAdmin(0);
include_once $g['path_core'].'function/update.func.php';

$build = preg_replace('/[^0-9A-Za-z._-]/', '', (string)($build ?? $ufile ?? ''));
if ($build === '') getLink('','','업데이트 빌드 번호가 올바르지 않습니다.','');
$historyFile = $g['path_var'].'update/'.$build.'.json';

if ($type === 'delete') {
    if (is_file($historyFile)) @unlink($historyFile);
    getLink('reload','parent.','업데이트 기록이 제거되었습니다.','');
}
if ($type === 'manual') getLink('','','보안을 위해 수동 완료 처리는 지원하지 않습니다.','');
if ($type !== 'auto') getLink('','','지원하지 않는 업데이트 작업입니다.','');

$lockPath = $g['path_var'].'update/.update.lock';
$lock = fopen($lockPath, 'c');
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) getLink('','','다른 업데이트가 실행 중입니다.','');

$stage = '';
$backup = '';
$root = realpath('./');
$copied = array();
$added = array();
$dbDump = '';

try {
    $manifest = jusqUpdateManifest();
    $release = jusqUpdateRelease($manifest, $build);
    foreach (array('version','build','url','sha256','size') as $required) if (empty($release[$required])) throw new Exception('릴리스 필수 정보가 누락됐습니다: '.$required);
    if (!preg_match('/^[a-f0-9]{64}$/', $release['sha256'])) throw new Exception('패키지 해시 형식이 올바르지 않습니다.');
    if ((int)$release['size'] < 1) throw new Exception('패키지 크기가 올바르지 않습니다.');
    if (!empty($release['min_version']) && version_compare($d['admin']['version'], $release['min_version'], '<')) throw new Exception('먼저 필요한 선행 업데이트가 있습니다.');
    if (version_compare($release['version'], $d['admin']['version'], '<=')) throw new Exception('현재 버전보다 새로운 업데이트가 아닙니다.');

    $config = jusqUpdateConfig();
    $workBase = $g['path_tmp'].'jusq-update';
    jusqUpdateEnsureDir($workBase);
    $stage = $workBase.'/'.$build.'-'.bin2hex(random_bytes(5));
    jusqUpdateEnsureDir($stage);
    $zipFile = $stage.'/package.zip';
    jusqUpdateDownload($release['url'], $zipFile, min((int)$config['update_max_bytes'], max((int)$release['size'] + 1048576, (int)$release['size'])));
    if (!hash_equals($release['sha256'], hash_file('sha256', $zipFile))) throw new Exception('패키지 SHA-256 검증에 실패했습니다.');
    if ((int)$release['size'] !== filesize($zipFile)) throw new Exception('패키지 파일 크기가 매니페스트와 다릅니다.');

    $zip = new ZipArchive();
    if ($zip->open($zipFile) !== true) throw new Exception('업데이트 ZIP을 열 수 없습니다.');
    $entries = array();
    for ($i=0; $i<$zip->numFiles; $i++) {
        $name = $zip->getNameIndex($i);
        if (!jusqUpdateSafePath($name)) throw new Exception('패키지에 위험한 경로가 포함돼 있습니다.');
        $opsys = 0; $attr = 0;
        if ($zip->getExternalAttributesIndex($i, $opsys, $attr) && (($attr >> 16) & 0170000) === 0120000) throw new Exception('심볼릭 링크는 업데이트에 포함할 수 없습니다.');
        $entries[] = $name;
    }
    if (!$zip->extractTo($stage.'/extract')) throw new Exception('패키지 압축 해제에 실패했습니다.');
    $zip->close();
    @unlink($zipFile);

    $packageMetaFile = $stage.'/extract/update.json';
    $packageMeta = is_file($packageMetaFile) ? json_decode(file_get_contents($packageMetaFile), true) : null;
    if (!is_array($packageMeta) || ($packageMeta['product'] ?? '') !== 'jusq' || ($packageMeta['version'] ?? '') !== $release['version'] || (string)($packageMeta['build'] ?? '') !== (string)$build) throw new Exception('패키지 내부 정보가 매니페스트와 일치하지 않습니다.');

    $backup = $g['path_var'].'update/backups/'.$build.'-'.$date['totime'];
    jusqUpdateEnsureDir($backup.'/files');
    $dbDump = $backup.'/database.sql';
    $dbError = $backup.'/database-error.log';
    $dbCommand = array('/usr/bin/mysqldump','--single-transaction','--skip-comments','-h',$DB['host'],'-u',$DB['user'],$DB['name']);
    $proc = proc_open($dbCommand, array(1=>array('file',$dbDump,'w'),2=>array('file',$dbError,'w')), $pipes, null, array('MYSQL_PWD'=>$DB['pass']));
    if (!is_resource($proc) || proc_close($proc) !== 0 || !is_file($dbDump) || filesize($dbDump) < 100) throw new Exception('데이터베이스 백업에 실패했습니다.');

    foreach ($entries as $name) {
        if (substr($name,-1) === '/' || $name === 'update.json' || $name === '_update.php') continue;
        $source = $stage.'/extract/'.$name;
        if (!is_file($source)) continue;
        $target = $root.'/'.$name;
        if (is_dir($target)) throw new Exception('파일 대상이 디렉터리와 충돌합니다: '.$name);
        if (is_file($target)) {
            $backupFile = $backup.'/files/'.$name;
            jusqUpdateEnsureDir(dirname($backupFile));
            if (!copy($target, $backupFile)) throw new Exception('기존 파일 백업 실패: '.$name);
            $copied[] = $name;
        } else $added[] = $name;
    }
    $versionConfigName = 'modules/admin/var/var.system.php';
    $versionConfig = $root.'/'.$versionConfigName;
    if (is_file($versionConfig) && !in_array($versionConfigName, $copied, true)) {
        $backupFile = $backup.'/files/'.$versionConfigName;
        jusqUpdateEnsureDir(dirname($backupFile));
        if (!copy($versionConfig, $backupFile)) throw new Exception('현재 버전 설정 백업에 실패했습니다.');
        $copied[] = $versionConfigName;
    }
    file_put_contents($backup.'/added.json', json_encode($added, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT), LOCK_EX);

    foreach ($entries as $name) {
        if (substr($name,-1) === '/' || $name === 'update.json' || $name === '_update.php') continue;
        $source = $stage.'/extract/'.$name;
        if (!is_file($source)) continue;
        $target = $root.'/'.$name;
        jusqUpdateEnsureDir(dirname($target));
        $temporary = $target.'.jusq-update';
        if (!copy($source, $temporary) || !rename($temporary, $target)) throw new Exception('파일 적용 실패: '.$name);
        @chmod($target, 0664);
    }

    $migration = $stage.'/extract/_update.php';
    if (is_file($migration)) {
        $updateContext = array('version'=>$release['version'], 'build'=>$build, 'backup'=>$backup);
        include $migration;
    }

    $versionConfigData = is_file($versionConfig) ? file_get_contents($versionConfig) : false;
    if ($versionConfigData === false) throw new Exception('현재 버전 설정을 읽을 수 없습니다.');
    $versionValue = addcslashes((string)$release['version'], "\\\"");
    $versionConfigData = preg_replace('/(\$d\[\'admin\'\]\[\'version\'\]\s*=\s*)[\'\"][^\'\"]*[\'\"]\s*;/', '$1"'.$versionValue.'";', $versionConfigData, 1, $versionReplaceCount);
    if ($versionReplaceCount !== 1) throw new Exception('현재 버전 설정을 갱신할 수 없습니다.');
    $versionTemporary = $versionConfig.'.jusq-update';
    if (file_put_contents($versionTemporary, $versionConfigData, LOCK_EX) === false || !rename($versionTemporary, $versionConfig)) throw new Exception('현재 버전 저장에 실패했습니다.');
    @chmod($versionConfig, 0664);

    $record = array('version'=>$release['version'],'build'=>$build,'installed_at'=>$date['totime'],'backup'=>$backup,'sha256'=>$release['sha256']);
    file_put_contents($historyFile, json_encode($record, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT), LOCK_EX);
    include_once $g['path_module'].'admin/lib/telemetry.func.php';
    jusqTelemetryReport('update',true,(string)$release['version']);
    jusqUpdateRemoveTree($stage);
    flock($lock, LOCK_UN); fclose($lock);
    getLink('reload','parent.','업데이트가 완료되었습니다.','');
} catch (Throwable $e) {
    if ($backup && is_dir($backup.'/files')) {
        foreach ($copied as $name) if (is_file($backup.'/files/'.$name)) { jusqUpdateEnsureDir(dirname($root.'/'.$name)); @copy($backup.'/files/'.$name, $root.'/'.$name); }
        foreach ($added as $name) @unlink($root.'/'.$name);
        if ($dbDump && is_file($dbDump)) {
            $dbCommand = array('/usr/bin/mysql','-h',$DB['host'],'-u',$DB['user'],$DB['name']);
            $proc = proc_open($dbCommand, array(0=>array('file',$dbDump,'r'),1=>array('file',$backup.'/restore.log','a'),2=>array('file',$backup.'/restore-error.log','a')), $pipes, null, array('MYSQL_PWD'=>$DB['pass']));
            if (is_resource($proc)) proc_close($proc);
        }
    }
    if ($stage) jusqUpdateRemoveTree($stage);
    if ($lock) { flock($lock, LOCK_UN); fclose($lock); }
    getLink('','','업데이트 실패: '.$e->getMessage(),'');
}
?>
