Files
wordpress-theme-aksrt/inc/classes/auth/class-aksrt-email-rollback.php
T
kate522 ce1c13b947 refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories
(core, auth, user, credits, content, payment/gateways, forum, shop,
sms, ai, messages, extensions, widgets, settings, admin). Update
bootstrap require paths, co-locate traits with owners, fix forum/user
relative requires, drop duplicate shop controller requires, and wire
home-modules load. Path-only; class names and init order unchanged.
2026-07-17 11:11:57 +08:00

229 lines
8.3 KiB
PHP

<?php
/**
* AKSRT Email Rollback
*
* 用户修改邮箱后会写入两条 user_meta:
* - aksrt_email_previous 旧邮箱
* - aksrt_email_changed_at 时间戳
*
* 14 天内管理员可执行回滚:把账号邮箱改回旧值并清除回滚 meta。
*
* 入口:
* - 后台用户列表新增"邮箱回滚"行操作
* - REST: GET /aksrt/v1/email-rollback/users 列出窗口内可回滚的用户
* - REST: POST /aksrt/v1/email-rollback/users/{id}/rollback 执行回滚
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Email_Rollback {
const META_PREVIOUS = 'aksrt_email_previous';
const META_CHANGED_AT = 'aksrt_email_changed_at';
const WINDOW_SECONDS = 1209600; // 14 days
private static $instance = null;
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public function init(): void {
add_filter('user_row_actions', [$this, 'add_row_action'], 20, 2);
add_action('admin_post_aksrt_email_rollback', [$this, 'handle_admin_rollback']);
// cron 清理过期 meta
add_action('aksrt_email_rollback_cleanup', [self::class, 'cron_cleanup']);
add_action('wp', [self::class, 'maybe_schedule_cleanup']);
}
public static function maybe_schedule_cleanup(): void {
if (!wp_next_scheduled('aksrt_email_rollback_cleanup')) {
wp_schedule_event(time() + DAY_IN_SECONDS, 'daily', 'aksrt_email_rollback_cleanup');
}
}
public static function cron_cleanup(): void {
global $wpdb;
$cutoff = time() - self::WINDOW_SECONDS;
$rows = $wpdb->get_col($wpdb->prepare(
"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND CAST(meta_value AS UNSIGNED) < %d",
self::META_CHANGED_AT,
$cutoff
));
if (empty($rows)) return;
foreach ($rows as $uid) {
delete_user_meta((int) $uid, self::META_PREVIOUS);
delete_user_meta((int) $uid, self::META_CHANGED_AT);
}
}
/**
* 在 14 天窗口内?
*/
public static function is_within_window(int $user_id): bool {
$changed_at = (int) get_user_meta($user_id, self::META_CHANGED_AT, true);
if ($changed_at <= 0) return false;
return (time() - $changed_at) <= self::WINDOW_SECONDS;
}
/**
* 列出窗口内可回滚的用户。
*/
public static function list_pending(int $page = 1, int $per_page = 20): array {
global $wpdb;
$page = max(1, $page);
$per_page = min(100, max(1, $per_page));
$offset = ($page - 1) * $per_page;
$cutoff = time() - self::WINDOW_SECONDS;
$sql_count = $wpdb->prepare(
"SELECT COUNT(DISTINCT um1.user_id)
FROM {$wpdb->usermeta} um1
INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s
WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d",
self::META_PREVIOUS,
self::META_CHANGED_AT,
$cutoff
);
$total = (int) $wpdb->get_var($sql_count);
$sql = $wpdb->prepare(
"SELECT um1.user_id, um1.meta_value AS changed_at, um2.meta_value AS previous_email
FROM {$wpdb->usermeta} um1
INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s
WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d
ORDER BY CAST(um1.meta_value AS UNSIGNED) DESC
LIMIT %d OFFSET %d",
self::META_PREVIOUS,
self::META_CHANGED_AT,
$cutoff,
$per_page,
$offset
);
$rows = $wpdb->get_results($sql, ARRAY_A);
$items = [];
foreach ((array) $rows as $r) {
$uid = (int) $r['user_id'];
$u = get_userdata($uid);
if (!$u) continue;
$items[] = [
'user_id' => $uid,
'login' => $u->user_login,
'display_name' => $u->display_name,
'current_email' => $u->user_email,
'previous_email' => (string) $r['previous_email'],
'changed_at' => (int) $r['changed_at'],
'expires_in' => max(0, (int) $r['changed_at'] + self::WINDOW_SECONDS - time()),
];
}
return [
'items' => $items,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
];
}
/**
* 执行回滚。
*
* @return array{success:bool, message?:string, error?:string}
*/
public static function rollback(int $user_id, int $by_admin_id = 0): array {
if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
}
if (!self::is_within_window($user_id)) {
return ['success' => false, 'error' => __('回滚窗口已过期或当前账号未发起过邮箱变更', 'aksrt')];
}
$previous = (string) get_user_meta($user_id, self::META_PREVIOUS, true);
if ($previous === '' || !is_email($previous)) {
return ['success' => false, 'error' => __('旧邮箱无效', 'aksrt')];
}
// 检查旧邮箱是否被其他用户占用(理论上极少见)
$occupier = email_exists($previous);
if ($occupier && (int) $occupier !== $user_id) {
return ['success' => false, 'error' => __('旧邮箱已被其他账号占用,无法回滚', 'aksrt')];
}
$update = wp_update_user([
'ID' => $user_id,
'user_email' => $previous,
]);
if (is_wp_error($update)) {
return ['success' => false, 'error' => $update->get_error_message()];
}
// 清空回滚 meta(一次性)
delete_user_meta($user_id, self::META_PREVIOUS);
delete_user_meta($user_id, self::META_CHANGED_AT);
// 记录管理员操作日志
$log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true);
if (!is_array($log)) $log = [];
$log[] = [
't' => time(),
'action' => 'email_rollback',
'by' => $by_admin_id ?: get_current_user_id(),
];
if (count($log) > 10) $log = array_slice($log, -10);
update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
if (class_exists('AKSRT_Auth_Service')) {
AKSRT_Auth_Service::log_event('admin.email_rollback', [
'uid' => $user_id,
'by' => $by_admin_id ?: get_current_user_id(),
]);
}
return [
'success' => true,
'message' => sprintf(__('已将用户邮箱回滚为 %s', 'aksrt'), $previous),
];
}
/**
* 在用户列表行操作中加入"回滚邮箱"。
*/
public function add_row_action(array $actions, WP_User $user): array {
if (!current_user_can('edit_user', $user->ID)) return $actions;
if (!self::is_within_window((int) $user->ID)) return $actions;
$url = wp_nonce_url(
admin_url('admin-post.php?action=aksrt_email_rollback&user_id=' . (int) $user->ID),
'aksrt_email_rollback_' . (int) $user->ID
);
$actions['aksrt_email_rollback'] = sprintf(
'<a href="%s">%s</a>',
esc_url($url),
esc_html__('回滚邮箱', 'aksrt')
);
return $actions;
}
public function handle_admin_rollback(): void {
$user_id = isset($_GET['user_id']) ? absint($_GET['user_id']) : 0;
if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
check_admin_referer('aksrt_email_rollback_' . $user_id);
$r = self::rollback($user_id, get_current_user_id());
$type = !empty($r['success']) ? 'success' : 'error';
$msg = $r['message'] ?? $r['error'] ?? '';
set_transient('aksrt_admin_notice_' . get_current_user_id(), [['type' => $type, 'msg' => $msg]], 30);
wp_safe_redirect(wp_get_referer() ?: admin_url('users.php'));
exit;
}
}