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, /