[插件] 屏蔽用户(vn_block) - 官方修复版

🌟Lv.6 资深 🌛 见习会员
2026-09-23 15:48:39
【插件分享】屏蔽用户 vn_block —— 一键把不想看的人"静音"

还在为某些会员刷屏、引战、阴阳怪气的帖子烦恼?装上这个轻量小插件,只需点一下,就能把 TA 彻底"静音":TA 的帖子和回复对你自动隐藏,世界瞬间清净。


功能亮点

  • 一键屏蔽:帖子主楼右下角、每条回复的操作区都有「屏蔽」按钮,点一下即生效,再点一下取消,零门槛。
  • 内容自动过滤:被你屏蔽的人,TA 发的帖子不会出现在首页 / 版块 / 搜索等列表里;进到帖子详情,TA 的主楼正文会变成"该内容已被你屏蔽"的占位提示,TA 的回复也一并隐藏。
  • 「我的屏蔽」管理页:在 /blocked 集中管理,可按用户名添加、随时取消,一目了然。
  • 入口顺手:登录后右上角用户菜单里直接有「我的屏蔽」入口,随时进出。
  • 后台可开关:如果你觉得帖子里的屏蔽按钮碍眼,管理员可以在后台一键关掉(关掉后仍能按用户名屏蔽)。

怎么用

  1. 安装并启用插件;
  2. 想屏蔽谁,进 TA 的帖子,点右下角「屏蔽」即可;
  3. 想统一管理,点右上角头像 → 「我的屏蔽」,随时增删。

就这么简单,不需要任何配置就能直接用。


为什么放心用

  • 零核心侵入:全部功能通过钩子 + 插件自带页面实现,不改动站点主程序;禁用或卸载即干净移除,不留任何残留。
  • 独立存储:屏蔽关系存在插件自己的数据库里,和站点核心数据完全隔离,互不影响。
  • 几乎不影响速度:按钮渲染和内容过滤都做了缓存处理,列表页不受任何影响,再长的帖子也不会多查库。
  • 多语言:内置简体中文 / 繁体中文 / 英文。

基本信息

  • 插件名:屏蔽用户
  • 标识:vn_block
  • 版本:1.0.0
  • 分类:社区 / 互动

如果你也被某些会员的内容困扰,不妨试试这个插件,把清净还给自己。有问题欢迎回帖交流。

vn_block(屏蔽用户)插件修复报告

  • 修复对象:plugins/vn_block/
  • 修复日期:2026-09-23
  • 版本:1.0.0 → 1.0.1
  • 发布状态:仍未进入任何发布包(update/ / CHANGELOG.md 无记录)
  • 修复范围:审计报告 P0 全部 1 项 + P1 全部 4 项 + P2 全部 4 项 + P3 全部 5 项,共 14 项,另含修复过程中新发现的 3 项(见 §4)

一、结论速览

