Files
wordpress-kratos-yang/inc/options-framework/includes/class-options-framework-admin.php
T

510 lines
18 KiB
PHP
Raw Normal View History

2022-01-07 11:15:42 +08:00
<?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 );
2022-01-07 11:15:42 +08:00
}
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 );
2022-01-07 11:15:42 +08:00
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
2022-01-07 11:15:42 +08:00
);
// 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();
}
2022-01-07 11:15:42 +08:00
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];
}
}
2022-01-07 11:15:42 +08:00
}
}
/*
* ★ 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();
}
}
2022-01-07 11:15:42 +08:00
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 );
2022-01-07 11:15:42 +08:00
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'
);
}
2022-01-07 11:15:42 +08:00
/**
* Display message when options have been saved
*/
function save_options_notice() {
add_settings_error( 'options-framework', 'save_options', __( '保存成功', 'kratos' ), 'updated fade' );
}
/**
* 测试邮件的提示
*
* 原来不管发没发出去都显示「发送完成」—— SMTP 密码填错时用户也看到
* "成功",然后去邮箱里干等。现在按真实返回值分三态:成功 / 失败 / 地址不合法。
*/
2022-01-07 11:15:42 +08:00
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' );
2022-01-07 11:15:42 +08:00
}
/**
* 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;
}
}