对照参考主题(Zibll)修正四项偏差,申诉链路从页面整体迁移至弹窗:
- feat(ban): 新增三个 1:1 复刻弹窗(REST /user-ban/{info,appeal,log}-modal):
查看禁封状态(多彩头 c-gray + 信息行 + 申诉按钮)、禁封申诉(edit-header
gavel/c-blue + 信息框 + 资料项 data[key] 输入 + 审核中态 + 禁止申诉态)、
禁封记录(多彩头 c-yellow + 计数 + 逐条卡片);user-ban.js 增加
data-aksrt-ban-info/appeal/log 委托与 admin-ajax 申诉提交
- feat(ban): 禁封/解封弹窗的「当前用户已有N次禁封记录」静态文案改为可点击
黄链(N>1),打开禁封记录弹窗;解封视图信息行改复用统一数据源
- refactor(ban): 治理展示态收敛至 Restriction::state_badges()(封号=c-red
违规已封号 / 小黑屋=c-yellow 小黑屋禁封中 / 禁言=c-gray 禁言中,颜色与
字段统一管理)+ get_ban_info()(状态/起止时间/原因/说明/申诉权限,
三弹窗与禁封页共用)
- feat(ban): 登录行为对齐参考主题——移除登录期拦截(enforce_login_ban)
与 complete_login 禁封闸门,封号用户可正常登录;新增 template_redirect
前台禁封页(1:1 zib_user_ban_restrict_login/zib_die_page:红盒提示 +
信息行 + 申诉权限 + 退出登录/申诉按钮,模板 templates/parts/user/ban-block.php)
- feat(profile): 公开主页禁封状态从资料卡下方提示框改为占据身份行元信息位
的 badg 徽章(参考主题 author_header_identity 语义);用户中心元信息改为
but 按钮(徽章+箭头)打开查看禁封状态弹窗;评论拒绝态链接同源
- refactor: 移除申诉页面模板(page-ban-appeal.php)及其资产/注册/入队、
get_profile_notice/profile_notice 链、state_labels/state_notices/
login_ban_message、UserRuntime 的 public_notice/ban_notice_text 死配置、
report_settings 死键、无消费方的 aksrt_ban_appeal_status 端点
- feat(ban): BanAppeal::ajax_submit 兼容弹窗 data[key] 表单结构
(逐项清洗入库,申诉说明取 data['申诉说明'],旧平铺字段保持兼容)
- chore: 重新构建 public.css/public.min.css(三个新弹窗几何与头部样式)
- test: 契约测试重写(徽章元信息位、三弹窗与记录链接、禁封页、允许登录 +
前台拦截、死代码移除断言),全量 362 项通过
376 lines
14 KiB
PHP
376 lines
14 KiB
PHP
<?php
|
||
/**
|
||
* @package AKSRT
|
||
*/
|
||
|
||
namespace AKSRT\User;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
class Restriction {
|
||
|
||
private static $instance = null;
|
||
|
||
const TYPE_WARN = 'warn';
|
||
const TYPE_MUTE = 'mute';
|
||
const TYPE_SUSPEND = 'suspend';
|
||
const TYPE_BAN = 'ban';
|
||
|
||
const SCOPE_GLOBAL = 'global';
|
||
const SCOPE_COMMENT = 'comment';
|
||
|
||
const TYPES = [self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_SUSPEND, self::TYPE_BAN];
|
||
const SCOPES = [self::SCOPE_GLOBAL, self::SCOPE_COMMENT];
|
||
|
||
/**
|
||
* 治理展示态(与 user-home.css 的 --banned/--restricted/--muted 色调修饰符同名):
|
||
* 资料卡提示框、用户中心页头、评论拒绝态、昵称徽章共用同一组键。
|
||
*/
|
||
const STATE_BANNED = 'banned';
|
||
const STATE_RESTRICTED = 'restricted';
|
||
const STATE_MUTED = 'muted';
|
||
|
||
public static function get_instance(): self {
|
||
if (null === self::$instance) {
|
||
self::$instance = new self();
|
||
}
|
||
return self::$instance;
|
||
}
|
||
|
||
public function init(): void {
|
||
// 禁封用户前台拦截(对齐参考主题 zib_user_ban_restrict_login:
|
||
// 封号用户可正常登录,但前台任何页面均展示禁封状态页;禁封中
|
||
// 不得拦截登录本身——登录后的落地页即禁封页)。
|
||
add_action('template_redirect', [$this, 'restrict_frontend_access'], 1);
|
||
// Enforce comment mute
|
||
add_filter('pre_comment_on_post', [$this, 'enforce_comment_mute']);
|
||
}
|
||
|
||
/**
|
||
* Get the restriction table name.
|
||
*/
|
||
private function table(): string {
|
||
global $wpdb;
|
||
return $wpdb->prefix . 'aksrt_user_restriction';
|
||
}
|
||
|
||
/**
|
||
* Add a restriction to a user.
|
||
*/
|
||
public function restrict(int $user_id, string $type, string $reason, string $scope = self::SCOPE_GLOBAL, string $expires_at = '', int $created_by = 0): array {
|
||
global $wpdb;
|
||
|
||
if (!in_array($type, self::TYPES, true)) {
|
||
return ['success' => false, 'message' => 'Invalid restriction type.'];
|
||
}
|
||
if (!in_array($scope, self::SCOPES, true)) {
|
||
return ['success' => false, 'message' => 'Invalid scope.'];
|
||
}
|
||
if ($created_by <= 0) {
|
||
$created_by = get_current_user_id();
|
||
}
|
||
|
||
$data = [
|
||
'user_id' => $user_id,
|
||
'type' => $type,
|
||
'reason' => sanitize_textarea_field($reason),
|
||
'scope' => $scope,
|
||
'expires_at' => $expires_at ? gmdate('Y-m-d H:i:s', strtotime($expires_at)) : null,
|
||
'created_by' => $created_by,
|
||
'created_at' => current_time('mysql', true),
|
||
];
|
||
|
||
$result = $wpdb->insert($this->table(), $data);
|
||
if ($result === false) {
|
||
return ['success' => false, 'message' => 'Database error.'];
|
||
}
|
||
|
||
// 封禁立即生效:销毁目标用户全部登录会话
|
||
if ($type === self::TYPE_BAN && $user_id > 0) {
|
||
\WP_Session_Tokens::get_instance($user_id)->destroy_all();
|
||
}
|
||
|
||
do_action('aksrt_user_restriction_changed', $user_id, $type, 'applied', (string) $data['reason']);
|
||
|
||
return ['success' => true, 'id' => $wpdb->insert_id];
|
||
}
|
||
|
||
/**
|
||
* Remove a restriction by ID.
|
||
*/
|
||
public function remove_restriction(int $restriction_id): bool {
|
||
global $wpdb;
|
||
$restriction = $this->get_restriction($restriction_id);
|
||
if (!$restriction) return false;
|
||
|
||
$result = $wpdb->delete($this->table(), ['id' => $restriction_id]);
|
||
if ($result) {
|
||
do_action('aksrt_user_restriction_changed', (int) $restriction['user_id'], (string) ($restriction['type'] ?? ''), 'removed', '');
|
||
}
|
||
return (bool) $result;
|
||
}
|
||
|
||
/**
|
||
* 解除用户生效中的限制(可选按类型过滤),用于申诉通过或举报工单"解除限制"。
|
||
*
|
||
* @param int $user_id 用户 ID
|
||
* @param array $types 要解除的类型,默认解除 ban/suspend/mute(保留 warn 警告记录)
|
||
* @return int 解除的记录数
|
||
*/
|
||
public function lift_user_restriction(int $user_id, array $types = [self::TYPE_BAN, self::TYPE_SUSPEND, self::TYPE_MUTE]): int {
|
||
global $wpdb;
|
||
|
||
$types = array_values(array_intersect($types, self::TYPES));
|
||
if ($user_id <= 0 || empty($types)) {
|
||
return 0;
|
||
}
|
||
|
||
$placeholders = implode(',', array_fill(0, count($types), '%s'));
|
||
$deleted = $wpdb->query($wpdb->prepare(
|
||
"DELETE FROM {$this->table()} WHERE user_id = %d AND (expires_at IS NULL OR expires_at > %s) AND type IN ({$placeholders})",
|
||
array_merge([$user_id, current_time('mysql', true)], $types)
|
||
));
|
||
|
||
if ($deleted > 0) {
|
||
do_action('aksrt_user_restriction_changed', $user_id, implode(',', $types), 'lifted', '');
|
||
}
|
||
|
||
return (int) $deleted;
|
||
}
|
||
|
||
/**
|
||
* Get a single restriction by ID.
|
||
*/
|
||
public function get_restriction(int $restriction_id): ?array {
|
||
global $wpdb;
|
||
$row = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT * FROM {$this->table()} WHERE id = %d",
|
||
$restriction_id
|
||
), ARRAY_A);
|
||
return $row ?: null;
|
||
}
|
||
|
||
/**
|
||
* Get all active restrictions for a user.
|
||
*/
|
||
public function get_user_restrictions(int $user_id, bool $active_only = true): array {
|
||
global $wpdb;
|
||
$now = current_time('mysql', true);
|
||
|
||
if ($active_only) {
|
||
$rows = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM {$this->table()} WHERE user_id = %d AND (expires_at IS NULL OR expires_at > %s) ORDER BY created_at DESC",
|
||
$user_id, $now
|
||
), ARRAY_A);
|
||
} else {
|
||
$rows = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM {$this->table()} WHERE user_id = %d ORDER BY created_at DESC",
|
||
$user_id
|
||
), ARRAY_A);
|
||
}
|
||
|
||
return $rows ?: [];
|
||
}
|
||
|
||
/**
|
||
* Check if a user has an active restriction of a given type and scope.
|
||
*/
|
||
public function is_restricted(int $user_id, string $type = '', string $scope = ''): bool {
|
||
$restrictions = $this->get_user_restrictions($user_id, true);
|
||
foreach ($restrictions as $r) {
|
||
if ($type && $r['type'] !== $type) continue;
|
||
if ($scope && $r['scope'] !== $scope && $r['scope'] !== self::SCOPE_GLOBAL) continue;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Check if a user is banned (global).
|
||
*/
|
||
public function is_banned(int $user_id): bool {
|
||
return $this->is_restricted($user_id, self::TYPE_BAN, self::SCOPE_GLOBAL);
|
||
}
|
||
|
||
/**
|
||
* 治理展示态(空串 = 无治理状态;ban 优先于 suspend,对齐后台单一状态语义)。
|
||
*
|
||
* 供公开主页身份行徽章、用户中心元信息、评论拒绝态、昵称徽章共用,
|
||
* 徽章字段(文案/颜色/图标)见 state_badges()。
|
||
*/
|
||
public function get_display_state(int $user_id): string {
|
||
if ($user_id <= 0) {
|
||
return '';
|
||
}
|
||
if ($this->is_banned($user_id)) {
|
||
return self::STATE_BANNED;
|
||
}
|
||
if ($this->is_suspended($user_id)) {
|
||
return self::STATE_RESTRICTED;
|
||
}
|
||
if ($this->is_restricted($user_id, self::TYPE_MUTE)) {
|
||
return self::STATE_MUTED;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 治理展示态徽章字段(颜色 / 文案 / 图标统一管理,全前端提示面共用):
|
||
* 封号(ban) → c-red 违规已封号(最严重:无法登录);
|
||
* 小黑屋(suspend) → c-yellow 小黑屋禁封中(部分限制);
|
||
* 禁言(mute) → c-gray 禁言中(评论域限制)。
|
||
* 对齐参考主题 zib_ban_author_header_identity_filter 的红/默认双色语义,
|
||
* 禁言为 AKSRT 扩展状态。
|
||
*
|
||
* @return array<string,array{label:string,tone:string,icon:string}> 展示态 => 徽章字段
|
||
*/
|
||
public static function state_badges(): array {
|
||
return [
|
||
self::STATE_BANNED => ['label' => __('违规已封号', 'aksrt'), 'tone' => 'c-red', 'icon' => 'ban'],
|
||
self::STATE_RESTRICTED => ['label' => __('小黑屋禁封中', 'aksrt'), 'tone' => 'c-yellow', 'icon' => 'ban'],
|
||
self::STATE_MUTED => ['label' => __('禁言中', 'aksrt'), 'tone' => 'c-gray', 'icon' => 'comment-slash'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 生效禁封信息行(查看禁封状态弹窗 / 申诉弹窗 / 登录后禁封页 / 管理端
|
||
* 禁封解封弹窗共用;对齐参考主题 zib_get_ban_info_modal 的字段集)。
|
||
*
|
||
* @return array<string,mixed>|array{} 空数组表示无生效禁封/小黑屋。
|
||
*/
|
||
public function get_ban_info(int $user_id): array {
|
||
if ($user_id <= 0) {
|
||
return [];
|
||
}
|
||
$ban = $this->get_active_ban($user_id) ?: $this->get_active_suspend($user_id);
|
||
if (!is_array($ban)) {
|
||
return [];
|
||
}
|
||
|
||
$type = (string) ($ban['type'] ?? '');
|
||
$raw = (string) ($ban['reason'] ?? '');
|
||
$reason = $raw;
|
||
$desc = '';
|
||
$marker = "\n" . __('禁封说明:', 'aksrt');
|
||
$pos = strpos($raw, $marker);
|
||
if (false !== $pos) {
|
||
$reason = substr($raw, 0, $pos);
|
||
$desc = trim(substr($raw, $pos + strlen($marker)));
|
||
}
|
||
|
||
$no_appeal = get_user_meta($user_id, '_aksrt_no_appeal', true);
|
||
$no_appeal_desc = is_array($no_appeal) ? trim((string) ($no_appeal['desc'] ?? '')) : '';
|
||
|
||
return [
|
||
'type' => $type,
|
||
'status' => $type === self::TYPE_BAN ? __('禁封中(禁止登录)', 'aksrt') : __('小黑屋禁封中', 'aksrt'),
|
||
'start' => (string) ($ban['created_at'] ?? ''),
|
||
'end' => !empty($ban['expires_at']) ? (string) $ban['expires_at'] : __('永久', 'aksrt'),
|
||
'reason' => trim($reason),
|
||
'desc' => $desc,
|
||
'no_appeal' => $no_appeal_desc !== '',
|
||
'no_appeal_desc' => $no_appeal_desc,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Check if a user is in 小黑屋 (active global suspend).
|
||
*
|
||
* 对齐参考主题语义:小黑屋用户失去所有发布权限(发帖、评论等)及大部分操作权限。
|
||
*/
|
||
public function is_suspended(int $user_id): bool {
|
||
return $this->is_restricted($user_id, self::TYPE_SUSPEND, self::SCOPE_GLOBAL);
|
||
}
|
||
|
||
/**
|
||
* Get the active suspend restriction for a user (if any).
|
||
*/
|
||
public function get_active_suspend(int $user_id): ?array {
|
||
$restrictions = $this->get_user_restrictions($user_id, true);
|
||
foreach ($restrictions as $r) {
|
||
if ($r['type'] === self::TYPE_SUSPEND && $r['scope'] === self::SCOPE_GLOBAL) {
|
||
return $r;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Get the active ban restriction for a user (if any).
|
||
*/
|
||
public function get_active_ban(int $user_id): ?array {
|
||
$restrictions = $this->get_user_restrictions($user_id, true);
|
||
foreach ($restrictions as $r) {
|
||
if ($r['type'] === self::TYPE_BAN && $r['scope'] === self::SCOPE_GLOBAL) {
|
||
return $r;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 前台禁封拦截页(1:1 复刻参考主题 zib_user_ban_restrict_login /
|
||
* zib_die_page):封号(ban)用户登录后访问任何前台页面,均展示禁封状态页
|
||
* (红盒提示 + 禁封信息行 + 申诉权限 + 退出登录/申诉按钮)。
|
||
* 小黑屋/禁言为部分限制,可正常浏览站点,不走此拦截。
|
||
*/
|
||
public function restrict_frontend_access(): void {
|
||
if (is_admin() || wp_doing_cron() || wp_doing_ajax()) {
|
||
return;
|
||
}
|
||
$user_id = get_current_user_id();
|
||
if ($user_id <= 0 || !$this->is_banned($user_id)) {
|
||
return;
|
||
}
|
||
if (!function_exists('aksrt_render_user_ban_block_page')) {
|
||
require_once AKSRT_DIR . '/inc/helpers/user-ban.php';
|
||
}
|
||
aksrt_render_user_ban_block_page($user_id);
|
||
}
|
||
|
||
/**
|
||
* Enforce comment restriction on submission (mute / 小黑屋).
|
||
*
|
||
* 禁言(mute,评论域)与小黑屋(suspend,全域)都会拦截评论——
|
||
* 对齐参考主题:小黑屋用户失去评论等发布权限。
|
||
*/
|
||
public function enforce_comment_mute($post_id) {
|
||
$user_id = get_current_user_id();
|
||
if ($user_id <= 0) {
|
||
return $post_id;
|
||
}
|
||
if ($this->is_restricted($user_id, self::TYPE_MUTE, self::SCOPE_COMMENT)) {
|
||
wp_die('您已被限制评论。', '评论限制', ['response' => 403]);
|
||
}
|
||
if ($this->is_suspended($user_id)) {
|
||
$suspend = $this->get_active_suspend($user_id);
|
||
$message = '您已被关入小黑屋,暂时无法评论。';
|
||
if (!empty($suspend['reason'])) {
|
||
$message .= '原因:' . $suspend['reason'];
|
||
}
|
||
wp_die($message, '发布限制', ['response' => 403]);
|
||
}
|
||
return $post_id;
|
||
}
|
||
|
||
/**
|
||
* Get restriction stats for admin overview.
|
||
*/
|
||
public function get_stats(): array {
|
||
global $wpdb;
|
||
$now = current_time('mysql', true);
|
||
$table = $this->table();
|
||
|
||
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
|
||
$active = (int) $wpdb->get_var($wpdb->prepare(
|
||
"SELECT COUNT(*) FROM {$table} WHERE expires_at IS NULL OR expires_at > %s", $now
|
||
));
|
||
$banned = (int) $wpdb->get_var($wpdb->prepare(
|
||
"SELECT COUNT(*) FROM {$table} WHERE type = %s AND (expires_at IS NULL OR expires_at > %s)",
|
||
self::TYPE_BAN, $now
|
||
));
|
||
|
||
return ['total' => $total, 'active' => $active, 'banned' => $banned];
|
||
}
|
||
}
|