Align post_list.list pagination values with PHP (numbered/load_more/infinite) so SchemaPage saves no longer 422 on 分页模式. Stop permissions schema build from fatally calling missing build_cap_matrix_groups() when loading login/moderation sections, normalize moderation dependsOn objects, and skip notice-only fields in schema field collection.
395 lines
13 KiB
PHP
395 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* AKSRT Admin Schema Provider
|
|
*
|
|
* Defines the canonical PHP-side settings contract:
|
|
* domain -> section -> group -> field.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class AKSRT_Admin_Schema_Provider {
|
|
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* @var array<string, array<string, mixed>>
|
|
*/
|
|
private $domain_schemas = [];
|
|
|
|
public static function get_instance(): self {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
public function normalize_domain(string $domain): string {
|
|
$domain = trim(str_replace('-', '_', $domain));
|
|
return $domain !== '' ? $domain : 'global';
|
|
}
|
|
|
|
public function normalize_section(string $section): string {
|
|
return trim($section);
|
|
}
|
|
|
|
public function get_domain_keys(): array {
|
|
return [
|
|
'global',
|
|
'page_display',
|
|
'post_list',
|
|
'permissions',
|
|
'users',
|
|
'payment',
|
|
'extensions',
|
|
'tools',
|
|
'shop',
|
|
'forum',
|
|
];
|
|
}
|
|
|
|
public function get_schema(string $domain): ?array {
|
|
$domain = $this->normalize_domain($domain);
|
|
|
|
if (isset($this->domain_schemas[$domain])) {
|
|
return $this->domain_schemas[$domain];
|
|
}
|
|
|
|
$builder_map = $this->get_schema_builder_map();
|
|
if (!isset($builder_map[$domain])) {
|
|
return null;
|
|
}
|
|
|
|
$builder = $builder_map[$domain];
|
|
$this->domain_schemas[$domain] = $builder::build();
|
|
|
|
return $this->domain_schemas[$domain];
|
|
}
|
|
|
|
public function get_sections(string $domain): array {
|
|
$schema = $this->get_schema($domain);
|
|
return is_array($schema['sections'] ?? null) ? $schema['sections'] : [];
|
|
}
|
|
|
|
public function get_section_schema(string $domain, string $section): ?array {
|
|
$sections = $this->get_sections($domain);
|
|
$section = $this->normalize_section($section);
|
|
return $sections[$section] ?? null;
|
|
}
|
|
|
|
public function get_option_key(string $domain, string $section = ''): string {
|
|
if ($section !== '') {
|
|
$schema = $this->get_section_schema($domain, $section);
|
|
return (string) ($schema['option_key'] ?? 'aksrt_settings');
|
|
}
|
|
|
|
$primary = [
|
|
'global' => 'aksrt_settings',
|
|
'page_display' => 'aksrt_settings',
|
|
'post_list' => 'aksrt_settings',
|
|
'permissions' => 'aksrt_settings',
|
|
'users' => 'aksrt_settings',
|
|
'payment' => 'aksrt_settings',
|
|
'extensions' => 'aksrt_settings',
|
|
'tools' => 'aksrt_settings',
|
|
'shop' => 'aksrt_settings',
|
|
'forum' => 'aksrt_settings',
|
|
];
|
|
|
|
return $primary[$this->normalize_domain($domain)] ?? 'aksrt_settings';
|
|
}
|
|
|
|
public function get_defaults(string $domain, string $section = ''): array {
|
|
if ($section !== '') {
|
|
$schema = $this->get_section_schema($domain, $section);
|
|
return $schema ? $this->collect_defaults($schema) : [];
|
|
}
|
|
|
|
$defaults = [];
|
|
foreach ($this->get_sections($domain) as $section_schema) {
|
|
$defaults = array_merge($defaults, $this->collect_defaults($section_schema));
|
|
}
|
|
|
|
return $defaults;
|
|
}
|
|
|
|
public function get_allowed_fields(string $domain, string $section = ''): array {
|
|
if ($section !== '') {
|
|
$schema = $this->get_section_schema($domain, $section);
|
|
return $schema ? $this->collect_allowed_fields($schema) : [];
|
|
}
|
|
|
|
$allowed = [];
|
|
foreach ($this->get_sections($domain) as $section_schema) {
|
|
$allowed = array_merge($allowed, $this->collect_allowed_fields($section_schema));
|
|
}
|
|
|
|
return $allowed;
|
|
}
|
|
|
|
public function validate(string $domain, array $data, string $section = ''): array {
|
|
$schemas = [];
|
|
|
|
if ($section !== '') {
|
|
$schema = $this->get_section_schema($domain, $section);
|
|
if (!$schema) {
|
|
return ['valid' => false, 'errors' => ['Unknown section']];
|
|
}
|
|
$schemas[] = $schema;
|
|
} else {
|
|
$schemas = array_values($this->get_sections($domain));
|
|
}
|
|
|
|
$errors = [];
|
|
|
|
foreach ($schemas as $schema) {
|
|
foreach ($this->get_all_fields($schema) as $field) {
|
|
$key = $field['key'];
|
|
if (!array_key_exists($key, $data)) {
|
|
continue;
|
|
}
|
|
|
|
$value = $data[$key];
|
|
if (!empty($field['required']) && ($value === '' || $value === null)) {
|
|
$errors[] = sprintf('%s is required', $field['label'] ?? $key);
|
|
continue;
|
|
}
|
|
|
|
$type = $field['type'] ?? 'input';
|
|
if (($type === 'number' || $type === 'slider') && !is_numeric($value)) {
|
|
$errors[] = sprintf('%s must be a number', $field['label'] ?? $key);
|
|
continue;
|
|
}
|
|
|
|
if ($type === 'checkbox' && !is_array($value)) {
|
|
$errors[] = sprintf('%s must be an array', $field['label'] ?? $key);
|
|
continue;
|
|
}
|
|
|
|
if ($type === 'repeater' && !is_array($value)) {
|
|
$errors[] = sprintf('%s must be an array of items', $field['label'] ?? $key);
|
|
continue;
|
|
}
|
|
|
|
if (isset($field['min']) && is_numeric($value) && $value < $field['min']) {
|
|
$errors[] = sprintf('%s must be at least %s', $field['label'] ?? $key, $field['min']);
|
|
}
|
|
|
|
if (isset($field['max']) && is_numeric($value) && $value > $field['max']) {
|
|
$errors[] = sprintf('%s must be at most %s', $field['label'] ?? $key, $field['max']);
|
|
}
|
|
|
|
if (in_array($type, ['select', 'radio'], true) && !empty($field['options'])) {
|
|
$valid_values = array_column($field['options'], 'value');
|
|
if (!in_array($value, $valid_values, true)) {
|
|
$errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
|
|
}
|
|
}
|
|
|
|
if ($type === 'checkbox' && !empty($field['options'])) {
|
|
$valid_values = array_column($field['options'], 'value');
|
|
foreach ((array) $value as $item) {
|
|
if (!in_array($item, $valid_values, true)) {
|
|
$errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ['valid' => empty($errors), 'errors' => $errors];
|
|
}
|
|
|
|
public function read_settings(string $domain, string $section = ''): array {
|
|
if ($section !== '') {
|
|
$schema = $this->get_section_schema($domain, $section);
|
|
if (!$schema) {
|
|
return [];
|
|
}
|
|
|
|
return $this->read_section_settings($schema);
|
|
}
|
|
|
|
$settings = [];
|
|
foreach ($this->get_sections($domain) as $section_schema) {
|
|
$settings = array_merge($settings, $this->read_section_settings($section_schema));
|
|
}
|
|
|
|
return $settings;
|
|
}
|
|
|
|
public function write_settings(string $domain, array $data, string $section = ''): bool {
|
|
if ($section !== '') {
|
|
$schema = $this->get_section_schema($domain, $section);
|
|
if (!$schema) {
|
|
return false;
|
|
}
|
|
|
|
return $this->write_section_settings($schema, $data);
|
|
}
|
|
|
|
$result = true;
|
|
foreach ($this->get_sections($domain) as $section_schema) {
|
|
$allowed = array_keys($this->collect_allowed_fields($section_schema));
|
|
$payload = array_intersect_key($data, array_flip($allowed));
|
|
if ($payload === []) {
|
|
continue;
|
|
}
|
|
$result = $this->write_section_settings($section_schema, $payload) && $result;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function reset_to_defaults(string $domain, string $section = ''): bool {
|
|
if ($section !== '') {
|
|
return $this->write_settings($domain, $this->get_defaults($domain, $section), $section);
|
|
}
|
|
|
|
return $this->write_settings($domain, $this->get_defaults($domain));
|
|
}
|
|
|
|
private function get_schema_builder_map(): array {
|
|
return [
|
|
'global' => 'AKSRT_Admin_Schema_Global',
|
|
'page_display' => 'AKSRT_Admin_Schema_Page_Display',
|
|
'post_list' => 'AKSRT_Admin_Schema_Post_List',
|
|
'permissions' => 'AKSRT_Admin_Schema_Permissions',
|
|
'users' => 'AKSRT_Admin_Schema_Users',
|
|
'payment' => 'AKSRT_Admin_Schema_Payment',
|
|
'extensions' => 'AKSRT_Admin_Schema_Extensions',
|
|
'tools' => 'AKSRT_Admin_Schema_Tools',
|
|
'shop' => 'AKSRT_Admin_Schema_Shop',
|
|
'forum' => 'AKSRT_Admin_Schema_Forum',
|
|
];
|
|
}
|
|
|
|
private function read_section_settings(array $schema): array {
|
|
$option_key = (string) ($schema['option_key'] ?? 'aksrt_settings');
|
|
$stored = get_option($option_key, []);
|
|
if (!is_array($stored)) {
|
|
$stored = [];
|
|
}
|
|
|
|
$data = $this->collect_defaults($schema);
|
|
foreach ($this->get_all_fields($schema) as $field) {
|
|
if (array_key_exists($field['key'], $stored)) {
|
|
$data[$field['key']] = $stored[$field['key']];
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function write_section_settings(array $schema, array $data): bool {
|
|
$option_key = (string) ($schema['option_key'] ?? 'aksrt_settings');
|
|
$stored = get_option($option_key, []);
|
|
if (!is_array($stored)) {
|
|
$stored = [];
|
|
}
|
|
|
|
$allowed = $this->collect_allowed_fields($schema);
|
|
foreach ($allowed as $key => $type) {
|
|
if (!array_key_exists($key, $data)) {
|
|
continue;
|
|
}
|
|
|
|
$value = $this->sanitize_value($data[$key], $type);
|
|
$stored[$key] = $value;
|
|
}
|
|
|
|
$updated = update_option($option_key, $stored);
|
|
AKSRT_Settings::flush_settings_cache();
|
|
|
|
if (isset($stored['site_favicon'])) {
|
|
AKSRT_Settings::sync_native_site_icon((string) $stored['site_favicon']);
|
|
}
|
|
|
|
return $updated || true;
|
|
}
|
|
|
|
private function collect_defaults(array $schema): array {
|
|
$defaults = [];
|
|
foreach ($this->get_all_fields($schema) as $field) {
|
|
if (array_key_exists('default', $field)) {
|
|
$defaults[$field['key']] = $field['default'];
|
|
}
|
|
}
|
|
return $defaults;
|
|
}
|
|
|
|
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');
|
|
}
|
|
return $allowed;
|
|
}
|
|
|
|
private function get_all_fields(array $schema): array {
|
|
$fields = [];
|
|
foreach ($schema['groups'] ?? [] as $group) {
|
|
if (!is_array($group)) {
|
|
continue;
|
|
}
|
|
foreach ($group['fields'] ?? [] as $field) {
|
|
if (!is_array($field) || empty($field['key'])) {
|
|
continue;
|
|
}
|
|
// Notice/help-only rows are UI chrome, not stored settings keys.
|
|
if (($field['type'] ?? '') === 'notice') {
|
|
continue;
|
|
}
|
|
$fields[] = $field;
|
|
}
|
|
}
|
|
return $fields;
|
|
}
|
|
|
|
private function sanitize_value($value, string $type) {
|
|
switch ($type) {
|
|
case 'html':
|
|
return wp_kses_post((string) $value);
|
|
case 'code':
|
|
return is_string($value) ? wp_unslash($value) : '';
|
|
case 'url':
|
|
return esc_url_raw((string) $value);
|
|
case 'int':
|
|
return (int) $value;
|
|
case 'bool':
|
|
return !empty($value);
|
|
case 'float':
|
|
return (float) $value;
|
|
case 'array':
|
|
return is_array($value) ? $value : [];
|
|
default:
|
|
return sanitize_text_field((string) $value);
|
|
}
|
|
}
|
|
|
|
private function infer_sanitize_type(string $field_type): string {
|
|
$map = [
|
|
'switch' => 'bool',
|
|
'input' => 'string',
|
|
'text' => 'string',
|
|
'textarea' => 'string',
|
|
'number' => 'int',
|
|
'slider' => 'int',
|
|
'select' => 'string',
|
|
'radio' => 'string',
|
|
'checkbox' => 'array',
|
|
'repeater' => 'array',
|
|
'color' => 'string',
|
|
'icon' => 'string',
|
|
'media' => 'url',
|
|
'code' => 'code',
|
|
];
|
|
|
|
return $map[$field_type] ?? 'string';
|
|
}
|
|
}
|