Commit Graph
9 Commits
Author SHA1 Message Date
laoyang 3fcaf25c58 3.2.36 新增「自动头像」:按邮箱本地生成,14 套 CC0/MIT 风格可选 #5
为没有头像的访客按邮箱确定性地生成一张 SVG 头像,替代 Gravatar
那个灰底"神秘人"(cn.gravatar.com 在国内已超时,不勾加速时头像
干脆是空白)。全程纯 PHP,零外网请求、零落盘。

后台「主题设置」新增「自动头像」复选框 + 风格列表:勾上之后才
出现 14 套风格,点一下即时换 16 格预览,保存后全站生效。

实现要点(都是踩出来的):
- PRNG 必须与 DiceBear 9.x 逐位一致:xorshift + hashSeed 全程跑在
  JS 的 int32 语义里,PHP 64 位整数要逐步 i32() 归一;
  charCodeAt 按 UTF-16 码元取值,emoji 必须真做代理对拆分。
- pickComponent 有简繁两版:只有 glass/rings/shapes/thumbs 额外取
  三次整数(rotation/offsetX/offsetY)。给简版也补三次 ⇒ PRNG 多走
  3N 步 ⇒ 31 套里 27 套全错。编译期算 pc_has_ints 才治得住。
- 自带人类调色板:官方默认配色几乎全单值,30 个邮箱只出 2 种配色,
  那才是"默认头像丑"的真因,光换画风解决不了。
- 只收录 CC0/MIT(13 CC0 + 1 MIT),CC BY 那 13 套要求署名,一律不收——
  主题装给用户用,用户不会维护署名,收进来等于转嫁合规义务。

接入 get_avatar 的两个关键修正:
- 必须先看原 src 是不是 Gravatar 家族域名再决定是否替换。原先无条件
  替换会把媒体库头像、外链头像、别的插件处理过的 data: 头像一起盖掉。
- 必须删 srcset。只改 src 留着 srcset,浏览器优先用 srcset,
  生成的图根本不会被加载(现象是"代码明明换了,页面还是老样子"),
  且不能重建 <img> 标签(会丢掉评论头像 CSS 依赖的 class)。

后台三处必须这么做:
- body class 由 PHP 输出(admin_body_class),否则每次进设置页/保存后
  重载都会先显示风格列表再藏起来,闪一下。
- 事件监听 radio 的 change,不能监听 label 的 click —— label 会把
  click 转发给关联 input,冒泡回来再触发一次 ⇒ 同一风格发两次请求
  (实测 hits 2→4)。
- 首屏已渲染的 16 格写进 JS 缓存,否则点一圈回到默认风格会白请求一次。

不依赖任何可选扩展:不用 mbstring(未安装时 mb_substr 是 Fatal error,
整站白屏)、不用 gd/imagick。UTF-8 切分/截断/大写全部自己实现。
PHP 7.0 语法,PHP 8.5 下 E_ALL 无任何 warning。

验证:
- 14 套 × 6 种子 = 84 个用例与 DiceBear 官方 Node 输出逐字节一致,
  PHP 8.4 / 8.5 / 无 mbstring(php -n)三种环境均 84/0。
- 静态守卫 55/0;反向验证 17 项注入全部被捕获。
- 后台面板浏览器验证 26/0(显隐、几何、点选、缓存、连点、零报错)。
- 包内内容契约 24/0;包校验反向验证 6 项注入全部被捕获。
- 单次渲染稳态 0.047~0.105ms,峰值内存 2~4MB。
2026-10-25 18:38:11 +08:00
laoyang 40e09b6309 3.2.34 新增「CDN 加速」:地址 + 白名单 + 黑名单,黑名单优先 #3
后台「主题设置」新增第十一个标签页(邮件通知之后、自定义代码之前),
只有三个输入框:CDN 地址、白名单、黑名单。地址留空即关闭功能。

实现(inc/theme-cdn.php,新增):
- 输出缓冲挂在 template_redirect 优先级 1,改整页 HTML 的资源地址,
  覆盖主题 enqueue 之外的文章图片、插件输出、区块图片。
