Files
wordpress-kratos-yang/inc/theme-mourning.php
T
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

259 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 纪念日自动哀悼
*
* ── 依据 ─────────────────────────────────────────────────────
* 以下日期分三类,可信度不同,代码里也分开对待:
*
* A. 法定纪念日(全国人大常委会决定设立,每年国家举行纪念活动)
* · 09-03 中国人民抗日战争胜利纪念日
* 2014-02-27 十二届全国人大常委会第七次会议决定设立
* · 09-30 烈士纪念日
* 2014-08-31 十二届全国人大常委会第十次会议决定设立
* · 12-13 南京大屠杀死难者国家公祭日
* 2014-02-27 同一会议决定设立
* 依据:全国人大网《铭记历史坐标 凝聚前行力量》
* http://www.npc.gov.cn/c2/c30834/202510/t20251020_448426.html
* http://npc.gov.cn/c2/c12435/c12487/201905/t20190521_220992.html
*
* B. 民间广泛采用(非法定,但主流网站普遍黑白)
* · 05-12 汶川地震纪念(2008-05-12 14:28)
* · 09-18 九一八事变纪念(1931-09-18)
* 这两个日期黑白是网络惯例,但不是法定「哀悼日」,所以默认**不勾选**,
* 由用户自己决定。硬编码成默认开启会替用户做价值判断,不合适。
*
* C. 历史全国哀悼日(一次性事件,非每年)
* · 2008-05-19~21 汶川地震(国务院决定)
* · 2010-04-21 玉树地震(国务院决定)
* · 2010-08-15 舟曲泥石流(国务院决定)
* · 2020-04-04 新冠疫情牺牲烈士和逝世同胞(国务院决定)
* 这些是**特定年份的特定日期**,不是周年循环。所以代码里不把它们
* 当成"每年重复",而是保留为可自定义的具体日期,避免明年 4 月 4 日
* 又自动变黑白(那是错的)。
*
* ── 设计取舍 ──────────────────────────────────────────────────
* 1. **按站点时区判断**,不按服务器时区。用 current_time('md'),
* 它走 WordPress 设置的时区。否则服务器在 UTC,北京时间 9 月 18 日
* 早上 8 点前还算 17 日,哀悼会晚 8 小时生效。
*
* 2. **只判断月日,不判断年份**。纪念日是周年性质,每年的同一天都要生效。
*
* 3. **兼容旧的 g_rip 开关**。老版本是纯手动「一直黑白」。
* 若用户勾着旧的 g_rip,仍然一直黑白;新逻辑是叠加在上面的。
* 不能因为升级就让原本黑白中的站点突然恢复彩色。
*
* @author Yongquan Yang <yangyq.net>
* @license MIT License
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* 内置纪念日清单
*
* 返回 array( 键 => array(月份, 日, 名称, 说明, 分类, 是否默认勾选) )
*
* 键名用 `m-d` 形式(补零),与 current_time('m') / current_time('d') 直接可比。
*/
function yang_mourning_presets()
{
return array(
// ── A 类:法定纪念日 ──
'09-03' => array(
'name' => __('中国人民抗日战争胜利纪念日', 'kratos'),
'note' => __('1945 年 9 月 2 日日本签署投降书,9 月 3 日定为胜利纪念日', 'kratos'),
'group' => 'legal',
'default' => 1,
),
'09-30' => array(
'name' => __('烈士纪念日', 'kratos'),
'note' => __('向人民英雄敬献花篮仪式在天安门广场举行', 'kratos'),
'group' => 'legal',
'default' => 1,
),
'12-13' => array(
'name' => __('南京大屠杀死难者国家公祭日', 'kratos'),
'note' => __('1937 年 12 月 13 日南京沦陷,30 万同胞遇难', 'kratos'),
'group' => 'legal',
'default' => 1,
),
// ── B 类:民间惯例,默认不勾 ──
'05-12' => array(
'name' => __('汶川地震纪念日', 'kratos'),
'note' => __('2008 年 5 月 12 日 14 时 28 分,汶川发生 8.0 级地震', 'kratos'),
'group' => 'custom',
'default' => 0,
),
'09-18' => array(
'name' => __('九一八事变纪念日', 'kratos'),
'note' => __('1931 年 9 月 18 日,日本关东军发动九一八事变', 'kratos'),
'group' => 'custom',
'default' => 0,
),
'07-07' => array(
'name' => __('七七事变纪念日', 'kratos'),
'note' => __('1937 年 7 月 7 日,卢沟桥事变,全民族抗战爆发', 'kratos'),
'group' => 'custom',
'default' => 0,
),
'12-12' => array(
'name' => __('西安事变纪念日', 'kratos'),
'note' => __('1936 年 12 月 12 日,张学良、杨虎城发动兵谏', 'kratos'),
'group' => 'custom',
'default' => 0,
),
'08-15' => array(
'name' => __('日本宣布无条件投降纪念日', 'kratos'),
'note' => __('1945 年 8 月 15 日,日本天皇宣布接受波茨坦公告', 'kratos'),
'group' => 'custom',
'default' => 0,
),
'04-04' => array(
'name' => __('清明节', 'kratos'),
'note' => __('节气日期每年浮动,这里固定为 4 月 4 日,仅作近似', 'kratos'),
'group' => 'custom',
'default' => 0,
),
);
}
/**
* 读取用户勾选的纪念日
*
* 老站点升级上来时这个选项还不存在,此时返回 presets 里 default=1 的那些。
* 注意**不能**简单用 `kratos_option('g_rip_days', 默认数组)`:kratos_option
* 对未设置的键返回默认值,但如果用户曾经保存过空数组(全部取消勾选),
* 存进去的就是空数组,此时必须尊重「用户就是要关掉」,不能再回退到默认。
* 所以这里用「option 数组里有没有这个键」来判断,而不是判断值是否为空。
*/
function yang_mourning_days()
{
$presets = yang_mourning_presets();
$opts = get_option('kratos');
$has = is_array($opts) && array_key_exists('g_rip_days', $opts);
if (!$has) {
// 从未保存过:用内置默认
$out = array();
foreach ($presets as $md => $info) {
if (!empty($info['default'])) {
$out[] = $md;
}
}
return $out;
}
$saved = $opts['g_rip_days'];
if (!is_array($saved)) {
return array();
}
// 只保留在 presets 里存在、且值为真的日期(防止脏数据)
$out = array();
foreach ($saved as $md => $v) {
if ($v && isset($presets[$md])) {
$out[] = $md;
}
}
return $out;
}
/**
* 今天是否应当哀悼
*/
function yang_is_mourning_today()
{
// 旧的手动开关:开了就一直黑白(保持向后兼容,不能让升级把黑白站点变回彩色)
if (kratos_option('g_rip', false)) {
return true;
}
if (!kratos_option('g_rip_auto', false)) {
return false;
}
// 走站点时区,不是服务器时区
$md = current_time('m') . '-' . current_time('d');
$days = yang_mourning_days();
// 自定义附加日期(用户自己填的,如特定年份的哀悼日)
if (kratos_option('g_rip_extra', '')) {
$extra = kratos_option('g_rip_extra', '');
foreach (preg_split('/[\s,,、;;]+/u', (string) $extra) as $one) {
$one = trim($one);
if ($one === '') {
continue;
}
// 支持 MM-DD 与 YYYY-MM-DD 两种写法
if (preg_match('/^\d{2}-\d{2}$/', $one)) {
$days[] = $one;
} elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $one, $m)) {
// 带年份的只在当年生效,不是周年循环
if ((int) $m[1] === (int) current_time('Y')) {
$days[] = $m[2] . '-' . $m[3];
}
}
}
}
return in_array($md, $days, true);
}
/**
* 输出黑白样式
*
* 几个细节:
* 1. `html { filter: grayscale(1) }` 会给整页建一个合成层,fixed 定位的元素
* 在某些旧版 Chrome 上会错位。所以额外加 `background-attachment: fixed`
* 的补丁样式不写;这里改用给 body 和 .k-header 分别处理更稳。
* —— 实际上实测最稳的是给 html 加 filter 并用 transform: translateZ(0)
* 在 body 上兜住,避免 iOS Safari 的渲染错位。这里保留 html 方案,
* 因为它在用户当前主题结构下工作正常(老功能的写法),只是补一条
* iOS 的兼容声明。
* 2. 不能只挂 is_home():详情页也应该黑白。老代码限定首页,那是 bug。
* 3. 只在真正哀悼的日子输出,避免给所有页面增加一条无用样式。
*/
function yang_mourning_style()
{
if (is_admin() || !yang_is_mourning_today()) {
return;
}
echo '<style id="yang-mourning">'
. "\nhtml{filter:grayscale(100%);-webkit-filter:grayscale(100%);}"
. "\nhtml body{-webkit-transform:translateZ(0);}"
// 打印时不灰度,否则打印出来是灰的
. "\n@media print{html{filter:none!important;-webkit-filter:none!important;}}"
. "\n</style>\n";
}
add_action('wp_head', 'yang_mourning_style', 1);
/**
* 后台显示今天的哀悼状态,便于确认配置是否生效
*/
function yang_mourning_status_text()
{
if (kratos_option('g_rip', false)) {
return __('当前为手动常开模式:站点一直黑白,与日期无关。', 'kratos');
}
if (!kratos_option('g_rip_auto', false)) {
return __('自动哀悼未开启。', 'kratos');
}
$days = yang_mourning_days();
if (empty($days)) {
return __('自动哀悼已开启,但一个日期都没勾选,因此不会生效。', 'kratos');
}
sort($days);
$today = current_time('m') . '-' . current_time('d');
$is = in_array($today, $days, true);
return sprintf(
/* translators: 1: 今天的月日, 2: 是否处于哀悼, 3: 勾选的日期列表 */
__('今天是 %1$s,%2$s。已启用:%3$s', 'kratos'),
$today,
$is ? __('正在哀悼,站点已黑白', 'kratos') : __('不在哀悼日', 'kratos'),
implode('、', $days)
);
}