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.
This commit is contained in:
kate522 committed 2026-07-17 11:11:57 +08:00
1 parent 2a8fe68048
commit ce1c13b947
115 files changed
+171 -224

No files matched your search

@@ -0,0 +1,421 @@
<?php
/**
* AKSRT Theme Auth Service
*
* 统一处理主题内认证业务:
* - 注册时直接设置密码
* - 主题自己的忘记密码 token 机制
* - 重置密码
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
final class AKSRT_Auth_Service {
use AKSRT_Auth_Service_Part_1a;
use AKSRT_Auth_Service_Part_1b;
private const RESET_TOKEN_HASH_META = 'aksrt_password_reset_token_hash';
private const RESET_TOKEN_EXPIRES_META = 'aksrt_password_reset_expires';
private const RESET_TOKEN_REQUESTED_AT = 'aksrt_password_reset_requested_at';
private const RESET_TOKEN_TTL = 3600;
public const EMAIL_VERIFIED_META = 'aksrt_email_verified';
/**
* 用户当时被发送的验证方式('link' | 'code')。
*
* 注意:这是 *审计字段*,仅用于后台展示「上一次给该用户发的是哪种验证邮件」。
* 路由分支(验证页应该显示链接提示还是验证码输入框、自助/管理员重发使用哪种方式)
* 一律读取全局配置 self::current_registration_verification_method(),
* 这样管理员把站点从 code 切换到 link 后,所有在途用户都立即跟随新方式。
* 不要根据这个 meta 决定页面模式或重发方式。
*/
public const EMAIL_METHOD_META = 'aksrt_email_verification_method';
public const EMAIL_TOKEN_HASH_META = 'aksrt_email_verification_token_hash';
public const EMAIL_CODE_HASH_META = 'aksrt_email_verification_code_hash';
public const EMAIL_EXPIRES_META = 'aksrt_email_verification_expires';
public const EMAIL_REQUESTED_AT_META = 'aksrt_email_verification_requested_at';
public const EMAIL_TARGET_META = 'aksrt_email_verification_email';
public const EMAIL_OBK_META = 'aksrt_email_verification_obk';
private const EMAIL_ACCESS_KEY_META = 'aksrt_email_verification_access_key';
public const EMAIL_CODE_ATTEMPTS_META = 'aksrt_email_verification_code_attempts';
public const EMAIL_CODE_MAX_ATTEMPTS = 5;
public const EMAIL_CODE_IP_TTL = 900;
// 自助重发的 IP 节流:每 IP 每 RESEND_IP_WINDOW 秒最多 RESEND_IP_MAX 次。
private const RESEND_IP_MAX = 6;
private const RESEND_IP_WINDOW = 600;
/**
* 注册流程内的"待消费"邀请码上下文。
*
* 由 register_user() 在 wp_create_user 之前写入,由 AKSRT_Invite::on_user_register
* 通过 take_pending_invite_context() 一次性消费。
*
* 设计为请求级静态:避免污染 transient 表,也避免依赖 cookie 在跨标签场景下的不确定性。
*
* @var int
*/
private static $pending_invite_inviter_id = 0;
/** @var string */
private static $pending_invite_code = '';
/**
* 取出并清空当前注册请求的"表单 invite_code 解析结果"。
*
* @return array{inviter_id:int, code:string}
*/
public static function take_pending_invite_context(): array {
$ctx = [
'inviter_id' => (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, '<strong>error</strong>: 该用户不存在')
) {
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,
];
}
}