- 规则按字面形态自动识别七种写法:* / 扩展名 / 扩展名列表 /
  路径前缀 / 通配符 / 路径子串 / ~正则~。
- 单个纯字母词(jpg 与 uploads 分不出谁是扩展名)同时按扩展名与
  路径子串匹配,两个含义互斥,避免 uploads 规则静默失效。
- 黑名单先于白名单判定;硬性排除不受两套名单影响:以 / 结尾、
  页面/动态后缀、无扩展名、wp-admin/wp-json/wp-cron/xmlrpc/feed、
  非本站域名、非 http(s) 协议。
- 后台/REST/AJAX/feed/cron/定制器预览/CLI 一律不改写。
- 查询串与锚点原样保留(?ver= 丢了会一直吃旧缓存)。
- 子目录站:判规则用完整路径,拼 CDN 时剥掉站点子目录。
- 写错的正则编译时丢弃,避免每个资源抛一条 warning 刷满日志。

验证:
- inc/theme-cdn.php 逻辑探针 92/0,反向验证 21/0(M1~M15 逐条注入,
  含 M5 拆成三条覆盖协议相对/绝对/整页三条分支,M10/M13 组合注入)。
- 后台探针 56/0(真实渲染 + 几何 + 类型 + 说明块),反向验证 15/0。
- 包契约 69/0,反向验证 23/0(改 zip 内字节,不改源码)。
- 旧探针回归:toc330 101/0、admin330 88/0、admin328 13/0、
  pkgcheck331/332/333 全绿、e2e322/323 通过。
- 线上仅在 theme-update/ 发 zip + readme + update.json,未解包覆盖服务器主题目录。
2026-10-25 16:15:09 +08:00
laoyang cbe0f2dc59 3.2.18: 清明按节气换算、主题内 [TOC] 目录、重建评论区替代 wpDiscuz
三项功能:

1. 清明不再用固定 04-04
   清明是节气,日期在 4/4~4/6 之间浮动。硬编码 04-04 实测 1900-2100
   共 201 年里只有 76 年是对的(约 38%)。改为内嵌逐年表换算真实日期,
   超出 1900-2100 返回 null 明确放弃,不退化成近似值。
   兼容 3.2.17 及以前勾过的 '04-04' 键,升级后勾选不会被静默丢弃。

2. 主题内实现 [TOC]
   文章里写 [TOC],该位置输出本文目录,点击跳到对应标题。
   在 the_content 优先级 9 处理(早于 wpautop 10 / do_shortcode 11),
   保证"补 id"与"扫目录"用的是同一份文本,href 与 id 必然对得上。
   嵌套用深度归一化,h1→h3 跳级不会算错层级。
   scroll-margin-top 声明式让位,原生锚点/外部 #hash/后退全都生效。

3. 重建评论区
   旧实现的问题:列表渲染与 AJAX 渲染各写一份 HTML,已经分叉
   (AJAX 返回的新评论没有回复按钮);<li> 被 <div> 包着,嵌套回复错位。
   现在统一到 yang_comment_html() 单一渲染函数,列表与 AJAX 只差一个
   is-new 类。同时修掉三处真实缺陷:
     · 评论项用 flex 会把 <ol class="yang-children"> 当成第三个 flex 项,
       子评论跑到父评论右侧、正文被挤到 156px、页面横向溢出 75px —— 改 grid
     · 成功提示在 exitReplyMode() 之前设置,被其末尾的 setStatus('','')
       擦掉,用户提交成功却看不到反馈 —— 调整顺序
     · 窄屏媒体查询打不过深层选择器(媒体查询不增加特异性),
       嵌套头像反向变大 —— 补深层覆盖

   与 wpDiscuz 解耦:邮件模块不再无条件 return false(那等于永久关掉
   notify_postauthor 且不报错);插件在场时不加载主题评论资源,
   避免双份表单互抢。

