初始化
缺陷:主题配色需要优化整体的同风格
This commit is contained in:
186
app/Service/business/WxTemplateSchemaService.php
Normal file
186
app/Service/business/WxTemplateSchemaService.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\business;
|
||||
|
||||
use App\Service\common\UtilsService;
|
||||
|
||||
/**
|
||||
* 装修模板 JSON 的白名单校验
|
||||
*
|
||||
* 导入的 JSON 会被小程序当成 CSS 变量注入根节点,一份坏 JSON 能把线上小程序打成白屏,
|
||||
* 所以入库前必须过白名单:键名不在表里的丢弃,值必须是安全的短字符串。
|
||||
* 这里只允许「值」,不允许任何 CSS 语句片段(分号、url()、表达式一律拒绝)。
|
||||
*/
|
||||
class WxTemplateSchemaService
|
||||
{
|
||||
private static mixed $_instance;
|
||||
|
||||
/**
|
||||
* 允许的令牌键,分组 => 键名清单
|
||||
*/
|
||||
public const TOKEN_SCHEMA = [
|
||||
'color' => [
|
||||
'primary', 'primary_soft', 'primary_strong', 'accent', 'bg', 'bg_soft',
|
||||
'surface', 'surface_soft', 'text', 'text_soft', 'text_muted', 'border',
|
||||
'price', 'success', 'warning', 'danger', 'mask',
|
||||
],
|
||||
'font' => [
|
||||
'family', 'family_title', 'size_xs', 'size_sm', 'size_md', 'size_lg',
|
||||
'size_xl', 'size_title', 'weight_normal', 'weight_bold', 'line_height', 'letter_spacing',
|
||||
],
|
||||
'radius' => ['none', 'sm', 'md', 'lg', 'xl', 'pill'],
|
||||
'shadow' => ['none', 'sm', 'md', 'lg'],
|
||||
'space' => ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'page'],
|
||||
'motion' => ['fast', 'base', 'slow', 'easing', 'easing_in', 'easing_out'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 允许的布局键与可选值
|
||||
*/
|
||||
public const LAYOUT_SCHEMA = [
|
||||
'home' => [
|
||||
'hero' => ['banner', 'carousel', 'split', 'fullscreen'],
|
||||
'category' => ['grid', 'scroll', 'card', 'sidebar'],
|
||||
'product' => ['waterfall', 'list', 'grid', 'magazine'],
|
||||
],
|
||||
'product' => [
|
||||
'gallery' => ['swiper', 'stack', 'fullbleed'],
|
||||
'price' => ['inline', 'card', 'sticky'],
|
||||
'action' => ['fixed', 'inline'],
|
||||
],
|
||||
'list' => [
|
||||
'style' => ['card', 'table', 'timeline'],
|
||||
],
|
||||
'mine' => [
|
||||
'header' => ['gradient', 'image', 'plain'],
|
||||
'menu' => ['grid', 'list'],
|
||||
],
|
||||
'effect' => [
|
||||
'transition' => ['fade', 'slide', 'zoom', 'none'],
|
||||
'skeleton' => ['shimmer', 'pulse', 'none'],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 危险片段:出现即拒绝整个值
|
||||
*/
|
||||
private const FORBIDDEN = ['<', '>', ';', '{', '}', 'url(', 'expression', 'javascript:', 'import'];
|
||||
|
||||
public static function getInstance(): null|static
|
||||
{
|
||||
$name = get_called_class();
|
||||
if (!isset(self::$_instance[$name])) {
|
||||
self::$_instance[$name] = new static();
|
||||
}
|
||||
return self::$_instance[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗令牌:只保留白名单键,值必须安全
|
||||
*
|
||||
* @param mixed $tokens 数组或 JSON 字符串
|
||||
* @param bool $strict true 时遇到非法值直接报错(导入场景),false 时静默丢弃
|
||||
*/
|
||||
public function sanitizeTokens(mixed $tokens, bool $strict = false): array
|
||||
{
|
||||
$tokens = $this->toArray($tokens);
|
||||
$clean = [];
|
||||
foreach (self::TOKEN_SCHEMA as $group => $keys) {
|
||||
$source = $tokens[$group] ?? [];
|
||||
if (!is_array($source)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($keys as $key) {
|
||||
if (!array_key_exists($key, $source)) {
|
||||
continue;
|
||||
}
|
||||
$value = $source[$key];
|
||||
if (!$this->isSafeValue($value)) {
|
||||
if ($strict) {
|
||||
UtilsService::getInstance()->errorThrow("模板令牌 {$group}.{$key} 的值不合法");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$clean[$group][$key] = (string) $value;
|
||||
}
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗布局:值必须是枚举里的选项,非法值退回该项的第一个选项
|
||||
*/
|
||||
public function sanitizeLayout(mixed $layout, bool $strict = false): array
|
||||
{
|
||||
$layout = $this->toArray($layout);
|
||||
$clean = [];
|
||||
foreach (self::LAYOUT_SCHEMA as $page => $options) {
|
||||
$source = $layout[$page] ?? [];
|
||||
if (!is_array($source)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($options as $key => $allowed) {
|
||||
if (!array_key_exists($key, $source)) {
|
||||
continue;
|
||||
}
|
||||
$value = (string) $source[$key];
|
||||
if (!in_array($value, $allowed, true)) {
|
||||
if ($strict) {
|
||||
UtilsService::getInstance()->errorThrow("模板布局 {$page}.{$key} 只能是:" . implode('/', $allowed));
|
||||
}
|
||||
$value = $allowed[0];
|
||||
}
|
||||
$clean[$page][$key] = $value;
|
||||
}
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序侧要的扁平 CSS 变量表:--color-primary 这种
|
||||
*/
|
||||
public function toCssVariables(array $tokens): array
|
||||
{
|
||||
$vars = [];
|
||||
foreach ($tokens as $group => $items) {
|
||||
if (!is_array($items)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($items as $key => $value) {
|
||||
$vars['--' . str_replace('_', '-', $group . '-' . $key)] = $value;
|
||||
}
|
||||
}
|
||||
return $vars;
|
||||
}
|
||||
|
||||
private function isSafeValue(mixed $value): bool
|
||||
{
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return true;
|
||||
}
|
||||
if (!is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
$value = trim($value);
|
||||
if ($value === '' || mb_strlen($value) > 64) {
|
||||
return false;
|
||||
}
|
||||
foreach (self::FORBIDDEN as $needle) {
|
||||
if (stripos($value, $needle) !== false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function toArray(mixed $value): array
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$value = json_decode($value, true);
|
||||
if (!is_array($value)) {
|
||||
UtilsService::getInstance()->errorThrow('模板 JSON 解析失败');
|
||||
}
|
||||
}
|
||||
return is_array($value) ? $value : [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user