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>
This commit is contained in:
1 parent
b2811cc248
commit
b45fd161f0
16 files changed
+797
-75
No files matched your search
@@ -123,10 +123,11 @@ trait UsersMenusBan {
|
||||
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>';
|
||||
}
|
||||
echo '<div class="aksrt-ban-summary">' . esc_html((string) ($payload['reason'] ?? '')) . '</div>';
|
||||
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>';
|
||||
|
||||
@@ -93,7 +93,7 @@ trait UsersMenusBanDialogs {
|
||||
$type = (string) ($payload['target_type'] ?? '');
|
||||
$id = (int) ($payload['target_id'] ?? 0);
|
||||
echo '<section class="aksrt-ban-section"><h3>' . esc_html__('举报对象', 'aksrt') . '</h3><div class="aksrt-ban-target">' . $this->ban_target_link($type, $id) . '</div></section>';
|
||||
if (!empty($payload['category'])) $this->render_ban_text_section(__('举报分类', 'aksrt'), (string) $payload['category']);
|
||||
if ((string) ($payload['category'] ?? '') !== '') $this->render_ban_text_section(__('举报分类', 'aksrt'), (string) $payload['category']);
|
||||
$this->render_ban_text_section(__('举报内容与举证', 'aksrt'), (string) ($payload['reason'] ?? ''));
|
||||
if ($type === 'user') {
|
||||
$this->render_ban_restriction_section($id);
|
||||
|
||||
@@ -171,6 +171,20 @@ class UserController extends ControllerBase {
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'submit_report'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
'args' => [
|
||||
'target_type' => ['type' => 'string', 'required' => true, 'enum' => ['post', 'comment', 'user']],
|
||||
'target_id' => ['type' => 'integer', 'required' => true, 'minimum' => 1],
|
||||
'category' => [
|
||||
'type' => 'string', 'required' => true,
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
'reason' => [
|
||||
'type' => 'string', 'default' => '',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'sanitize_textarea_field',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Follow toggle (logged-in users)
|
||||
|
||||
@@ -46,7 +46,7 @@ trait UserControllerReportOperations {
|
||||
$page = max(1, (int) ($request->get_param('page') ?: 1));
|
||||
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: $request->get_param('limit') ?: 20)));
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$category = sanitize_key((string) $request->get_param('category'));
|
||||
$category = sanitize_text_field((string) $request->get_param('category'));
|
||||
$target_type = sanitize_key((string) $request->get_param('target_type'));
|
||||
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
@@ -95,7 +95,7 @@ trait UserControllerReportOperations {
|
||||
$counts = \AKSRT\Messages\ReportCenter::get_instance()->get_status_counts();
|
||||
$categories = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$cat = sanitize_key((string) ($row['category'] ?? ''));
|
||||
$cat = sanitize_text_field((string) ($row['category'] ?? ''));
|
||||
if ($cat !== '') {
|
||||
$categories[$cat] = $cat;
|
||||
}
|
||||
@@ -190,11 +190,14 @@ trait UserControllerReportOperations {
|
||||
return $this->error(__('举报工单不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$action_reason = (string) ($row['category'] ?? '');
|
||||
$details = (string) ($row['reason'] ?? '');
|
||||
if ($details !== '') $action_reason .= "\n\n" . $details;
|
||||
$effect = $this->apply_report_action(
|
||||
sanitize_key((string) $row['target_type']),
|
||||
(int) $row['target_id'],
|
||||
$action_taken,
|
||||
(string) ($row['reason'] ?? '')
|
||||
$action_reason
|
||||
);
|
||||
|
||||
$notes = json_decode((string) ($row['notes_json'] ?? '[]'), true);
|
||||
@@ -260,7 +263,7 @@ trait UserControllerReportOperations {
|
||||
'target_type' => sanitize_key((string) ($row['target_type'] ?? '')),
|
||||
'target_id' => (int) ($row['target_id'] ?? 0),
|
||||
'reason' => (string) ($row['reason'] ?? ''),
|
||||
'category' => sanitize_key((string) ($row['category'] ?? '')),
|
||||
'category' => (string) ($row['category'] ?? ''),
|
||||
'status' => sanitize_key((string) ($row['status'] ?? 'pending')),
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'updated_at' => (string) ($row['updated_at'] ?? ''),
|
||||
@@ -408,16 +411,25 @@ trait UserControllerReportOperations {
|
||||
return $this->error('举报系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$target_type = sanitize_key((string) ($params['target_type'] ?? ''));
|
||||
$target_id = (int) ($params['target_id'] ?? 0);
|
||||
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
|
||||
$category = sanitize_key((string) ($params['category'] ?? ''));
|
||||
$target_type = sanitize_key((string) $request->get_param('target_type'));
|
||||
$target_id = (int) $request->get_param('target_id');
|
||||
$reason = sanitize_textarea_field((string) $request->get_param('reason'));
|
||||
$category = sanitize_text_field((string) $request->get_param('category'));
|
||||
|
||||
if ($target_type === '' || $target_id <= 0 || $reason === '') {
|
||||
if ($target_type === '' || $target_id <= 0 || $category === '') {
|
||||
return $this->error('请填写完整的举报信息');
|
||||
}
|
||||
|
||||
$other_category = '其他原因';
|
||||
$categories = array_column(\AKSRT\Settings\UserRuntime::ban_settings()['reasons'], 'name');
|
||||
$categories[] = $other_category;
|
||||
if (!in_array($category, $categories, true)) {
|
||||
return $this->error(__('举报原因无效或已更新,请刷新页面后重新选择', 'aksrt'));
|
||||
}
|
||||
if ($category === $other_category && $reason === '') {
|
||||
return $this->error(__('请填写举报原因的具体说明', 'aksrt'));
|
||||
}
|
||||
|
||||
$valid_types = ['post', 'comment', 'user'];
|
||||
if (!in_array($target_type, $valid_types, true)) {
|
||||
return $this->error('无效的举报类型');
|
||||
|
||||
@@ -26,7 +26,7 @@ class DBMigrator {
|
||||
// 沟通记录从工单上的 JSON 日志升级为独立的 ticket_reply 行(支持引用回复)。
|
||||
// 3.1.0: aksrt_ticket 新增 urge_count / last_urge_time(催单次数与最近催单时间),
|
||||
// 催单冷却改由 last_urge_time 判定,不再依赖 transient;催单同时写入 system 类型的 ticket_reply。
|
||||
const SCHEMA_VERSION = '3.1.0';
|
||||
const SCHEMA_VERSION = '3.2.0';
|
||||
|
||||
const OPT_KEY = 'aksrt_db_schema_version';
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ trait DBMigratorPart2 {
|
||||
target_type VARCHAR(20) NOT NULL,
|
||||
target_id BIGINT UNSIGNED NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
category VARCHAR(40) NOT NULL DEFAULT '',
|
||||
category TEXT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME DEFAULT NULL,
|
||||
|
||||
@@ -26,33 +26,37 @@ class ReportCenter {
|
||||
* @param int $reporter_id 举报人 ID
|
||||
* @param string $target_type 举报目标类型 (post/comment/user)
|
||||
* @param int $target_id 目标 ID
|
||||
* @param string $reason 举报原因
|
||||
* @param string $category 举报分类
|
||||
* @param string $reason 补充说明与举证
|
||||
* @param string $category 举报分类名称
|
||||
* @return int 举报 ID(0 表示失败)
|
||||
*/
|
||||
public function submit(int $reporter_id, string $target_type, int $target_id, string $reason, string $category = ''): int {
|
||||
public function submit(int $reporter_id, string $target_type, int $target_id, string $reason, string $category): int {
|
||||
if ($reporter_id <= 0 || $target_id <= 0) return 0;
|
||||
|
||||
$valid_types = ['post', 'comment', 'user'];
|
||||
if (!in_array($target_type, $valid_types, true)) return 0;
|
||||
$category = sanitize_text_field($category);
|
||||
$reason = sanitize_textarea_field($reason);
|
||||
if ($category === '') return 0;
|
||||
|
||||
$table = $this->table();
|
||||
if ($table === '') return 0;
|
||||
|
||||
global $wpdb;
|
||||
$wpdb->insert($table, [
|
||||
$inserted = $wpdb->insert($table, [
|
||||
'reporter_id' => $reporter_id,
|
||||
'target_type' => sanitize_key($target_type),
|
||||
'target_id' => $target_id,
|
||||
'reason' => sanitize_textarea_field($reason),
|
||||
'category' => sanitize_key($category),
|
||||
'reason' => $reason,
|
||||
'category' => $category,
|
||||
'status' => 'pending',
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%s', '%d', '%s', '%s', '%s', '%s']);
|
||||
if ($inserted === false) return 0;
|
||||
|
||||
$id = $wpdb->insert_id ? (int) $wpdb->insert_id : 0;
|
||||
if ($id > 0) {
|
||||
$this->notify_admins_new_report($reporter_id, $target_type, $target_id, sanitize_textarea_field($reason));
|
||||
$this->notify_admins_new_report($reporter_id, $target_type, $target_id, $category, $reason);
|
||||
do_action('aksrt_report_submitted', $id, [
|
||||
'reporter_id' => $reporter_id,
|
||||
'target_type' => $target_type,
|
||||
@@ -67,17 +71,18 @@ class ReportCenter {
|
||||
/**
|
||||
* 举报提交 → 管理员站内信(与禁封申诉同模式:直达工单列表)。
|
||||
*/
|
||||
private function notify_admins_new_report(int $reporter_id, string $target_type, int $target_id, string $reason): void {
|
||||
private function notify_admins_new_report(int $reporter_id, string $target_type, int $target_id, string $category, string $reason): void {
|
||||
if (!class_exists('\AKSRT\Messages\Message') || !class_exists('WP_User_Query')) return;
|
||||
$type_labels = ['post' => __('文章', 'aksrt'), 'comment' => __('评论', 'aksrt'), 'user' => __('用户', 'aksrt')];
|
||||
$reporter = get_userdata($reporter_id);
|
||||
$summary = $category . ($reason !== '' ? ':' . $reason : '');
|
||||
$content = sprintf(
|
||||
/* translators: 1: reporter name 2: target type 3: target id 4: reason */
|
||||
/* translators: 1: reporter name 2: target type 3: target id 4: report summary */
|
||||
__('用户 %1$s 举报了%2$s #%3$d:%4$s', 'aksrt'),
|
||||
$reporter instanceof \WP_User ? $reporter->display_name : __('匿名用户', 'aksrt'),
|
||||
$type_labels[$target_type] ?? $target_type,
|
||||
$target_id,
|
||||
mb_substr($reason, 0, 80)
|
||||
mb_substr($summary, 0, 80)
|
||||
);
|
||||
$admins = get_users(['role' => 'administrator', 'fields' => 'ID', 'number' => 20]);
|
||||
foreach ($admins as $admin_id) {
|
||||
|
||||
@@ -175,34 +175,12 @@ class UserRuntime {
|
||||
public static function ban_settings(): array {
|
||||
$settings = self::read_report_section();
|
||||
|
||||
// 预置原因:group 数组 [{t:...}](对齐参考主题)或旧版每行一条文本。
|
||||
// slug 不能用 sanitize_key(标签) 派生:中文标签会被清洗成空串,
|
||||
// 导致前端按 slug 非空过滤后整列表被丢弃。改用标签内容的稳定短哈希
|
||||
// (跨请求一致、可通过服务端 sanitize_key、ASCII 标签同样稳定)。
|
||||
$reasons_raw = $settings['ban_preset_reason'] ?? null;
|
||||
$reasons_raw = $settings['ban_preset_reason'] ?? [];
|
||||
$reasons = [];
|
||||
$reason_slug = static function (string $text): string {
|
||||
$key = sanitize_key($text);
|
||||
return $key !== '' ? $key : substr(md5($text), 0, 12);
|
||||
};
|
||||
if (is_array($reasons_raw)) {
|
||||
foreach ($reasons_raw as $row) {
|
||||
$text = trim((string) ($row['t'] ?? ''));
|
||||
if ($text !== '') {
|
||||
$reasons[] = ['slug' => $reason_slug($text), 'name' => sanitize_text_field($text)];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach (preg_split('/\r\n|\r|\n/', (string) $reasons_raw) as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$parts = explode('|', $line, 2);
|
||||
$reasons[] = [
|
||||
'slug' => $reason_slug((string) ($parts[1] ?? $parts[0])),
|
||||
'name' => sanitize_text_field($parts[1] ?? $parts[0]),
|
||||
];
|
||||
foreach (is_array($reasons_raw) ? $reasons_raw : [] as $row) {
|
||||
$text = is_array($row) ? sanitize_text_field((string) ($row['t'] ?? '')) : '';
|
||||
if ($text !== '') {
|
||||
$reasons[] = ['name' => $text];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user