验证:PHP 语法 73/73;_static_qingming 36、_test_toc 53、
_test_comments 62、_test_comments_falsify 17、
_toc_geometry 121、_toc_geometry_falsify 9、
_comments_geometry 98、_comments_geometry_falsify 23,全部 0 失败。
线上端到端 29/0(真实文章渲染目录 + 真实评论数据渲染)。
2026-10-24 01:03:36 +08:00
laoyang ef9a58dc51 v3.2.13 图片选项全部自带默认图,新装站点不再出现空洞/破图
根本原因:kratos_option() 运行时只读数据库,`std` 仅在后台表单预填和
「恢复默认」时生效,前台根本读不到。因此新装站点所有图片项都是空值。
另外 isset() 挡不住「用户清空」——of_sanitize_upload() 对空输入返回 '',
键存在且值为空串,调用方传的兜底值被绕过。

同时不能把绝对 URL 写进 std:它会随保存一起写库,主题目录改名/换 CDN/
迁站后永久 404。本站 s_wechat_url / g_donate_alipay / g_donate_wechat
三项就指向已删除的 kratos-master 目录,一直在破图(这三项是 hidden 字段,
后台界面上看不到,所以坏了很久没人发现)。

做法:
- 新增 inc/theme-option-defaults.php,图片默认值注册表(11 项),
  默认图一律运行时给出,不写数据库。
- yang_option_image() 三层判断:空/纯空白 → 默认图;指向已不存在的主题
  目录 → 默认图;文件不存在则不输出地址(避免换来一个 404)。
- 陈旧判断用 get_template()(主题 slug)而非 basename(目录名),避免本地
  开发目录名与 slug 不一致时误伤当前主题自己的图。
- 模板统一走取值函数:header(logo/favicon/banner)、footer(微信码/国旗)、
  404、index、article 缩略图、widgets 头像、page-toolbar、theme-core 赞赏码。
- 国旗与 favicon 的渲染抽成 yang_flag_block() / yang_favicon_tag(),
  空值时不输出空 src / 空 href。
- 后台 9 处 std 改为 yang_options_default_img(),与前台默认图同源;
  该包装函数自带 function_exists 防护,单独加载 theme-options.php 不会 Fatal。

新增图片(本地生成,合计 291KB):default-404 / default-share /
default-empty / default-avatar / default-banner / default-flag / favicon.ico。
favicon 为 WordPress 风格自绘(品牌蓝圆底 + 白色 W),非站长个人专用那张。

验证:run_tests 146 条(新增第 4 组图片默认值 22+ 条);falsify 35 条注入
全部能被抓住(新增 10 条);verify_all.sh 14 组全绿。
2026-10-23 19:50:07 +08:00
laoyang 269364bed4 版本号 3.2.6 -> 3.2.7(纪念日全部默认不勾 + 彩色说明页 + 邮件彩色 + 双引用块)
哀悼默认值全部改为不勾选
- 九个纪念日的 default 全改成 0。之前法定那三个是 1,导致全新安装的站点
  一到日子就自己变黑白,而站主根本没同意过这件事。
- 后台多选组件的 std 全为 0,打开设置页一个都不勾。
- 「从未保存过」与「保存过但全不勾」两条路径都返回空数组,
  不能因为结果为空就回退到默认 —— 那等于用户取消勾选无效。

新增彩色说明页(inc/theme-mourning-page.php + pages/page-mourning.php)
- 站点变黑白后必须有个地方解释清楚,否则访客只看到一片灰、以为站点坏了。
- 三处入口:顶部提示条(wp_body_open,首屏可见)、页脚常驻链接、
  [yang_mourning] 短代码。
- 页面内容由真实配置驱动,不是写死的文案:没勾的日期不会出现,
  今天在列表里带「今天」标记,一个都没勾时明说「不会生效」。
- 页面必须保持彩色,否则解释「为什么是灰的」的页面自己也是灰的。
  取消灰度做了两层:filter:none 与 grayscale(0%) 都写,且带 !important;
  优先级放在 wp_head 的 2,晚于灰度样式的 1。
- 路由走 parse_request 优先级 5 而非 template_redirect:
  避免先走一遍 404 判定与 404 模板。带子目录安装时先剥掉前缀再比对。
