[审计]FlintHub — 安装链路审计 + PHP 8.4/8.5 兼容性报告(含实施计划)

👑Lv.11 元老 🌞 骨干会员
2026-10-25 00:29:16

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/待办事宜/PHP8.5兼容性与安装链路审计报告.md。

FlintHub — 安装链路审计 + PHP 8.4/8.5 兼容性报告(含实施计划)

项值
审计日期2026-09-15(当日增补:PHP 8.4 专项 + 真机实测 + 全部修复完成并打包)
审计范围安装链路(install.php + Schema::bootstrap/seedDefaults);全库 PHP 8.4 / 8.5 兼容性(app/ plugins/ cli/ lang/ + 根目录,共 568 个 PHP 文件)
本机环境Nginx 1.15.11 + PHP 8.0.2(另装 7.3.4 / 8.2.9);另下载便携版 PHP 8.4.25 / 8.5.10 用于真机验证
目标环境手机端 WSToolbox 内置 PHP(实测 8.4+);IIS + 腾讯云函数(每 5 分钟触发 cron_trigger.php)
结论安装链路可跑通;站点在 8.4.25 / 8.5.10 上 19 条路由全部正常;全部 11 处兼容性问题已修复,全库 568 文件编译期告警归零
交付update/3/(V1.0.3) —— 7 个源码文件 + version.php + 改动说明.md


一、结论摘要

安装链路

检查项结论依据
Schema::bootstrap() 建库骨架✅ 通过实测:1.6s 完成,新装库表结构与生产库逐表一致
Schema::seedDefaults() 默认数据✅ 通过settings 12 行、categories 1 行、level_config 11 行
幂等性(重复 bootstrap)✅ 通过第二次执行新建 0 条目、无报错
data/ 目录 web 防护✅ 通过.htaccess + web.config 均已生成
P-2 索引在新装路径生效✅ 通过idx_reply_index_status 已建
管理员账号创建✅ 代码审查通过未实跑(需真实 data 目录,避免污染)
协议勾选✅ 代码正确,未发现缺陷Alpine 绑定结构正确 + 已确认打包版 Alpine 支持 checkbox
协议勾选的历史成因⚠️ 已定位设计脆弱点见 §2.4

PHP 8.4(你手机服务器当前命中)

严重度数量说明状态
HIGH1session_set_save_handler() 传「独立回调」—— 每次请求必触发✅ 已修(§3.3 / H-2)
HIGH1隐式可空参数(ViewCounter.php)—— 每次请求必触发 2 条,经实测严重度由 MED 升为 HIGH✅ 已修(§3.3 / M-4)

PHP 8.4 只引入两类与本项目相关的弃用:session_set_save_handler() 独立回调 与 隐式可空参数。

其余告警项(ord() / curl_close / imagedestroy / finfo_close)均为 8.5 才引入,8.4 下不会触发。

两项都是「每请求级」日志刷屏,现已全部消除。

PHP 8.5

严重度数量说明状态
HIGH1ord() 传多字节字符串 —— 中文标题必然触发(已实测复现)✅ 已修(§3.2 / H-1)
MED7弃用函数:curl_close ×2、imagedestroy ×4、finfo_close ×1✅ 已修(§3.4)
LOW1gc_collect_cycles() 返回值语义变更(实测无告警)✅ 无需处理

本报告结论已用真实 PHP 8.4.25 / 8.5.10 便携版实测复核(见 §3.7),不是纯静态分析。

唯一的真实生产运行时证据来自你的手机服务器。


二、第一部分:安装链路审计

2.1 链路全貌

访问 install.php
  │
  ├─ ① 基础防护:error_reporting / 常量定义 / 载入 version.php
  ├─ ② 已安装判定:config.php 含 SPLITDB_DATA_PATH 或 data/meta/business.sqlite 存在
  │      └─ 命中 → renderAlreadyInstalled()(HTTP 410)并 exit
  ├─ ③ Session 加固:HttpOnly / SameSite=Lax / use_strict_mode / Secure(按 HTTPS 判断)
  │      └─ 生成 install_csrf 存入 session
  │
  ├─ ④ 分步向导(Alpine 控制,5 步)
  │      1 欢迎 → 2 许可协议(勾选) → 3 环境检测 → 4 管理员 → 5 安装中
  │
  └─ ⑤ POST action=install
         ├─ CSRF 校验(hash_equals,先 is_string 防护)
         ├─ 环境复检 envAllOk()
         ├─ 协议复检 $_POST['agree_license'] === '1'
         ├─ 字段校验(用户名 ≥2 / 邮箱格式 / 密码 ≥6)
         └─ runInstall()
              ├─ flock install.lock(非阻塞,防并发双装)
              ├─ session_write_close()(避免 config.php 的 session ini_set 告警)
              ├─ writeConfig() → 生成 config.php(MAIL_PASS_KEY 随机 32 字节)
              ├─ Schema::bootstrap()  ← 实测通过
              ├─ Schema::seedDefaults() ← 实测通过
              ├─ 创建管理员(users,role=admin,level=11,points=9999)
              ├─ 分配确定性 seed 头像
              └─ 原子写 protected/.install_done(tmp + rename)

2.2 核心链路实测结果

