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'], ]); // Docs page stats register_rest_route(self::API_NAMESPACE, '/docs/stats', [ 'methods' => 'GET', 'callback' => [$this, 'get_docs_stats'], 'permission_callback' => [$this, 'check_permission'], ]); // Docs search (public, for frontend AJAX) register_rest_route(self::API_NAMESPACE, '/docs/search', [ 'methods' => 'GET', 'callback' => [$this, 'search_docs'], 'permission_callback' => '__return_true', ]); // 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 { $nav_count = 0; $special_count = 0; $nav_query = new WP_Query([ 'post_type' => 'page', 'post_status' => 'publish', 'meta_key' => '_wp_page_template', 'meta_value' => 'templates/pages/page-documentnav.php', 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => false, ]); $nav_count = (int) $nav_query->found_posts; $special_query = new WP_Query([ 'post_type' => 'page', 'post_status' => 'publish', 'meta_key' => '_wp_page_template', 'meta_value' => 'templates/pages/page-special.php', 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => false, ]); $special_count = (int) $special_query->found_posts; return $this->success([ 'data' => [ 'total' => $nav_count + $special_count, 'nav_count' => $nav_count, 'special_count' => $special_count, ], ]); } /** * GET /docs/search — search document pages by keyword. */ public function search_docs(WP_REST_Request $request): WP_REST_Response { $keyword = trim(sanitize_text_field((string) $request->get_param('keyword'))); $root_id = (int) $request->get_param('root_id'); $limit = max(1, min(50, (int) ($request->get_param('limit') ?: 20))); if ($keyword === '') { return $this->success(['data' => ['results' => [], 'total' => 0]]); } if (!class_exists('AKSRT_Docs_Presenter')) { return $this->success(['data' => ['results' => [], 'total' => 0]]); } $results = AKSRT_Docs_Presenter::get_instance()->search_pages($keyword, $root_id, $limit); return $this->success([ 'data' => [ 'results' => $results, 'total' => count($results), ], ]); } /** * 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. */ 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); } $result = AKSRT_IP_Geo::get_instance()->lookup($ip); return $this->success([ 'data' => [ 'ip' => $ip, 'result' => $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 $docs_en = !empty($settings['docs_mode_enabled']); $services['docs_mode'] = [ 'enabled' => $docs_en, 'provider' => 'builtin', 'configured' => $docs_en, 'available' => $docs_en, 'verified' => true, 'transport' => 'builtin', 'missing_fields' => [], ]; return $services; } }