From 27187b38f1da9b9f0765637b7de77fdb2472e4b0 Mon Sep 17 00:00:00 2001 From: laoyang Date: Mon, 28 Sep 2026 22:53:55 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B5=E8=84=9A=E5=85=A5=E5=8F=A3=E4=B8=8E?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E5=90=91=E5=AF=BC=EF=BC=9B=E5=8A=A0=E5=9B=BA?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E5=8F=A3=E4=BB=A4=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 需求落实: 1. 页脚常驻「管理后台」链接,不用再手敲 admin.php。 2. 页脚「由 FeedLite 驱动」中的 FeedLite 改为超链接(指向开源仓库); 后台新增「页脚」设置组,可关闭整句(关闭后只留管理后台链接),也可改链接地址。 3. 管理员口令存储方式在后台如实展示:数据库 settings.admin.password_hash, 存 bcrypt 单向哈希(不可逆),明文不落盘。 4. 新增安装向导 install.php:data/ 下没有数据库时自动 302 引导; 体检环境 -> 填站点信息与口令 -> 生成 config.php -> 建库建表 -> 完成。 顺带修掉一个真实安全隐患: config.php 里的出厂占位口令 CHANGE_ME_admin 原先可被当作有效口令登录后台 (等于公开仓库里就写着线上后门口令)。现改为占位/空值一律拒绝, 未设口令时后台明确提示去安装向导,不再含糊报「口令不正确」。 实现要点: - 新增 fb_cfg_file():只读配置文件、不叠加 settings 表、绝不触达数据库。 「是否已安装」以数据库文件是否存在为判据,而 fb_cfg() 读 settings 会触发 fb_db() 就地建库 —— 那会把安装态自己造出来,所以判定必须走不碰库的通道。 - bootstrap 只在已安装时建库;未安装则 302 到 install.php(CLI 下报错退出)。 - fb_settings_flat() 在未安装时直接返回空,杜绝任何隐式建库。 - install.php 用文本级替换生成 config.php,保留全部注释与 OSS/SMTP 段落; 生成的 PHP 用 token_get_all(TOKEN_PARSE) 校验后才落盘。 - 页脚链接只放行 http/https,后台保存时校验(防 javascript: 伪协议)。 - style.css 页脚行改 flex,版本号 v6 -> v7。 验证: - php -l 全量通过;现有回归 25/25、附件专项 13/13 保持通过。 - 安装向导专项 60/60(含「明文口令未写入任何文件」「数据库字节里搜不到明文」)。 - 反向验证:撤销三处修复后对应断言确实失败(断言非装饰)。 - 几何检查:本机 15/15 + 页脚专项全过;线上真实 URL 15/15 + 页脚专项全过。 - 线上真机冒烟 22/22。 --- README.md | 74 ++++++++- admin.php | 87 +++++++++-- assets/style.css | 3 + config.example.php | 22 ++- install.php | 371 +++++++++++++++++++++++++++++++++++++++++++++ lib/bootstrap.php | 62 ++++++-- lib/layout.php | 18 ++- lib/util.php | 56 ++++++- 8 files changed, 654 insertions(+), 39 deletions(-) create mode 100644 install.php diff --git a/README.md b/README.md index aeb3720..0d0cf12 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ feedback/ ├─ thread.php 问题详情 + 回复(含"官方答复"与状态管理、附件展示) ├─ new.php 发布新问题(含图片 / 视频上传) ├─ admin.php 管理后台(口令登录:审核 / 管理全部问题 / 站点设置 / 操作日志) +├─ install.php 安装向导(数据库不存在时自动引导进来;装完建议删除) ├─ upload.php 附件上传接口(两步:领对象键 → 收文件本体,local 落盘 / OSS 代理 PUT) ├─ media.php 附件代理输出(私有桶 + 内网 Endpoint 下图片/视频的唯一对外读取通道,带鉴权与 Range) ├─ captcha.php "换一题"用的人机验证刷新接口(返回 JSON) @@ -60,8 +61,9 @@ feedback/ chown -R www:www /www/wwwroot/你的域名/feedback chmod -R 755 /www/wwwroot/你的域名/feedback/data ``` -4. 首次访问 `https://你的域名/feedback/` 会自动建库建表。 -5. **打开 `config.php`,修改这三项**(务必!): +4. 首次访问 `https://你的域名/feedback/` —— 此时 `data/` 下还没有数据库, + 程序会**自动把你送到安装向导 `install.php`**(见 **第二节之二**)。按向导填完即装好。 +5. 若你更愿意手工配置:从 `config.example.php` 复制一份为 `config.php`,改掉这三项后刷新页面即可建库。 - `admin_password` —— 管理员口令(回复时填入即以「官方」身份答复;也是审核后台 `admin.php` 的登录口令) - `api_token` —— 读取接口的令牌(自己设一串随机字符) - `ip_salt` —— 随机字符串(用于给访客 IP 做哈希) @@ -76,11 +78,50 @@ feedback/ --- +## 二之二、安装向导(`install.php`) + +**什么时候出现**:只要 `data/` 下还没有数据库文件,访问站点的任何页面都会 302 到 `install.php`。 +反过来,数据库一旦存在,向导就只显示「已经装好了」,**不会**覆盖任何数据。 + +向导分三步,一屏走完: + +1. **环境体检** —— PHP 版本(≥7.4)、必需扩展(`pdo_sqlite` / `openssl`)、 + `站点根 / data / uploads` 目录可写性、`config.example.php` 是否在。有任一项不过就拒绝提交。 +2. **填写站点信息** —— 站点标题、站点描述、官方邮箱、站点根地址(后两项可留空)。 +3. **设置管理员口令** —— 至少 8 位;也可以留空、装完后在后台补设(**补设之前后台登不进去**)。 + +提交后向导会: + +- 生成 `config.php`(在 `config.example.php` 的**文本**上逐项替换,注释与 OSS / SMTP 段落原样保留); +- 自动填好 `api_token` 与 `ip_salt`(随机串,不用你操心); +- 建库建表; +- 把口令以 **bcrypt 哈希**写入数据库 `settings` 表 —— **明文不进 `config.php`、不进任何文件**。 + +**装完请做两件事**: + +1. **删掉 `install.php`**(已安装态下它会拒绝重装,但少一个入口少一分风险)。 +2. 若口令留空了,进后台「站点设置 → 登录口令」补设一个。 + +> 想重装(例如换了数据库位置):先手工移走 `data/feedback.db`,再刷新即可。 +> 向导**不会**替你删除任何数据。 + +--- + ## 三、首页加链接 已在官网 `index.html` 的**顶部导航**与**页脚「产品」列**加入「问题反馈」入口,指向 `feedback/`。 (本地文件:`(你的站点首页)`) +### 页脚里有「管理后台」入口 + +站点页脚固定显示一个「管理后台」链接(指向 `admin.php`),不需要手动在地址栏敲文件名。 +旁边那句「由 FeedLite 驱动」可以关掉: + +- 后台「**站点设置 → 页脚**」→ 取消勾选「在页脚显示『由 FeedLite 驱动』」 + → 页脚只剩「管理后台」链接; +- 同一处还能改「FeedLite」二字指向的地址(默认是本项目开源仓库)。 + 只接受 `http://` / `https://` 开头的网址。 + --- ## 四、邮件通知(SMTP) @@ -566,23 +607,42 @@ curl "https://你的域名/feedback/api.php?token=XXX&action=digest&status=open" - `data/` 目录用 `.htaccess` 拒绝一切直接访问;请勿删除该文件。 ### 更换管理员口令 / 令牌 -直接修改 `config.php` 即可,无需改代码。 + +- **管理员口令**:登录后台后在「站点设置 → 登录口令」里改(至少 8 位)。改完即以 bcrypt 哈希存进数据库,`config.php` 里的 `admin_password` 只是「未设置」时的初始值通道。 +- **令牌 `api_token` / 盐 `ip_salt`**:这两项没有后台界面,改 `config.php` 即可,无需改代码。 + +### 口令存在哪、是否加密(重要) + +| 状态 | 存在哪 | 是否加密 | 怎么变成下一档 | +|---|---|---|---| +| 刚 clone 下来、没装 | `config.php` 的 `admin_password` = `CHANGE_ME_admin` | 明文,但**该值已被程序明确禁用**,无法登录 | 跑 `install.php`,或到后台设定口令 | +| 手工在 `config.php` 指定了口令 | `config.php` 的 `admin_password` | **明文**(配置文件里) | 登录一次后到后台改一次口令 → 转为数据库哈希 | +| 正常状态(推荐) | 数据库 `settings` 表的 `admin.password_hash` | **是**,bcrypt 单向哈希(`$2y$…`),不可逆 | 已是最终形态 | + +- 后台「站点设置 → 登录口令」会把当前处于哪一档**如实显示出来**,Linux 下也能直接查库确认。 +- 明文口令**永远不会**由安装向导或后台写进任何文件:向导只把哈希写进数据库。 +- 怀疑口令泄露:到后台改掉即可。若把数据库里 `admin.password_hash` 这一行删掉, + 会退回使用 `config.php` 的值(也因此回到明文档,建议删完立刻重新设定)。 --- ## 七之二、`config.php` 里的三个"必改"参数是什么 -这三项都写在 `config.php` 的「安全」段落里,出厂值都是 `CHANGE_ME_...` 占位符,**上线前必须换成自己的随机值**。 +> **走安装向导的话,这一段可以跳过**:`install.php` 会替你生成 `config.php`, +> 并自动填好 `api_token` 与 `ip_salt`(都是随机串),管理员口令则直接以哈希入库、不进文件。 +> 下面这些是「想手工配置」时看的说明。三项出厂值都是 `CHANGE_ME_...` 占位符。 -### `admin_password` —— 登录口令(初始值) +### `admin_password` —— 登录口令(初始值,可留空) -- **干什么用**:两个地方共用同一串,所以必须一致 —— +- **干什么用**:两个地方共用同一串 —— ① 详情页回复框里填它 → 以「官方」身份发言; ② `/feedback/admin.php` 登录 → 进管理后台(审核、回复、改状态、删帖、改站点设置)。 -- **建议**:**登录后台后在「站点设置 → 修改登录口令」里改成你自己的一串**(至少 8 位)。 +- **建议**:优先用安装向导;若手工配,**登录后台后立刻在「站点设置 → 登录口令」里改成你自己的一串**(至少 8 位)。 改完会以 `password_hash()` 存进数据库,`config.php` 里这行就只是"出厂默认值"了 —— ⚠️ 此时改 `config.php` **不会**再影响登录口令,忘了新口令就去数据库把 `settings` 表里 `admin.password_hash` 这一行删掉,即可退回用 `config.php` 里的值。 +- ⚠️ **占位值 `CHANGE_ME_admin` 已被程序明确禁用**(空值同样):也就是说,**clone 下来不做任何配置时, + 后台任何人都登不进去** —— 这是刻意的,避免"文档里公开的默认口令"变成线上后门。 - **它不是什么**:站点面向访客没有账号体系,也没有访客登录页。它只是一串比对用的口令 —— 表单里填的内容与库里的哈希(或 `config.php` 里的值)比对一次,一致就按官方处理。 diff --git a/admin.php b/admin.php index d7baed8..178cf08 100644 --- a/admin.php +++ b/admin.php @@ -30,6 +30,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) { $loginErr = '会话已过期,请重试。'; } elseif (!fb_rate_check('admin_login', 10)) { $loginErr = '尝试次数过多,请一小时后再试。'; + } elseif (!fb_admin_password_is_set()) { + $loginErr = '本系统还没有设置登录口令,请先运行安装向导(install.php)完成设置。'; } elseif ($pw !== '' && fb_admin_verify_password($pw)) { fb_admin_login(); fb_mod_log('login'); @@ -46,7 +48,10 @@ if (!fb_admin_logged_in()) {
← 返回问题列表

