refactor: flatten src/AKSRT → src (drop theme-identifier dir)

Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
This commit is contained in:
2026-08-02 22:06:06 +08:00
parent 56f1a8175f
commit 8c5b1555c9
285 changed files with 26 additions and 3600 deletions
+450
View File
@@ -0,0 +1,450 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
class SchemaProvider {
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',
];
}
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',
];
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');
// Multi-select: accept array values when schema.multiple is set
// (legacy / SPA may still use type=select multiple).
if (!empty($field['multiple']) && is_array($value)) {
foreach ($value as $item) {
if (!in_array($item, $valid_values, true)) {
$errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
break;
}
}
} elseif (!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');
if (!is_array($value)) {
$errors[] = sprintf('%s must be an array', $field['label'] ?? $key);
continue;
}
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 {
// Builders live under AKSRT\Admin\Schema (Composer PSR-4). Relative
// Schema\*::class resolves correctly from this AKSRT\Admin namespace.
return [
'global' => Schema\GlobalSchema::class,
'page_display' => Schema\SchemaPageDisplay::class,
'post_list' => Schema\SchemaPostList::class,
'permissions' => Schema\SchemaPermissions::class,
'users' => Schema\SchemaUsers::class,
'payment' => Schema\SchemaPayment::class,
'extensions' => Schema\SchemaExtensions::class,
'tools' => Schema\SchemaTools::class,
];
}
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']];
}
}
// Captcha: always surface normalized enabled_scenarios (legacy disabled list inverted).
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_get_captcha_config')) {
$normalized = aksrt_get_captcha_config();
foreach (['enabled', 'active_provider', 'enabled_scenarios', 'disabled_scenarios', 'tencent', 'aliyun', 'cloudflare', 'geetest', 'mode'] as $key) {
if (array_key_exists($key, $normalized)) {
$data[$key] = $normalized[$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;
}
// Captcha: keep enabled_scenarios / disabled_scenarios mirrors in sync.
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_captcha_scenes')) {
$all = array_keys(aksrt_captcha_scenes());
if (array_key_exists('enabled_scenarios', $stored)) {
$enabled = function_exists('aksrt_captcha_sanitize_scene_list')
? aksrt_captcha_sanitize_scene_list($stored['enabled_scenarios'])
: (array) $stored['enabled_scenarios'];
$stored['enabled_scenarios'] = $enabled;
$stored['disabled_scenarios'] = array_values(array_diff($all, $enabled));
} elseif (array_key_exists('disabled_scenarios', $stored)) {
$disabled = function_exists('aksrt_captcha_sanitize_scene_list')
? aksrt_captcha_sanitize_scene_list($stored['disabled_scenarios'])
: (array) $stored['disabled_scenarios'];
$stored['disabled_scenarios'] = $disabled;
$stored['enabled_scenarios'] = array_values(array_diff($all, $disabled));
}
}
$updated = update_option($option_key, $stored);
\AKSRT\Settings\Settings::flush_settings_cache();
if (isset($stored['site_favicon'])) {
\AKSRT\Settings\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 'textarea':
return sanitize_textarea_field((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' => 'textarea',
'number' => 'int',
'slider' => 'int',
'select' => 'string',
'radio' => 'string',
'checkbox' => 'array',
'repeater' => 'array',
'json' => 'array',
// 自定义面板(验证码服务商等)按对象存储
'custom' => 'array',
'color' => 'string',
'icon' => 'string',
'media' => 'url',
'code' => 'code',
];
return $map[$field_type] ?? 'string';
}
}