Files
wordpress-theme-aksrt/src/Admin/Native/UsersMenusBan.php
T
kate522andClaude Fable 5 b45fd161f0 fix(reports): separate readable categories from report content
Store readable category snapshots separately from details and evidence, validate report categories, and preserve context in admin displays and moderation actions. Widen report categories to TEXT and bump the schema to 3.2.0 without legacy data conversion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-31 00:57:13 +08:00

203 lines
13 KiB
PHP

<?php
/** Native report and ban appeal queue. @package AKSRT */
namespace AKSRT\Admin\Native;
use AKSRT\User\Restriction;
defined('ABSPATH') || exit;
trait UsersMenusBan {
use UsersMenusBanQuery;
use UsersMenusBanDialogs;
use UsersMenusBanActions;
private array $ban_restrictions = [];
public function enqueue_ban_assets(string $hook): void {
if ($hook !== 'users_page_aksrt_user_ban') return;
$css = '/assets/css/admin/user-ban.css';
$js = '/assets/js/admin-user-ban.js';
wp_enqueue_style('aksrt-admin-user-ban', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-user-ban', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
}
public function render_ban_page(): void {
$this->ensure_cap();
$filters = $this->get_ban_filters($_GET);
$data = $this->query_ban_requests($filters, max(1, (int) ($_GET['paged'] ?? 1)));
$this->cache_ban_targets($data['items']);
$this->load_ban_restrictions($data['items']);
NativeUi::page_open('aksrt-ban-page');
NativeUi::header(
__('举报与禁封申诉', 'aksrt'),
__('核对举报内容与申诉资料,在审核弹窗中记录处理结果。用户禁封操作仍在前台用户主页完成。', 'aksrt'),
'dashicons-shield'
);
$this->flash();
NativeUi::card_open('', '', true, 'aksrt-ban-list-card');
$this->render_ban_filters($filters, $data['counts']);
echo '<div class="aksrt-native-table-wrap">';
$this->render_ban_table($data['items'], $filters, $data['page']);
NativeUi::table_wrap_close();
AdminListPagination::render('aksrt_user_ban', $data['total'], $data['page'], self::BAN_PER_PAGE, $filters, 'users.php');
NativeUi::card_close();
echo '<dialog id="aksrt-ban-dialog" class="aksrt-ban-dialog" aria-labelledby="aksrt-ban-dialog-title" aria-describedby="aksrt-ban-dialog-description"></dialog>';
echo '<noscript><p class="aksrt-native-callout">' . esc_html__('请启用 JavaScript 后查看详情和处理审核。', 'aksrt') . '</p></noscript>';
NativeUi::page_close();
}
private function ban_page_url(array $args = []): string {
return add_query_arg(array_merge(['page' => 'aksrt_user_ban'], $args), admin_url('users.php'));
}
private function render_ban_filters(array $filters, array $counts): void {
echo '<div class="aksrt-ban-toolbar"><div class="aksrt-ban-toolbar__top"><h2 class="aksrt-native-card__title">' . esc_html__('处理记录', 'aksrt') . '</h2>';
echo '<nav class="aksrt-ban-types" aria-label="' . esc_attr__('记录类型', 'aksrt') . '">';
foreach (['' => __('全部类型', 'aksrt'), 'report' => __('用户举报', 'aksrt'), 'appeal' => __('禁封申诉', 'aksrt')] as $key => $label) {
echo '<a href="' . esc_url($this->ban_page_url(array_merge($filters, ['type' => $key]))) . '"' . ($key === $filters['type'] ? ' aria-current="page"' : '') . '>' . esc_html($label) . '</a>';
}
echo '</nav></div><div class="aksrt-ban-toolbar__bottom"><ul class="subsubsub" aria-label="' . esc_attr__('处理状态筛选', 'aksrt') . '">';
foreach (['pending' => __('待处理', 'aksrt'), 'resolved' => __('已处理 / 已通过', 'aksrt'), 'dismissed' => __('已驳回', 'aksrt'), '' => __('全部状态', 'aksrt')] as $key => $label) {
if ($filters['type'] === 'report' && $key === 'resolved') $label = __('已处理', 'aksrt');
if ($filters['type'] === 'appeal' && $key === 'resolved') $label = __('已通过', 'aksrt');
echo '<li><a href="' . esc_url($this->ban_page_url(array_merge($filters, ['status' => $key]))) . '"' . ($key === $filters['status'] ? ' class="current" aria-current="page"' : '') . '>' . esc_html($label)
. ' <span class="count">' . esc_html(number_format_i18n($counts[$key === '' ? 'all' : $key])) . '</span></a></li>';
}
echo '</ul><form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
echo '<input type="hidden" name="page" value="aksrt_user_ban">';
foreach (['type', 'status', 'send_user', 'orderby', 'order'] as $key) {
echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr((string) $filters[$key]) . '">';
}
echo '<label class="screen-reader-text" for="aksrt-ban-search">' . esc_html__('搜索用户或记录内容', 'aksrt') . '</label>';
echo '<input type="search" id="aksrt-ban-search" name="s" value="' . esc_attr($filters['s']) . '" placeholder="' . esc_attr__('用户 / 目标 ID / 原因 / 申诉资料', 'aksrt') . '">';
echo '<button type="submit" class="button">' . esc_html__('搜索', 'aksrt') . '</button>';
if ($filters['s'] !== '' || $filters['send_user'] > 0) {
echo '<a class="button-link" href="' . esc_url($this->ban_page_url(array_merge($filters, ['s' => '', 'send_user' => 0]))) . '">' . esc_html__('清除筛选', 'aksrt') . '</a>';
}
echo '</form></div>';
if ($filters['send_user'] > 0) {
$user = get_userdata($filters['send_user']);
echo '<p class="aksrt-ban-filter-hint">' . esc_html(sprintf(__('当前仅显示 %s 提交的记录。', 'aksrt'), $user ? $user->display_name : '#' . $filters['send_user'])) . '</p>';
}
echo '</div>';
}
private function render_ban_table(array $items, array $filters, int $page): void {
echo '<table class="widefat fixed striped aksrt-ban-table"><caption class="screen-reader-text">' . esc_html__('举报与禁封申诉处理记录', 'aksrt') . '</caption><thead><tr>';
foreach (['type' => __('类型 / 编号', 'aksrt'), 'user' => __('提交用户', 'aksrt'), 'summary' => __('事项摘要', 'aksrt'), 'status' => __('状态', 'aksrt'), 'time' => __('提交时间', 'aksrt'), 'updated' => __('更新时间', 'aksrt'), 'actions' => __('操作', 'aksrt')] as $key => $label) {
$sortable = in_array($key, ['type', 'status', 'time', 'updated'], true);
$active = $sortable && $filters['orderby'] === $key;
echo '<th scope="col" class="column-' . esc_attr($key) . '"' . ($active ? ' aria-sort="' . ($filters['order'] === 'ASC' ? 'ascending' : 'descending') . '"' : '') . '>';
if ($sortable) {
$order = $active && $filters['order'] === 'DESC' ? 'ASC' : 'DESC';
$title = sprintf($order === 'ASC' ? __('按%s升序排列', 'aksrt') : __('按%s降序排列', 'aksrt'), $label);
echo '<a class="aksrt-ban-sort" href="' . esc_url($this->ban_page_url(array_merge($filters, ['orderby' => $key, 'order' => $order]))) . '" aria-label="' . esc_attr($title) . '">' . esc_html($label) . '<span aria-hidden="true">' . ($active ? ($filters['order'] === 'ASC' ? '↑' : '↓') : '↕') . '</span></a>';
} else {
echo esc_html($label);
}
echo '</th>';
}
echo '</tr></thead><tbody>';
if ($items === []) {
$filtered = $filters['s'] !== '' || $filters['send_user'] > 0;
echo NativeUi::empty_row(7,
!$filtered && $filters['status'] === 'pending' ? __('暂无待处理记录', 'aksrt') : __('没有符合条件的记录', 'aksrt'),
$filtered ? __('请调整关键词、清除用户筛选或切换类型和状态。', 'aksrt') : __('可切换状态查看处理记录,新举报和申诉将在这里显示。', 'aksrt')
);
}
foreach ($items as $row) $this->render_ban_row($row, $filters, $page);
echo '</tbody></table>';
}
private function render_ban_row(array $row, array $filters, int $page): void {
$report = $row['kind'] === 'report';
$payload = $row['payload'];
$key = $row['kind'] . '-' . $row['id'];
$user_id = (int) $row['user_id'];
echo '<tr><td class="column-type">' . NativeUi::badge($report ? __('用户举报', 'aksrt') : __('禁封申诉', 'aksrt'), $report ? 'info' : 'neutral', false);
echo '<div class="aksrt-ban-sub">#' . (int) $row['id'] . '</div></td><td class="column-user">' . NativeUi::user_cell($user_id, __('用户已删除', 'aksrt'));
if ($user_id > 0) {
echo '<a class="aksrt-ban-user-filter" href="' . esc_url($this->ban_page_url(array_merge($filters, ['send_user' => $user_id]))) . '">' . esc_html__('筛选此用户', 'aksrt') . '</a>';
}
echo '</td><td class="column-summary">';
if ($report) {
echo '<strong>' . $this->ban_target_link((string) ($payload['target_type'] ?? ''), (int) ($payload['target_id'] ?? 0)) . '</strong>';
echo '<div class="aksrt-ban-summary">' . esc_html((string) ($payload['category'] ?? '')) . '</div>';
} else {
echo '<strong>' . esc_html__('申请解除禁封', 'aksrt') . '</strong>';
}
if ((string) ($payload['reason'] ?? '') !== '') echo '<div class="aksrt-ban-summary">' . esc_html((string) $payload['reason']) . '</div>';
$this->render_ban_detail_template($row, $filters, $page);
echo '</td><td class="column-status">' . $this->ban_status_badge($row) . '</td>';
echo '<td class="column-time">' . esc_html($this->ban_time((string) $row['time'])) . '</td>';
echo '<td class="column-updated">' . esc_html($this->ban_time((string) $row['updated'])) . '</td>';
echo '<td class="column-actions"><button type="button" class="button' . ($row['status'] === 'pending' ? ' button-primary' : '') . '" data-aksrt-ban-view="' . esc_attr($key) . '" aria-haspopup="dialog" aria-controls="aksrt-ban-dialog">'
. esc_html($row['status'] === 'pending' ? __('审核', 'aksrt') : __('查看详情', 'aksrt')) . '</button></td></tr>';
}
private function ban_status_badge(array $row): string {
if ($row['status'] === 'pending') return NativeUi::badge($row['raw_status'] === 'reviewing' ? __('审核中', 'aksrt') : __('待处理', 'aksrt'), 'warning');
if ($row['status'] === 'resolved') return NativeUi::badge($row['kind'] === 'report' ? __('已处理', 'aksrt') : __('已通过', 'aksrt'), 'success');
return NativeUi::badge(__('已驳回', 'aksrt'), 'danger');
}
private function ban_time(string $time): string {
return $time !== '' && $time !== '0000-00-00 00:00:00' ? $time : '—';
}
private function ban_user_home(int $user_id): string {
return function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user_id) : get_author_posts_url($user_id);
}
private function ban_target_link(string $type, int $id): string {
$labels = ['post' => __('文章', 'aksrt'), 'comment' => __('评论', 'aksrt'), 'user' => __('用户', 'aksrt')];
$label = ($labels[$type] ?? __('目标', 'aksrt')) . ' #' . $id;
$url = '';
if ($type === 'post' && get_post($id)) {
$label = get_the_title($id) ?: $label;
$url = (string) get_permalink($id);
} elseif ($type === 'comment' && get_comment($id)) {
$url = (string) get_comment_link($id);
} elseif ($type === 'user' && ($user = get_userdata($id))) {
$label = $user->display_name . ' #' . $id;
$url = $this->ban_user_home($id);
}
return $url !== '' ? '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($label) . '</a>' : esc_html($label . ' ' . __('(目标已不可用)', 'aksrt'));
}
private function cache_ban_targets(array $items): void {
$ids = ['post' => [], 'comment' => [], 'user' => []];
foreach ($items as $row) {
if ($row['kind'] !== 'report') continue;
$type = (string) ($row['payload']['target_type'] ?? '');
$id = (int) ($row['payload']['target_id'] ?? 0);
if (isset($ids[$type]) && $id > 0) $ids[$type][] = $id;
}
foreach ($ids as &$values) $values = array_values(array_unique($values));
unset($values);
if ($ids['post'] !== []) _prime_post_caches($ids['post'], false, false);
if ($ids['comment'] !== []) _prime_comment_caches($ids['comment'], false);
if ($ids['user'] !== []) cache_users($ids['user']);
}
private function load_ban_restrictions(array $items): void {
$this->ban_restrictions = [];
if (!class_exists(Restriction::class)) return;
$ids = [];
foreach ($items as $row) {
$id = $row['kind'] === 'appeal' ? (int) $row['user_id'] : (($row['payload']['target_type'] ?? '') === 'user' ? (int) $row['payload']['target_id'] : 0);
if ($id > 0) $ids[] = $id;
}
$ids = array_values(array_unique($ids));
if ($ids === []) return;
global $wpdb;
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT user_id, type, scope, reason, created_at, expires_at FROM {$wpdb->prefix}aksrt_user_restriction WHERE user_id IN ({$placeholders}) AND (expires_at IS NULL OR expires_at > %s) ORDER BY created_at DESC",
array_merge($ids, [current_time('mysql', true)])
), ARRAY_A);
foreach ((array) $rows as $row) $this->ban_restrictions[(int) $row['user_id']][] = $row;
}
}