440 lines
16 KiB
PHP
440 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* AKSRT Admin — Extension Controller
|
|
*
|
|
* Owns REST routes for the "扩展&增强" domain.
|
|
* Aggregates real service status from SMS, WeChat, Captcha, IP Geo, Content Audit, Search Push.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class AKSRT_Admin_Extension_Controller extends AKSRT_Admin_Controller_Base {
|
|
|
|
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(AKSRT_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'] ?? '');
|
|
}
|
|
|
|
// 保留旧布尔字段兼容
|
|
$flat['sms_enabled'] = !empty($services['sms']['enabled']);
|
|
$flat['wechat_enabled'] = !empty($services['wechat']['enabled']);
|
|
$flat['captcha_enabled'] = !empty($services['captcha']['enabled']);
|
|
$flat['baidu_push_enabled'] = !empty($services['push']['enabled']);
|
|
$flat['content_audit_enabled'] = !empty($services['audit']['enabled']);
|
|
|
|
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::get_all();
|
|
|
|
$email_gate_enabled = true;
|
|
if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) {
|
|
$email_gate_enabled = false;
|
|
}
|
|
|
|
$captcha_comment_enabled = !empty($settings['captcha_comment_enabled']);
|
|
$captcha_download_enabled = !empty($settings['captcha_download_enabled']);
|
|
|
|
$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_comment_enabled' => $captcha_comment_enabled,
|
|
'captcha_download_enabled' => $captcha_download_enabled,
|
|
'content_audit_enabled' => !empty($services['audit']['enabled']),
|
|
'content_audit_configured' => !empty($services['audit']['configured']),
|
|
'ip_geo_enabled' => !empty($services['ip_geo']['enabled']),
|
|
'ip_geo_configured' => !empty($services['ip_geo']['configured']),
|
|
'captcha_enabled' => $captcha_comment_enabled || $captcha_download_enabled,
|
|
'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::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_IP_Geo')) {
|
|
return $this->error(__('IP 归属地服务不可用', 'aksrt'), 500);
|
|
}
|
|
|
|
$geo = AKSRT_IP_Geo::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']);
|
|
|
|
if (!$ok) {
|
|
switch ($reason) {
|
|
case 'private_ip':
|
|
$message = __('该地址为私有/保留 IP,无法查询归属地。请换公网 IP 测试。', 'aksrt');
|
|
break;
|
|
case 'provider_empty':
|
|
if (!empty($status['missing_fields'])) {
|
|
$message = sprintf(
|
|
/* translators: %s: missing config field keys */
|
|
__('当前服务商凭据不完整(缺少:%s),请先在「扩展 → IP归属地」补全配置。', 'aksrt'),
|
|
implode(', ', (array) $status['missing_fields'])
|
|
);
|
|
} else {
|
|
$message = __('服务商未返回有效归属地,请检查凭据、网络出口或换一个 IP 重试。', 'aksrt');
|
|
}
|
|
break;
|
|
case 'disabled':
|
|
$message = __('IP 归属地功能未启用。', 'aksrt');
|
|
break;
|
|
default:
|
|
$message = __('未查询到归属地信息。', '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.
|
|
*/
|
|
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_Content_Audit')) {
|
|
return $this->error(__('内容审核服务不可用', 'aksrt'), 500);
|
|
}
|
|
|
|
$result = AKSRT_Content_Audit::get_instance()->audit_text($text);
|
|
|
|
return $this->success([
|
|
'data' => [
|
|
'text' => $text,
|
|
'result' => $result,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 收集所有扩展服务的运行时状态
|
|
*/
|
|
private function collect_service_statuses(): array {
|
|
$services = [];
|
|
|
|
// SMS
|
|
if (class_exists('AKSRT_SMS')) {
|
|
$services['sms'] = AKSRT_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_WeChat_Notify')) {
|
|
$services['wechat'] = AKSRT_WeChat_Notify::get_instance()->get_status();
|
|
} else {
|
|
$services['wechat'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
|
}
|
|
|
|
// Captcha
|
|
$settings = AKSRT_Settings::get_all();
|
|
$captcha_en = class_exists('AKSRT_Captcha') && !empty($settings['captcha_enabled']);
|
|
$services['captcha'] = [
|
|
'enabled' => $captcha_en,
|
|
'provider' => sanitize_key((string) ($settings['captcha_provider'] ?? '')),
|
|
'configured' => $captcha_en,
|
|
'available' => $captcha_en,
|
|
'verified' => false,
|
|
'transport' => 'http_api',
|
|
'missing_fields' => [],
|
|
];
|
|
|
|
// IP Geo
|
|
if (class_exists('AKSRT_IP_Geo')) {
|
|
$services['ip_geo'] = AKSRT_IP_Geo::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_Content_Audit')) {
|
|
$services['audit'] = AKSRT_Content_Audit::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_Search_Push')) {
|
|
$services['push'] = AKSRT_Search_Push::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;
|
|
}
|
|
}
|