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:
1 parent
74021d26bb
commit
57ff405691
32 files changed
+522
-456
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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']),
|
||||
|
||||
@@ -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
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user