文件
freeshop/update.php
T
2026-08-16 17:03:10 +08:00

317 行
13 KiB
PHP
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
<?php
if (!function_exists('up_h')) {
function up_h($s) {
return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
$base = __DIR__;
if (!is_file($base . '/config.php')) {
http_response_code(500);
exit('未找到 config.php,请确认 update.php 位于商城根目录。');
}
require_once $base . '/config.php';
if (!is_file($base . '/includes/app_config.php')) {
http_response_code(500);
exit('未找到 includes/app_config.php,无法获取版本信息。');
}
require_once $base . '/includes/app_config.php';
if (!defined('SITE_KEY')) {
http_response_code(500);
exit('config.php 中未定义 SITE_KEY。');
}
$currentVersion = defined('MALL_VERSION') ? MALL_VERSION : 'unknown';
$apiUrl = defined('UPDATE_API_URL') ? rtrim(UPDATE_API_URL, '/') : '';
$apiHost = '';
if ($apiUrl !== '') {
$p = parse_url($apiUrl);
$apiHost = isset($p['host']) ? strtolower($p['host']) : '';
}
$step = 'form';
$errMsg = '';
$log = [];
function up_log(&$log, $msg, $ok = true) {
$log[] = ['msg' => $msg, 'ok' => $ok];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted = trim((string) ($_POST['site_key'] ?? ''));
if ($submitted === '') {
$step = 'error';
$errMsg = '请输入 SITE_KEY。';
} elseif (!hash_equals((string) SITE_KEY, $submitted)) {
$step = 'error';
$errMsg = 'SITE_KEY 校验失败,请确认后重试。';
} else {
$step = 'updating';
}
}
if ($step === 'updating') {
$ts = date('Ymd_His');
$tmpRoot = $base . '/storage/cache/update_' . $ts;
$zipPath = $base . '/storage/cache/update_' . $ts . '.zip';
$backupDir = $base . '/storage/backups/update_' . $ts;
$excludeRel = ['config.php', 'update.php'];
try {
if ($apiUrl === '') {
throw new RuntimeException('未配置 UPDATE_API_URL(请检查 includes/app_config.php)。');
}
$infoUrl = $apiUrl . '?v=' . urlencode($currentVersion);
$raw = up_fetch($infoUrl, 15);
if ($raw === false || $raw === null) {
throw new RuntimeException('无法连接更新服务器,请检查网络或 UPDATE_API_URL。');
}
$info = json_decode($raw, true);
if (!is_array($info)) {
throw new RuntimeException('更新服务器返回的数据格式异常。');
}
if (empty($info['update_available']) || empty($info['latest'])) {
up_log($log, '当前已是最新版本(v' . up_h($currentVersion) . '),无需更新。', true);
$step = 'done';
} else {
$latest = $info['latest'];
$newVersion = $latest['version'] ?? '?';
$dlUrl = $latest['download_url'] ?? '';
up_log($log, '检测到新版本:v' . up_h($newVersion)
. '' . up_h($latest['type'] ?? 'patch') . ',发布于 '
. up_h($latest['release_date'] ?? '-') . '', true);
if ($dlUrl === '') {
throw new RuntimeException('更新信息中缺少下载地址。');
}
$dlParts = parse_url($dlUrl);
$dlHost = isset($dlParts['host']) ? strtolower($dlParts['host']) : '';
if ($dlHost === '' || ($apiHost !== '' && $dlHost !== $apiHost)) {
throw new RuntimeException('下载地址域名不在信任列表(期望:' . up_h($apiHost) . ',实际:' . up_h($dlHost) . '),已拒绝以防供应链攻击。');
}
if (!is_dir(dirname($zipPath))) {
@mkdir(dirname($zipPath), 0755, true);
}
$ok = up_download($dlUrl, $zipPath, 60, 100 * 1024 * 1024);
if (!$ok || !is_file($zipPath)) {
throw new RuntimeException('安装包下载失败。');
}
up_log($log, '安装包已下载:' . up_h(basename($zipPath)) . '' . round(filesize($zipPath) / 1024, 1) . ' KB', true);
$sig = file_get_contents($zipPath, false, null, 0, 4);
if ($sig === false || substr($sig, 0, 2) !== "PK") {
@unlink($zipPath);
throw new RuntimeException('安装包不是有效的 ZIP 文件(签名校验失败),已删除。');
}
up_log($log, 'ZIP 签名校验通过。', true);
if (!class_exists('ZipArchive')) {
@unlink($zipPath);
throw new RuntimeException('服务器未启用 ZipArchive 扩展,无法解压更新包。');
}
if (!is_dir($tmpRoot)) {
@mkdir($tmpRoot, 0755, true);
}
$za = new ZipArchive();
if ($za->open($zipPath) !== TRUE) {
@unlink($zipPath);
throw new RuntimeException('无法打开 ZIP 安装包。');
}
if (!$za->extractTo($tmpRoot)) {
$za->close();
@unlink($zipPath);
throw new RuntimeException('解压安装包失败。');
}
$za->close();
up_log($log, '安装包已解压到临时目录。', true);
if (!is_dir($backupDir)) {
@mkdir($backupDir, 0755, true);
}
$copied = 0;
$skipped = 0;
up_apply($tmpRoot, $base, $backupDir, $excludeRel, $log, $copied, $skipped);
up_log($log, '更新完成:已覆盖 ' . $copied . ' 个文件,跳过 ' . $skipped . ' 个(含 config.php / storage / update.php)。', true);
up_log($log, '旧文件已备份至:storage/backups/update_' . $ts . '/', true);
@unlink($zipPath);
up_rrmdir($tmpRoot);
up_log($log, '已从 v' . up_h($currentVersion) . ' 升级到 v' . up_h($newVersion) . '。', true);
$step = 'done';
}
} catch (Throwable $e) {
$step = 'error';
$errMsg = '更新失败:' . $e->getMessage();
up_log($log, $errMsg, false);
}
}
function up_fetch($url, $timeout = 15) {
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_USERAGENT => 'FNW-Mall-Updater',
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
]);
$r = curl_exec($ch);
curl_close($ch);
return $r;
}
if (function_exists('file_get_contents')) {
$ctx = stream_context_create([
'http' => ['timeout' => $timeout, 'ignore_errors' => true, 'method' => 'GET',
'header' => "User-Agent: FNW-Mall-Updater\r\n"]
]);
return @file_get_contents($url, false, $ctx);
}
return false;
}
function up_download($url, $dest, $timeout = 60, $maxBytes = 104857600) {
if (function_exists('curl_init')) {
$fp = @fopen($dest, 'wb');
if (!$fp) return false;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_FILE => $fp,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_USERAGENT => 'FNW-Mall-Updater',
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_NOPROGRESS => false,
CURLOPT_BUFFERSIZE => 8192,
]);
$ok = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
if (!$ok || $code >= 400 || !is_file($dest) || filesize($dest) > $maxBytes) {
@unlink($dest);
return false;
}
return true;
}
$data = up_fetch($url, $timeout);
if ($data === false || strlen($data) > $maxBytes) {
return false;
}
return file_put_contents($dest, $data) !== false;
}
function up_apply($src, $dstRoot, $backupDir, $excludeRel, &$log, &$copied, &$skipped) {
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iter as $fileInfo) {
$srcNorm = str_replace('\\', '/', $src);
$pathNorm = str_replace('\\', '/', $fileInfo->getPathname());
$rel = ltrim(substr($pathNorm, strlen($srcNorm)), '/');
if ($rel === '') continue;
if (in_array($rel, $excludeRel, true)) {
$skipped++;
continue;
}
if ($rel === 'storage' || strpos($rel, 'storage/') === 0) {
$skipped++;
continue;
}
if (basename($rel) === 'update.php') {
$skipped++;
continue;
}
$target = $dstRoot . '/' . $rel;
if ($fileInfo->isDir()) {
if (!is_dir($target)) @mkdir($target, 0755, true);
continue;
}
if (is_file($target)) {
$bk = $backupDir . '/' . $rel;
$bkDir = dirname($bk);
if (!is_dir($bkDir)) @mkdir($bkDir, 0755, true);
@copy($target, $bk);
} else {
$td = dirname($target);
if (!is_dir($td)) @mkdir($td, 0755, true);
}
if (@copy($fileInfo->getPathname(), $target)) {
$copied++;
} else {
up_log($log, '复制失败:' . up_h($rel), false);
}
}
}
function up_rrmdir($dir) {
if (!is_dir($dir)) return;
$items = array_diff(scandir($dir), ['.', '..']);
foreach ($items as $item) {
$p = $dir . '/' . $item;
if (is_dir($p)) up_rrmdir($p);
else @unlink($p);
}
@rmdir($dir);
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统更新 - 自由云商城</title>
<style>
* { box-sizing: border-box; }
body { font-family: system-ui, "Microsoft YaHei", sans-serif; margin: 0;
background:
.wrap { max-width: 680px; margin: 48px auto; padding: 0 16px; }
.card { background:
padding: 28px; box-shadow: 0 2px 8px rgba(16,24,40,.08); }
h1 { font-size: 1.4rem; margin: 0 0 6px; color:
.muted { color:
label { display: block; font-weight: 600; margin-bottom: 8px; }
input[type=password] { width: 100%; padding: 12px 14px; border: 1px solid
border-radius: 10px; font-size: 1rem; outline: none; }
input[type=password]:focus { border-color:
.btn { display: inline-block; margin-top: 16px; background:
border: none; padding: 11px 22px; border-radius: 999px; font-size: .95rem;
font-weight: 600; cursor: pointer; }
.btn:hover { background:
.err { background:
padding: 12px 14px; border-radius: 10px; margin-bottom: 14px; }
.ok { background:
padding: 12px 14px; border-radius: 10px; margin-bottom: 14px; }
.log { list-style: none; padding: 0; margin: 14px 0 0; font-size: .9rem; }
.log li { padding: 6px 0; border-bottom: 1px dashed
.log li.bad { color:
.log li .tick { font-weight: 700; margin-right: 6px; }
code { background:
a.back { color:
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1><i class="fas fa-cloud-arrow-up"></i> 系统更新</h1>
<p class="muted">当前版本:<strong><?= up_h($currentVersion) ?></strong></p>
<?php if (!empty($log)): ?>
<ul class="log">
<?php foreach ($log as $l): ?>
<li class="<?= $l['ok'] ? '' : 'bad' ?>">
<span class="tick"><?= $l['ok'] ? '✓' : '✗' ?></span><?= up_h($l['msg']) ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if ($step === 'form' || $step === 'error'): ?>
<?php if ($step === 'error' && $errMsg): ?>
<div class="err"><i class="fas fa-triangle-exclamation"></i> <?= up_h($errMsg) ?></div>
<?php endif; ?>
<form method="post" action="">
<label for="sk">请输入 SITE_KEY 以验证身份</label>
<input type="password" id="sk" name="site_key" autocomplete="off" placeholder="SITE_KEY" required>
<button type="submit" class="btn"><i class="fas fa-rocket"></i> 开始检查并更新</button>
</form>
<p class="muted" style="margin-top:14px">
SITE_KEY 与数据库凭据一同保存在 <code>config.php</code> 中,仅站点所有者可知。
更新前建议先备份数据库与 <code>storage/</code> 目录。
</p>
<?php elseif ($step === 'done'): ?>
<p class="ok" style="margin-top:16px">
<i class="fas fa-circle-check"></i> 更新流程已结束。
</p>
<p><a class="back" href="admin/update.php"><i class="fas fa-arrow-left"></i> 返回后台更新页</a></p>
<?php endif; ?>
</div>
</div>
</body>
</html>