refactor: flatten src/AKSRT → src (drop theme-identifier dir)

Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
This commit is contained in:
2026-08-02 22:06:06 +08:00
parent 56f1a8175f
commit 8c5b1555c9
285 changed files with 26 additions and 3600 deletions
+523
View File
@@ -0,0 +1,523 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Auth;
defined('ABSPATH') || exit;
class EmailVerification {
public static function user_requires_email_verification($user): bool {
if (!$user instanceof \WP_User) {
return false;
}
if (!\AKSRT\Settings\Settings::registration_requires_email_verification()) {
return false;
}
return !self::is_email_verified((int) $user->ID);
}
public static function is_email_verified(int $user_id): bool {
return (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1;
}
public static function mark_email_verified(int $user_id): void {
$was_verified = (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1;
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1);
self::clear_email_verification($user_id);
// 仅在状态由"未验证 → 已验证"跳变时触发,避免历史 backfill / 重复调用造成奖励重发。
// 监听方负责自己幂等(aksrt_invite 表的 status 检查已经做到这一点)。
if (!$was_verified) {
do_action('aksrt_email_verified', $user_id);
}
}
public static function clear_email_verification(int $user_id): void {
delete_user_meta($user_id, Service::EMAIL_METHOD_META);
delete_user_meta($user_id, Service::EMAIL_TOKEN_HASH_META);
delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META);
delete_user_meta($user_id, Service::EMAIL_EXPIRES_META);
delete_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META);
delete_user_meta($user_id, Service::EMAIL_TARGET_META);
delete_user_meta($user_id, Service::EMAIL_OBK_META);
self::reset_email_code_attempt_trackers($user_id);
}
// ============================================================
// 公开 API:管理员后台 / 内部业务调用
// ============================================================
/**
* 是否已验证邮箱(公开版本,给后台 UI / 用户列表渲染用)。
*/
public static function user_is_email_verified(int $user_id): bool {
return self::is_email_verified($user_id);
}
/**
* 取一个用户当前的验证状态摘要,用于后台资料页展示。
*
* @return array{verified:bool, method:string, target_email:string, expires_at:int, requested_at:int, last_admin_log:array}
*/
public static function get_user_verification_state(int $user_id): array {
return [
'verified' => self::is_email_verified($user_id),
'method' => (string) get_user_meta($user_id, Service::EMAIL_METHOD_META, true),
'target_email' => (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true),
'expires_at' => (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true),
'requested_at' => (int) get_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, true),
'last_admin_log' => (array) (get_user_meta($user_id, 'aksrt_email_verification_admin_log', true) ?: []),
];
}
/**
* 管理员:把指定用户标记为已验证(清除所有进行中的验证 meta)。
*/
public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
if ($user_id <= 0) {
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
}
if (!current_user_can('edit_user', $user_id)) {
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
}
self::mark_email_verified($user_id);
self::record_admin_action($user_id, 'mark_verified', $by_admin_id);
Service::log_event('admin.action', ['action' => 'mark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
return ['success' => true, 'message' => __('已标记为已验证', 'aksrt')];
}
/**
* 管理员:撤销已验证状态(用于测试 / 回滚 / 应对邮箱泄漏)。
*/
public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
if ($user_id <= 0) {
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
}
if (!current_user_can('edit_user', $user_id)) {
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
}
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 0);
self::clear_email_verification($user_id);
self::record_admin_action($user_id, 'unmark_verified', $by_admin_id);
Service::log_event('admin.action', ['action' => 'unmark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
return ['success' => true, 'message' => __('已撤销验证状态', 'aksrt')];
}
/**
* 管理员:立即发送 / 重发验证邮件(绕过 60s 节流)。
*/
public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array {
if ($user_id <= 0) {
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
}
if (!current_user_can('edit_user', $user_id)) {
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
}
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return ['success' => false, 'error' => __('用户不存在', 'aksrt')];
}
$method = Service::current_registration_verification_method();
$email = (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true);
if (!in_array($method, ['link', 'code'], true)) {
$method = \AKSRT\Settings\Settings::registration_verification_method();
}
if ($email === '' || !is_email($email)) {
$email = (string) $user->user_email;
}
$result = self::begin_email_verification($user, $method, [
'email' => $email,
'obk' => (string) get_user_meta($user_id, Service::EMAIL_OBK_META, true),
'bypass_throttle' => true,
'force' => true,
]);
// 标记为待验证(确保 admin 主动发邮件后用户被视为未验证)
if (!empty($result['success'])) {
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 0);
self::record_admin_action($user_id, 'send_verification', $by_admin_id);
Service::log_event('admin.action', ['action' => 'send_verification', 'uid' => $user_id, 'by' => $by_admin_id]);
}
return $result;
}
/**
* 把已存在的用户视为受信任(grandfather):写入 aksrt_email_verified=1。
*
* 仅写入 meta 不存在的用户;已经显式为 0 的(正在走验证流程的新注册)保持不动。
*
* 用户量 > 50000 时改为异步分批;否则内联完成。返回处理总数。
*
* @param bool $force 强制再次执行(即便 backfill_done 标记已存在),调试用
*/
public static function backfill_legacy_users_as_verified(bool $force = false): int {
if (!$force && get_option('aksrt_email_verification_backfill_done')) {
return 0;
}
$total = 0;
if (function_exists('count_users')) {
$counts = count_users();
$total = (int) ($counts['total_users'] ?? 0);
}
// 大表保护:超过 5w 用户改为后台 cron 分批
if ($total > 50000) {
if (!wp_next_scheduled('aksrt_email_verification_backfill_batch')) {
wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [0]);
}
update_option('aksrt_email_verification_backfill_log', [
'mode' => 'async',
'total' => $total,
'scheduled' => time(),
'by' => get_current_user_id(),
]);
return 0;
}
// 内联分批,每批 500
$updated = 0;
$paged = 1;
do {
$users = get_users([
'number' => 500,
'paged' => $paged,
'fields' => ['ID'],
'meta_query' => [
[
'key' => Service::EMAIL_VERIFIED_META,
'compare' => 'NOT EXISTS',
],
],
]);
if (empty($users)) {
break;
}
foreach ($users as $u) {
update_user_meta((int) $u->ID, Service::EMAIL_VERIFIED_META, 1);
$updated++;
}
$paged++;
// 让 MySQL 喘口气
if (function_exists('usleep')) {
usleep(50000);
}
} while (count($users) === 500 && $paged < 200); // 上限 10w 兜底防呆
update_option('aksrt_email_verification_backfill_done', time());
update_option('aksrt_email_verification_backfill_log', [
'mode' => 'inline',
'count' => $updated,
'ran_at' => time(),
'by' => get_current_user_id(),
]);
Service::log_event('admin.backfill', ['count' => $updated, 'by' => get_current_user_id()]);
return $updated;
}
/**
* 异步分批回填:cron 钩子。每批 500,自动调度下一批直到完成。
*/
public static function run_async_backfill_batch(int $offset = 0): void {
$users = get_users([
'number' => 500,
'offset' => max(0, $offset),
'fields' => ['ID'],
'orderby' => 'ID',
'order' => 'ASC',
'meta_query' => [
[
'key' => Service::EMAIL_VERIFIED_META,
'compare' => 'NOT EXISTS',
],
],
]);
if (empty($users)) {
update_option('aksrt_email_verification_backfill_done', time());
$log = (array) get_option('aksrt_email_verification_backfill_log', []);
$log['finished_at'] = time();
update_option('aksrt_email_verification_backfill_log', $log);
Service::log_event('admin.backfill.finished', []);
return;
}
foreach ($users as $u) {
update_user_meta((int) $u->ID, Service::EMAIL_VERIFIED_META, 1);
}
// 调度下一批
wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [$offset + 500]);
}
/**
* 记录管理员操作日志(每个用户最多 10 条 ring buffer)。
*/
public static function record_admin_action(int $user_id, string $action, int $by_admin_id): void {
$log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true);
if (!is_array($log)) {
$log = [];
}
$log[] = [
't' => time(),
'action' => $action,
'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);
}
// ============================================================
// 验证码失败计数 / IP 节流
// ============================================================
public static function email_code_ip_throttle_key(int $user_id): string {
$ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
if ($user_id <= 0 || $ip === '') {
return '';
}
return 'aksrt_email_code_try_' . $user_id . '_' . md5($ip);
}
public static function reset_email_code_attempt_trackers(int $user_id): void {
delete_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META);
$ip_key = self::email_code_ip_throttle_key($user_id);
if ($ip_key !== '') {
delete_transient($ip_key);
}
}
public static function fail_email_code_attempt(int $user_id): array {
$attempts = (int) get_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, true);
$attempts++;
update_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, $attempts);
$ip_attempts = 0;
$ip_key = self::email_code_ip_throttle_key($user_id);
if ($ip_key !== '') {
$ip_attempts = (int) get_transient($ip_key);
$ip_attempts++;
set_transient($ip_key, $ip_attempts, Service::EMAIL_CODE_IP_TTL);
}
if ($attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS || $ip_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) {
delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META);
update_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, 0);
Service::log_event('verify.code.locked', ['uid' => $user_id, 'attempts' => $attempts]);
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
}
Service::log_event('verify.code.fail', ['uid' => $user_id, 'attempts' => $attempts]);
return ['success' => false, 'error' => __('邮箱验证码错误或已过期,请重新输入。', 'aksrt')];
}
// ============================================================
// 验证流程:发起 / 校验 / 完成
// ============================================================
public static function begin_email_verification(\WP_User $user, string $method, array $args = []): array {
$method = in_array($method, ['link', 'code'], true) ? $method : 'link';
$email = sanitize_email((string) ($args['email'] ?? $user->user_email));
$obk = sanitize_text_field((string) ($args['obk'] ?? ''));
$bypass_throttle = !empty($args['bypass_throttle']);
if ($email === '' || !is_email($email)) {
return ['success' => false, 'error' => __('当前账号缺少有效邮箱,无法发送验证邮件', 'aksrt')];
}
// 节流:仅在非管理员路径生效(admin_send_verification 会传 bypass_throttle=true)
if (!$bypass_throttle) {
$last_requested = (int) get_user_meta($user->ID, Service::EMAIL_REQUESTED_AT_META, true);
if ($last_requested > 0 && (time() - $last_requested) < 60 && empty($args['force'])) {
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
}
}
$expires_at = time() + (\AKSRT\Settings\Settings::registration_verification_ttl_minutes() * MINUTE_IN_SECONDS);
update_user_meta($user->ID, Service::EMAIL_VERIFIED_META, 0);
update_user_meta($user->ID, Service::EMAIL_METHOD_META, $method);
update_user_meta($user->ID, Service::EMAIL_TARGET_META, $email);
update_user_meta($user->ID, Service::EMAIL_EXPIRES_META, $expires_at);
update_user_meta($user->ID, Service::EMAIL_REQUESTED_AT_META, time());
if ($obk !== '') {
update_user_meta($user->ID, Service::EMAIL_OBK_META, $obk);
}
delete_user_meta($user->ID, Service::EMAIL_TOKEN_HASH_META);
delete_user_meta($user->ID, Service::EMAIL_CODE_HASH_META);
if ($method === 'code') {
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
update_user_meta($user->ID, Service::EMAIL_CODE_HASH_META, wp_hash_password($code));
// 新验证码 = 重置失败计数 / IP 节流,避免被旧计数立即锁死
self::reset_email_code_attempt_trackers((int) $user->ID);
$sent = self::send_registration_verification_code_email($user, $email, $code);
} else {
$token = wp_generate_password(48, false, false);
update_user_meta($user->ID, Service::EMAIL_TOKEN_HASH_META, hash('sha256', $token));
$sent = self::send_registration_verification_link_email($user, $email, $token);
}
if (!$sent) {
self::clear_email_verification((int) $user->ID);
Service::log_event('email.send.fail', ['uid' => (int) $user->ID, 'method' => $method]);
return ['success' => false, 'error' => __('验证邮件发送失败,请稍后重试', 'aksrt')];
}
Service::log_event('email.send.ok', ['uid' => (int) $user->ID, 'method' => $method]);
$message = $method === 'code'
? __('注册成功,验证码已发送到你的邮箱,请完成验证后继续。', 'aksrt')
: __('注册成功,验证邮件已发送到你的邮箱,请点击邮件中的链接完成验证。', 'aksrt');
return [
'success' => true,
'message' => $message,
];
}
public static function verify_email_token(int $user_id, string $token): array {
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
}
$stored_hash = (string) get_user_meta($user_id, Service::EMAIL_TOKEN_HASH_META, true);
$expires_at = (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true);
if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, hash('sha256', $token))) {
return ['success' => false, 'error' => __('验证链接无效或已过期,请重新发送验证邮件。', 'aksrt')];
}
return self::complete_email_verification($user);
}
public static function verify_email_code(int $user_id, string $code): array {
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
}
// 先做 IP 节流:相同 IP 在 EMAIL_CODE_IP_TTL 内累计失败到上限即直接拒绝
$ip_key = self::email_code_ip_throttle_key($user_id);
if ($ip_key !== '') {
$ip_attempts = (int) get_transient($ip_key);
if ($ip_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) {
Service::log_event('verify.code.ip_throttled', ['uid' => $user_id, 'attempts' => $ip_attempts]);
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
}
}
// 用户级失败计数:达到上限后该验证码立即作废
$user_attempts = (int) get_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, true);
if ($user_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) {
delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META);
update_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, 0);
Service::log_event('verify.code.locked.read', ['uid' => $user_id]);
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
}
$stored_hash = (string) get_user_meta($user_id, Service::EMAIL_CODE_HASH_META, true);
$expires_at = (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true);
if ($stored_hash === '' || $expires_at <= time() || !wp_check_password($code, $stored_hash)) {
return self::fail_email_code_attempt($user_id);
}
self::reset_email_code_attempt_trackers($user_id);
return self::complete_email_verification($user);
}
public static function complete_email_verification(\WP_User $user): array {
$user_id = (int) $user->ID;
$obk = (string) get_user_meta($user_id, Service::EMAIL_OBK_META, true);
$oauth = $obk !== '' ? Service::get_oauth_bind_context($obk) : null;
$email = (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true);
$was_verified = (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1;
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1);
self::clear_email_verification($user_id);
Service::log_event('verify.success', ['uid' => $user_id]);
// 状态跳变时触发 hook,邀请系统等模块据此发奖
if (!$was_verified) {
do_action('aksrt_email_verified', $user_id);
}
if ($email !== '' && is_email($email)) {
self::send_registration_verified_notice_email($user, $email);
}
if ($oauth && is_array($oauth)) {
Service::finalize_oauth_registration($user_id, $oauth, $obk);
} elseif (\AKSRT\Settings\Settings::registration_auto_login_after_verify()) {
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id, true);
}
$redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/');
if (!\AKSRT\Settings\Settings::registration_auto_login_after_verify()) {
$redirect = aksrt_get_login_url();
}
return [
'success' => true,
'message' => __('邮箱验证成功,即将进入你的账号。', 'aksrt'),
'redirect' => $redirect,
];
}
// ============================================================
// 通知邮件
// ============================================================
public static function send_registration_verification_link_email(\WP_User $user, string $email, string $token): bool {
$url = Service::get_email_verification_url((int) $user->ID, ['token' => $token]);
$site_name = get_bloginfo('name');
$default_subject = '[' . $site_name . '] ' . __('验证你的邮箱地址', 'aksrt');
$default_content = sprintf(
__("您好 %s,\n\n欢迎注册 %s。\n\n请点击以下链接完成邮箱验证:\n\n%s\n\n验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
$user->user_login, $site_name, $url
);
return \AKSRT\Settings\Settings::send_event_email(
'reg_verify_link', $email, $default_subject, $default_content,
['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $url]
);
}
public static function send_registration_verification_code_email(\WP_User $user, string $email, string $code): bool {
$site_name = get_bloginfo('name');
$default_subject = '[' . $site_name . '] ' . __('邮箱验证码', 'aksrt');
$default_content = sprintf(
__("您好 %s,\n\n欢迎注册 %s。\n\n你的邮箱验证码为:\n\n【%s】\n\n验证码验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
$user->user_login, $site_name, $code
);
return \AKSRT\Settings\Settings::send_event_email(
'reg_verify_code', $email, $default_subject, $default_content,
['{username}' => $user->user_login, '{site_name}' => $site_name, '{code}' => $code]
);
}
public static function send_registration_verified_notice_email(\WP_User $user, string $email): bool {
$site_name = get_bloginfo('name');
$default_subject = '[' . $site_name . '] ' . __('邮箱验证成功', 'aksrt');
$default_content = sprintf(
__("您好 %s,\n\n你在 %s 的账号邮箱已经验证成功。\n\n如果这不是你的操作,请立即联系站点管理员。", 'aksrt'),
$user->user_login, $site_name
);
return \AKSRT\Settings\Settings::send_event_email(
'reg_success', $email, $default_subject, $default_content,
['{username}' => $user->user_login, '{site_name}' => $site_name]
);
}
}