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:
@@ -0,0 +1,551 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class ExtensionController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
private $admin;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function set_admin(Admin $admin): void {
|
||||
$this->admin = $admin;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// Extensions overview
|
||||
register_rest_route(self::API_NAMESPACE, '/extensions/overview', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_overview'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Extensions service status summary (aggregated)
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/extensions', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_extensions_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Security settings summary
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/security', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_security_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Security settings — save risk switches
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/security', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'save_security_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Security audit log
|
||||
register_rest_route(self::API_NAMESPACE, '/security/audit-log', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_audit_log'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// IP geo test tool
|
||||
register_rest_route(self::API_NAMESPACE, '/extensions/ip-test', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'test_ip_geo'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Content audit test tool
|
||||
register_rest_route(self::API_NAMESPACE, '/extensions/audit-test', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'test_content_audit'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /extensions/overview — real extension status aggregation.
|
||||
*/
|
||||
public function get_overview(): \WP_REST_Response {
|
||||
$services = $this->collect_service_statuses();
|
||||
|
||||
$installed = 0;
|
||||
$available = count($services);
|
||||
$unconfigured = 0;
|
||||
|
||||
foreach ($services as $svc) {
|
||||
if (!empty($svc['enabled'])) $installed++;
|
||||
if (!empty($svc['enabled']) && !empty($svc['configured']) && empty($svc['verified'])) {
|
||||
$unconfigured++;
|
||||
}
|
||||
}
|
||||
|
||||
// Build message
|
||||
if ($installed === 0) {
|
||||
$message = sprintf(__('已注册 %d 项扩展,均未启用。', 'aksrt'), $available);
|
||||
} elseif ($unconfigured > 0) {
|
||||
$message = sprintf(__('已启用 %d/%d 项扩展,%d 项待补全配置。', 'aksrt'), $installed, $available, $unconfigured);
|
||||
} else {
|
||||
$message = sprintf(__('已启用 %d/%d 项扩展,全部就绪。', 'aksrt'), $installed, $available);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'installed' => $installed,
|
||||
'available' => $available,
|
||||
'message' => $message,
|
||||
'services' => $services,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_docs_stats(): \WP_REST_Response {
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'total' => 0,
|
||||
'nav_count' => 0,
|
||||
'special_count' => 0,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /docs/search — search document pages by keyword.
|
||||
*/
|
||||
public function search_docs(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'results' => [],
|
||||
'total' => 0,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /settings/extensions — aggregated extension service status.
|
||||
*/
|
||||
public function get_extensions_status(): \WP_REST_Response {
|
||||
$services = $this->collect_service_statuses();
|
||||
$flat = [];
|
||||
|
||||
foreach ($services as $key => $svc) {
|
||||
$flat["{$key}_enabled"] = !empty($svc['enabled']);
|
||||
$flat["{$key}_configured"] = !empty($svc['configured']);
|
||||
$flat["{$key}_available"] = !empty($svc['available']);
|
||||
$flat["{$key}_verified"] = !empty($svc['verified']);
|
||||
$flat["{$key}_provider"] = (string) ($svc['provider'] ?? '');
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => $flat,
|
||||
'services' => $services,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /settings/security — security settings summary with real service status.
|
||||
*/
|
||||
public function get_security_settings(): \WP_REST_Response {
|
||||
$services = $this->collect_service_statuses();
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
|
||||
$email_gate_enabled = true;
|
||||
if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) {
|
||||
$email_gate_enabled = false;
|
||||
}
|
||||
|
||||
$captcha = function_exists('aksrt_get_captcha_status_summary')
|
||||
? aksrt_get_captcha_status_summary()
|
||||
: ['enabled' => false, 'provider' => '', 'configured' => false, 'scenes' => []];
|
||||
$features = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_cms_features() : [];
|
||||
$scenes = is_array($captcha['scenes'] ?? null) ? $captcha['scenes'] : [];
|
||||
|
||||
$risk_switches = [
|
||||
'show_unapproved_comments' => !empty($settings['show_unapproved_comments']),
|
||||
'skip_submit_moderation' => !empty($settings['skip_submit_moderation']),
|
||||
'guest_comment' => !empty($settings['guest_comment']),
|
||||
];
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'email_gate_enabled' => $email_gate_enabled,
|
||||
'captcha' => [
|
||||
'enabled' => !empty($captcha['enabled']),
|
||||
'provider' => (string) ($captcha['provider'] ?? ''),
|
||||
'configured' => !empty($captcha['configured']),
|
||||
'scenes' => $scenes,
|
||||
'download_required' => !empty($features['download_captcha']),
|
||||
'download_active' => !empty($features['download_captcha']) && !empty($scenes['download']),
|
||||
],
|
||||
'content_audit' => [
|
||||
'enabled' => !empty($services['audit']['enabled']),
|
||||
'configured' => !empty($services['audit']['configured']),
|
||||
],
|
||||
'ip_geo' => [
|
||||
'enabled' => !empty($services['ip_geo']['enabled']),
|
||||
'configured' => !empty($services['ip_geo']['configured']),
|
||||
],
|
||||
'risk_switches' => $risk_switches,
|
||||
'services' => $services,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /settings/security — save risk switches.
|
||||
*/
|
||||
public function save_security_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$risk_switches = $params['risk_switches'] ?? [];
|
||||
|
||||
if (!is_array($risk_switches)) {
|
||||
return $this->error('无效的风险设置数据');
|
||||
}
|
||||
|
||||
$allowed = [
|
||||
'show_unapproved_comments' => 'bool',
|
||||
'skip_submit_moderation' => 'bool',
|
||||
'guest_comment' => 'bool',
|
||||
];
|
||||
|
||||
$sanitized = $this->sanitize_fields($risk_switches, $allowed);
|
||||
|
||||
// Merge into main settings
|
||||
$all = \AKSRT\Settings\Settings::get_all();
|
||||
foreach ($sanitized as $key => $val) {
|
||||
$all[$key] = $val;
|
||||
}
|
||||
|
||||
update_option('aksrt_settings', $all);
|
||||
|
||||
return $this->success([
|
||||
'message' => __('风险设置已保存', 'aksrt'),
|
||||
'data' => $sanitized,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /security/audit-log — recent security audit entries.
|
||||
*/
|
||||
public function get_audit_log(): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$limit = 20;
|
||||
$logs = [];
|
||||
|
||||
// Recent user registrations
|
||||
$recent_users = get_users([
|
||||
'number' => $limit,
|
||||
'orderby' => 'registered',
|
||||
'order' => 'DESC',
|
||||
'fields' => ['ID', 'display_name', 'user_email', 'user_registered'],
|
||||
]);
|
||||
|
||||
foreach ($recent_users as $user) {
|
||||
$logs[] = [
|
||||
'id' => 'reg_' . $user->ID,
|
||||
'action' => '用户注册',
|
||||
'detail' => sprintf('%s (%s) 完成注册', $user->display_name, $user->user_email),
|
||||
'time' => $user->user_registered,
|
||||
'type' => 'auth',
|
||||
];
|
||||
}
|
||||
|
||||
// Content audit log
|
||||
$audit_log = get_option('aksrt_content_audit_log', []);
|
||||
foreach (array_slice(array_reverse($audit_log), 0, 10) as $i => $entry) {
|
||||
$logs[] = [
|
||||
'id' => 'audit_' . $i,
|
||||
'action' => '内容审核',
|
||||
'detail' => sprintf('%s — %s', $entry['content'] ?? '', ($entry['passed'] ?? false) ? '通过' : '拦截'),
|
||||
'time' => $entry['time'] ?? '',
|
||||
'type' => 'audit',
|
||||
];
|
||||
}
|
||||
|
||||
// Sort by time descending
|
||||
usort($logs, function ($a, $b) {
|
||||
return strcmp($b['time'] ?? '', $a['time'] ?? '');
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'logs' => array_slice($logs, 0, $limit),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /extensions/ip-test — test current IP provider with a given IP.
|
||||
*
|
||||
* Admin tests force a live lookup (skip enable-gate + cache) so operators can
|
||||
* verify credentials before flipping the runtime switch.
|
||||
*/
|
||||
public function test_ip_geo(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$ip = trim((string) ($params['ip'] ?? ''));
|
||||
|
||||
if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $this->error(__('请输入有效的 IP 地址', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Extensions\IPGeo')) {
|
||||
return $this->error(__('IP 归属地服务不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$geo = \AKSRT\Extensions\IPGeo::get_instance();
|
||||
$status = $geo->get_status();
|
||||
$result = $geo->lookup($ip, true);
|
||||
|
||||
$reason = (string) ($result['reason'] ?? '');
|
||||
$message = '';
|
||||
$ok = !empty($result['country']) || !empty($result['province']) || !empty($result['city']);
|
||||
|
||||
// Persist "verified" once a live lookup succeeds so overview cards leave "待补全".
|
||||
if ($ok) {
|
||||
$all = \AKSRT\Settings\Settings::get_all();
|
||||
if (empty($all['ip_geo_verified'])) {
|
||||
$all['ip_geo_verified'] = true;
|
||||
update_option('aksrt_settings', $all);
|
||||
if (method_exists('\AKSRT\Settings\Settings', 'flush_settings_cache')) {
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
}
|
||||
$status = $geo->get_status();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
$detail = trim((string) ($result['detail'] ?? ''));
|
||||
switch ($reason) {
|
||||
case 'private_ip':
|
||||
$message = __('该地址为私有/保留 IP(如 127.0.0.1 / 内网),无法查询。请用公网 IP 测试,例如 114.114.114.114。', 'aksrt');
|
||||
break;
|
||||
case 'invalid_ip':
|
||||
$message = __('IP 格式无效。', 'aksrt');
|
||||
break;
|
||||
case 'missing_key':
|
||||
$message = __('当前服务商 AppKey 未填写,请在「扩展 → IP归属地」补全凭据。', 'aksrt');
|
||||
break;
|
||||
case 'provider_error':
|
||||
// e.g. USERKEY_PLAT_NOMATCH / INVALID_USER_KEY from Amap
|
||||
if (stripos($detail, 'USERKEY_PLAT_NOMATCH') !== false) {
|
||||
$message = __('高德返回 USERKEY_PLAT_NOMATCH:当前 Key 平台类型不匹配。请在高德开放平台新建「Web服务」类型 Key,并开通 IP 定位,不要用 Web端(JS)/Android/iOS Key。', 'aksrt');
|
||||
} elseif (stripos($detail, 'INVALID_USER_KEY') !== false) {
|
||||
$message = __('高德返回 INVALID_USER_KEY:AppKey 无效或未启用,请核对 Key 是否复制完整、是否已启用。', 'aksrt');
|
||||
} else {
|
||||
$message = $detail !== ''
|
||||
? sprintf(
|
||||
/* translators: %s: provider error code/message */
|
||||
__('服务商拒绝请求:%s。请检查 Key 权限(IP定位)与配额。', 'aksrt'),
|
||||
$detail
|
||||
)
|
||||
: __('服务商返回错误,请检查 Key 权限与配额。', 'aksrt');
|
||||
}
|
||||
break;
|
||||
case 'http_error':
|
||||
$message = $detail !== ''
|
||||
? sprintf(
|
||||
/* translators: %s: transport error */
|
||||
__('无法访问归属地接口:%s。请检查服务器出站网络/防火墙。', 'aksrt'),
|
||||
$detail
|
||||
)
|
||||
: __('无法访问归属地接口,请检查服务器出站网络。', 'aksrt');
|
||||
break;
|
||||
case 'provider_empty':
|
||||
if (!empty($status['missing_fields'])) {
|
||||
$message = sprintf(
|
||||
/* translators: %s: missing config field keys */
|
||||
__('当前服务商凭据不完整(缺少:%s)。', 'aksrt'),
|
||||
implode(', ', (array) $status['missing_fields'])
|
||||
);
|
||||
} else {
|
||||
$message = __('服务商未返回有效归属地,请换公网 IP 或检查 Key 权限。', 'aksrt');
|
||||
}
|
||||
break;
|
||||
case 'disabled':
|
||||
$message = __('IP 归属地功能未启用。', 'aksrt');
|
||||
break;
|
||||
default:
|
||||
$message = $detail !== '' ? $detail : __('未查询到归属地信息。', 'aksrt');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'ip' => $ip,
|
||||
'ok' => $ok,
|
||||
'message' => $message,
|
||||
'status' => $status,
|
||||
'result' => $result,
|
||||
'display' => $ok ? $geo->format($result) : '',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /extensions/audit-test — test text content against current audit provider.
|
||||
*
|
||||
* Admin tests force a live provider call (skip enable gates + failure allow policy)
|
||||
* so operators can verify credentials before flipping runtime switches.
|
||||
*/
|
||||
public function test_content_audit(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$text = trim((string) ($params['text'] ?? ''));
|
||||
|
||||
if ($text === '') {
|
||||
return $this->error(__('请输入要测试的文本内容', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Extensions\ContentAudit')) {
|
||||
return $this->error(__('内容审核服务不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$audit = \AKSRT\Extensions\ContentAudit::get_instance();
|
||||
$status = $audit->get_status();
|
||||
$result = $audit->audit_text($text, true);
|
||||
|
||||
$reason = (string) ($result['reason'] ?? '');
|
||||
$message = '';
|
||||
$ok = !empty($result['passed']);
|
||||
|
||||
// config/api 错误时 passed=false 且 reason 是机器码,需要可读提示
|
||||
$error_reasons = ['config_missing', 'api_error', 'file_error', 'disabled'];
|
||||
$is_error = in_array($reason, $error_reasons, true)
|
||||
|| (str_starts_with($reason, 'config_') && $reason !== '');
|
||||
|
||||
if ($is_error || (!$ok && $reason !== '')) {
|
||||
switch ($reason) {
|
||||
case 'config_missing':
|
||||
if (!empty($status['missing_fields'])) {
|
||||
$message = sprintf(
|
||||
/* translators: %s: missing config field keys */
|
||||
__('当前审核服务商凭据不完整(缺少:%s),请先在「扩展 → 内容审核」补全配置。', 'aksrt'),
|
||||
implode(', ', (array) $status['missing_fields'])
|
||||
);
|
||||
} else {
|
||||
$message = __('审核服务商凭据缺失,请先补全 API Key / Secret。', 'aksrt');
|
||||
}
|
||||
$ok = false;
|
||||
break;
|
||||
case 'api_error':
|
||||
$message = __('调用审核接口失败,请检查凭据、网络出口或服务商状态。', 'aksrt');
|
||||
$ok = false;
|
||||
break;
|
||||
case 'disabled':
|
||||
$message = __('内容审核功能未启用。', 'aksrt');
|
||||
$ok = false;
|
||||
break;
|
||||
case 'non_compliant':
|
||||
$message = __('审核结论:不合规(已拦截)。', 'aksrt');
|
||||
break;
|
||||
case 'suspected':
|
||||
$message = __('审核结论:疑似违规。', 'aksrt');
|
||||
break;
|
||||
case 'review_failed':
|
||||
$message = __('审核结论:服务商审核失败。', 'aksrt');
|
||||
break;
|
||||
default:
|
||||
if (!$ok) {
|
||||
$message = $reason !== ''
|
||||
? sprintf(/* translators: %s: provider reason */ __('审核未通过:%s', 'aksrt'), $reason)
|
||||
: __('审核未通过。', 'aksrt');
|
||||
}
|
||||
}
|
||||
} elseif ($ok) {
|
||||
if ($reason === 'passthrough' || $reason === '') {
|
||||
$message = __('审核通过。', 'aksrt');
|
||||
} else {
|
||||
$message = sprintf(/* translators: %s: provider reason */ __('审核通过(%s)。', 'aksrt'), $reason);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'text' => $text,
|
||||
'ok' => $ok,
|
||||
'message' => $message,
|
||||
'status' => $status,
|
||||
'result' => $result,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集所有扩展服务的运行时状态
|
||||
*/
|
||||
private function collect_service_statuses(): array {
|
||||
$services = [];
|
||||
|
||||
// SMS
|
||||
if (class_exists('\AKSRT\Sms\SMS')) {
|
||||
$services['sms'] = \AKSRT\Sms\SMS::get_instance()->get_provider_status();
|
||||
} else {
|
||||
$services['sms'] = ['enabled' => false, 'provider' => 'none', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// WeChat
|
||||
if (class_exists('\AKSRT\Payment\WeChatNotify')) {
|
||||
$services['wechat'] = \AKSRT\Payment\WeChatNotify::get_instance()->get_status();
|
||||
} else {
|
||||
$services['wechat'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Captcha — 唯一真相源 aksrt_captcha_config
|
||||
$captcha = function_exists('aksrt_get_captcha_status_summary')
|
||||
? aksrt_get_captcha_status_summary()
|
||||
: ['enabled' => false, 'provider' => '', 'configured' => false, 'scenes' => []];
|
||||
$services['captcha'] = [
|
||||
'enabled' => !empty($captcha['enabled']) && !empty($captcha['configured']),
|
||||
'provider' => (string) ($captcha['provider'] ?? ''),
|
||||
'configured' => !empty($captcha['configured']),
|
||||
'available' => !empty($captcha['configured']),
|
||||
'verified' => false,
|
||||
'transport' => 'http_api',
|
||||
'missing_fields' => [],
|
||||
'scenes' => $captcha['scenes'] ?? [],
|
||||
];
|
||||
|
||||
// IP Geo
|
||||
if (class_exists('\AKSRT\Extensions\IPGeo')) {
|
||||
$services['ip_geo'] = \AKSRT\Extensions\IPGeo::get_instance()->get_status();
|
||||
} else {
|
||||
$services['ip_geo'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Content Audit
|
||||
if (class_exists('\AKSRT\Extensions\ContentAudit')) {
|
||||
$services['audit'] = \AKSRT\Extensions\ContentAudit::get_instance()->get_status();
|
||||
} else {
|
||||
$services['audit'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Search Push
|
||||
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
||||
$services['push'] = \AKSRT\Extensions\SearchPush::get_instance()->get_status();
|
||||
} else {
|
||||
$services['push'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Docs Mode (compat path is retired from rebuilt mainline)
|
||||
$services['docs_mode'] = [
|
||||
'enabled' => false,
|
||||
'provider' => 'builtin',
|
||||
'configured' => false,
|
||||
'available' => false,
|
||||
'verified' => true,
|
||||
'transport' => 'builtin',
|
||||
'missing_fields' => [],
|
||||
];
|
||||
|
||||
return $services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user