Files
wordpress-theme-aksrt/src/AKSRT/Admin/CommentController.php
T
kate522 1e21911b03 fix(ns): root-qualify WP_* classes after PSR-4 move
Bare WP_REST_Server etc. resolve under AKSRT\* namespaces; prefix with
\ for Balance REST and other type hints / constructors.
2026-07-17 15:12:14 +08:00

423 lines
16 KiB
PHP

<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
class CommentController extends ControllerBase {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function register_routes(): void {
// List comments (admin)
register_rest_route(self::API_NAMESPACE, '/comments', [
'methods' => 'GET',
'callback' => [$this, 'list_comments'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
'per_page' => ['default' => 20, 'sanitize_callback' => 'absint'],
'post_id' => ['default' => 0, 'sanitize_callback' => 'absint'],
'status' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
'search' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
],
]);
// Create comment (admin or frontend)
register_rest_route(self::API_NAMESPACE, '/comments', [
'methods' => 'POST',
'callback' => [$this, 'create_comment'],
'permission_callback' => [$this, 'check_logged_in_permission'],
'args' => [
'post_id' => ['required' => true, 'sanitize_callback' => 'absint'],
'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'],
'parent' => ['default' => 0, 'sanitize_callback' => 'absint'],
],
]);
// Update comment status (admin)
register_rest_route(self::API_NAMESPACE, '/comments/(?P<id>\d+)/status', [
'methods' => 'POST',
'callback' => [$this, 'update_comment_status'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'id' => ['required' => true, 'sanitize_callback' => 'absint'],
'status' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'],
],
]);
// Edit comment content (admin)
register_rest_route(self::API_NAMESPACE, '/comments/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'update_comment'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'id' => ['required' => true, 'sanitize_callback' => 'absint'],
'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'],
],
]);
// Delete comment (admin)
register_rest_route(self::API_NAMESPACE, '/comments/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'delete_comment'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'id' => ['required' => true, 'sanitize_callback' => 'absint'],
],
]);
// Bulk action (admin)
register_rest_route(self::API_NAMESPACE, '/comments/bulk', [
'methods' => 'POST',
'callback' => [$this, 'bulk_action'],
'permission_callback' => [$this, 'check_permission'],
]);
// Frontend: list comments for a post (public)
register_rest_route(self::API_NAMESPACE, '/comments/post/(?P<post_id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_post_comments'],
'permission_callback' => '__return_true',
'args' => [
'post_id' => ['required' => true, 'sanitize_callback' => 'absint'],
'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
'per_page' => ['default' => 10, 'sanitize_callback' => 'absint'],
'order' => ['default' => 'DESC', 'sanitize_callback' => 'sanitize_text_field'],
],
]);
}
/**
* GET /comments — List comments with filtering.
*/
public function list_comments(\WP_REST_Request $request): \WP_REST_Response {
$args = [
'number' => min(100, (int) $request->get_param('per_page')),
'offset' => ((int) $request->get_param('page') - 1) * (int) $request->get_param('per_page'),
'orderby' => 'comment_date_gmt',
'order' => 'DESC',
];
$post_id = (int) $request->get_param('post_id');
if ($post_id > 0) {
$args['post_id'] = $post_id;
}
$status = $request->get_param('status');
if ($status !== '') {
$args['status'] = $status;
}
$search = $request->get_param('search');
if ($search !== '') {
$args['search'] = $search;
}
$query = new \WP_Comment_Query($args);
// Batch prefetch to avoid N+1 queries
$user_ids = array_unique(array_filter(array_map(fn($c) => (int) $c->user_id, $query->comments)));
$post_ids = array_unique(array_map(fn($c) => (int) $c->comment_post_ID, $query->comments));
$users_map = [];
$posts_map = [];
if (!empty($user_ids)) {
$users = get_users(['include' => $user_ids, 'fields' => ['ID', 'display_name']]);
foreach ($users as $u) $users_map[$u->ID] = $u;
}
if (!empty($post_ids)) {
global $wpdb;
$ids_csv = implode(',', array_map('intval', $post_ids));
$titles = $wpdb->get_results("SELECT ID, post_title FROM {$wpdb->posts} WHERE ID IN ({$ids_csv})", OBJECT_K);
foreach ($titles as $t) $posts_map[$t->ID] = $t->post_title;
}
$comments = [];
foreach ($query->comments as $comment) {
$comments[] = $this->format_comment($comment, $users_map, $posts_map);
}
// Total count for pagination
$count_args = $args;
unset($count_args['number'], $count_args['offset'], $count_args['orderby'], $count_args['order']);
$count_args['count'] = true;
$total = (int) (new \WP_Comment_Query($count_args))->get_comments('count');
return $this->success([
'data' => $comments,
'total' => $total,
'pages' => ceil($total / (int) $request->get_param('per_page')),
]);
}
/**
* POST /comments — Create a comment.
*/
public function create_comment(\WP_REST_Request $request): \WP_REST_Response {
$user_id = get_current_user_id();
$post_id = (int) $request->get_param('post_id');
$content = trim((string) $request->get_param('content'));
$parent = (int) $request->get_param('parent');
if (!$content) {
return $this->error('content_required', '评论内容不能为空。', 400);
}
$post = get_post($post_id);
if (!$post || $post->post_status !== 'publish') {
return $this->error('invalid_post', '文章不存在或未发布。', 400);
}
$comment_data = [
'comment_post_ID' => $post_id,
'comment_content' => $content,
'user_id' => $user_id,
'comment_parent' => $parent,
'comment_approved'=> 1, // Admin-created comments are auto-approved
];
$comment_id = wp_insert_comment($comment_data);
if (is_wp_error($comment_id)) {
return $this->error('create_failed', $comment_id->get_error_message(), 500);
}
$comment = get_comment($comment_id);
return $this->success(['data' => $this->format_comment($comment)]);
}
/**
* POST /comments/{id}/status — Approve/unapprove/spam/trash.
*/
public function update_comment_status(\WP_REST_Request $request): \WP_REST_Response {
$comment_id = (int) $request->get_param('id');
$new_status = $request->get_param('status');
$comment = get_comment($comment_id);
if (!$comment) {
return $this->error('not_found', '评论不存在。', 404);
}
$valid_statuses = ['approve', 'hold', 'spam', 'trash'];
if (!in_array($new_status, $valid_statuses, true)) {
return $this->error('invalid_status', '无效的状态值。', 400);
}
$result = wp_set_comment_status($comment_id, $new_status);
if (is_wp_error($result)) {
return $this->error('update_failed', $result->get_error_message(), 500);
}
// Handle spam/trash
if ($new_status === 'spam') {
wp_spam_comment($comment_id);
} elseif ($new_status === 'trash') {
wp_trash_comment($comment_id);
}
$comment = get_comment($comment_id);
return $this->success(['data' => $this->format_comment($comment)]);
}
/**
* POST /comments/{id} — Edit comment content.
*/
public function update_comment(\WP_REST_Request $request): \WP_REST_Response {
$comment_id = (int) $request->get_param('id');
$content = trim((string) $request->get_param('content'));
$comment = get_comment($comment_id);
if (!$comment) {
return $this->error('not_found', '评论不存在。', 404);
}
if (!$content) {
return $this->error('content_required', '评论内容不能为空。', 400);
}
$result = wp_update_comment(['comment_ID' => $comment_id, 'comment_content' => $content]);
if (is_wp_error($result)) {
return $this->error('update_failed', $result->get_error_message(), 500);
}
$comment = get_comment($comment_id);
return $this->success(['data' => $this->format_comment($comment)]);
}
/**
* DELETE /comments/{id} — Delete a comment.
*/
public function delete_comment(\WP_REST_Request $request): \WP_REST_Response {
$comment_id = (int) $request->get_param('id');
$comment = get_comment($comment_id);
if (!$comment) {
return $this->error('not_found', '评论不存在。', 404);
}
$result = wp_delete_comment($comment_id, true);
if (!$result) {
return $this->error('delete_failed', '删除评论失败。', 500);
}
return $this->success(['data' => ['deleted' => true, 'id' => $comment_id]]);
}
/**
* POST /comments/bulk — Bulk action on comments.
*/
public function bulk_action(\WP_REST_Request $request): \WP_REST_Response {
$body = $request->get_json_params();
$ids = array_map('absint', $body['ids'] ?? []);
$action = sanitize_text_field($body['action'] ?? '');
if (empty($ids) || !$action) {
return $this->error('missing_params', '缺少参数。', 400);
}
$success = 0;
$failed = 0;
foreach ($ids as $id) {
$comment = get_comment($id);
if (!$comment) { $failed++; continue; }
switch ($action) {
case 'approve':
wp_set_comment_status($id, 'approve');
$success++;
break;
case 'hold':
wp_set_comment_status($id, 'hold');
$success++;
break;
case 'spam':
wp_spam_comment($id);
$success++;
break;
case 'trash':
wp_trash_comment($id);
$success++;
break;
case 'delete':
wp_delete_comment($id, true);
$success++;
break;
default:
$failed++;
}
}
return $this->success(['data' => ['success' => $success, 'failed' => $failed]]);
}
/**
* GET /comments/post/{post_id} — Public: list comments for a post with nesting.
*/
public function get_post_comments(\WP_REST_Request $request): \WP_REST_Response {
$post_id = (int) $request->get_param('post_id');
$page = max(1, (int) $request->get_param('page'));
$per_page = min(50, max(1, (int) $request->get_param('per_page')));
$order = in_array(strtoupper((string) $request->get_param('order')), ['ASC', 'DESC'], true)
? strtoupper((string) $request->get_param('order'))
: 'DESC';
$args = [
'post_id' => $post_id,
'status' => 'approve',
'number' => $per_page,
'offset' => ($page - 1) * $per_page,
'orderby' => 'comment_date_gmt',
'order' => $order,
'hierarchical' => 'threaded',
];
$query = new \WP_Comment_Query($args);
// Batch prefetch users
$user_ids = array_unique(array_filter(array_map(fn($c) => (int) $c->user_id, $query->comments)));
$users_map = [];
if (!empty($user_ids)) {
$users = get_users(['include' => $user_ids, 'fields' => ['ID', 'display_name']]);
foreach ($users as $u) $users_map[$u->ID] = $u;
}
$comments = [];
foreach ($query->comments as $comment) {
$comments[] = $this->format_comment_public($comment, $users_map);
}
// Total approved comments for this post
$total = (int) get_comments_number($post_id);
$total_approved = get_comments([
'post_id' => $post_id,
'status' => 'approve',
'count' => true,
]);
return $this->success([
'data' => $comments,
'total' => (int) $total_approved,
'pages' => ceil((int) $total_approved / $per_page),
]);
}
/**
* Format comment for admin views.
*/
private function format_comment(\WP_Comment $comment, array $users_map = [], array $posts_map = []): array {
$uid = (int) $comment->user_id;
$pid = (int) $comment->comment_post_ID;
$user = $users_map[$uid] ?? ($uid ? get_userdata($uid) : null);
return [
'id' => (int) $comment->comment_ID,
'post_id' => $pid,
'post_title' => $posts_map[$pid] ?? get_the_title($pid),
'author' => $comment->comment_author,
'author_email' => $comment->comment_author_email,
'user_id' => (int) $comment->user_id,
'user_name' => $user ? $user->display_name : '',
'content' => $comment->comment_content,
'status' => $comment->comment_approved,
'parent' => (int) $comment->comment_parent,
'date' => $comment->comment_date_gmt,
'author_ip' => $comment->comment_author_IP,
'avatar_url' => get_avatar_url($comment->comment_author_email, ['size' => 48]),
];
}
/**
* Format comment for public views (no email/IP).
*/
private function format_comment_public(\WP_Comment $comment, array $users_map = []): array {
$uid = (int) $comment->user_id;
$user = $users_map[$uid] ?? ($uid ? get_userdata($uid) : null);
return [
'id' => (int) $comment->comment_ID,
'author' => $comment->comment_author,
'user_id' => (int) $comment->user_id,
'content' => $comment->comment_content,
'date' => $comment->comment_date_gmt,
'parent' => (int) $comment->comment_parent,
'avatar_url' => get_avatar_url($comment->comment_author_email, ['size' => 48]),
'children' => [], // Will be populated by JS if threaded
];
}
/**
* Helper: check logged-in permission (any authenticated user).
*/
public function check_logged_in_permission(): bool {
return is_user_logged_in();
}
}