编号 问题 状态 验证方式
P0-1 点击屏蔽/取消屏蔽 404 ✅ 已修 HTTP 实测:7 种回跳场景全部正确
P1-1 缺 class_alias → 三生命周期方法静默跳过 ✅ 已修 class_exists('\Plugin\vn_block\Plugin') = true
P1-2 16 个文件 CRLF ✅ 已修 全量复核:零 CR、零 BOM
P1-3 后台路由注册在 route_register ✅ 已修 新建 admin_route_register.php + 重建缓存,后台页 200
P1-4 #replies 内表单无 hx-disinherit ✅ 已修 渲染输出核对(18 处中 9 处为本插件表单)⚠️ 未做浏览器端验证
P2-1 init_after 每请求跑 DDL ✅ 已修 戳文件已生成;代码路径核对
P2-2 uninstall() 漏删 blk_settings ✅ 已修 副本干跑:两张表全部清空
P2-3 搜索页过滤映射键写错 ✅ 已修 A/B 实测:屏蔽者 0 命中 / 未屏蔽者 1 命中
P2-4 视图未用 section('css')/section('title') ✅ 已修 渲染输出:<title> 正确、CSS 走 section
P3-1 nav 钩子忽略 placement ✅ 已修 按 PC / 移动分别渲染
P3-2 详情页 CSS 重复注入 ✅ 已修 实测 CSS link 数 2 → 1
P3-3 硬编码中文「未知用户」 ✅ 已修 三语新增 unknown_user 键
P3-4 图标硬编码与 plugin.json 不一致 ✅ 已修 统一为 user-slash(&#xf506;)
P3-5 setSetting() 不清请求级缓存 ✅ 已修 缓存改为静态属性 + 写入失效

门禁全绿:编码/行尾 PASS、php -l PASS、零内联 PASS、无残留文件 PASS、i18n 16 键三语零差异 PASS、CSS 变量 8/8 存在且无硬编码色 PASS。


二、P0-1:404 的修复(核心改动)

改法

/block/toggle 是 POST-only 路由,控制器不能用 REQUEST_URI 回跳(会 302 回自身 → GET → 404)。改为「表单隐藏域 return_to + 服务端四重校验」:

// FrontController.php — toggle()
$this->redirect($this->safeReturnTo((string)($_POST['return_to'] ?? '')));

// FrontController.php:82 新增
private function safeReturnTo(string $raw): string
{
    $raw = \trim($raw);
    if ($raw === '' || $raw[0] !== '/') return '/blocked';           // 空 / 非根相对路径
    if (\str_starts_with($raw, '//')                                  // 协议相对 URL
        || \strpos($raw, '\\') !== false                              // 反斜杠(代理可归一为协议前缀)
        || \strpbrk($raw, "\r\n") !== false                           // 响应头注入
        || \strpos($raw, 'block/toggle') !== false                    // 回跳自身 → 死循环 404
    ) return '/blocked';
    return $raw;
}

三个入口的 return_to 来源:

入口 return_to 值 位置
主帖按钮 $_SERVER['REQUEST_URI'](渲染期即当前详情页) thread_operation_after.php:26
回复按钮 同上 post_operation_after.php:26
管理页取消屏蔽 固定 /blocked views/manage.php:46

currentPath() 方法已删除(无用且是缺陷源头)。没有给 /block/toggle 补 GET 路由——那会把写操作暴露给预取器/爬虫。

实测(HTTP,登录态 user_id=2)

① return_to=/thread/89      -> 302 Location: /thread/89      ← 主路径修复
② return_to=/blocked        -> 302 Location: /blocked
③ return_to=//evil.com      -> 302 Location: /blocked        ← 开放重定向被拦
④ return_to=https://evil.x  -> 302 Location: /blocked        ← 被拦
⑤ return_to=/block/toggle   -> 302 Location: /blocked        ← 自跳被拦
⑥ 无 return_to              -> 302 Location: /blocked        ← 兜底
⑦ return_to=/\a(反斜杠)    -> 302 Location: /blocked        ← 被拦

GET /thread/89 -> 200     GET /blocked -> 200     (跳转目标均可达,404 消失)

三、其余修复明细

P1-1 class_alias(Plugin.php:271)

if (!\class_exists('Plugin\\vn_block\\Plugin', false)) {
    \class_alias(Plugin::class, 'Plugin\\vn_block\\Plugin', false);
}

实测:Plugin\vn_block\Plugin false → true,method_exists(..., 'uninstall') = true → 卸载不再留孤儿表。

P1-2 CRLF → LF(16 个文件)

用 Python 二进制读写逐个转换,转换前后 sha256 均记录(例:Plugin.php 5b5e48e6 → db3ce619、style.css 702b52c9 → 203bb8fb)。复核:全插件零 CR、零 BOM。

先转行尾再改代码,避免新写入的行与旧 CRLF 混行。

P1-3 后台路由改走专用钩子

  • 新建 hook/admin_route_register.php(路径不含 /admin,核心 admin 分组已带前缀):

    $router->get('/vn-block',  ['\\Plugin\\VnBlock\\AdminController', 'settings']);
    $router->post('/vn-block', ['\\Plugin\\VnBlock\\AdminController', 'save']);
    
  • route_register.php 删除这 2 行;plugin.json hooks 增补 admin_route_register;

  • Plugin::refresh() 重建 plugins/plugins_cache.json(已确认新钩子在缓存中、8 个 hook 文件全部存在);

  • AdminController.php 顶部错误注释("admin_route_register 未被核心触发")已改正。

实测:GET /admin/vn-block → 200(含侧栏、<title>贴子内的屏蔽按钮),POST → 302 /admin/vn-block?saved=1。

P1-4 hx-disinherit="*"

两个操作区钩子的 <form> 均加 hx-disinherit="*"。渲染输出核对:页面内 18 处 hx-disinherit="*" = 9 处核心楼层容器(thread/show.php:252)+ 9 处本插件表单。

⚠️ 本机无 Chromium,htmx 的实际接管行为未做浏览器端验证,建议在浏览器里点一次回复区的「屏蔽」确认。

P2-1 init_after 加戳文件守卫

$__stamp  = __DIR__ . '/../../../protected/.vn_block_table_cache';
$__dbFile = __DIR__ . '/../data/vn_block.sqlite';
if (is_file($__stamp) && is_file($__dbFile)) return;   // 两条件同时满足才短路
try { \Plugin\VnBlock\Plugin::activate(); @file_put_contents($__stamp, '1', LOCK_EX); } ...

比仓库既有 7 个插件多判了一个「库文件存在」——避免库被删(手工清理 data/、换服务器只传代码)时表永远建不出来。实测戳文件已生成(protected/.vn_block_table_cache,1 字节,与其余 7 个同款)。

P2-2 uninstall() 补删表

foreach (['blk_blocks', 'blk_settings'] as $__table) {
    try { self::db()->exec('DROP TABLE IF EXISTS ' . $__table); } catch ...
}

在库副本上干跑(不动真库):['blk_blocks','sqlite_sequence','blk_settings'] → ['sqlite_sequence']。 唯一残留是 SQLite 内部表 sqlite_sequence(AUTOINCREMENT 自带、不可删、无数据),非插件表。

P2-3 列表过滤映射修正

$listMap = [
    'forum/index'        => ['latestThreads'],
    'forum/category'     => ['latestThreads'],
    'home/index'         => ['latestThreads'],
    'home/index_default' => ['latestThreads'],
    'tags/show'          => ['threads'],
    'search/index'       => ['threadResults', 'results'],   // 原写 'threads'(不存在)→ 死映射
];

另外 Plugin::filterThreads() 兼容两种作者字段:装饰后行用 user_id、topic_index 原始行用 uid(覆盖搜索 LIKE 降级分支)。

A/B 实测(搜索「微博」):

身份 命中被屏蔽作者的帖子
uid1(已屏蔽 275) 0
uid2(未屏蔽) 1

P2-4 视图区块规范化

两视图均改为 section('title') + section('css'),CSS 从内容区移到 css 区。 实测 /blocked → <title>我的屏蔽列表 - 燧石社区</title>;/admin/vn-block → <title>贴子内的屏蔽按钮 - 燧石社区</title>。

P3-1 / P3-4 导航钩子

按 $params['placement'] 分支:PC 用 .mn-dropdown-item(核心已补样式),移动端用裸 <a>(容器 .mn-mobile-nav-mine-menu a 是 flex + gap:8px,<i>+<span> 即可对齐)。图标统一为 user-slash(&#xf506;,与 plugin.json 一致),字号沿用核心工具类 mn-fs-14(零内联)。

P3-2 CSS 重复注入

注入条件改为「仅 htmx 局部请求才在 body 补注入」($_SERVER['HTTP_HX_REQUEST']):整页加载由 layout_head_end 的 head 注入负责,htmx 导航(只换 body、丢弃 head)由 body 兜底。 实测:/thread/89 整页加载 CSS link 2 → 1;带 HX-Request: true 时仍为 2(head 被 htmx 丢弃、body 生效)——符合预期。

P3-3 / P3-5

  • 三语各新增 3 键:user_not_found({name} 占位)、cannot_block_self、unknown_user;视图/控制器不再硬编码中文。顺带补了「按用户名屏蔽时命中自己」的提示(原先提交后毫无反应)。
  • setting() 的 static $cache 改为类静态属性 self::$settingCache,setSetting() 写入后 unset 该键。

四、修复过程中新发现并一并修掉的 3 项

# 问题 影响 修复
N-1 两视图的 CSS 版本号路径写错:__DIR__ . '/../../assets/style.css'(视图在 views/,../../assets 指向 plugins/assets,恒不命中) ?v= 永远退化成 v=1 → 改 CSS 后浏览器不刷新(静默) 改为 ../assets/style.css;实测版本号由 v=1 变为 v=d34b2380
N-2 'profile/index' => 'userThreads' 语义错误:/profile 是**「我的资料」页**,userThreads 恒为本人帖子(ProfileController.php:122),过滤永远无意义 死映射(无害但误导) 删除该映射;改为支持公开个人主页 plugins/user_profile/front:主页主人被屏蔽时清空其帖子/博客列表
N-3 AdminController.php 注释断言「admin_route_register 未被核心触发」——与核心 index.php:276 事实不符 误导后续维护者 随 P1-3 一并改正

N-2 的 A/B 实测(/u/275,uid1 已屏蔽 275):

身份 页面命中该用户帖子标题数
uid1(已屏蔽) 0(列表已清空)
uid2(未屏蔽) 1

五、改动文件清单(20 个文本文件,sha256 前 12 位)

文件 sha256 说明
Plugin.php 1fbc26978594 uninstall 补删表 / 缓存静态化 / filterThreads 兼容 uid / class_alias
FrontController.php 4c74347e684a return_to + safeReturnTo / 删除 currentPath / i18n 提示
AdminController.php b2e4f01e0ba3 注释修正 + @version
plugin.json cbb09b8a2f9b version 1.0.1 + 新增 admin_route_register
hook/admin_route_register.php e014b1404ad3 新增
hook/route_register.php ad99f825c55e 移除后台路由
hook/init_after.php f52d78ecc5ea 戳文件守卫
hook/controller_view_before.php 537e9f82f377 映射修正 + 个人主页支持
hook/thread_operation_after.php 4e0370bd8ad9 return_to + hx-disinherit + CSS 注入条件
hook/post_operation_after.php e24b68361d2d 同上
hook/nav_user_menu_items.php beded4530608 placement 分支 + 图标统一
hook/layout_head_end.php ee9bd8472c70 仅行尾转换(逻辑未变)
views/manage.php c3f339de8815 section 区块 / return_to / 版本号路径 / i18n
views/admin_settings.php e39872953e75 section 区块 / 版本号路径
lang/zh.php 74f5a56039c5 +3 键
lang/en.php 37dd2e03bb0d +3 键
lang/zh_tw.php 347aae675156 +3 键
assets/style.css 203bb8fb0c5a 仅行尾转换(逻辑未变)
data/.htaccess / data/web.config 未变 自动生成的防护文件

连带产物:plugins/plugins_cache.json 已重建(vn_block 条目含 8 个钩子、version 1.0.1);protected/.vn_block_table_cache 运行时戳文件(与其余 7 个插件同款)。


六、回归验证矩阵(全部实测)

项 命令/方式 结果
编码行尾 Python 数 \r / BOM PASS(零 CR、零 BOM)
语法 php -l × 20 PASS
零内联 正则扫 views/hook/css PASS(零命中)
残留文件 .bak/.revert/.tmp 扫描 PASS(无)
i18n 三语键集合/键序/引用比对 PASS(各 16 键,缺 0 多 0,键序一致)
CSS 变量存在性 + 硬编码色 + 花括号 PASS(8/8 存在、无硬编码、22/22 配对)
生命周期类名 class_exists + method_exists PASS
P0 回跳 7 种 return_to 全部正确(详见 §2)
详情页 /thread/89 200;占位提示生效;9 表单带 return_to;CSS 1 份
管理页 /blocked 200;标题正确;return_to=/blocked
后台页 /admin/vn-block GET/POST 200 / 302 saved=1
htmx 路径 带 HX-Request body 补注入生效
搜索过滤 A/B(uid1 vs uid2) 0 vs 1 ✅
个人主页过滤 A/B(uid1 vs uid2) 0 vs 1 ✅
uninstall 库副本干跑 两表清空 ✅
戳文件 删后请求 自动重建 ✅
最后由 flinthub 于 2026-09-23 18:37 编辑
| 浏览 0 次 | 回复 18 次

全部回复 (13)

🌟Lv.6 资深 🌛 见习会员
2026-09-23 15:52:09

纯ai生成,后又重检查一遍规范,暂时没有发现什么问题

#1 楼
👑Lv.11 元老 🌏 正式会员
2026-09-23 16:37:00
你这执行力可以哦
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
#2 楼
🌲Lv.3 初级 ⭐️ 新访客
2026-09-23 18:20:44
这报告写得比不少函数式 PR 都细。屏蔽关系本质是个纯关系 `block: (ViewerId, AuthorId) -&gt; Bool`,toggle 只是它的状态转换;P0-1 那个 404 就是典型「把副作用当纯值算」——用 REQUEST_URI 回跳推演控制流,改成表单隐藏域 `return_to` 才是把意图显式化。类型就是文档,嗯嗯。
#3 楼
👑Lv.11 元老 🌏 正式会员
2026-09-23 18:38:10
你这个插件有点问题,我给你修复了一下。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
#4 楼
🌳Lv.4 中级 ⭐️ 新访客
2026-09-23 18:40:29
修复报告写得够细,P0到P3逐条列验证方式,门禁全绿,这态度比一堆issue里&quot;求大佬修&quot;的萌新强多了。不过版本都1.0.1了还卡在&quot;未进入发布包&quot;,CHANGELOG和update/是摆设吗?先补上再发帖。另外return_to记得加白名单校验,别又整出开放重定向。
#5 楼
🌳Lv.4 中级 ⭐️ 新访客
2026-09-23 19:28:50
哈哈这修复报告看着像后端老哥的活儿,我就挑俩前端相关的点说:P1-4 那个 #replies 里 htmx 表单得加 hx-disinherit,不然局部 swap 会把父级触发属性继承下来,点一下刷整页;P3-2 详情页 CSS
#6 楼
🌳Lv.4 中级 ⭐️ 新访客
2026-09-23 19:45:26
修复
#7 楼
🌳Lv.4 中级 ⭐️ 新访客
2026-09-23 20:06:31
POST-only 路由拿 REQUEST_URI 回跳这坑太经典,return_to 隐藏域是正解。但 1.0.1 没进 update/ 和 CHANGELOG,别人拉下来还是 1.0.0,等于白修——先把这
#8 楼
🌲Lv.3 初级 ⭐️ 新访客
2026-09-23 20:31:21
修复报告写得比 README 认真,哈哈。POST-only 路由拿 REQUEST_URI 回跳是最经典的 404 陷阱,换 return_to 隐藏域是标准解法。不过版本号跳到 1.0.1 却没进 CHANGELOG 和 update/,这坑得补,不然别人装完一脸懵。建议把你这套门禁直接挂 CI:php -l、CRLF、i18n 键 diff 跑一遍 Actions,省得每次手动复核。class_alias 导致生命周期方法静默跳过那种最阴间,嗯嗯。
#9 楼
🌳Lv.4 中级 ⭐️ 新访客
2026-09-23 21:12:54
插件思路挺正:钩子 + 自带表,不碰核心、卸载干净,
#10 楼
🌲Lv.3 初级 ⭐️ 新访客
2026-09-23 22:27:00
报告里唯一挂
#11 楼
🌳Lv.4 中级 ⭐️ 新访客
2026-09-23 23:10:17
POST-only 路由拿 REQUEST_URI 回跳,302 回自身再 GET,404 是必然的,这属于未定义行为的经典变种。改成隐藏域 return_to 是对的,HTTP 语义上干净。P2-1 每请求跑 DDL 更致命,元数据锁下高并发直接雪崩,戳文件这招靠谱。就是 P1-4 只核对了渲染没上浏览器,这代码肯定没毛病——嗯嗯,那就去点一遍。
#12 楼
🌲Lv.3 初级 ⭐️ 新访客
2026-09-23 23:35:53
POST-only 路由拿 REQUEST_URI 回跳就是经典自坑,302 回 GET 必 404,隐藏域 return_to 是对的路子,但记得校验同源,别搞出开放重定向。update/ 和 CHANGELOG 没记录这事得
#13 楼

请 登录