概述
插件兼容性扫描器用于检测已安装插件中与 XIUNOX 现代化改造不兼容的代码,涵盖 PHP 8.0+ 语法、Bootstrap 5 迁移、jQuery 去除、安全规范等方面。
入口:后台 → 插件管理 → 插件兼容性检查(/*****/?plugin-scanner.htm)
相关文件
文件 | 说明 |
|---|
lib/PluginScanner.php
| 扫描器核心,负责文件遍历、行级/文件级检测、结果构建 |
lib/PluginScannerRules.php
| 规则定义,包含分类、匹配模式、严重级别、中文名、PHP-only/HTML-only 分类 |
lib/PluginScannerSuggestion.php
| 动态建议构建器,根据匹配内容生成具体迁移建议 |
*****/route/plugin_scanner.php
| 后台路由,处理扫描/导出请求 |
*****/view/htm/plugin_scanner.htm
| 扫描器页面模板 |
lang/zh-cn/bbs_*****.php
| 中文语言项(plugin_scanner_*) |
lang/zh-tw/bbs_*****.php
| 繁体中文语言项 |
lang/en-us/bbs_*****.php
| 英文语言项 |
使用方式
进入后台「插件兼容性检查」页面
点击「开始扫描」按钮
前端通过 fetch() 请求 ?plugin-scanner-do.htm,后端返回 JSON 格式扫描结果
结果按插件分组展示,支持按严重级别筛选
可导出 CSV 报告
扫描范围
扫描 plugin/ 目录下所有包含 conf.json 的插件目录,递归检查以下扩展名的文件:
.php — PHP 代码
.htm / .html — 模板文件
.js — JavaScript 脚本
.css — 样式文件
自动跳过 img/、images/、fonts/、vendor/、node_modules/ 等目录。
为避免误报,扫描器会将 .htm/.php/.html 文件中的 <script>/<style> 块与 PHP/HTML 部分分离:
规则类别
严重级别说明
级别 | 含义 |
|---|
fatal | 致命:代码在 PHP 8.0+ 下会直接报错或崩溃 |
warning | 警告:存在安全风险或功能异常 |
medium | 中等:不兼容但不会直接崩溃,需要迁移 |
info | 信息:建议优化,非必须 |
PHP 兼容性规则
废弃函数(PHP 7.x 移除)— fatal
检测 mysql_* 系列函数及其他在 PHP 7.x 已移除的函数:
匹配模式 | 说明 |
|---|
mysql_connect 等 19 个
| mysql 扩展已在 PHP 7.0 移除 |
each(
| PHP 7.2 废弃,8.0 移除 |
create_function(
| PHP 7.2 废弃,8.0 移除,用闭包替代 |
split( / spliti(
| PHP 7.0 移除,用 preg_split() 或 explode() |
ereg( / ereg_replace( 等
| PHP 7.0 移除,用 preg_match() / preg_replace() |
call_user_method(
| PHP 7.0 移除,用 call_user_func() |
PHP 8 不兼容语法 — fatal
匹配模式 | 说明 |
|---|
&new
| 按引用创建对象已移除,改用 new |
preg_replace.*\/e
| /e 修饰符已移除,用 preg_replace_callback()
|
花括号数组访问 — fatal
匹配模式 | 说明 |
|---|
{$
| {$arr['key']} 语法已移除,改用 [$arr['key']]
|
HTTP_*_VARS 变量 — fatal
匹配模式 | 说明 |
|---|
HTTP_POST_VARS 等
| 已移除,用 $_POST / $_GET / $_SESSION |
PHP 8.0+ 废弃函数 — fatal
匹配模式 | 说明 |
|---|
get_magic_quotes_gpc
| PHP 7.4 废弃、8.0 移除,始终返回 false |
get_magic_quotes_runtime
| PHP 7.4 废弃、8.0 移除 |
utf8_encode(
| PHP 8.2 废弃,用 mb_convert_encoding() |
utf8_decode(
| PHP 8.2 废弃,用 mb_convert_encoding() |
money_format(
| PHP 7.4 废弃、8.0 移除,用 NumberFormatter |
is_resource(
| 对 PDO/MySQLi 对象返回 false(PHP 8.0+),改用 instanceof |
PHP 注释陷阱检测 — fatal
匹配模式 | 说明 |
|---|
//.*?>
| 单行注释 // 中包含 ?> 会触发 headers already sent |
^\s*#.*?>
| 行首 # 注释中包含 ?> |
\s+#.*?>
| 行中 # 注释中包含 ?>(前面有空格避免匹配 shebang) |
仅扫描 .php 文件。PHP 解析器会把单行注释中的 ?> 识别为 PHP 代码块结束标签,导致后续代码被当作纯文本输出。建议:移除注释中的 <?php 和 ?>,改用块注释 /* */。
HEREDOC 语法检测 — fatal
匹配模式 | 说明 |
|---|
<<<EOT ... <?php ... EOT;
| HEREDOC 块内含 PHP 标签 |
仅扫描 .php 文件。HEREDOC 语法中需使用 {$variable} 语法嵌入 PHP 变量,避免使用 <?php echo ... ?> 导致解析错误。
Service 类 SQL 未定义变量检测 — fatal
匹配模式 | 说明 |
|---|
$tableName / $tablePrefix(在含 SQL 关键字的行中)
| Service 类拼接 SQL 使用未定义变量 |
仅扫描 model/ 目录下 PHP 文件。Service 类中拼接 SQL 表名必须用 $this->tablepre . '表名',不能使用未定义变量(如 $tableName、$tablePrefix)。
APP_PATH 误用检测 — fatal
匹配模式 | 说明 |
|---|
<script src="*APP_PATH*">
| script src 用 APP_PATH |
<link href="*APP_PATH*">
| link href 用 APP_PATH |
仅扫描 .htm/.html 文件。APP_PATH 是文件系统绝对路径,浏览器无法访问,必须用 $conf['view_url'] 生成资源 URL。
Hook 文件头检测
分类 | 级别 | 匹配模式 | 说明 |
|---|
hook_htm_header
| fatal | .htm 文件以 <?php exit; 开头
| 会白屏!只能用 <?php 开头 |
hook_php_header
| info | .php 文件不以 <?php exit; 开头
| 安全防护缺失,建议添加 exit 保护 |
检测 hook/*.* 文件的首行内容。.htm 模板 hook 文件会被编译拼进模板执行,以 <?php exit; 开头会白屏;.php 源码 hook 文件建议以 <?php exit; 开头防止直接访问。
安全规则
危险函数 — warning
匹配模式 | 说明 |
|---|
eval( / system( / exec( 等
| 代码注入/命令执行风险,应避免使用 |
权限安全(敏感字段修改)— warning
匹配模式 | 说明 |
|---|
user_update.*password
| 应使用 user_change_password() |
user_update.*gid
| 应使用 user_change_group() |
user_update.*salt
| salt 由系统自动管理,不应直接修改 |
user_update.*password_hash
| 应使用 user_change_password() |
POST 表单缺少 CSRF 令牌 — warning
匹配模式 | 说明 |
|---|
method="post" 但无 CsrfService / csrf_token
| 所有 POST 表单必须包含 CSRF 令牌 |
此规则采用文件级检测:读取整个文件内容,检查是否同时包含 method="post" 和 CsrfService/csrf_token,若缺少则报告。
前端 MD5 哈希检测 — warning
匹配模式 | 说明 |
|---|
hex_md5(
| 前端 MD5 哈希代码 |
md5_hex(
| 前端 MD5 哈希代码 |
密码必须明文提交,由服务端 password_md5() 处理。前端 MD5 哈希已移除,MD5.js 不得全局加载。
MD5.js 全局加载检测 — warning
匹配模式 | 说明 |
|---|
<script src="*md5*.js">
| 模板文件全局加载 md5.js |
仅扫描 .htm/.html 文件。MD5.js 不得全局加载,前端 MD5 哈希已移除。
user_update 修改密码检测 — warning
匹配模式 | 说明 |
|---|
user_update(...password...)
| 使用 user_update() 修改 password 字段 |
找回密码必须使用 user__update() 而非 user_update(),因为后者会过滤掉 password 字段。
数据库字符集检测 — warning
匹配模式 | 说明 |
|---|
charset=utf8(不含 mb4)
| 数据库连接字符集为 utf8 |
set names utf8(不含 mb4)
| 设置字符集为 utf8 |
数据库连接字符集必须为 utf8mb4(支持 emoji 等 4 字节字符)。
裸 htmlspecialchars 检测 — warning
匹配模式 | 说明 |
|---|
htmlspecialchars(
| 裸写 htmlspecialchars 函数 |
禁止裸写 htmlspecialchars,必须用 esc_html() / esc_attr() / esc_js() 统一转义。
Bootstrap Tab 误用检测 — warning
匹配模式 | 说明 |
|---|
<a data-bs-toggle="tab" href="*.htm">
| 外层导航误用 Bootstrap Tab |
仅扫描 .htm/.html 文件。外层导航(页面跳转)禁止用 Bootstrap Tab 系统(role="tablist" + data-bs-toggle="tab"),否则 Bootstrap 会把 <a href="./?xxx.htm"> 的 href 当作 CSS 选择器,触发 SyntaxError。应改为普通 <a> 链接跳转;内层导航(同一页面内的 tab 切换)才用 tab。
db_find_one 参数类型检测 — warning
匹配模式 | 说明 |
|---|
db_find_one(..., 'string')
| 第 4 参数为字符串字面量 |
db_find_one() 第 4 个参数 $col 必须传入数组(如 array('fid', 'uid')),禁止传入字符串以避免 implode() 参数类型错误。
install.php 非幂等建表检测 — warning
匹配模式 | 说明 |
|---|
CREATE TABLE(缺少 IF NOT EXISTS)
| 建表语句不幂等 |
仅对 install.php 文件检测。install.php 所有建表语句必须用 IF NOT EXISTS 保证幂等。
Bootstrap 迁移规则
BS4 旧类名 — medium
匹配模式 | 替换建议 |
|---|
ml-
| ms-
|
mr-
| me-
|
pl-
| ps-
|
pr-
| pe-
|
form-group
| mb-3
|
custom-select
| form-select
|
custom-control
| form-check
|
btn-block
| w-100
|
input-group-prepend / input-group-append
| 直接 input-group-text |
BS4 旧 data 属性 — medium
匹配模式 | 替换建议 |
|---|
data-toggle
| data-bs-toggle
|
data-dismiss
| data-bs-dismiss
|
data-target
| data-bs-target
|
data-slide-to
| data-bs-slide-to
|
data-slide
| data-bs-slide
|
BS3 旧类名 — medium
匹配模式 | 替换建议 |
|---|
panel-heading
| card-header
|
panel-body
| card-body
|
panel-footer
| card-footer
|
panel-default 等
| card + 颜色
|
well
| card.card-body 或自定义样式
|
glyphicon
| Tabler Icons ti-* |
pull-left
| float-start
|
pull-right
| float-end
|
hidden-xs
| d-none .d-sm-block
|
visible-xs
| d-sm-none
|
label-default 等
| badge .bg-*
|
img-responsive
| img-fluid
|
img-circle
| rounded-circle
|
img-rounded
| rounded
|
col-xs-
| col-(xs 断点已移除)
|
Bootstrap jQuery 插件调用 — warning
匹配模式 | 替换建议 |
|---|
.modal(
| new bootstrap.Modal() 或 htmx hx-get 加载弹窗
|
.dropdown(
| new bootstrap.Dropdown()
|
.tooltip(
| new bootstrap.Tooltip()
|
.popover(
| new bootstrap.Popover()
|
.collapse(
| new bootstrap.Collapse()
|
.carousel(
| new bootstrap.Carousel()
|
.alert(
| new bootstrap.Alert()
|
.button(
| htmx hx-disabled-elt 或原生 JS disabled 属性 |
.tab(
| new bootstrap.Tab()
|
.button('loading') 在 Bootstrap 5 中已移除,应使用 htmx hx-disabled-elt 在请求期间禁用按钮,或用原生 JS 设置 btn.disabled = true。
前端迁移规则
jQuery 使用 — medium
匹配模式 | 替换建议 |
|---|
$.ajax(
| htmx 或原生 fetch |
$.post(
| htmx hx-post 或原生 fetch |
$.get(
| htmx hx-get 或原生 fetch |
$.each(
| Array.forEach()
|
$.fn.
| 原生 JS class 或 htmx 组件 |
$.extend(
| Object.assign()
|
$.trim(
| String.trim()
|
$.parseJSON(
| JSON.parse()
|
$.isArray(
| Array.isArray()
|
$.isFunction(
| typeof fn === "function"
|
$.browser
| 特性检测 |
$(document).ready
| DOMContentLoaded
|
$(function(
| DOMContentLoaded
|
jQuery(
| htmx 4 属性或原生 JS |
Fontello 旧图标 — medium
匹配模式 | 替换建议 |
|---|
icon-lock
| ti-lock
|
icon-home
| ti-home
|
icon-edit
| ti-pencil
|
icon-remove
| ti-trash
|
icon-eye
| ti-eye
|
icon-ok
| ti-check
|
icon-cog / icon-cogs
| ti-settings
|
icon-comment
| ti-message
|
icon-user
| ti-user
|
icon-envelope
| ti-mail
|
icon-key
| ti-key
|
icon-star
| ti-star
|
非 Tabler Icons 图标库 — medium
匹配模式 | 说明 |
|---|
class="[^"]*\bfa-[a-z]
| Font Awesome 图标 → Tabler Icons ti-* |
class="[^"]*\bbi-[a-z]
| Bootstrap Icons → Tabler Icons ti-* |
class="[^"]*glyphicon glyphicon-
| Glyphicon 图标 → Tabler Icons ti-* |
采用 class 属性正则匹配模式,避免误匹配 fa- 出现在其他上下文(如变量名、注释)中。建议生成时会提取具体的 fa-xxx/bi-xxx/glyphicon glyphicon-xxx 类名,提示用户参考 Tabler Icons 查找对应图标。
代码规范规则
直接数据库操作 — info
仅检测 model/ 目录下的 PHP 文件中的原始 SQL 函数,避免业务路由的误报。
匹配模式 | 说明 |
|---|
db_exec(
| 原始 SQL 执行(非 SELECT 语句),注意 SQL 注入风险 |
db_sql_find_one(
| 原始 SQL 单条查询,建议优先使用 db_find_one() |
db_sql_find(
| 原始 SQL 多条查询,建议优先使用 db_find() |
共 3 个 db_* 函数。此规则排除 install.php、uninstall.php、unstall.php、upgrade.php 文件(安装/升级脚本中直接操作数据库是合理的)。
保留注释抑制机制:
对于无法用 db_find* 替代的复杂 SQL(JOIN、系统表、复杂聚合等),在代码中添加包含 保留 db_sql_find 或 保留 db_exec 关键字的注释,扫描器会自动跳过后续 10 行的 direct_db 报告:
// 联表查询,db_find 不支持 JOIN,保留 db_sql_find
$sql = "SELECT a.*, b.name FROM a LEFT JOIN b ON a.id=b.id";
$rows = db_sql_find($sql); // 此行不会被报告
支持的关键字格式(正则 /(保留|@suppress).*db_(?:sql_find|sql_find_one|exec)/):
// 保留 db_sql_find
// 保留 db_sql_find_one
// 保留 db_exec
// @suppress db_sql_find
注释必须放在 db_* 调用上方 10 行以内。抑制区间为注释行 + 后续 10 行,覆盖多行 SQL 拼接。
迁移指南:
原始函数 | 替代方案 | 适用场景 |
|---|
db_sql_find_one($sql)
| db_find_one($table, $cond, $orderby, $col)
| 单条记录查询(无 GROUP BY) |
db_sql_find_one($sql)
| db_find_one_group($table, $cond, $groupby, $having, $orderby, $col)
| 单条聚合查询(含 GROUP BY + COUNT/SUM/MAX) |
db_sql_find($sql)
| db_find($table, $cond, $orderby, $page, $pagesize, $key, $col)
| 多条记录查询(无 GROUP BY) |
db_sql_find($sql)
| db_find_group($table, $cond, $groupby, $having, $orderby, $page, $pagesize, $key, $col)
| 多条聚合查询(含 GROUP BY + COUNT/SUM/MAX) |
db_count($table, $cond)
| 已封装,可直接使用 | 计数查询 |
db_exec($sql)
| 视语句类型选择 db_insert/db_update/db_delete | 非 SELECT 语句 |
保留 db_sql_find / db_exec 的合理场景:
以下复杂 SQL 无法用 db_find* 系列替代,可在代码中添加注释说明保留原因:
JOIN 查询:SELECT ... FROM a LEFT JOIN b ON ...(db_find 不支持 JOIN)
系统表查询:SELECT ... FROM INFORMATION_SCHEMA.*(如列存在性检查)
非 SELECT 语句:db_exec 执行 UPDATE ... SET x = GREATEST(x-1, 0)、INSERT IGNORE ... SELECT、含子查询的 DML
时间函数聚合:SELECT ... WHERE HOUR(FROM_UNIXTIME(...))(db_cond 不支持 SQL 函数)
db_find_group / db_find_one_group 使用示例:
// 替代:db_sql_find_one("SELECT uid, COUNT(*) as cnt FROM tbl GROUP BY uid HAVING cnt > 5")
$row = db_find_one_group(
'tbl', // 表名(不含前缀)
[], // WHERE 条件
['uid'], // GROUP BY 字段
['cnt' => ['>' => 5]], // HAVING 条件(格式同 WHERE)
[], // ORDER BY
['uid', 'COUNT(*) as cnt'] // SELECT 字段(聚合字段必须用别名)
);
// 替代:db_sql_find("SELECT fid, COUNT(*) as cnt FROM tbl GROUP BY fid ORDER BY cnt DESC LIMIT 10")
$rows = db_find_group(
'tbl',
[],
['fid'],
[],
['cnt' => -1], // -1 降序,1 升序
1, 10, // page, pagesize
'fid', // 返回数组的 key 字段
['fid', 'COUNT(*) as cnt']
);
其他规则
conf.json 版本检查 — info
检查 conf.json 中 bbs_version 字段,若 < 4.5 则提示更新。
Hook 文件名检查 — info
检查 hook/ 目录下文件名是否符合 [a-z_][a-z0-9_]* 命名规范。
特殊处理逻辑
missing_csrf(跨行检测)
POST 表单的 CSRF 检测不能逐行匹配,因为 method="post" 和 CsrfService::input() 可能分布在文件不同位置。因此采用文件级检测:
读取整个文件内容
检查是否包含 method="post"
若包含,再检查是否包含 CsrfService 或 csrf_token
仅当 POST 表单存在且缺少 CSRF 令牌时才报告
direct_db(文件名排除)
安装/升级脚本中直接操作数据库是合理的,因此排除以下文件:
install.php
uninstall.php
unstall.php(Xiuno 旧版拼写)
upgrade.php
service_undefined_var(路径限制)
仅在 model/ 目录下的 PHP 文件中检测,避免误报业务路由中的同名变量。检测条件:当前行包含 SQL 关键字(SELECT/INSERT/UPDATE/DELETE/FROM/INTO)且引用了 $tableName 或 $tablePrefix。
文件级跨行检测
以下规则需要读取整个文件内容并使用正则跨行匹配,无法逐行扫描:
分类 | 检测方式 |
|---|
missing_csrf
| 检查 method="post" 与 CsrfService/csrf_token 是否共存 |
php_comment_close_tag
| 逐行匹配 ///# 注释中的 ?> |
md5js_global_load
| 正则匹配 <script src="*md5*.js"> |
heredoc_php_tag
| 正则匹配 <<<(\w+).*?<\?php.*?\1;(跨行) |
bs_tab_navigation
| 正则匹配 <a data-bs-toggle="tab" href="*.htm"> |
app_path_in_url
| 正则匹配 <script/link src/href="*APP_PATH*"> |
install_non_idempotent
| 正则匹配 CREATE TABLE(缺少 IF NOT EXISTS) |
hook_htm_header / hook_php_header
| 读取 hook/*.* 文件首行 |
API 接口
扫描请求
GET /*****/?plugin-scanner-do.htm
返回 JSON 数组,每个元素代表一个插件的扫描结果:
[
{
"dir": "xn_tag",
"name": "标签",
"version": "1.0",
"bbs_version": "4.0",
"installed": true,
"enable": true,
"issues": [
{
"file": "plugin/xn_tag/hook/post_js.htm",
"line": 15,
"category": "jquery_usage",
"match": "$.ajax(",
"suggestion": "使用 htmx 或原生 fetch 替代 $.ajax",
"severity": "medium",
"context": "$.ajax({url: '...'})"
}
],
"total": 3,
"fatal": 0,
"warning": 1
}
]
CSV 导出
GET /*****/?plugin-scanner-export.htm
下载 CSV 文件,包含所有插件的扫描结果。
指定插件导出
GET /*****/?plugin-scanner-export.htm&dir=xn_tag
仅导出指定插件的扫描结果。
扩展规则
如需添加新的扫描规则,按以下步骤操作:
行级检测规则
在 lib/PluginScannerRules.php 的 getRules() 中添加新分类和匹配模式(仅适用于行级检测)
在 getSeverityLevels() 中添加严重级别映射
在 getCategoryNames() 中添加中文名称
如果是 PHP-only 规则,在 getPhpOnlyCategories() 中添加(避免扫描 JS/CSS 内容)
如果是 HTML-only 规则,在 getHtmlOnlyCategories() 中添加(避免扫描纯 PHP 文件)
对于需要动态建议的分类,在 lib/PluginScannerSuggestion.php 的 build() 方法中添加 case 分支
在 lang/zh-cn/bbs_*****.php、lang/zh-tw/bbs_*****.php、lang/en-us/bbs_*****.php 中添加 plugin_scanner_cat_* 语言项
文件级检测规则
对于跨行检测、文件头检测等无法逐行扫描的规则,在 lib/PluginScanner.php 的 scanPluginDir() 方法中添加相应代码,使用 buildIssue() 辅助方法构建 issue 数组。
严重级别约定
fatal:PHP 8.0+ 下直接报错或崩溃(如废弃函数、注释陷阱、HEREDOC 错误、APP_PATH 误用)
warning:安全风险或功能异常(如 CSRF 缺失、MD5 哈希、字符集错误、Tab 误用)
medium:不兼容但不会崩溃,需要迁移(如 BS4/BS3 旧类名、jQuery 使用、图标库)
info:建议优化,非必须(如 direct_db、conf.json 版本、Hook 文件名)
管理员标记为「干货」,奖励 +10 积分