- 不抢占用户自己建的页面:如果站里已有同名 slug 的页面就放手。
  否则用户会觉得「我建的页面坏了」,且没有任何提示。

邮件不受影响,且回复邮件同时给出双方内容
- 邮件是独立 HTML,不走 wp_head,结构上就不可能被灰度,已断言验证。
- theme-mail-template 的单个 quote 改成 quotes 列表,一个模板同时渲染
  「被回复的那条」与「新的回复」,最后一块用主色高亮(回复是重点),
  前面的用弱化色。只有一块时退化为主色,与旧行为一致。
- 通过 wpdiscuz_email_content 重建整封邮件,不需要改插件。
  审核通过通知用 wpdiscuz_comment_approved_email_content 走同一模板,
  但它只给 post_id、不给评论对象,能放的内容受此限制。

两个必须记下来的坑
- wpDiscuz 在过滤器之后还会做一次 html_entity_decode 再发信。
  esc_html 的输出会被那一步还原成真标签,评论里的 <script> 直接生效。
  所以返回前先把 & 编码一次。本地 stub 没有这一步,测试看不出来。
- header.php 从来没有调用 wp_body_open,挂在上面的东西全部静默失效。
  不调用不会报错,只会「什么都没发生」。已补在 <body> 之后。

yang_mail_text 保留换行
- 原来用 wp_strip_all_tags($t, true),第二个参数 remove_breaks 会把换行
  压成空格;而 strip_tags 遇到 <br> 是直接删掉不补换行的。
  结果整条评论被拍成一行,模板里的 nl2br 成了死代码。
- 改为先把块级边界降级成 \n,再去标签(remove_breaks=false)。
  收行内空白用 [^\S\n] 而不是 [ \t\f\v]:PCRE 的 \v 是垂直空白类,
  会把 \n 一起匹配掉。

