[插件] 外链跳转提醒(external_link_guard)插件发布。

👑Lv.11 元老 🌏 正式会员
2026-09-30 17:06:05

版本:1.0.0 | 

编制:2026-09-30 

一句话:前台点击站外链接时先弹「即将离开本站」确认框,用户确认后才跳转 —— 防流失。


一、功能简介

能力 说明
外链拦截 前台任意页面点击站外链接时不立即跳转,先弹确认框
确认框 遮罩 + 居中卡片:「即将离开本站」+ 目标域名 + 「继续访问」/「留在本站」
拦截范围可配 全站所有外链(默认)/仅帖子·博客正文内的外链
白名单域名 配置的域名及其子域不弹确认框(友链、自家附属站点)
总开关 后台一键关闭;关闭时前台零注入、零请求

不做:点击统计(避免每请求写库)、「不再提醒此站点」、mailto:/tel: 拦截、右键菜单拦截。


二、技术路线(为什么是纯客户端)

核心 43 个钩子(规范 §9.1)没有任何「重写 / 过滤已渲染 HTML」的能力:

  • controller_view_before(#6)只能改渲染前的 $data,改不到渲染后的 HTML;
  • C 组内容钩子(#17~#25)全是锚点插入,不是过滤器;
  • 插件自建 ob_start() 会与核心 PageCache / header_register_callback 互相打架。

→ 所以本插件不改核心、不改正文 HTML,只在 layout_body_end 直出空壳弹窗标记, 由 assets/script.js 在浏览器里用事件委托拦 click。

收益:零额外网络请求、零服务端查询、零卡顿风险;不改 HTML 结构(SEO 与分享不受影响)。


三、目录结构

plugins/external_link_guard/
├── plugin.json                      # 清单(activated: true)
├── Plugin.php                       # 主类:设置读写 / 后台判定 / 弹窗渲染 / class_alias
├── AdminController.php              # 后台配置页(继承 Admin\BaseController)
├── hook/
│   ├── layout_head_end.php          # </head> 前:注入 CSS/JS(shouldInject 门控 + ?v=filemtime)
│   ├── layout_body_end.php          # </body> 前:直出弹窗空壳标记
│   └── admin_route_register.php     # 注册后台路由(不写 /admin 前缀)
├── views/
│   ├── popup.php                    # 前台弹窗标记片段(空壳)
│   └── admin.php                    # 后台配置视图(容器三件套)
├── assets/
│   ├── style.css                    # 弹窗样式(z-index 100020,只用核心 --mn-* 变量)
│   └── script.js                    # 拦截脚本(捕获阶段事件委托)
└── lang/
    ├── zh.php / zh_tw.php / en.php  # 三语(各 23 键,集合零差异 + 键序一致)

无 data/ 目录 —— 本插件零持久化数据(配置走 Settings),不需要建表, 故没有 init_after / ddlStatements() / ensureSchemaOnce()。


四、路由

方法 路径 处理
GET /admin/external-link-guard AdminController::index(配置页)
POST /admin/external-link-guard AdminController::save(保存,PRG)

无前台路由(纯客户端拦截,不需要中转页)。


五、配置项

设置键 类型 默认 说明
external_link_guard_enabled '1' / '0' '1' 总开关
external_link_guard_mode all / content all 拦截范围
external_link_guard_whitelist 逗号分隔字符串 '' 白名单域名(已归一化)

白名单归一化(写入期,Plugin::normWhitelist()):小写、去协议、去路径、去端口、 去通配前缀、去重;只保留形如域名的串(无点或非法字符一律丢弃)。 支持换行 / 半角逗号 / 全角逗号 / 分号混合分隔。


六、核心规则

6.1 外链判定(assets/script.js)

只能用 host 比对:

new URL(a.href, location.href).host !== location.host

⚠️ 不能用 target="_blank" 或 rel 判外链 —— 核心 app/Helpers/Content.php:123-126 给所有 <a>(含站内链接)统一加了 target="_blank" + rel="noopener noreferrer nofollow" → 靠它们判会误拦站内链接。

豁免清单(一律放行):

# 对象 判据
① 锚点 / 空链接 href 以 # 开头或为空
② 伪协议 javascript: / data: / vbscript:
③ 邮件 / 电话 mailto: / tel:
④ 非 http(s) 协议白名单
⑤ 同域链接 url.host === location.host(含端口)
⑥ 白名单 域名精确匹配 或 . + 域名 的子域匹配
⑦ 下载链接 有 download 属性
⑧ 修饰键 / 中键 Ctrl/Cmd/Shift/Alt 或 button !== 0
⑨ 具名 target target 不是 _blank / _self
⑩ htmx 链接 有 hx-get / hx-post / … 属性(交给 htmx)

6.2 白名单匹配(安全关键)

hostname === d || hostname.slice(-(d.length + 1)) === '.' + d

⚠️ 必须带 '.' 前缀,不能用 hostname.endsWith(d) —— 'evil-example.com'.endsWith('example.com') 为 true → 会误放行同后缀的另一个域。

6.3 「继续访问」必须是 <a>(防弹窗拦截器)

弹窗里的「继续访问」是一个真实的 <a href target="_blank">,JS 只负责把目标 URL 填进 href。

⚠️ 不能用 window.open() —— 它在「用户点确认」的回调里执行时已脱离「用户手势」上下文 → 被浏览器弹窗拦截器静默拦掉(点了没反应)。且 localhost 开发常不拦、生产会拦 → 属「本机测不出、线上才暴露」类故障。

配套:确认跳转时立即关弹窗 —— 跳转发生在新标签,本站页面从未离开; 不关则弹窗常驻 + body 锁滚动,用户回到原标签会以为页面卡死。

6.4 后台页不注入

layout_body_end 全站触发(含后台)且无参数 → 只能靠请求路径判后台:

Plugin::isBackend('')   // 模板名 admin/…  OR  请求路径以 /admin 开头

后台(含本插件配置页)不输出弹窗标记、不引前台 CSS/JS。

6.5 z-index

.elg-root 取 100020 —— 高于核心浮层天花板 100005(.image-lightbox = 99999), 也高于 notice_center 的 100010(两个弹窗可能同时存在,外链确认框应在最上层)。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 6 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录