diff --git a/assets/js/report.js b/assets/js/report.js index bdfd4111..7b7a047c 100644 --- a/assets/js/report.js +++ b/assets/js/report.js @@ -41,7 +41,7 @@ name: String((cat && (cat.name || cat.label || cat.t)) || ''), }; }) - .filter(function (cat) { return cat.slug !== '' && cat.name !== ''; }); + .filter(function (cat) { return cat.name !== ''; }); if (items.length) return items; } return [ diff --git a/src/Settings/UserRuntime.php b/src/Settings/UserRuntime.php index ca881667..690c56fe 100644 --- a/src/Settings/UserRuntime.php +++ b/src/Settings/UserRuntime.php @@ -216,14 +216,21 @@ class UserRuntime { public static function ban_settings(): array { $settings = self::read_report_section(); - // 预置原因:group 数组 [{t:...}](对齐参考主题)或旧版每行一条文本 + // 预置原因:group 数组 [{t:...}](对齐参考主题)或旧版每行一条文本。 + // slug 不能用 sanitize_key(标签) 派生:中文标签会被清洗成空串, + // 导致前端按 slug 非空过滤后整列表被丢弃。改用标签内容的稳定短哈希 + // (跨请求一致、可通过服务端 sanitize_key、ASCII 标签同样稳定)。 $reasons_raw = $settings['ban_preset_reason'] ?? null; $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' => sanitize_key($text), 'name' => sanitize_text_field($text)]; + $reasons[] = ['slug' => $reason_slug($text), 'name' => sanitize_text_field($text)]; } } } else { @@ -234,7 +241,7 @@ class UserRuntime { } $parts = explode('|', $line, 2); $reasons[] = [ - 'slug' => sanitize_key($parts[0]), + 'slug' => $reason_slug((string) ($parts[1] ?? $parts[0])), 'name' => sanitize_text_field($parts[1] ?? $parts[0]), ]; } diff --git a/tests/dev/verify-ban-settings-defaults.php b/tests/dev/verify-ban-settings-defaults.php new file mode 100644 index 00000000..6e5a006f --- /dev/null +++ b/tests/dev/verify-ban-settings-defaults.php @@ -0,0 +1,73 @@ + $report['enabled'] ?? null, + 'img_enabled' => $report['img_enabled'] ?? null, + 'desc_len' => isset($report['desc']) ? strlen($report['desc']) : null, +]); + +// 再模拟“后台保存过自定义原因”的场景 +$GLOBALS['__test_options']['aksrt_settings'] = [ + 'ban_preset_reason' => [ + ['t' => '自定义原因A'], + ['t' => '自定义原因B'], + ], +]; +\AKSRT\Settings\Settings::flush_settings_cache(); +$stored = \AKSRT\Settings\UserRuntime::ban_settings()['reasons'] ?? null; +echo "\n\n=== ban_settings()['reasons'] (custom stored) ===\n"; +var_export($stored); +echo "\n"; diff --git a/tests/widget-system/report-ban-contract.test.cjs b/tests/widget-system/report-ban-contract.test.cjs index 1f78bfe9..324843f3 100644 --- a/tests/widget-system/report-ban-contract.test.cjs +++ b/tests/widget-system/report-ban-contract.test.cjs @@ -108,6 +108,10 @@ test('ban/report settings page mirrors the reference theme schema', () => { // 缺键回退 schema 默认值(参考主题 _pz 语义):运行时经 read_domain 读取, // 裸 get_all() 会让未保存过分区的站点拿不到 ban_preset_reason 默认值。 assert.match(runtime, /read_domain\('users', 'report'\)/) + // 预置原因 slug 不得由 sanitize_key(标签) 直接派生:中文标签会被清洗成 + // 空串,前端按 slug 非空过滤后整张原因列表会被丢弃。ASCII 键优先, + // 否则用标签内容的稳定 md5 短哈希。 + assert.match(runtime, /md5\(\$text\)/) // 死链清理:user_report_categories 从不是真实存储键,解析器必须删除。 assert.ok(!runtime.includes('parse_report_categories'), 'dead parse_report_categories must stay removed') assert.ok(!runtime.includes('user_report_categories'), 'dead user_report_categories key must stay removed')