EP127. “设置表单收尾:回显、复用 HTML 函数与自定义校验”
🔒 登录后可标记已读把上一讲的设置表单填完整:① 用 selected()/checked() 让下拉框/勾选框正确回显数据库里已保存的值;② 补齐剩下的「标题文字」输入框和三个勾选框(字数统计/字符统计/阅读时间),其中三个勾选框刻意写成一个可复用的函数(用参数区分具体是哪个字段),而不是复制三份几乎一样的代码;③ 学习自定义校验逻辑(sanitize_callback 换成自己写的函数),演示如果用户绕过前端下拉框、用浏览器开发者工具硬塞一个非法值,服务端也能拦截并保留旧值。
涉及文件
wp-content/plugins/our-first-unique-plugin/our-first-unique-plugin.php(修改)
代码实现
settings():补齐剩余 4 个字段的注册(标题文字 + 三个勾选框,勾选框共用同一个 HTML 回调函数,用第 6 个参数传入区分用的 theName):
function settings() {
add_settings_section('wcp_first_section', null, null, 'word-count-settings-page');
add_settings_field('wcp_location', 'Display Location', array($this, 'locationHTML'), 'word-count-settings-page', 'wcp_first_section');
register_setting('wordcountplugin', 'wcp_location', array('sanitize_callback' => array($this, 'sanitizeLocation'), 'default' => '0'));
add_settings_field('wcp_headline', 'Headline Text', array($this, 'headlineHTML'), 'word-count-settings-page', 'wcp_first_section');
register_setting('wordcountplugin', 'wcp_headline', array('sanitize_callback' => 'sanitize_text_field', 'default' => 'Post Statistics'));
add_settings_field('wcp_wordcount', 'Word Count', array($this, 'checkboxHTML'), 'word-count-settings-page', 'wcp_first_section', array('theName' => 'wcp_wordcount'));
register_setting('wordcountplugin', 'wcp_wordcount', array('sanitize_callback' => 'sanitize_text_field', 'default' => '1'));
add_settings_field('wcp_charactercount', 'Character Count', array($this, 'checkboxHTML'), 'word-count-settings-page', 'wcp_first_section', array('theName' => 'wcp_charactercount'));
register_setting('wordcountplugin', 'wcp_charactercount', array('sanitize_callback' => 'sanitize_text_field', 'default' => '1'));
add_settings_field('wcp_readtime', 'Read Time', array($this, 'checkboxHTML'), 'word-count-settings-page', 'wcp_first_section', array('theName' => 'wcp_readtime'));
register_setting('wordcountplugin', 'wcp_readtime', array('sanitize_callback' => 'sanitize_text_field', 'default' => '1'));
}
下拉框回显(selected())+ 输入框回显(esc_attr() + get_option()):
function headlineHTML() { ?>
<input type="text" name="wcp_headline" value="<?php echo esc_attr(get_option('wcp_headline')) ?>">
<?php }
function locationHTML() { ?>
<select name="wcp_location">
<option value="0" <?php selected(get_option('wcp_location'), '0') ?>>Beginning of post</option>
<option value="1" <?php selected(get_option('wcp_location'), '1') ?>>End of post</option>
</select>
<?php }
可复用的勾选框 HTML 函数(接收一个 $args 数组,靠 theName 区分具体是哪个字段,避免写三份几乎相同的函数):
// reusable checkbox function
function checkboxHTML($args) { ?>
<input type="checkbox" name="<?php echo $args['theName'] ?>" value="1" <?php checked(get_option($args['theName']), '1') ?>>
<?php }
自定义校验函数(把 sanitize_callback 从通用的 sanitize_text_field 换成自己的方法,拦截非法值):
function sanitizeLocation($input) {
if ($input != '0' AND $input != '1') {
add_settings_error('wcp_location', 'wcp_location_error', 'Display location must be either beginning or end.');
return get_option('wcp_location');
}
return $input;
}
关键改动点:
selected($value1, $value2)/checked($value1, $value2)是 WordPress 内建的一对辅助函数:两个值相等时分别输出selected="selected"/checked="checked"这个 HTML 属性,不相等就什么都不输出——不用自己写if判断再手动拼字符串get_option($name)可以在同一个请求里反复调用多次而不会有性能损耗——WordPress 会在首次请求时把wp_options表里所有标记为autoload = yes的选项一次性加载进内存缓存,之后同一请求内的get_option()调用都是读内存,不会重复查询数据库- 三个勾选框的差异只在字段名(
name属性和get_option()查询的键名),所以把这部分「唯一变化的字符串」抽成$args['theName']参数,checkboxHTML($args)就能被三次add_settings_field()共用——add_settings_field()的第六个参数(数组)会被原样传给对应的 HTML 回调函数,属性名(这里是theName)是自己随便取的,只要跟读取时的 key 对上就行 esc_attr()包住get_option()的输出结果再塞进value="..."属性——即使这个页面只有管理员能看到,依然坚持「凡是把数据库内容输出到 HTML 属性就要转义」的安全习惯- 自定义校验函数
sanitizeLocation($input)替换掉的是register_setting()第三个参数里的sanitize_callback,写法从字符串'sanitize_text_field'换成array($this, 'sanitizeLocation')(跟其他方法引用方式一致);WordPress 会把用户提交的原始值作为$input传进来,只要这个函数最终return了什么值,那就是最终会被存进数据库的值 - 校验失败时的标准做法:
add_settings_error($setting, $code, $message)往页面顶部添加一条红色错误提示(三个参数依次是关联的设置名、这条错误自己的 ID、给用户看的文字),然后return get_option('wcp_location')——返回数据库里原本的旧值,而不是用户刚提交的非法值,等于让这次非法提交完全不生效 - 转录里演示了真实的攻击场景:用浏览器开发者工具直接把
<select>元素的某个<option>的value改成500再提交——证明「前端限制了下拉框只有两个选项」不能替代「后端也要做校验」,因为前端的 HTML 结构本身可以被访问者随意篡改后再提交
[截图:wp-admin Word Count 设置页面完整表单,含 Headline Text 输入框和三个已回显勾选状态的复选框]
[截图:用开发者工具篡改下拉框选项后提交,页面顶部出现的 "Display location must be either beginning or end." 错误提示]
Hook / Function 速查
| 名称 | 类型 | 用途 |
|---|---|---|
selected($a, $b) | WP 内建 function | 两值相等时输出 selected="selected",用于 <option> |
checked($a, $b) | WP 内建 function | 两值相等时输出 checked="checked",用于 checkbox/radio |
get_option($name) | WP 内建 function | 读取指定名称的选项值,同一请求内多次调用不会重复查库 |
add_settings_error($setting, $code, $message, $type) | WP 内建 function | 在设置页面顶部显示一条提示/错误信息 |
常见坑
- 以为前端
<select>/<input>限制了取值范围就足够安全——浏览器开发者工具能让任何人绕过前端 HTML 的限制直接提交任意值,服务端必须独立做一遍校验,不能只信任前端 - 自定义校验函数校验失败时忘记
return旧值——如果不return任何东西(或者返回了非法的新值),会导致数据库存入错误数据,或者报 PHP 相关的警告 - 多个几乎相同的字段各写一份几乎重复的 HTML 回调函数——转录特意展示了「保留旧代码作为注释、换成参数化的复用函数」这个重构过程,值得作为「发现重复代码时如何重构」的参考
延伸 / 后续讲座会用到
表单和存储都已经完成,下一讲开始写插件真正的核心逻辑——读取这些设置,计算字数/字符数/预估阅读时间,拼进博客文章详情页。
Sources
Udemy:
- Become a WordPress Developer: Unlocking Power With Code — Section 23, EP127