(int) self::$pending_invite_inviter_id, 'code' => (string) self::$pending_invite_code, ]; self::$pending_invite_inviter_id = 0; self::$pending_invite_code = ''; return $ctx; } /** * 启动钩子:注册全局 authenticate 拦截器(阶段 4) * 调用方:bootstrap.php 在所有类加载完成后调用一次。 */ public static function init(): void { add_filter('authenticate', [self::class, 'enforce_email_verification_on_authenticate'], 30, 3); } /** * 全局登录拦截器:堵住 wp-login.php / REST / XML-RPC / Application Password 等所有 * 不经过 AKSRT_Auth_Service::login_user() 的认证路径。 * * 多重短路保护,按"成本最低"顺序排列: * 1. 紧急降级常量 AKSRT_NO_EMAIL_GATE * 2. $user 不是 WP_User(认证失败 / 未开始) * 3. WP_Error(前置错误,原样返回) * 4. 功能未启用 * 5. 管理员豁免(manage_options) * 6. cron / 安装 / 升级豁免 * 7. REST/XML-RPC 阶段化(option aksrt_email_verification_rest_block_at 控制) * 8. 自定义豁免过滤器 * * @param mixed $user * @param string $username * @param string $password * @return mixed */ public static function enforce_email_verification_on_authenticate($user, $username = '', $password = '') { if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) { return $user; } if (!($user instanceof WP_User)) { return $user; } if (is_wp_error($user)) { return $user; } if (!class_exists('AKSRT_Settings') || !AKSRT_Settings::registration_requires_email_verification()) { return $user; } if (user_can($user, 'manage_options')) { return $user; } if (defined('DOING_CRON') && DOING_CRON) { return $user; } if (defined('WP_INSTALLING') && WP_INSTALLING) { return $user; } $is_rest = (defined('REST_REQUEST') && REST_REQUEST); $is_xmlrpc = (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST); $rest_block_at = (int) get_option('aksrt_email_verification_rest_block_at', 0); // 阶段化:option=0 时 REST/XML-RPC 仅记录不拦截(Day 0~3 灰度期) if (($is_rest || $is_xmlrpc) && $rest_block_at <= 0) { if (!AKSRT_Auth_Email_Verification::is_email_verified((int) $user->ID)) { self::log_event('auth.rest_unverified', [ 'uid' => (int) $user->ID, 'rest' => $is_rest, 'xmlrpc' => $is_xmlrpc, ]); } return $user; } // 已验证 → 直接放行 if (AKSRT_Auth_Email_Verification::is_email_verified((int) $user->ID)) { return $user; } // 自定义豁免:filter 返回 false 即放行 $should_block = apply_filters('aksrt_email_verification_should_block', true, $user, [ 'rest' => $is_rest, 'xmlrpc' => $is_xmlrpc, ]); if (!$should_block) { return $user; } self::log_event('auth.blocked', [ 'uid' => (int) $user->ID, 'rest' => $is_rest, 'xmlrpc' => $is_xmlrpc, ]); return new WP_Error( 'aksrt_email_unverified', __('账号尚未完成邮箱验证,请先完成邮箱验证后再登录。', 'aksrt'), [ 'verify_url' => self::get_email_verification_url((int) $user->ID), ] ); } /** * 轻量事件日志(ring buffer,最近 200 条),用于健康检查路由统计。 * 不写入文件,仅写 transient(24h TTL),不影响性能。 */ public static function log_event(string $code, array $context = []): void { $key = 'aksrt_email_verify_log'; $logs = get_transient($key); if (!is_array($logs)) { $logs = []; } $logs[] = [ 't' => time(), 'c' => $code, 'd' => $context, ]; // 保留最近 200 条 if (count($logs) > 200) { $logs = array_slice($logs, -200); } set_transient($key, $logs, DAY_IN_SECONDS); } /** * 读取最近 24h 的事件日志聚合。 */ public static function get_email_verification_url(int $user_id, array $args = []): string { if ($user_id <= 0 || !function_exists('aksrt_get_verify_email_url')) { return ''; } $access_key = self::get_or_issue_email_access_key($user_id); if ($access_key !== '') { $args['vk'] = $access_key; } return aksrt_get_verify_email_url($user_id, $args); } public static function current_registration_verification_method(): string { if (!class_exists('AKSRT_Settings')) { return 'link'; } $method = AKSRT_Settings::registration_verification_method(); return in_array($method, ['link', 'code'], true) ? $method : 'link'; } private static function get_or_issue_email_access_key(int $user_id, bool $rotate = false): string { if ($user_id <= 0) { return ''; } $current = (string) get_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, true); if (!$rotate && $current !== '') { return $current; } $key = wp_generate_password(48, false, false); update_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, $key); return $key; } private static function has_valid_email_access_key(int $user_id, string $access_key): bool { if ($user_id <= 0 || $access_key === '') { return false; } $stored = (string) get_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, true); return $stored !== '' && hash_equals($stored, $access_key); } private static function masked_email(string $email): string { if ($email === '' || !is_email($email) || strpos($email, '@') === false) { return ''; } [$local, $domain] = explode('@', $email, 2); $local_visible = function_exists('mb_substr') ? mb_substr($local, 0, 2, 'UTF-8') : substr($local, 0, 2); $local_masked = $local_visible . str_repeat('*', max(1, strlen($local) - strlen($local_visible))); $domain_parts = explode('.', $domain); $domain_head = $domain_parts[0] ?? ''; $domain_visible = substr($domain_head, 0, 1); $domain_parts[0] = $domain_visible . str_repeat('*', max(1, strlen($domain_head) - strlen($domain_visible))); return $local_masked . '@' . implode('.', $domain_parts); } public static function get_recent_log_summary(): array { $logs = get_transient('aksrt_email_verify_log'); if (!is_array($logs)) { return []; } $cutoff = time() - DAY_IN_SECONDS; $summary = []; foreach ($logs as $entry) { if (!is_array($entry) || !isset($entry['t'], $entry['c'])) continue; if ((int) $entry['t'] < $cutoff) continue; $code = (string) $entry['c']; $summary[$code] = ($summary[$code] ?? 0) + 1; } return $summary; } public static function get_oauth_bind_context(string $obk): ?array { $obk = sanitize_text_field($obk); if ($obk === '') { return null; } $data = get_transient($obk); return is_array($data) ? $data : null; } public static function get_oauth_provider_name(string $provider): string { $provider_names = [ 'github' => 'GitHub', 'gitee' => 'Gitee', 'qq' => 'QQ', 'google' => 'Google', ]; $provider = sanitize_key($provider); return $provider_names[$provider] ?? ucfirst($provider); } public static function resolve_email_to_username(string $login): string { $login = trim($login); if (is_email($login)) { $user = get_user_by('email', $login); if ($user && !is_wp_error($user)) { return $user->user_login; } } return sanitize_user($login, true); } public static function friendly_login_error(string $raw_message): string { $msg_lower = wp_strip_all_tags(strtolower($raw_message)); if (false !== strpos($msg_lower, 'incorrect password') || false !== strpos($msg_lower, '密码不正确')) { return __('用户名或密码错误,请重新输入', 'aksrt'); } if ( false !== strpos($msg_lower, 'invalid username') || false !== strpos($msg_lower, 'invalid email') || false !== strpos($msg_lower, 'error: 该用户不存在') ) { return __('账号不存在,请检查输入或先注册', 'aksrt'); } if ( false !== strpos($msg_lower, 'empty_password') || false !== strpos($msg_lower, 'empty username') || false !== strpos($msg_lower, 'empty') ) { return __('请填写用户名和密码', 'aksrt'); } return sprintf(__('登录失败:%s', 'aksrt'), wp_strip_all_tags($raw_message)); } public static function validate_password(string $password, string $confirm = '') { if ($password === '') { return __('请输入密码', 'aksrt'); } if (mb_strlen($password) < 6) { return __('密码长度至少6位', 'aksrt'); } if (mb_strlen($password) > 128) { return __('密码过长,请控制在128位以内', 'aksrt'); } if ($confirm !== '' && $password !== $confirm) { return __('两次输入的密码不一致', 'aksrt'); } return true; } public static function login_user(array $post, bool $is_ajax = false, bool $skip_nonce = false): array { $resolved_login = self::resolve_email_to_username((string) ($post['log'] ?? '')); if (!$skip_nonce) { if ($is_ajax) { if (!wp_verify_nonce($post['_ajax_nonce'] ?? '', 'aksrt_ajax_login')) { return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')]; } } else { if (!wp_verify_nonce($post['_login_nonce'] ?? '', 'aksrt_login')) { return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')]; } } } if (function_exists('aksrt_require_captcha')) { $captcha_result = aksrt_require_captcha('login', $post); if ($captcha_result !== true) { return ['success' => false, 'error' => $captcha_result]; } } $creds = [ 'user_login' => $resolved_login, 'user_password' => (string) ($post['pwd'] ?? ''), 'remember' => !empty($post['rememberme']), ]; $original_post = $_POST; $_POST = array_merge($_POST, $post); try { $user = wp_signon($creds); } finally { $_POST = $original_post; } if (is_wp_error($user)) { return ['success' => false, 'error' => self::friendly_login_error($user->get_error_message())]; } // REST API 上下文中 wp_signon 可能无法正确设置 Cookie // 手动调用 wp_set_auth_cookie 确保 Cookie 被设置 if (defined('REST_REQUEST') && REST_REQUEST) { wp_set_auth_cookie($user->ID, true); wp_set_current_user($user->ID); } if (self::user_requires_email_verification($user)) { wp_logout(); return [ 'success' => false, 'error' => __('账号尚未完成邮箱验证,请先验证邮箱后再登录', 'aksrt'), 'verify_url' => self::get_email_verification_url((int) $user->ID), ]; } $redirect = !empty($post['redirect_to']) ? wp_unslash((string) $post['redirect_to']) : home_url('/'); return [ 'success' => true, 'user' => $user, 'redirect' => $redirect, ]; } }