精华 干货 XIUNOX 插件兼容性扫描器

概述

插件兼容性扫描器用于检测已安装插件中与 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

英文语言项

使用方式

  1. 进入后台「插件兼容性检查」页面

  2. 点击「开始扫描」按钮

  3. 前端通过 fetch() 请求 ?plugin-scanner-do.htm,后端返回 JSON 格式扫描结果

  4. 结果按插件分组展示,支持按严重级别筛选

  5. 可导出 CSV 报告

扫描范围

扫描 plugin/ 目录下所有包含 conf.json 的插件目录,递归检查以下扩展名的文件:

  • .php — PHP 代码

  • .htm / .html — 模板文件

  • .js — JavaScript 脚本

  • .css — 样式文件

自动跳过 img/images/fonts/vendor/node_modules/ 等目录。

为避免误报,扫描器会将 .htm/.php/.html 文件中的 <script>/<style> 块与 PHP/HTML 部分分离:

  • PHP-only 规则只扫描 PHP/HTML 部分,不会误扫 <script> 中的 JS 代码

  • HTML-only 规则只扫描 PHP/HTML 部分,不会扫描纯 .php 文件

规则类别

严重级别说明

级别

含义

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.phpuninstall.phpunstall.phpupgrade.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.jsonbbs_version 字段,若 < 4.5 则提示更新。

Hook 文件名检查 — info

检查 hook/ 目录下文件名是否符合 [a-z_][a-z0-9_]* 命名规范。

特殊处理逻辑

missing_csrf(跨行检测)

POST 表单的 CSRF 检测不能逐行匹配,因为 method="post"CsrfService::input() 可能分布在文件不同位置。因此采用文件级检测:

  1. 读取整个文件内容

  2. 检查是否包含 method="post"

  3. 若包含,再检查是否包含 CsrfServicecsrf_token

  4. 仅当 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

仅导出指定插件的扫描结果。

扩展规则

如需添加新的扫描规则,按以下步骤操作:

行级检测规则

  1. lib/PluginScannerRules.phpgetRules() 中添加新分类和匹配模式(仅适用于行级检测)

  2. getSeverityLevels() 中添加严重级别映射

  3. getCategoryNames() 中添加中文名称

  4. 如果是 PHP-only 规则,在 getPhpOnlyCategories() 中添加(避免扫描 JS/CSS 内容)

  5. 如果是 HTML-only 规则,在 getHtmlOnlyCategories() 中添加(避免扫描纯 PHP 文件)

  6. 对于需要动态建议的分类,在 lib/PluginScannerSuggestion.phpbuild() 方法中添加 case 分支

  7. lang/zh-cn/bbs_*****.phplang/zh-tw/bbs_*****.phplang/en-us/bbs_*****.php 中添加 plugin_scanner_cat_* 语言项

文件级检测规则

对于跨行检测、文件头检测等无法逐行扫描的规则,在 lib/PluginScanner.phpscanPluginDir() 方法中添加相应代码,使用 buildIssue() 辅助方法构建 issue 数组。

严重级别约定

  • fatal:PHP 8.0+ 下直接报错或崩溃(如废弃函数、注释陷阱、HEREDOC 错误、APP_PATH 误用)

  • warning:安全风险或功能异常(如 CSRF 缺失、MD5 哈希、字符集错误、Tab 误用)

  • medium:不兼容但不会崩溃,需要迁移(如 BS4/BS3 旧类名、jQuery 使用、图标库)

  • info:建议优化,非必须(如 direct_db、conf.json 版本、Hook 文件名)

管理员标记为「干货」,奖励 +10 积分
最新回复

请先登录后再回复 登录

uid:1 管理
关注
随遇而安,随缘而行
发帖 44
评论 236
粉丝 9
关注 1
发新帖
目录

扫码手机打开本帖