以临时 dataPath 跑真实代码(不触碰站点 data/),PHP 8.0.2:

【1】Schema::bootstrap() 完成,耗时 1625 ms,新建 9 个目录条目
【2】目录树:meta / meta/task_queue / meta/search / bucket/active/2026Q3
            / bucket/archive / extern / lock / log / runtime   全部 ✓
【3】meta 库:business.sqlite(23 表) / main_index.sqlite(2 表)
            / sessions.sqlite(1 表) / global_id.sqlite(id_generator)  全部 ✓
【4】队列库:queue_0 / queue_1 / queue_2  全部 ✓
【5】默认数据:settings 12 行 / categories 1 行 / level_config 11 行  ✓
【6】reply_index 索引:idx_reply_index_pid, idx_reply_index_uid,
                    idx_reply_index_status   ← P-2 新装路径生效 ✓
【7】data 防护文件:.htaccess ✓  web.config ✓
【8】幂等复跑:新建 0 条目,无报错 ✓

关键交叉验证 —— 新装库与生产库逐表比对:

库新装生产结果
meta/business.sqlite23 表23 表表名与数量完全一致
meta/main_index.sqlitereply_index, topic_index同一致
meta/global_id.sqliteid_generator同一致

→ 全新安装产出的库结构与现网一致,安装链路核心无缺陷。

2.3 协议勾选专项核查

先说结论:当前代码的勾选逻辑是正确的,没有发现缺陷。

实现方式(install.php:586-599):

...协议全文...
我已阅读并同意上述《许可协议》(MIT License) 下一步 →

逐环节验证:

环节验证结果
脚本加载顺序fhstate.js 带 defer(解析后执行),内联 installer() 在解析期即定义 → Alpine 启动时函数已就绪 ✅
x-data="installer()"内联函数声明在顶层 → 挂在 window,Alpine 可解析 ✅
x-model 支持 checkbox打包版内确认存在 t.type==="checkbox" 与 ["checkbox","radio"].includes(t.type)?"change":"input" ✅
x-cloak 移除打包版内确认 p("cloak",t=>queueMicrotask(()=>t.removeAttribute(...))) ✅
:disabled 简写打包版内确认 x-bind 实现存在 ✅
HTML 结构实测渲染 HTML 标签闭合正确、无嵌套错误 ✅
服务端复检$_POST['agree_license'] === '1' 兜底校验存在 ✅

未完成的一步(需说明):本应再用真实浏览器点击验证,但沙箱拦截了 Chromium 下载

(agent-browser install 写入 ...\chrome-153.0.8010.36\locales\et_NEUTER.pak 被拒),

故未做真人点击实测。这一环建议你自己 5 秒确认:

http://127.0.0.1/.workbuddy-ai/tmp/install-test/install.php

(该极小测试目录仅含 install.php / version.php / LICENSE / fhstate.js,

不依赖站点已安装状态,经你的 Nginx 已返回 200。)

2.4 发现的问题(安装链路)

问题 1|⚠️ 设计脆弱点:整个向导强依赖 Alpine,无降级

[x-cloak]{display:none!important} 配合 5 个步骤卡片全部带 x-cloak。

**若 assets/lib/fhstate.js 加载失败(404 / JS 被禁 / CDN 拦截 / 路径错误),

所有步骤卡片将永久 display:none** —— 页面只剩步骤指示条,

用户既看不到协议也无法勾选,更无法进入下一步。

这高度符合「协议不能勾选」的历史反馈特征:用户看到的不是"勾选框点不动",

而是"协议步骤没有内容/点了没反应"。

当前状态:install.php 与 assets/lib/fhstate.js 均与 flinthub1.0.0/ 发行版

逐字节一致,说明修复(若曾修复)已包含在发行版内。但**无 <noscript> 兜底、

无"Alpine 未启动"检测**这一脆弱性依然存在。

建议:加一段极轻量的兜底 —— 在向导末尾脚本里检测 window.Alpine,

若不存在则移除所有 x-cloak 并给出提示;或补 <noscript> 提示块。

(属加固项,非缺陷修复。)

问题 2|ℹ️ 服务端协议校验形同虚设(低危)

步骤 4 的表单里硬编码了 <input type="hidden" name="agree_license" value="1">,

而服务端只校验该字段等于 '1'。因此直接 POST 即可绕过协议勾选,

前端勾选框实际只起提示作用。协议为 MIT,风险可忽略,如需要可改为

"步骤 4 提交时由前端把 agreed 一并提交"再服务端校验。

问题 3|ℹ️ 无"安装前置自检"独立入口

环境检测只在向导第 3 步与 POST 时执行。若环境不达标,用户要先点完前两步才知道。

非缺陷,属体验优化空间。


三、第二部分:PHP 8.4 / 8.5 兼容性

3.1 扫描方法

  1. 取 PHP 官方 migration84.deprecated + migration85.incompatible + migration85.deprecated 全量条目,逐条转为可扫描规则;
  2. 对 app/ plugins/ cli/ lang/ + 根目录共 556 个 PHP 文件做正则扫描,

剔除注释行、字符串内反引号、已可空参数等误报;

  1. 对无法正则判定的项(解构非数组、浮点转 int、printf 精度、字符串自增)

