* @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() { ?>
menu_settings(); ?>

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 地址是空的,功能当前没有生效(前台与关闭时完全一样)。请填写 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; } }