说明页对比度修正(WCAG AA)
- 金色 #b8860b 在白底只有 3.06:1,当装饰色没问题,当 12-15px 文字偏虚。
  拆成 --ym-gold(装饰)与 --ym-gold-text(#946600,4.75:1,仍属金色)。
- 分组小标题 #9aa5b1 只有 2.36:1,改用 #5f6b78(5.12:1)。
- 「今天」标签白字压金底 3.25:1,改用 #8a6100(5.54:1)。
2026-10-23 14:43:47 +08:00
laoyang 37c52948c4 版本号 3.2.5 -> 3.2.6(统一邮件出口 + 邮件模板 + 纪念日自动哀悼 + 去掉广告位)
评论邮件重复的根因是发信源不止一个,分两层处理:一层管「谁发」,一层管「发几封」。
只做前者的话邮件照样重复,只是都从同一个 SMTP 出去。

邮件治理(新增 inc/theme-mail.php)
- phpmailer_init 优先级 9999 接管通道,保证主题配置优先于其它 SMTP 插件;
  接管前先重置可能被别的插件设过的字段,避免残留半套配置。
- From 强制等于 SMTP 登录账号。QQ / 163 会以「发件人不符」拒收。
- wp_mail 优先级 99 统一 From 与 Content-Type;调用方自带的 From 与
  Content-Type 被剥离后重建,自定义 header 保留。
- wp_mail 优先级 100 做短期去重:同一收件人 + 同主题 + 同正文,90 秒内只发一次。
  用 transient 是因为评论通知跨请求(wpDiscuz 走 AJAX、审核通知走后台请求),
  静态变量跨不过去。90 秒足够宽(一次提交引发的通知都在同一秒)也足够窄
  (手动重发通常隔几分钟,不该被吞)。
- 被去重的那次把 to 置空并由 pre_wp_mail 返回 true,而不是 return null:
  返回 null 会改变 wp_mail 返回值语义,有的插件据此判定失败并重试,反而更多封。
- 用 filter 关掉核心的 notify_postauthor / notify_moderator,不改进数据库里的
  comments_notify / moderation_notify。改用户设置属于越权,而且用户下次进后台
  一勾又回来了;filter 是可逆的。只拦通知,不影响评论审核流程。
- 线上站点 m_smtp=1 但 m_host 为空时判定为「未就绪」并完全放手。只勾开关不填
  主机时若强行接管,所有邮件会静默失败,比回退到 mail() 更糟。

邮件模板(新增 inc/theme-mail-template.php)
- 表格布局 + 全内联样式:Outlook 2007-2019 用 Word 引擎,不认 flex / grid;
  Gmail 会剥掉 <head> 里的 style 块(含 media query)。
- 不用外部图片、不用 CSS 变量、不用 ::before。按钮的 padding 放在 td 上,
  因为 Outlook 忽略 a 的 padding。
- 宽度用混合写法:HTML 属性 width=600 给 Outlook,CSS 的
  width:100% + max-width:600px 让手机窄屏自动收窄。只写死 600px 会让手机横向滚动。
- badge 背景用预算的实色而非 rgba。Outlook 不支持 rgba,会当成非法值丢掉,
  半透明背景变成全透明,badge 直接消失。
- 主色下沿那条 3px 深色带必须 height 同时写成 HTML 属性和 CSS,
  否则 Outlook 把它撑成一行文字高,比例就毁了。
- 用 WCAG 相对亮度判断主色深浅来决定叠在上面的文字用白还是深色。
  简单取平均会把 #00ff00 判成中等亮度(人眼对绿最敏感、蓝最不敏感)。
- 次要文字统一调到 #5f6b78。原来的浅灰在白底上只有 1.84:1,看着"高级"但读不出来;
  现在最低 4.62:1,全部过 AA。

theme-smtp.php
- 删掉 comment_notify / comment_approved 两个钩子及其硬编码模板。这两个钩子与
  wpDiscuz 的 notifyCommentSubscribers 完全重复,是重复收信的直接来源。

纪念日自动哀悼(新增 inc/theme-mourning.php)
- 法定纪念日三个默认勾选:09-03 抗战胜利纪念日、09-30 烈士纪念日、
  12-13 南京大屠杀死难者国家公祭日。依据是 2014 年全国人大常委会两次决定,
  已在代码注释里记下来源。
- 民间通行做法(05-12、09-18 等)默认不勾,由用户自己决定。替用户做价值判断不合适。
- 一次性全国哀悼日(2020-04-04 等)支持 YYYY-MM-DD 写法,只在当年生效。
  不能当周年循环,否则明年 4 月 4 日又自动变黑白。
- 按站点时区判断(current_time),不按服务器时区。服务器在 UTC 时会晚 8 小时生效。
- 只挂 wp_head 而不限定 is_home():老代码限定首页,详情页不黑白,那是 bug。
- 打印时取消灰度,否则打印出来是灰的。
- 保留旧的 g_rip 手动常开开关。不能因为升级把原本黑白中的站点变回彩色。

options-framework
- 补 multicheck 的渲染分支与 of_sanitize_multicheck。框架原来只在保存侧处理了
  这个类型,没有渲染分支,所以这个类型此前根本不可用。
- 值存成 array(键 => 1|0) 而不是「勾选了的键组成的列表」:列表无法区分
  「用户明确全部取消」和「从未配置」,会导致取消勾选后静默回退到默认值。
- 读取侧用 array_key_exists 判断是否保存过,而不是判断值是否为空。
- 配套补上 of-multicheck 样式,没有样式时 9 个选项会挤成一行流水文字。

去掉广告配置
- 删除主题选项里的广告配置段,以及 single.php 里上下两处广告位输出。

验证
- 全主题 68 个 PHP 文件语法检查通过。
- 行为测试 82 条、multicheck 闭环 25 条、邮件几何 15 条、设计审计 23 条。
- 反向验证 16 个注入 bug 全部被断言抓住(去重失效、不写标记、to 未清空、
  From 未剥离、亮度算法退化、时区改用 date()、全不勾回退默认等)。
- 邮件最低文字对比度 4.62:1;320px 视口下卡片自动收窄到 296px 不溢出。
2026-10-23 13:55:23 +08:00
laoyang 4f0d19fb91 版本号 3.2.4 -> 3.2.5(修 Prism 报错,项目地址改为自建 Git)
## 修 Prism is not defined

首页控制台报 `Uncaught ReferenceError: Prism is not defined`,来源是
wp-editormd 插件(不是本主题):它在 Prism 脚本加载之前就无条件执行了
`Prism.plugins.autoloader.languages_path = "..."`。

线上页面里的真实顺序:
    第 668 行  该赋值语句
    第 695 行  prism-core.min.js
    第 696 行  prism-autoloader.min.js

在 `wp_head` 优先级 0 垫一个最小空壳 `window.Prism`(含 plugins.autoloader
这条访问链路),让那句赋值有地方可写。已核对 prism-core.min.js 源码:它开头
只读 `u.Prism.manual` 与 `u.Prism.disableWorkerMessageHandler`,结尾
`global.Prism = Prism` 整体覆盖 —— 空壳必定被真实对象替换,不承担持久职责,
只为吸收那条过早的赋值。已用 jsdom 验证:去掉空壳重现原始报错,加上后消失,
且之后 `Prism.highlightAll` 仍可正常调用。

## 项目地址统一到 code.yangyq.net

原作者署名、原主题地址、README、语言包、JS 里的控制台横幅全部换成自建仓库地址。
LICENSE 文件保持原样不动(MIT 要求保留原始版权声明)。

## CDN 分支改成拿不到就回退本地

原来的 `g_cdn` 分支指向 `cdn.jsdelivr.net/gh/vtrois/kratos`,本仓库在自建
Gitea 上,jsDelivr 只认 github/npm/wordpress,实测该地址 404。现改为仅在
`YANG_THEME_CDN_REPO` 常量存在时才走 CDN,否则一律用
`get_template_directory_uri()`。留一个 404 的地址比留空更危险:页面能开、
样式全丢,极难排查。

## 顺带

- `.gitattributes` 补 `*.mo binary`:原来是靠"含 NUL"启发式判定,不是契约
- `update.json` 的版本号跟进到 3.2.5
- `kratos.min.js` / `kratos.min.css` 用压缩器重新生成,不手工改产物
2026-10-23 12:55:41 +08:00
laoyang eaabc7717f 把自定义样式从主题目录搬到主题选项,升级不再丢
原来主题里有个 custom/ 目录(custom.css / custom.js / custom.php),
是给用户随手加样式的入口。问题是它位于主题目录内,而 WordPress 升级
主题的做法是把整个主题目录删掉再解压(class-theme-upgrader.php 里
clear_destination = true,随后 delete_old_theme() 递归删除),
所以每次升级这些文件都会丢。custom.php 还会被 require,
等于在主题里留了一个执行任意 PHP 的口子。

改成后台选项:
- 主题选项新增「自定义代码」组,含自定义 CSS / 自定义 JS 两个输入框
- 内容存在数据库的 kratos 选项里,与主题文件解耦,升级、重装都不影响
- 分别在主题样式和主题脚本之后加载,优先级最高

为此新增了 code 字段类型,没有复用 textarea:textarea 的 sanitizer
走 wp_kses(),CSS 的子选择器 > 和 JS 的 < 会被当成 HTML 标签剥掉,
样式会静默失效。code 类型只去掉 PHP 标签、打断 </style> 和 </script>,
其余原样保留。渲染时也不走公共的 stripslashes —— options.php 已经
wp_unslash 过,再剥一层会把 CSS 的 \f101、JS 的 \n 啃坏。

自定义 CSS 的默认值就是原来 custom.css 的内容,逐字节一致(354 字节),
所以升级瞬间站点外观没有任何变化。清空输入框保存即可彻底去掉。

验证:真实 WP 7.1.3 / PHP 8.5.12 环境 20 条断言全过(含 DB 往返、
子选择器 / 反斜杠保真、危险内容过滤),生产选项指纹前后一致;
反向注入「默认值改空」能立刻被抓到。
2026-10-23 10:35:56 +08:00
laoyang a826b26bee 从gitee代码库中初始化项目。 2022-01-07 11:15:42 +08:00