section -> group -> field. * * @package AKSRT */ defined('ABSPATH') || exit; class AKSRT_Admin_Schema_Provider { private static $instance = null; /** * @var array> */ 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': // 深层字符串净化,保留嵌套服务商配置(如 geetest.captcha_id) if (!is_array($value)) { return []; } $out = []; foreach ($value as $k => $v) { $key = is_string($k) ? sanitize_key($k) : $k; if (is_array($v)) { $out[$key] = $this->sanitize_value($v, 'array'); } elseif (is_bool($v)) { $out[$key] = $v; } elseif (is_int($v) || is_float($v)) { $out[$key] = $v; } else { $out[$key] = sanitize_text_field((string) $v); } } return $out; 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', // 自定义面板(验证码服务商等)按对象存储 'custom' => 'array', 'color' => 'string', 'icon' => 'string', 'media' => 'url', 'code' => 'code', ]; return $map[$field_type] ?? 'string'; } }