feat(users): enforce real bans from report processing and fix appeal lifting
- apply_report_action now writes aksrt_user_restriction via Restriction::restrict
and lifts via new lift_user_restriction(), replacing the display-only
aksrt_user_ban_state meta dual-track
- ban type restrictions destroy the target user's sessions on apply
- appeal approval lifts active ban/suspend/mute restrictions instead of
updating a nonexistent status column
- UsersBan.vue posts to /users/{id}/restrict with valid types (warn/mute/
suspend/ban), scope and expires_at
- add comment report entry to the comment actions dropdown
- add duplicate pending report guard on submit
- remove dead ReportCenter ajax handlers, option migration, REST alias
routes and write-only restriction meta
- fix report admin mail link to the real aksrt_user_ban page
- add report-ban contract tests
This commit is contained in:
1 parent
4dafe96c4b
commit
835e0f983d
12 files changed
+291
-214
No files matched your search
@@ -190,129 +190,18 @@ class ReportCenter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性迁移:把 option 中的举报数据搬到表中。
|
||||
* 幂等 — 已存在的 ID 不会重复插入。
|
||||
* 同一举报人对同一目标是否已有待处理工单(防止重复提交)。
|
||||
*/
|
||||
public function migrate_from_option(): int {
|
||||
public function has_pending_report(int $reporter_id, string $target_type, int $target_id): bool {
|
||||
$table = $this->table();
|
||||
if ($table === '') return 0;
|
||||
|
||||
$reports = get_option('aksrt_reports', []);
|
||||
if (!is_array($reports) || empty($reports)) return 0;
|
||||
if ($table === '') return false;
|
||||
|
||||
global $wpdb;
|
||||
$migrated = 0;
|
||||
|
||||
foreach ($reports as $old_id => $report) {
|
||||
// 检查是否已迁移(通过 reporter_id + target_type + target_id + created_at 去重)
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE reporter_id = %d AND target_type = %s AND target_id = %d AND created_at = %s",
|
||||
(int) ($report['reporter_id'] ?? 0),
|
||||
sanitize_key((string) ($report['target_type'] ?? '')),
|
||||
(int) ($report['target_id'] ?? 0),
|
||||
(string) ($report['created_at'] ?? '')
|
||||
));
|
||||
if ($existing) continue;
|
||||
|
||||
$notes_json = '';
|
||||
if (!empty($report['notes']) && is_array($report['notes'])) {
|
||||
$notes_json = wp_json_encode($report['notes']);
|
||||
}
|
||||
|
||||
$wpdb->insert($table, [
|
||||
'reporter_id' => (int) ($report['reporter_id'] ?? 0),
|
||||
'target_type' => sanitize_key((string) ($report['target_type'] ?? '')),
|
||||
'target_id' => (int) ($report['target_id'] ?? 0),
|
||||
'reason' => sanitize_textarea_field((string) ($report['reason'] ?? '')),
|
||||
'category' => sanitize_key((string) ($report['category'] ?? '')),
|
||||
'status' => sanitize_key((string) ($report['status'] ?? 'pending')),
|
||||
'created_at' => (string) ($report['created_at'] ?? current_time('mysql')),
|
||||
'updated_at' => (string) ($report['updated_at'] ?? ''),
|
||||
'notes_json' => $notes_json,
|
||||
], ['%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||||
|
||||
if ($wpdb->insert_id) $migrated++;
|
||||
}
|
||||
|
||||
if ($migrated > 0) {
|
||||
update_option('aksrt_report_migrated_to_table', time());
|
||||
}
|
||||
|
||||
return $migrated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize AJAX hooks for admin operations.
|
||||
*/
|
||||
public function init(): void {
|
||||
add_action('wp_ajax_aksrt_report_update_status', [$this, 'ajax_update_status']);
|
||||
add_action('wp_ajax_aksrt_report_list', [$this, 'ajax_list']);
|
||||
add_action('wp_ajax_aksrt_report_counts', [$this, 'ajax_counts']);
|
||||
|
||||
// 一次性迁移钩子
|
||||
add_action('admin_init', [$this, 'maybe_migrate'], 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动迁移旧 option 数据(幂等)
|
||||
*/
|
||||
public function maybe_migrate(): void {
|
||||
if (get_option('aksrt_report_migrated_to_table')) return;
|
||||
$this->migrate_from_option();
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Update report status (admin only).
|
||||
*/
|
||||
public function ajax_update_status(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
check_ajax_referer('aksrt_report', '_nonce');
|
||||
|
||||
$report_id = (int) ($_POST['report_id'] ?? 0);
|
||||
$new_status = sanitize_key((string) ($_POST['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($_POST['note'] ?? ''));
|
||||
|
||||
if ($report_id <= 0 || $new_status === '') {
|
||||
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
|
||||
}
|
||||
|
||||
$result = $this->update_status($report_id, $new_status, $note);
|
||||
if ($result) {
|
||||
wp_send_json_success(['message' => __('状态已更新', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => __('更新失败', 'aksrt')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get report list (admin only).
|
||||
*/
|
||||
public function ajax_list(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$status = sanitize_key((string) ($_GET['status'] ?? ''));
|
||||
$limit = max(1, min(100, (int) ($_GET['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($_GET['offset'] ?? 0));
|
||||
|
||||
$reports = $this->get_reports(['status' => $status, 'limit' => $limit, 'offset' => $offset]);
|
||||
$counts = $this->get_status_counts();
|
||||
|
||||
wp_send_json_success(['reports' => $reports, 'counts' => $counts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get status counts (admin only).
|
||||
*/
|
||||
public function ajax_counts(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_success($this->get_status_counts());
|
||||
return (bool) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE reporter_id = %d AND target_type = %s AND target_id = %d AND status = 'pending' LIMIT 1",
|
||||
$reporter_id,
|
||||
sanitize_key($target_type),
|
||||
$target_id
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user