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

原来主题里有个 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
+20 -7
View File
@@ -86,7 +86,7 @@ theme-update/readme.html # 「详情」页,可省
**先改 `style.css` 里的 `Version`,再打包**,否则 WordPress 认为没有新版本,不会提示升级。
1. 改 `style.css` 的 `Version`(例如 3.2.0 → 3.3.0)
1. 改 `style.css` 的 `Version`(例如 3.2.2 → 3.3.0)
2. 打包:`zip -r kratos_yang.zip kratos_yang -x "*.DS_Store" "*/.git/*"`
3. 传 `kratos_yang.zip` 到 `theme-update/`
4. **最后**才改 `update.json` 里的 `version`
@@ -96,14 +96,27 @@ theme-update/readme.html # 「详情」页,可省
## ⚠️ 自动升级会覆盖主题目录
WordPress 升级主题的做法是**删掉整个 `kratos_yang/` 再解压新包**。
所以任何直接改在服务器主题目录里的东西都会丢,包括:
WordPress 升级主题的做法是**删掉整个 `kratos_yang/` 再解压新包**
(`wp-admin/includes/class-theme-upgrader.php` 里 `clear_destination => true`,
随后 `delete_old_theme()` 递归删除)。所以任何直接改在服务器主题目录里的东西都会丢,
包括手工打的补丁(例如曾经直接在 `footer.php` 里加的 Prism 脚本)。
- `custom/custom.css`、`custom/custom.js`(放本地自定义样式的常用位置)
- 手工打的补丁(例如曾经直接在 `footer.php` 里加的 Prism 脚本)
**自定义样式不再放主题目录。** 早期版本有一个 `custom/` 目录(`custom.css` /
`custom.js` / `custom.php`),是给用户随手加样式的入口,但它位于主题目录内,
每次升级都会被清空 —— 而且 `custom.php` 会被 `require`,等于允许在主题里执行任意
PHP 代码。该模块已在 3.2.2 移除,改为后台选项:
把自定义内容放主题目录外,或者在升级后重新部署。本仓库的 `custom/` 已经纳入版本管理,
修改请改仓库再发布新版本,不要在服务器上直接编辑。
> 后台 → 主题选项 → **自定义代码** → 自定义 CSS / 自定义 JS
这两段内容存在数据库的 `kratos` 选项里,**升级、重装、改主题目录名都不受影响**。
它们分别在主题样式表和主题脚本之后加载,优先级最高。
几个要点:
- 不需要写 `<style>` / `<script>` 标签,直接写 CSS / JS 内容
- 内容不会被当作 HTML 处理,`>`、`<`、`~`、反斜杠转义(如 `content: "\f101"`)都原样保留
- 想彻底清空,把框里内容删掉保存即可(空值会覆盖内置默认值)
- 点“恢复默认”会把「自定义 CSS」还原成主题自带的默认样式
## 换域名 / 退役旧域名
-36
View File
@@ -1,36 +0,0 @@
.article .content p{ text-indent:2em }
.k-main .details .article .content h2 {
margin: 16px -16px;
padding: 0 10px;
border-left: 5px solid #00a2ff;
background-color: rgba(243, 243, 243, 0.7);
font-size: 20px;
line-height: 41px;
}
.k-main .details .comments-area {
margin-top: 16px;
padding: 1.6px 16px 16px;
border-radius: 2px;
background-color: #fff;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.k-main .details .post-2 .comments-area {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
ul{
margin-left:2em;
}
ol{
margin-left:2em;
}
ol > li{
line-height: 1.8;
}
ul > li{
line-height: 1.8;
}
-2
View File
@@ -1,2 +0,0 @@
;
// 请在第三行开始编写代码
-2
View File
@@ -1,2 +0,0 @@
<?php
// 请在第三行开始编写代码
+3 -3
View File
@@ -11,6 +11,9 @@ define('THEME_VERSION', wp_get_theme()->get('Version'));
// 主题配置
require get_template_directory() . '/inc/options-framework/autoload.php';
// 自定义代码(CSS / JS,存选项表而非主题目录)
require get_template_directory() . '/inc/theme-custom-code.php';
// 更新配置
require get_template_directory() . '/inc/update-checker/autoload.php';
@@ -37,6 +40,3 @@ require get_template_directory() . '/inc/theme-dogecloud.php';
// SMTP 配置
require get_template_directory() . '/inc/theme-smtp.php';
// 自定义函数
require get_template_directory() . '/custom/custom.php';
@@ -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
*
+11 -2
View File
@@ -63,14 +63,23 @@ function theme_autoload()
wp_add_inline_style('kratos', $admin_bar_css);
}
}
wp_enqueue_style('custom', get_template_directory_uri() . '/custom/custom.css', array(), THEME_VERSION);
// 自定义样式:存在主题选项里(数据库),排在主题样式之后,优先级最高。
// 放选项而非主题目录,是为了让它在主题升级(升级会清空主题目录)后依然存在。
$custom_css = yang_custom_css();
if (trim($custom_css) !== '') {
wp_add_inline_style('kratos', $custom_css);
}
// js
wp_deregister_script('jquery');
wp_enqueue_script('jquery', ASSET_PATH . '/assets/js/jquery.min.js', array(), '3.4.1', false);
wp_enqueue_script('bootstrap-bundle', ASSET_PATH . '/assets/js/bootstrap.bundle.min.js', array(), '4.5.0', true);
wp_enqueue_script('layer', ASSET_PATH . '/assets/js/layer.min.js', array(), '3.1.1', true);
wp_enqueue_script('kratos', ASSET_PATH . '/assets/js/kratos.min.js', array(), THEME_VERSION, true);
wp_enqueue_script('custom', get_template_directory_uri() . '/custom/custom.js', array(), THEME_VERSION, true);
// 自定义脚本:同样存在主题选项里,挂在 kratos 之后(页脚输出)。
$custom_js = yang_custom_js();
if (trim($custom_js) !== '') {
wp_add_inline_script('kratos', $custom_js);
}
$data = array(
'site' => home_url(),
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* 自定义代码(CSS / JS)
*
* 这两段代码存在主题选项里(数据库 wp_options 的 kratos 项),不存主题目录。
* 原因:WordPress 升级/更新主题时会**清空整个主题目录**再解压
* (wp-admin/includes/class-theme-upgrader.php 里 clear_destination = true,
* 随后 delete_old_theme() 递归删除),写在主题目录里的任何自定义文件都会丢。
* 放进选项表就与主题文件解耦,升级、重装、换主题目录名都不影响。
*
* @author Seaton Jiang <seaton@vtrois.com>
* @license MIT License
*/
/**
* 默认自定义 CSS。
*
* 历史沿革:这些规则原本放在主题目录下的一个 custom 子目录里,是该主题早期的
* 「自定义样式」入口。改成配置项后,把它作为默认值,保证切换瞬间站点外观
* 完全不变;同时 `恢复默认` 也能拿回这套样式。
*
* 想彻底清掉:在后台把「自定义 CSS」清空保存即可(空值会覆盖这里的默认值)。
*/
function yang_default_custom_css()
{
return <<<'CSS'
.article .content p{ text-indent:2em }
.k-main .details .article .content h2 {
margin: 16px -16px;
padding: 0 10px;
border-left: 5px solid #00a2ff;
background-color: rgba(243, 243, 243, 0.7);
font-size: 20px;
line-height: 41px;
}
ul{
margin-left:2em;
}
ol{
margin-left:2em;
}
ol > li{
line-height: 1.8;
}
ul > li{
line-height: 1.8;
}
CSS;
}
/**
* 取自定义 CSS(未设置时回退到默认值)
*/
function yang_custom_css()
{
$css = kratos_option('g_custom_css', yang_default_custom_css());
return is_string($css) ? $css : '';
}
/**
* 取自定义 JS(未设置时为空)
*/
function yang_custom_js()
{
$js = kratos_option('g_custom_js', '');
return is_string($js) ? $js : '';
}
+22
View File
@@ -515,6 +515,28 @@ $options[] = array(
'type' => 'text',
);
$options[] = array(
'name' => __('自定义代码', 'kratos'),
'type' => 'heading',
);
$options[] = array(
'name' => __('自定义 CSS', 'kratos'),
'desc' => __('附加样式,在所有主题样式之后加载。保存后立即生效,随主题升级保留。', 'kratos'),
'id' => 'g_custom_css',
'std' => yang_default_custom_css(),
'type' => 'code',
'settings' => array('rows' => 14),
);
$options[] = array(
'name' => __('自定义 JS', 'kratos'),
'desc' => __('附加脚本,在页面底部加载,不需要写 &lt;script&gt; 标签。', 'kratos'),
'id' => 'g_custom_js',
'std' => '',
'type' => 'code',
'settings' => array('rows' => 14),
);
return $options;
}
+1 -1
View File
@@ -1,7 +1,7 @@
/*
Theme Name: Kratos_Yang
Text Domain: kratos_Yang
Version: 3.2.1
Version: 3.2.2
Requires PHP: 7.0
Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。
Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好