做定向人工复核;

  1. 补充:首轮 8.5 扫描漏掉了 8.4 的 session_set_save_handler(),

后经用户手机服务器线上报错暴露,遂扩写为 scan_php81_85.py(覆盖 8.0–8.5)重扫全库,

补齐本节 §3.3。

3.2 HIGH(8.5)— 必修(1 处)

H-1 app/Helpers/CoverTemplate.php:155 — ord() 传多字节字符串

PHP 8.5 变更:ord() 传入非单字节字符串已弃用(官方注释:这通常是 bug 的征兆)。

触发条件(必然触发):

// CoverTemplate.php:118
$chars = \preg_split('//u', $title, -1, PREG_SPLIT_NO_EMPTY);  // 按「字符」切分

// CoverTemplate.php:126-131
foreach ($chars as $ch) { $u = self::charUnits($ch); ... }

// CoverTemplate.php:155
private static function charUnits(string $ch): float {
    $o = \ord($ch);      // ← 中文"中"是 3 字节,ord() 在此收到多字节字符串

preg_split('//u', ...) 按 UTF-8 字符切分,中文/全角字符是 2–4 字节。

即:任何含中文的标题在生成封面图时都会触发 E_DEPRECATED。

对一个中文论坛而言,这是最常见路径,且日志会被刷屏。

✅ 真机实测(PHP 8.5.10)—— 已确认触发:

实际报错原文:

Deprecated: ord(): Providing a string that is not one byte long is deprecated.
Use ord($str[0]) instead in app/Helpers/CoverTemplate.php on line 155

实测告警条数(与非 ASCII 字符数完全成正比):

测试标题8.5.10 告警数8.4.25 告警数
「PHP 8.5 兼容性改造实战:从告警刷屏到零告警」170
「你好世界」40
「FlintHub 1.0 发布:纯 SQLite 分片论坛(性能提升 300%)」100
「Hello FlintHub Release Notes」(纯 ASCII)00
6 个模板 × 中文标题各 9各 0

结论:8.5 下必触发、8.4 下不触发,与版本矩阵完全一致;纯 ASCII 标题不触发。

修复方向(语义等价,零行为变更):

private static function charUnits(string $ch): float
{
    if (\strlen($ch) > 1) {
        return 1.0;                       // 多字节 = 中文/全角,直接返回 1 单位
    }
    $o = \ord($ch);                       // 此时必为单字节,安全
    return ($o < 0x80) ? (\preg_match('/[MWmw@#%&]/', $ch) ? 0.9 : 0.5) : 1.0;
}

依据:\strlen($ch) === 1 时 ord() 必为单字节;>= 0x80 的单字节分支原样保留,

且原代码对多字节字符也是返回 1.0,故行为完全一致。

3.3 PHP 8.4 专项 — 线上实测命中项

本节由真实运行时报错驱动(用户手机服务器,PHP 8.4+),优先级高于静态扫描结论。

H-2 ✅ 已修 app/init.php:41 — session_set_save_handler() 传「独立回调」

PHP 8.4 变更:session_set_save_handler() 若传入「独立回调」(第 1~8 个参数为 callable),

必须改为传入一个实现 SessionHandlerInterface 的对象。

线上原始报错(用户提供):

Deprecated: session_set_save_handler(): Providing individual callbacks instead of
an object implementing SessionHandlerInterface is deprecated
in /storage/emulated/0/Android/data/com.xiaojiangi.wstoolbox/files/WSToolbox/flinthub/app/init.php on line 41

原实现:6 个闭包(open / close / read / write / destroy / gc)直接传给

session_set_save_handler()。该写法每次请求都会触发一次 E_DEPRECATED ——

对论坛而言等于每个页面请求都往日志写一行,日志会被迅速刷满。

修复方案(已落地):

文件变更
app/Core/SessionHandler.php新增。实现 \SessionHandlerInterface,6 个方法逐一对应原闭包
app/init.php:416 个闭包 → session_set_save_handler(new \app\Core\SessionHandler());

关键设计决策(防行为漂移):

  1. 刻意不实现 SessionUpdateTimestampHandlerInterface(validateId / updateTimestamp)。

原写法只传了 6 个回调,未提供第 7/8 个;PHP 在 session.use_strict_mode=1 下走的是

「read() 返回空串即判定为新 ID」的默认路径。若补上 validateId,会改变会话固定攻击防护的判定路径,

属于隐性行为变更,故明确不补。

  1. 失败返回值与原实现逐一对齐:read → ''、write → false、destroy → false、gc → 0。
  2. TTL 保持 1800 秒(expires = time() + 1800),与原闭包完全一致。
  3. 沿用 Database::getSessionConnection()(独立 PDO 连接,与业务连接物理隔离),

避免 SplitDB 的 2014 游标冲突。

实测验证(本机 PHP 8.0.2 + 真实 sessions.sqlite):

验证项结果
实现 SessionHandlerInterface✅
未实现 SessionUpdateTimestampHandlerInterface✅(保持原行为)
6 个接口方法全部实现✅ open, close, read, write, destroy, gc
返回类型与接口兼容✅ 全部一致
read(不存在) → ''✅
write → read 回读一致✅
UPSERT 覆盖写✅
destroy → read → ''✅
gc() 返回 int✅
php -l(8.0.2 / 8.2.9)✅ 无语法错误
HTTP 端到端:主页 63,863 字节、0 条告警✅
跨请求会话持久化:请求 1 与请求 2 的 CSRF token 完全一致✅ 证明落盘 + 读回均正常
数据库侧佐证✅ sessions 表记录 expires - now = 1800s

关于 PHP 7.3.4:php -l 在 7.3.4 下报联合类型 string|false 语法错误 —— 这是预期的。

项目本就要求 PHP 8.0+(install.php:74 有版本门禁),且 app/SplitDB/CountingPDO.php:32

早已使用 int|false。7.3 不在支持范围内,无需处理。

✅ 真机复核(PHP 8.4.25 / 8.5.10)—— 复现 + 修复双验证

用便携版 PHP 跑同一脚本,分别注册「旧闭包写法」与「新对象写法」:

PHP 版本旧写法(6 个闭包)新写法(对象)判定
8.0.2(对照)0 条(该版本尚未引入此弃用)0 条无法区分
8.4.251 条 ✗0 条 ✓✅ 复现 + 修复有效
8.5.101 条 ✗0 条 ✓✅ 复现 + 修复有效

8.4.25 / 8.5.10 上旧写法输出的告警与你手机上那条逐字一致:

Deprecated: session_set_save_handler(): Providing individual callbacks instead of
an object implementing SessionHandlerInterface is deprecated

这证明:① 你手机服务器的报错被准确复现;② 修复后该告警在 8.4 与 8.5 上均彻底消失。

M-4 ⚠️ 严重度由 MED 上调为 HIGH 隐式可空参数(2 处)

PHP 8.4 起弃用,8.5 仍弃用 —— 类型前缺 ? 会发 E_DEPRECATED。

文件:行现状应改为
app/SplitDB/ViewCounter.php:30public static function inc(int $topicId, PDO $mainDb = null): void?PDO $mainDb = null
app/SplitDB/ViewCounter.php:57public static function flush(PDO $mainDb = null): int?PDO $mainDb = null

佐证:两处 docblock 已写明 @param PDO|null $mainDb,即意图本就是可空,

只是语法漏了 ?。修复纯属补全,零行为变更。

✅ 真机实测:为什么它是 HIGH 而不是 MED

静态扫描时按「命中 2 处」记为 MED,但实测发现它每次请求都会触发 2 条:

[15-Sep-2026 11:44:51 Asia/Shanghai] PHP Deprecated:  app\SplitDB\ViewCounter::inc(): Implicitly marking
parameter $mainDb as nullable is deprecated, the explicit nullable type must be used instead
in ...\app\SplitDB\ViewCounter.php on line 30

[15-Sep-2026 11:44:51 Asia/Shanghai] PHP Deprecated:  app\SplitDB\ViewCounter::flush(): Implicitly marking
parameter $mainDb as nullable is deprecated, the explicit nullable type must be used instead
in ...\app\SplitDB\ViewCounter.php on line 57

触发链(关键在 index.php:285):

// index.php:280 —— 每次请求都会注册这个 shutdown 钩子
register_shutdown_function(function () {
    \app\Helpers\Settings::flushCounters();
    try {
        \app\SplitDB\ViewCounter::flush();   // ← index.php:285 无条件调用
    } catch (\Throwable $e) { ... }
});

ViewCounter::flush() 是无条件调用的,因此类文件 ViewCounter.php 每次请求都会被编译,

两个隐式可空参数就在编译期各发一条告警 → 每请求固定 2 条。

⚠️ 修正一处早先的判断:先前曾推测「未启用 APCu 时只有 inc() 一条路径会命中」——

这是错的。隐式可空参数是编译期告警,只要 ViewCounter.php 被加载就必然触发,

与 APCu 是否启用、是否真的浏览主题都无关。实测已证实每请求 2 条。

实测计数:19 条路由的冒烟测试中,8.4.25 与 8.5.10 均在 protected/error.log 中

留下 各 21 次 inc() + 21 次 flush() = 42 行(对应约 21 次请求)。

结论:这与 H-2 属于同一性质的问题(每请求级日志刷屏),

在 8.4 和 8.5 上都会触发。只改 2 个字符即可彻底消除,性价比最高,

建议与 H-2 同批发布。

⚠️ 顺带发现:日志被应用改道

config.php:85 有 ini_set('error_log', __DIR__ . '/protected/error.log');

——应用的告警不会写到 php.ini 指定的日志,而是全部落到

protected/error.log。排查线上问题时请直接看这个文件(手机端路径同理)。

3.4 MED — 建议修(7 处)

M-1 弃用函数:curl_close()(2 处)

PHP 8.5 起 curl_close() 已弃用(CurlHandle 对象自动释放)。

文件:行代码
app/Controllers/ApiController.php:371\curl_close($ch);
app/Helpers/Plugin.php:201curl_close($ch);

修复:直接删除该行。注意:删除前确认该句柄不再被引用(两个位置都是请求内局部句柄,安全)。

M-2 弃用函数:imagedestroy()(4 处)

PHP 8.5 起已弃用(GdImage 对象自动释放)。

文件:行代码
app/Controllers/ApiController.php:500imagedestroy($img);
app/Helpers/Upload.php:220if ($thumb === false) { \imagedestroy($src); return; }
app/Helpers/Upload.php:229\imagedestroy($src);
app/Helpers/Upload.php:240\imagedestroy($thumb);

修复:删除调用即可(对象在作用域结束/引用清零时自动释放)。

Upload.php:220 需保留 return;,仅删 \imagedestroy($src);。

M-3 弃用函数:finfo_close()(1 处)

文件:行代码
app/Helpers/Upload.php:58\finfo_close($finfo);

修复:删除该行。上下文是 $finfo 局部变量,finfo_file() 已调用完毕。

M-5 gc_collect_cycles() 返回值语义变更(1 处,LOW)

文件:行代码影响
cli/migrate_notifications_builtin.php:86gc_collect_cycles();无影响 —— 裸调用未使用返回值

修复:无需改动,登记备查即可。

3.5 已确认「清白」的项(无需处理)

以下变更点已逐项扫描,本项目未命中:

变更点核查结果
session_start() options 更严格(8.5)全站仅 2 处调用(app/init.php:115、install.php:48),均为无参调用 ✅
session_set_save_handler() 独立回调(8.4)~~原命中~~ → 已修,见 §3.3 H-2 ✅
PDO::FETCH_CLASS 构造函数参数 CUFA 语义全库无 FETCH_CLASS / fetchObject / FETCH_INTO / FETCH_LAZY ✅
PDOStatement::setFetchMode() fetch 期间调用全库无此调用 ✅
PDO fetch 常量值变更未使用 FETCH_GROUP/UNIQUE/CLASSTYPE/PROPS_LATE/SERIALIZE ✅
SplFileObject::fwrite() length 默认值变更全库无 SplFileObject / ->fwrite() ✅
PDO::sqliteCreateFunction/Collation/Aggregate全库无调用 ✅
class_alias("array"/"callable")无 class_alias 调用 ✅
disable_classes INI 被移除代码中无引用 ✅
setlocale() 整数 0无 setlocale 调用 ✅
zend_extension=opcache 告警代码中无该配置(但需检查服务器 php.ini,见 §4.3)
非规范类型转换 (boolean)/(integer)/(double)/(binary)全库无 ✅
case X; 分号结尾全库 0 处 ✅
反引号运算符(shell_exec 别名)初筛 11 处全部为误报(注释 / SQL 反引号 / preg 正则内),实际 0 处 ✅
__sleep() / __wakeup() / __debugInfo()全库无 ✅
readdir(null) / rewinddir(null) / closedir(null)全库无 ✅
$http_response_header全库无 ✅
ReflectionProperty::setAccessible()全库无 ✅
SplObjectStorage / ArrayObject / ArrayIterator全库无 ✅
DATE_RFC7231全库无 ✅
report_memleaks / register_argc_argv代码中无引用 ✅
SimpleXMLElement::xpath()全库无 ✅
spl_autoload_call 注销加载器全库无 ✅
chr() 越界仅有 ord()(见 H-1),无 chr() ✅
解构非数组值告警全库 14 处解构均来自函数返回的数组(explode / config / 自定义方法) ✅
字符串自增弃用所有 $x++ 均为整数计数器($i / $count / $hops) ✅
printf 系列精度处理变更全库无 %f / %F 浮点格式符 ✅
浮点数转 int 告警无静态可判定的可疑转换(需运行时观察) ⚠️
$_SESSION 键含管道符 `\`
mbstring Unicode 17.054 个文件使用 mb_* 函数,属数据表更新,正常业务无影响 ✅

3.6 风险总览

编号严重度版本文件行问题触发频率修复成本状态
H-2HIGH8.4+app/init.php + app/Core/SessionHandler.php41session_set_save_handler() 独立回调每请求 1 条新增 1 类 + 改 1 行✅ 已修
M-4HIGH ⬆8.4+app/SplitDB/ViewCounter.php30 / 57隐式可空参数每请求 2 条加 2 个 ?✅ 已修
H-1HIGH8.5app/Helpers/CoverTemplate.php155ord() 收多字节字符串每次生成封面(中文标题)3 行✅ 已修
M-1MED8.5app/Controllers/ApiController.php371curl_close() 弃用每次远程拉图删 1 行✅ 已修
M-1MED8.5app/Helpers/Plugin.php201curl_close() 弃用每次插件远程请求删 1 行✅ 已修
M-2MED8.5app/Controllers/ApiController.php500imagedestroy() 弃用每次图片处理删 1 行✅ 已修
M-2MED8.5app/Helpers/Upload.php220 / 229 / 240imagedestroy() 弃用每次上传含缩略图删 3 处✅ 已修
M-3MED8.5app/Helpers/Upload.php58finfo_close() 弃用每次上传删 1 行✅ 已修
M-5LOW8.5cli/migrate_notifications_builtin.php86gc_collect_cycles 语义—无需改✅ 实测无告警

合计:7 个文件(1 新增 + 6 修改)、11 个代码位置,全部为「删除调用」或「补 1 个字符」级别,无逻辑重构。

全部 ✅ 已修复,已打包为 update/3/(V1.0.3)。

修复后复核:全库 568 个 PHP 文件在 8.4.25 与 8.5.10 下编译期告警归零;

19 条路由冒烟后 protected/error.log 为空(修复前为 42 行)。详见 §3.7。


3.7 真机实测记录(PHP 8.4.25 / 8.5.10)

为把结论从「静态分析」升级为「实测」,本轮下载了 PHP 官方便携版

8.4.25 与 8.5.10(Windows NTS x64)并配置 pdo_sqlite / mbstring /

fileinfo / curl / gd / openssl,做了四组验证。

7.1 全库编译期扫描(568 个 PHP 文件)

用 php -l 对 app/ cli/ plugins/ lang/ update/ 待办事宜/ + 根目录共 568 个 PHP 文件

逐一做编译检查(脚本:.workbuddy-ai/tmp/scan_compile_84_85.py):

PHP 版本有编译期告警的文件告警数
8.4.251 个(app/SplitDB/ViewCounter.php)2
8.5.101 个(同上)2

✅ 这是权威结论:全库除 ViewCounter.php 外,再无任何编译期弃用

(包括此前静态扫描怀疑的各类隐式可空参数)。比正则扫描可靠得多。

7.2 站点路由冒烟(19 条路由)

用 PHP 内置服务器 + 模拟 nginx try_files 的路由脚本

(.workbuddy-ai/tmp/router.php)跑真实路由:

路由8.4.258.5.10
/、/index.php、/forum、/login、/register、/forgot-password200200
/blog、/tags、/thread/1、/blog/1、/tag/1、/forum/category/1200200
/search、/message、/notifications、/profile、/post/new、/admin302(未登录跳转,正确)302
/assets/css/modern.css200200

两个版本结果完全一致,页面字节数一致(首页 63,863 字节)。

⚠️ 但日志并非干净 —— 见 7.3。

7.3 运行期告警实测

项PHP 8.4.25PHP 8.5.10报错原文
H-2 旧写法 session_set_save_handler()1 条 ✗1 条 ✗Providing individual callbacks instead of an object implementing SessionHandlerInterface is deprecated
H-2 新写法(对象)0 条 ✓0 条 ✓—
M-4 隐式可空参数(ViewCounter)2 条 / 请求 ✗2 条 / 请求 ✗Implicitly marking parameter $mainDb as nullable is deprecated...
H-1 ord() 多字节(中文标题)0 ✓17 / 4 / 10 条(随非 ASCII 字符数) ✗ord(): Providing a string that is not one byte long is deprecated. Use ord($str[0]) instead
M-1 curl_close()0 ✓1 条 ✗Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0
M-2 imagedestroy()0 ✓1 条 ✗Function imagedestroy() is deprecated since 8.5, as it has no effect since PHP 8.0
M-3 finfo_close()0 ✓1 条 ✗Function finfo_close() is deprecated since 8.5, as finfo objects are freed automatically
M-5 gc_collect_cycles()0 ✓0 条 ✓—

结论:版本矩阵 100% 得到实测验证 —— 8.4 只中 H-2 与 M-4,其余为 8.5 专属。

7.4 关键运维发现

  1. 告警不写在 php.ini 指定的日志里 —— config.php:85 会

ini_set('error_log', __DIR__ . '/protected/error.log')。

排查线上问题请直接看 protected/error.log(手机端同理)。

  1. 实测 19 条路由后,protected/error.log 中新增 42 行 M-4 告警,

证明「每请求 2 条」的判断成立。

  1. 日志中另有 15 条 Template not found: home/index_default (/),

均为 9-14 的历史记录,本轮冒烟未复现,且 app/Views/home/index_default.php

文件存在 —— 属已解决的旧问题,与 PHP 版本无关。


四、第三部分:实施结果

4.0 全部完成 ✅

2026-09-15 已按用户指令一次性完成全部 11 处修复,并打包为 update/3/(V1.0.3)。

编号项文件变更状态
H-2session_set_save_handler() 独立回调新增 app/Core/SessionHandler.php;改 app/init.php:416 闭包 → 传对象✅
M-4隐式可空参数app/SplitDB/ViewCounter.php:30 / 57PDO $x = null → ?PDO $x = null✅
H-1ord() 多字节app/Helpers/CoverTemplate.php:155前置 strlen($ch) > 1 判断✅
M-1curl_close() ×2ApiController.php:371、Plugin.php:201删调用✅
M-2imagedestroy() ×4ApiController.php:500、Upload.php:220/229/240删调用(220 保留 return;)✅
M-3finfo_close() ×1Upload.php:58删调用✅

交付物:update/3/ —— 7 个源码文件 + version.php(V1.0.3)+ 改动说明.md(9 个文件)。

不涉及 CSS/JS(sw.js 无需递增)、不涉及数据库结构、不涉及配置项。

4.1 修复后复核结果

复核项结果
全库编译期扫描(568 个 PHP 文件 × 8.4.25 / 8.5.10)告警归零 ✅(修复前:1 文件 / 2 条)
19 条路由冒烟(8.4.25 / 8.5.10)全部正常;首页均 63,863 字节,与修复前一致 ✅
protected/error.log(冒烟后)空 ✅(修复前 42 行)
H-1 中文标题告警(8.5.10)17 条 → 0 条 ✅
H-1 输出一致性(30 个 SVG)逐字节一致,内容哈希 5b377287…b2772 ✅
M-4 反射签名两处 allowsNull=true,类型 ?PDO ✅
功能回归(缩略图 / finfo / 插件 HTTP / 拉图 / captcha / 封面预览)全部正常、0 告警 ✅
语法检查(8.0.2 / 8.4.25 / 8.5.10)7 个文件全通过 ✅

4.2 验证方法(可复跑)

验证项方法
H-2① .workbuddy-ai/tmp/test_session_handler.php 接口 + 功能双向验证;② HTTP 端到端:主页 63,863 字节 / 0 告警;③ 跨请求会话持久化:两次请求 CSRF token 一致;④ 查 sessions 表确认 expires - now = 1800s;⑤ 8.4.25 / 8.5.10 真机:旧写法 1 条告警 → 新写法 0 条(test_84_85_session.php)
M-4① 反射:(new ReflectionMethod('app\SplitDB\ViewCounter','inc'))->getParameters()[1]->allowsNull() 应为 true;② 权威验证:php84/php85 -l app/SplitDB/ViewCounter.php 不再输出任何 Deprecated;③ 冒烟 19 条路由后确认 protected/error.log 无新增
H-1造含中文的标题调用 CoverTemplate::generateCover();在 8.5.10 下确认告警数由 17 → 0;比对生成 SVG 与改前逐字节一致(test_h1_ord.php + gen_svg.php)
M-1/2/3触发远程拉图(ApiController)、附件上传含缩略图(Upload)、插件远程请求(Plugin)、/api/captcha、/api/cover-preview;在 8.5.10 下确认无 Deprecated(test_med_85.php + test_med_functional.php)
全阶段php -l 语法检查;已具备 8.4.25 / 8.5.10 便携版,可直接做真机冒烟(见 §3.7)

4.3 后续可选:安装链路加固(阶段 D,未做)

项内容涉及文件预估
阶段 DAlpine 未启动兜底 + <noscript> 提示(防「协议勾选框看不见/点不动」历史问题复发)install.php约 15 行

本次未做(不在用户指令范围内)。属锦上添花,建议单独评估后再定。

4.4 上线前你需自行确认的环境项(代码之外)

先确认手机服务器到底是哪个版本(决定上面哪些项真的会命中):

版本会命中的项建议动作
8.4.xH-2(已修)+ M-4补发 H-2 + M-4 即可
8.5.xH-2(已修)+ M-4 + H-1 + M-1 + M-2 + M-3建议一次性全修

查版本的方法:手机端建一个 pv.php,内容 <?php echo PHP_VERSION;,浏览器访问即可。

或在 WSToolbox 的 PHP 设置页看版本号。

看日志的正确位置:不是 php.ini 里 error_log 指向的文件,

而是站点目录下的 protected/error.log(config.php:85 会改写 error_log)。

其余环境项:

  1. php.ini 是否含 zend_extension=opcache.so / php_opcache.dll

→ PHP 8.5 起 opcache 始终内置,该行会触发告警,应删除。(8.4 无此问题)

  1. php.ini 是否含 disable_classes → 8.5 已移除该指令,应删除。
  2. php.ini 的 register_argc_argv → 建议显式设 0(非 CLI 从查询串推导 argc/argv 已弃用)。
  3. display_errors / error_reporting → 建议生产关闭 E_DEPRECATED 显示,

避免升级后日志/页面被弃用告警污染(但不应掩盖 H-1 / M-4,仍应修)。

  1. 服务器 PHP 是否已装 pdo_sqlite / mbstring / fileinfo / gd 或 imagick / curl / openssl

→ 与 install.php 的环境检测项一致。

  1. ~~是否启用 APCu~~ → 已证实与 M-4 无关(编译期告警,无条件触发),无需确认。

4.5 红线约束(沿用既有约定)

  • 本次只动上述 7 个文件;H-2 例外地新增了 app/Core/SessionHandler.php

(新增独立类,未触碰 app/Core/ 既有任何文件);

  • 不碰 app/Controllers/Admin/、其它插件;
  • 所有 SQL 走 prepare() + 参数绑定;
  • 输出统一 $this->e() / htmlspecialchars();
  • 所有文件 UTF-8 无 BOM + LF;
  • 打包放 update/3/,版本文件末位数字 = 目录号;若涉及 CSS/JS 需同步递增 sw.js 版本。

(H-2 与 M-4 均不涉及任何 CSS/JS,故若只发这两项,sw.js 无需递增。)

4.6 回滚方案

变更回滚方式
任意文件用上一版文件覆盖即可,无 schema 变更、无数据迁移
H-2恢复 app/init.php 旧版(6 个闭包版)即可;app/Core/SessionHandler.php 可留着不影响(无引用即无副作用)。sessions 表结构与数据格式未变,新旧实现可互相读取已存会话 → 回滚不会导致任何在线用户掉线
阶段 D(未做)—

本次全部改动均为「消除告警」性质,不涉及业务逻辑、SQL、模板与静态资源,回滚后行为与升级前完全一致。


五、附录

5.1 审计产物

文件说明
.workbuddy-ai/tmp/scan_php85.pyPHP 8.5 兼容性扫描器(可复跑,已修正误报)
.workbuddy-ai/tmp/scan_php81_85.pyPHP 8.0–8.5 扩面扫描器(H-2 即由此发现)
.workbuddy-ai/tmp/php85_findings.json8.5 扫描明细
.workbuddy-ai/tmp/php81_85_findings.json8.0–8.5 扫描明细
.workbuddy-ai/tmp/audit_nullable.py隐式可空参数精确审计(跨行签名,已修正误报)
.workbuddy-ai/tmp/php85_extra.json补充项明细
.workbuddy-ai/tmp/test_install_core.php安装核心链路验证脚本(可复跑)
.workbuddy-ai/tmp/test_session_handler.phpH-2 验证脚本(接口一致性 + 功能 + 清理,可复跑)
.workbuddy-ai/tmp/php84/、php85/PHP 8.4.25 / 8.5.10 便携版(含自建 php.ini),真机实测环境
.workbuddy-ai/tmp/scan_compile_84_85.py全库编译期弃用扫描器(调 php -l,最权威的静态手段)
.workbuddy-ai/tmp/test_84_85_session.phpH-2 新旧写法对比测试(8.4/8.5 复现 + 修复验证)
.workbuddy-ai/tmp/test_h1_ord.phpH-1 中文标题 ord() 告警测试(8.5 触发 / 8.4 不触发)
.workbuddy-ai/tmp/test_med_85.phpM-1~M-5 逐项告警测试(版本矩阵验证)
.workbuddy-ai/tmp/router.php + crawl.sh模拟 nginx try_files 的内置服务器路由 + 19 路由冒烟脚本
.workbuddy-ai/tmp/gen_svg.php + svg_before/ svg_after/H-1 输出一致性验证(30 个 SVG 逐字节比对)
.workbuddy-ai/tmp/test_med_functional.phpM-1/2/3 功能回归(缩略图 / finfo / 插件 HTTP / 拉图)
.workbuddy-ai/tmp/diff/update3.diffupdate/3 的完整改动 diff(218 行,含全部 7 个文件)
.workbuddy-ai/backup/20260915_errorlog/清空前备份的 protected/error.log(57 行)
.workbuddy-ai/tmp/install-core-data/测试生成的临时库(可人工检查后删除)
.workbuddy-ai/tmp/install-test/极小安装向导测试目录(4 文件,可经 Nginx 访问)
.workbuddy-ai/tmp/install-e2e/⚠️ 未使用的完整项目副本(待清理,见 §5.2)
.workbuddy-ai/tmp/home_check.html、ck.txtH-2 端到端验证的响应快照与 cookie jar(可删)
.workbuddy-ai/tmp/php84.zip、php85.zipPHP 便携版安装包(各约 35MB,可删)

交付物(源码侧):

文件变更
app/Core/SessionHandler.php新增(H-2)
app/init.php修改第 41 行(H-2)
app/SplitDB/ViewCounter.php修改第 30 / 57 行(M-4)
app/Helpers/CoverTemplate.php修改第 155 行(H-1)
app/Controllers/ApiController.php删 2 处(M-1 / M-2)
app/Helpers/Plugin.php删 1 处(M-1)
app/Helpers/Upload.php删 4 处(M-2 / M-3)

发布包:update/3/(V1.0.3)—— 上述 7 个文件 + version.php + 改动说明.md;

另有 update/增量补丁1.0.3.zip(同内容压缩包,便于上传)。

5.2 遗留事项

  1. 待清理目录:.workbuddy-ai/tmp/install-e2e/(一次未采用的整项目复制尝试留下)、

.workbuddy-ai/tmp/install-core-data/(测试库)、.workbuddy-ai/tmp/php84、php85

(便携版 + zip,约 140MB)。等你确认后我再删(保留着方便后续复测)。

  1. 未完成的实测:协议勾选的真人点击验证(Chromium 下载被沙箱拦截)。

可自行访问 http://127.0.0.1/.workbuddy-ai/tmp/install-test/install.php 确认。

  1. ~~8.4 / 8.5 运行时实测缺失~~ → 已解决:本轮下载了 8.4.25 / 8.5.10 便携版,

全部结论已实测复核(见 §3.7)。

  1. ~~M-4 尚未修~~ → 已修,见 §4.0。
  2. ~~H-2 / M-4 尚未打包~~ → 已打包为 update/3/(V1.0.3),含全部 7 个源码文件。
  3. 是否要同步 flinthub1.0.0/ 发布目录:该目录是发布快照,

本次修复未同步进去(保持快照纯净)。若要做下一个发行版,需另行同步。

  1. protected/error.log 已清空:原 57 行(含 15 条 9-14 的 Template not found 历史记录)

已备份到 .workbuddy-ai/backup/20260915_errorlog/error.log.before-truncate,当前为空。

  1. app/Helpers/Plugin.php 使用 CRLF 换行 —— 既有状态,非本次引入

(flinthub1.0.0 与 update/1 中该文件同样为 CRLF)。本次刻意保持原样以免产生整文件 diff。

全库共 15 个文件含 CRLF,若要统一为 LF 建议单独做一次格式化提交。

  1. 阶段 D(安装链路加固)未做 —— 不在本次指令范围内,见 §4.3。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 2 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录