Files
laoyang adcf80af62 3.2.35 CDN 加速加启用开关 + 预制白/黑名单,勾了不填地址拒绝保存 #4
「启用 CDN 加速」复选框放在 CDN 段最前。不勾时下面填什么都不产生作用,
前台与没装该功能完全一致(三个入口 should_rewrite / rewrite_html /
buffer_start 都过 yang_cdn_active())。

预制名单写进输入框的 std,用户可直接增删改,保存后以用户内容为准:
  白名单 13 项(.avif .css .gif .jpeg .jpg .js .json .mp3 .mp4 .pdf
  .png .svg .webp);黑名单 13 项安全后缀(.php* .phtml .phar .env
  .sql .bak .log .ini .sh .htaccess)。同时 yang_cdn_defaults() 给同一份
  —— std 在运行时完全不生效,两处不同源就会出现「后台 13 项、前端 0 项」。

勾了开关但地址留空:保存被整单拒绝(validate_options 返回旧值),并
add_settings_error 提示。回调里绝不 update_option(会递归触发
sanitize_option_kratos,3.2.11 吃爆内存的路径)。另加 admin_init 进页面
提示,救「勾着但地址空」的老站点。

开关默认值按「地址是否非空」推算,老站点升级不丢加速;没填过地址的站点
默认不勾,避免第一次点保存就被「必须填地址」拦住。

顺带修掉 parse_url 对主机过于宽容导致的畸形地址(https://not a url at all
曾被当合法,会把全站静态资源拼成畸形地址)。

验证:_cdn335_logic.php 116/0 · _cdn335_falsify.php 22/0 ·
_admin335_cdn_check.py 74/0 · _admin335_falsify.py 23/0 ·
_pkgcheck335.py 117/0 · _pkgcheck335_falsify.py 27/0 ·
_cdn334_logic.php 92/0(向后兼容)· _cdn334_falsify.php 21/0
2026-10-25 17:10:06 +08:00

