feat(admin): rebuild page display settings
This commit is contained in:
@@ -123,6 +123,26 @@ class SchemaProvider {
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a schema payload with the full nested field contract.
|
||||
*/
|
||||
public function sanitize_payload(string $domain, array $data, string $section = ''): array {
|
||||
$schema = $section !== '' ? $this->get_section_schema($domain, $section) : $this->get_schema($domain);
|
||||
if (!$schema) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$clean = [];
|
||||
foreach ($this->get_all_fields($schema) as $field) {
|
||||
$key = (string) $field['key'];
|
||||
if (array_key_exists($key, $data)) {
|
||||
$clean[$key] = $this->sanitize_field_value($data[$key], $field);
|
||||
}
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
public function validate(string $domain, array $data, string $section = ''): array {
|
||||
$schemas = [];
|
||||
|
||||
@@ -151,7 +171,7 @@ class SchemaProvider {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $field['type'] ?? 'input';
|
||||
$type = $field['type'] ?? 'input';
|
||||
if (($type === 'number' || $type === 'slider') && !is_numeric($value)) {
|
||||
$errors[] = sprintf('%s must be a number', $field['label'] ?? $key);
|
||||
continue;
|
||||
@@ -162,6 +182,25 @@ class SchemaProvider {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($type, ['fieldset', 'accordion', 'sortable', 'link'], true) && !is_array($value)) {
|
||||
$errors[] = sprintf('%s must be an object', $field['label'] ?? $key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type === 'group' && !is_array($value)) {
|
||||
$errors[] = sprintf('%s must be an array of items', $field['label'] ?? $key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($type, ['group', 'repeater'], true) && is_array($value)) {
|
||||
if (isset($field['min']) && count($value) < (int) $field['min']) {
|
||||
$errors[] = sprintf('%s must contain at least %s items', $field['label'] ?? $key, $field['min']);
|
||||
}
|
||||
if (isset($field['max']) && count($value) > (int) $field['max']) {
|
||||
$errors[] = sprintf('%s must contain at most %s items', $field['label'] ?? $key, $field['max']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === 'repeater' && !is_array($value)) {
|
||||
$errors[] = sprintf('%s must be an array of items', $field['label'] ?? $key);
|
||||
continue;
|
||||
@@ -175,7 +214,7 @@ class SchemaProvider {
|
||||
$errors[] = sprintf('%s must be at most %s', $field['label'] ?? $key, $field['max']);
|
||||
}
|
||||
|
||||
if (in_array($type, ['select', 'radio'], true) && !empty($field['options'])) {
|
||||
if (in_array($type, ['select', 'radio', 'palette', 'image_select'], true) && !empty($field['options'])) {
|
||||
$valid_values = array_column($field['options'], 'value');
|
||||
// Multi-select: accept array values when schema.multiple is set
|
||||
// (legacy / SPA may still use type=select multiple).
|
||||
@@ -204,6 +243,8 @@ class SchemaProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$errors = array_merge($errors, $this->validate_nested_field($field, $value, (string) ($field['label'] ?? $key)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,14 +367,16 @@ class SchemaProvider {
|
||||
$stored = [];
|
||||
}
|
||||
|
||||
$allowed = $this->collect_allowed_fields($schema);
|
||||
foreach ($allowed as $key => $type) {
|
||||
$fields = [];
|
||||
foreach ($this->get_all_fields($schema) as $field) {
|
||||
$fields[(string) $field['key']] = $field;
|
||||
}
|
||||
foreach ($fields as $key => $field) {
|
||||
if (!array_key_exists($key, $data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->sanitize_value($data[$key], $type);
|
||||
$stored[$key] = $value;
|
||||
$stored[$key] = $this->sanitize_field_value($data[$key], $field);
|
||||
}
|
||||
|
||||
// Captcha stores a single positive scene list.
|
||||
@@ -431,9 +474,161 @@ class SchemaProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private function sanitize_field_value($value, array $field) {
|
||||
$type = (string) ($field['type'] ?? 'text');
|
||||
$sanitize_type = (string) ($field['sanitize_type'] ?? '');
|
||||
if ($sanitize_type !== '') {
|
||||
return $this->sanitize_value($value, $sanitize_type);
|
||||
}
|
||||
|
||||
if (in_array($type, ['fieldset', 'accordion'], true)) {
|
||||
return $this->sanitize_object_fields($value, (array) ($field['fields'] ?? []));
|
||||
}
|
||||
|
||||
if (in_array($type, ['group', 'repeater'], true)) {
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
$items = [];
|
||||
$limit = isset($field['max']) ? max(0, (int) $field['max']) : 100;
|
||||
foreach (array_slice(array_values($value), 0, $limit ?: 100) as $item) {
|
||||
if (is_array($item)) {
|
||||
$items[] = $this->sanitize_object_fields($item, (array) ($field['fields'] ?? []));
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
if ($type === 'sortable') {
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
$items = [];
|
||||
foreach ((array) ($field['items'] ?? []) as $item_schema) {
|
||||
$item_key = sanitize_key((string) ($item_schema['key'] ?? ''));
|
||||
if ($item_key !== '' && array_key_exists($item_key, $value) && is_array($value[$item_key])) {
|
||||
$items[$item_key] = $this->sanitize_object_fields($value[$item_key], (array) ($item_schema['fields'] ?? []));
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
if ($type === 'link') {
|
||||
$value = is_array($value) ? $value : [];
|
||||
return [
|
||||
'url' => esc_url_raw((string) ($value['url'] ?? '')),
|
||||
'target' => in_array(($value['target'] ?? ''), ['', '_self', '_blank'], true) ? (string) ($value['target'] ?? '') : '',
|
||||
'text' => sanitize_text_field((string) ($value['text'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
if ($type === 'checkbox' || !empty($field['multiple'])) {
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
return array_values(array_map(static function ($item) {
|
||||
return is_numeric($item) ? (int) $item : sanitize_key((string) $item);
|
||||
}, $value));
|
||||
}
|
||||
|
||||
if (in_array($type, ['switch', 'switcher'], true)) {
|
||||
return !empty($value);
|
||||
}
|
||||
if (in_array($type, ['number', 'slider', 'spinner'], true)) {
|
||||
return is_float($value) ? (float) $value : (int) $value;
|
||||
}
|
||||
if (in_array($type, ['media', 'upload', 'url'], true)) {
|
||||
return esc_url_raw((string) $value);
|
||||
}
|
||||
|
||||
return sanitize_text_field((string) $value);
|
||||
}
|
||||
|
||||
private function sanitize_object_fields($value, array $fields): array {
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
$clean = [];
|
||||
foreach ($fields as $field) {
|
||||
if (!is_array($field) || empty($field['key'])) {
|
||||
continue;
|
||||
}
|
||||
$key = (string) $field['key'];
|
||||
if (array_key_exists($key, $value)) {
|
||||
$clean[$key] = $this->sanitize_field_value($value[$key], $field);
|
||||
}
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function validate_nested_field(array $field, $value, string $path): array {
|
||||
$type = (string) ($field['type'] ?? 'text');
|
||||
$errors = [];
|
||||
if (in_array($type, ['fieldset', 'accordion'], true) && is_array($value)) {
|
||||
$errors = $this->validate_object_fields((array) ($field['fields'] ?? []), $value, $path);
|
||||
} elseif (in_array($type, ['group', 'repeater'], true) && is_array($value)) {
|
||||
foreach (array_values($value) as $index => $item) {
|
||||
if (is_array($item)) {
|
||||
$errors = array_merge($errors, $this->validate_object_fields((array) ($field['fields'] ?? []), $item, $path . '[' . $index . ']'));
|
||||
}
|
||||
}
|
||||
} elseif ($type === 'sortable' && is_array($value)) {
|
||||
$items = [];
|
||||
foreach ((array) ($field['items'] ?? []) as $item_schema) {
|
||||
if (is_array($item_schema) && !empty($item_schema['key'])) {
|
||||
$items[(string) $item_schema['key']] = $item_schema;
|
||||
}
|
||||
}
|
||||
foreach (array_keys($value) as $item_key) {
|
||||
if (!isset($items[$item_key])) {
|
||||
$errors[] = sprintf('%s has an invalid item', $path);
|
||||
} elseif (is_array($value[$item_key])) {
|
||||
$errors = array_merge($errors, $this->validate_object_fields((array) ($items[$item_key]['fields'] ?? []), $value[$item_key], $path . '.' . $item_key));
|
||||
}
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
private function validate_object_fields(array $fields, array $value, string $path): array {
|
||||
$errors = [];
|
||||
foreach ($fields as $field) {
|
||||
$key = (string) ($field['key'] ?? '');
|
||||
if ($key === '' || !array_key_exists($key, $value)) {
|
||||
continue;
|
||||
}
|
||||
$type = (string) ($field['type'] ?? 'text');
|
||||
if (in_array($type, ['select', 'radio', 'palette', 'image_select'], true) && !empty($field['options'])) {
|
||||
$allowed = array_column($field['options'], 'value');
|
||||
if (!empty($field['multiple']) && is_array($value[$key])) {
|
||||
foreach ($value[$key] as $item) {
|
||||
if (!in_array($item, $allowed, true)) {
|
||||
$errors[] = sprintf('%s.%s has an invalid value', $path, $key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} elseif (!in_array($value[$key], $allowed, true)) {
|
||||
$errors[] = sprintf('%s.%s has an invalid value', $path, $key);
|
||||
}
|
||||
}
|
||||
if (in_array($type, ['number', 'slider', 'spinner'], true)) {
|
||||
if (!is_numeric($value[$key])) {
|
||||
$errors[] = sprintf('%s.%s must be a number', $path, $key);
|
||||
} elseif (isset($field['min']) && $value[$key] < $field['min']) {
|
||||
$errors[] = sprintf('%s.%s must be at least %s', $path, $key, $field['min']);
|
||||
} elseif (isset($field['max']) && $value[$key] > $field['max']) {
|
||||
$errors[] = sprintf('%s.%s must be at most %s', $path, $key, $field['max']);
|
||||
}
|
||||
}
|
||||
$errors = array_merge($errors, $this->validate_nested_field($field, $value[$key], $path . '.' . $key));
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
private function infer_sanitize_type(string $field_type): string {
|
||||
$map = [
|
||||
'switch' => 'bool',
|
||||
'switcher' => 'bool',
|
||||
'input' => 'string',
|
||||
'text' => 'string',
|
||||
'textarea' => 'textarea',
|
||||
@@ -443,12 +638,19 @@ class SchemaProvider {
|
||||
'radio' => 'string',
|
||||
'checkbox' => 'array',
|
||||
'repeater' => 'array',
|
||||
'group' => 'array',
|
||||
'fieldset' => 'array',
|
||||
'accordion'=> 'array',
|
||||
'sortable' => 'array',
|
||||
'link' => 'array',
|
||||
'async_select' => 'array',
|
||||
'json' => 'array',
|
||||
// 自定义面板(验证码服务商等)按对象存储
|
||||
'custom' => 'array',
|
||||
'color' => 'string',
|
||||
'icon' => 'string',
|
||||
'media' => 'url',
|
||||
'upload' => 'url',
|
||||
'code' => 'code',
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user