// +----------------------------------------------------------------------
namespace ywxapp\library;
/**
* 站点级 layui 主题变量层(整站换肤的唯一入口)。
*
* 与 SkinVariables(模板级、仅插件上下文生效)的定位区别:
* - SkinVariables:某套皮肤自己的变量(--primary / --secondary),
* 由 template.json 声明,用于该皮肤自己的视图与 CSS。
* - SiteTheme:整站 layui 组件的底色(--ywx-primary / --ywx-accent 等),
* 对所有页面生效(后台 / 会员 / 前台 / 插件前台),与皮肤层互不干扰。
*
* 变量名对应见 config/theme.php;CSS 消费方是 public/static/theme/layui-theme.css
* (由 layui.css 自动提取生成:主色硬编码已改写为 var(--ywx-*))。
*
* 安全:变量值会被拼进 ';
}
/**
* 保存后台配置的变量值到 config/theme.php(保留注释头),并同步内存配置。
*
* @param array $vars ['primary' => '#5b8cff', ...];非法值直接丢弃
* @return int 实际写入的变量数
*/
public static function save(array $vars): int
{
$clean = [];
foreach (self::DEFAULTS as $key => $default) {
if (! isset($vars[$key])) {
continue;
}
$val = trim((string) $vars[$key]);
if ($val === '') {
// 显式留空 = 恢复该变量的默认值(raw()/values() 会自动回落)
$clean[$key] = '';
continue;
}
if (! self::validate($key, $val)) {
continue; // 非法值直接丢弃,防止 CSS 注入
}
$clean[$key] = $val;
}
$current = config('theme', []);
$current['variables'] = array_merge(
is_array(config('theme.variables', [])) ? config('theme.variables', []) : [],
$clean
);
$current['enable'] = $current['enable'] ?? true;
self::writeConfig($current);
return count($clean);
}
/**
* 值白名单校验:只允许「CSS 安全」取值,禁止 ; { } < > @ url( 等逃逸字符。
*/
public static function validate(string $key, string $value): bool
{
$v = $value;
// 通用禁止:CSS 上下文逃逸字符
if (preg_match('/[;{}<>]/', $v)) {
return false;
}
if (stripos($v, 'url(') !== false || strpos($v, '@') !== false) {
return false;
}
// 控制字符(换行可注入新行导致解析异常)
if (preg_match('/[\x00-\x1F\x7F]/', $v)) {
return false;
}
switch ($key) {
case 'primary':
case 'accent':
// 颜色:hex / rgb() / rgba() / 颜色关键字
return (bool) preg_match('/^(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgba?\(\s*[\d.,%\s]+\)|[a-zA-Z]{3,20})$/', $v);
case 'radius':
return (bool) preg_match('/^[\d.]+(?:px|rem|em|%)?$/', $v);
case 'font':
return (bool) preg_match('/^[\w\s,\'"\-]+$/u', $v);
default:
return false;
}
}
/**
* 写回 config/theme.php(含注释头),同步内存配置使当前请求立即生效。
*/
protected static function writeConfig(array $data): void
{
$file = config_path() . 'theme.php';
$vars = is_array($data['variables'] ?? null) ? $data['variables'] : [];
$lines = [];
foreach (self::DEFAULTS as $key => $default) {
$val = isset($vars[$key]) ? (string) $vars[$key] : '';
$lines[] = " '" . $key . "' => '" . addslashes($val) . "',";
}
$linesStr = implode("\n", $lines);
$enable = ! empty($data['enable']) ? 'true' : 'false';
$content = << --ywx-primary 主色(按钮/表单聚焦/进度条/时间轴)
// | accent -> --ywx-accent 强调色(导航/分页当前页/上传/logo)
// | radius -> --ywx-radius 统一圆角
// | font -> --ywx-font 正文字体
// |
// | 改这里即可全站换肤,无需改动任何视图文件、也无需清理缓存。
// | 后台可在「模板/主题」界面写入本文件(见 SiteTheme::save())。
// +----------------------------------------------------------------------
return [
// 主题开关:关闭后不注入任何主题样式(用于排查样式冲突)
'enable' => {$enable},
// 站点主题变量(值为空表示使用 layui-theme.css 内的兜底默认值)
'variables' => [
{$linesStr}
],
];
PHP;
file_put_contents($file, $content);
// 刚写入的 PHP 配置可能被 OPcache 缓存 2 秒(revalidate_freq),显式失效保证即时换肤
if (function_exists('opcache_invalidate')) {
@opcache_invalidate($file, true);
}
\think\facade\Config::set(['enable' => ! empty($data['enable']), 'variables' => $vars], 'theme');
}
}