Files
wordpress-theme-aksrt/src/Admin/ControllerBase.php
T
kate522andClaude Fable 5 57ff405691 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>
2026-10-28 20:47:41 +08:00

196 lines
6.8 KiB
PHP

<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
abstract class ControllerBase {
/**
* REST API namespace (shared with Admin).
*/
const API_NAMESPACE = 'aksrt/v1';
/**
* Register REST routes for this domain.
* Subclasses must implement this method.
*/
abstract public function register_routes(): void;
/**
* Permission callback — requires manage_options capability.
*/
public function check_permission(?\WP_REST_Request $request = null) {
if (!current_user_can('manage_options')) {
if ($request === null) {
return false;
}
return new \WP_Error('aksrt_admin_forbidden', __('无权执行此操作', 'aksrt'), ['status' => 403]);
}
if ($request !== null) {
$nonce = (string) $request->get_header('X-WP-Nonce');
if ($nonce === '' || !wp_verify_nonce($nonce, 'wp_rest')) {
return new \WP_Error('aksrt_rest_nonce', __('安全验证失败,请刷新页面后重试', 'aksrt'), ['status' => 403]);
}
}
return true;
}
/**
* Create a standard success response.
*/
protected function success($data = [], int $status = 200): \WP_REST_Response {
return new \WP_REST_Response(array_merge(['success' => true], $data), $status);
}
/**
* Create a standard error response.
*/
protected function error(string $message, int $status = 400): \WP_REST_Response {
return new \WP_REST_Response([
'success' => false,
'message' => $message,
], $status);
}
/**
* Read JSON request body safely.
*/
protected function get_json_params(\WP_REST_Request $request): array {
$params = $request->get_json_params();
return is_array($params) ? $params : [];
}
/**
* Sanitize a settings array using a whitelist of allowed keys.
*
* @param array $input Raw input data.
* @param array $allowed Allowed keys with their types (string|int|bool|float|array).
* @return array Sanitized data.
*/
protected function sanitize_fields(array $input, array $allowed): array {
$out = [];
foreach ($allowed as $key => $type) {
if (!array_key_exists($key, $input)) continue;
$val = $input[$key];
switch ($type) {
case 'string':
$out[$key] = sanitize_text_field($val);
break;
case 'textarea':
$out[$key] = sanitize_textarea_field($val);
break;
case 'html':
$out[$key] = wp_kses_post($val);
break;
case 'code':
$out[$key] = is_string($val) ? wp_unslash($val) : '';
break;
case 'url':
$out[$key] = esc_url_raw($val);
break;
case 'int':
$out[$key] = (int) $val;
break;
case 'bool':
$out[$key] = !empty($val);
break;
case 'float':
$out[$key] = (float) $val;
break;
case 'array':
$out[$key] = is_array($val) ? $val : [];
break;
case 'attachment_ids':
$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);
}
}
return $out;
}
protected function register_section_routes(string $route_slug, string $domain): void {
register_rest_route(self::API_NAMESPACE, '/settings/' . $route_slug . '/(?P<section>[a-z0-9_-]+)', [
'methods' => 'GET',
'callback' => function (\WP_REST_Request $request) use ($domain) {
return $this->get_section_settings($request, $domain);
},
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/settings/' . $route_slug . '/(?P<section>[a-z0-9_-]+)', [
'methods' => 'POST',
'callback' => function (\WP_REST_Request $request) use ($domain) {
return $this->save_section_settings($request, $domain);
},
'permission_callback' => [$this, 'check_permission'],
]);
}
protected function get_section_settings(\WP_REST_Request $request, string $domain): \WP_REST_Response {
$provider = SchemaProvider::get_instance();
$section = (string) $request->get_param('section');
$schema = $provider->get_section_schema($domain, $section);
if (!$schema) {
return $this->error('未知的设置分组: ' . esc_html($section), 404);
}
return $this->success([
'data' => $provider->read_settings($domain, $section),
'schema' => $schema,
'payload' => [
'domain' => $provider->normalize_domain($domain),
'section' => $provider->normalize_section($section),
'version' => $schema['version'] ?? '1.0.0',
],
]);
}
protected function save_section_settings(\WP_REST_Request $request, string $domain): \WP_REST_Response {
$provider = SchemaProvider::get_instance();
$section = (string) $request->get_param('section');
$schema = $provider->get_section_schema($domain, $section);
if (!$schema) {
return $this->error('未知的设置分组: ' . esc_html($section), 404);
}
$params = $this->get_json_params($request);
$payload = $params['data'] ?? $params;
if (!is_array($payload)) {
return $this->error('无效的设置载荷');
}
$sanitized = $provider->sanitize_payload($domain, $payload, $section);
$validation = $provider->validate($domain, $sanitized, $section);
if (!$validation['valid']) {
return $this->error(implode('; ', $validation['errors']), 422);
}
$provider->write_settings($domain, $sanitized, $section);
return $this->success([
'message' => sprintf('%s/%s 设置已保存', $provider->normalize_domain($domain), $provider->normalize_section($section)),
'data' => $provider->read_settings($domain, $section),
'payload' => [
'domain' => $provider->normalize_domain($domain),
'section' => $provider->normalize_section($section),
'version' => $schema['version'] ?? '1.0.0',
],
]);
}
}