Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
423 lines
16 KiB
PHP
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();
|
|
}
|
|
}
|