本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/待办事宜/FlintHub_invite插件_彻查报告.md。
邀请注册(invite)插件 — 彻查报告
日期:2026-10-05 | 范围:
plugins/invite/(15 文件 / 492 行)方式:只读(未改任何插件代码);对照核心
modern.css/Csrf/Plugin/Controller逐条验证当前状态:
plugins_cache.json里activated: false(未启用,plugins/invite/data/亦不存在)
一、结论摘要
| # | 严重度 | 问题 | 位置 |
|---|---|---|---|
| P1 | 🔴 高(用户点名) | 注册页注入的邀请码输入框未适配暗夜星辰 → 白底黑字 | assets/script.js:15 + assets/style.css:4 |
| P2 | 🟠 中高 | 邀请码大小写不对称 → 手输小写码被判「无效」 | hook/auth_register_before.php:10 |
| P3 | 🟠 中高 | 无 PRG → 生成后刷新页面会重复生成 + 重复扣积分 | InviteController.php:35-73 |
| P4 | 🟡 中 | 注册后标记邀请码缺 AND used_by_user_id IS NULL → 并发可覆盖 | hook/auth_register_after.php:15 |
| P5 | 🟡 中 | :focus / 必填星号硬编码色,未走主题变量 | assets/style.css:5,7 |
| P6 | 🟢 低 | 输入框圆角/内距与核心 .mn-input 不一致(6px/12px vs 8px/13px) | assets/style.css:4 |
| P7 | 🟢 低 | <div> 被注入 <head> 内(HTML 非法) | hook/layout_head_end.php:14 |
| P8 | 🟢 低 | 邀请码列表 LIMIT 10 无分页 | InviteController.php:80 |
| P9 | 🟢 低 | 版本号三处不一致(1.6.0 / 1.5.0 / 1.0.0) | plugin.json / 说明.md / 各 PHP 头 |
| P10 | ⚪ 极低 | 熵 32bit、usedIds 含 0、relatedId 传 0、服务端无上界、子串匹配 | 多处 |
二、P1 暗夜星辰适配(用户点名的问题)— 根因与证据
2.1 现象
注册页里,用户名 / 邮箱 / 密码 / 验证码框都是深色底浅字(随主题),
唯独邀请码框是白底黑字,在暗夜星辰下非常突兀。
2.2 证据链(三处代码)
① 插件注入的 input 是裸 <input>,没有任何 class —— assets/script.js:15
wrapper.innerHTML = '' + ... + ''
+ '';
// ↑ 没有 class="mn-input"② 插件 CSS 只补了边框,没补 background / color —— assets/style.css:4
.invite-field input { width: 100%; padding: 9px 12px; border: 1px solid var(--mn-border, #ddd); border-radius: 6px; font-size: 14px; box-sizing: border-box; }
/* ↑ 无 background ↑ 无 color */③ 核心对「裸 input」只设字体,暗色适配全靠 .mn-input 类
assets/css/modern.css:26:input, textarea, select, button { font-family: inherit; font-size: inherit; outline: none; }—— 仅字体assets/css/modern.css:1105-1114:
```css
.mn-input { width:100%; padding:9px 13px; border:1px solid var(--mn-border);
border-radius:8px; background:var(--mn-bg-input); color:var(--mn-text); font-size:14px; }
```
④ 对照核心注册页 —— app/Views/auth/register.php 里每个输入框都带 mn-input:
username / email / password / confirm_password / captcha 全部 class="mn-input"。
2.3 量化对照(探针实测)
=== 关键属性对照 ===
background | 核心 .mn-input: var(--mn-bg-input) | 插件 .invite-field input: (无)
color | 核心 .mn-input: var(--mn-text) | 插件 .invite-field input: (无)
border | 1px solid var(--mn-border) | 1px solid var(--mn-border, #ddd)
border-radius | 8px | 6px
padding | 9px 13px | 9px 12px
=== 暗夜星辰关键变量(modern-night.css)===
--mn-bg-input = #16203a ← 暗夜下输入框应有的底色
--mn-text = #e2e8f0
--mn-border = #2c3a4f→ 因插件 input 无 class 且未声明 background/color,最终回落浏览器 UA 默认(白底黑字)。
CSS 变量切换主题不会影响 UA 默认值 → 暗夜下恒为白框。
2.4 修复方案(推荐 A)
| 方案 | 改动 | 说明 |
|---|---|---|
| A(推荐) | script.js:15 的 <input ...> 加 class="mn-input" | 一行,直接复用核心暗色适配;.invite-field input(0,1,1) 未声明 background/color,不会压掉 .mn-input(0,1,0) 的这两条 |
| B | style.css:4 补 background: var(--mn-bg-input); color: var(--mn-text); | 不依赖 class,但两处样式源(核心/插件)易再分叉 |
建议 A + 顺手修 P5/P6(把插件 CSS 的圆角/内距对齐核心,
#667eea→var(--mn-primary)、#e74c3c→var(--mn-error))。
三、P2 邀请码大小写不对称(功能 bug)
- 生成(
InviteController.php:42):$code = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8));→ 存大写 - 校验(
hook/auth_register_before.php:10):$inviteCode = trim($_POST['invite_code'] ?? '');→ 原样查库 - 输入框(
script.js:15)也没有text-transform: uppercase
→ 用户手输小写邀请码(如 a1b2c3d4)→ WHERE code = 'a1b2c3d4' 查不到 → 提示「邀请码无效或已过期」。
→ 修复:校验前 $inviteCode = strtoupper(trim($_POST['invite_code'] ?? ''));(一行)
四、P3 无 PRG → 刷新重复扣积分
InviteController::index()在 POST 生成后直接渲染视图,不 redirect(:35-73→:98)。- 核心
Csrf::verify()不消费 Token(app/Helpers/Csrf.php:38-58明确注释「校验本身不轮换 Token 值」,仅滑动续期csrf_time)。 Csrf::token()也只在 30 分钟过期时轮换(:17-24)。
→ 用户点「生成邀请码」后按 F5 / 浏览器回退再前进 → 重发同一 POST → CSRF 仍通过 → 再生成一个邀请码 + 再扣一次积分。
→ 修复:POST 成功后 redirect('/invites'),成功消息放 $_SESSION flash(符合核心「表单走 PRG」约定)。
五、P4 注册后标记缺并发保护
hook/auth_register_after.php:15:
$db->prepare('UPDATE invites SET used_by_user_id = :uid, used_at = :now WHERE id = :id')- 缺
AND used_by_user_id IS NULL。 - 窗口:
auth_register_before(校验通过)→User::create()等 →auth_register_after(标记)。同码被两人同时注册时,后者会覆盖前者的used_by_user_id。
→ 修复:WHERE id = :id AND used_by_user_id IS NULL + 检查 rowCount()(=0 说明已被用,可记 error_log)。
六、已排除的怀疑项(彻查验证过、确认无问题)
| 项 | 结论 |
|---|---|
| i18n 键 | ✅ 三语各 26 键,集合零差异;代码引用 26 键 ↔ 语言包零缺键、零冗余 |
| 视图类名 | ✅ 41 个类名中 40 个核心存在;唯一 invite-code 是插件自定义类(在插件 style.css:9 定义) |
getData('currentUser'/'csrfToken'/'siteName') | ✅ 核心 Controller::view()(:23-41)自动注入,视图取值有效 |
auth_register_after 的 $user_id | ✅ Plugin::hook() 用 `extract($params, EXTR_SKIP\ |
| 表单匹配 | ✅ 表单 action = $this->url('/register') 与插件 data-action = BASE_PATH.'/register' 完全一致 |
Points::deduct 调用 | ✅ 签名 (int $userId, int $points, string $reason, int $relatedId=0, string $relatedType='') 匹配 |
| 图标 | ✅ icon('gift')、icon('users') 均存在(TemplateCompiler.php:697 等) |
| 视图路径 | ✅ view('plugins/invite/index') 由 resolveTemplate():67-74 解析到插件 views/ |
| 跨库事务 | ✅ 插件库 + 核心库两连接无法共事务,代码已用「扣分失败即抛错回滚邀请码」正确处理(注释详实) |
uninstall() | ✅ 只 DROP TABLE 符合核心「保留文件、插件自清数据」约定(Plugin.php:832) |
| XSS | ✅ script.js 的 innerHTML 数据来自 PHP 侧 htmlspecialchars(..., ENT_QUOTES) 转义后的 data-* |
| 卸载残留 | ✅ 无需删 data/(核心约定保留文件;且当前 data/ 不存在) |
七、修复建议(按优先级)—— ✅ 已于 2026-10-05 全部实施,见 §九
第一批(必做,4 处 / 3 文件)
assets/script.js:15— input 加class="mn-input"(修 P1)hook/auth_register_before.php:10— 校验前strtoupper()(修 P2)InviteController.php— POST 成功后redirect+ session flash(修 P3)hook/auth_register_after.php:15—UPDATE加AND used_by_user_id IS NULL+ 判rowCount()(修 P4)
第二批(视觉统一,1 文件)
assets/style.css— 圆角/内距对齐.mn-input;#667eea→var(--mn-primary);#e74c3c→var(--mn-error)(修 P5/P6)hook/layout_head_end.php—<div id="inviteConfig">+<script>移到layout_body_end,<head>只留<link>(修 P7)
第三批(打磨,可选)
- 列表分页 /
LIMIT放宽(P8);版本号统一(P9);relatedId传真实邀请码 id(P10)
改完需按红线跑:
php -l(8.2 + 8.0)、fh_i18n_check、php_var_cjk_check、plugin_hygiene;并更新
plugin.json版本 +说明.md。
八、一句话给站长
邀请码框在暗夜星辰下是白的,根因是它没带 mn-input 类、插件 CSS 也没补 background/color(同表单其它框都带了)。
顺带查出 3 个真问题:手输小写邀请码会被判无效、生成后刷新会重复扣积分、注册标记缺并发保护。
→ 以上问题已于 2026-10-05 一次性全部修复,详见 §九。
九、修复实施记录(2026-10-05,站长批准「一次性修复完」)
9.1 改动清单(10 处 / 8 文件,含 1 新增)
| # | 文件 | 改动 |
|---|---|---|
| 1 | assets/script.js | 注入的 <input> 加 class="mn-input"、<label> 加 class="mn-label"、spellcheck="false"(P1) |
| 2 | assets/style.css | 删冗余规则;.invite-field input 只留 width:100%(其余交核心);#e74c3c→var(--mn-error)(P5/P6) |
| 3 | hook/auth_register_before.php | 邀请码 strtoupper 归一 + 非标量入参防护(P2) |
| 4 | hook/auth_register_after.php | UPDATE 加 AND used_by_user_id IS NULL + rowCount() 检查(P4) |
| 5 | InviteController.php | 抽出 handleGenerate(),统一 302 收尾(PRG);分页 LIMIT/OFFSET + 总数;relatedId 传真实邀请码 id;expires_days 服务端上界 365(P3/P8/P10/P13) |
| 6 | views/index.php | 接 $this->pagination()(P8) |
| 7 | hook/layout_head_end.php | 只留 <link>(P7) |
| 8 | hook/layout_body_end.php(新增) | #inviteConfig + <script>(P7) |
| 9 | plugin.json | 注册 layout_body_end;版本 1.6.0 → 1.6.1(P7/P9) |
| 10 | 说明.md + 6 个 PHP 文件头 | 版本号统一 1.6.1(P9) |
9.2 验证结果(全绿)
| 项 | 结果 |
|---|---|
php -l 8.2 | 10/10 通过 |
php -l 8.0 | 10/10 通过 |
php_var_cjk_check | 13 个 PHP 文件,无问题 |
| i18n 键 | 26 引用 = 26 定义,零缺键零冗余;三语键集合一致 |
逻辑探针 .workbuddy-ai/tmp/fh_inv_fix_verify.php | 18 PASS / 0 FAIL —— T1 大小写归一(含「修复前查不到」反向复现)/T2 并发保护(含「不带 IS NULL 会被覆盖」反向对照)/T3 PRG(真实调用 + 静态出口断言)/T4 分页边界(25 条 → 3 页 + 越界钳制) |
| 静态断言 | 10/10 修复点落盘(含 head 钩子无 <div> 输出、body 钩子含 #inviteConfig) |
| 视图零内联 / lang 重复键 | 通过 |
| 零残留 | 探针改用 SQLite 内存库(无文件);未碰生产数据/配置/缓存 |
插件缓存:plugin.json mtime 已新于 plugins_cache.json → 按 Plugin.php:491 规则自动失效重建,无需手动清缓存。
9.3 边界说明
- 未验证(需真实环境):暗夜星辰下的视觉效果(UI,交站长验收);启用后的真实注册全流程(需启用插件 + 邮件/积分环境)。
- 未做(极低危,刻意未动):邀请码熵 32bit(UNIQUE 已兜底)、
strpos子串匹配/register、nav_plugin_links的BASE_PATH未加defined保护。 - 回归可重跑:
php .workbuddy-ai/tmp/fh_inv_fix_verify.php(幂等、零写入)。
十、发布记录(2026-10-05)
站长定版:1.7.0(minor —— 本次含新功能「邀请记录分页」)。§九 中所述 1.6.1 已按此要求上调为 1.7.0(3 处:plugin.json / 说明.md / 9 个 PHP 文件头)。
发布路径:Hub 官方(cli/release_build.php --type=plugin),非前台投稿包。
| 产物 | 位置 | sha256 |
|---|---|---|
| 包体 | data/market/packages/flinthub-plugin-invite-1.7.0.zip | f293978c22ac796914c6e0b3ee28f9f540b9ebe19831828bba4e5553d3c01815 |
| 清单 | data/market/manifests/manifest-invite-1.7.0.json | 1bee16e0dc5516e7fff95f5818dc4abda4bd0fe9154a3d763545208fb03eb3e8 |
| 镜像 | data/market/mirrors/flinthub-plugin-invite-1.7.0/ | 18 文件 |
- zip:20,051 B / 18 文件 + 6 目录,CRC 全通过;签名
RS256 / key_id=flinthub-2026。 - 公开索引
release/plugins/index.json已用--type=index重建 → invite 条目 1.7.0、signed: true。 - 包内
plugin.json.activated = false(符合 §0.5b ① 新红线:启用与否由站长安装时决定)。 - 旧版 1.6.0 保留(包/manifest/镜像均在;索引只展示最新版)。
- 发布后校验
.workbuddy-ai/tmp/fh_plugin_release_verify.php invite→ 30 PASS / 0 FAIL(通用脚本,可传任意插件名复用)。