把自定义样式从主题目录搬到主题选项,升级不再丢

原来主题里有个 custom/ 目录(custom.css / custom.js / custom.php),
是给用户随手加样式的入口。问题是它位于主题目录内,而 WordPress 升级
主题的做法是把整个主题目录删掉再解压(class-theme-upgrader.php 里
clear_destination = true,随后 delete_old_theme() 递归删除),
所以每次升级这些文件都会丢。custom.php 还会被 require,
等于在主题里留了一个执行任意 PHP 的口子。

改成后台选项:
- 主题选项新增「自定义代码」组,含自定义 CSS / 自定义 JS 两个输入框
- 内容存在数据库的 kratos 选项里,与主题文件解耦,升级、重装都不影响
- 分别在主题样式和主题脚本之后加载,优先级最高

为此新增了 code 字段类型,没有复用 textarea:textarea 的 sanitizer
走 wp_kses(),CSS 的子选择器 > 和 JS 的 < 会被当成 HTML 标签剥掉,
样式会静默失效。code 类型只去掉 PHP 标签、打断 </style> 和 </script>,
其余原样保留。渲染时也不走公共的 stripslashes —— options.php 已经
wp_unslash 过,再剥一层会把 CSS 的 \f101、JS 的 \n 啃坏。

自定义 CSS 的默认值就是原来 custom.css 的内容,逐字节一致(354 字节),
所以升级瞬间站点外观没有任何变化。清空输入框保存即可彻底去掉。

验证:真实 WP 7.1.3 / PHP 8.5.12 环境 20 条断言全过(含 DB 往返、
子选择器 / 反斜杠保真、危险内容过滤),生产选项指纹前后一致;
反向注入「默认值改空」能立刻被抓到。
This commit is contained in:
2026-10-23 10:35:56 +08:00
parent 8a55457210
commit eaabc7717f
11 changed files with 180 additions and 54 deletions
@@ -141,6 +141,36 @@ class Options_Framework_Interface
$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']) . '">';
@@ -55,6 +55,30 @@ function of_sanitize_textarea( $input ) {
}
add_filter( 'of_sanitize_textarea', 'of_sanitize_textarea' );
/**
* Sanitization for raw code fields (theme's own and user's CSS / JS).
*
* 为什么不能复用 textarea:textarea 走 wp_kses(),CSS 里的子选择器
* `>`、`+`、`~` 以及 JS 里的比较运算符 `<`、`>` 都会被当成 HTML 标签
* 剥掉。例:`a > b { }` 存进库会变成 `a b { }`,样式静默失效。
* 所以这里只做三件必要的事:
* 1. 去掉 PHP 标签,避免这段文本将来被 eval / include 时执行代码;
* 2. 把 `</style>`、`</script>` 打断,避免提前闭合输出标签;
* 3. 其余内容原样返回(含反斜杠转义,如 CSS 的 `\f101`)。
*
* @param $input string
* @return $output sanitized string
*/
function of_sanitize_code( $input ) {
if ( ! is_string( $input ) ) {
return '';
}
$output = str_replace( array( '<?', '?>' ), '', $input );
$output = preg_replace( '#</\s*(style|script)#i', '<\\/$1', $output );
return $output;
}
add_filter( 'of_sanitize_code', 'of_sanitize_code' );
/**
* Sanitization for checkbox input
*