\WP_REST_Server::READABLE, 'callback' => [$this, 'list_comments'], 'permission_callback' => '__return_true', 'args' => [ 'post_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'], 'page' => ['required' => false, 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint'], 'order' => ['required' => false, 'type' => 'string', 'default' => 'newest', 'sanitize_callback' => 'sanitize_key'], 'author_only' => ['required' => false, 'type' => 'boolean', 'default' => false], ], ]); register_rest_route('aksrt/v1', '/comment/submit', [ 'methods' => 'POST', 'callback' => [$this, 'submit_comment'], 'permission_callback' => '__return_true', ]); register_rest_route('aksrt/v1', '/comment/edit', [ 'methods' => 'POST', 'callback' => [$this, 'edit_comment'], 'permission_callback' => [$this, 'check_edit_permission'], 'args' => [ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'], 'content' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_textarea_field'], ], ]); register_rest_route('aksrt/v1', '/comment/delete', [ 'methods' => 'POST', 'callback' => [$this, 'delete_comment'], 'permission_callback' => [$this, 'check_delete_permission'], 'args' => [ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'], ], ]); register_rest_route('aksrt/v1', '/comment/moderate', [ 'methods' => 'POST', 'callback' => [$this, 'moderate_comment'], 'permission_callback' => [$this, 'check_moderate_permission'], 'args' => [ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'], 'status' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'], ], ]); register_rest_route('aksrt/v1', '/comment/pin', [ 'methods' => 'POST', 'callback' => [$this, 'pin_comment'], 'permission_callback' => [$this, 'check_pin_permission'], 'args' => [ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'], 'pinned' => ['required' => false, 'type' => 'boolean'], ], ]); register_rest_route('aksrt/v1', '/comment/like', [ 'methods' => 'POST', 'callback' => [$this, 'like_comment'], // Guests may like; abuse is bounded by per-visitor toggle state. 'permission_callback' => '__return_true', 'args' => [ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'], 'active' => ['required' => false, 'type' => 'boolean'], ], ]); register_rest_route('aksrt/v1', '/comment/get', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [$this, 'get_comment'], 'permission_callback' => [$this, 'check_get_permission'], 'args' => [ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'], ], ]); } /** * Permission: can load raw content for edit? */ public function check_get_permission(\WP_REST_Request $request): bool { $comment_id = (int) $request->get_param('comment_id'); if ($comment_id <= 0 || !is_user_logged_in()) { return false; } $perms = $this->permissions($comment_id); return !empty($perms['can_edit']); } /** * Return raw comment content for moveForm edit. */ public function get_comment(\WP_REST_Request $request): \WP_REST_Response { $comment_id = (int) $request->get_param('comment_id'); $comment = get_comment($comment_id); if (!$comment) { return new \WP_REST_Response([ 'success' => false, 'code' => 'comment_not_found', 'message' => __('评论不存在。', 'aksrt'), ], 404); } return new \WP_REST_Response([ 'success' => true, 'content' => (string) $comment->comment_content, 'data' => [ 'content' => (string) $comment->comment_content, 'comment_id' => $comment_id, 'parent' => (int) $comment->comment_parent, ], ]); } /** * Resolve the per-comment action contract (single source of truth). * * @param int $comment_id Comment ID. * @return array{can_edit:bool,can_delete:bool,can_moderate:bool,can_pin:bool} */ private function permissions(int $comment_id): array { if (function_exists('aksrt_comment_action_permissions')) { return aksrt_comment_action_permissions($comment_id); } // Defensive fallback if the helper is unavailable. $comment = get_comment($comment_id); $is_mod = current_user_can('moderate_comments'); $is_owner = $comment && is_user_logged_in() && (int) $comment->user_id === get_current_user_id(); return [ 'can_edit' => $is_mod || $is_owner, 'can_delete' => $is_mod || $is_owner, 'can_moderate' => $is_mod, 'can_pin' => $is_mod, ]; } /** * Permission: can edit? (author with 允许用户编辑评论 on, or moderator) */ public function check_edit_permission(\WP_REST_Request $request): bool { if (!is_user_logged_in()) return false; return $this->permissions((int) $request->get_param('comment_id'))['can_edit']; } /** * Permission: can delete? (author, or moderator) */ public function check_delete_permission(\WP_REST_Request $request): bool { if (!is_user_logged_in()) return false; return $this->permissions((int) $request->get_param('comment_id'))['can_delete']; } /** * Permission: can pin? (moderator, top-level only) */ public function check_pin_permission(\WP_REST_Request $request): bool { if (!is_user_logged_in()) return false; return $this->permissions((int) $request->get_param('comment_id'))['can_pin']; } /** * Fragment list for order / author / page without full HTML scrape. */ public function list_comments(\WP_REST_Request $request): \WP_REST_Response { $post_id = (int) $request->get_param('post_id'); $post = get_post($post_id); if (!$post) { return new \WP_REST_Response([ 'success' => false, 'code' => 'post_not_found', 'message' => __('文章不存在。', 'aksrt'), ], 404); } if (!function_exists('aksrt_comments_render_list_fragment')) { return new \WP_REST_Response([ 'success' => false, 'code' => 'fragment_unavailable', 'message' => __('评论片段渲染不可用。', 'aksrt'), ], 500); } $order = sanitize_key((string) ($request->get_param('order') ?: 'newest')); $author_only = (bool) $request->get_param('author_only'); $page = max(1, (int) ($request->get_param('page') ?: 1)); $fragment = aksrt_comments_render_list_fragment($post_id, [ 'order' => $order, 'author_only' => $author_only, 'page' => $page, ]); return new \WP_REST_Response([ 'success' => true, 'data' => $fragment, ]); } /** * AJAX submit with HTML item for local insert. * Runs through native WP comment pipeline (preprocess_comment / moderation). */ public function submit_comment(\WP_REST_Request $request): \WP_REST_Response { $params = $request->get_json_params(); if (!is_array($params) || empty($params)) { $params = $request->get_params(); } if (!is_array($params)) { $params = []; } $edit_id = absint($params['edit_comment_ID'] ?? $params['edit_comment_id'] ?? 0); if ($edit_id > 0) { $request->set_param('comment_id', $edit_id); $request->set_param('content', (string) ($params['comment'] ?? $params['content'] ?? '')); return $this->edit_comment($request); } $post_id = absint($params['comment_post_ID'] ?? $params['post_id'] ?? 0); $content = isset($params['comment']) ? (string) $params['comment'] : ''; $parent = absint($params['comment_parent'] ?? 0); if ($post_id <= 0 || trim(wp_strip_all_tags($content)) === '') { return new \WP_REST_Response([ 'success' => false, 'code' => 'invalid_payload', 'message' => __('请输入评论内容。', 'aksrt'), ], 400); } $post = get_post($post_id); if (!$post || !comments_open($post_id)) { return new \WP_REST_Response([ 'success' => false, 'code' => 'comments_closed', 'message' => __('评论已关闭。', 'aksrt'), ], 403); } $user_id = get_current_user_id(); if (!function_exists('aksrt_user_can') || !aksrt_user_can('comment_add', $user_id, $post_id)) { return new \WP_REST_Response([ 'success' => false, 'code' => 'comment_forbidden', 'message' => __('当前账号没有发表评论的权限。', 'aksrt'), ], $user_id > 0 ? 403 : 401); } $nonce = (string) ($params['_aksrt_comment_nonce'] ?? $params['_wpnonce'] ?? ''); if ($nonce !== '' && !wp_verify_nonce($nonce, 'aksrt_comment_nonce') && !wp_verify_nonce($nonce, 'wp_rest')) { return new \WP_REST_Response([ 'success' => false, 'code' => 'invalid_nonce', 'message' => __('安全校验失败,请刷新后重试。', 'aksrt'), ], 403); } $captcha_result = aksrt_require_captcha('comment', $params); if ($captcha_result !== true) { $captcha_error = is_wp_error($captcha_result) ? $captcha_result : null; $captcha_data = $captcha_error ? $captcha_error->get_error_data() : null; $captcha_status = is_array($captcha_data) ? (int) ($captcha_data['status'] ?? 400) : 400; return new \WP_REST_Response([ 'success' => false, 'code' => $captcha_error ? ($captcha_error->get_error_code() ?: 'captcha_failed') : 'captcha_failed', 'message' => $captcha_error ? ($captcha_error->get_error_message() ?: __('人机验证失败', 'aksrt')) : __('人机验证失败', 'aksrt'), ], $captcha_status > 0 ? $captcha_status : 400); } $user = wp_get_current_user(); $commentdata = [ 'comment_post_ID' => $post_id, 'comment_content' => $content, 'comment_parent' => $parent, 'comment_type' => '', // Prefer real client IP behind CDN/proxy (REMOTE_ADDR alone is often edge IP). 'comment_author_IP' => function_exists('aksrt_get_client_ip') ? (string) aksrt_get_client_ip() : (isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash((string) $_SERVER['REMOTE_ADDR'])) : ''), 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash((string) $_SERVER['HTTP_USER_AGENT'])) : '', 'comment_date' => current_time('mysql'), 'comment_date_gmt' => current_time('mysql', true), ]; if ($user && $user->ID > 0) { $commentdata['user_id'] = (int) $user->ID; $commentdata['comment_author'] = $user->display_name ?: $user->user_login; $commentdata['comment_author_email'] = $user->user_email; $commentdata['comment_author_url'] = $user->user_url; } else { $commentdata['user_id'] = 0; $commentdata['comment_author'] = sanitize_text_field((string) ($params['author'] ?? '')); $commentdata['comment_author_email'] = sanitize_email((string) ($params['email'] ?? '')); $commentdata['comment_author_url'] = esc_url_raw((string) ($params['url'] ?? '')); if ($commentdata['comment_author'] === '' || $commentdata['comment_author_email'] === '') { return new \WP_REST_Response([ 'success' => false, 'code' => 'guest_fields_required', 'message' => __('请填写昵称和邮箱。', 'aksrt'), ], 400); } } foreach (['author', 'email', 'url', 'comment', 'comment_post_ID', 'comment_parent', '_aksrt_comment_nonce'] as $k) { if (isset($params[$k])) { $_POST[$k] = $params[$k]; } } $comment_id = null; if (class_exists('\AKSRT\Auth\Moderation')) { \AKSRT\Auth\Moderation::get_instance()->begin_captcha_verified_comment_request(); } try { $comment_id = wp_new_comment(wp_slash($commentdata), true); } finally { if (class_exists('\AKSRT\Auth\Moderation')) { \AKSRT\Auth\Moderation::get_instance()->end_captcha_verified_comment_request(); } } if (is_wp_error($comment_id)) { return new \WP_REST_Response([ 'success' => false, 'code' => $comment_id->get_error_code() ?: 'submit_failed', 'message' => $comment_id->get_error_message() ?: __('评论提交失败。', 'aksrt'), ], 400); } $comment = get_comment((int) $comment_id); if (!$comment) { return new \WP_REST_Response([ 'success' => false, 'code' => 'submit_failed', 'message' => __('评论提交失败。', 'aksrt'), ], 500); } $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_comment_display_settings() : []; $html = function_exists('aksrt_comments_render_single_html') ? aksrt_comments_render_single_html($comment, $settings) : ''; $needs_moderation = '0' === (string) $comment->comment_approved; $wait_time = 0; return new \WP_REST_Response([ 'success' => true, 'code' => 'submitted', 'message' => $needs_moderation ? __('评论已提交,等待管理员审核', 'aksrt') : __('评论发布成功', 'aksrt'), 'html' => $html, 'is_reply'=> $parent > 0, 'parent_id' => $parent, 'needs_moderation' => $needs_moderation, 'wait_time' => $wait_time, 'data' => [ 'comment_id' => (int) $comment_id, 'approved' => (string) $comment->comment_approved, ], ]); } /** * Edit comment content */ public function edit_comment(\WP_REST_Request $request): \WP_REST_Response { $comment_id = $request->get_param('comment_id'); $content = $request->get_param('content'); $comment = get_comment($comment_id); if (!$comment) { return new \WP_REST_Response([ 'success' => false, 'code' => 'comment_not_found', 'message' => __('评论不存在。', 'aksrt'), ], 404); } // Sanitize with wp_kses (allow basic HTML) $allowed_html = [ 'a' => ['href' => [], 'title' => [], 'target' => []], 'code' => [], 'pre' => [], 'strong' => [], 'em' => [], 'br' => [], 'p' => [], ]; $clean_content = wp_kses($content, $allowed_html); $updated = wp_update_comment([ 'comment_ID' => $comment_id, 'comment_content' => $clean_content, ]); if (is_wp_error($updated)) { return new \WP_REST_Response([ 'success' => false, 'code' => 'update_failed', 'message' => __('更新失败。', 'aksrt'), ], 500); } $rendered = function_exists('aksrt_render_comment_content') ? aksrt_render_comment_content($clean_content) : wpautop($clean_content); return new \WP_REST_Response([ 'success' => true, 'code' => 'updated', 'message' => __('评论已更新。', 'aksrt'), 'data' => [ 'content' => $rendered, ], ]); } /** * Delete comment */ public function delete_comment(\WP_REST_Request $request): \WP_REST_Response { $comment_id = $request->get_param('comment_id'); $comment = get_comment($comment_id); if (!$comment) { return new \WP_REST_Response([ 'success' => false, 'code' => 'comment_not_found', 'message' => __('评论不存在。', 'aksrt'), ], 404); } $deleted = wp_delete_comment($comment_id, false); // soft delete if (!$deleted) { return new \WP_REST_Response([ 'success' => false, 'code' => 'delete_failed', 'message' => __('删除失败。', 'aksrt'), ], 500); } return new \WP_REST_Response([ 'success' => true, 'code' => 'deleted', 'message' => __('评论已删除。', 'aksrt'), ]); } /** * Moderate comment (approve / hold / spam) */ public function moderate_comment(\WP_REST_Request $request): \WP_REST_Response { $comment_id = $request->get_param('comment_id'); $status = $request->get_param('status'); $allowed_statuses = ['approve', 'hold', 'spam']; if (!in_array($status, $allowed_statuses, true)) { return new \WP_REST_Response([ 'success' => false, 'code' => 'invalid_status', 'message' => __('无效状态。', 'aksrt'), ], 400); } $comment = get_comment($comment_id); if (!$comment) { return new \WP_REST_Response([ 'success' => false, 'code' => 'comment_not_found', 'message' => __('评论不存在。', 'aksrt'), ], 404); } $status_map = [ 'approve' => 'approved', 'hold' => 'hold', 'spam' => 'spam', ]; $result = wp_set_comment_status($comment_id, $status_map[$status]); if (is_wp_error($result)) { return new \WP_REST_Response([ 'success' => false, 'code' => 'moderate_failed', 'message' => __('操作失败。', 'aksrt'), ], 500); } return new \WP_REST_Response([ 'success' => true, 'code' => 'moderated', 'message' => __('操作成功。', 'aksrt'), 'data' => [ 'status' => $status_map[$status], ], ]); } /** * Toggle a comment's pinned ("置顶") state. Top-level comments only. */ public function pin_comment(\WP_REST_Request $request): \WP_REST_Response { $comment_id = (int) $request->get_param('comment_id'); $comment = get_comment($comment_id); if (!$comment) { return new \WP_REST_Response([ 'success' => false, 'code' => 'comment_not_found', 'message' => __('评论不存在。', 'aksrt'), ], 404); } if ((int) $comment->comment_parent !== 0) { return new \WP_REST_Response([ 'success' => false, 'code' => 'cannot_pin_reply', 'message' => __('仅顶层评论可置顶。', 'aksrt'), ], 400); } $current = '1' === (string) get_comment_meta($comment_id, '_aksrt_comment_pinned', true); $target = null !== $request->get_param('pinned') ? (bool) $request->get_param('pinned') : !$current; if ($target) { update_comment_meta($comment_id, '_aksrt_comment_pinned', '1'); } else { delete_comment_meta($comment_id, '_aksrt_comment_pinned'); } return new \WP_REST_Response([ 'success' => true, 'code' => 'pinned', 'message' => $target ? __('评论已置顶。', 'aksrt') : __('已取消置顶。', 'aksrt'), 'data' => [ 'pinned' => $target, ], ]); } /** * Toggle the current visitor's like on a comment. */ public function like_comment(\WP_REST_Request $request): \WP_REST_Response { $comment_id = (int) $request->get_param('comment_id'); $comment = get_comment($comment_id); if (!$comment || '1' !== (string) $comment->comment_approved) { return new \WP_REST_Response([ 'success' => false, 'code' => 'comment_unavailable', 'message' => __('评论不存在或未通过审核。', 'aksrt'), ], 404); } if (!function_exists('aksrt_is_comment_liked') || !function_exists('aksrt_set_comment_like_state') || !function_exists('aksrt_update_comment_counter')) { return new \WP_REST_Response([ 'success' => false, 'code' => 'like_unavailable', 'message' => __('点赞功能不可用。', 'aksrt'), ], 500); } $current = aksrt_is_comment_liked($comment_id); $target = null !== $request->get_param('active') ? (bool) $request->get_param('active') : !$current; if ($target !== $current) { aksrt_set_comment_like_state($comment_id, $target); $count = aksrt_update_comment_counter($comment_id, 'aksrt_comment_likes', $target ? 1 : -1); if (is_user_logged_in()) { do_action('aksrt_comment_liked', $comment_id, get_current_user_id(), (bool) $target); } } else { $count = aksrt_get_comment_likes($comment_id); } return new \WP_REST_Response([ 'success' => true, 'code' => 'liked', 'data' => [ 'active' => $target, 'count' => $count, ], ]); } }