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 展示态 => 徽章字段 */ 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|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]; } }