refactor(color): unify color sanitizing and picking behind shared components

- Add AKSRT\Core\Color::sanitize($value, $allow_alpha) as the single PHP
  color sanitizer; replace sanitize_hex_color calls and per-class regexes
  (FloatButtons, FieldSchema, ThemeSearch, icon cards, TicketStatusService)
- SchemaProvider / ControllerBase sanitize `color` fields as colors;
  alpha kept only when the schema sets showAlpha
- Split SchemaProvider OAuth validation and option matching into sibling
  traits to stay under the 1000-line limit
- Add AksColorPicker.vue + utils/color.js mirroring the PHP rules; AksField,
  FloatBtnCommonFields and TicketStatusPanel use it
- Widget background color input now uses wpColorPicker
- Remove dead AksCompositeField dimensions/spacing/background branches and
  the unused Gutenberg ColorPicker var
- Add color-unification contract test; update harness requires and tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-28 20:47:41 +08:00
1 parent 74021d26bb
commit 57ff405691
32 files changed
+522 -456

No files matched your search

+6
View File
@@ -108,6 +108,12 @@ abstract class ControllerBase {
$ids = is_array($val) ? array_filter(array_map('absint', $val)) : [];
$out[$key] = array_values(array_unique($ids));
break;
case 'color':
$out[$key] = \AKSRT\Core\Color::sanitize($val);
break;
case 'color_alpha':
$out[$key] = \AKSRT\Core\Color::sanitize($val, true);
break;
default:
$out[$key] = sanitize_text_field($val);
}
+2 -2
View File
@@ -365,8 +365,8 @@ class GlobalController extends ControllerBase {
'icon_size' => max(16, min(32, (int) ($params['icon_size'] ?? 22))),
'text_size' => max(8, min(14, (int) ($params['text_size'] ?? 10))),
'show_text' => !empty($params['show_text']),
'active_color' => sanitize_hex_color($params['active_color'] ?? '') ?: '#f04494',
'bg_color' => sanitize_hex_color($params['bg_color'] ?? '') ?: '',
'active_color' => \AKSRT\Core\Color::sanitize($params['active_color'] ?? '') ?: '#f04494',
'bg_color' => \AKSRT\Core\Color::sanitize($params['bg_color'] ?? '') ?: '',
'border' => !empty($params['border']),
'shadow' => !empty($params['shadow']),
'animation' => !empty($params['animation']),
+1 -1
View File
@@ -122,7 +122,7 @@ final class PageConfigMetaBox {
$config = [
'header_slider_show' => !empty($raw['header_slider_show']),
'header_slider_show_type' => self::enum((string) ($raw['header_slider_show_type'] ?? ''), ['', 'only_pc', 'only_sm']),
'header_slider_nav_color' => sanitize_hex_color((string) ($raw['header_slider_nav_color'] ?? '')) ?: '#ffffff',
'header_slider_nav_color' => \AKSRT\Core\Color::sanitize((string) ($raw['header_slider_nav_color'] ?? '')) ?: '#ffffff',
'header_slider' => self::sanitize_slides(self::decode_json($raw['header_slider'] ?? '')),
'header_slider_option' => self::sanitize_slider_options(self::decode_json($raw['header_slider_option'] ?? '')),
'header_slider_search_s' => self::sanitize_devices($raw['header_slider_search_s'] ?? []),
+20 -190
View File
@@ -9,6 +9,9 @@ defined('ABSPATH') || exit;
class SchemaProvider {
use SchemaProviderOAuthValidation;
use SchemaProviderOptionMatch;
private static $instance = null;
/**
@@ -304,132 +307,6 @@ class SchemaProvider {
return ['valid' => empty($errors), 'errors' => $errors];
}
private function validate_oauth_settings(array $settings, array $stored = []): array {
if (empty($settings['oauth_s'])) {
return [];
}
$errors = [];
$mode = sanitize_key((string) ($settings['oauth_proxy_mode'] ?? 'close'));
$secret_configured = $this->oauth_secret_is_configured($settings['oauth_proxy_secret'] ?? '', $stored['oauth_proxy_secret'] ?? '');
if ($mode === 'client') {
$url = trim((string) ($settings['oauth_proxy_server_url'] ?? ''));
if (!$this->is_secure_oauth_url($url)) {
$errors[] = __('客户端代理模式需要有效的 HTTPS 服务端地址', 'aksrt');
}
if (!$secret_configured) {
$errors[] = __('客户端代理模式需要代理 HMAC 密钥', 'aksrt');
}
if (empty($settings['oauth_proxy_client_providers']) || !is_array($settings['oauth_proxy_client_providers'])) {
$errors[] = __('客户端代理模式至少需要启用一个登录平台', 'aksrt');
}
return $errors;
}
if ($mode === 'server') {
if (!$secret_configured) {
$errors[] = __('服务端代理模式需要代理 HMAC 密钥', 'aksrt');
}
$selected = is_array($settings['oauth_proxy_server_providers'] ?? null) ? $settings['oauth_proxy_server_providers'] : [];
if ($selected === []) {
$errors[] = __('服务端代理模式至少需要允许一个登录平台', 'aksrt');
}
$provider_fields = [
'github' => ['GitHub', 'github_appid', 'github_appkey'],
'gitee' => ['Gitee', 'gitee_appid', 'gitee_appkey'],
'qq' => ['QQ', 'qq_appid', 'qq_appkey'],
'google' => ['Google', 'google_client_id', 'google_client_secret'],
'weibo' => ['微博', 'weibo_appid', 'weibo_appkey'],
'weixin' => ['微信开放平台', 'weixin_appid', 'weixin_appsecret'],
];
foreach ($selected as $provider) {
if (!isset($provider_fields[$provider])) {
continue;
}
[$label, $id_key, $secret_key] = $provider_fields[$provider];
if (trim((string) ($settings[$id_key] ?? '')) === '' || !$this->oauth_secret_is_configured($settings[$secret_key] ?? '', $stored[$secret_key] ?? '')) {
$errors[] = sprintf(__('代理服务端的 %s 平台缺少完整凭据', 'aksrt'), $label);
}
}
return $errors;
}
$providers = [
'github' => ['GitHub', 'github_appid', 'github_appkey'],
'gitee' => ['Gitee', 'gitee_appid', 'gitee_appkey'],
'qq' => ['QQ', 'qq_appid', 'qq_appkey'],
'google' => ['Google', 'google_client_id', 'google_client_secret'],
'weibo' => ['微博', 'weibo_appid', 'weibo_appkey'],
'weixin' => ['微信开放平台', 'weixin_appid', 'weixin_appsecret'],
];
$enabled = 0;
foreach ($providers as $provider => [$label, $id_key, $secret_key]) {
if (empty($settings['oauth_' . $provider . '_s'])) {
continue;
}
$enabled++;
if (trim((string) ($settings[$id_key] ?? '')) === '' || !$this->oauth_secret_is_configured($settings[$secret_key] ?? '', $stored[$secret_key] ?? '')) {
$errors[] = sprintf(__('%s 登录已启用,请填写完整凭据', 'aksrt'), $label);
}
}
if (!empty($settings['oauth_weixingzh_s'])) {
$enabled++;
$gzh_type = sanitize_key((string) ($settings['weixingzh_gzh_type'] ?? 'service'));
$base_ready = $this->oauth_secret_is_configured($settings['weixingzh_token'] ?? '', $stored['weixingzh_token'] ?? '')
&& trim((string) ($settings['weixingzh_appid'] ?? '')) !== '';
$mode_ready = $gzh_type === 'subscribe'
? trim((string) ($settings['weixingzh_code_qrcode'] ?? '')) !== ''
&& trim((string) ($settings['weixingzh_code_keyword'] ?? '')) !== ''
: $this->oauth_secret_is_configured($settings['weixingzh_appsecret'] ?? '', $stored['weixingzh_appsecret'] ?? '');
if (!$base_ready || !$mode_ready) {
$errors[] = $gzh_type === 'subscribe'
? __('微信公众号订阅号登录已启用,请填写 Token、AppID、二维码和回复关键词', 'aksrt')
: __('微信公众号服务号登录已启用,请填写 Token、AppID 和 AppSecret', 'aksrt');
}
}
if (!empty($settings['clogin_s'])) {
$enabled++;
if (!$this->is_secure_oauth_url((string) ($settings['clogin_url'] ?? ''))
|| trim((string) ($settings['clogin_id'] ?? '')) === ''
|| !$this->oauth_secret_is_configured($settings['clogin_key'] ?? '', $stored['clogin_key'] ?? '')
|| empty($settings['clogin_oauth_s'])
) {
$errors[] = __('彩虹聚合登录已启用,请填写 HTTPS 接口地址、AppID、AppKey 并选择平台', 'aksrt');
}
}
if ($enabled === 0) {
$errors[] = __('启用第三方登录后,至少需要启用一个本地登录提供商', 'aksrt');
}
return $errors;
}
private function oauth_secret_is_configured($value, $stored_value = ''): bool {
$value = trim((string) $value);
if ($value === '***') {
return trim((string) $stored_value) !== '';
}
return $value !== '';
}
private function is_secure_oauth_url(string $url): bool {
$url = trim($url);
if ($url === '' || filter_var($url, FILTER_VALIDATE_URL) === false) {
return false;
}
$parts = wp_parse_url($url);
return is_array($parts)
&& strtolower((string) ($parts['scheme'] ?? '')) === 'https'
&& !empty($parts['host'])
&& empty($parts['user'])
&& empty($parts['pass'])
&& empty($parts['fragment']);
}
public function read_settings(string $domain, string $section = ''): array {
if ($section !== '') {
$schema = $this->get_section_schema($domain, $section);
@@ -710,7 +587,9 @@ class SchemaProvider {
private function collect_allowed_fields(array $schema): array {
$allowed = [];
foreach ($this->get_all_fields($schema) as $field) {
$allowed[$field['key']] = $field['sanitize_type'] ?? $this->infer_sanitize_type($field['type'] ?? 'input');
$type = (string) ($field['type'] ?? 'input');
$allowed[$field['key']] = $field['sanitize_type']
?? ($type === 'color' ? $this->color_sanitize_type($field) : $this->infer_sanitize_type($type));
}
return $allowed;
}
@@ -840,73 +719,15 @@ class SchemaProvider {
return function_exists('aksrt_sanitize_icon_value')
? aksrt_sanitize_icon_value((string) $value)
: sanitize_text_field((string) $value);
case 'color':
return \AKSRT\Core\Color::sanitize($value);
case 'color_alpha':
return \AKSRT\Core\Color::sanitize($value, true);
default:
return sanitize_text_field((string) $value);
}
}
/**
* 对大小写不敏感字段(case_insensitive),把值归一化为选项中的规范写法;
* 匹配不到返回 null。
*
* @param mixed $value
* @param array<int,mixed> $options
*/
private function normalize_case_insensitive_option($value, array $options): ?string {
if (!is_string($value) || $value === '') {
return null;
}
$lower = strtolower(trim($value));
foreach ($options as $opt) {
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
if (is_string($opt_value) && strtolower($opt_value) === $lower) {
return $opt_value;
}
}
return null;
}
/**
* Resolve a submitted enum value to the value declared by its option.
*
* Payloads reach the sanitizer as strings (or numbers), so a strict match
* against the schema would reject a valid choice whenever the option value
* is not itself a string -- e.g. 积分兑换明细.level declares 1 / 2 while the
* SPA submits them as numbers and sanitize_text_field() then turns them into
* "1" / "2". Returning the declared value keeps the stored type aligned with
* the schema, which the radio/select controls compare with === when they
* re-render a saved setting.
*
* Returns null when nothing matches, so validation still reports the
* invalid value instead of silently rewriting the operator's input.
*
* @param mixed $value
* @param array<int,mixed> $options
* @return mixed
*/
private function match_option_value($value, array $options) {
foreach ($options as $opt) {
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
if ($opt_value === $value) {
return $opt_value;
}
}
if (is_bool($value) || !is_scalar($value)) {
return null;
}
$needle = (string) $value;
foreach ($options as $opt) {
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
if (is_bool($opt_value) || !is_scalar($opt_value)) {
continue;
}
if ((string) $opt_value === $needle) {
return $opt_value;
}
}
return null;
}
private function sanitize_field_value($value, array $field) {
if (!empty($field['captcha_provider'])) {
return function_exists('aksrt_captcha_sanitize_provider_config')
@@ -933,6 +754,10 @@ class SchemaProvider {
: $this->sanitize_value($value, 'icon');
}
if ($type === 'color') {
return $this->sanitize_value($value, $this->color_sanitize_type($field));
}
if (in_array($type, ['select', 'radio'], true) && !empty($field['case_insensitive']) && !empty($field['options'])) {
// 旧自由文本值(如小写货币代码)归一化为选项规范写法。
$normalized = $this->normalize_case_insensitive_option($value, (array) $field['options']);
@@ -1146,7 +971,7 @@ class SchemaProvider {
'json' => 'array',
// 自定义面板(验证码服务商等)按对象存储
'custom' => 'array',
'color' => 'string',
'color' => 'color',
'icon' => 'icon',
'media' => 'url',
'upload' => 'url',
@@ -1156,4 +981,9 @@ class SchemaProvider {
return $map[$field_type] ?? 'string';
}
/** Color fields keep alpha only when the schema opts in via showAlpha. */
private function color_sanitize_type(array $field): string {
return !empty($field['showAlpha']) ? 'color_alpha' : 'color';
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
/**
* SchemaProvider OAuth 设置校验(代理模式 / 各平台凭据 / HTTPS 地址)。
*
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait SchemaProviderOAuthValidation {
private function validate_oauth_settings(array $settings, array $stored = []): array {
if (empty($settings['oauth_s'])) {
return [];
}
$errors = [];
$mode = sanitize_key((string) ($settings['oauth_proxy_mode'] ?? 'close'));
$secret_configured = $this->oauth_secret_is_configured($settings['oauth_proxy_secret'] ?? '', $stored['oauth_proxy_secret'] ?? '');
if ($mode === 'client') {
$url = trim((string) ($settings['oauth_proxy_server_url'] ?? ''));
if (!$this->is_secure_oauth_url($url)) {
$errors[] = __('客户端代理模式需要有效的 HTTPS 服务端地址', 'aksrt');
}
if (!$secret_configured) {
$errors[] = __('客户端代理模式需要代理 HMAC 密钥', 'aksrt');
}
if (empty($settings['oauth_proxy_client_providers']) || !is_array($settings['oauth_proxy_client_providers'])) {
$errors[] = __('客户端代理模式至少需要启用一个登录平台', 'aksrt');
}
return $errors;
}
if ($mode === 'server') {
if (!$secret_configured) {
$errors[] = __('服务端代理模式需要代理 HMAC 密钥', 'aksrt');
}
$selected = is_array($settings['oauth_proxy_server_providers'] ?? null) ? $settings['oauth_proxy_server_providers'] : [];
if ($selected === []) {
$errors[] = __('服务端代理模式至少需要允许一个登录平台', 'aksrt');
}
$provider_fields = [
'github' => ['GitHub', 'github_appid', 'github_appkey'],
'gitee' => ['Gitee', 'gitee_appid', 'gitee_appkey'],
'qq' => ['QQ', 'qq_appid', 'qq_appkey'],
'google' => ['Google', 'google_client_id', 'google_client_secret'],
'weibo' => ['微博', 'weibo_appid', 'weibo_appkey'],
'weixin' => ['微信开放平台', 'weixin_appid', 'weixin_appsecret'],
];
foreach ($selected as $provider) {
if (!isset($provider_fields[$provider])) {
continue;
}
[$label, $id_key, $secret_key] = $provider_fields[$provider];
if (trim((string) ($settings[$id_key] ?? '')) === '' || !$this->oauth_secret_is_configured($settings[$secret_key] ?? '', $stored[$secret_key] ?? '')) {
$errors[] = sprintf(__('代理服务端的 %s 平台缺少完整凭据', 'aksrt'), $label);
}
}
return $errors;
}
$providers = [
'github' => ['GitHub', 'github_appid', 'github_appkey'],
'gitee' => ['Gitee', 'gitee_appid', 'gitee_appkey'],
'qq' => ['QQ', 'qq_appid', 'qq_appkey'],
'google' => ['Google', 'google_client_id', 'google_client_secret'],
'weibo' => ['微博', 'weibo_appid', 'weibo_appkey'],
'weixin' => ['微信开放平台', 'weixin_appid', 'weixin_appsecret'],
];
$enabled = 0;
foreach ($providers as $provider => [$label, $id_key, $secret_key]) {
if (empty($settings['oauth_' . $provider . '_s'])) {
continue;
}
$enabled++;
if (trim((string) ($settings[$id_key] ?? '')) === '' || !$this->oauth_secret_is_configured($settings[$secret_key] ?? '', $stored[$secret_key] ?? '')) {
$errors[] = sprintf(__('%s 登录已启用,请填写完整凭据', 'aksrt'), $label);
}
}
if (!empty($settings['oauth_weixingzh_s'])) {
$enabled++;
$gzh_type = sanitize_key((string) ($settings['weixingzh_gzh_type'] ?? 'service'));
$base_ready = $this->oauth_secret_is_configured($settings['weixingzh_token'] ?? '', $stored['weixingzh_token'] ?? '')
&& trim((string) ($settings['weixingzh_appid'] ?? '')) !== '';
$mode_ready = $gzh_type === 'subscribe'
? trim((string) ($settings['weixingzh_code_qrcode'] ?? '')) !== ''
&& trim((string) ($settings['weixingzh_code_keyword'] ?? '')) !== ''
: $this->oauth_secret_is_configured($settings['weixingzh_appsecret'] ?? '', $stored['weixingzh_appsecret'] ?? '');
if (!$base_ready || !$mode_ready) {
$errors[] = $gzh_type === 'subscribe'
? __('微信公众号订阅号登录已启用,请填写 Token、AppID、二维码和回复关键词', 'aksrt')
: __('微信公众号服务号登录已启用,请填写 Token、AppID 和 AppSecret', 'aksrt');
}
}
if (!empty($settings['clogin_s'])) {
$enabled++;
if (!$this->is_secure_oauth_url((string) ($settings['clogin_url'] ?? ''))
|| trim((string) ($settings['clogin_id'] ?? '')) === ''
|| !$this->oauth_secret_is_configured($settings['clogin_key'] ?? '', $stored['clogin_key'] ?? '')
|| empty($settings['clogin_oauth_s'])
) {
$errors[] = __('彩虹聚合登录已启用,请填写 HTTPS 接口地址、AppID、AppKey 并选择平台', 'aksrt');
}
}
if ($enabled === 0) {
$errors[] = __('启用第三方登录后,至少需要启用一个本地登录提供商', 'aksrt');
}
return $errors;
}
private function oauth_secret_is_configured($value, $stored_value = ''): bool {
$value = trim((string) $value);
if ($value === '***') {
return trim((string) $stored_value) !== '';
}
return $value !== '';
}
private function is_secure_oauth_url(string $url): bool {
$url = trim($url);
if ($url === '' || filter_var($url, FILTER_VALIDATE_URL) === false) {
return false;
}
$parts = wp_parse_url($url);
return is_array($parts)
&& strtolower((string) ($parts['scheme'] ?? '')) === 'https'
&& !empty($parts['host'])
&& empty($parts['user'])
&& empty($parts['pass'])
&& empty($parts['fragment']);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
/**
* SchemaProvider 枚举字段取值归一化(大小写不敏感匹配 / 选项声明值回写)。
*
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait SchemaProviderOptionMatch {
/**
* 对大小写不敏感字段(case_insensitive),把值归一化为选项中的规范写法;
* 匹配不到返回 null。
*
* @param mixed $value
* @param array<int,mixed> $options
*/
private function normalize_case_insensitive_option($value, array $options): ?string {
if (!is_string($value) || $value === '') {
return null;
}
$lower = strtolower(trim($value));
foreach ($options as $opt) {
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
if (is_string($opt_value) && strtolower($opt_value) === $lower) {
return $opt_value;
}
}
return null;
}
/**
* Resolve a submitted enum value to the value declared by its option.
*
* Payloads reach the sanitizer as strings (or numbers), so a strict match
* against the schema would reject a valid choice whenever the option value
* is not itself a string -- e.g. 积分兑换明细.level declares 1 / 2 while the
* SPA submits them as numbers and sanitize_text_field() then turns them into
* "1" / "2". Returning the declared value keeps the stored type aligned with
* the schema, which the radio/select controls compare with === when they
* re-render a saved setting.
*
* Returns null when nothing matches, so validation still reports the
* invalid value instead of silently rewriting the operator's input.
*
* @param mixed $value
* @param array<int,mixed> $options
* @return mixed
*/
private function match_option_value($value, array $options) {
foreach ($options as $opt) {
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
if ($opt_value === $value) {
return $opt_value;
}
}
if (is_bool($value) || !is_scalar($value)) {
return null;
}
$needle = (string) $value;
foreach ($options as $opt) {
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
if (is_bool($opt_value) || !is_scalar($opt_value)) {
continue;
}
if ((string) $opt_value === $needle) {
return $opt_value;
}
}
return null;
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
/**
* 全站唯一的颜色值净化器(SPA 设置、小工具、悬浮按钮、工单状态、前台渲染共用)。
*
* 前端对应实现:assets/admin/src/utils/color.js(normalizeColor),两端规则保持一致。
*
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
final class Color {
private const HEX = '/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i';
private const HEX_ALPHA = '/^#?([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i';
private const RGB = '/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(0(?:\.\d+)?|1(?:\.0+)?|\.\d+)\s*)?\)$/i';
/**
* 返回规范化颜色;非法值返回空串。
*
* - 默认只接受 #rgb / #rrggbb(可省略 #),输出小写并补 #。
* - $allow_alpha 时额外接受 #rgba / #rrggbbaa 与 rgb()/rgba(),
* rgb 形态统一输出为 `rgb(r, g, b)` / `rgba(r, g, b, a)`。
*
* @param mixed $value
*/
public static function sanitize($value, bool $allow_alpha = false): string {
if (!is_string($value)) {
return '';
}
$value = trim($value);
if ($value === '') {
return '';
}
if (preg_match($allow_alpha ? self::HEX_ALPHA : self::HEX, $value, $m) === 1) {
return '#' . strtolower($m[1]);
}
if (!$allow_alpha || preg_match(self::RGB, $value, $m) !== 1) {
return '';
}
$channels = [(int) $m[1], (int) $m[2], (int) $m[3]];
foreach ($channels as $channel) {
if ($channel > 255) {
return '';
}
}
if (!isset($m[4]) || $m[4] === '') {
return 'rgb(' . implode(', ', $channels) . ')';
}
$alpha = rtrim(rtrim(number_format((float) $m[4], 3, '.', ''), '0'), '.');
return 'rgba(' . implode(', ', $channels) . ', ' . ($alpha === '' ? '0' : $alpha) . ')';
}
}
+1 -1
View File
@@ -77,7 +77,7 @@ trait DisplayPageGroups {
'slides' => is_array($s['header_slider'] ?? null) ? $s['header_slider'] : self::header_slider_default_slides(),
'show_on_pages' => $show_on,
'show_type' => $show_type,
'nav_color' => sanitize_hex_color((string) ($s['header_slider_nav_color'] ?? '')) ?: '',
'nav_color' => \AKSRT\Core\Color::sanitize((string) ($s['header_slider_nav_color'] ?? '')) ?: '',
'slider_config' => $slider_option,
'search_show' => self::sanitize_device_visibility($s['header_slider_search_s'] ?? ['pc_s', 'm_s']),
'search_config' => $search_option,
+2 -2
View File
@@ -18,9 +18,9 @@ trait FacadeBulk {
return [
'custom' => !empty($s['header_theme_custom']),
'bg_custom' => sanitize_hex_color((string) ($s['header_theme_bg_custom'] ?? '')) ?: '',
'bg_custom' => \AKSRT\Core\Color::sanitize((string) ($s['header_theme_bg_custom'] ?? '')) ?: '',
'bg' => self::normalize_palette_hex((string) ($s['header_theme_bg'] ?? '')),
'color_custom' => sanitize_hex_color((string) ($s['header_theme_color_custom'] ?? '')) ?: '',
'color_custom' => \AKSRT\Core\Color::sanitize((string) ($s['header_theme_color_custom'] ?? '')) ?: '',
];
}
+2 -19
View File
@@ -218,25 +218,8 @@ class FloatButtons {
private static function sanitize_color_group($group): array {
$group = is_array($group) ? $group : [];
return [
'color' => self::sanitize_color((string) ($group['color'] ?? '')),
'bg' => self::sanitize_color((string) ($group['bg'] ?? '')),
'color' => \AKSRT\Core\Color::sanitize($group['color'] ?? '', true),
'bg' => \AKSRT\Core\Color::sanitize($group['bg'] ?? '', true),
];
}
/**
* 允许 #hex(3/4/6/8 位)与 rgb()/rgba()。
*/
public static function sanitize_color(string $value): string {
$value = trim($value);
if ($value === '') {
return '';
}
if (preg_match('/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $value)) {
return $value;
}
if (preg_match('/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(?:,\s*(?:0|1|0?\.\d+)\s*)?\)$/', $value)) {
return $value;
}
return '';
}
}
+2 -2
View File
@@ -61,9 +61,9 @@ class HomeCms {
$s = self::get_footer_settings();
return [
'custom' => !empty($s['footer_theme_custom']),
'bg_custom' => sanitize_hex_color((string) ($s['footer_theme_bg_custom'] ?? '')) ?: '',
'bg_custom' => \AKSRT\Core\Color::sanitize((string) ($s['footer_theme_bg_custom'] ?? '')) ?: '',
'bg' => self::normalize_palette_hex((string) ($s['footer_theme_bg'] ?? '')),
'color_custom' => sanitize_hex_color((string) ($s['footer_theme_color_custom'] ?? '')) ?: '',
'color_custom' => \AKSRT\Core\Color::sanitize((string) ($s['footer_theme_color_custom'] ?? '')) ?: '',
];
}
+4 -8
View File
@@ -388,12 +388,10 @@ class ThemeSearch {
return '';
}
private const HEX_PATTERN = '/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/';
/** 全局主题色:theme_skin_custom 优先,清空时使用 theme_skin 预置色(对齐参考主题 zib_head_css)。 */
public static function theme_color(): string {
$custom = trim((string) Settings::get_value('theme_skin_custom', ''));
if (preg_match(self::HEX_PATTERN, $custom)) {
$custom = \AKSRT\Core\Color::sanitize(Settings::get_value('theme_skin_custom', ''));
if ($custom !== '') {
return $custom;
}
$skin = ltrim(trim((string) Settings::get_value('theme_skin', 'f04494')), '#');
@@ -402,8 +400,7 @@ class ThemeSearch {
/** 夜间模式主题色;未设置返回空串,夜间沿用全局主题色。 */
public static function dark_theme_color(): string {
$color = trim((string) Settings::get_value('dark_theme_skin_custom', ''));
return preg_match(self::HEX_PATTERN, $color) ? $color : '';
return \AKSRT\Core\Color::sanitize(Settings::get_value('dark_theme_skin_custom', ''));
}
public static function get_theme_mode_button_positions(): array {
@@ -429,8 +426,7 @@ class ThemeSearch {
if (!is_array($bg)) {
return '';
}
$color = trim((string) ($bg['color'] ?? ''));
$color = preg_match(self::HEX_PATTERN, $color) ? $color : '';
$color = \AKSRT\Core\Color::sanitize($bg['color'] ?? '');
$image = esc_url_raw(trim((string) ($bg['image'] ?? '')));
if ($color === '' && $image === '') {
return '';
+2 -5
View File
@@ -134,11 +134,8 @@ final class TicketStatusService {
/** 颜色只接受 #rgb / #rrggbb,其余回落默认灰。 */
public static function sanitize_color(string $color): string {
$color = strtolower(trim($color));
if (preg_match('/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/', $color) !== 1) {
return '#6b7280';
}
return $color;
$color = \AKSRT\Core\Color::sanitize($color);
return $color !== '' ? $color : '#6b7280';
}
/**
+2 -14
View File
@@ -79,7 +79,7 @@ final class FieldSchema {
case 'upload':
return esc_url_raw((string) $value);
case 'color':
return self::color((string) $value);
return \AKSRT\Core\Color::sanitize($value, true);
case 'icon':
return function_exists('aksrt_sanitize_icon_value')
? aksrt_sanitize_icon_value((string) $value)
@@ -192,7 +192,7 @@ final class FieldSchema {
private static function background($value): array {
$value = is_array($value) ? $value : [];
return [
'color' => self::color((string) ($value['color'] ?? '')),
'color' => \AKSRT\Core\Color::sanitize($value['color'] ?? '', true),
'image' => esc_url_raw((string) ($value['image'] ?? '')),
'position' => self::allow((string) ($value['position'] ?? 'center center'), ['center center', 'top center', 'bottom center', 'center left', 'center right'], 'center center'),
'repeat' => self::allow((string) ($value['repeat'] ?? 'no-repeat'), ['no-repeat', 'repeat', 'repeat-x', 'repeat-y'], 'no-repeat'),
@@ -213,18 +213,6 @@ final class FieldSchema {
return self::normalize($children, is_array($value) ? $value : []);
}
private static function color(string $value): string {
$value = trim($value);
if ($value === '') {
return '';
}
$hex = sanitize_hex_color($value);
if ($hex) {
return $hex;
}
return preg_match('/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)$/', $value) ? $value : '';
}
private static function allow(string $value, array $allowed, string $fallback): string {
return in_array($value, $allowed, true) ? $value : $fallback;
}
+3 -3
View File
@@ -221,7 +221,7 @@ final class FormRenderer {
private static function background(SchemaWidget $widget, string $key, array $value): void {
echo '<div class="aksrt-widget-background">';
self::plain_input($widget, $key . '[color]', (string) ($value['color'] ?? ''), 'text', '背景颜色');
self::plain_input($widget, $key . '[color]', (string) ($value['color'] ?? ''), 'text', '背景颜色', 'widefat aksrt-widget-color');
self::plain_input($widget, $key . '[image]', (string) ($value['image'] ?? ''), 'url', '背景图片 URL');
foreach ([
'position' => ['center center' => '居中', 'top center' => '顶部', 'bottom center' => '底部', 'center left' => '左侧', 'center right' => '右侧'],
@@ -273,9 +273,9 @@ final class FormRenderer {
echo '</div></details>';
}
private static function plain_input(SchemaWidget $widget, string $key, string $value, string $type, string $placeholder): void {
private static function plain_input(SchemaWidget $widget, string $key, string $value, string $type, string $placeholder, string $class = 'widefat'): void {
$name = $widget->get_field_name($key);
$id = $widget->get_field_id(str_replace(['[', ']'], ['-', ''], $key));
echo '<input class="widefat" type="' . esc_attr($type) . '" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" placeholder="' . esc_attr($placeholder) . '">';
echo '<input class="' . esc_attr($class) . '" type="' . esc_attr($type) . '" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" placeholder="' . esc_attr($placeholder) . '">';
}
}