用户功能设置收口与废弃后台链路清理
This commit is contained in:
@@ -1,210 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Auth;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class EmailRollback {
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user