From 8763e8e17fa9c3e96d8825fdbea552634372b9d1 Mon Sep 17 00:00:00 2001 From: laoyang Date: Tue, 3 Nov 2026 10:02:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=9E=E5=A4=8D=E6=A1=86=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E5=8F=A3=E4=BB=A4=E4=B8=8E=E5=A4=84?= =?UTF-8?q?=E7=90=86=E7=8A=B6=E6=80=81=EF=BC=9B=E5=B8=A6=20token=20?= =?UTF-8?q?=E6=97=B6=E6=98=B5=E7=A7=B0/=E9=82=AE=E7=AE=B1=E9=94=81?= =?UTF-8?q?=E5=AE=9A=E4=B8=BA=E5=8F=AA=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户三个问题,本质是同一件事:token 就是身份凭证,那就不该再让表单声明身份。 一、去掉管理员口令 / 处理状态 用户:「回复这个地方,下面有个管理员口令,还有处理状态,都不要填了, 只保留回复内容,加上人机验证就可以了」 - 删掉表单里这两块,删掉 admin-pw/admin-status 的 CSS 与 bindAdminToggle 的 JS - 管理员身份只认后台登录态(fb_is_admin()),不再从公开表单收口令 - 状态流转只在后台详情页那排按钮里做 二、带 token 时昵称/邮箱锁定为只读 用户:「如果他带着 Token 进来之后邮箱又让他改了,我们又没办法知道谁是谁了」 - 有 token = 已证明是提交人本人,表单不再让填昵称/邮箱, 改为显示只读的「回复身份」块(原昵称 + 原邮箱) 三、服务端强制(关键) 🔴 前端设成 hidden 是不够的 —— hidden 可以被改: 谁拿到链接、改个 email 提交,这条反馈的联系人就变成他的了。 所以服务端在 isOwnerView 时**直接丢弃 POST 里的 name/email**, 一律以库里那条帖子的 author_name / author_email 为准。 身份校验必须在服务端做。 验证(这次确保请求真的通过人机验证,不是被验证码挡住造成的假通过): 题目「2 乘以 7」答 14 → 跳转到 #r3,回复确实入库; 提交的是 黑客改名 / attacker@evil.example, 实际入库 name=jervis email=zeussun@msn.com —— 伪造的两个字段都被丢弃。 另外:无 token 的普通访客仍可自由填昵称邮箱;全站已无「管理员口令」残留。 测试回复已清理,reply_count 已同步。 --- assets/app.js | 27 +-------- assets/style.css | 15 ++++- bootstrap.php | 145 ++++++++++++++++++++++++++++++++++++++++++++++ lib/bootstrap.php | 2 +- thread.php | 87 ++++++++++++++++------------ 5 files changed, 213 insertions(+), 63 deletions(-) create mode 100644 bootstrap.php diff --git a/assets/app.js b/assets/app.js index cafdfe3..3fd9d7c 100644 --- a/assets/app.js +++ b/assets/app.js @@ -61,29 +61,9 @@ } } - /* ---------- 管理员口令:填写后显示"处理状态",并解除邮箱必填 ---------- */ - function bindAdminToggle() { - var pw = document.querySelector('input[name=admin_password]'); - if (!pw) { return; } - var statusBox = document.querySelector('.admin-status'); - var emailInput = document.querySelector('input[name=email]'); - var emailField = document.getElementById('emailField'); - - function sync() { - var isAdmin = pw.value.trim() !== ''; - if (statusBox) { statusBox.hidden = !isAdmin; } - if (emailInput) { - emailInput.required = !isAdmin; - if (isAdmin) { emailInput.removeAttribute('required'); } - } - if (emailField) { - var req = emailField.querySelector('.req'); - if (req) { req.style.visibility = isAdmin ? 'hidden' : 'visible'; } - } - } - pw.addEventListener('input', sync); - sync(); - } + /* ---------- (已删)管理员口令切换 ---------- + * 回复表单里不再有「管理员口令」和「处理状态」——用户 2026-11-03 要求去掉。 + * 管理员身份改由后台登录态判断,状态流转在后台详情页的按钮里做。 */ /* ---------- 防止重复提交(上传未完成时阻止提交) ---------- */ function bindNoDoubleSubmit() { @@ -884,7 +864,6 @@ function boot() { bindCaptchaRefresh(); - bindAdminToggle(); bindWontfixPanel(); bindColorPicker(); bindIconPicker(); diff --git a/assets/style.css b/assets/style.css index 84395ab..4d3e220 100644 --- a/assets/style.css +++ b/assets/style.css @@ -455,6 +455,20 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh} .item-meta{display:flex; flex-wrap:wrap; align-items:center; gap:6px; font-size:13px; color:var(--muted)} .item-meta .who{color:#3a424d; font-weight:600} +/* 带 token 回复时展示只读身份(昵称 + 邮箱) */ +.reply-identity{ + display:flex; align-items:center; flex-wrap:wrap; gap:8px; + padding:9px 12px; border-radius:9px; + background:var(--bg, #f6f8fb); border:1px solid var(--line); + font-size:14px; +} +.reply-identity-nm{font-weight:600; color:var(--fg)} +.reply-identity-mail{ + color:var(--muted); font-size:13px; + padding:1px 8px; border-radius:99px; background:var(--card); + border:1px solid var(--line); +} + /* 提交人凭 token 看自己未公开反馈时的提示条 */ .owner-note{ display:flex; flex-direction:column; gap:4px; @@ -596,7 +610,6 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh} .form-actions{margin-top:20px} .form-actions .btn{padding:10px 26px; font-size:15px} -.admin-pw{margin-top:6px} .admin-status{margin-bottom:16px} /* 蜜罐字段:对用户不可见,但保留在 DOM 中诱捕机器人 */ diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..a3603f4 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,145 @@ + config.php(本地配置)> config.example.php(出厂默认)> $default + * + * config.php 不参与版本管理(见 .gitignore),新部署时可从 config.example.php 复制一份; + * 若不存在则直接以 config.example.php 为默认值运行,保证 clone 下来即可跑。 + */ + function fb_cfg($path = null, $default = null) { + static $merged = null; + static $stamp = -1; + $now = isset($GLOBALS['__fb_cfg_stamp']) ? (int)$GLOBALS['__fb_cfg_stamp'] : 0; + if ($merged === null || $stamp !== $now) { + $cfg = fb_cfg_file(); + // 后台设置以嵌套数组形式叠加在文件配置之上(只覆盖同名键) + $tree = function_exists('fb_setting_tree') ? fb_setting_tree() : []; + $merged = is_array($tree) && $tree ? array_replace_recursive($cfg, $tree) : $cfg; + $stamp = $now; + } + if ($path === null || $path === '') { return $merged; } + $cur = $merged; + foreach (explode('.', (string)$path) as $k) { + if (is_array($cur) && array_key_exists($k, $cur)) { + $cur = $cur[$k]; + } else { + return $default; + } + } + return $cur; + } +} + +/** 让 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'; +require_once FB_ROOT . '/lib/attach.php'; +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(); + $https = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off'); + @session_set_cookie_params([ + 'lifetime' => 0, + 'path' => !empty($p['path']) ? $p['path'] : '/', + 'domain' => '', + 'secure' => $https, + 'httponly' => true, + 'samesite' => 'Lax', + ]); + @session_start(); +} + +// 初始化数据库(建表)。 +// ★ 只在【已安装】时做:未安装就建库,等于第一秒把这个状态自己抹掉, +// 安装向导(install.php)随后就会被判定成「已经装好了」而拒绝工作。 +// install.php 在真正提交安装时会自己显式调用 fb_db()。 +if (fb_installed()) { + fb_db(); + // 顺手确保 uploads/ 目录与「禁止直读」规则在位且为最新版(local / oss 都需要)。 + // ⚠️ 为什么放在这里:老站点上早已存在的旧版 .htaccess(曾允许直读)不会自己更新, + // 而部署账号通常对 uploads/ 无写权限 —— 只有 PHP(目录属主)能改。 + // 函数内部只在「文件缺失或版本标记过期」时才写盘,命中后每次请求几乎零开销。 + if (function_exists('fb_local_ensure_dir')) { + fb_local_ensure_dir(); + } +} diff --git a/lib/bootstrap.php b/lib/bootstrap.php index 996c072..a3603f4 100644 --- a/lib/bootstrap.php +++ b/lib/bootstrap.php @@ -8,7 +8,7 @@ if (!defined('FB_ROOT')) { define('FB_ROOT', dirname(__DIR__)); // 静态资源版本号:改 assets/ 下的 css/js 后把这个数字 +1, // 让浏览器与 CDN 缓存失效。全站只此一处,别再往页面里写死数字。 - if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 30); } + if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 31); } } /* ------------------------------------------------------------------ diff --git a/thread.php b/thread.php index 0c7f209..82cc40c 100644 --- a/thread.php +++ b/thread.php @@ -61,7 +61,7 @@ if (!fb_thread_viewable($thread)) { exit; } -$statuses = fb_status_keys(); + $err = ''; $atts = []; $form = ['name' => '', 'email' => '', 'body' => '', 'captcha' => '']; @@ -81,14 +81,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $form['name'] = trim(isset($_POST['name']) ? (string)$_POST['name'] : ''); $form['email'] = trim(isset($_POST['email']) ? (string)$_POST['email'] : ''); $form['body'] = trim(isset($_POST['body']) ? (string)$_POST['body'] : ''); + + /* 🔴 带 token(= 提交人本人)时,身份以库里那条帖子为准,**不采信 POST**。 + * 前端已经把这两个字段设成 hidden 了,但 hidden 是可以被改的: + * 谁拿到链接、改个 email 提交,这条反馈的联系人就变成他的了。 + * 身份校验必须在服务端做,不能只靠前端不给输入框。 */ + if ($isOwnerView) { + $form['name'] = (string)$thread['author_name']; + $form['email'] = (string)$thread['author_email']; + } + $capInput = isset($_POST['captcha']) ? (string)$_POST['captcha'] : ''; - $adminPw = isset($_POST['admin_password']) ? (string)$_POST['admin_password'] : ''; $hp = trim(isset($_POST['website']) ? (string)$_POST['website'] : ''); $t0 = isset($_POST['t0']) ? (int)$_POST['t0'] : 0; list($attOk, $attList, $attErr) = fb_attach_parse(isset($_POST['attachments']) ? $_POST['attachments'] : ''); - $isAdmin = ($adminPw !== '' && fb_admin_verify_password($adminPw)); + /* 管理员身份只认「后台登录态」,不再从回复表单收口令。 + * 用户 2026-11-03:「管理员口令、处理状态都不要填了, + * 用户进来之后就没有这些东西,只保留回复内容 + 人机验证」。 + * 状态流转在后台详情页那排按钮里做,不该混在公开的回复框里。 */ + $isAdmin = fb_is_admin(); if (!fb_csrf_ok(isset($_POST['csrf']) ? $_POST['csrf'] : '')) { $err = '会话已过期,请重新提交。'; @@ -127,18 +140,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $replyId = (int)$pdo->lastInsertId(); fb_attach_save($pdo, $id, $replyId, $vList); - // 更新主题:回复数 +1、活跃时间;管理员可同时调整状态 - $newStatus = null; - if ($isAdmin && isset($_POST['status']) && isset($statuses[(string)$_POST['status']])) { - $newStatus = (string)$_POST['status']; - } - if ($newStatus !== null) { - $upd = $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ?, status = ? WHERE id = ?'); - $upd->execute([$now, $newStatus, $id]); - } else { - $upd = $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ? WHERE id = ?'); - $upd->execute([$now, $id]); - } + /* 更新主题:回复数 +1、活跃时间。 + * 回复框不再承担改状态的职责 —— 状态流转在后台详情页那排按钮里做。 */ + $upd = $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ? WHERE id = ?'); + $upd->execute([$now, $id]); fb_rate_hit('reply'); // 发送通知邮件给发帖人 @@ -252,14 +257,36 @@ fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);
- - + + +
+ 回复身份 +
+ + +
+ 这是您提交时用的身份。回复会以它显示,通知也会发到这个邮箱。 +
+ + + + + + +
- - - -