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_summary = function_exists('aksrt_get_captcha_status_summary') ? aksrt_get_captcha_status_summary() : ['enabled' => false, 'configured' => false, 'scenes' => []]; $captcha_comment_enabled = !empty($captcha_summary['scenes']['comment']); $features = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_cms_features() : []; $captcha_download_enabled = !empty($features['download_captcha']) && !empty($captcha_summary['scenes']['download']); $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' => !empty($captcha_summary['enabled']) && !empty($captcha_summary['configured']), 'captcha_summary' => $captcha_summary, '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. * * 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_Content_Audit')) { return $this->error(__('内容审核服务不可用', 'aksrt'), 500); } $audit = AKSRT_Content_Audit::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')) { $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 — 唯一真相源 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_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; } }