510 lines
18 KiB
PHP
Raw Permalink 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
/**
* @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_Admin {
/**
* Page hook for the options screen
*
* @since 1.7.0
* @type string
*/
protected $options_screen = null;
/**
* Hook in the scripts and styles
*
* @since 1.7.0
*/
public function init() {
// Gets options to load
$options = & Options_Framework::_optionsframework_options();
// Checks if options are available
if ( $options ) {
// Add the options page and menu item.
add_action('admin_menu', array($this, 'add_top_options_page'));
add_action('admin_menu', array($this, 'add_sub_options_page'));
// Add the required scripts and styles
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
// Settings need to be registered after admin_init
add_action( 'admin_init', array( $this, 'settings_init' ) );
}
}
/**
* Registers the settings
*
* @since 1.7.0
*/
function settings_init() {
// Get the option name
$options_framework = new Options_Framework;
$name = $options_framework->get_option_name();
// Registers the settings fields and callback
register_setting( 'optionsframework', $name, array ( $this, 'validate_options' ) );
// Displays notice after options save
add_action( 'optionsframework_after_validate', array( $this, 'save_options_notice' ) );
add_action( 'optionsframework_after_sendmail', array( $this, 'send_mail_notice' ) );
/*
* 进设置页就检查 CDN 配置是否"勾了但没填地址"。
*
* 挂在 admin_init 上、在 options_page() 渲染之前跑,
* 这样 add_settings_error 的结果会被页面里的 settings_errors() 显示。
* 用 current_user_can 兜一层:虽然本类整体已经在
* optionsframework_init 里判过权限,但这个钩子单独挂,
* 以后若有人把它挪出去也不会漏权限判断。
*/
add_action( 'admin_init', array( $this, 'yang_cdn_config_notice' ), 21 );
}
public function add_top_options_page()
{
add_menu_page(
__('主题设置', 'kratos'),
__('主题设置', 'kratos'),
'manage_options',
'kratos_options',
'',
'dashicons-admin-generic',
99
);
}
public static function menu_settings()
{
$menu = array(
'parent_slug' => 'kratos_options',
'page_title' => __('主题设置', 'kratos'),
'menu_title' => __('主题设置', 'kratos'),
'capability' => 'manage_options',
'menu_slug' => 'kratos_options',
);
return $menu;
}
public function add_sub_options_page()
{
$menu = $this->menu_settings();
$this->options_screen = add_submenu_page(
$menu['parent_slug'],
$menu['page_title'],
$menu['menu_title'],
$menu['capability'],
$menu['menu_slug'],
array($this, 'options_page')
);
}
/**
* Loads the required stylesheets
*
* @since 1.7.0
*/
function enqueue_admin_styles( $hook ) {
if ( $this->options_screen != $hook )
return;
// ⚠️ 版本号必须跟主题版本(THEME_VERSION)走:3.2.24 踩坑——
// 原来用框架常量 Options_Framework::VERSION(1.9.1 永不变),主题升级后
// 资源 URL 不变,浏览器拿缓存旧 CSS,后台「设置页重排」看起来像没做。
wp_enqueue_style( 'optionsframework', get_stylesheet_directory_uri() . '/inc/options-framework/css/optionsframework.css', array(), THEME_VERSION );
wp_enqueue_style( 'wp-color-picker' );
}
/**
* Loads the required javascript
*
* @since 1.7.0
*/
function enqueue_admin_scripts( $hook ) {
if ( $this->options_screen != $hook )
return;
// Enqueue custom option panel JS
wp_enqueue_script(
'options-custom',
get_stylesheet_directory_uri() . '/inc/options-framework/js/options-custom.js',
array( 'jquery','wp-color-picker' ),
THEME_VERSION
);
// Inline scripts from options-interface.php
add_action( 'admin_head', array( $this, 'of_admin_head' ) );
}
function of_admin_head() {
// Hook to add custom scripts
do_action( 'optionsframework_custom_scripts' );
}
/**
* Builds out the options panel.
*
* If we were using the Settings API as it was intended we would use
* do_settings_sections here. But as we don't want the settings wrapped in a table,
* we'll call our own custom optionsframework_fields. See options-interface.php
* for specifics on how each individual field is generated.
*
* Nonces are provided using the settings_fields()
*
* @since 1.7.0
*/
function options_page() { ?>
<div id="optionsframework-wrap" class="wrap">
<?php $menu = $this->menu_settings(); ?>
<h2><?php echo esc_html( $menu['page_title'] ); ?></h2>
<h2 class="nav-tab-wrapper">
<?php echo Options_Framework_Interface::optionsframework_tabs(); ?>
</h2>
<?php settings_errors( 'options-framework' ); ?>
<div id="optionsframework-metabox" class="metabox-holder">
<div id="optionsframework" class="postbox">
<form action="options.php" method="post">
<?php settings_fields( 'optionsframework' ); ?>
<?php Options_Framework_Interface::optionsframework_fields(); /* Settings */ ?>
<div id="optionsframework-submit">
<input type="submit" class="button-primary" name="update" value="<?php esc_attr_e( '保存配置', 'kratos' ); ?>" />
<input type="submit" class="reset-button button-secondary" name="reset" value="<?php esc_attr_e( '恢复默认', 'kratos' ); ?>" onclick="return confirm( '<?php print esc_js( __( '单击「确定」进行恢复,但所有的配置都将丢失!', 'kratos' ) ); ?>' );" />
<div class="clear"></div>
</div>
</form>
</div> <!-- / #container -->
</div>
<?php do_action( 'optionsframework_after' ); ?>
</div> <!-- / .wrap -->
<?php
}
/**
* Validate Options.
*
* This runs after the submit/reset button has been clicked and
* validates the inputs.
*
* @uses $_POST['reset'] to restore default options
*/
function validate_options( $input ) {
/*
* Restore Defaults.
*
* In the event that the user clicked the "Restore Defaults"
* button, the options defined in the theme's options.php
* file will be added to the option for the active theme.
*/
if ( isset( $_POST['reset'] ) ) {
add_settings_error( 'options-framework', 'restore_defaults', __( '恢复完成', 'kratos' ), 'updated fade' );
return $this->get_default_values();
}
/*
* Update Settings
*
* This used to check for $_POST['update'], but has been updated
* to be compatible with the theme customizer introduced in WordPress 3.4
*/
$clean = array();
$options = & Options_Framework::_optionsframework_options();
// 已保存的值,供"字段未提交时沿用旧值"用。放循环外,避免重复查库。
$saved_options = get_option( 'kratos' );
if ( ! is_array( $saved_options ) ) {
$saved_options = array();
}
foreach ( $options as $option ) {
if ( ! isset( $option['id'] ) ) {
continue;
}
if ( ! isset( $option['type'] ) ) {
continue;
}
$id = preg_replace( '/[^a-zA-Z0-9._\-]/', '', strtolower( $option['id'] ) );
// Set checkbox to false if it wasn't sent in the $_POST
if ( 'checkbox' == $option['type'] && ! isset( $input[$id] ) ) {
$input[$id] = false;
}
// Set each item in the multicheck to false if it wasn't sent in the $_POST
if ( 'multicheck' == $option['type'] && ! isset( $input[$id] ) ) {
foreach ( $option['options'] as $key => $value ) {
$input[$id][$key] = false;
}
}
// For a value to be submitted to database it must pass through a sanitization filter
if ( has_filter( 'of_sanitize_' . $option['type'] ) ) {
/*
* ⚠️ 表单里没有这个键时,**必须保留库里已有的值**,不能写空。
*
* 原来这里直接写 `$input[$id]`。如果该字段没被提交(比如某个
* 分组没渲染出来、或者字段被条件隐藏、或者表单中途出错只提交了一半),
* `$input[$id]` 是未定义索引 ⇒ 取到 null ⇒
* `sanitize_text_field(null)` 返回 `''` ⇒ **把用户存了很久的
* 配置静默清成空串**。而"清成空串"和"用户主动清空"在库里长得一模一样,
* 事后完全无法区分,只会表现为"我明明填过,怎么没了"。
*
* 例外:checkbox 和 multicheck 上面已经把缺省项补成了 false,
* 这两类的"没提交"在语义上确实等于"没勾",保持原样。
*/
$is_toggle = in_array( $option['type'], array( 'checkbox', 'multicheck' ), true );
if ( array_key_exists( $id, $input ) ) {
$clean[$id] = apply_filters( 'of_sanitize_' . $option['type'], $input[$id], $option );
} elseif ( $is_toggle ) {
$clean[$id] = apply_filters( 'of_sanitize_' . $option['type'], $input[$id], $option );
} else {
// 没提交 ⇒ 沿用旧值(旧值也没有就不写这个键,让运行时默认值生效)
if ( array_key_exists( $id, $saved_options ) ) {
$clean[$id] = $saved_options[$id];
}
}
}
}
/*
* ★ CDN 配置校验(3.2.35)
*
* 「勾了启用但地址为空」是**非法配置**,不是"关闭"。用户原话:
* 「如果点了这个复选框,但是 CDN 地址不填,那就不行,得提示他
* 说这个东西必须填。」所以这里拦住整个保存。
*
* ── 怎么做到"拒绝保存" ──────────────────────────────────
* 本函数是 register_setting() 的 sanitize_callback,WP 会把
* 它的返回值直接写进 options 表。所以**返回旧值就等于拒绝本次
* 保存**(新值一个字段都不落库),不需要额外抛异常或 unset。
*
* 为什么是"整单拒绝"而不是"只把开关取消":用户明确选了这条路
* (选项一「整个保存被拒绝」)。附带效果是同一次里改的其它设置
* 也会一起作废 —— 这是有意的:提交了一个不合法的表单,就不该
* 让其中一部分悄悄生效,否则用户会以为"我保存成功了"。
*
* ── 为什么用 $_POST 而不是 $input ──────────────────────
* $input 已经过 WP 的 wp_unslash,但复选框"没勾"时这个键
* **根本不会出现在 POST 里**(浏览器不提交未勾选的 checkbox)。
* 而这里的 $input 是同名过滤器链上拿到的数组,语义一致;
* 用 $input 就够,且不依赖具体表单字段名。$clean 里已经有
* 经过 of_sanitize_checkbox() 的值,直接用它最准。
*/
if ( function_exists( 'yang_cdn_check_config' ) ) {
$cdn_enable = isset( $clean['cdn_enable'] ) ? $clean['cdn_enable'] : false;
$cdn_url = isset( $clean['cdn_url'] ) ? $clean['cdn_url'] : '';
$cdn_err = yang_cdn_check_config( $cdn_enable, $cdn_url );
if ( $cdn_err !== '' ) {
/*
* ⚠️ 这里**绝对不能**调 update_option() 把旧值写回去。
* 本函数就在 sanitize_option_kratos 过滤器上,
* update_option() 会再触发一次同名过滤器 ⇒ 无限递归
* ⇒ 吃爆内存(3.2.11 的事故,报错落在区块解析器上,
* 看那个报错会完全找错方向)。见下面 sendmail 分支的注释。
*
* 正确做法就是"返回旧值":WP 自己会把它写进库,
* 结果与"没保存"等价。
*/
add_settings_error(
'options-framework',
'cdn_needs_url',
__( '未保存:', 'kratos' ) . $cdn_err,
'error'
);
/*
* 「保存成功」那条提示不用手动清:本函数在校验分支里
* 直接 return,后面的 do_action('optionsframework_after_validate')
* 根本不会执行,所以它不会被加进 settings_errors。
* 若改成"清一下",反而是多余动作,会让人误以为
* 两条提示都会出现、需要手工压掉。
*/
$current = get_option( 'kratos' );
return is_array( $current ) ? $current : array();
}
}
if ( isset( $_POST['sendmail'] ) ) {
/*
* 测试发信要让「这一批新配置」立刻生效,但**绝对不能**
* 在这里调 update_option()。
*
* ── 为什么不调(3.2.11 的血案)──────────────────────────
* 本函数是 register_setting() 注册的 sanitize_callback,
* WP 把挂在 `sanitize_option_kratos` 这个过滤器上
* (wp-includes/option.php:3073)。而 update_option() 内部
* **会再跑一遍**同名过滤器 ⇒ 又调回本函数 ⇒ 本函数再
* update_option() ⇒ 无限递归。
*
* 递归每层都在复制 $clean 和 $_POST 派生的大数组,最终把
* 内存吃到上限,报出来的是
* 「Allowed memory size of 268435456 bytes exhausted
* in class-wp-block-parser.php」。报错落在区块解析器上纯属
* 巧合 —— 那只是递归链最后执行到的一行,看它会完全找错方向。
*
* 正确做法:不写库、不递归,只把新值放进一个一次性覆盖层,
* 让 kratos_option() 在本次请求里优先读它。写完库由 WP 自己
* 在 validate_options 返回后完成。
*/
$GLOBALS['__yang_options_override'] = is_array( $clean ) ? $clean : array();
$this->yang_send_test_mail( $clean );
do_action( 'optionsframework_after_sendmail', $clean );
return $clean;
} else {
do_action( 'optionsframework_after_validate', $clean );
return $clean;
}
}
/**
* 发测试邮件,走主题模板
*
* 原来这里是一句裸 wp_mail(),正文只有一行纯文本 —— 点「测试邮件」
* 收到的样子和真实评论通知(HTML 模板)完全不同,等于没法用它验收版式。
* 主题里早有 yang_mail_send_test() 专门干这件事,但一直没接线,
* 所以优先调它;主题被换掉或不完全时退回原样,不能因此白屏。
*/
private function yang_send_test_mail( $clean )
{
$to = get_bloginfo( 'admin_email' );
if ( function_exists( 'yang_mail_send_test' ) ) {
$r = yang_mail_send_test( $to );
$GLOBALS['__yang_sendmail_result'] = $r;
return;
}
wp_mail( $to, __( '[测试]邮件服务配置成功', 'kratos' ), __( '恭喜您 SMTP 邮件服务配置成功!', 'kratos' ) );
$GLOBALS['__yang_sendmail_result'] = null;
}
/**
* 进入设置页时的状态提示
*
* ── 为什么需要在"进页面"时就提示 ────────────────────────────
* 保存时的拦截只在用户点保存那一刻生效。而"勾了启用但地址空"
* 这个状态还有另一种来源:**老站点从 3.2.34 升级上来**。
* 3.2.34 没有这个复选框,升级后如果用户清空过地址、又一直没动
* 过这个开关,他就是"看起来已启用、实际没生效"的状态。
* 这种人不会点保存(他没想改设置),所以只靠保存拦截永远救不到他。
*
* 提示放在 options_page() 渲染之前(本函数挂在 admin_init),
* add_settings_error 会随 settings_errors() 一起显示。
*/
function yang_cdn_config_notice() {
if ( ! function_exists( 'yang_cdn_missing_url' ) ) {
return;
}
if ( ! yang_cdn_missing_url() ) {
return;
}
add_settings_error(
'options-framework',
'cdn_missing_url',
__( '「启用 CDN 加速」是勾选状态,但 CDN 地址是空的,功能当前<b>没有生效</b>(前台与关闭时完全一样)。请填写 CDN 地址,或者取消勾选这个开关。', 'kratos' ),
'error'
);
}
/**
* Display message when options have been saved
*/
function save_options_notice() {
add_settings_error( 'options-framework', 'save_options', __( '保存成功', 'kratos' ), 'updated fade' );
}
/**
* 测试邮件的提示
*
* 原来不管发没发出去都显示「发送完成」—— SMTP 密码填错时用户也看到
* "成功",然后去邮箱里干等。现在按真实返回值分三态:成功 / 失败 / 地址不合法。
*/
function send_mail_notice() {
$to = get_bloginfo( 'admin_email' );
$r = isset( $GLOBALS['__yang_sendmail_result'] ) ? $GLOBALS['__yang_sendmail_result'] : null;
if ( is_wp_error( $r ) ) {
add_settings_error(
'options-framework', 'send_mail',
sprintf( __( '测试邮件未发出:%s', 'kratos' ), $r->get_error_message() ),
'error fade'
);
return;
}
if ( $r === false ) {
add_settings_error(
'options-framework', 'send_mail',
__( '测试邮件发送失败,请检查服务器地址、端口、加密方式与密码。', 'kratos' ),
'error fade'
);
return;
}
add_settings_error( 'options-framework', 'send_mail', sprintf( __( '测试邮件已发出,请查收:%s(若没收到请先看垃圾箱)', 'kratos' ), $to ), 'updated fade' );
}
/**
* Get the default values for all the theme options
*
* Get an array of all default values as set in
* options.php. The 'id','std' and 'type' keys need
* to be defined in the configuration array. In the
* event that these keys are not present the option
* will not be included in this function's output.
*
* @return array Re-keyed options configuration array.
*
*/
function get_default_values() {
$output = array();
$config = & Options_Framework::_optionsframework_options();
foreach ( (array) $config as $option ) {
if ( ! isset( $option['id'] ) ) {
continue;
}
if ( ! isset( $option['std'] ) ) {
continue;
}
if ( ! isset( $option['type'] ) ) {
continue;
}
if ( has_filter( 'of_sanitize_' . $option['type'] ) ) {
$output[$option['id']] = apply_filters( 'of_sanitize_' . $option['type'], $option['std'], $option );
}
}
return $output;
}
}