管理登录

-

请输入管理员口令(初始口令在 config.php 的 admin_password,登录后在「站点设置」里可以改掉它)。

+

请输入管理员口令(当前仍是 config.php 里的 admin_password 明文,登录后请到「站点设置」改成加密存储)。

+ +
尚未设置登录口令,且出厂占位口令已被禁用。请运行 install.php 设定一个。
+
@@ -184,7 +189,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) { $old = (string)(isset($_POST['old_password']) ? $_POST['old_password'] : ''); $n1 = (string)(isset($_POST['new_password']) ? $_POST['new_password'] : ''); $n2 = (string)(isset($_POST['new_password2']) ? $_POST['new_password2'] : ''); - if (!fb_admin_verify_password($old)) { + // 从没设过口令(或仍是出厂占位值)时,跳过"当前口令"校验 —— 否则谁也进不来、又改不了 + $firstSet = !fb_admin_password_is_set(); + if (!$firstSet && !fb_admin_verify_password($old)) { $msg = '当前口令不正确。'; $msgType = 'error'; } elseif (fb_len($n1) < 8) { @@ -195,8 +202,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) { $msgType = 'error'; } else { fb_admin_set_password($n1); - fb_mod_log('password'); - $msg = '口令已修改,下次登录请使用新口令。'; + fb_mod_log('password', null, $firstSet ? '首次设定登录口令(bcrypt 哈希存库)' : '修改登录口令'); + $msg = $firstSet ? '口令已设定,下次登录请使用它。' : '口令已修改,下次登录请使用新口令。'; } } } @@ -223,6 +230,16 @@ function fb_admin_save_settings() { else { fb_set_setting($k, $v); } } + // 页脚:是否显示「由 FeedLite 驱动」这句版权,以及链接指向的开源仓库地址 + fb_set_setting('footer.show_credit', $post('footer_show_credit') ? 1 : 0); + $curl = trim((string)$post('footer_credit_url', '')); + // 只接受 http/https:这条会被直接渲染成 ,不能让 javascript:/data: 混进来 + if ($curl !== '' && !preg_match('~^https?://~i', $curl)) { + return '页脚链接地址需以 http:// 或 https:// 开头。'; + } + if ($curl === '' || $curl === fb_credit_url_default()) { fb_unset_setting('footer.credit_url'); } + else { fb_set_setting('footer.credit_url', $curl); } + $ints = [ 'upload.image_max_per_post' => [0, 20], 'upload.video_max_per_post' => [0, 10], @@ -544,6 +561,14 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']); placeholder="欢迎在这里反馈使用问题、提出功能建议。"> 显示在首页标题下方,一句话说明这里可以反馈什么。两项都留空则恢复为出厂默认。 +

页脚

+ + +

附件存储

+ +
+ 存储位置: + + 数据库 settings 表(键名 admin.password_hash) + + config.php 的 admin_password + + 尚未设置 + + + 是否加密: + + 是 —— 存的是 password_hash() 生成的 bcrypt 单向哈希($2y$…), + 不可逆、无法还原明文;校验用 password_verify()。 + + 否 —— 目前是明文写在 config.php 里。 + 在下面改一次口令后即会转存为数据库中的 bcrypt 哈希。 + + —— 还没有可用口令,当前后台无法登录。请用下方表单设定一个。 + + + + 数据库文件在 data/feedback.db(已由 .htaccess 拒绝外部访问),且只存哈希,明文不落盘。 + +
+
+ +
-
- - - -
+ +
+ + + +
+ +
+ + +
+
- +
-

新口令保存在数据库里,优先级高于 config.php 的 admin_password;改完立即生效。

+

新口令以 bcrypt 哈希写入数据库,优先级高于 config.php 的 admin_password;改完立即生效,明文不会留在任何文件里。

+
'', - /* ---------------- 安全 ---------------- */ - // 管理员口令:在"回复"表单里填入它,即以「官方」身份回复(高亮显示),并触发邮件通知。 - // ★★ 请务必修改! ★★ + /* ---------------- 页脚 ---------------- + * 页脚永远会显示「管理后台」入口;这里的开关只控制右侧那句版权说明。 + * show_credit = false:页脚不再出现「由 FeedLite 驱动」,只留管理后台链接。 + */ + 'footer' => [ + 'show_credit' => true, + 'credit_url' => 'https://code.yangyq.net/laoyang/FeedLite', // 页脚「FeedLite」二字指向的地址 + ], + + /* ---------------- 安全 ---------------- + * 管理员口令的存放规则(重要): + * · 正常安装(走 install.php,或登录后台改口令)→ 口令以 **bcrypt 单向哈希** + * 存在数据库 settings 表的 admin.password_hash,**明文不落盘**。 + * · 下面这个 admin_password 只是「初始值」通道,且**出厂占位值已被程序明确禁用**: + * 值仍为 CHANGE_ME_admin 时,任何人(包括你)都无法用它登录后台。 + * · 想手工指定初始口令,就把下面这行改成你自己的口令;登录一次后建议立即改掉, + * 改完即转为数据库哈希存储、这里可以再改回占位值。 + */ + // ★★ 留空或保持占位值 = 未设置口令(后台不可登录,需先跑安装向导)★★ 'admin_password' => 'CHANGE_ME_admin', // API 令牌:供自动化脚本 / AI 读取数据用。★ 请务必修改 ★ diff --git a/install.php b/install.php new file mode 100644 index 0000000..3ff7e1d --- /dev/null +++ b/install.php @@ -0,0 +1,371 @@ + 'SQLite 数据库(PDO)', 'openssl' => 'SMTP / OSS 加密通信']; + $opt = ['curl' => '访问 OSS(可选,没有会退回 PHP 流式请求)', 'mbstring' => '中文字数统计(可选)']; + + $dirs = [ + FB_ROOT => '站点根目录(要写入 config.php)', + FB_ROOT . '/data' => '数据库目录 data/', + FB_ROOT . '/uploads' => '本机附件目录 uploads/(本机存储模式用)', + ]; + + $rows = []; + $ok = true; + + $verOk = version_compare(PHP_VERSION, '7.4.0', '>='); + $rows[] = ['PHP 版本', PHP_VERSION, $verOk, '需要 7.4 及以上(推荐 8.0~8.3)', $verOk ? '' : '当前版本过低,请切换站点的 PHP 版本']; + if (!$verOk) { $ok = false; } + + foreach ($need as $ext => $why) { + $has = extension_loaded($ext); + if (!$has) { $ok = false; } + $rows[] = ['PHP 扩展:' . $ext, $has ? '已启用' : '未启用', $has, $why, $has ? '' : ('请在 PHP 设置里开启 ' . $ext . ' 扩展')]; + } + foreach ($opt as $ext => $why) { + $has = extension_loaded($ext); + $rows[] = ['PHP 扩展:' . $ext, $has ? '已启用' : '未启用(不影响安装)', true, $why, '']; + } + + foreach ($dirs as $d => $why) { + if (!is_dir($d)) { @mkdir($d, 0755, true); } + $w = is_dir($d) && is_writable($d); + if (!$w) { $ok = false; } + $shown = ($d === FB_ROOT) ? '(站点根)' : str_replace(FB_ROOT, '', $d); + $rows[] = ['目录可写 ' . $shown, $w ? '可写' : '不可写', $w, $why, $w ? '' : ('请给 Web 用户写权限,例如 chown www:www ' . $d)]; + } + + // config.php 若已存在(例如从模板复制过),必须可写才能就地更新 + if (is_file(FB_ROOT . '/config.php')) { + $w = is_writable(FB_ROOT . '/config.php'); + if (!$w) { $ok = false; } + $rows[] = ['config.php 可写', $w ? '可写' : '不可写', $w, '需要就地更新配置', $w ? '' : '请给该文件写权限,例如 chown www:www config.php']; + } else { + $rows[] = ['config.php', '尚不存在(将自动生成)', true, '由安装向导创建', '']; + } + + $ex = is_file(FB_ROOT . '/config.example.php'); + $rows[] = ['配置模板 config.example.php', $ex ? '存在' : '缺失', $ex, '生成 config.php 的底稿', $ex ? '' : '仓库文件不完整,请重新获取源码']; + if (!$ex) { $ok = false; } + + return [$ok, $rows]; +} + +/* ================================================================== + * 生成 config.php(在模板文本上逐项替换,保留注释与其它段落) + * ================================================================== */ +function fb_install_write_config(array $v) { + $example = FB_ROOT . '/config.example.php'; + $src = (string)@file_get_contents($example); + if ($src === '') { return '读不到 config.example.php。'; } + + /** 把顶层键(含引号形式)整项替换成新值;找不到就返回原串由调用方兜底 */ + $setTop = function ($key, $valueExpr, $txt) { + $pat = "/^([ \t]*'" . preg_quote($key, '/') . "'[ \t]*=>[ \t]*)(.*?)(,[ \t]*)(\r?\n)/m"; + $rep = '$1' . $valueExpr . '$3$4'; + return preg_replace($pat, $rep, $txt, 1); + }; + $phpStr = function ($s) { return "'" . str_replace(["\\", "'"], ["\\\\", "\\'"], (string)$s) . "'"; }; + + $dbPath = str_replace('\\', '/', FB_ROOT . '/data/feedback.db'); + + $src = $setTop('db_path', "'" . $dbPath . "'", $src); + $src = $setTop('site_name', $phpStr($v['site_name']), $src); + $src = $setTop('site_desc', $phpStr($v['site_desc']), $src); + $src = $setTop('admin_email', $phpStr($v['admin_email']), $src); + $src = $setTop('base_url', $phpStr($v['base_url']), $src); + // 口令不写进文件:此处只把残留的旧明文重置为占位值(正常都是占位值,等价于不动) + $src = $setTop('admin_password', "'CHANGE_ME_admin'", $src); + $src = $setTop('api_token', $phpStr($v['api_token']), $src); + $src = $setTop('ip_salt', $phpStr($v['ip_salt']), $src); + + // 兜底校验:生成的文本必须是可解析的 PHP。 + // 注意必须带 TOKEN_PARSE —— 不加这一位 token_get_all 只做词法切分、不报语法错,等于没校验。 + $syntaxOk = true; + if (defined('TOKEN_PARSE') && function_exists('token_get_all')) { + try { @token_get_all($src, TOKEN_PARSE); } + catch (Throwable $e) { $syntaxOk = false; } + } + if (!$syntaxOk) { return '生成的配置内容语法不正确,已放弃写入(请把 config.php 恢复为模板后再试)。'; } + + $stamp = date('Y-m-d H:i:s'); + $header = " 80) { + $err = '站点标题请填写 1~80 个字符。'; + } elseif ($admEmail !== '' && !filter_var($admEmail, FILTER_VALIDATE_EMAIL)) { + $err = '官方邮箱格式不正确(也可以留空)。'; + } elseif ($baseUrl !== '' && !preg_match('~^https?://~i', $baseUrl)) { + $err = '站点根地址需要以 http:// 或 https:// 开头(留空则自动探测)。'; + } elseif ($setPw && fb_len($pw) < 8) { + $err = '管理员口令至少 8 个字符。'; + } elseif ($setPw && $pw !== $pw2) { + $err = '两次输入的口令不一致。'; + } else { + // ① 先建库建表 —— 后面写设置才有地方写 + $pdo = fb_db(); + + // ② 写 config.php(口令不入文件) + $cfgErr = fb_install_write_config([ + 'site_name' => $siteName, + 'site_desc' => ($siteDesc !== '' ? $siteDesc : (string)fb_cfg('site_desc', '')), + 'admin_email' => $admEmail, + 'base_url' => $baseUrl, + 'api_token' => fb_install_rand(16), + 'ip_salt' => fb_install_rand(16), + ]); + + if ($cfgErr === '') { + fb_cfg_flush(); // 让后续取值看到刚落盘的新 config.php + } else { + // config.php 写不进去不算致命:settings 表里的值同样生效,先把系统跑起来并如实告知 + $err = '提示:' . $cfgErr . '(站内设置已写入数据库,系统可用;但建议修好后手工补齐 config.php)'; + } + + // ③ 站点信息入库(优先生效,且不依赖 config.php 是否可写) + fb_set_setting('site_name', $siteName); + if ($siteDesc !== '') { fb_set_setting('site_desc', $siteDesc); } + fb_set_setting('installed_at', time()); + + // ④ 口令:只写 bcrypt 哈希,绝不落盘明文 + $pwSet = false; + if ($setPw && fb_len($pw) >= 8 && $pw === $pw2) { + $pwSet = fb_admin_set_password($pw); + } + + $done = true; + $GLOBALS['__fb_install_pw_set'] = $pwSet; + } +} + +?> + + + + + + +安装 · 轻反馈 FeedLite + + + + + + +
+
+ + + 轻反馈 FeedLite + +
+ 安装向导 +
+
+
+ + +
+

已经装好了

+

检测到数据库已存在,无需重复安装。

+

+ 进入问题列表 + 管理后台 +

+
+

如果你确实想重装(例如换了数据库位置),请先手动移走 + data/feedback.db 再刷新本页。本向导不会替你删除任何数据。

+
+
+ + + +
+

安装完成 🎉

+

已生成 config.php、建好数据库表。下面是接下来要做的两件事:

+ +
+
+ ① 删除安装文件 —— 请把这个 install.php 从服务器上删掉(已安装态下它会拒绝重装,但删掉更干净)。 + ② 设置口令 —— + + 管理员口令已设成你刚才填的那个,并以 bcrypt 哈希存进数据库;config.php 里没有任何明文口令。 + + 你这次没设口令。在设好之前后台无法登录:请进入 管理后台,在「站点设置 → 登录口令」里设定(或重新运行本向导)。 + + +
+
+ + + +

+ 进入问题列表 + 管理后台 +

+
+ + + + +
+
+

安装向导

+

数据库目录 data/ 下还没有数据,先完成这几步设置,装完即可使用。

+
+
+ + + + + +

一、环境体检

+
+
+ + + + : + () +
↳ +
+ +
+
+ +

二、填写站点信息

+ +
+ + + + + + +
+ + +
+ +

三、设置管理员口令

+

+ 口令用来登录管理后台、以及以「官方」身份回复。它以 bcrypt 单向哈希写入数据库,明文不会保存在 config.php 或任何文件里。 + 留空则暂不设定,装完后可在后台补设(补设之前后台不可登录)。 +

+
+ + +
+ + +
+ +
+ +

环境体检未通过前无法提交,请先处理标红项。

+ +
+ +
+

安装后再删掉本文件即可。
+ 邮件通知(SMTP)与附件上传(阿里云 OSS)都在安装完成后、在后台「站点设置」里配置,这里不需要填。

+
+ + +
+ + + \ No newline at end of file diff --git a/lib/bootstrap.php b/lib/bootstrap.php index 23d3d21..7e62235 100644 --- a/lib/bootstrap.php +++ b/lib/bootstrap.php @@ -29,6 +29,30 @@ if (!isset($GLOBALS['__fb_cfg_stamp'])) { } if (!function_exists('fb_cfg')) { + /** + * 只读【配置文件】的值(config.php → config.example.php), + * **不叠加 settings 表、绝不触达数据库**。 + * + * 为什么必须单独有这么一条通道:正常的 fb_cfg() 会去读 settings 表, + * 而读表要先 fb_db() —— 那一步会就地建出一个空数据库文件。 + * 「是否已安装」的判定恰恰以「数据库文件在不在」为依据, + * 所以判定必须走这条不碰库的路径,否则第一秒就把安装态自己造出来了。 + */ + function fb_cfg_file() { + static $file = null; + static $stamp = -1; + $now = isset($GLOBALS['__fb_cfg_stamp']) ? (int)$GLOBALS['__fb_cfg_stamp'] : 0; + if ($file === null || $stamp !== $now) { + $local = FB_ROOT . '/config.php'; + $example = FB_ROOT . '/config.example.php'; + if (is_file($local)) { $file = (array)(require $local); } + elseif (is_file($example)) { $file = (array)(require $example); } + else { $file = []; } + $stamp = $now; + } + return $file; + } + /** * 读取配置(点分路径,如 fb_cfg('smtp.host')) * 取值顺序:settings 表(后台改过的)> config.php(本地配置)> config.example.php(出厂默认)> $default @@ -41,15 +65,7 @@ if (!function_exists('fb_cfg')) { static $stamp = -1; $now = isset($GLOBALS['__fb_cfg_stamp']) ? (int)$GLOBALS['__fb_cfg_stamp'] : 0; if ($merged === null || $stamp !== $now) { - $local = FB_ROOT . '/config.php'; - $example = FB_ROOT . '/config.example.php'; - if (is_file($local)) { - $cfg = (array)(require $local); - } elseif (is_file($example)) { - $cfg = (array)(require $example); - } else { - $cfg = []; - } + $cfg = fb_cfg_file(); // 后台设置以嵌套数组形式叠加在文件配置之上(只覆盖同名键) $tree = function_exists('fb_setting_tree') ? fb_setting_tree() : []; $merged = is_array($tree) && $tree ? array_replace_recursive($cfg, $tree) : $cfg; @@ -68,6 +84,11 @@ if (!function_exists('fb_cfg')) { } } +/** 让 fb_cfg() 的进程内缓存失效(改了磁盘上的 config.php 后必须调用) */ +function fb_cfg_flush() { + $GLOBALS['__fb_cfg_stamp'] = ((int)$GLOBALS['__fb_cfg_stamp']) + 1; +} + require_once FB_ROOT . '/lib/util.php'; require_once FB_ROOT . '/lib/db.php'; require_once FB_ROOT . '/lib/oss.php'; @@ -76,6 +97,20 @@ require_once FB_ROOT . '/lib/captcha.php'; require_once FB_ROOT . '/lib/mailer.php'; require_once FB_ROOT . '/lib/layout.php'; +/* ------------------------------------------------------------------ + * 安装引导:数据库还没生成(data/ 下没数据)就说明是全新部署, + * 一律先送去 install.php 填配置、建库、设口令,装完才进系统。 + * 判据只看「数据库文件在不在」——绝不能在这里调 fb_db(), + * 因为那会就地建出一个空库、把安装态自己"装"出来,之后就再也进不了向导。 + * ------------------------------------------------------------------ */ +if (!defined('FB_SKIP_INSTALL_REDIRECT') && !fb_installed()) { + if (PHP_SAPI === 'cli') { + fwrite(STDERR, "FeedLite 尚未安装:请先在浏览器中打开本目录下的 install.php 完成安装。\n"); + exit(1); + } + fb_redirect('install.php'); +} + // 会话(CLI 下不启用) if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) { $p = session_get_cookie_params(); @@ -91,5 +126,10 @@ if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) { @session_start(); } -// 初始化数据库(建表) -fb_db(); +// 初始化数据库(建表)。 +// ★ 只在【已安装】时做:未安装就建库,等于第一秒把这个状态自己抹掉, +// 安装向导(install.php)随后就会被判定成「已经装好了」而拒绝工作。 +// install.php 在真正提交安装时会自己显式调用 fb_db()。 +if (fb_installed()) { + fb_db(); +} diff --git a/lib/layout.php b/lib/layout.php index 3def926..b7542c4 100644 --- a/lib/layout.php +++ b/lib/layout.php @@ -27,7 +27,7 @@ function fb_layout_head($title, array $opt = []) { - +
@@ -48,10 +48,22 @@ function fb_layout_head($title, array $opt = []) { /** 输出页面底部 */ function fb_layout_foot() { - ?>