[伪静态路径] 修复 10 个文件中硬编码 /view/img/avatar.png 导致子目录安装失效
前台: sidebar_user_card.inc.htm, my_avatar.htm, forum_followers.htm, forum.htm, my.common.template.htm
后台: audit.htm(3处), security_audit.htm(3处), admin_notice_list.inc.htm
核心: lib/avatar_component.php, model/post.func.php
修复内容: 所有硬编码路径改为 $conf['view_url'].'img/avatar.png'; 所有缺少 this.onerror=null 的 onerror 加上防死循环
必测: 子目录安装下头像加载、头像加载失败回退、后台审计/通知页头像
[伪静态路径] 引入 base_path 子目录安装支持机制
index.php: 从 SCRIPT_NAME 自动检测安装子目录(如 /xiunox),存入 $conf['base_path'],支持 conf.php 手动覆盖
index.php: view_url/upload_url/logo 路径改用 base_path 前缀,替代原来强制 / 开头
conf.default.php: view_url/upload_url/logo 默认值去掉 / 前缀(改为相对路径,由 index.php 自动加 base_path)
必测: 根目录部署、子目录部署、admin 后台、CDN 配置场景
[伪静态路径] 改造 url() 函数支持子目录安装
model/misc.func.php: url() 生成路径前拼接 base_path 前缀,admin 保留 ./ 相对路径逻辑
model/misc.func.php: 新增 default_avatar_url() 辅助函数,统一默认头像路径入口
必测: 6种伪静态格式(url_rewrite_on=0~5)的 URL 生成、admin 后台 URL、空路由首页跳转
[伪静态路径] 同步 JS 端 URL 生成函数支持 base_path
view/htm/footer.inc.htm: 注入 base_path 全局变量
view/js/xiuno.js: xn.url() 添加 base_path 前缀,admin 用 ./ 相对路径
view/js/xiuno-modern.js: XN.url() 同步改造,移除强制 / 前缀改用 base_path
必测: 前台 AJAX 请求 URL、分页链接、htmx 请求路径
[伪静态路径] 批量替换核心代码硬编码 /view/img/avatar.png 为 default_avatar_url()
涉及 16 个文件约 30 处: model/(thread/user/notify).func.php, route/(notice/my/forum/rank/user/post).php, service/RankService.php, lib/avatar_component.php, api/v1/(my/notify/user).php, admin/(route/user.php, view/htm/banned_user_list.htm)
修复内容: 字符串字面量替换为函数调用;onerror 属性加 this.onerror=null 防死循环
必测: 帖子列表头像、用户主页头像、通知列表头像、排行榜头像、API 返回的默认头像
[伪静态路径] 修复 index.inc.php 301 重定向子目录兼容问题
旧格式检测正则改为兼容子目录: /xiunox/?user-1.htm 也能匹配
url_rewrite_on=5 两处 url() 调用改为手动生成相对路径,避免与 http_url_path() 拼接导致 base_path 重复
必测: 子目录安装下旧格式 URL 301 跳转、跨格式重定向(url_rewrite_on=3/4/5)
本次学到的教训
重构者引入"绝对路径更好"的假设时,没有考虑子目录安装场景,一刀切把原版 Xiuno 的相对路径机制废掉,导致子目录安装全线失效。引入新机制时必须验证不破坏原有兼容性。
今日更新后需要测试的地方
[必测] 根目录部署: 首页、发帖、回帖、登录、注册、找回密码、用户主页、版块列表、排行榜
[必测] 子目录部署(如 /xiunox/): 同上全部功能,重点验证静态资源加载、URL 跳转、CSRF token
[必测] admin 后台: 所有管理页面链接不跳到前台、表单提交、插件设置页
[必测] 6种伪静态格式: 依次切换 url_rewrite_on=0~5,验证 URL 生成和 301 重定向
[必测] 头像加载: 正常头像、头像加载失败回退、默认头像显示
[回归] CDN 配置: view_url 配置为完整 URL(http://cdn.com/view/)时不受 base_path 影响
[回归] 反向代理: 如 Nginx 反代到子路径,SCRIPT_NAME 可能不准,需手动配 $conf['base_path']
其他修复
[逻辑错误] 修复帖子详情页右侧栏二维码/复制按钮的帖子地址多一个斜杠(
https://xiuno.xcxgy.cn//thread-362.html)根因:
http_url_path()末尾带/,thread_url($tid)返回值带前导/,route/thread.php 第 896 行直接拼接产生双斜杠修复: 改用
rtrim(http_url_path(), '/') . thread_url($tid)去重必测: 任意帖子详情页右侧栏「扫码访问」区块,确认二维码地址、显示网址、复制按钮三者均为单斜杠
https://域名/thread-{tid}.html回归: canonical 标签、og:url、json-ld 结构化数据 URL 同步生效(共用 $thread_url 变量)
影响范围: 仅 route/thread.php 一处,全站仅此一处使用
http_url_path() . xxx_url()拼接模式
[语言包不同步] 修复发帖页"字数要求:最少 10 字字"多出一个"字"字(char_unit 与 char_max_unit 拼接重复)
必测: ?thread-create-{fid}.htm 标题与正文字数提示
三语言包同步: zh-cn / zh-tw / en-us
[逻辑错误] 修复 admin 后台出现 /admin/admin/audit 双重路径
index.inc.php: 301 重定向逻辑加 admin 排除条件,admin 下不执行 301 重定向
根因: 改正则支持子目录时,新正则 (?:^|/)? 误匹配了 /admin/?audit.htm,导致 admin 下触发前台 301 重定向,http_url_path() 返回带 /admin/ 的路径,拼接后变成 /admin/admin/audit
必测: admin 后台所有菜单链接不出现双重路径
[伪静态路径] 修复 SEO/canonical/sitemap/301 双重 base_path 问题
model/misc.func.php: 新增 absolute_url() 辅助函数,剥离 url() 结果中的 base_path 后用 http_url_path() 拼接完整 URL
route/thread.php: 5处 http_url_path().url() 拼接改为 absolute_url()(canonical、og:image、JSON-LD 作者 URL×2、回帖作者 URL)
route/forum.php: canonical 改为 absolute_url(forum_url($fid))
route/sitemap.php: 版块/帖子 URL 改为 absolute_url()
index.inc.php: 旧格式 301 重定向剥离 base_path 前缀避免与 http_url_path() 重复
必测: 子目录安装下帖子页 canonical、og:url、JSON-LD、sitemap.xml、旧格式 URL 301 跳转
回归: 根目录安装 SEO 标签正常
[伪静态路径] 修复模板硬编码绝对路径(10个文件16处)
view/htm/header_nav.inc.htm: href="/" → base_path, href="/admin/" → base_path
view/htm/footer.inc.htm: /lang/ 脚本路径 → base_path
view/htm/footer_nav.inc.htm: /view/img/xiunox.svg → view_url
view/htm/forum_index.htm, thread_main.inc.htm, forum_followers.htm: /view/img/forum.png → view_url
admin/view/htm/forum_update.htm, forum_list.htm: /view/img/forum.png → view_url
view/htm/forum_members_block.htm, sidebar_right.inc.htm, my_avatar_page.htm, banned.htm: avatar onerror 加 onerror=null + default_avatar_url()
必测: 子目录安装下首页logo、后台入口、语言JS、版块图标、头像加载
[伪静态路径] 修复 PHP 层硬编码默认版块图标和预设头像
model/forum.func.php: 默认图标 /view/img/forum.png → $conf['view_url'];自定义图标补 / 改为补 base_path
service/ForumService.php: 默认图标 → conf[′viewurl′],新增globalconf
model/user.func.php: 预设头像 → conf[′viewurl′],新增globalconf
必测: 版块图标显示、自定义图标、头像预设选择
[伪静态路径] 修复 bbs.js 删除附件 AJAX URL 硬编码
view/js/bbs.js: '/attach-delete-' + aid → XN.url('attach-delete-' + aid)
admin/view/htm/forum_list.htm: forum_frontend_url() 补 base_path 前缀
必测: 子目录安装下发帖删除附件、后台版块列表前台链接
[类型误用] 修复 attach_assoc_post() 中 SESSION["data"] 为 null 时传入 pregmatch 触发 PHP 8.1+ 弃用警告
model/attach.func.php 第 279 行: preg_match 第二参数 $subject 接收 null 被弃用,改用
_SESSION["data"] ?? ""兜底必测: 发帖/回复时 session 无 data 字段场景、带临时附件发帖场景
[类型误用] 修复 admin/route/setting.php 状态标签颜色 statuscolor[k] 未兜底 isset 可能传 null 给 preg_match
第 899 行: 与 textcolor的isset兜底对齐,补上‘isset(status_color[$k]) ? ... : "#6c757d"`
必测: admin/?setting-display.htm 状态标签配置保存(含删除部分颜色项场景)
CSRF 防护强化
[CSRF防护缺失] CsrfService token 从 session 改为 cookie 存储,解决 session GC(1h) 清除 token 导致发帖 CSRF 失效
必测: 登录后发帖/回复/快捷回复(停留1h+后提交不再报 CSRF 无效)
必测: 登录/注册/点赞/收藏/关注等所有 POST 表单
[CSRF防护缺失] 前台 index.inc.php 增加中央化 CSRF 校验(与 admin 对齐)
必测: 任意前台 POST 请求(已有 check 的路由不受影响)
[CSRF防护缺失] dice.php 投注接口改为只接受 POST,修复 GET 请求跳过 CSRF 直接扣积分
必测: /dice 页面正常投注;GET /dice-play 应返回 405
[CSRF防护缺失] theme.php 主题切换补 CSRF 校验 + 限制管理员
必测: 非管理员 POST theme 应被拒;管理员正常切换
[CSRF防护缺失] invite/api.php GET 分支不再自动生成邀请码,改为仅查询
必测: GET api?action=code 返回已有码或空;POST 生成新码
[信息泄露] xnx_magic/api.php 移除日志中的 CSRF token 明文,改用 hash_equals
必测: /magic 提交正常;检查 magic_error 日志无 token 明文
[类型误用] ai.php、xnx_api_tester/admin.php 的 $_SESSION['csrf_token'] 改为 CsrfService::getToken()
必测: AI 对话功能正常;api_tester POST 正常
版块管理与后台体验修复
[逻辑错误] 修复版块列表分区循环覆盖子版块导致版块消失
根因: forum_list.htm 第16行
$sub_forums_map[$_fid] = array();在分区被处理时无条件重置数组,当子版块(fid小、rank相同)先于分区循环时,已收集的子版块被覆盖丢失修复: 改为
if(!isset($sub_forums_map[$_fid])) $sub_forums_map[$_fid] = array();仅在不存在时初始化必测: admin/?forum-list.htm 安装后默认版块fid1移动到新建分区内,fid1应在分区下显示
回归: 多分区多子版块列表显示完整
[组件混用] forum_create.htm / forum_update.htm 的 jQuery 代码改为原生 JS
根因: 两文件使用 jQuery 绑定 toggleFupRow(选类型为分区时隐藏上级分区),依赖
$全局;若 jQuery 加载时序/路径异常则整个 script 静默失败,导致选分区时上级分区字段不隐藏修复: 改为原生 addEventListener + fetch,与 forum_list.htm 风格一致
必测: admin/?forum-create.htm 选类型为分区时上级分区字段隐藏;admin/?forum-update-1.htm 同样
回归: 表单提交(AJAX+toast)、图标预览、权限表全选(update)正常
[用户体验] 后台令牌失效不再显示整页错误卡片,改为 toast 提示 + 自动跳转登录页
根因: admin_token_check() 令牌失效时调用 message() 渲染 admin/view/htm/message.htm 整页,用户点返回后令牌仍失效陷入循环
修复: 新增 admin_token_expiry_redirect(),通过 flash cookie 传递 toast 消息 + 303/HX-Redirect 跳转登录页
必测: 后台停留超过1小时后操作,应直接跳登录页并显示 toast「管理登陆令牌失效,请重新登录」
必测: HTMX 请求中令牌失效,应整页跳转登录页(非局部 swap)
[CSS优先级] 修复选类型为分区时上级分区字段不隐藏(d-flex !important 覆盖 display:none)
根因: fup_row/moduids_row 的 div 使用 Bootstrap d-flex 类(display:flex !important),JS 设置 style.display="none"(非 important)被 !important 覆盖,隐藏无效;之前的 jQuery .hide() 同理从未生效
修复: 改用 setProperty("display", "none", "important") 设置 inline !important 覆盖 class !important;恢复时 removeProperty("display") 回到 d-flex
必测: admin/?forum-create.htm 选类型为分区时上级分区字段隐藏;admin/?forum-update-1.htm 选分区时上级分区+版主字段隐藏
回归: 选回版块类型时字段恢复显示
日志管理
[配置不同步] 修复 update_20260708/09/10.md 出现双份日志(workspace 根目录 + xiunobbs-master/ 子目录)
根因: 项目代码在 xiunobbs-master/ 子目录,但 workspace 根目录是 cwd;不同会话 Glob 搜索范围不一致,误判日志不存在后用 Write 新建,产生分叉
修复: 统一日志目录到 workspace 根目录(/Users/hfbi/Desktop/2026/xiuno/update_YYYYMMDD.md),合并 07-07/08/09/10 四份分叉日志,删除 xiunobbs-master/ 下的副本
规则沉淀: bugfix_rules.md 1.3 节明确日志统一目录 + Glob 递归搜索要求
[伪静态路径] Nginx 伪静态规则根据 base_path 动态适配子目录安装
admin/route/setting.php: 检测 base_path,子目录安装时 location 路径自动加子目录前缀(如 location /xiunox/)
根目录安装不受影响(location / 不变)
必测: 子目录安装下后台伪静态设置页显示的 Nginx 规则
[逻辑错误] 切换固定链接形式时提交前提醒用户配置伪静态规则
admin/view/htm/setting_permalink.htm: 从默认模式(0)切到伪静态模式(1,3,4,5)时弹出 Bootstrap Modal 确认框
确认框引导用户先配置 rewrite 规则再保存,避免切换后网站无法访问
三种语言包同步新增 4 个 key(admin_permalink_confirm_rewrite_title/body/continue/cancel)
必测: 后台伪静态设置页从默认模式切到伪静态模式点保存时的弹窗
回归: 伪静态模式间切换不弹窗、默认模式不弹窗
[伪静态路径] 修复插件代码硬编码 /view/img/avatar.png 导致子目录安装失效(15个文件约30处)
PHP 文件(5个): xnx_hidden/HiddenService.php(748行), xnx_medal/MedalService.php(1025/1062行), xnx_landing/landing.php(22行), xnx_invite/InviteService.php(411/413行), xnx_status/hook/index_site_brief_after.htm(33行)
HTM 模板(10个): xnx_checkin(3文件5处), xnx_medal(4文件9处), xnx_landing/landing.htm(272行), xnx_invite/invite_center.htm(4处)
额外修复: xnx_status/admin/view/htm/status_admin.htm(559行, 任务清单遗漏)
PHP 上下文用 default_avatar_url();模板 HTML 属性用 img/avatar.png;JS 字符串用 json_encode 注入
必测: 子目录安装下签到排行、勋章详情/侧栏/设置/后台、邀请中心、Landing 页、隐藏内容解锁列表、贡献者卡片头像加载
[逻辑错误] 修复后台固定链接页伪静态规则折叠面板需手动展开
admin/view/htm/setting_permalink.htm: 移除 collapse 折叠组件,切换伪静态模式时 Nginx/Apache 规则直接展示
必测: admin/?setting-permalink.htm 切换到伪静态模式,规则直接可见可复制
[伪静态路径] 修复后台顶部导航头像在子目录安装下路径重复(..//xiuno/view/img/avatar.png)
admin/view/htm/header_nav.inc.htm: 移除 ../ 前缀逻辑,avatar_url 已是带 base_path 绝对路径,直接使用
必测: 子目录安装下后台顶部导航头像加载
回归: 根目录安装下后台头像正常
[伪静态路径] 修复子目录安装下 Nginx 伪静态规则 try_files/error_page 指向根目录 index.php 导致前台 404
admin/route/setting.php: try_files 和 error_page 中的 /index.php 是相对 server root 的绝对路径,子目录安装时必须带 base_path 前缀
子目录安装(如 /xiuno)生成: error_page 404 =404 /xiuno/index.php; try_files uriuri/ /xiuno/index.phpisargsargs;
必测: 子目录安装下后台伪静态页复制规则部署后前台访问正常(帖子/版块/用户页)
回归: 根目录安装规则不变
[配置不同步] Nginx 伪静态规则补充 fastcgi_intercept_errors off,防止宝塔拦截 PHP 404 跳默认错误页
admin/route/setting.php: 宝塔默认 fastcgi_intercept_errors on 会把 PHP 返回的 404 拦截跳到宝塔默认 404 页,需 off 让 Xiuno 自定义错误页透传
必测: 子目录/根目录安装下访问不存在帖子,应显示 Xiuno 自定义 404 而非宝塔默认 404
后台升级功能优化
[逻辑错误] 修复在线升级页面左侧导航「仪表盘」误判为 active
根因: sidebar.inc.htm 的 route_menu_map 有 upgrade=>other 但漏了 online_upgrade=>other,访问 admin/?online_upgrade.htm 时回退默认 home
修复: 补 online_upgrade=>other 映射
必测: admin/?online_upgrade.htm 左侧导航「系统工具」高亮
[配置不同步] 隐藏后台「系统升级」菜单(在线升级已包含 DB 升级,是 upgrade 超集)
menu.conf.php: 移除 other 菜单下 upgrade 菜单项,保留 admin/route/upgrade.php 路由和 UpgradeService.php(online_upgrade 复用)
必测: admin 后台「系统工具」只剩「缓存设置/清理缓存/在线升级」三个 tab;旧书签 admin/?upgrade.htm 仍可访问
[缓存失效] 升级/重装完成后未清理数据缓存和 opcache 导致新代码不生效
根因: OnlineUpgradeService::cleanup() 只删 tmp/* 编译缓存保留 tmp/cache/ 数据缓存,且未清 opcache;reinstall() 完全不清缓存
修复: 新增 clearCaches() 委托 CacheService::clearByType();cleanup() 末尾清 data+opcache;reinstall() 末尾清 data+tmp+opcache
必测: 在线升级走完全流程后访问页面确认新代码生效;重装当前版本后确认缓存已清
[新功能] my-profile 侧边栏新增「我的等级」模块(放在「我的帖子」上方)
基于后台用户组系统(bbs_group 表 creditsfrom/creditsto 阈值)展示等级与经验进度
route/my.php: 新增 level action,获取可升级组(gid>=100)、当前等级、下一等级、进度条百分比、距下一级差值
view/htm/my_level.htm: 新模板,当前等级卡片(图标+名称+经验值+进度条+距下一级) + 所有等级列表表格
model/route.func.php: 新增 my_level 路由规则和 my_level_url() 函数
view/htm/my.layout.inc.htm: 侧边栏导航和移动端 Tab 在「我的帖子」上方新增「我的等级」入口
lang/(zh-cn/zh-tw/en-us)/bbs_common.php: 新增 15 个语言项
系统组(gid<100,如管理员/版主)显示「系统用户组,不参与等级升级」;已最高级显示「已达最高等级」
必测: ?my-level.htm 等级卡片显示、进度条百分比、距下一级经验值、所有等级列表高亮当前等级
回归: ?my-profile.htm 侧边栏导航、移动端 Tab、?my-thread.htm 等其他 my 子页面不受影响
[新功能] 安装向导环境检测新增「部署路径」检测项 + 文档补充子目录部署提示
install.func.php: 通过 SCRIPT_NAME 去掉 /install/ 后缀检测是否子目录部署,子目录时 status=2 黄色警告
三语言包同步新增 deploy_path/root_dir/subdir_detected 三个 key(zh-cn/zh-tw/en-us)
install.md: 第 1.1 节环境要求新增子目录部署提示块
必测: 根目录安装时 env 页「部署路径」显示绿色根目录;子目录安装时显示黄色警告文案
回归: Windows 警告检测不受影响
[XSS风险] xnx_duel 两处 innerHTML 改用 DOM API(createElement+createTextNode),避免 submittingText 被 HTML 解析
必测: plugin/xnx_duel/ 发起挑战提交按钮 loading 效果
[XSS风险] xnx_icon buildSvg 对颜色值/paths 加 escapeHtml 防御性转义(数据来源已受控,纵深防御)
必测: plugin/xnx_icon/ 图标实时预览、角标渲染
[误报优化] 插件扫描器 php_superglobal_output 区分大小写匹配 + 转义函数识别
修复 post误匹配_POST(扫描器统一用 #i 修饰符,超全局变量分类额外做区分大小写二次匹配)
修复 echo esc_attr($_SERVER[...]) 已转义仍误报(增加 esc_html/esc_attr/esc_js/intval/urlencode 等安全函数白名单)
必测: admin/?plugin-scanner.htm 扫描 xnx_checkin / xnx_report
[误报优化] 插件扫描器 js_dom_xss / jquery_html_xss 注释抑制机制
新增 // 保留 innerHTML 注释抑制(类似 direct_db 的保留机制,后续 5 行跳过 XSS 警告)
用于 SVG 渲染、受控数据源等必须用 innerHTML 的合理场景
必测: admin/?plugin-scanner.htm 扫描 xnx_duel / xnx_icon
[误报优化] 插件扫描器 JS/CSS 跳过 HTML-only 规则
修复 JS 文件注释/字符串中的 data-target 等 HTML 属性被误匹配(bs4_data_attrs 是 HTML-only 规则不应扫描 JS 上下文)
必测: admin/?plugin-scanner.htm 扫描 xnx_landing
[误报优化] 插件扫描器支持文件级 innerHTML 抑制
行级抑制(// 保留 innerHTML)→ 后续 5 行跳过;文件级抑制(// @suppress dom_xss)→ 整个文件跳过
xnx_icon 文件头加 @suppress dom_xss(13 处 innerHTML 均为受控数据 + escapeHtml 转义)
xnx_duel 第72行加 // 保留 innerHTML(submittingText 来自 esc_attr(lang()) 受控)
必测: admin/?plugin-scanner.htm 扫描 xnx_duel / xnx_icon
日志分析与优化
[逻辑错误] 修复 xnx_api_tester lang hook 语法错误导致每次访问后台产生报错日志(134 条/月)
根因: bbs_admin.php 的 hook 注入点在 return array(...) 内部,与校验器要求的 $lang['key']='value'; 语句格式根本不兼容
修复: 删除 3 个有语法冲突的 lang hook 文件(zh-cn/zh-tw/en-us),在 admin/index.php 加 foreach 兜底加载插件语言包
必测: admin 后台任意页面无 lang_error 报错日志;xnx_api_tester 后台语言包正常显示
回归: admin 后台所有页面语言包正常
[边界未校验] 修复 session useragent 为 null 时写入 NOT NULL 字段导致 SQL 错误
model/session.func.php: SERVER('HTTPUSER_AGENT') 改为 SERVER('HTTPUSER_AGENT', ''),两处(sess_new + sess_write)
必测: 无 User-Agent 的客户端(如某些爬虫/命令行)访问不报 SQL 错误
[边界未校验] 修复 forum_access_user() 访问不存在的用户组(gid=3)报 Undefined array key
model/forum_access.func.php line 139: 加 isset(grouplist[gid]) 检查,不存在则返回 FALSE
必测: api/v1/user/me/permissions 接口不报错
[配置不同步] 优化日志量减少 DEBUG=1 时高频调试日志
cache_debug: if(DEBUG) 改为 if(DEBUG>1),文件名 cache_debug_error 改为 cache_debug(去掉 error 后缀避免 DEBUG=0 时仍写)
api_tester_debug: 15 处 xn_log 调用加 $api_tester_debug 前缀(defined('DEBUG') && DEBUG > 1)
db_exec: 保持不变(已有 DEBUG AND 控制,1.5M 条为 DEBUG=1 历史积累)
必测: DEBUG=0 时无调试日志;DEBUG=1 时无 cache_debug/api_tester_debug 日志;DEBUG=2 时全部记录
[变量未定义] 修复 online_upgrade.htm startBtn 调用不存在的 startUpgrade() 导致 ReferenceError,改为 preflight() 触发升级前确认 Modal
必测: admin/?online_upgrade.htm


