评论邮件重复的根因是发信源不止一个,分两层处理:一层管「谁发」,一层管「发几封」。 只做前者的话邮件照样重复,只是都从同一个 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 不溢出。
249 lines
6.7 KiB
PHP
249 lines
6.7 KiB
PHP
<?php
|
||
/**
|
||
* @package Options_Framework
|
||
* @author Devin Price <devin@wptheming.com>
|
||
* @license GPL-2.0+
|
||
* @link http://wptheming.com
|
||
* @copyright 2010-2014 WP Theming
|
||
*/
|
||
|
||
/**
|
||
* Sanitization for text input
|
||
*
|
||
* @link http://developer.wordpress.org/reference/functions/sanitize_text_field/
|
||
*/
|
||
add_filter( 'of_sanitize_text', 'sanitize_text_field' );
|
||
|
||
/**
|
||
* Sanitization for password input
|
||
*
|
||
* @link http://developer.wordpress.org/reference/functions/sanitize_text_field/
|
||
*/
|
||
add_filter( 'of_sanitize_password', 'sanitize_text_field' );
|
||
|
||
/**
|
||
* Sanitization for select input
|
||
*
|
||
* Validates that the selected option is a valid option.
|
||
*/
|
||
add_filter( 'of_sanitize_select', 'of_sanitize_enum', 10, 2 );
|
||
|
||
/**
|
||
* Sanitization for radio input
|
||
*
|
||
* Validates that the selected option is a valid option.
|
||
*/
|
||
add_filter( 'of_sanitize_radio', 'of_sanitize_enum', 10, 2 );
|
||
|
||
/**
|
||
* Sanitization for image selector
|
||
*
|
||
* Validates that the selected option is a valid option.
|
||
*/
|
||
add_filter( 'of_sanitize_images', 'of_sanitize_enum', 10, 2 );
|
||
|
||
/**
|
||
* Sanitization for textarea field
|
||
*
|
||
* @param $input string
|
||
* @return $output sanitized string
|
||
*/
|
||
function of_sanitize_textarea( $input ) {
|
||
global $allowedposttags;
|
||
$output = wp_kses( $input, $allowedposttags );
|
||
return $output;
|
||
}
|
||
add_filter( 'of_sanitize_textarea', 'of_sanitize_textarea' );
|
||
|
||
/**
|
||
* Sanitization for raw code fields (theme's own and user's CSS / JS).
|
||
*
|
||
* 为什么不能复用 textarea:textarea 走 wp_kses(),CSS 里的子选择器
|
||
* `>`、`+`、`~` 以及 JS 里的比较运算符 `<`、`>` 都会被当成 HTML 标签
|
||
* 剥掉。例:`a > b { }` 存进库会变成 `a b { }`,样式静默失效。
|
||
* 所以这里只做三件必要的事:
|
||
* 1. 去掉 PHP 标签,避免这段文本将来被 eval / include 时执行代码;
|
||
* 2. 把 `</style>`、`</script>` 打断,避免提前闭合输出标签;
|
||
* 3. 其余内容原样返回(含反斜杠转义,如 CSS 的 `\f101`)。
|
||
*
|
||
* @param $input string
|
||
* @return $output sanitized string
|
||
*/
|
||
function of_sanitize_code( $input ) {
|
||
if ( ! is_string( $input ) ) {
|
||
return '';
|
||
}
|
||
$output = str_replace( array( '<?', '?>' ), '', $input );
|
||
$output = preg_replace( '#</\s*(style|script)#i', '<\\/$1', $output );
|
||
return $output;
|
||
}
|
||
add_filter( 'of_sanitize_code', 'of_sanitize_code' );
|
||
|
||
/**
|
||
* Sanitization for checkbox input
|
||
*
|
||
* @param $input string (1 or empty) checkbox state
|
||
* @return $output '1' or false
|
||
*/
|
||
function of_sanitize_checkbox( $input ) {
|
||
if ( $input ) {
|
||
$output = '1';
|
||
} else {
|
||
$output = false;
|
||
}
|
||
return $output;
|
||
}
|
||
add_filter( 'of_sanitize_checkbox', 'of_sanitize_checkbox' );
|
||
|
||
/**
|
||
* Sanitization for multicheck input
|
||
*
|
||
* 保存成 array( 键 => 1|0 ),键表来自选项定义里的 options。
|
||
*
|
||
* 关键点:**所有已知键都要出现在结果里**,未勾选的写 0。
|
||
* 如果只保留勾选的键,用户取消全部勾选后存进去是空数组,
|
||
* 读取时无法与「这个选项从未配置过」区分,会静默回退到默认值,
|
||
* 表现成「取消勾选不生效」。这是同类字段最常见的 bug。
|
||
*
|
||
* 输入的键先与选项定义里的白名单求交集,防止用户 POST 伪造任意键。
|
||
*
|
||
* @param array $input 提交上来的值
|
||
* @param array $option 选项定义(含 options 白名单)
|
||
* @return array
|
||
*/
|
||
function of_sanitize_multicheck( $input, $option ) {
|
||
$output = array();
|
||
if ( ! isset( $option['options'] ) || ! is_array( $option['options'] ) ) {
|
||
return $output;
|
||
}
|
||
$input = is_array( $input ) ? $input : array();
|
||
foreach ( $option['options'] as $key => $value ) {
|
||
// 只认白名单里的键;值为真即视为勾选
|
||
$output[ $key ] = ( isset( $input[ $key ] ) && $input[ $key ] ) ? 1 : 0;
|
||
}
|
||
return $output;
|
||
}
|
||
add_filter( 'of_sanitize_multicheck', 'of_sanitize_multicheck', 10, 2 );
|
||
|
||
/**
|
||
* File upload sanitization.
|
||
*
|
||
* Returns a sanitized filepath if it has a valid extension.
|
||
*
|
||
* @param string $input filepath
|
||
* @returns string $output filepath
|
||
*/
|
||
function of_sanitize_upload( $input ) {
|
||
$output = '';
|
||
$filetype = wp_check_filetype( $input );
|
||
if ( $filetype["ext"] ) {
|
||
$output = esc_url( $input );
|
||
}
|
||
return $output;
|
||
}
|
||
add_filter( 'of_sanitize_upload', 'of_sanitize_upload' );
|
||
|
||
/**
|
||
* Sanitization of input with allowed tags and wpautotop.
|
||
*
|
||
* Allows allowed tags in html input and ensures tags close properly.
|
||
*
|
||
* @param string $input
|
||
* @returns string $output
|
||
*/
|
||
function of_sanitize_allowedtags( $input ) {
|
||
global $allowedtags;
|
||
$output = wpautop( wp_kses( $input, $allowedtags ) );
|
||
return $output;
|
||
}
|
||
|
||
/**
|
||
* Sanitization of input with allowed post tags and wpautotop.
|
||
*
|
||
* Allows allowed post tags in html input and ensures tags close properly.
|
||
*
|
||
* @param string $input
|
||
* @returns string $output
|
||
*/
|
||
function of_sanitize_allowedposttags( $input ) {
|
||
global $allowedposttags;
|
||
$output = wpautop( wp_kses( $input, $allowedposttags) );
|
||
return $output;
|
||
}
|
||
|
||
/**
|
||
* Validates that the $input is one of the avilable choices
|
||
* for that specific option.
|
||
*
|
||
* @param string $input
|
||
* @returns string $output
|
||
*/
|
||
function of_sanitize_enum( $input, $option ) {
|
||
$output = '';
|
||
if ( array_key_exists( $input, $option['options'] ) ) {
|
||
$output = $input;
|
||
}
|
||
return $output;
|
||
}
|
||
|
||
/**
|
||
* Sanitization for background option.
|
||
*
|
||
* @returns array $output
|
||
*/
|
||
function of_sanitize_background( $input ) {
|
||
|
||
$output = wp_parse_args( $input, array(
|
||
'color' => '',
|
||
'image' => '',
|
||
'repeat' => 'repeat',
|
||
'position' => 'top center',
|
||
'attachment' => 'scroll'
|
||
) );
|
||
|
||
$output['color'] = apply_filters( 'of_sanitize_hex', $input['color'] );
|
||
$output['image'] = apply_filters( 'of_sanitize_upload', $input['image'] );
|
||
|
||
return $output;
|
||
}
|
||
add_filter( 'of_sanitize_background', 'of_sanitize_background' );
|
||
|
||
/**
|
||
* Sanitize a color represented in hexidecimal notation.
|
||
*
|
||
* @param string Color in hexidecimal notation. "#" may or may not be prepended to the string.
|
||
* @param string The value that this function should return if it cannot be recognized as a color.
|
||
* @return string
|
||
*/
|
||
|
||
function of_sanitize_hex( $hex, $default = '' ) {
|
||
if ( of_validate_hex( $hex ) ) {
|
||
return $hex;
|
||
}
|
||
return $default;
|
||
}
|
||
add_filter( 'of_sanitize_color', 'of_sanitize_hex' );
|
||
|
||
/**
|
||
* Is a given string a color formatted in hexidecimal notation?
|
||
*
|
||
* @param string Color in hexidecimal notation. "#" may or may not be prepended to the string.
|
||
* @return bool
|
||
*/
|
||
function of_validate_hex( $hex ) {
|
||
$hex = trim( $hex );
|
||
/* Strip recognized prefixes. */
|
||
if ( 0 === strpos( $hex, '#' ) ) {
|
||
$hex = substr( $hex, 1 );
|
||
}
|
||
elseif ( 0 === strpos( $hex, '%23' ) ) {
|
||
$hex = substr( $hex, 3 );
|
||
}
|
||
/* Regex match. */
|
||
if ( 0 === preg_match( '/^[0-9a-fA-F]{6}$/', $hex ) ) {
|
||
return false;
|
||
}
|
||
else {
|
||
return true;
|
||
}
|
||
} |