评论邮件重复的根因是发信源不止一个,分两层处理:一层管「谁发」,一层管「发几封」。 只做前者的话邮件照样重复,只是都从同一个 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 不溢出。
330 lines
15 KiB
PHP
330 lines
15 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
|
||
*/
|
||
|
||
class Options_Framework_Interface
|
||
{
|
||
|
||
/**
|
||
* Generates the tabs that are used in the options menu
|
||
*/
|
||
public static function optionsframework_tabs()
|
||
{
|
||
$counter = 0;
|
||
$options = &Options_Framework::_optionsframework_options();
|
||
$menu = '';
|
||
|
||
foreach ($options as $value) {
|
||
// Heading for Navigation
|
||
if ($value['type'] == "heading") {
|
||
$counter++;
|
||
$class = '';
|
||
$class = !empty($value['id']) ? $value['id'] : $value['name'];
|
||
$class = preg_replace('/[^a-zA-Z0-9._\-]/', '', strtolower($class)) . '-tab';
|
||
$menu .= '<a id="options-group-' . $counter . '-tab" class="nav-tab ' . $class . '" title="' . esc_attr($value['name']) . '" href="' . esc_attr('#options-group-' . $counter) . '">' . esc_html($value['name']) . '</a>';
|
||
}
|
||
}
|
||
|
||
return $menu;
|
||
}
|
||
|
||
/**
|
||
* Generates the options fields that are used in the form.
|
||
*/
|
||
public static function optionsframework_fields()
|
||
{
|
||
|
||
global $allowedtags;
|
||
|
||
$options_framework = new Options_Framework;
|
||
$option_name = $options_framework->get_option_name();
|
||
$settings = get_option($option_name);
|
||
$options = &Options_Framework::_optionsframework_options();
|
||
|
||
$counter = 0;
|
||
$menu = '';
|
||
|
||
foreach ($options as $value) {
|
||
|
||
$val = '';
|
||
$select_value = '';
|
||
$output = '';
|
||
|
||
// Wrap all options
|
||
if (($value['type'] != "heading") && ($value['type'] != "info") && ($value['type'] != "about")) {
|
||
|
||
// Keep all ids lowercase with no spaces
|
||
$value['id'] = preg_replace('/[^a-zA-Z0-9._\-]/', '', strtolower($value['id']));
|
||
|
||
$id = 'section-' . $value['id'];
|
||
|
||
$class = 'section';
|
||
if (isset($value['type'])) {
|
||
$class .= ' section-' . $value['type'];
|
||
}
|
||
if (isset($value['class'])) {
|
||
$class .= ' ' . $value['class'];
|
||
}
|
||
|
||
$output .= '<div id="' . esc_attr($id) . '" class="' . esc_attr($class) . '">' . "\n";
|
||
if (isset($value['name'])) {
|
||
$output .= '<h4 class="heading">' . esc_html($value['name']) . '</h4>' . "\n";
|
||
}
|
||
if ($value['type'] != 'editor') {
|
||
$output .= '<div class="option">' . "\n" . '<div class="controls">' . "\n";
|
||
} else {
|
||
$output .= '<div class="option">' . "\n" . '<div>' . "\n";
|
||
}
|
||
}
|
||
|
||
// Set default value to $val
|
||
if (isset($value['std'])) {
|
||
$val = $value['std'];
|
||
}
|
||
|
||
// If the option is already saved, override $val
|
||
if (($value['type'] != 'heading') && ($value['type'] != 'info') && ($value['type'] != "about")) {
|
||
if (isset($settings[($value['id'])])) {
|
||
$val = $settings[($value['id'])];
|
||
// Striping slashes of non-array options
|
||
if (!is_array($val)) {
|
||
$val = stripslashes($val);
|
||
}
|
||
}
|
||
}
|
||
|
||
// If there is a description save it for labels
|
||
$explain_value = '';
|
||
if (isset($value['desc'])) {
|
||
$explain_value = $value['desc'];
|
||
}
|
||
|
||
// Set the placeholder if one exists
|
||
$placeholder = '';
|
||
if (isset($value['placeholder'])) {
|
||
$placeholder = ' placeholder="' . esc_attr($value['placeholder']) . '"';
|
||
}
|
||
|
||
if (has_filter('optionsframework_' . $value['type'])) {
|
||
$output .= apply_filters('optionsframework_' . $value['type'], $option_name, $value, $val);
|
||
}
|
||
|
||
switch ($value['type']) {
|
||
|
||
// Basic text input
|
||
case 'text':
|
||
$output .= '<input id="' . esc_attr($value['id']) . '" class="of-input" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" type="text" value="' . esc_attr($val) . '"' . $placeholder . ' />';
|
||
break;
|
||
|
||
// Password input
|
||
case 'password':
|
||
$output .= '<input id="' . esc_attr($value['id']) . '" class="of-input" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" type="password" value="' . esc_attr($val) . '" />';
|
||
break;
|
||
|
||
// Textarea
|
||
case 'textarea':
|
||
$rows = '8';
|
||
|
||
if (isset($value['settings']['rows'])) {
|
||
$custom_rows = $value['settings']['rows'];
|
||
if (is_numeric($custom_rows)) {
|
||
$rows = $custom_rows;
|
||
}
|
||
}
|
||
|
||
$val = stripslashes($val);
|
||
$output .= '<textarea id="' . esc_attr($value['id']) . '" class="of-input" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" rows="' . $rows . '"' . $placeholder . '>' . esc_textarea($val) . '</textarea>';
|
||
break;
|
||
|
||
// Raw code textarea(自定义 CSS / JS 用)
|
||
// 这里必须直接读 $settings 拿原始值,不能用 $val:
|
||
// 上面的公共逻辑对所有非数组值做了 stripslashes(),那是给老版本
|
||
// WP 入库带反斜杠的历史写法留的兼容;现在 options.php 已经
|
||
// wp_unslash 过,值本身是干净的,再剥一层会把 CSS 里合法的
|
||
// 转义(content: "\f101")和 JS 的 "\n" 啃坏。
|
||
case 'code':
|
||
$rows = '10';
|
||
|
||
if (isset($value['settings']['rows'])) {
|
||
$custom_rows = $value['settings']['rows'];
|
||
if (is_numeric($custom_rows)) {
|
||
$rows = $custom_rows;
|
||
}
|
||
}
|
||
|
||
if (isset($settings[($value['id'])])) {
|
||
$raw = $settings[($value['id'])];
|
||
} elseif (isset($value['std'])) {
|
||
$raw = $value['std'];
|
||
} else {
|
||
$raw = '';
|
||
}
|
||
if (!is_string($raw)) {
|
||
$raw = '';
|
||
}
|
||
|
||
$output .= '<textarea id="' . esc_attr($value['id']) . '" class="of-input code" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" rows="' . $rows . '"' . $placeholder . ' spellcheck="false">' . esc_textarea($raw) . '</textarea>';
|
||
break;
|
||
|
||
// Select Box
|
||
case 'select':
|
||
$output .= '<select class="of-input" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" id="' . esc_attr($value['id']) . '">';
|
||
|
||
foreach ($value['options'] as $key => $option) {
|
||
$output .= '<option' . selected($val, $key, false) . ' value="' . esc_attr($key) . '">' . esc_html($option) . '</option>';
|
||
}
|
||
$output .= '</select>';
|
||
break;
|
||
|
||
// Radio Box
|
||
case "radio":
|
||
$name = $option_name . '[' . $value['id'] . ']';
|
||
foreach ($value['options'] as $key => $option) {
|
||
$id = $option_name . '-' . $value['id'] . '-' . $key;
|
||
$output .= '<input class="of-input of-radio" type="radio" name="' . esc_attr($name) . '" id="' . esc_attr($id) . '" value="' . esc_attr($key) . '" ' . checked($val, $key, false) . ' /><label for="' . esc_attr($id) . '">' . esc_html($option) . '</label>';
|
||
}
|
||
break;
|
||
|
||
// Image Selectors
|
||
case "images":
|
||
$name = $option_name . '[' . $value['id'] . ']';
|
||
foreach ($value['options'] as $key => $option) {
|
||
$selected = '';
|
||
if ($val != '' && ($val == $key)) {
|
||
$selected = ' of-radio-img-selected';
|
||
}
|
||
$output .= '<input type="radio" id="' . esc_attr($value['id'] . '_' . $key) . '" class="of-radio-img-radio" value="' . esc_attr($key) . '" name="' . esc_attr($name) . '" ' . checked($val, $key, false) . ' />';
|
||
$output .= '<div class="of-radio-img-label">' . esc_html($key) . '</div>';
|
||
$output .= '<img src="' . esc_url($option) . '" alt="' . $option . '" class="of-radio-img-img' . $selected . '" onclick="document.getElementById(\'' . esc_attr($value['id'] . '_' . $key) . '\').checked=true;" />';
|
||
}
|
||
break;
|
||
|
||
// Checkbox
|
||
case "checkbox":
|
||
$output .= '<input id="' . esc_attr($value['id']) . '" class="checkbox of-input" type="checkbox" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" ' . checked($val, 1, false) . ' />';
|
||
$output .= '<label class="explain" for="' . esc_attr($value['id']) . '">' . wp_kses($explain_value, $allowedtags) . '</label>';
|
||
break;
|
||
|
||
// 多选复选框组
|
||
//
|
||
// 框架原来只在保存侧处理了 multicheck(把未提交的项补 false),
|
||
// **没有渲染分支、也没有 of_sanitize_multicheck**,
|
||
// 所以这个类型此前是不可用的。这里补齐两处。
|
||
//
|
||
// 值统一存成 array( 键 => 1|0 ),而不是「勾选了的键组成的列表」:
|
||
// 用 map 才能区分「用户明确取消勾选」和「这个键是新增的」。
|
||
// 若存成列表,用户取消全部勾选后存进去是空数组,
|
||
// 读的时候无法与「从未配置」区分,会静默回退到默认值。
|
||
case "multicheck":
|
||
$name = $option_name . '[' . $value['id'] . ']';
|
||
$saved = is_array($val) ? $val : array();
|
||
if (!empty($value['options'])) {
|
||
$output .= '<fieldset class="of-multicheck">';
|
||
foreach ($value['options'] as $key => $opt) {
|
||
$id = $option_name . '-' . $value['id'] . '-' . $key;
|
||
$on = !empty($saved[$key]);
|
||
$label = is_array($opt) && isset($opt['name']) ? $opt['name'] : $opt;
|
||
$note = is_array($opt) && isset($opt['note']) ? $opt['note'] : '';
|
||
$output .= '<label class="of-multicheck-item" for="' . esc_attr($id) . '">';
|
||
$output .= '<input class="checkbox of-input" type="checkbox" '
|
||
. 'id="' . esc_attr($id) . '" '
|
||
. 'name="' . esc_attr($name . '[' . $key . ']') . '" '
|
||
. 'value="1" ' . checked($on, true, false) . ' />';
|
||
// 名称与说明必须包在同一个竖排容器里。
|
||
// 若让它们各自作为 label 的直接子项,flex 布局会把
|
||
// 说明挤到名称右边同一行(不是名称下方)。
|
||
$output .= '<span class="of-multicheck-text">';
|
||
$output .= '<span class="of-multicheck-label">' . esc_html($label) . '</span>';
|
||
if ($note !== '') {
|
||
$output .= '<span class="of-multicheck-note">' . esc_html($note) . '</span>';
|
||
}
|
||
$output .= '</span>';
|
||
$output .= '</label>';
|
||
}
|
||
$output .= '</fieldset>';
|
||
}
|
||
break;
|
||
|
||
// Color picker
|
||
case "color":
|
||
$default_color = '';
|
||
if (isset($value['std'])) {
|
||
if ($val != $value['std']) {
|
||
$default_color = ' data-default-color="' . $value['std'] . '" ';
|
||
}
|
||
|
||
}
|
||
$output .= '<input name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" id="' . esc_attr($value['id']) . '" class="of-color" type="text" value="' . esc_attr($val) . '"' . $default_color . ' />';
|
||
|
||
break;
|
||
|
||
// Uploader
|
||
case "upload":
|
||
$output .= Options_Framework_Media_Uploader::optionsframework_uploader($value['id'], $val, null);
|
||
|
||
break;
|
||
|
||
case "info":
|
||
$id = '';
|
||
$class = 'section';
|
||
if (isset($value['id'])) {
|
||
$id = 'id="' . esc_attr($value['id']) . '" ';
|
||
}
|
||
if (isset($value['type'])) {
|
||
$class .= ' section-' . $value['type'];
|
||
}
|
||
if (isset($value['class'])) {
|
||
$class .= ' ' . $value['class'];
|
||
}
|
||
|
||
$output .= '<div ' . $id . 'class="' . esc_attr($class) . '">' . "\n";
|
||
if (isset($value['name'])) {
|
||
$output .= '<h4 class="heading">' . esc_html($value['name']) . '</h4>' . "\n";
|
||
}
|
||
if (isset($value['desc'])) {
|
||
$output .= $value['desc'] . "\n";
|
||
}
|
||
$output .= '</div>' . "\n";
|
||
break;
|
||
case "sendmail":
|
||
$output .= '<input type="submit" name="sendmail" class="button-secondary" value="' . __('测试邮件', 'kratos') . '">';
|
||
break;
|
||
|
||
// Heading for Navigation
|
||
case "heading":
|
||
$counter++;
|
||
if ($counter >= 2) {
|
||
$output .= '</div>' . "\n";
|
||
}
|
||
$class = '';
|
||
$class = !empty($value['id']) ? $value['id'] : $value['name'];
|
||
$class = preg_replace('/[^a-zA-Z0-9._\-]/', '', strtolower($class));
|
||
$output .= '<div id="options-group-' . $counter . '" class="group ' . $class . '">';
|
||
$output .= '<h3>' . esc_html($value['name']) . '</h3>' . "\n";
|
||
break;
|
||
}
|
||
|
||
if (($value['type'] != "heading") && ($value['type'] != "info") && ($value['type'] != "about")) {
|
||
$output .= '</div>';
|
||
if (($value['type'] != "checkbox") && ($value['type'] != "editor")) {
|
||
$output .= '<div class="explain">' . wp_kses($explain_value, $allowedtags) . '</div>' . "\n";
|
||
}
|
||
$output .= '</div></div>' . "\n";
|
||
}
|
||
|
||
echo $output;
|
||
}
|
||
|
||
// Outputs closing div if there tabs
|
||
if (Options_Framework_Interface::optionsframework_tabs() != '') {
|
||
echo '</div>';
|
||
}
|
||
}
|
||
|
||
}
|