diff --git a/assets/js/report.js b/assets/js/report.js
index e27ab4bb..0005ee89 100644
--- a/assets/js/report.js
+++ b/assets/js/report.js
@@ -24,7 +24,7 @@
function getReportCategories() {
return (Array.isArray(config.reportCategories) ? config.reportCategories : [])
- .filter(function (cat) { return cat && cat.name; });
+ .filter(function (cat) { return cat && cat.name && cat.name !== '其他原因'; });
}
function buildReportModalHtml(data, categories) {
@@ -226,9 +226,10 @@
form.querySelector('[name="report_reason"]').focus();
return;
}
- var category = selected.value === 'other' ? null : categories[Number(selected.value)];
- var reason = category ? String(category.name) : otherInput.value.trim();
- if (!reason) {
+ var other = selected.value === 'other';
+ var category = other ? '其他原因' : String(categories[Number(selected.value)].name);
+ var explanation = other ? otherInput.value.trim() : '';
+ if (other && !explanation) {
showError(reasonError, '请填写举报原因');
otherInput.setAttribute('aria-invalid', 'true');
otherInput.focus();
@@ -238,9 +239,12 @@
showError(error, '请等待图片上传完成,或重试、删除上传失败的图片');
return;
}
+ var content = [];
+ if (explanation) content.push(explanation);
var description = descInput.value.trim();
- if (description) reason += '\n详细描述:\n' + description;
- if (images.length) reason += '\n图片举证:\n' + images.map(function (image) { return image.url; }).join('\n');
+ if (description) content.push(description);
+ if (images.length) content.push('图片举证:\n' + images.map(function (image) { return image.url; }).join('\n'));
+ var reason = content.join('\n\n');
submitting = true;
syncState();
var toastId = form.id + '-submit-' + (++submitAttempts);
@@ -249,7 +253,7 @@
window.fetch(API_ROOT + '/report/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE },
- body: JSON.stringify({ target_type: data.target_type, target_id: data.target_id, category: category ? category.slug : '', reason: reason }),
+ body: JSON.stringify({ target_type: data.target_type, target_id: data.target_id, category: category, reason: reason }),
}).then(function (response) { return response.json(); }).then(function (result) {
if (!result || !result.success) throw new Error((result && result.message) || '举报提交失败,请重试');
// Submitted evidence belongs to the report even if its dialog has already closed.
diff --git a/src/Admin/Native/UsersMenusBan.php b/src/Admin/Native/UsersMenusBan.php
index 112920be..5dfbf45d 100644
--- a/src/Admin/Native/UsersMenusBan.php
+++ b/src/Admin/Native/UsersMenusBan.php
@@ -123,10 +123,11 @@ trait UsersMenusBan {
echo '
';
if ($report) {
echo '' . $this->ban_target_link((string) ($payload['target_type'] ?? ''), (int) ($payload['target_id'] ?? 0)) . '';
+ echo ' ' . esc_html((string) ($payload['category'] ?? '')) . ' ';
} else {
echo '' . esc_html__('申请解除禁封', 'aksrt') . '';
}
- echo '' . esc_html((string) ($payload['reason'] ?? '')) . ' ';
+ if ((string) ($payload['reason'] ?? '') !== '') echo '' . esc_html((string) $payload['reason']) . ' ';
$this->render_ban_detail_template($row, $filters, $page);
echo ' | ' . $this->ban_status_badge($row) . ' | ';
echo '' . esc_html($this->ban_time((string) $row['time'])) . ' | ';
diff --git a/src/Admin/Native/UsersMenusBanDialogs.php b/src/Admin/Native/UsersMenusBanDialogs.php
index d88e644c..ca9780c0 100644
--- a/src/Admin/Native/UsersMenusBanDialogs.php
+++ b/src/Admin/Native/UsersMenusBanDialogs.php
@@ -93,7 +93,7 @@ trait UsersMenusBanDialogs {
$type = (string) ($payload['target_type'] ?? '');
$id = (int) ($payload['target_id'] ?? 0);
echo '' . esc_html__('举报对象', 'aksrt') . '
' . $this->ban_target_link($type, $id) . '
';
- 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);
diff --git a/src/Admin/UserController.php b/src/Admin/UserController.php
index c544f4da..620b735a 100644
--- a/src/Admin/UserController.php
+++ b/src/Admin/UserController.php
@@ -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)
diff --git a/src/Admin/UserControllerReportOperations.php b/src/Admin/UserControllerReportOperations.php
index d4528e20..3b266cef 100644
--- a/src/Admin/UserControllerReportOperations.php
+++ b/src/Admin/UserControllerReportOperations.php
@@ -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('无效的举报类型');
diff --git a/src/Core/DBMigrator.php b/src/Core/DBMigrator.php
index 61f5a49b..abb7f291 100644
--- a/src/Core/DBMigrator.php
+++ b/src/Core/DBMigrator.php
@@ -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';
diff --git a/src/Core/DBMigratorPart2.php b/src/Core/DBMigratorPart2.php
index 780a1c2c..c733d235 100644
--- a/src/Core/DBMigratorPart2.php
+++ b/src/Core/DBMigratorPart2.php
@@ -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,
diff --git a/src/Messages/ReportCenter.php b/src/Messages/ReportCenter.php
index 634a2ce9..aae57680 100644
--- a/src/Messages/ReportCenter.php
+++ b/src/Messages/ReportCenter.php
@@ -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) {
diff --git a/src/Settings/UserRuntime.php b/src/Settings/UserRuntime.php
index b4e79c76..f99de90b 100644
--- a/src/Settings/UserRuntime.php
+++ b/src/Settings/UserRuntime.php
@@ -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];
}
}
diff --git a/tests/widget-system/ban-admin-contract.test.cjs b/tests/widget-system/ban-admin-contract.test.cjs
index 6eec8849..0faed3e3 100644
--- a/tests/widget-system/ban-admin-contract.test.cjs
+++ b/tests/widget-system/ban-admin-contract.test.cjs
@@ -82,6 +82,23 @@ test('PHP templates escape all appeal data and report evidence while preserving
assert.doesNotMatch(html, /