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>
472 lines
20 KiB
PHP
472 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Exercise report submission, storage, notifications, and admin filtering without WordPress.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Core {
|
|
final class DBMigrator {
|
|
public static function table(string $name): string { return 'wp_aksrt_' . $name; }
|
|
}
|
|
}
|
|
|
|
namespace AKSRT\Settings {
|
|
final class Settings {
|
|
public static array $values = [];
|
|
public static function get_all(): array { return self::$values; }
|
|
public static function read_domain(string $domain, string $section): array {
|
|
unset($domain, $section);
|
|
return self::$values;
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AKSRT\Messages {
|
|
final class Message {
|
|
public static function get_instance(): self { return new self(); }
|
|
public function send(int $user_id, string $type, string $content, array $args = []): void {
|
|
$GLOBALS['aksrt_test_messages'][] = compact('user_id', 'type', 'content', 'args');
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AKSRT\Attachment {
|
|
final class PendingUploads {
|
|
public static function claim_text(string $text, int $user_id): void {
|
|
$GLOBALS['aksrt_test_claims'][] = [$text, $user_id];
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AKSRT\User {
|
|
final class Restriction {
|
|
public const TYPE_WARN = 'warn';
|
|
public const TYPE_MUTE = 'mute';
|
|
public const TYPE_SUSPEND = 'suspend';
|
|
public const TYPE_BAN = 'ban';
|
|
public const TYPES = [self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_SUSPEND, self::TYPE_BAN];
|
|
public const SCOPE_GLOBAL = 'global';
|
|
public static array $calls = [];
|
|
|
|
public static function get_instance(): self { return new self(); }
|
|
public function restrict(int $target_id, string $type, string $reason, string $scope, string $expires_at, int $actor_id): array {
|
|
self::$calls[] = compact('target_id', 'type', 'reason', 'scope', 'expires_at', 'actor_id');
|
|
return ['success' => true];
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
define('ABSPATH', dirname(__DIR__, 2) . DIRECTORY_SEPARATOR);
|
|
define('ARRAY_A', 'ARRAY_A');
|
|
|
|
class WP_User {
|
|
public function __construct(public int $ID, public string $display_name) {}
|
|
}
|
|
|
|
class WP_User_Query {}
|
|
|
|
class WP_Post {
|
|
public string $post_status = 'publish';
|
|
public function __construct(public int $ID) {}
|
|
}
|
|
|
|
class WP_REST_Request {
|
|
public function __construct(private array $params = []) {}
|
|
public function get_json_params(): array { return $this->params; }
|
|
public function get_param(string $key) { return $this->params[$key] ?? null; }
|
|
}
|
|
|
|
class WP_REST_Response {
|
|
public function __construct(private array $data = [], private int $status = 200) {}
|
|
public function get_data(): array { return $this->data; }
|
|
public function get_status(): int { return $this->status; }
|
|
}
|
|
|
|
require_once ABSPATH . 'src/Messages/ReportCenter.php';
|
|
require_once ABSPATH . 'src/Settings/UserRuntime.php';
|
|
require_once ABSPATH . 'src/Admin/UserControllerReportOperations.php';
|
|
|
|
final class TestWpdb {
|
|
public string $prefix = 'wp_';
|
|
public int $insert_id = 0;
|
|
public string $last_error = '';
|
|
public bool $fail_insert = false;
|
|
public array $rows = [];
|
|
public array $insert_calls = [];
|
|
private int $next_id = 1;
|
|
private array $last_args = [];
|
|
|
|
public function prepare(string $query, ...$args): string {
|
|
if (count($args) === 1 && is_array($args[0])) {
|
|
$args = $args[0];
|
|
}
|
|
$this->last_args = $args;
|
|
return $query;
|
|
}
|
|
|
|
public function insert(string $table, array $data, array $format = []) {
|
|
$this->insert_calls[] = [$table, $data, $format];
|
|
if ($this->fail_insert) {
|
|
return false;
|
|
}
|
|
$id = $this->next_id++;
|
|
$this->insert_id = $id;
|
|
$this->rows[] = array_merge(['id' => $id], $data, ['notes_json' => '[]']);
|
|
return 1;
|
|
}
|
|
|
|
public function get_var(string $query) {
|
|
if (str_contains($query, 'reporter_id = %d')) {
|
|
[$reporter_id, $target_type, $target_id] = array_pad($this->last_args, 3, null);
|
|
foreach ($this->rows as $row) {
|
|
if ((int) $row['reporter_id'] === (int) $reporter_id
|
|
&& (string) $row['target_type'] === (string) $target_type
|
|
&& (int) $row['target_id'] === (int) $target_id
|
|
&& (string) $row['status'] === 'pending'
|
|
) {
|
|
return (int) $row['id'];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
if (str_contains($query, 'COUNT(*)') && str_contains($query, 'target_type = %s')) {
|
|
[$target_type, $target_id] = array_pad($this->last_args, 2, null);
|
|
return count(array_filter($this->rows, static fn (array $row): bool =>
|
|
(string) $row['target_type'] === (string) $target_type
|
|
&& (int) $row['target_id'] === (int) $target_id
|
|
));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public function get_results(string $query, $output = null): array {
|
|
if (str_contains($query, 'GROUP BY status')) {
|
|
$counts = [];
|
|
foreach ($this->rows as $row) {
|
|
$status = (string) ($row['status'] ?? '');
|
|
$counts[$status] = ($counts[$status] ?? 0) + 1;
|
|
}
|
|
$results = [];
|
|
foreach ($counts as $status => $count) {
|
|
$results[] = ['status' => $status, 'cnt' => $count];
|
|
}
|
|
return $results;
|
|
}
|
|
|
|
$rows = $this->rows;
|
|
$index = 0;
|
|
foreach (['status', 'category', 'target_type'] as $field) {
|
|
if (str_contains($query, $field . ' = %s')) {
|
|
$value = (string) ($this->last_args[$index++] ?? '');
|
|
$rows = array_values(array_filter($rows, static fn (array $row): bool =>
|
|
(string) ($row[$field] ?? '') === $value
|
|
));
|
|
}
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
public function get_row(string $query, $output = null) {
|
|
$id = (int) ($this->last_args[0] ?? 0);
|
|
foreach ($this->rows as $row) {
|
|
if ((int) $row['id'] === $id) {
|
|
return $output === ARRAY_A ? $row : (object) $row;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function update(string $table, array $data, array $where, array $format = [], array $where_format = []) {
|
|
unset($table, $format, $where_format);
|
|
foreach ($this->rows as &$row) {
|
|
if ((int) ($row['id'] ?? 0) === (int) ($where['id'] ?? 0)) {
|
|
$row = array_merge($row, $data);
|
|
unset($row);
|
|
return 1;
|
|
}
|
|
}
|
|
unset($row);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
final class ReportFixtureController {
|
|
use \AKSRT\Admin\UserControllerReportOperations;
|
|
|
|
protected function success($data = [], int $status = 200): WP_REST_Response {
|
|
return new WP_REST_Response(array_merge(['success' => true], $data), $status);
|
|
}
|
|
|
|
protected function error(string $message, int $status = 400): WP_REST_Response {
|
|
return new WP_REST_Response(['success' => false, 'message' => $message], $status);
|
|
}
|
|
|
|
protected function get_json_params(WP_REST_Request $request): array {
|
|
return $request->get_json_params();
|
|
}
|
|
|
|
private function build_admin_user_summary(int $user_id, $user): array {
|
|
return [
|
|
'id' => $user_id,
|
|
'display_name' => $user ? $user->display_name : '',
|
|
'email' => '',
|
|
];
|
|
}
|
|
}
|
|
|
|
function __(string $text, string $domain = ''): string { return $text; }
|
|
function sanitize_key($value): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $value)) ?? ''; }
|
|
function sanitize_text_field($value): string {
|
|
return trim(preg_replace('/[\r\n\t]+/', ' ', strip_tags((string) $value)) ?? '');
|
|
}
|
|
function sanitize_textarea_field($value): string { return trim(strip_tags((string) $value)); }
|
|
function esc_url_raw($url): string { return (string) $url; }
|
|
function wp_kses_post($value): string { return strip_tags((string) $value); }
|
|
function current_time(string $type): string { unset($type); return '2026-10-30 12:00:00'; }
|
|
function wp_json_encode($value): string { return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '[]'; }
|
|
function get_current_user_id(): int { return (int) ($GLOBALS['aksrt_test_user_id'] ?? 0); }
|
|
function get_userdata(int $user_id) { return $user_id > 0 ? new WP_User($user_id, 'Reporter ' . $user_id) : false; }
|
|
function get_users(array $args = []): array { unset($args); return [901]; }
|
|
function admin_url(string $path = ''): string { return 'https://example.test/wp-admin/' . ltrim($path, '/'); }
|
|
function get_post(int $id) { return $id > 0 ? new WP_Post($id) : false; }
|
|
function get_the_title($post): string { return 'Article #' . (is_object($post) ? $post->ID : (int) $post); }
|
|
function get_permalink($post): string { return 'https://example.test/post/' . (is_object($post) ? $post->ID : (int) $post); }
|
|
function do_action(string $hook, ...$args): void {
|
|
if ($hook === 'aksrt_report_submitted') {
|
|
$GLOBALS['aksrt_test_actions'][] = $args;
|
|
}
|
|
}
|
|
if (!function_exists('mb_substr')) {
|
|
function mb_substr(string $text, int $start, ?int $length = null): string {
|
|
return $length === null ? substr($text, $start) : substr($text, $start, $length);
|
|
}
|
|
}
|
|
|
|
function reset_report_fixture(array $preset_names = ['垃圾信息', '人身攻击']): void {
|
|
$GLOBALS['wpdb'] = new TestWpdb();
|
|
$GLOBALS['aksrt_test_user_id'] = 42;
|
|
$GLOBALS['aksrt_test_actions'] = [];
|
|
$GLOBALS['aksrt_test_messages'] = [];
|
|
$GLOBALS['aksrt_test_claims'] = [];
|
|
\AKSRT\User\Restriction::$calls = [];
|
|
\AKSRT\Settings\Settings::$values = [
|
|
'ban_preset_reason' => array_map(static fn (string $name): array => ['t' => $name], $preset_names),
|
|
'user_report_img_s' => true,
|
|
];
|
|
}
|
|
|
|
function submit_report_case(array $payload): WP_REST_Response {
|
|
return (new ReportFixtureController())->submit_report(new WP_REST_Request($payload));
|
|
}
|
|
|
|
function snapshot_response(WP_REST_Response $response): array {
|
|
$wpdb = $GLOBALS['wpdb'];
|
|
return [
|
|
'response' => $response->get_data(),
|
|
'status' => $response->get_status(),
|
|
'rows' => $wpdb->rows,
|
|
'insert_calls' => $wpdb->insert_calls,
|
|
'actions' => $GLOBALS['aksrt_test_actions'],
|
|
'messages' => $GLOBALS['aksrt_test_messages'],
|
|
'claims' => $GLOBALS['aksrt_test_claims'],
|
|
];
|
|
}
|
|
|
|
$results = [];
|
|
|
|
reset_report_fixture(['垃圾信息', '人身攻击']);
|
|
$results['runtime_reasons'] = \AKSRT\Settings\UserRuntime::ban_settings()['reasons'];
|
|
|
|
reset_report_fixture();
|
|
$results['preset_with_evidence'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post',
|
|
'target_id' => 73,
|
|
'category' => '垃圾信息',
|
|
'reason' => '补充详情 https://example.test/uploads/proof.png',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$results['preset_label_only'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'comment',
|
|
'target_id' => 88,
|
|
'category' => '人身攻击',
|
|
'reason' => '',
|
|
]));
|
|
|
|
reset_report_fixture(['0']);
|
|
$results['falsey_text'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post',
|
|
'target_id' => 90,
|
|
'category' => '0',
|
|
'reason' => '0',
|
|
]));
|
|
|
|
$long_label = '非常长的预置举报分类名称超过数据库原先四十字符限制并且应完整保留在工单及后台列表当中';
|
|
reset_report_fixture([$long_label]);
|
|
$results['long_preset_label'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post',
|
|
'target_id' => 19,
|
|
'category' => $long_label,
|
|
'reason' => '',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$results['other_with_details'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'comment',
|
|
'target_id' => 99,
|
|
'category' => '其他原因',
|
|
'reason' => '请核查相关资料',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$results['sanitized_snapshot'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post',
|
|
'target_id' => 12,
|
|
'category' => ' <b>垃圾信息</b> ',
|
|
'reason' => '<strong>补充详情</strong>',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$results['blank_category'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 5, 'category' => '', 'reason' => '详情',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$results['missing_other_details'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 5, 'category' => '其他原因', 'reason' => ' ',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$results['unknown_category'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 5, 'category' => '未配置的分类', 'reason' => '详情',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$results['hash_category'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 5, 'category' => 'ab73c91f054e', 'reason' => '详情',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$GLOBALS['aksrt_test_user_id'] = 0;
|
|
$results['unauthenticated'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 5, 'category' => '垃圾信息', 'reason' => '',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$results['invalid_target'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 0, 'category' => '垃圾信息', 'reason' => '',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$GLOBALS['wpdb']->fail_insert = true;
|
|
$GLOBALS['wpdb']->insert_id = 777;
|
|
$results['insert_failure'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 5, 'category' => '垃圾信息', 'reason' => '含有举证',
|
|
]));
|
|
|
|
reset_report_fixture();
|
|
$GLOBALS['wpdb']->rows[] = [
|
|
'id' => 51,
|
|
'reporter_id' => 42,
|
|
'target_type' => 'post',
|
|
'target_id' => 73,
|
|
'reason' => '既有详情',
|
|
'category' => '垃圾信息',
|
|
'status' => 'pending',
|
|
'created_at' => '2026-10-30 11:00:00',
|
|
'updated_at' => '',
|
|
'notes_json' => '[]',
|
|
];
|
|
$results['duplicate'] = snapshot_response(submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 73, 'category' => '垃圾信息', 'reason' => '重复提交',
|
|
]));
|
|
|
|
reset_report_fixture(['涉嫌侵权']);
|
|
$admin_controller = new ReportFixtureController();
|
|
$submit_response = submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 31, 'category' => '涉嫌侵权', 'reason' => '',
|
|
]);
|
|
$list_response = $admin_controller->get_report_list(new WP_REST_Request(['category' => '涉嫌侵权']));
|
|
$list_data = $list_response->get_data()['data'] ?? [];
|
|
$detail_response = $admin_controller->get_report_detail(new WP_REST_Request(['id' => 1]));
|
|
$detail_data = $detail_response->get_data()['data'] ?? [];
|
|
$results['admin_category'] = [
|
|
'submit_success' => !empty($submit_response->get_data()['success']),
|
|
'list_items' => $list_data['items'] ?? [],
|
|
'list_options' => $list_data['options']['categories'] ?? [],
|
|
'detail_report' => $detail_data['report'] ?? [],
|
|
];
|
|
|
|
reset_report_fixture();
|
|
$service_id = \AKSRT\Messages\ReportCenter::get_instance()->submit(42, 'post', 6, '详情', '');
|
|
$results['service_blank_category'] = [
|
|
'id' => $service_id,
|
|
'rows' => $GLOBALS['wpdb']->rows,
|
|
'insert_calls' => $GLOBALS['wpdb']->insert_calls,
|
|
];
|
|
|
|
reset_report_fixture(['提交时的分类', '待删除的分类']);
|
|
$first_snapshot_submit = submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 61, 'category' => '提交时的分类', 'reason' => '',
|
|
]);
|
|
$second_snapshot_submit = submit_report_case([
|
|
'target_type' => 'post', 'target_id' => 62, 'category' => '待删除的分类', 'reason' => '',
|
|
]);
|
|
$admin_controller = new ReportFixtureController();
|
|
\AKSRT\Settings\Settings::$values['ban_preset_reason'] = [['t' => '当前新分类']];
|
|
$changed_list = $admin_controller->get_report_list(new WP_REST_Request())->get_data()['data'] ?? [];
|
|
\AKSRT\Settings\Settings::$values['ban_preset_reason'] = [];
|
|
$removed_list = $admin_controller->get_report_list(new WP_REST_Request())->get_data()['data'] ?? [];
|
|
$first_detail = $admin_controller->get_report_detail(new WP_REST_Request(['id' => 1]))->get_data()['data']['report'] ?? [];
|
|
$second_detail = $admin_controller->get_report_detail(new WP_REST_Request(['id' => 2]))->get_data()['data']['report'] ?? [];
|
|
$results['category_snapshot'] = [
|
|
'submit_success' => !empty($first_snapshot_submit->get_data()['success']) && !empty($second_snapshot_submit->get_data()['success']),
|
|
'changed_list_categories' => array_column($changed_list['items'] ?? [], 'category'),
|
|
'changed_list_options' => array_column($changed_list['options']['categories'] ?? [], 'value'),
|
|
'removed_list_categories' => array_column($removed_list['items'] ?? [], 'category'),
|
|
'details' => [
|
|
(string) ($first_detail['category'] ?? ''),
|
|
(string) ($second_detail['category'] ?? ''),
|
|
],
|
|
];
|
|
|
|
reset_report_fixture(['人身攻击']);
|
|
$GLOBALS['wpdb']->rows[] = [
|
|
'id' => 101, 'reporter_id' => 42, 'target_type' => 'user', 'target_id' => 701,
|
|
'reason' => '', 'category' => '人身攻击', 'status' => 'pending',
|
|
'created_at' => '2026-10-30 11:00:00', 'updated_at' => null, 'notes_json' => '[]',
|
|
];
|
|
$preset_process = (new ReportFixtureController())->process_report(new WP_REST_Request([
|
|
'id' => 101, 'status' => 'resolved', 'action_taken' => 'warn', 'note' => '',
|
|
]));
|
|
$preset_row = $GLOBALS['wpdb']->rows[0];
|
|
$results['process_preset_only'] = [
|
|
'success' => !empty($preset_process->get_data()['success']),
|
|
'restriction' => \AKSRT\User\Restriction::$calls[0] ?? [],
|
|
'report_reason' => $preset_row['reason'],
|
|
'report_category' => $preset_row['category'],
|
|
];
|
|
|
|
reset_report_fixture();
|
|
$custom_reason = '自定义说明不能丢';
|
|
$GLOBALS['wpdb']->rows[] = [
|
|
'id' => 102, 'reporter_id' => 42, 'target_type' => 'user', 'target_id' => 702,
|
|
'reason' => $custom_reason, 'category' => '其他原因', 'status' => 'pending',
|
|
'created_at' => '2026-10-30 11:00:00', 'updated_at' => null, 'notes_json' => '[]',
|
|
];
|
|
$other_process = (new ReportFixtureController())->process_report(new WP_REST_Request([
|
|
'id' => 102, 'status' => 'resolved', 'action_taken' => 'warn', 'note' => '',
|
|
]));
|
|
$other_row = $GLOBALS['wpdb']->rows[0];
|
|
$results['process_other_with_details'] = [
|
|
'success' => !empty($other_process->get_data()['success']),
|
|
'restriction' => \AKSRT\User\Restriction::$calls[0] ?? [],
|
|
'report_reason' => $other_row['reason'],
|
|
'report_category' => $other_row['category'],
|
|
];
|
|
|
|
echo json_encode($results, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
|
}
|