['GET', 'POST'], 'callback' => [$this, 'rest_handle_mail'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route(self::API_NAMESPACE, '/mail/test', [ 'methods' => 'POST', 'callback' => [$this, 'rest_test_mail'], 'permission_callback' => [$this, 'check_permission'], ]); // Footer register_rest_route(self::API_NAMESPACE, '/settings/footer', [ 'methods' => ['GET', 'POST'], 'callback' => [$this, 'rest_handle_footer_settings'], 'permission_callback' => [$this, 'check_permission'], ]); // Footer Tabbar register_rest_route(self::API_NAMESPACE, '/settings/footer-tabbar', [ 'methods' => ['GET', 'POST'], 'callback' => [$this, 'rest_handle_footer_tabbar'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route(self::API_NAMESPACE, '/settings/footer-tabbar/reorder', [ 'methods' => 'POST', 'callback' => [$this, 'rest_reorder_footer_tabbar'], 'permission_callback' => [$this, 'check_permission'], ]); // Floating Buttons register_rest_route(self::API_NAMESPACE, '/settings/floating-buttons', [ 'methods' => ['GET', 'POST'], 'callback' => [$this, 'rest_handle_floating_buttons'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route(self::API_NAMESPACE, '/settings/floating-buttons/reorder', [ 'methods' => 'POST', 'callback' => [$this, 'rest_reorder_floating_buttons'], 'permission_callback' => [$this, 'check_permission'], ]); // Friend Links register_rest_route(self::API_NAMESPACE, '/friend-links', [ 'methods' => ['GET', 'POST'], 'callback' => [$this, 'rest_handle_friend_links'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route(self::API_NAMESPACE, '/friend-links/(?P\d+)', [ 'methods' => 'DELETE', 'callback' => [$this, 'rest_delete_friend_link'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route(self::API_NAMESPACE, '/friend-links/reorder', [ 'methods' => 'POST', 'callback' => [$this, 'rest_reorder_friend_links'], 'permission_callback' => [$this, 'check_permission'], ]); // CMS Features register_rest_route(self::API_NAMESPACE, '/settings/cms-features', [ 'methods' => ['GET', 'POST'], 'callback' => [$this, 'rest_handle_cms_features'], 'permission_callback' => [$this, 'check_permission'], ]); // Homepage settings register_rest_route(self::API_NAMESPACE, '/settings/homepage', [ 'methods' => ['GET', 'POST'], 'callback' => [$this, 'rest_handle_homepage_settings'], 'permission_callback' => [$this, 'check_permission'], ]); // Category recommend register_rest_route(self::API_NAMESPACE, '/settings/category-recommend', [ 'methods' => ['GET', 'POST'], 'callback' => [$this, 'rest_handle_category_recommend'], 'permission_callback' => [$this, 'check_permission'], ]); // Categories list (for admin selectors) register_rest_route(self::API_NAMESPACE, '/categories', [ 'methods' => 'GET', 'callback' => [$this, 'rest_get_categories'], 'permission_callback' => [$this, 'check_permission'], ]); // Category term meta register_rest_route(self::API_NAMESPACE, '/categories/(?P\d+)/meta', [ 'methods' => 'POST', 'callback' => [$this, 'rest_update_category_meta'], 'permission_callback' => [$this, 'check_permission'], ]); // Homepage tab posts (public) register_rest_route(self::API_NAMESPACE, '/homepage/tab-posts', [ 'methods' => 'GET', 'callback' => [$this, 'rest_homepage_tab_posts'], 'permission_callback' => '__return_true', 'args' => [ 'term_id' => ['type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint'], 'page' => ['type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint'], 'per_page' => ['type' => 'integer', 'default' => 10, 'sanitize_callback' => 'absint'], 'sort' => ['type' => 'string', 'default' => 'modified', 'sanitize_callback' => 'sanitize_text_field'], 'view_mode' => ['type' => 'string', 'default' => 'list', 'sanitize_callback' => 'sanitize_text_field'], 'page_url' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'esc_url_raw'], 'panel' => ['type' => 'string', 'default' => 'latest', 'sanitize_callback' => 'sanitize_key'], 'source' => ['type' => 'string', 'default' => 'home', 'sanitize_callback' => 'sanitize_key'], 'tab_orderby' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key'], ], ]); // Archive AJAX filter (public) — §2.5/§2.6/§2.7 register_rest_route(self::API_NAMESPACE, '/archive/posts', [ 'methods' => 'GET', 'callback' => [$this, 'rest_archive_posts'], 'permission_callback' => '__return_true', 'args' => [ 'archive_type' => ['type' => 'string', 'default' => 'category', 'sanitize_callback' => 'sanitize_key'], 'term_id' => ['type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint'], 'filter_cat' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'], 'filter_tag' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'], 'sort' => ['type' => 'string', 'default' => 'date', 'sanitize_callback' => 'sanitize_key'], 'order' => ['type' => 'string', 'default' => 'DESC', 'sanitize_callback' => 'sanitize_key'], 'page' => ['type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint'], 'per_page' => ['type' => 'integer', 'default' => 10, 'sanitize_callback' => 'absint'], 'custom_filters' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'], ], ]); // Homepage categories (for admin tab editor) register_rest_route(self::API_NAMESPACE, '/homepage/categories', [ 'methods' => 'GET', 'callback' => [$this, 'rest_homepage_categories'], 'permission_callback' => [$this, 'check_permission'], ]); // User search (for admin user selectors) register_rest_route(self::API_NAMESPACE, '/users/search', [ 'methods' => 'GET', 'callback' => [$this, 'rest_search_users'], 'permission_callback' => [$this, 'check_permission'], 'args' => [ 'search' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'], 'number' => ['type' => 'integer', 'default' => 20, 'sanitize_callback' => 'absint'], ], ]); } public function rest_handle_cms_features(WP_REST_Request $request): WP_REST_Response { if ($request->get_method() === 'GET') { return $this->success(['cms_features' => AKSRT_Settings::get_cms_features()]); } $params = $this->get_json_params($request); $allowed = [ 'resource_cpt_enabled' => 'bool', 'download_login' => 'bool', 'download_credit' => 'bool', 'download_captcha' => 'bool', 'search_include_resource' => 'bool', ]; $sanitized = $this->sanitize_fields($params, $allowed); $settings = AKSRT_Settings::get_all(); $settings['cms_features'] = array_merge( is_array($settings['cms_features'] ?? null) ? $settings['cms_features'] : [], $sanitized ); update_option('aksrt_settings', $settings); AKSRT_Settings::flush_settings_cache(); return $this->success(['message' => 'CMS 功能已保存', 'cms_features' => AKSRT_Settings::get_cms_features()]); } public function rest_handle_mail(WP_REST_Request $request): WP_REST_Response { $option_key = 'aksrt_mail_config'; if ($request->get_method() === 'GET') { $config = get_option($option_key, [ 'smtp_host' => '', 'smtp_port' => 465, 'smtp_user' => '', 'smtp_pass' => '', 'smtp_secure' => 'ssl', 'from_email' => '', 'from_name' => '', ]); return $this->success(['data' => $config, 'code' => 'success']); } $params = $this->get_json_params($request); $current = get_option($option_key, []); $merged = array_merge(is_array($current) ? $current : [], $params); update_option($option_key, $merged); return $this->success(['message' => '邮件配置已保存', 'code' => 'success']); } public function rest_test_mail(WP_REST_Request $request): WP_REST_Response { $params = $this->get_json_params($request); $to = sanitize_email($params['to'] ?? ''); if (!is_email($to)) { return $this->error(__('请输入有效的邮箱地址', 'aksrt')); } $sent = wp_mail($to, __('AKSRT 邮件测试', 'aksrt'), __('如果你收到这封邮件,说明 SMTP 配置正确。', 'aksrt')); if ($sent) { return $this->success(['message' => '测试邮件已发送']); } return $this->error(__('邮件发送失败,请检查 SMTP 配置', 'aksrt')); } public function rest_homepage_tab_posts(WP_REST_Request $request): WP_REST_Response { $term_id = (int) $request->get_param('term_id'); $page = max(1, (int) $request->get_param('page')); $per_page = min(50, max(1, (int) $request->get_param('per_page'))); $sort = sanitize_key($request->get_param('sort') ?: 'modified'); $view_mode = sanitize_key($request->get_param('view_mode') ?: 'list'); $tab_orderby = sanitize_key($request->get_param('tab_orderby') ?: ''); $panel = sanitize_key($request->get_param('panel') ?: 'latest'); $post_types = function_exists('aksrt_homepage_get_post_types') ? aksrt_homepage_get_post_types() : ['post']; $args = [ 'post_type' => $post_types, 'post_status' => 'publish', 'posts_per_page' => $per_page, 'paged' => $page, 'ignore_sticky_posts' => $term_id > 0, ]; if ($term_id > 0) { $args['cat'] = $term_id; } $exclude_ids = class_exists('AKSRT_Settings') ? AKSRT_Settings::home_exclude_post_ids() : []; if (!empty($exclude_ids)) { $args['post__not_in'] = $exclude_ids; } if ($panel === 'latest' || $term_id <= 0) { $exclude_cat_ids = class_exists('AKSRT_Settings') ? AKSRT_Settings::home_exclude_category_ids() : []; if (!empty($exclude_cat_ids)) { $args['category__not_in'] = $exclude_cat_ids; } } $effective_sort = $tab_orderby !== '' ? $tab_orderby : $sort; if (function_exists('aksrt_homepage_apply_named_sort_args')) { $args = aksrt_homepage_apply_named_sort_args($args, $effective_sort); } else { $args['orderby'] = 'date'; $args['order'] = 'DESC'; } $query = new WP_Query($args); $mode = in_array($view_mode, ['card', 'list'], true) ? $view_mode : 'list'; $html = ''; if (function_exists('aksrt_render_post_list')) { $html = aksrt_render_post_list($query, [ 'mode' => $mode, 'context' => 'home', ], false); } elseif ($query->have_posts()) { $html = '
'; while ($query->have_posts()) { $query->the_post(); if (function_exists('aksrt_homepage_render_post_card')) { ob_start(); aksrt_homepage_render_post_card(); $html .= ob_get_clean(); } } $html .= '
'; wp_reset_postdata(); } $has_more = $page < (int) $query->max_num_pages; return $this->success([ 'html' => $html, 'total' => (int) $query->found_posts, 'has_more' => $has_more, ]); } public function rest_archive_posts(WP_REST_Request $request): WP_REST_Response { return $this->rest_homepage_tab_posts($request); } public function rest_homepage_categories(): WP_REST_Response { $cats = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']); $data = []; foreach ($cats as $cat) { $data[] = ['id' => $cat->term_id, 'name' => $cat->name, 'slug' => $cat->slug, 'count' => $cat->count, 'parent' => $cat->parent]; } return $this->success(['data' => $data]); } public function rest_handle_friend_links(WP_REST_Request $request): WP_REST_Response { if ($request->get_method() === 'GET') { $links = aksrt_get_friend_links(); return rest_ensure_response(['code' => 'success', 'data' => $links]); } $params = $this->get_json_params($request); $links = get_option('aksrt_friend_links', []); $new_link = [ 'id' => uniqid('fl_'), 'name' => sanitize_text_field($params['name'] ?? ''), 'url' => esc_url_raw($params['url'] ?? ''), 'logo' => esc_url_raw($params['logo'] ?? ''), 'description' => sanitize_textarea_field($params['description'] ?? ''), 'sort_order' => (int) ($params['sort_order'] ?? 0), 'active' => 1, ]; $links[] = $new_link; usort($links, function ($a, $b) { return ($a['sort_order'] ?? 0) <=> ($b['sort_order'] ?? 0); }); update_option('aksrt_friend_links', $links); return rest_ensure_response(['code' => 'success', 'data' => $new_link, 'message' => __('Friend link added.', 'aksrt')]); } public function rest_delete_friend_link(WP_REST_Request $request): WP_REST_Response { $id = $request->get_param('id'); $links = get_option('aksrt_friend_links', []); $links = array_filter($links, function ($link) use ($id) { return ($link['id'] ?? '') !== $id; }); $links = array_values($links); update_option('aksrt_friend_links', $links); return rest_ensure_response(['code' => 'success', 'message' => __('Friend link deleted.', 'aksrt')]); } public function rest_reorder_friend_links(WP_REST_Request $request): WP_REST_Response { $ordered_ids = $request->get_param('ids'); if (!is_array($ordered_ids)) { return new WP_REST_Response(['code' => 'error', 'message' => __('参数错误', 'aksrt')], 400); } $links = get_option('aksrt_friend_links', []); $order_map = array_flip($ordered_ids); usort($links, function ($a, $b) use ($order_map) { $idx_a = $order_map[($a['id'] ?? '')] ?? 999; $idx_b = $order_map[($b['id'] ?? '')] ?? 999; return $idx_a <=> $idx_b; }); foreach ($links as $index => &$link) { $link['sort_order'] = $index; } update_option('aksrt_friend_links', $links); return rest_ensure_response(['code' => 'success', 'message' => __('Order saved.', 'aksrt')]); } public function rest_handle_footer_settings(WP_REST_Request $request): WP_REST_Response { if ($request->get_method() === 'GET') { $footer = get_option('aksrt_footer_settings', []); $defaults = [ 'contacts' => [], 'quick_links' => [], 'more_links' => [], 'hot_categories' => 5, 'copyright_text' => '', 'layout_template' => 'default', 'footer_t1_m_s' => ['pc', 'mobile'], 'footer_t1_img' => '', 'footer_t1_img_dark' => '', 'footer_t1_t' => '', 'fcode_t1_code' => '', 'footer_contact_m_s' => ['pc', 'mobile'], 'footer_contact_wechat_img' => '', 'footer_contact_qq' => '', 'footer_contact_weibo' => '', 'footer_contact_email' => '', 'footer_contact_more' => '', 'fcode_t2_code_1' => '', 'fcode_t2_code_2' => '', 'footer_mini_img_m_s' => ['pc', 'mobile'], 'footer_mini_img' => [], 'fcode_customize_code' => '', ]; $data = array_merge($defaults, $footer); return rest_ensure_response(['code' => 'success', 'data' => $data]); } $params = $this->get_json_params($request); $footer = [ 'contacts' => isset($params['contacts']) ? $this->sanitize_contacts($params['contacts']) : [], 'quick_links' => isset($params['quick_links']) ? $this->sanitize_link_list($params['quick_links']) : [], 'more_links' => isset($params['more_links']) ? $this->sanitize_link_list($params['more_links']) : [], 'hot_categories' => (int) ($params['hot_categories'] ?? 5), 'copyright_text' => sanitize_textarea_field($params['copyright_text'] ?? ''), 'layout_template' => in_array($params['layout_template'] ?? '', ['default', 'compact', 'centered'], true) ? $params['layout_template'] : 'default', 'footer_t1_m_s' => $this->sanitize_device_visibility($params['footer_t1_m_s'] ?? ['pc', 'mobile']), 'footer_t1_img' => esc_url_raw($params['footer_t1_img'] ?? ''), 'footer_t1_img_dark' => esc_url_raw($params['footer_t1_img_dark'] ?? ''), 'footer_t1_t' => sanitize_text_field($params['footer_t1_t'] ?? ''), 'fcode_t1_code' => wp_kses_post($params['fcode_t1_code'] ?? ''), 'footer_contact_m_s' => $this->sanitize_device_visibility($params['footer_contact_m_s'] ?? ['pc', 'mobile']), 'footer_contact_wechat_img' => esc_url_raw($params['footer_contact_wechat_img'] ?? ''), 'footer_contact_qq' => sanitize_text_field($params['footer_contact_qq'] ?? ''), 'footer_contact_weibo' => sanitize_text_field($params['footer_contact_weibo'] ?? ''), 'footer_contact_email' => sanitize_email($params['footer_contact_email'] ?? ''), 'footer_contact_more' => isset($params['footer_contact_more']) && is_array($params['footer_contact_more']) ? $this->sanitize_contact_more($params['footer_contact_more']) : [], 'fcode_t2_code_1' => wp_kses_post($params['fcode_t2_code_1'] ?? ''), 'fcode_t2_code_2' => wp_kses_post($params['fcode_t2_code_2'] ?? ''), 'footer_mini_img_m_s' => $this->sanitize_device_visibility($params['footer_mini_img_m_s'] ?? ['pc', 'mobile']), 'footer_mini_img' => isset($params['footer_mini_img']) && is_array($params['footer_mini_img']) ? $this->sanitize_mini_img_group($params['footer_mini_img']) : [], 'fcode_customize_code' => wp_kses_post($params['fcode_customize_code'] ?? ''), ]; update_option('aksrt_footer_settings', $footer); return rest_ensure_response(['code' => 'success', 'message' => __('Footer settings saved.', 'aksrt')]); } private function sanitize_contacts(array $contacts): array { $allowed_icons = ['github', 'x', 'twitter', 'email', 'wechat', 'qq', 'weibo', 'telegram', 'discord', 'bilibili', 'linkedin', 'facebook', 'youtube', 'instagram', 'rss', 'web', 'globe']; $clean = []; foreach ($contacts as $item) { $icon = sanitize_text_field($item['icon'] ?? ''); if (!in_array($icon, $allowed_icons)) { continue; } $clean[] = [ 'icon' => $icon, 'url' => esc_url_raw($item['url'] ?? ''), 'title' => sanitize_text_field($item['title'] ?? ''), 'enabled' => !empty($item['enabled']), ]; } return $clean; } private function sanitize_link_list(array $links): array { $clean = []; foreach ($links as $item) { $clean[] = [ 'id' => isset($item['id']) ? sanitize_text_field($item['id']) : uniqid('link_'), 'name' => sanitize_text_field($item['name'] ?? ''), 'url' => esc_url_raw($item['url'] ?? '#'), 'enabled' => !empty($item['enabled']), 'sort_order' => (int) ($item['sort_order'] ?? 0), ]; } return $clean; } private function sanitize_device_visibility($value): array { $arr = is_array($value) ? $value : [$value]; $allowed = ['pc', 'mobile']; $clean = array_values(array_intersect(array_map('sanitize_key', $arr), $allowed)); return $clean ?: ['pc', 'mobile']; } private function sanitize_contact_more(array $items): array { $clean = []; foreach ($items as $item) { if (!is_array($item)) { continue; } $clean[] = [ 'desc' => sanitize_text_field($item['desc'] ?? ''), 'icon' => sanitize_text_field($item['icon'] ?? ''), 'link' => esc_url_raw($item['link'] ?? ''), 'img' => esc_url_raw($item['img'] ?? ''), ]; } return $clean; } private function sanitize_mini_img_group(array $items): array { $clean = []; foreach ($items as $item) { if (!is_array($item)) { continue; } if (count($clean) >= 4) { break; } $clean[] = [ 'text' => sanitize_text_field($item['text'] ?? ''), 'img' => esc_url_raw($item['img'] ?? ''), ]; } return $clean; } public function rest_handle_footer_tabbar(WP_REST_Request $request): WP_REST_Response { if ($request->get_method() === 'GET') { $settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_footer_tabbar_settings() : ['enabled' => true, 'scroll_hide' => false, 'scroll_down_hide' => true, 'items' => []]; return rest_ensure_response(['code' => 'success', 'data' => $settings]); } $params = $this->get_json_params($request); $items = isset($params['items']) ? array_map([$this, 'sanitize_tabbar_item'], (array) $params['items']) : []; $max_items = max(3, min(7, (int) ($params['max_items'] ?? 5))); if (count($items) > $max_items) { return new WP_REST_Response([ 'code' => 'error', 'message' => sprintf(__('Footer tabbar supports at most %d items.', 'aksrt'), $max_items), ], 400); } $tabbar = [ 'enabled' => !empty($params['enabled']), 'scroll_hide' => !empty($params['scroll_hide']), 'scroll_down_hide' => !empty($params['scroll_down_hide']), 'max_items' => $max_items, 'icon_size' => max(16, min(32, (int) ($params['icon_size'] ?? 22))), 'text_size' => max(8, min(14, (int) ($params['text_size'] ?? 10))), 'show_text' => !empty($params['show_text']), 'active_color' => sanitize_hex_color($params['active_color'] ?? '') ?: '#f04494', 'bg_color' => sanitize_hex_color($params['bg_color'] ?? '') ?: '', 'border' => !empty($params['border']), 'shadow' => !empty($params['shadow']), 'animation' => !empty($params['animation']), 'double_click_home' => !empty($params['double_click_home']), 'badge_dot' => !empty($params['badge_dot']), 'items' => $items, ]; update_option('aksrt_footer_tabbar', $tabbar); if (class_exists('AKSRT_Settings')) { AKSRT_Settings::flush_settings_cache(); } return rest_ensure_response(['code' => 'success', 'message' => __('Footer tabbar settings saved.', 'aksrt')]); } }