From 286a01beb8c0a5a2d31b60ef67f0e5a861514374 Mon Sep 17 00:00:00 2001 From: laoyang Date: Tue, 3 Nov 2026 21:14:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=89=E8=A3=85=E5=90=91=E5=AF=BC=20install.?= =?UTF-8?q?php=20+=20data/=20=E9=98=B2=E6=8A=A4=E6=96=87=E4=BB=B6=E5=85=A5?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install.php(新增): - 环境自检(PHP 版本 / PDO SQLite / mbstring / GD / 网络抓取 / fileinfo / data 与 uploads 可写性),缺 GD 只警告不阻断 - 输入校验:用户名格式、密码 ≥6 位、两次一致 - 建库 + 建管理员 + 写 data/install.lock - 已存在管理员时拒绝安装(防删锁重装夺权);锁文件删掉也不认 - 装完展示数据库信息(类型/路径/大小/表内条目数) data/ 防护文件入库(关键安全修复): - 原 .gitignore 整目录排除 /data/,导致 data/.htaccess 永远进不了仓库, 部署后 data/ 没有禁止 Web 访问的规则 —— nav.db 可被公网直接下载, 里面是管理员密码哈希和全部站点数据 - 改为 /data/* + 白名单放行 .htaccess、index.html --- .gitignore | 6 +- data/.htaccess | 10 ++ data/index.html | 1 + install.php | 311 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 data/.htaccess create mode 100644 data/index.html create mode 100755 install.php diff --git a/.gitignore b/.gitignore index 995e3a4..a63ac74 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,11 @@ # 它们是不同维度(本站一个 vs 每站一个), # 分开写才能各自加白名单、也便于日后单独清理。 # --------------------------------------------------------------------------- -/data/ +# ⚠️ 只忽略数据本件,白名单放行 .htaccess —— 否则部署后 data/ 没有防护文件, +# SQLite 数据库可被公网直接下载(nav.db 里含管理员哈希与全部站点数据)。 +/data/* +!/data/.htaccess +!/data/index.html /uploads/icons/* !/uploads/icons/.gitkeep /uploads/site/* diff --git a/data/.htaccess b/data/.htaccess new file mode 100644 index 0000000..5d8319d --- /dev/null +++ b/data/.htaccess @@ -0,0 +1,10 @@ +# 禁止 Web 访问 data 目录(SQLite 数据库等敏感文件) +# ⚠️ 这个文件必须进仓库:nav.db 里含管理员密码哈希与全部站点数据, +# 一旦能被公网下载,等于整站失守。 + + Require all denied + + + Order allow,deny + Deny from all + diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..d947d8e --- /dev/null +++ b/data/index.html @@ -0,0 +1 @@ +404 diff --git a/install.php b/install.php new file mode 100755 index 0000000..6bcecae --- /dev/null +++ b/install.php @@ -0,0 +1,311 @@ + */ +function sn_env_checks(): array +{ + $c = []; + $add = static function (string $name, bool $ok, string $detail, bool $required = true) use (&$c): void { + $c[] = ['name' => $name, 'ok' => $ok, 'detail' => $detail, 'required' => $required]; + }; + + $add('PHP 版本 ≥ 8.0', PHP_VERSION_ID >= 80000, '当前 ' . PHP_VERSION . '(' . PHP_SAPI . ')'); + $add('PDO SQLite 扩展', extension_loaded('pdo_sqlite'), + extension_loaded('pdo_sqlite') ? '已启用' : '未启用,无法建库(必需)'); + $add('mbstring 扩展', extension_loaded('mbstring'), + extension_loaded('mbstring') ? '已启用' : '未启用,中文长度校验会失效(必需)'); + $add('GD 扩展', extension_loaded('gd'), + extension_loaded('gd') ? '已启用' : '未启用,无法抓取/缩放网站图标;仍可安装,但图标只能显示首字母块', false); + $add('网络抓取能力', function_exists('curl_init') || (bool)ini_get('allow_url_fopen'), + function_exists('curl_init') ? 'curl 可用' : ((bool)ini_get('allow_url_fopen') ? '仅 allow_url_fopen' : '都没有,无法自动抓取网站名称/图标(建议开启)')); + $add('fileinfo 扩展', extension_loaded('fileinfo'), + extension_loaded('fileinfo') ? '已启用' : '未启用,自定义图标的类型识别会退化'); + + $dirs = [ + DATA_PATH => '数据目录 data/(放 SQLite 数据库)', + ICON_PATH => '网站图标目录 uploads/icons/', + SITE_ICON_PATH => '站点图标目录 uploads/site/', + ]; + foreach ($dirs as $dir => $label) { + $rel = ltrim(str_replace(APP_PATH, '', $dir), '/\\'); + $add($label, sn_dir_writable($dir), sn_dir_writable($dir) ? ($rel . ' 可写') : ($rel . ' 不可写,请给 Web 用户写权限')); + } + + // 这两项只提醒,不阻断安装:即使漏了也能装,但数据文件会被公网下载 + $dataHt = is_file(DATA_PATH . '/.htaccess'); + $add('data/ 已禁止 Web 访问', $dataHt, + $dataHt ? 'data/.htaccess 存在' : '缺少 data/.htaccess,数据库可能被直接下载(强烈建议补上)', false); + + return $c; +} + +$checks = sn_env_checks(); +$envOk = true; +$warnOnly = []; +foreach ($checks as $ck) { + if (!$ck['ok']) { + if ($ck['required']) { + $envOk = false; + } else { + $warnOnly[] = $ck['name']; + } + } +} + +/* ========================================================================= + * 安装状态 + * ========================================================================= */ + +$lockFile = DATA_PATH . '/install.lock'; +$dbExists = is_file(DB_PATH); +$hasAdmin = false; +if ($dbExists) { + try { + $hasAdmin = (int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn() > 0; + } catch (Throwable $e) { + // 库文件存在但读不出来(损坏 / 不可写)—— 环境检查会报,这里不重复喊 + } +} +$installed = is_file($lockFile) || $hasAdmin; + +$err = ''; +$done = false; + +/* ========================================================================= + * 执行安装 + * ========================================================================= */ +if (!$installed && $_SERVER['REQUEST_METHOD'] === 'POST') { + $siteName = trim((string)($_POST['site_name'] ?? '')); + $u = trim((string)($_POST['username'] ?? '')); + $p = (string)($_POST['password'] ?? ''); + $p2 = (string)($_POST['password2'] ?? ''); + + if (!$envOk) { + $err = '环境检查未全部通过,请先解决上面标红的问题'; + } elseif ($u === '' || $p === '') { + $err = '用户名和密码都不能为空'; + } elseif (!preg_match('/^[A-Za-z0-9_.\-]{2,32}$/', $u)) { + $err = '用户名只能用字母、数字、下划线、点、连字符,长度 2~32'; + } elseif (mb_strlen($p) < 6) { + $err = '密码至少 6 位(建议 8 位以上,混合大小写和数字)'; + } elseif ($p !== $p2) { + $err = '两次输入的密码不一致'; + } else { + try { + $pdo = db(); // 触发建表 + 写入默认设置/搜索引擎/示例板块 + + // 双保险:并发下第二个请求别重复创建 + if ((int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn() > 0) { + throw new RuntimeException('已有管理员账号,安装已中止。请直接登录。'); + } + + $st = $pdo->prepare('INSERT INTO users (username, password_hash, created_at) VALUES (?,?,?)'); + $st->execute([$u, password_hash($p, PASSWORD_DEFAULT), date('Y-m-d H:i:s')]); + + if ($siteName !== '') { + $pdo->prepare('INSERT INTO settings (k,v) VALUES (?,?) ON CONFLICT(k) DO UPDATE SET v=excluded.v') + ->execute(['site_name', $siteName]); + } + + // 锁文件:写不进去不算致命(已有管理员账号时本页本来就拒绝执行) + @file_put_contents($lockFile, date('c') . " installed\n"); + + if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); + } + $_SESSION['uid'] = (int)$pdo->lastInsertId(); + $done = true; + } catch (Throwable $e) { + $err = '安装失败:' . $e->getMessage(); + } + } +} + +/* 装成功后统计一下库的信息(给用户看,也证明库真的建好了) */ +$dbInfo = null; +if ($done || $installed) { + try { + $pdo = db(); + $cnt = static function (string $t) use ($pdo): int { + return (int)$pdo->query("SELECT COUNT(*) FROM {$t}")->fetchColumn(); + }; + $dbInfo = [ + 'path' => DB_PATH, + 'size' => is_file(DB_PATH) ? (int)filesize(DB_PATH) : 0, + 'groups' => $cnt('groups'), + 'sites' => $cnt('sites'), + 'engines' => $cnt('engines'), + 'users' => $cnt('users'), + ]; + } catch (Throwable $e) { + $dbInfo = null; + } +} + +$title = $installed ? '已安装' : '安装向导'; +?> + + + + +<?= e($title) ?> · <?= e(APP_NAME) ?> + + + + + +
+ +
+ + \ No newline at end of file