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,475 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Auth_Service_Part_1a {
public static function register_user(array $post, array $context = []): array {
// 先 reset pending invite context,避免同一进程内残留污染
self::$pending_invite_inviter_id = 0;
self::$pending_invite_code = '';
$user_login = sanitize_user(wp_unslash($post['user_login'] ?? ''), true);
$user_email = sanitize_email(wp_unslash($post['user_email'] ?? ''));
$password = (string) ($post['user_pass'] ?? '');
$confirm = (string) ($post['user_pass_confirm'] ?? '');
if (!wp_verify_nonce($post['_register_nonce'] ?? '', 'aksrt_register')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
if (empty($post['agree_terms'])) {
return ['success' => false, 'error' => __('请先同意服务条款', 'aksrt')];
}
if (empty($user_login) || !validate_username($user_login)) {
return ['success' => false, 'error' => __('请输入有效的用户名', 'aksrt')];
}
if (mb_strlen($user_login) < 3 || mb_strlen($user_login) > 60) {
return ['success' => false, 'error' => __('用户名长度需在 3-60 个字符之间', 'aksrt')];
}
if (username_exists($user_login)) {
return ['success' => false, 'error' => __('该用户名已被使用', 'aksrt')];
}
if (empty($user_email) || !is_email($user_email)) {
return ['success' => false, 'error' => __('请输入有效的邮箱地址', 'aksrt')];
}
if (email_exists($user_email)) {
return ['success' => false, 'error' => __('该邮箱已被其他账号使用', 'aksrt')];
}
$password_check = self::validate_password($password, $confirm);
if ($password_check !== true) {
return ['success' => false, 'error' => $password_check];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('register', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
// ====== 解析邀请码 ======
// 优先级:表单 invite_code(用户显式填写) > cookie aksrt_invite
$invite_registration_enabled = class_exists('AKSRT_Settings') && AKSRT_Settings::invite_registration_enabled();
$invite_mode = $invite_registration_enabled && class_exists('AKSRT_Settings')
? AKSRT_Settings::invite_register_field_mode()
: 'hidden';
$invite_code = isset($post['invite_code']) ? sanitize_key((string) wp_unslash($post['invite_code'])) : '';
$resolved_inviter_id = 0;
$invite_is_valid = false;
if (!$invite_registration_enabled) {
// 邀请功能关闭,忽略所有邀请码
$invite_code = '';
} elseif ($invite_code !== '') {
if (class_exists('AKSRT_Invite')) {
$invite_service = AKSRT_Invite::get_instance();
$resolved_inviter_id = $invite_service->resolve_code($invite_code);
$invite_is_valid = $invite_service->validate_code($invite_code);
}
// 必填模式下,邀请码无效时根据后台开关决定是否拒绝
$reject_invalid = class_exists('AKSRT_Settings') && AKSRT_Settings::invite_reject_invalid();
if ($invite_mode === 'required' && $reject_invalid && !$invite_is_valid) {
return ['success' => false, 'error' => __('邀请码无效,请检查后重试', 'aksrt')];
}
} elseif ($invite_mode === 'required') {
return ['success' => false, 'error' => __('请填写邀请码', 'aksrt')];
}
// 把解析到的 inviter_id 暂存到静态字段,user_register hook 内部读取(优先于 cookie)
self::$pending_invite_inviter_id = $resolved_inviter_id;
self::$pending_invite_code = $invite_code;
$user_id = wp_create_user($user_login, $password, $user_email);
if (is_wp_error($user_id)) {
self::$pending_invite_inviter_id = 0;
self::$pending_invite_code = '';
return ['success' => false, 'error' => $user_id->get_error_message()];
}
if ($invite_code !== '' && $resolved_inviter_id <= 0 && class_exists('AKSRT_Invite')) {
AKSRT_Invite::get_instance()->handle_registration_invite((int) $user_id, $invite_code);
}
// wp_create_user 内部已触发 user_register hook,AKSRT_Invite::on_user_register 已读取 pending 字段。
// 这里清空,避免污染后续请求。
self::$pending_invite_inviter_id = 0;
self::$pending_invite_code = '';
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return ['success' => false, 'error' => __('注册成功,但读取用户信息失败,请联系管理员', 'aksrt')];
}
$oauth_context = isset($context['oauth_bind_data']) && is_array($context['oauth_bind_data']) ? $context['oauth_bind_data'] : [];
$oauth_verified = self::oauth_email_is_verified($oauth_context);
$requires_verify = AKSRT_Settings::registration_requires_email_verification() && !$oauth_verified;
if (!$requires_verify) {
self::mark_email_verified($user_id);
return [
'success' => true,
'user_id' => (int) $user_id,
'user' => $user,
'user_login' => $user_login,
];
}
$method = AKSRT_Settings::registration_verification_method();
$begin = self::begin_email_verification(
$user,
$method,
[
'obk' => sanitize_text_field((string) ($context['obk'] ?? '')),
]
);
if (empty($begin['success'])) {
self::delete_user_account($user_id);
return $begin;
}
return [
'success' => true,
'user_id' => (int) $user_id,
'user' => $user,
'user_login' => $user_login,
'verification_required' => true,
'verification_method' => $method,
'verify_url' => self::get_email_verification_url((int) $user_id, ['mode' => $method]),
'message' => $begin['message'] ?? '',
];
}
public static function bind_oauth_account(array $post, array $oauth_data): array {
if (!wp_verify_nonce($post['_bind_nonce'] ?? '', 'aksrt_bind')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('bind', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
$creds = [
'user_login' => self::resolve_email_to_username((string) ($post['log'] ?? '')),
'user_password' => (string) ($post['pwd'] ?? ''),
'remember' => true,
];
$user = wp_signon($creds);
if (is_wp_error($user)) {
return ['success' => false, 'error' => self::friendly_login_error($user->get_error_message())];
}
if (self::user_requires_email_verification($user)) {
wp_logout();
return [
'success' => false,
'error' => __('该账号尚未完成邮箱验证,请先完成验证后再绑定第三方登录。', 'aksrt'),
'verify_url' => self::get_email_verification_url((int) $user->ID),
];
}
$provider = sanitize_key((string) ($oauth_data['provider'] ?? ''));
if ($provider === '') {
return ['success' => false, 'error' => __('绑定数据无效,请重新使用第三方登录', 'aksrt')];
}
$uid = (int) $user->ID;
update_user_meta($uid, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
$bindings = get_user_meta($uid, 'aksrt_oauth_bindings', true) ?: [];
if (!is_array($bindings)) {
$bindings = [];
}
if (!in_array($provider, $bindings, true)) {
$bindings[] = $provider;
}
update_user_meta($uid, 'aksrt_oauth_bindings', $bindings);
if (!empty($oauth_data['user_data']['avatar_url'])) {
update_user_meta($uid, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
}
if (!empty($post['obk'])) {
delete_transient(sanitize_text_field((string) $post['obk']));
}
wp_set_auth_cookie($uid, true);
return [
'success' => true,
'user' => $user,
'provider' => $provider,
];
}
public static function finalize_oauth_registration(int $user_id, array $oauth_data, string $obk = ''): void {
if ($user_id <= 0) {
return;
}
$provider = sanitize_key((string) ($oauth_data['provider'] ?? ''));
if ($provider === '') {
return;
}
update_user_meta($user_id, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
update_user_meta($user_id, 'aksrt_oauth_bindings', [$provider]);
if (!empty($oauth_data['user_data']['avatar_url'])) {
update_user_meta($user_id, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
}
if (self::oauth_email_is_verified($oauth_data)) {
AKSRT_Auth_Email_Verification::mark_email_verified($user_id);
}
if ($obk !== '') {
delete_transient(sanitize_text_field($obk));
}
wp_set_auth_cookie($user_id, true);
}
public static function get_email_verification_context(array $query): array {
$invalid_payload = [
'mode' => 'invalid',
'user' => null,
'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt'),
'method' => '',
'email' => '',
'uid' => 0,
'verified' => false,
'access_key' => '',
];
$user_id = isset($query['uid']) ? absint($query['uid']) : 0;
if ($user_id <= 0) {
return $invalid_payload;
}
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return $invalid_payload;
}
$method = self::current_registration_verification_method();
$email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
$token = sanitize_text_field((string) ($query['token'] ?? ''));
$access = sanitize_text_field((string) ($query['vk'] ?? ''));
// 容错:meta 缺失时(例如历史账号被强制要求验证),回退到设置默认值与 user_email
if (!in_array($method, ['link', 'code'], true)) {
$method = self::current_registration_verification_method();
}
if ($email === '' || !is_email($email)) {
$email = (string) $user->user_email;
}
$masked_email = self::masked_email($email);
$has_access = self::has_valid_email_access_key($user_id, $access);
// token 路径:链接模式直接走 token 校验;不依赖 vk
if ($token !== '') {
if (AKSRT_Auth_Email_Verification::is_email_verified($user_id)) {
return [
'mode' => 'invalid',
'user' => $user,
'error' => __('该验证链接已失效或已使用,请重新发起验证流程', 'aksrt'),
'method' => $has_access ? $method : '',
'email' => $has_access ? $masked_email : '',
'uid' => $has_access ? $user_id : 0,
'verified' => true,
'access_key' => $has_access ? $access : '',
];
}
$result = self::verify_email_token($user_id, $token);
if (!empty($result['success'])) {
return [
'mode' => 'completed',
'user' => $user,
'error' => '',
'method' => $method,
'email' => $masked_email,
'uid' => $user_id,
'verified' => true,
'redirect' => $result['redirect'] ?? home_url('/'),
'message' => $result['message'] ?? '',
'access_key' => $has_access ? $access : '',
];
}
return [
'mode' => 'invalid',
'user' => $user,
'error' => $result['error'] ?? __('验证链接无效或已过期', 'aksrt'),
'method' => $has_access ? $method : '',
'email' => $has_access ? $masked_email : '',
'uid' => $has_access ? $user_id : 0,
'verified' => false,
'access_key' => $has_access ? $access : '',
];
}
// 没有 token 也没有 vk:返回完全脱敏的通用错误,避免根据 uid 枚举账号状态
if (!$has_access) {
return $invalid_payload;
}
if (!self::user_requires_email_verification($user)) {
return [
'mode' => 'verified',
'user' => $user,
'error' => '',
'method' => '',
'email' => self::masked_email((string) $user->user_email),
'uid' => $user_id,
'verified' => true,
'access_key' => $access,
];
}
return [
'mode' => $method === 'code' ? 'code' : 'pending',
'user' => $user,
'error' => '',
'method' => $method,
'email' => $masked_email ?: self::masked_email((string) $user->user_email),
'uid' => $user_id,
'verified' => false,
'access_key' => $access,
];
}
public static function verify_email_code_submission(array $post): array {
if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
$user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
$access = sanitize_text_field((string) ($post['vk'] ?? ''));
$code = sanitize_text_field((string) ($post['email_code'] ?? ''));
if ($user_id <= 0 || $code === '') {
return ['success' => false, 'error' => __('请输入完整的邮箱验证码', 'aksrt')];
}
if (!self::has_valid_email_access_key($user_id, $access)) {
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('verify_email', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
return self::verify_email_code($user_id, $code);
}
public static function resend_email_verification(array $post): array {
if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
$user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
$access = sanitize_text_field((string) ($post['vk'] ?? ''));
// 必须先持有有效 vk 才允许重发,避免未登录用户用 uid 枚举触发邮件
if ($user_id <= 0 || !self::has_valid_email_access_key($user_id, $access)) {
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
}
// IP 节流:每 IP 每 RESEND_IP_WINDOW 秒最多 RESEND_IP_MAX 次(跨账号合并计数)
$ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
if ($ip !== '') {
$ip_key = 'aksrt_email_resend_ip_' . md5($ip);
$ip_attempts = (int) get_transient($ip_key);
if ($ip_attempts >= self::RESEND_IP_MAX) {
self::log_event('verify.resend.ip_throttled', ['uid' => $user_id, 'ip' => $ip]);
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
}
set_transient($ip_key, $ip_attempts + 1, self::RESEND_IP_WINDOW);
}
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
}
if (!self::user_requires_email_verification($user)) {
return ['success' => false, 'error' => __('该账号已完成邮箱验证,无需重复发送', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('verify_email', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
$method = self::current_registration_verification_method();
$email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
// 容错:meta 缺失时回退到设置默认值与 user_email(兼容历史账号 / meta 被外部清理的情况)
if (!in_array($method, ['link', 'code'], true)) {
$method = AKSRT_Settings::registration_verification_method();
}
if ($email === '' || !is_email($email)) {
$email = (string) $user->user_email;
}
if ($email === '' || !is_email($email)) {
return ['success' => false, 'error' => __('当前账号缺少有效邮箱,请联系管理员', 'aksrt')];
}
$requested_at = (int) get_user_meta($user_id, self::EMAIL_REQUESTED_AT_META, true);
if ($requested_at > 0 && (time() - $requested_at) < 60) {
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
}
return self::begin_email_verification($user, $method, [
'email' => $email,
'obk' => (string) get_user_meta($user_id, self::EMAIL_OBK_META, true),
'force' => true,
]);
}
private static function user_requires_email_verification($user): bool {
return AKSRT_Auth_Email_Verification::user_requires_email_verification($user);
}
private static function mark_email_verified(int $user_id): void {
AKSRT_Auth_Email_Verification::mark_email_verified($user_id);
}
// ============================================================
// 公开 API:管理员后台 / 内部业务调用(委托 AKSRT_Auth_Email_Verification)
// ============================================================
public static function user_is_email_verified(int $user_id): bool {
return AKSRT_Auth_Email_Verification::user_is_email_verified($user_id);
}
public static function get_user_verification_state(int $user_id): array {
return AKSRT_Auth_Email_Verification::get_user_verification_state($user_id);
}
public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
return AKSRT_Auth_Email_Verification::admin_mark_verified($user_id, $by_admin_id);
}
public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
return AKSRT_Auth_Email_Verification::admin_unmark_verified($user_id, $by_admin_id);
}
}
@@ -0,0 +1,473 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Auth_Service_Part_1b {
public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array {
return AKSRT_Auth_Email_Verification::admin_send_verification($user_id, $by_admin_id);
}
public static function backfill_legacy_users_as_verified(bool $force = false): int {
return AKSRT_Auth_Email_Verification::backfill_legacy_users_as_verified($force);
}
public static function run_async_backfill_batch(int $offset = 0): void {
AKSRT_Auth_Email_Verification::run_async_backfill_batch($offset);
}
private static function delete_user_account(int $user_id): void {
if ($user_id <= 0) {
return;
}
if (!function_exists('wp_delete_user')) {
require_once ABSPATH . 'wp-admin/includes/user.php';
}
if (function_exists('wp_delete_user')) {
wp_delete_user($user_id);
}
}
private static function oauth_email_is_verified(array $oauth_data): bool {
$user_data = isset($oauth_data['user_data']) && is_array($oauth_data['user_data']) ? $oauth_data['user_data'] : $oauth_data;
if (empty($user_data['email'])) {
return false;
}
if (!empty($user_data['email_verified'])) {
return true;
}
$provider = sanitize_key((string) ($user_data['provider'] ?? $oauth_data['provider'] ?? ''));
if ($provider === 'google' && !empty($user_data['email'])) {
return true;
}
if ($provider === 'github' && isset($user_data['email_verified'])) {
return !empty($user_data['email_verified']);
}
return false;
}
private static function begin_email_verification(WP_User $user, string $method, array $args = []): array {
return AKSRT_Auth_Email_Verification::begin_email_verification($user, $method, $args);
}
private static function verify_email_token(int $user_id, string $token): array {
return AKSRT_Auth_Email_Verification::verify_email_token($user_id, $token);
}
private static function verify_email_code(int $user_id, string $code): array {
return AKSRT_Auth_Email_Verification::verify_email_code($user_id, $code);
}
public static function get_reset_context(array $query): array {
if (!empty($query['key']) || !empty($query['login']) || !empty($query['legacy'])) {
return [
'mode' => 'invalid',
'user' => null,
'error' => __('旧的重置密码链接已失效,请重新申请重置密码。', 'aksrt'),
'token' => '',
'user_id'=> 0,
];
}
$token = sanitize_text_field($query['token'] ?? '');
$user_id = isset($query['uid']) ? absint($query['uid']) : 0;
if ($token === '' || $user_id <= 0) {
return [
'mode' => 'request',
'user' => null,
'error' => '',
'token' => '',
'user_id'=> 0,
];
}
$user = self::validate_reset_token($user_id, $token);
if (is_wp_error($user)) {
return [
'mode' => 'invalid',
'user' => null,
'error' => $user->get_error_message(),
'token' => $token,
'user_id'=> $user_id,
];
}
return [
'mode' => 'reset',
'user' => $user,
'error' => '',
'token' => $token,
'user_id'=> $user_id,
];
}
public static function request_password_reset(array $post): array {
if (!wp_verify_nonce($post['_reset_nonce'] ?? '', 'aksrt_reset_password')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('lostpassword', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
$raw_login = trim((string) ($post['user_login'] ?? ''));
if ($raw_login === '') {
return ['success' => false, 'error' => __('请输入邮箱地址或用户名', 'aksrt')];
}
$user = null;
if (is_email($raw_login)) {
$user = get_user_by('email', $raw_login);
} else {
$user = get_user_by('login', sanitize_user($raw_login, true));
}
if (!$user || is_wp_error($user)) {
return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
}
$token = self::create_reset_token($user->ID);
$sent = self::send_password_reset_email($user, $token);
if (!$sent) {
return ['success' => false, 'error' => __('重置邮件发送失败,请稍后再试', 'aksrt')];
}
return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
}
public static function reset_password_with_token(array $post): array {
if (!wp_verify_nonce($post['_setpw_nonce'] ?? '', 'aksrt_set_password')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
$token = sanitize_text_field($post['reset_token'] ?? '');
$user_id = isset($post['reset_uid']) ? absint($post['reset_uid']) : 0;
$pass1 = (string) ($post['pass1'] ?? '');
$pass2 = (string) ($post['pass2'] ?? '');
$user = self::validate_reset_token($user_id, $token);
if (is_wp_error($user)) {
return ['success' => false, 'error' => $user->get_error_message(), 'mode' => 'invalid'];
}
$password_check = self::validate_password($pass1, $pass2);
if ($password_check !== true) {
return ['success' => false, 'error' => $password_check, 'mode' => 'reset'];
}
wp_set_password($pass1, $user_id);
self::clear_reset_token($user_id);
return ['success' => true, 'message' => __('密码已设置成功!', 'aksrt')];
}
private static function create_reset_token(int $user_id): string {
$token = wp_generate_password(48, false, false);
update_user_meta($user_id, self::RESET_TOKEN_HASH_META, hash('sha256', $token));
update_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, time() + self::RESET_TOKEN_TTL);
update_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT, time());
return $token;
}
private static function validate_reset_token(int $user_id, string $token) {
if ($user_id <= 0 || $token === '') {
return new WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
}
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return new WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
}
$stored_hash = (string) get_user_meta($user_id, self::RESET_TOKEN_HASH_META, true);
$expires_at = (int) get_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, true);
$token_hash = hash('sha256', $token);
if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, $token_hash)) {
return new WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
}
return $user;
}
private static function clear_reset_token(int $user_id): void {
delete_user_meta($user_id, self::RESET_TOKEN_HASH_META);
delete_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META);
delete_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT);
}
private static function send_password_reset_email(WP_User $user, string $token): bool {
$reset_url = aksrt_get_reset_password_url($token, $user->ID);
$site_name = get_bloginfo('name');
$default_subject = '[' . $site_name . '] ' . __('重置密码', 'aksrt');
$default_content = sprintf(
__("您好 %s,\n\n有人请求重置您在 %s 的密码。\n\n请点击以下链接设置新密码:\n\n%s\n\n如果这不是您本人的操作,请忽略此邮件,您的密码不会被更改。", 'aksrt'),
$user->user_login, $site_name, $reset_url
);
return AKSRT_Settings::send_event_email(
'password_reset', $user->user_email, $default_subject, $default_content,
['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $reset_url]
);
}
/* =================================================================
* 手机号认证(注册 / 登录 / 绑定)
* 对齐 zibll 的 signup_captcha / signin_captcha / bind_phone_captcha
* ================================================================= */
/**
* 手机号注册
*
* @param array $post POST 数据 (phone, code, password, invite_code)
* @return array{success: bool, user_id?: int, error?: string, redirect?: string}
*/
public static function register_by_phone(array $post): array {
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
$code = sanitize_text_field((string) ($post['code'] ?? ''));
$pass = (string) ($post['password'] ?? '');
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
}
if (empty($code)) {
return ['success' => false, 'error' => __('请输入验证码', 'aksrt')];
}
if (strlen($pass) < 6) {
return ['success' => false, 'error' => __('密码至少 6 位', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('register', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
// 验证 SMS 验证码
if (class_exists('AKSRT_SMS')) {
$verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code);
if (empty($verify_result['success'])) {
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
}
} else {
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
}
// 检查手机号是否已注册
$existing_user = self::get_user_by_phone($phone);
if ($existing_user) {
return ['success' => false, 'error' => __('该手机号已注册', 'aksrt')];
}
// 生成用户名(phone_ + 后4位)
$username = 'phone_' . substr($phone, -4);
$suffix = 1;
$base = $username;
while (username_exists($username)) {
$username = $base . '_' . $suffix;
$suffix++;
}
// 创建用户
$user_id = wp_create_user($username, $pass, $phone . '@sms.aksrt.local');
if (is_wp_error($user_id)) {
return ['success' => false, 'error' => $user_id->get_error_message()];
}
// 绑定手机号
update_user_meta($user_id, 'aksrt_phone', $phone);
update_user_meta($user_id, 'aksrt_phone_verified', '1');
// 邮箱标记为已验证(手机注册等同于已验证)
update_user_meta($user_id, 'aksrt_email_verified', '1');
// 处理邀请码
$invite_code = sanitize_key((string) ($post['invite_code'] ?? ''));
if ($invite_code !== '' && class_exists('AKSRT_Invite')) {
AKSRT_Invite::get_instance()->handle_registration_invite($user_id, $invite_code);
}
// 自动登录
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('/');
return [
'success' => true,
'user_id' => $user_id,
'redirect' => $redirect,
'message' => __('注册成功', 'aksrt'),
];
}
/**
* 手机号登录(密码登录)
*
* @param array $post (phone, password)
* @return array
*/
public static function login_by_phone_password(array $post): array {
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
$pass = (string) ($post['password'] ?? '');
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
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];
}
}
$user = self::get_user_by_phone($phone);
if (!$user) {
return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
}
// 验证密码
if (!wp_check_password($pass, $user->data->user_pass, $user->ID)) {
return ['success' => false, 'error' => __('密码不正确', 'aksrt')];
}
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('/');
return [
'success' => true,
'user_id' => $user->ID,
'redirect' => $redirect,
];
}
/**
* 手机号免密登录(SMS 验证码登录)
*
* @param array $post (phone, code)
* @return array
*/
public static function login_by_phone_code(array $post): array {
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
$code = sanitize_text_field((string) ($post['code'] ?? ''));
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
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];
}
}
// 验证 SMS 验证码
if (class_exists('AKSRT_SMS')) {
$verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code);
if (empty($verify_result['success'])) {
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
}
} else {
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
}
$user = self::get_user_by_phone($phone);
if (!$user) {
return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
}
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('/');
return [
'success' => true,
'user_id' => $user->ID,
'redirect' => $redirect,
];
}
/**
* 绑定手机号到当前用户
*
* @param array $post (phone, code)
* @return array
*/
public static function bind_phone(array $post): array {
if (!is_user_logged_in()) {
return ['success' => false, 'error' => __('请先登录', 'aksrt')];
}
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
$code = sanitize_text_field((string) ($post['code'] ?? ''));
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('bind_phone', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
// 验证 SMS 验证码
if (class_exists('AKSRT_SMS')) {
$verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code);
if (empty($verify_result['success'])) {
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
}
} else {
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
}
// 检查手机号是否已被其他用户绑定
$existing = self::get_user_by_phone($phone);
if ($existing && (int) $existing->ID !== get_current_user_id()) {
return ['success' => false, 'error' => __('该手机号已被其他账号绑定', 'aksrt')];
}
$user_id = get_current_user_id();
update_user_meta($user_id, 'aksrt_phone', $phone);
update_user_meta($user_id, 'aksrt_phone_verified', '1');
return [
'success' => true,
'message' => __('手机号绑定成功', 'aksrt'),
];
}
/**
* 根据手机号查找用户
*/
private static function get_user_by_phone(string $phone): ?WP_User {
$users = get_users([
'meta_key' => 'aksrt_phone',
'meta_value' => $phone,
'number' => 1,
'fields' => 'all',
]);
return !empty($users) ? $users[0] : null;
}
}
@@ -0,0 +1,217 @@
<?php
/**
* AKSRT Theme Auth Router
*
* 统一处理主题内认证路由:
* - /login
* - /register
* - /bind-account
* - /reset-password
* - /logout
* - wp-login.php 前台分流
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
final class AKSRT_Auth_Router {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {}
public function init(): void {
add_filter('query_vars', [$this, 'register_query_vars']);
add_action('init', [$this, 'register_rewrite_rules']);
add_filter('template_include', [$this, 'handle_template_include'], 99);
add_action('init', [$this, 'maybe_flush_rewrite_rules']);
add_filter('login_url', [$this, 'filter_login_url'], 10, 3);
add_filter('lostpassword_url', [$this, 'filter_lostpassword_url'], 10, 2);
add_filter('register_url', [$this, 'filter_register_url']);
add_filter('logout_url', [$this, 'filter_logout_url'], 10, 2);
add_action('login_init', [$this, 'redirect_native_login']);
add_action('template_redirect', [$this, 'handle_logout_route']);
}
public function register_query_vars(array $vars): array {
$vars[] = 'aksrt_sign';
$vars[] = 'aksrt_auth_tab';
$vars[] = 'aksrt_logout';
return $vars;
}
private function auth_route_map(): array {
return [
'login' => 'index.php?aksrt_sign=1&aksrt_auth_tab=login',
'register' => 'index.php?aksrt_sign=1&aksrt_auth_tab=register',
'bind-account' => 'index.php?aksrt_sign=1&aksrt_auth_tab=bind',
'logout' => 'index.php?aksrt_logout=1',
'reset-password' => 'index.php?aksrt_sign=1&aksrt_auth_tab=reset-password',
'verify-email' => 'index.php?aksrt_sign=1&aksrt_auth_tab=verify-email',
];
}
public function register_rewrite_rules(): void {
foreach ($this->auth_route_map() as $path => $query) {
add_rewrite_rule('^' . preg_quote($path, '/') . '/?$', $query, 'top');
}
}
public function handle_template_include(string $template): string {
if (get_query_var('aksrt_sign')) {
// §4.1 Global sign gate: when close_sign is active, block non-logged-in access to sign page
if (class_exists('AKSRT_Settings') && AKSRT_Settings::close_sign() && !is_user_logged_in()) {
$tab = (string) get_query_var('aksrt_auth_tab');
if (!in_array($tab, ['reset-password', 'verify-email', 'bind-account'], true) && empty($_REQUEST['interim-login'])) {
if (!headers_sent()) {
wp_safe_redirect(home_url('/'), 302);
exit;
}
}
}
$auth_template = function_exists('aksrt_get_auth_template_file')
? aksrt_get_auth_template_file((string) get_query_var('aksrt_auth_tab'), true)
: 'templates/pages/page-sign.php';
$tpl = aksrt_locate_template_file($auth_template);
if ($tpl) {
return $tpl;
}
}
return $template;
}
public function maybe_flush_rewrite_rules(): void {
$rewrite_version = AKSRT_VERSION . ':sign-router-v1';
if (get_option('aksrt_auth_routes_flushed') !== $rewrite_version) {
flush_rewrite_rules(false);
update_option('aksrt_auth_routes_flushed', $rewrite_version);
}
}
public function filter_login_url(string $login_url, string $redirect, bool $force_reauth): string {
if (strpos($login_url, 'wp-login.php') === false) {
return $login_url;
}
$custom_login_url = aksrt_get_login_url();
if ($redirect !== '') {
$custom_login_url = add_query_arg('redirect_to', urlencode($redirect), $custom_login_url);
}
if ($force_reauth) {
$custom_login_url = add_query_arg('reauth', '1', $custom_login_url);
}
return $custom_login_url;
}
public function filter_lostpassword_url(string $lostpassword_url, string $redirect): string {
$custom_url = aksrt_get_reset_password_url();
if ($redirect !== '') {
$custom_url = add_query_arg('redirect_to', urlencode($redirect), $custom_url);
}
return $custom_url;
}
public function filter_register_url(string $register_url): string {
return aksrt_get_register_url();
}
public function filter_logout_url(string $logout_url, string $redirect): string {
return aksrt_get_logout_url($redirect ?: home_url('/'));
}
public function redirect_native_login(): void {
if (wp_doing_ajax()) {
return;
}
// §4.1 Emergency bypass key (admin_sign_key): allow wp-login.php direct access when key matches
if (isset($_GET['aksrt_bypass'])) {
$key = class_exists('AKSRT_Settings') ? AKSRT_Settings::admin_sign_key() : '';
if ($key !== '' && hash_equals($key, (string) $_GET['aksrt_bypass'])) {
return;
}
}
$action = isset($_REQUEST['action']) ? sanitize_key((string) $_REQUEST['action']) : 'login';
$redirect_to = isset($_REQUEST['redirect_to']) ? wp_unslash((string) $_REQUEST['redirect_to']) : '';
$interim = !empty($_REQUEST['interim-login']);
$reauth = !empty($_REQUEST['reauth']);
$is_admin_target = $redirect_to !== '' && strpos($redirect_to, admin_url()) === 0;
if (is_user_logged_in() && current_user_can('manage_options')) {
return;
}
if ($is_admin_target || $interim || $reauth || $action === 'logout') {
return;
}
switch ($action) {
case 'register':
$target = aksrt_get_register_url();
break;
case 'lostpassword':
$target = aksrt_get_reset_password_url();
break;
case 'rp':
case 'resetpass':
$target = add_query_arg('legacy', '1', aksrt_get_reset_password_url());
break;
default:
$target = aksrt_get_login_url();
break;
}
$passthrough = [];
if ($redirect_to !== '') {
$passthrough['redirect_to'] = $redirect_to;
}
if ($reauth) {
$passthrough['reauth'] = '1';
}
if (!empty($_REQUEST['obk'])) {
$passthrough['obk'] = sanitize_text_field((string) $_REQUEST['obk']);
}
if (!empty($passthrough)) {
$target = add_query_arg($passthrough, $target);
}
wp_safe_redirect($target, 302);
exit;
}
public function handle_logout_route(): void {
if (!get_query_var('aksrt_logout')) {
return;
}
if (!is_user_logged_in()) {
wp_safe_redirect(home_url('/'));
exit;
}
$nonce = isset($_GET['_wpnonce']) ? sanitize_text_field((string) $_GET['_wpnonce']) : '';
if (!wp_verify_nonce($nonce, 'aksrt-logout')) {
wp_die(__('退出登录请求无效,请重试。', 'aksrt'));
}
$redirect_to = isset($_GET['redirect_to']) ? wp_unslash((string) $_GET['redirect_to']) : home_url('/');
wp_logout();
wp_safe_redirect($redirect_to ?: home_url('/'));
exit;
}
}
@@ -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,
];
}
}
+971
View File
@@ -0,0 +1,971 @@
<?php
/**
* AKSRT 人机验证(Cloud Captcha)核心。
*
* 唯一配置源:option `aksrt_captcha_config`。
* 闭环契约:
* aksrt_get_captcha_config() -> aksrt_is_captcha_enabled_for(scene)
* -> aksrt_render_captcha(scene) -> 客户端 AKSRTCaptchaState + token
* -> 业务入口 aksrt_require_captcha(scene, $payload) -> proceed/fail。
*
* 仅支持云厂商:tencent / aliyun / cloudflare / geetest。
* 不含本地图形码 / 本地滑块。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
/**
* 支持的验证服务商。
*/
function aksrt_captcha_providers(): array {
return ['tencent', 'aliyun', 'cloudflare', 'geetest'];
}
/**
* 人机验证场景白名单。
*/
function aksrt_captcha_scenes(): array {
return [
'comment' => __('发表评论', 'aksrt'),
'login' => __('用户登录', 'aksrt'),
'register' => __('用户注册', 'aksrt'),
'bind' => __('绑定第三方', 'aksrt'),
'bind_phone' => __('绑定手机', 'aksrt'),
'lostpassword' => __('找回密码', 'aksrt'),
'change_password' => __('修改密码', 'aksrt'),
'change_email' => __('更改邮箱', 'aksrt'),
'verify_email' => __('邮箱验证', 'aksrt'),
'download' => __('资源下载', 'aksrt'),
'signin' => __('每日签到', 'aksrt'),
];
}
/**
* 默认配置结构。
*/
function aksrt_captcha_default_config(): array {
$all_scenes = array_keys(aksrt_captcha_scenes());
return [
'enabled' => false,
'active_provider' => '',
// Positive list: checked scenes require captcha. Default all scenes.
'enabled_scenarios' => $all_scenes,
// Legacy mirror kept for BC consumers / admin summaries.
'disabled_scenarios' => [],
'tencent' => [],
'aliyun' => [],
'cloudflare' => [],
'geetest' => [],
];
}
/**
* Sanitize a list of captcha scene keys against the whitelist.
*
* @param mixed $raw
* @return array<int, string>
*/
function aksrt_captcha_sanitize_scene_list($raw): array {
$list = is_array($raw) ? $raw : [];
$clean = [];
foreach ($list as $scene) {
$scene = sanitize_key((string) $scene);
if ($scene !== '' && array_key_exists($scene, aksrt_captcha_scenes()) && !in_array($scene, $clean, true)) {
$clean[] = $scene;
}
}
return array_values($clean);
}
/**
* 获取人机验证完整配置(规范化,不合并历史脏键)。
*
* @return array
*/
function aksrt_get_captcha_config(): array {
$cfg = get_option('aksrt_captcha_config', []);
if (!is_array($cfg)) {
$cfg = [];
}
$all_scenes = array_keys(aksrt_captcha_scenes());
$provider = isset($cfg['active_provider']) ? sanitize_key((string) $cfg['active_provider']) : '';
if (!in_array($provider, aksrt_captcha_providers(), true)) {
$provider = '';
}
if (array_key_exists('enabled_scenarios', $cfg)) {
// Explicit positive list (may be empty = no scenes).
$enabled = aksrt_captcha_sanitize_scene_list($cfg['enabled_scenarios']);
} else {
// Legacy: only disabled list stored → all minus disabled.
$disabled_legacy = aksrt_captcha_sanitize_scene_list($cfg['disabled_scenarios'] ?? []);
$enabled = array_values(array_diff($all_scenes, $disabled_legacy));
}
$disabled = array_values(array_diff($all_scenes, $enabled));
$result = [
'enabled' => !empty($cfg['enabled']),
'active_provider' => $provider,
'enabled_scenarios' => $enabled,
'disabled_scenarios' => $disabled,
];
foreach (aksrt_captcha_providers() as $p) {
$result[$p] = isset($cfg[$p]) && is_array($cfg[$p]) ? $cfg[$p] : [];
}
return $result;
}
/**
* 各服务商必填字段。
*/
function aksrt_captcha_required_fields(string $provider): array {
$map = [
'tencent' => ['secret_id', 'secret_key', 'app_id', 'app_secret_key'],
'aliyun' => ['region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'],
'cloudflare' => ['site_key', 'secret_key'],
'geetest' => ['captcha_id', 'captcha_key'],
];
return $map[$provider] ?? [];
}
/**
* 当前服务商必填凭据是否齐全(避免空 captcha_id 打到第三方 403)。
*/
function aksrt_captcha_provider_is_configured(string $provider, array $pcfg = []): bool {
if (!in_array($provider, aksrt_captcha_providers(), true)) {
return false;
}
$required = aksrt_captcha_required_fields($provider);
if (empty($required)) {
return false;
}
foreach ($required as $field) {
if (!isset($pcfg[$field]) || trim((string) $pcfg[$field]) === '') {
return false;
}
}
return true;
}
/**
* 指定场景是否启用人机验证。
*
* @param string $scene 见 aksrt_captcha_scenes()
* @return bool
*/
function aksrt_is_captcha_enabled_for($scene): bool {
$scene = sanitize_key((string) $scene);
if (!array_key_exists($scene, aksrt_captcha_scenes())) {
return false;
}
$cfg = aksrt_get_captcha_config();
if (empty($cfg['enabled']) || $cfg['active_provider'] === '') {
return false;
}
$provider = $cfg['active_provider'];
$pcfg = $cfg[$provider] ?? [];
if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) {
return false;
}
$enabled = isset($cfg['enabled_scenarios']) && is_array($cfg['enabled_scenarios'])
? $cfg['enabled_scenarios']
: array_values(array_diff(array_keys(aksrt_captcha_scenes()), (array) ($cfg['disabled_scenarios'] ?? [])));
return in_array($scene, $enabled, true);
}
/**
* 业务入口统一调用:未启用放行;启用则校验。
*
* @param string $scene 场景
* @param array $payload 客户端提交的验证字段
* @return true|string 成功返回 true,失败返回错误信息字符串
*/
function aksrt_require_captcha(string $scene, array $payload = []) {
if (!aksrt_is_captcha_enabled_for($scene)) {
return true;
}
return aksrt_verify_captcha($payload);
}
/**
* 后台摘要:enabled / provider / configured / 场景命中状态。
*/
function aksrt_get_captcha_status_summary(): array {
$cfg = aksrt_get_captcha_config();
$provider = $cfg['active_provider'];
$scenes = [];
foreach (aksrt_captcha_scenes() as $key => $label) {
$scenes[$key] = aksrt_is_captcha_enabled_for($key);
}
return [
'enabled' => !empty($cfg['enabled']),
'provider' => $provider,
'configured' => $provider !== '' && aksrt_captcha_provider_is_configured($provider, $cfg[$provider] ?? []),
'scenes' => $scenes,
];
}
// ============================================================
// 客户端真实 IP(穿透 CDN / 代理)
// ============================================================
function aksrt_get_client_ip(): string {
$keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];
foreach ($keys as $key) {
if (!empty($_SERVER[$key])) {
$parts = explode(',', (string) $_SERVER[$key]);
$ip = trim($parts[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
}
// ============================================================
// 腾讯云 API 3.0 TC3-HMAC-SHA256 签名
// ============================================================
function aksrt_tencent_tc3_sign($secret_id, $secret_key, $service, $timestamp, $date, $action, $version, $payload): array {
$content_type = 'application/json; charset=utf-8';
$host = 'captcha.tencentcloudapi.com';
$payload_hash = hash('sha256', $payload);
$canonical_headers = "content-type:{$content_type}\nhost:{$host}\n";
$signed_headers = 'content-type;host';
$canonical_request = "POST\n/\n\n{$canonical_headers}\n{$signed_headers}\n{$payload_hash}";
$credential_scope = "{$date}/{$service}/tc3_request";
$string_to_sign = "TC3-HMAC-SHA256\n{$timestamp}\n{$credential_scope}\n" . hash('sha256', $canonical_request);
$secret_date = hash_hmac('sha256', $date, 'TC3' . $secret_key, true);
$secret_service = hash_hmac('sha256', $service, $secret_date, true);
$secret_signing = hash_hmac('sha256', 'tc3_request', $secret_service, true);
$signature = hash_hmac('sha256', $string_to_sign, $secret_signing);
$authorization = 'TC3-HMAC-SHA256 '
. "Credential={$secret_id}/{$credential_scope}, "
. "SignedHeaders={$signed_headers}, "
. "Signature={$signature}";
return [
'Authorization' => $authorization,
'Content-Type' => $content_type,
'Host' => $host,
'X-TC-Action' => $action,
'X-TC-Timestamp'=> (string) $timestamp,
'X-TC-Version' => $version,
'X-TC-Region' => '',
];
}
/**
* 阿里云 OpenAPI POP Signature v1.0 (HMAC-SHA1)。
*/
function aksrt_aliyun_afs_sign(array $params, string $secret): string {
ksort($params);
$canonicalized = '';
foreach ($params as $k => $v) {
$canonicalized .= '&' . rawurlencode((string) $k) . '=' . rawurlencode((string) $v);
}
$string_to_sign = 'POST&%2F&' . rawurlencode(substr($canonicalized, 1));
return base64_encode(hash_hmac('sha1', $string_to_sign, $secret . '&', true));
}
// ============================================================
// 服务端二次校验
// ============================================================
/**
* 校验客户端提交的人机验证字段。
*
* @param array $post_data 客户端字段(captcha_verified + 厂商 token)
* @return true|string 成功返回 true,失败返回错误信息
*/
function aksrt_verify_captcha($post_data) {
$cfg = aksrt_get_captcha_config();
if (empty($cfg['enabled']) || $cfg['active_provider'] === '') {
return true;
}
$provider = $cfg['active_provider'];
$pcfg = $cfg[$provider] ?? [];
$client_ip = aksrt_get_client_ip();
// 客户端验证字段(前端写入 captcha_verified=1 表示用户已完成交互)
if (($post_data['captcha_verified'] ?? '') !== '1') {
return __('人机验证失败', 'aksrt');
}
switch ($provider) {
case 'tencent':
$result = aksrt_verify_tencent($pcfg, $post_data, $client_ip);
break;
case 'cloudflare':
$result = aksrt_verify_cloudflare($pcfg, $post_data, $client_ip);
break;
case 'geetest':
$result = aksrt_verify_geetest($pcfg, $post_data);
break;
case 'aliyun':
$result = aksrt_verify_aliyun($pcfg, $post_data);
break;
default:
return true;
}
return $result === true ? true : ($result ?: __('人机验证失败', 'aksrt'));
}
/**
* 腾讯云 Captcha 2.0(DescribeCaptchaResult + TC3-HMAC-SHA256)。
*
* @return true|string
*/
function aksrt_verify_tencent(array $pcfg, array $post_data, string $client_ip) {
$ticket = (string) ($post_data['tencent_ticket'] ?? '');
$randstr = (string) ($post_data['tencent_randstr'] ?? '');
if ($ticket === '' || $randstr === '') {
return __('缺少验证参数', 'aksrt');
}
if (str_starts_with($ticket, 'trerror_')) {
return __('验证码 SDK 异常,请刷新页面重试(容灾票据)', 'aksrt');
}
$secret_id = (string) ($pcfg['secret_id'] ?? '');
$secret_key = (string) ($pcfg['secret_key'] ?? '');
if ($secret_id === '' || $secret_key === '') {
return __('验证码配置不完整:缺少云 API 密钥 (SecretId/SecretKey)', 'aksrt');
}
$timestamp = time();
$date = gmdate('Y-m-d', $timestamp);
$payload = wp_json_encode([
'CaptchaType' => 9,
'CaptchaAppId' => (int) ($pcfg['app_id'] ?? 0),
'AppSecretKey' => (string) ($pcfg['app_secret_key'] ?? ''),
'Ticket' => $ticket,
'Randstr' => $randstr,
'UserIp' => $client_ip,
]);
$headers = aksrt_tencent_tc3_sign($secret_id, $secret_key, 'captcha', $timestamp, $date, 'DescribeCaptchaResult', '2019-07-22', $payload);
$curl_headers = [];
foreach ($headers as $k => $v) {
$curl_headers[] = "{$k}: {$v}";
}
$ch = curl_init('https://captcha.tencentcloudapi.com/');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => $curl_headers,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_FOLLOWLOCATION => false,
]);
$response_body = curl_exec($ch);
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_error) {
return '验证码服务请求失败: ' . $curl_error;
}
$body = json_decode((string) $response_body, true);
if (isset($body['Response']['Error'])) {
$err = $body['Response']['Error'];
return '腾讯 API 调用失败: ' . ($err['Message'] ?? $err['Code'] ?? '未知错误');
}
$resp = $body['Response'] ?? [];
$captcha_code = (int) ($resp['CaptchaCode'] ?? 0);
if ($captcha_code === 1) {
return true;
}
$messages = [
0 => '验证未通过',
7 => 'Randstr 不合法',
8 => 'Ticket 已过期(有效期 5 分钟)',
9 => 'Ticket 被重复使用',
15 => 'Ticket 不合法(解密失败)',
16 => 'CaptchaAppId 与 Ticket 不匹配',
21 => '票据校验异常(风控拦截)',
100 => '参数校验失败(AppId/SecretKey/Ticket 不匹配)',
];
$msg = $resp['CaptchaMsg'] ?? ($messages[$captcha_code] ?? '未知错误');
return '腾讯验证失败: ' . $msg . " (CaptchaCode: {$captcha_code})";
}
/**
* Cloudflare Turnstile siteverify。
*
* @return true|string
*/
function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_ip) {
$token = (string) ($post_data['cf_turnstile_response'] ?? '');
if ($token === '' || strlen($token) < 10) {
return __('缺少验证参数', 'aksrt');
}
$secret = (string) ($pcfg['secret_key'] ?? '');
$bodies = [
['secret' => $secret, 'response' => $token],
['secret' => $secret, 'response' => $token, 'remoteip' => $client_ip],
];
$body = [];
foreach ($bodies as $attempt) {
$resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'timeout' => 10,
'body' => $attempt,
]);
if (is_wp_error($resp)) {
continue;
}
$body = json_decode(wp_remote_retrieve_body($resp), true);
if (!empty($body['success'])) {
return true;
}
}
$codes = isset($body['error-codes']) && is_array($body['error-codes']) ? implode(', ', $body['error-codes']) : '未知错误';
return 'Cloudflare 验证失败: ' . $codes;
}
/**
* 极验 GeeTest v4 validate。
*
* @return true|string
*/
function aksrt_verify_geetest(array $pcfg, array $post_data) {
$lot_number = (string) ($post_data['geetest_lot_number'] ?? '');
$captcha_output = (string) ($post_data['geetest_captcha_output'] ?? '');
$pass_token = (string) ($post_data['geetest_pass_token'] ?? '');
$gen_time = (string) ($post_data['geetest_gen_time'] ?? '');
if ($lot_number === '' || $captcha_output === '' || $pass_token === '') {
return __('缺少验证参数', 'aksrt');
}
$captcha_key = (string) ($pcfg['captcha_key'] ?? '');
$sign_token = hash_hmac('sha256', $lot_number, $captcha_key);
$resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', [
'timeout' => 10,
'body' => [
'captcha_id' => (string) ($pcfg['captcha_id'] ?? ''),
'lot_number' => $lot_number,
'captcha_output' => $captcha_output,
'pass_token' => $pass_token,
'gen_time' => $gen_time,
'sign_token' => $sign_token,
],
]);
if (is_wp_error($resp)) {
return '验证请求失败: ' . $resp->get_error_message();
}
$body = json_decode(wp_remote_retrieve_body($resp), true);
if (!empty($body['result']) && $body['result'] === 'success') {
return true;
}
$reason = $body['reason'] ?? ($body['msg'] ?? '未知错误');
return '极验验证失败: ' . $reason;
}
/**
* 阿里云 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha)。
*
* @return true|string
*/
function aksrt_verify_aliyun(array $pcfg, array $post_data) {
$captcha_verify_param = (string) ($post_data['aliyun_captcha_verify_param'] ?? '');
if ($captcha_verify_param === '') {
return __('缺少验证参数 (captchaVerifyParam)', 'aksrt');
}
$region = (string) ($pcfg['region'] ?? 'cn');
$endpoint = $region === 'sgp'
? 'captcha.ap-southeast-1.aliyuncs.com'
: 'captcha.cn-shanghai.aliyuncs.com';
$params = [
'AccessKeyId' => (string) ($pcfg['access_key_id'] ?? ''),
'Action' => 'VerifyIntelligentCaptcha',
'Format' => 'JSON',
'SignatureMethod' => 'HMAC-SHA1',
'SignatureVersion' => '1.0',
'SignatureNonce' => uniqid('', true),
'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
'Version' => '2023-03-05',
'CaptchaVerifyParam' => $captcha_verify_param,
];
if (!empty($pcfg['scene_id'])) {
$params['SceneId'] = (string) $pcfg['scene_id'];
}
$params['Signature'] = aksrt_aliyun_afs_sign($params, (string) ($pcfg['access_key_secret'] ?? ''));
$resp = wp_remote_post('https://' . $endpoint . '/', [
'timeout' => 10,
'body' => $params,
]);
if (is_wp_error($resp)) {
return '验证请求失败: ' . $resp->get_error_message();
}
$body = json_decode(wp_remote_retrieve_body($resp), true);
if (!empty($body['Success'])) {
$verify_result = $body['Result']['VerifyResult'] ?? false;
if ($verify_result === true) {
return true;
}
$code_messages = [
'F001' => '疑似攻击请求,风险策略不通过',
'F002' => '验证参数为空',
'F003' => '验证参数格式不合法',
'F004' => '测试模式配置为不通过',
'F005' => '场景 ID 不合法',
'F006' => '场景 ID 与配置不匹配',
'F008' => '验证数据重复提交',
'F009' => '检测到虚拟设备环境',
'F010' => '同 IP 访问频率超限',
'F011' => '同设备访问频率超限',
'F012' => 'SceneID 与前端配置不一致',
'F014' => '无初始化记录或已过期(20 分钟)',
'F015' => '验证交互不通过',
'F018' => 'CaptchaVerifyParam 重复使用',
'F019' => '验证请求超时(90 秒)',
'F020' => 'CaptchaVerifyParam 与场景 ID 不匹配',
];
$verify_code = (string) ($body['Result']['VerifyCode'] ?? '');
return '阿里云 V3 验证失败: ' . ($code_messages[$verify_code] ?? '未知错误 (' . ($verify_code ?: 'N/A') . ')');
}
return '阿里云 V3 验证失败: ' . ($body['Message'] ?? '未知错误');
}
// ============================================================
// 前端渲染(挂载点 + 内联 SDK + AKSRTCaptchaState)
// ============================================================
/**
* 渲染人机验证挂载点。未启用场景直接返回。
*/
function aksrt_render_captcha($scene = 'comment') {
$scene = sanitize_key((string) $scene);
if (!aksrt_is_captcha_enabled_for($scene)) {
return;
}
$cfg = aksrt_get_captcha_config();
$provider = $cfg['active_provider'];
$pcfg = $cfg[$provider] ?? [];
if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) {
if (current_user_can('manage_options')) {
echo '<div class="aksrt-captcha-misconfig" style="font-size:12px;color:#d03050;margin:8px 0;">';
echo esc_html(sprintf(
/* translators: %s: captcha provider key */
__('人机验证服务商「%s」已启用但凭据未配齐,前台已跳过渲染。请到「功能&权限 → 人机验证」补全配置。', 'aksrt'),
$provider
));
echo '</div>';
}
return;
}
$uid = 'aksrt-captcha-' . $scene . '-' . wp_unique_id();
$vid = 'captcha_verified_' . $scene . '_' . wp_unique_id();
$js_config = [
'provider' => $provider,
'scene' => $scene,
'uid' => $uid,
'vid' => $vid,
'tc' => ['app_id' => (string) ($pcfg['app_id'] ?? '')],
'cf' => [
'site_key' => (string) ($pcfg['site_key'] ?? ''),
'theme' => (string) ($pcfg['theme'] ?? 'auto'),
'size' => (string) ($pcfg['size'] ?? 'normal'),
],
'gt' => [
'captcha_id' => trim((string) ($pcfg['captcha_id'] ?? '')),
'product' => (string) ($pcfg['product'] ?? 'bind'),
],
'al' => [
'region' => (string) ($pcfg['region'] ?? 'cn'),
'prefix' => (string) ($pcfg['prefix'] ?? ''),
'scene_id' => (string) ($pcfg['scene_id'] ?? ''),
],
];
$token_fields = ['cf_turnstile_response', 'tencent_ticket', 'tencent_randstr', 'tencent_sid',
'geetest_lot_number', 'geetest_captcha_output', 'geetest_pass_token', 'geetest_gen_time',
'aliyun_captcha_verify_param'];
$token_fields_json = wp_json_encode($token_fields, JSON_UNESCAPED_UNICODE);
?>
<div class="aksrt-captcha-wrap" data-provider="<?php echo esc_attr($provider); ?>" data-scene="<?php echo esc_attr($scene); ?>">
<div id="<?php echo esc_attr($uid); ?>" class="aksrt-captcha-box" style="display:none"></div>
<input type="hidden" name="captcha_verified" id="<?php echo esc_attr($vid); ?>" value="0">
</div>
<link rel="stylesheet" href="<?php echo esc_url(AKSRT_URI . '/assets/css/shared/captcha.css'); ?>">
<script>(function(){
var C=<?php echo wp_json_encode($js_config, JSON_UNESCAPED_UNICODE); ?>;
var TOKEN_FIELDS=<?php echo $token_fields_json; ?>;
var root=window.AKSRTCaptchaState=window.AKSRTCaptchaState||{loaders:{}, scenes:{}};
function loadScriptOnce(key, src, readyCheck){
if(typeof readyCheck==='function' && readyCheck()){ return Promise.resolve(); }
if(root.loaders[key]){ return root.loaders[key]; }
root.loaders[key]=new Promise(function(resolve, reject){
var existing=document.querySelector('script[data-aksrt-sdk="'+key+'"]');
if(existing){
if(typeof readyCheck==='function' && readyCheck()){ resolve(); return; }
existing.addEventListener('load', function(){ resolve(); }, {once:true});
existing.addEventListener('error', function(){ reject(new Error(key+' load failed')); }, {once:true});
return;
}
var script=document.createElement('script');
script.src=src; script.async=true; script.setAttribute('data-aksrt-sdk', key);
script.onload=function(){ resolve(); };
script.onerror=function(){ reject(new Error(key+' load failed')); };
document.head.appendChild(script);
});
return root.loaders[key];
}
var box, hField, form=null;
var verified=false;
var pendingAction=null;
var triggerCaptcha=function(){ showTip('验证码加载中,请稍候', '#e67e22'); };
function findForm(){
form=box.closest('form');
if(!form&&C.scene==='comment') form=document.getElementById('commentform');
if(!form&&(C.scene==='login'||C.scene==='register')){
form=document.getElementById('aksrt-login-form')
||document.getElementById('aksrt-modal-login-form')
||document.getElementById('sign-login-form')
||document.getElementById('aksrt-phone-login-form')
||document.getElementById('aksrt-modal-register-form')
||document.getElementById('sign-register-form')
||document.getElementById('aksrt-phone-register-form')
||document.querySelector('.auth-form')
||document.querySelector('.sign-form');
}
if(!form&&C.scene==='bind_phone'){
form=document.getElementById('aksrt-bind-phone-form')
||document.querySelector('[data-aksrt-bind-phone-form]')
||document.querySelector('.auth-form');
}
return form;
}
function bindSubmit(){
if(!form) return;
form.addEventListener('submit', function(e){
if(verified || hField.value==='1') return;
e.preventDefault(); e.stopImmediatePropagation();
pendingAction=null; triggerCaptcha();
}, true);
}
function setField(name, value){
var container = form || box;
var el=container.querySelector('input[name="'+name+'"]');
if(!el){ el=document.createElement('input'); el.type='hidden'; el.name=name; container.appendChild(el); }
el.value=value;
}
function resetVerificationState(){
verified=false;
hField.value='0';
var container = form || box;
TOKEN_FIELDS.forEach(function(n){
var f=container.querySelector('input[name="'+n+'"]');
if(f) f.remove();
});
if(C.scene==='download'){
syncDownloadSiblings(false);
}
}
function syncDownloadSiblings(passed){
var myContainer=box.closest('[data-aksrt-download-captcha]');
if(!myContainer) return;
document.querySelectorAll('[data-aksrt-download-captcha]').forEach(function(target){
if(target===myContainer) return;
var tgtH=target.querySelector('input[name="captcha_verified"]');
if(tgtH) tgtH.value=passed ? '1' : '0';
TOKEN_FIELDS.forEach(function(name){
var f=target.querySelector('input[name="'+name+'"]');
if(f) f.remove();
if(passed){
var srcEl=myContainer.querySelector('input[name="'+name+'"]');
if(srcEl&&srcEl.value!==''){
var tgtEl=document.createElement('input'); tgtEl.type='hidden'; tgtEl.name=name; target.appendChild(tgtEl);
tgtEl.value=srcEl.value;
}
}
});
});
}
function submitNativeForm(){
if(!form) return;
if(form.requestSubmit){ form.requestSubmit(); }
else { HTMLFormElement.prototype.submit.call(form); }
}
function submitForm(){
verified=true; hField.value='1';
if(C.scene==='download') syncDownloadSiblings(true);
if(typeof pendingAction==='function'){
finishPending(true); return;
}
setTimeout(resetVerificationState, 3000);
submitNativeForm();
}
function showTip(msg, color){
var t=box.querySelector('.captcha-tip');
if(!t){ t=document.createElement('div'); t.className='captcha-tip'; t.style.cssText='font-size:12px;margin-top:4px;text-align:left;'; box.appendChild(t); }
t.style.color=color||'#e74c3c'; t.textContent=msg;
setTimeout(function(){ if(t.parentNode) t.remove(); }, 4000);
}
function showLoading(){ box.innerHTML='<div style="text-align:center;padding:20px;color:#9ca3af;">验证码加载中...</div>'; }
function clearLoading(){ box.innerHTML=''; }
function finishPending(ok){
var action=pendingAction; pendingAction=null;
if(typeof action==='function'){ action(!!ok); }
}
function cancelPending(msg){
if(msg) showTip(msg, '#e67e22');
finishPending(false);
}
root.scenes[C.scene]={
verify:function(callback){
if(verified || hField.value==='1'){
if(typeof callback==='function') callback(true);
return;
}
pendingAction=(typeof callback==='function')?function(ok){ callback(ok!==false); }:null;
triggerCaptcha();
},
reset:resetVerificationState
};
// ============ 腾讯云 Captcha 2.0 ============
function loadTencent(){
showLoading();
loadScriptOnce('tencent', 'https://turing.captcha.qcloud.com/TJCaptcha.js', function(){
return typeof TencentCaptcha!=='undefined';
}).then(function(){
clearLoading();
var capIns=null;
triggerCaptcha=function(){
if(!capIns){
capIns=new TencentCaptcha(C.tc.app_id, function(res){
if(res.ret===2){ cancelPending('已取消验证'); return; }
if(res.ret===0 && res.ticket){
if(res.ticket.indexOf('trerror_')===0){
showTip('验证码服务异常,请刷新页面重试', '#e67e22'); return;
}
setField('tencent_ticket', res.ticket);
setField('tencent_randstr', res.randstr);
setField('tencent_sid', res.sid||'');
submitForm();
return;
}
if(res.errorCode){
var errorMap={1001:'验证码组件加载失败',1002:'验证码超时',1006:'验证码配置拉取失败',1008:'验证码页面加载异常',1012:'验证码刷新次数过多',1013:'验证码网络异常'};
showTip(errorMap[res.errorCode]||('验证码异常(errorCode:'+res.errorCode+')'), '#e74c3c');
}
}, {userLanguage:'zh-cn'});
}
capIns.show();
};
}).catch(function(){ clearLoading(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
}
// ============ Cloudflare Turnstile(弹窗) ============
function loadCloudflare(){
var cfWidgetId=null, cfVerifying=false, cfModal=null;
if(typeof turnstile==='undefined'){
window['onTurnstileLoad_'+C.uid]=function(){};
loadScriptOnce('cloudflare', 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileLoad_'+C.uid+'&render=explicit', function(){
return typeof turnstile!=='undefined';
});
}
function renderWidget(){
if(cfWidgetId){ try { turnstile.remove(cfWidgetId); } catch(e) {} cfWidgetId=null; }
if(typeof turnstile==='undefined'){ setTimeout(renderWidget, 200); return; }
cfWidgetId=turnstile.render('#modalWidget_'+C.uid, {
sitekey:C.cf.site_key, theme:C.cf.theme, size:C.cf.size, action:'aksrt-verify',
callback:function(token){
if(!cfVerifying) return;
cfVerifying=false;
setField('cf_turnstile_response', token);
closeModal(); submitForm();
},
'error-callback':function(){ showTip('验证码失败,请重试', '#e74c3c'); cfWidgetId=null; setTimeout(renderWidget, 500); },
'expired-callback':function(){ showTip('验证码已过期,请重新验证', '#e67e22'); cfWidgetId=null; setTimeout(renderWidget, 500); }
});
}
function createModal(){
cfModal=document.createElement('div');
cfModal.className='aksrt-captcha-modal-overlay';
cfModal.innerHTML='<div class="aksrt-captcha-modal-backdrop"></div>'+
'<div class="aksrt-captcha-modal-box"><div class="aksrt-captcha-modal-header">'+
'<span class="aksrt-captcha-modal-title">人机验证</span>'+
'<button type="button" class="aksrt-captcha-modal-close">&times;</button></div>'+
'<div class="aksrt-captcha-modal-body"><p class="aksrt-captcha-modal-hint">请完成验证后继续操作</p>'+
'<div id="modalWidget_'+C.uid+'" class="aksrt-captcha-modal-widget"></div></div></div>';
document.body.appendChild(cfModal);
var cancelFn=function(){ cfVerifying=false; closeModal(); cancelPending('已取消验证'); };
cfModal.querySelector('.aksrt-captcha-modal-backdrop').addEventListener('click', cancelFn);
cfModal.querySelector('.aksrt-captcha-modal-close').addEventListener('click', cancelFn);
renderWidget();
}
function closeModal(){
if(cfWidgetId){ try { turnstile.remove(cfWidgetId); } catch(e) {} cfWidgetId=null; }
if(cfModal && cfModal.parentNode){ cfModal.parentNode.removeChild(cfModal); }
cfModal=null;
}
triggerCaptcha=function(){
if(cfVerifying){ showTip('验证码进行中,请稍候', '#e67e22'); return; }
cfVerifying=true; createModal();
};
}
// ============ 极验 GeeTest v4 ============
function loadGeetest(){
showLoading();
var captchaId=(C.gt && C.gt.captcha_id) ? String(C.gt.captcha_id).trim() : '';
var product=(C.gt && C.gt.product) ? String(C.gt.product) : 'bind';
if(!captchaId){
clearLoading();
showTip('极验 Captcha ID 未配置,请到后台「功能&权限 → 人机验证」填写并保存', '#e74c3c');
return;
}
function render(){
clearLoading();
initGeetest4({captchaId:captchaId, product:product, hideSuccess:true}, function(capIns){
window['__gt_'+C.uid]=capIns;
function onSuccess(){
var result=capIns.getValidate();
if(!result) return;
setField('geetest_lot_number', result.lot_number);
setField('geetest_captcha_output', result.captcha_output);
setField('geetest_pass_token', result.pass_token);
setField('geetest_gen_time', result.gen_time);
submitForm();
}
if(product==='bind' || product==='popup'){
capIns.onReady(function(){}).onSuccess(onSuccess).onClose(function(){});
}else{
capIns.appendTo('#'+C.uid).onSuccess(onSuccess).onClose(function(){});
}
});
triggerCaptcha=function(){
var instance=window['__gt_'+C.uid];
if(!instance){ showTip('验证码加载中,请稍候再试', '#e67e22'); return; }
if(product==='bind' || product==='popup'){ instance.showCaptcha(); return; }
showTip('请先完成人机验证', '#e67e22');
};
}
if(typeof initGeetest4==='undefined'){
loadScriptOnce('geetest', 'https://static.geetest.com/v4/gt4.js', function(){
return typeof initGeetest4!=='undefined';
}).then(render).catch(function(){ clearLoading(); showTip('极验组件加载失败', '#e74c3c'); });
return;
}
render();
}
// ============ 阿里云 CAPTCHA 2.0 V3 ============
function loadAliyun(){
showLoading();
window.AliyunCaptchaConfig={ region:C.al.region||'cn', prefix:C.al.prefix };
function render(){
clearLoading();
var captchaIns=null;
var btn=box.closest('form') ? box.closest('form').querySelector('button[type="submit"], input[type="submit"]') : null;
window.initAliyunCaptcha({
SceneId:C.al.scene_id, mode:'popup', element:'#'+C.uid,
button:btn ? '#' + (btn.id || (function(){ btn.id='aksrt-captcha-btn-'+C.scene; return btn.id; })()) : null,
success:function(captchaVerifyParam){
setField('aliyun_captcha_verify_param', captchaVerifyParam);
submitForm();
},
fail:function(result){ showTip('验证码失败: ' + (result || '请重试'), '#e74c3c'); captchaIns=null; },
getInstance:function(instance){ captchaIns=instance; },
slideStyle:{width:360, height:40}, language:'cn'
});
triggerCaptcha=function(){
if(captchaIns && captchaIns.show){ captchaIns.show(); return; }
showTip('验证码组件加载中,请稍候再试', '#e67e22');
};
}
if(typeof window.initAliyunCaptcha==='undefined'){
loadScriptOnce('aliyun', 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js', function(){
return typeof window.initAliyunCaptcha!=='undefined';
}).then(render).catch(function(){ clearLoading(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
return;
}
render();
}
function loadProvider(){
switch(C.provider){
case 'tencent': loadTencent(); break;
case 'cloudflare': loadCloudflare(); break;
case 'geetest': loadGeetest(); break;
case 'aliyun': loadAliyun(); break;
}
}
function init(){
box=document.getElementById(C.uid);
hField=document.getElementById(C.vid);
if(!box||!hField) return;
var tries=0;
(function retryFind(){
findForm();
if(!form && ++tries < 10){ setTimeout(retryFind, 150); }
else {
if(form) bindSubmit();
setTimeout(loadProvider, 0);
}
})();
}
if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', init); }
else { setTimeout(init, 50); }
})();</script>
<?php
}
@@ -0,0 +1,228 @@
<?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;
}
}
+325
View File
@@ -0,0 +1,325 @@
<?php
/**
* AKSRT Comment Moderation System
*
* @package AKSRT
* @version 3.4.1
*/
defined('ABSPATH') || exit;
class AKSRT_Moderation {
use AKSRT_Moderation_Part_1;
use AKSRT_Moderation_Part_2;
private static $instance = null;
private $config = [];
/**
* Runtime map: fingerprint => moderation decision payload.
*
* @var array<string, array<string, mixed>>
*/
private $moderation_result_map = [];
/**
* Runtime map: fingerprint => forced comment_approved value.
*
* @var array<string, string|int>
*/
private $forced_approved_map = [];
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->config = get_option('aksrt_moderation_config', []);
}
public function init(): void {
add_filter('preprocess_comment', [$this, 'process'], 5);
add_filter('pre_comment_approved', [$this, 'enforce_pre_comment_approved'], 50, 2);
add_action('comment_post', [$this, 'after_post'], 10, 3);
// Native WP Admin > Comments column.
add_filter('manage_edit-comments_columns', [$this, 'register_comments_columns']);
add_action('manage_comments_custom_column', [$this, 'render_comments_custom_column'], 10, 2);
}
public function get_config(): array {
$defaults = [
'enabled' => false,
'akismet_enabled' => false,
'akismet_site_url' => '',
'akismet_blog_lang' => 'zh-CN',
'akismet_api_key' => '',
'ai_enabled' => false,
'ai_moderate_comments' => true,
'ai_moderate_background' => true,
'auto_approve_low_risk' => true,
'auto_reject_high_risk' => true,
'low_risk_max_score' => 35,
'high_risk_min_score' => 80,
'blocked_keywords' => '',
'rate_limit_enabled' => false,
'rate_limit_min_interval' => 8,
'rate_limit_max_per_hour' => 15,
'geoip_enabled' => false,
'geoip_provider' => 'ipapi',
'geoip_api_key' => '',
'geoip_block_countries' => [],
];
return array_merge($defaults, $this->config);
}
/**
* Main moderation entry.
*/
public function process(array $commentdata): array {
// 人机验证:覆盖 REST 与原生 wp-comments-post 两条提交合同(与审核开关无关)
if (function_exists('aksrt_require_captcha')) {
$payload = is_array($_POST ?? null) ? wp_unslash($_POST) : [];
if (isset($payload['captcha']) && is_array($payload['captcha'])) {
$payload = array_merge($payload, $payload['captcha']);
}
$captcha_result = aksrt_require_captcha('comment', $payload);
if ($captcha_result !== true) {
$msg = is_string($captcha_result) ? $captcha_result : __('人机验证失败', 'aksrt');
wp_die(
esc_html($msg),
esc_html__('评论提交失败', 'aksrt'),
['response' => 400, 'back_link' => true]
);
}
}
$config = $this->get_config();
if (empty($config['enabled'])) {
return $commentdata;
}
$client_ip = $this->get_client_ip();
$decision = $this->new_default_result();
$decision['ip'] = $client_ip;
try {
// 1) Rate limit.
$rate_result = $this->check_rate_limit($commentdata, $config, $client_ip);
if ($rate_result !== true) {
$this->remember_result($commentdata, [
'result' => 'blocked',
'source' => 'rate_limit',
'reason' => 'rate_limit_triggered',
]);
wp_die(
'<p>' . esc_html($rate_result) . '</p>',
__('Rate limit', 'aksrt'),
['response' => 429]
);
}
// 2) GeoIP restriction.
$geoip_result = $this->check_geoip($config, $client_ip);
if ($geoip_result !== true) {
$this->remember_result($commentdata, [
'result' => 'blocked',
'source' => 'geoip',
'reason' => 'geoip_blocked',
]);
wp_die(
'<p>' . esc_html($geoip_result) . '</p>',
__('Geo restriction', 'aksrt'),
['response' => 403]
);
}
// 3) Blocked keywords: force pending and write AI-like reason.
$keyword = $this->check_blocked_keywords($commentdata, $config);
$keyword_hit = !empty($keyword['hit']);
if ($keyword_hit) {
$commentdata['comment_approved'] = 0;
$decision['result'] = 'pending';
$decision['source'] = 'ai';
$decision['reason'] = 'keyword_hit';
$decision['ai_flagged'] = '1';
$decision['ai_score'] = '100';
$decision['ai_top_category'] = 'blocked_keyword';
$decision['keyword'] = (string) ($keyword['keyword'] ?? '');
$decision['keyword_match_type'] = (string) ($keyword['match_type'] ?? 'plain');
}
// 4) Akismet.
$akismet = $this->check_akismet($commentdata, $config, $client_ip);
if ($akismet === 'spam') {
$commentdata['comment_approved'] = 'spam';
$decision['result'] = 'spam';
$decision['source'] = 'akismet';
$decision['reason'] = 'spam';
$decision['akismet'] = 'spam';
} elseif ($akismet === 'ham') {
$decision['akismet'] = 'ham';
if ($decision['source'] === 'wp_default') {
$decision['source'] = 'akismet';
$decision['reason'] = 'ham';
}
} elseif ($akismet === 'error') {
$decision['akismet'] = 'error';
} else {
$decision['akismet'] = 'unknown';
}
// 5) AI moderation provider (Baidu only).
// If keyword already hit, keep keyword decision locked and skip AI override.
if (!$keyword_hit) {
$ai = $this->check_ai_moderation($commentdata, $config);
if (is_array($ai)) {
$score = (float) ($ai['score'] ?? 0);
$flagged = !empty($ai['flagged']) ? '1' : '0';
$top_category = (string) ($ai['top_category'] ?? '');
$decision['ai_score'] = (string) round($score, 2);
$decision['ai_flagged'] = $flagged;
$decision['ai_top_category'] = $top_category;
if (!empty($config['auto_reject_high_risk']) && $score >= (float) ($config['high_risk_min_score'] ?? 80)) {
// High-risk comments are auto rejected into spam list.
$commentdata['comment_approved'] = 'spam';
$decision['result'] = 'spam';
$decision['source'] = 'ai';
$decision['reason'] = 'ai_high_risk_auto_reject';
} elseif (!empty($config['auto_approve_low_risk']) && $score <= (float) ($config['low_risk_max_score'] ?? 35)) {
// Do not override stricter decision.
if (!isset($commentdata['comment_approved']) || (string) $commentdata['comment_approved'] === '1') {
$commentdata['comment_approved'] = 1;
$decision['result'] = 'approved';
$decision['source'] = 'ai';
$decision['reason'] = 'ai_low_risk_auto_approve';
}
} else {
if (!isset($commentdata['comment_approved']) || (string) $commentdata['comment_approved'] === '1') {
$commentdata['comment_approved'] = 0;
$decision['result'] = 'pending';
$decision['source'] = 'ai';
$decision['reason'] = 'ai_medium_risk_pending';
}
}
} elseif ($ai === 'rejected') {
$commentdata['comment_approved'] = 'spam';
$decision['result'] = 'spam';
$decision['source'] = 'ai';
$decision['reason'] = 'ai_rejected_auto_reject';
} elseif ($ai === 'error') {
// Fail-safe: AI enabled but response is invalid/unusable, do not auto approve.
if (!isset($commentdata['comment_approved']) || (string) $commentdata['comment_approved'] === '1') {
$commentdata['comment_approved'] = 0;
$decision['result'] = 'pending';
}
$decision['source'] = 'ai';
$decision['reason'] = 'ai_response_invalid_pending';
$decision['ai_flagged'] = '1';
}
}
$decision = $this->enrich_moderation_result($decision, $commentdata);
$this->remember_result($commentdata, $decision);
} catch (Exception $e) {
// Intentionally silent: keep moderation flow without writing runtime logs.
}
return $commentdata;
}
/**
* Keep pre_comment_approved in sync with preprocess_comment outcome.
*/
public function enforce_pre_comment_approved($approved, array $commentdata) {
if (!is_array($commentdata)) {
return $approved;
}
$fingerprint = $this->comment_fingerprint($commentdata);
if ($fingerprint !== '' && array_key_exists($fingerprint, $this->forced_approved_map)) {
$forced = $this->forced_approved_map[$fingerprint];
return $forced;
}
if (array_key_exists('comment_approved', $commentdata)) {
return $commentdata['comment_approved'];
}
return $approved;
}
/**
* Persist moderation details to comment meta.
*/
public function after_post(int $comment_id, $comment_approved, array $commentdata = []): void {
$comment = get_comment($comment_id);
if (!$comment instanceof WP_Comment) {
return;
}
$decision = $this->resolve_result_from_runtime_or_default($commentdata, $comment);
$decision = $this->enrich_moderation_result($decision, (array) $comment);
update_comment_meta($comment_id, 'aksrt_moderation_result', (string) $decision['result']);
update_comment_meta($comment_id, 'aksrt_moderation_source', (string) $decision['source']);
update_comment_meta($comment_id, 'aksrt_moderation_reason', (string) $decision['reason']);
update_comment_meta($comment_id, 'aksrt_moderation_akismet', (string) $decision['akismet']);
update_comment_meta($comment_id, 'aksrt_moderation_ai_score', (string) $decision['ai_score']);
update_comment_meta($comment_id, 'aksrt_moderation_ai_flagged', (string) $decision['ai_flagged']);
update_comment_meta($comment_id, 'aksrt_moderation_ai_top_category', (string) $decision['ai_top_category']);
update_comment_meta($comment_id, 'aksrt_moderation_keyword', (string) $decision['keyword']);
update_comment_meta($comment_id, 'aksrt_moderation_keyword_match_type', (string) $decision['keyword_match_type']);
$moderation_user_id = isset($comment->user_id) ? (int) $comment->user_id : 0;
$moderation_user_ip = trim((string) ($comment->comment_author_IP ?? ''));
if ($moderation_user_ip === '') {
$moderation_user_ip = trim((string) ($decision['ip'] ?? ''));
}
if ($moderation_user_ip === '') {
$moderation_user_ip = trim((string) ($commentdata['comment_author_IP'] ?? ($commentdata['comment_author_ip'] ?? '')));
}
update_comment_meta($comment_id, 'aksrt_moderation_user_id', (string) $moderation_user_id);
update_comment_meta($comment_id, 'aksrt_moderation_user_ip', $moderation_user_ip);
// Convenience marker for external tools expecting Akismet style keys.
if ($decision['source'] === 'akismet' && $decision['akismet'] === 'spam') {
update_comment_meta($comment_id, 'akismet_result', 'true');
} elseif ($decision['source'] === 'akismet' && $decision['akismet'] === 'ham') {
update_comment_meta($comment_id, 'akismet_result', 'false');
}
// Clean runtime maps.
$fp_comment = $this->comment_fingerprint((array) $comment);
$fp_data = $this->comment_fingerprint($commentdata);
foreach (array_unique(array_filter([$fp_comment, $fp_data])) as $fp) {
unset($this->moderation_result_map[$fp], $this->forced_approved_map[$fp]);
}
}
public function register_comments_columns(array $columns): array {
$new = [];
foreach ($columns as $key => $label) {
$new[$key] = $label;
if ($key === 'comment') {
$new['aksrt_moderation'] = __('Moderation', 'aksrt');
}
}
if (!isset($new['aksrt_moderation'])) {
$new['aksrt_moderation'] = __('Moderation', 'aksrt');
}
return $new;
}
}
return AKSRT_Moderation::get_instance();
@@ -0,0 +1,316 @@
<?php
/**
* AKSRT OAuth Proxy Client
*
* 代理登录客户端:将 OAuth 流程委托给代理服务端执行。
* 本机无需配置任何平台的 OAuth 凭据,只需配置服务端地址和通讯密钥。
*
* 流程:
* 1. 用户点击第三方登录 → 生成代理 URL 跳转到服务端
* 2. 服务端执行 OAuth 流程 → 回调时重定向回本机
* 3. 本机验证签名、提取用户数据 → find_or_create_user
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_OAuth_Proxy_Client {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {}
/**
* 初始化:注册回调路由
*/
public function init(): void {
add_action('template_redirect', [$this, 'handle_proxy_callback']);
add_action('init', [$this, 'add_rewrite_rules']);
}
/**
* 添加 URL 重写规则
*/
public function add_rewrite_rules(): void {
add_rewrite_rule(
'^oauth/proxy/callback/?$',
'index.php?aksrt_proxy_callback=1',
'top'
);
add_filter('query_vars', function ($vars) {
$vars[] = 'aksrt_proxy_callback';
return $vars;
});
}
/**
* 生成代理登录 URL
*
* @param string $provider 平台标识
* @param string $action 操作类型: 'login' | 'bind'
* @return string 代理登录 URL
*/
public function get_proxy_auth_url(string $provider, string $action = 'login'): string {
$server_url = AKSRT_Settings::oauth_proxy_server_url();
$secret = AKSRT_Settings::oauth_proxy_secret();
if (empty($server_url) || empty($secret)) {
return '';
}
$back_url = home_url('/oauth/proxy/callback');
$uid = get_current_user_id();
$nonce = wp_create_nonce('aksrt_proxy_' . $provider . '_' . $uid);
$params = [
'provider' => $provider,
'agent_back_url' => $back_url,
'action' => $action,
'nonce' => $nonce,
'uid' => $uid,
'ts' => time(),
];
$params['sign'] = $this->sign($params, $secret);
$server_url = rtrim($server_url, '/');
return $server_url . '/oauth/proxy/' . $provider . '?' . http_build_query($params);
}
/**
* 处理代理回调(从服务端重定向回来)
*/
public function handle_proxy_callback(): void {
if (!get_query_var('aksrt_proxy_callback')) {
return;
}
$secret = AKSRT_Settings::oauth_proxy_secret();
if (empty($secret)) {
wp_die(__('代理登录配置错误', 'aksrt'));
}
$sign = sanitize_text_field($_GET['sign'] ?? '');
$provider = sanitize_key($_GET['provider'] ?? '');
$action = sanitize_key($_GET['action'] ?? 'login');
if (empty($sign) || empty($provider)) {
wp_die(__('代理回调参数无效', 'aksrt'));
}
// 验证签名
$verify_params = $_GET;
unset($verify_params['sign']);
if (!$this->verify_sign($verify_params, $secret, $sign)) {
wp_die(__('代理回调签名验证失败', 'aksrt'));
}
// 验证时间戳(5分钟内有效)
$ts = (int) ($_GET['ts'] ?? 0);
if (abs(time() - $ts) > 300) {
wp_die(__('代理回调已过期,请重新登录', 'aksrt'));
}
// 提取用户数据
$provider_id = sanitize_text_field($_GET['provider_id'] ?? '');
if (empty($provider_id)) {
wp_die(__('代理回调缺少用户标识', 'aksrt'));
}
$user_data = [
'provider' => $provider,
'provider_id' => $provider_id,
'username' => sanitize_user($_GET['username'] ?? '', true),
'name' => sanitize_text_field($_GET['name'] ?? ''),
'email' => sanitize_email($_GET['email'] ?? ''),
'email_verified' => !empty($_GET['email_verified']),
'avatar_url' => esc_url_raw($_GET['avatar_url'] ?? ''),
'bio' => sanitize_text_field($_GET['bio'] ?? ''),
];
if ($action === 'bind') {
$this->handle_proxy_bind($provider, $user_data);
return;
}
$this->handle_proxy_login($provider, $user_data);
}
/**
* 处理代理登录
*/
private function handle_proxy_login(string $provider, array $user_data): void {
$user = $this->find_or_create_user_from_proxy($provider, $user_data);
// 未匹配已有账号 → 跳转绑定页
if (is_wp_error($user) && $user->get_error_code() === 'oauth_no_match') {
$error_data = $user->get_error_data();
$obk = 'aksrt_ob_' . wp_generate_password(24, false);
set_transient($obk, [
'provider' => $error_data['provider'],
'provider_id' => $error_data['user_data']['provider_id'] ?? '',
'user_data' => $error_data['user_data'],
'token' => [],
], 15 * MINUTE_IN_SECONDS);
$bind_url = aksrt_get_bind_url();
wp_safe_redirect(add_query_arg('obk', $obk, $bind_url));
exit;
}
if (is_wp_error($user)) {
wp_die(__('登录失败:', 'aksrt') . esc_html($user->get_error_message()));
}
// 登录成功
wp_set_auth_cookie($user->ID, true);
$redirect_url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/');
wp_safe_redirect(add_query_arg(['oauth_action' => 'login', 'oauth_bound' => $provider], $redirect_url));
exit;
}
/**
* 处理代理绑定(已登录用户绑定第三方账号)
*/
private function handle_proxy_bind(string $provider, array $user_data): void {
$current_uid = get_current_user_id();
if (!$current_uid) {
wp_safe_redirect(aksrt_get_login_url());
exit;
}
// 检查是否已被其他用户占用
$existing = $this->find_user_by_provider_meta($provider, $user_data['provider_id']);
if ($existing && (int) $existing->ID !== $current_uid) {
wp_die(__('该第三方账号已被其他用户绑定', 'aksrt'));
}
$this->bind_provider_to_user($current_uid, $provider, $user_data);
$redirect_url = function_exists('aksrt_get_user_center_url')
? add_query_arg(['oauth_action' => 'bind', 'oauth_bound' => $provider], aksrt_get_user_center_url('settings'))
: home_url('/');
wp_safe_redirect($redirect_url);
exit;
}
/**
* 查找或创建用户(代理模式)
*
* @return WP_User|WP_Error
*/
private function find_or_create_user_from_proxy(string $provider, array $user_data) {
$email = $user_data['email'] ?? '';
// 1. 通过 provider_id 查找
if (!empty($user_data['provider_id'])) {
$existing = $this->find_user_by_provider_meta($provider, $user_data['provider_id']);
if ($existing instanceof WP_User) {
$this->ensure_binding_meta($existing->ID, $provider, $user_data);
return $existing;
}
}
// 2. 通过邮箱查找
if (!empty($email) && is_email($email)) {
$existing = get_user_by('email', $email);
if ($existing && !$existing instanceof WP_Error) {
$this->ensure_binding_meta($existing->ID, $provider, $user_data);
return $existing;
}
}
// 3. 未匹配
return new WP_Error('oauth_no_match', __('需要绑定已有账号', 'aksrt'), [
'provider' => $provider,
'user_data' => $user_data,
]);
}
/**
* 确保绑定 meta 存在
*/
private function ensure_binding_meta(int $user_id, string $provider, array $user_data): void {
update_user_meta($user_id, "aksrt_{$provider}_id", $user_data['provider_id'] ?? '');
$bindings = get_user_meta($user_id, 'aksrt_oauth_bindings', true) ?: [];
if (!is_array($bindings)) {
$bindings = [];
}
if (!in_array($provider, $bindings, true)) {
$bindings[] = $provider;
update_user_meta($user_id, 'aksrt_oauth_bindings', $bindings);
}
if (!empty($user_data['avatar_url'])) {
update_user_meta($user_id, 'aksrt_oauth_avatar', esc_url_raw($user_data['avatar_url']));
}
}
/**
* 绑定第三方账号到用户
*/
private function bind_provider_to_user(int $user_id, string $provider, array $user_data): void {
if (!empty($user_data['provider_id'])) {
update_user_meta($user_id, "aksrt_{$provider}_id", $user_data['provider_id']);
}
$bindings = get_user_meta($user_id, 'aksrt_oauth_bindings', true) ?: [];
if (!is_array($bindings)) {
$bindings = [];
}
if (!in_array($provider, $bindings, true)) {
$bindings[] = $provider;
update_user_meta($user_id, 'aksrt_oauth_bindings', $bindings);
}
if (!empty($user_data['avatar_url'])) {
update_user_meta($user_id, 'aksrt_oauth_avatar', esc_url_raw($user_data['avatar_url']));
}
}
/**
* 通过 provider meta 查找用户
*/
private function find_user_by_provider_meta(string $provider, string $provider_id): ?WP_User {
if (empty($provider_id)) {
return null;
}
$users = get_users([
'meta_key' => "aksrt_{$provider}_id",
'meta_value' => $provider_id,
'number' => 1,
]);
return !empty($users[0]) ? $users[0] : null;
}
/**
* 生成 HMAC-SHA256 签名
*/
private function sign(array $data, string $secret): string {
unset($data['sign']);
ksort($data);
$payload = http_build_query($data);
return hash_hmac('sha256', $payload, $secret);
}
/**
* 验证 HMAC-SHA256 签名
*/
private function verify_sign(array $data, string $secret, string $expected_sign): bool {
$computed = $this->sign($data, $secret);
return hash_equals($computed, $expected_sign);
}
}
return AKSRT_OAuth_Proxy_Client::get_instance();
@@ -0,0 +1,294 @@
<?php
/**
* AKSRT OAuth Proxy Server
*
* 代理登录服务端:接收客户端的代理请求,执行 OAuth 流程后将用户数据回传。
* 服务端需要配置各平台的 OAuth 凭据。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_OAuth_Proxy_Server {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {}
public function init(): void {
add_action('template_redirect', [$this, 'handle_proxy_routes'], 5);
add_action('init', [$this, 'add_rewrite_rules']);
}
public function add_rewrite_rules(): void {
add_rewrite_rule('^oauth/proxy/([a-z]+)/?$', 'index.php?aksrt_proxy_provider=$matches[1]', 'top');
add_rewrite_rule('^oauth/proxy/([a-z]+)/callback/?$', 'index.php?aksrt_proxy_provider=$matches[1]&aksrt_proxy_server_callback=1', 'top');
add_filter('query_vars', function ($vars) {
$vars[] = 'aksrt_proxy_provider';
$vars[] = 'aksrt_proxy_server_callback';
return $vars;
});
}
public function handle_proxy_routes(): void {
$provider = sanitize_key(get_query_var('aksrt_proxy_provider'));
if (empty($provider)) return;
if (AKSRT_Settings::oauth_proxy_mode() !== 'server') return;
$allowed = AKSRT_Settings::oauth_proxy_allowed_providers();
if (!in_array($provider, $allowed, true)) {
wp_die(__('不支持的代理登录平台', 'aksrt'));
}
$is_callback = !empty(get_query_var('aksrt_proxy_server_callback'));
if ($is_callback) {
$this->handle_proxy_callback($provider);
} else {
$this->handle_proxy_request($provider);
}
}
private function handle_proxy_request(string $provider): void {
$secret = AKSRT_Settings::oauth_proxy_secret();
if (empty($secret)) {
wp_die(__('代理登录服务端未配置密钥', 'aksrt'));
}
$agent_back_url = esc_url_raw($_GET['agent_back_url'] ?? '');
$sign = sanitize_text_field($_GET['sign'] ?? '');
$action = sanitize_key($_GET['action'] ?? 'login');
if (empty($agent_back_url) || empty($sign)) {
wp_die(__('代理请求参数不完整', 'aksrt'));
}
$verify_params = $_GET;
unset($verify_params['sign']);
if (!$this->verify_sign($verify_params, $secret, $sign)) {
wp_die(__('代理请求签名验证失败', 'aksrt'));
}
$ts = (int) ($_GET['ts'] ?? 0);
if (abs(time() - $ts) > 300) {
wp_die(__('代理请求已过期,请重试', 'aksrt'));
}
@session_start();
$_SESSION['aksrt_proxy_agent_back_url'] = $agent_back_url;
$_SESSION['aksrt_proxy_action'] = $action;
$_SESSION['aksrt_proxy_nonce'] = sanitize_text_field($_GET['nonce'] ?? '');
$_SESSION['aksrt_proxy_uid'] = (int) ($_GET['uid'] ?? 0);
try {
$oauth = AKSRT_OAuth::get_instance();
$auth_url = $oauth->get_auth_url($provider, $action);
if (empty($auth_url)) {
wp_die(__('无法生成授权链接', 'aksrt'));
}
wp_safe_redirect($auth_url);
exit;
} catch (\Exception $e) {
wp_die(__('代理登录错误:', 'aksrt') . esc_html($e->getMessage()));
}
}
private function handle_proxy_callback(string $provider): void {
@session_start();
$agent_back_url = $_SESSION['aksrt_proxy_agent_back_url'] ?? '';
if (empty($agent_back_url)) return;
$_SESSION['aksrt_proxy_agent_back_url'] = '';
$_SESSION['aksrt_proxy_action'] = '';
$_SESSION['aksrt_proxy_nonce'] = '';
$_SESSION['aksrt_proxy_uid'] = 0;
$secret = AKSRT_Settings::oauth_proxy_secret();
if (empty($secret)) {
wp_die(__('代理登录服务端配置错误', 'aksrt'));
}
$error = $_GET['error'] ?? '';
if ($error) {
wp_safe_redirect(add_query_arg(['error' => urlencode($error)], $agent_back_url));
exit;
}
$code = sanitize_text_field($_GET['code'] ?? '');
if (empty($code)) {
wp_safe_redirect(add_query_arg(['error' => urlencode('missing_code')], $agent_back_url));
exit;
}
try {
$user_data = $this->fetch_user_data($provider, $code);
if (empty($user_data['provider_id'])) {
wp_safe_redirect(add_query_arg(['error' => urlencode('no_user_info')], $agent_back_url));
exit;
}
$callback_params = [
'provider' => $provider,
'provider_id' => $user_data['provider_id'],
'username' => $user_data['username'] ?? '',
'name' => $user_data['name'] ?? '',
'email' => $user_data['email'] ?? '',
'email_verified' => !empty($user_data['email_verified']) ? '1' : '0',
'avatar_url' => $user_data['avatar_url'] ?? '',
'bio' => $user_data['bio'] ?? '',
'action' => sanitize_key($_SESSION['aksrt_proxy_action'] ?? 'login'),
'ts' => time(),
];
$callback_params['sign'] = $this->sign($callback_params, $secret);
$redirect_url = $agent_back_url . (strpos($agent_back_url, '?') !== false ? '&' : '?') . http_build_query($callback_params);
wp_safe_redirect($redirect_url);
exit;
} catch (\Exception $e) {
wp_safe_redirect(add_query_arg(['error' => urlencode($e->getMessage())], $agent_back_url));
exit;
}
}
/**
* 获取用户数据
*/
private function fetch_user_data(string $provider, string $code): array {
$config = get_option('aksrt_oauth', [])[$provider] ?? [];
if (empty($config['client_id']) || empty($config['client_secret'])) {
throw new \Exception(__('未配置此平台的 OAuth 凭据', 'aksrt'));
}
$redirect_uri = !empty($config['redirect_uri'])
? $config['redirect_uri']
: home_url('/oauth/proxy/' . $provider . '/callback');
switch ($provider) {
case 'github':
$oauth = new \Yurun\OAuthLogin\Github\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$userInfo = $oauth->getUserInfo();
$email = $userInfo['email'] ?? '';
if (empty($email)) {
$emails_json = $oauth->http->header('Authorization', ' token ' . $oauth->accessToken)
->get('https://api.github.com/user/emails')
->body();
$emails = json_decode($emails_json, true) ?: [];
foreach ($emails as $em) {
if (!empty($em['primary']) && !empty($em['email'])) {
$email = $em['email'];
break;
}
}
}
return [
'provider' => 'github',
'provider_id' => (string) ($userInfo['id'] ?? ''),
'username' => $userInfo['login'] ?? '',
'name' => $userInfo['name'] ?? '',
'email' => $email,
'avatar_url' => $userInfo['avatar_url'] ?? '',
'bio' => $userInfo['bio'] ?? '',
];
case 'gitee':
$oauth = new \Yurun\OAuthLogin\Gitee\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$userInfo = $oauth->getUserInfo();
return [
'provider' => 'gitee',
'provider_id' => (string) ($userInfo['id'] ?? ''),
'username' => $userInfo['login'] ?? '',
'name' => $userInfo['name'] ?? '',
'email' => $userInfo['email'] ?? '',
'avatar_url' => $userInfo['avatar_url'] ?? '',
'bio' => $userInfo['bio'] ?? '',
];
case 'qq':
$oauth = new \Yurun\OAuthLogin\QQ\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$openid = $oauth->getOpenID();
$userInfo = $oauth->getUserInfo();
return [
'provider' => 'qq',
'provider_id' => $openid,
'username' => $userInfo['nickname'] ?? '',
'name' => $userInfo['nickname'] ?? '',
'email' => '',
'avatar_url' => $userInfo['figureurl_qq_2'] ?? $userInfo['figureurl_2'] ?? '',
];
case 'google':
$response = wp_remote_post('https://oauth2.googleapis.com/token', [
'body' => [
'code' => $code,
'client_id' => $config['client_id'],
'client_secret' => $config['client_secret'],
'redirect_uri' => $redirect_uri,
'grant_type' => 'authorization_code',
],
]);
if (is_wp_error($response)) throw new \Exception($response->get_error_message());
$token_data = json_decode(wp_remote_retrieve_body($response), true);
if (empty($token_data['access_token'])) throw new \Exception(__('Google token 获取失败', 'aksrt'));
$user_response = wp_remote_get('https://www.googleapis.com/oauth2/v2/userinfo', [
'headers' => ['Authorization' => 'Bearer ' . $token_data['access_token']],
]);
if (is_wp_error($user_response)) throw new \Exception($user_response->get_error_message());
$user_info = json_decode(wp_remote_retrieve_body($user_response), true);
return [
'provider' => 'google',
'provider_id' => (string) ($user_info['id'] ?? ''),
'username' => $user_info['email'] ?? '',
'name' => $user_info['name'] ?? '',
'email' => $user_info['email'] ?? '',
'avatar_url' => $user_info['picture'] ?? '',
];
default:
throw new \Exception(__('不支持的 OAuth 平台', 'aksrt'));
}
}
private function sign(array $data, string $secret): string {
unset($data['sign']);
ksort($data);
return hash_hmac('sha256', http_build_query($data), $secret);
}
private function verify_sign(array $data, string $secret, string $expected_sign): bool {
return hash_equals($this->sign($data, $secret), $expected_sign);
}
}
return AKSRT_OAuth_Proxy_Server::get_instance();
+422
View File
@@ -0,0 +1,422 @@
<?php
/**
* AKSRT OAuth Third-party Login (v5 - 基于 yurunsoft/yurun-oauth-login)
*
* 使用 yurunsoft/yurun-oauth-login 统一处理各平台的授权流程:
* - GitHub: Yurun\OAuthLogin\Github\OAuth2
* - Gitee: Yurun\OAuthLogin\Gitee\OAuth2
* - QQ: Yurun\OAuthLogin\QQ\OAuth2
* - Google: 手动实现标准 OAuth2 流程
*
* 支持平台:GitHub、Gitee、QQ、Google
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_OAuth {
use AKSRT_OAuth_Part_1;
use AKSRT_OAuth_Part_2;
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_action('init', [$this, 'add_rewrite_rules']);
add_action('template_redirect', [$this, 'handle_callback']);
add_action('show_user_profile', [$this, 'profile_bindings'], 10, 1);
add_action('wp_ajax_aksrt_oauth_unbind', [$this, 'ajax_unbind']);
}
public function add_rewrite_rules(): void {
add_rewrite_rule('^oauth/callback/?$', 'index.php?aksrt_oauth=callback', 'top');
add_filter('query_vars', function($vars) {
$vars[] = 'aksrt_oauth';
return $vars;
});
}
/**
* 生成OAuth授权URL
*/
public function get_auth_url(string $provider, string $action = 'login'): string {
if (class_exists('AKSRT_Settings') && AKSRT_Settings::oauth_proxy_mode() === 'client') {
if (class_exists('AKSRT_OAuth_Proxy_Client')) {
return AKSRT_OAuth_Proxy_Client::get_instance()->get_proxy_auth_url($provider, $action);
}
return '';
}
$config = $this->get_provider_config($provider);
if (!$config) return '';
$uid = get_current_user_id();
$nonce = wp_create_nonce('aksrt_oauth_' . $provider . '_' . $uid);
$state = ($action === 'bind' && $uid)
? 'bind_' . $provider . '_' . $nonce
: $provider . '_' . $nonce;
$redirect_uri = !empty($config['redirect_uri']) ? $config['redirect_uri'] : home_url('/oauth/callback/');
try {
switch ($provider) {
case 'github':
$oauth = new \Yurun\OAuthLogin\Github\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
return $oauth->getAuthUrl($redirect_uri, $state);
case 'gitee':
$oauth = new \Yurun\OAuthLogin\Gitee\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
return $oauth->getAuthUrl($redirect_uri, $state);
case 'qq':
$oauth = new \Yurun\OAuthLogin\QQ\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
return $oauth->getAuthUrl($redirect_uri, $state);
case 'weibo':
$oauth = new \Yurun\OAuthLogin\Weibo\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
return $oauth->getAuthUrl($redirect_uri, $state);
case 'weixin':
$oauth = new \Yurun\OAuthLogin\Weixin\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
return $oauth->getAuthUrl($redirect_uri, $state);
case 'baidu':
$oauth = new \Yurun\OAuthLogin\Baidu\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
return $oauth->getAuthUrl($redirect_uri, $state);
case 'alipay':
$oauth = new \Yurun\OAuthLogin\Alipay\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
return $oauth->getAuthUrl($redirect_uri, $state);
case 'google':
return $this->get_google_auth_url($config, $redirect_uri, $state);
case 'weixingzh':
return $this->get_weixingzh_auth_url($config, $redirect_uri, $state);
default:
return '';
}
} catch (\Exception $e) {
return '';
}
}
/**
* 处理OAuth回调 — 路由分发
*/
public function handle_callback(): void {
$provider = $this->resolve_callback_provider();
if ($provider === '' || !in_array($provider, $this->get_enabled_providers())) return;
$error = $_GET['error'] ?? '';
if ($error) {
wp_die(__('登录失败:', 'aksrt') . esc_html($error));
}
$code = $_GET['code'] ?? '';
$state = $_GET['state'] ?? '';
if (empty($code)) {
wp_die(__('OAuth回调缺少授权码', 'aksrt'));
}
$is_bind = strpos($state, 'bind_') === 0;
$rest = $is_bind ? substr($state, 5) : $state;
$sep = strpos($rest, '_');
if ($sep === false || $sep >= strlen($rest) - 1) {
wp_die(__('无效的OAuth状态参数', 'aksrt'));
}
$parsed_provider = sanitize_key(substr($rest, 0, $sep));
if (!in_array($parsed_provider, $this->get_enabled_providers())) {
wp_die(__('未知的OAuth提供商', 'aksrt'));
}
$nonce_state = substr($rest, $sep + 1);
if ($is_bind) {
$this->handle_bind_callback($parsed_provider, $code, $nonce_state);
} else {
$this->handle_login_callback($parsed_provider, $code, $nonce_state);
}
}
private function handle_login_callback(string $provider, string $code, string $nonce): void {
try {
if (!wp_verify_nonce($nonce, "aksrt_oauth_{$provider}_0")) {
throw new \Exception(__('安全验证失败,请重试', 'aksrt'));
}
$user_data = $this->get_user_data($provider, $code);
if (!$user_data || empty($user_data['provider_id'])) {
throw new \Exception(__('无法获取用户信息', 'aksrt'));
}
$user = $this->find_or_create_user($provider, $user_data);
if (is_wp_error($user) && $user->get_error_code() === 'oauth_no_match') {
$error_data = $user->get_error_data();
$obk = 'aksrt_ob_' . wp_generate_password(24, false);
set_transient($obk, [
'provider' => $error_data['provider'],
'provider_id' => $error_data['user_data']['provider_id'] ?? '',
'user_data' => $error_data['user_data'],
], 15 * MINUTE_IN_SECONDS);
$bind_url = aksrt_get_bind_url();
wp_safe_redirect(add_query_arg('obk', $obk, $bind_url));
exit;
}
if (is_wp_error($user)) {
throw new \Exception($user->get_error_message());
}
wp_set_auth_cookie($user->ID, true);
$redirect_url = home_url('/');
if (function_exists('aksrt_get_user_center_url')) {
$redirect_url = aksrt_get_user_center_url();
}
wp_safe_redirect(add_query_arg(['oauth_action' => 'login', 'oauth_bound' => $provider], $redirect_url));
exit;
} catch (\Exception $e) {
wp_die(__('登录失败:', 'aksrt') . '<br>' . esc_html($e->getMessage()));
}
}
private function handle_bind_callback(string $provider, string $code, string $nonce): void {
$current_uid = get_current_user_id();
if (!$current_uid) {
wp_safe_redirect(aksrt_get_login_url());
exit;
}
try {
$expected = wp_create_nonce("aksrt_oauth_{$provider}_{$current_uid}");
if (!hash_equals($expected, $nonce)) {
throw new \Exception(__('安全验证失败,请重试', 'aksrt'));
}
$existing_binding = get_user_meta($current_uid, "aksrt_{$provider}_id", true);
if (!empty($existing_binding)) {
wp_safe_redirect(add_query_arg(['tab' => 'settings', 'oauth_action' => 'bind', 'oauth_bound' => $provider], $this->get_profile_url()));
exit;
}
$user_data = $this->get_user_data($provider, $code);
if (!$user_data || empty($user_data['provider_id'])) {
throw new \Exception(__('无法获取用户信息', 'aksrt'));
}
$existing = $this->find_user_by_provider($provider, $user_data['provider_id'] ?? '');
if ($existing && $existing->ID !== $current_uid) {
throw new \Exception(__('该第三方账号已被其他用户绑定', 'aksrt'));
}
$this->bind_provider_to_user($current_uid, $provider, $user_data);
wp_safe_redirect(add_query_arg(['tab' => 'settings', 'oauth_action' => 'bind', 'oauth_bound' => $provider], $this->get_profile_url()));
exit;
} catch (\Exception $e) {
wp_die(__('绑定失败:', 'aksrt') . '<br>' . esc_html($e->getMessage()));
}
}
/**
* 通过授权码获取用户数据(统一输出格式)
*/
private function get_user_data(string $provider, string $code): array {
$config = $this->get_provider_config($provider);
$redirect_uri = !empty($config['redirect_uri']) ? $config['redirect_uri'] : home_url('/oauth/callback/');
switch ($provider) {
case 'github':
return $this->get_github_user_data($config, $code, $redirect_uri);
case 'gitee':
return $this->get_gitee_user_data($config, $code, $redirect_uri);
case 'qq':
return $this->get_qq_user_data($config, $code, $redirect_uri);
case 'weibo':
return $this->get_weibo_user_data($config, $code, $redirect_uri);
case 'weixin':
return $this->get_weixin_user_data($config, $code, $redirect_uri);
case 'baidu':
return $this->get_baidu_user_data($config, $code, $redirect_uri);
case 'alipay':
return $this->get_alipay_user_data($config, $code, $redirect_uri);
case 'google':
return $this->get_google_user_data($config, $code, $redirect_uri);
case 'weixingzh':
return $this->get_weixingzh_user_data($config, $code, $redirect_uri);
default:
throw new \Exception(__('不支持的OAuth平台:', 'aksrt') . $provider);
}
}
private function get_github_user_data(array $config, string $code, string $redirect_uri): array {
$oauth = new \Yurun\OAuthLogin\Github\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$userInfo = $oauth->getUserInfo();
// 获取邮箱
$email = $userInfo['email'] ?? '';
if (empty($email)) {
$emails_json = $oauth->http->header('Authorization', ' token ' . $oauth->accessToken)
->get('https://api.github.com/user/emails')
->body();
$emails = json_decode($emails_json, true) ?: [];
foreach ($emails as $em) {
if (!empty($em['primary']) && !empty($em['email'])) {
$email = $em['email'];
break;
}
}
}
return [
'provider' => 'github',
'provider_id' => (string) ($userInfo['id'] ?? ''),
'username' => $userInfo['login'] ?? '',
'name' => $userInfo['name'] ?? '',
'email' => $email,
'avatar_url' => $userInfo['avatar_url'] ?? '',
'url' => $userInfo['html_url'] ?? '',
'bio' => $userInfo['bio'] ?? '',
];
}
private function get_gitee_user_data(array $config, string $code, string $redirect_uri): array {
$oauth = new \Yurun\OAuthLogin\Gitee\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$userInfo = $oauth->getUserInfo();
return [
'provider' => 'gitee',
'provider_id' => (string) ($userInfo['id'] ?? ''),
'username' => $userInfo['login'] ?? '',
'name' => $userInfo['name'] ?? '',
'email' => $userInfo['email'] ?? '',
'avatar_url' => $userInfo['avatar_url'] ?? '',
'url' => $userInfo['html_url'] ?? '',
'bio' => $userInfo['bio'] ?? '',
];
}
private function get_qq_user_data(array $config, string $code, string $redirect_uri): array {
$oauth = new \Yurun\OAuthLogin\QQ\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$openid = $oauth->getOpenID();
$userInfo = $oauth->getUserInfo();
return [
'provider' => 'qq',
'provider_id' => $openid,
'username' => $userInfo['nickname'] ?? '',
'name' => $userInfo['nickname'] ?? '',
'email' => '',
'avatar_url' => $userInfo['figureurl_qq_2'] ?? $userInfo['figureurl_2'] ?? $userInfo['figureurl'] ?? '',
'gender' => $userInfo['gender'] ?? '',
];
}
private function get_weibo_user_data(array $config, string $code, string $redirect_uri): array {
$oauth = new \Yurun\OAuthLogin\Weibo\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$userInfo = $oauth->getUserInfo();
return [
'provider' => 'weibo',
'provider_id' => (string) ($userInfo['id'] ?? ''),
'username' => $userInfo['screen_name'] ?? '',
'name' => $userInfo['name'] ?? '',
'email' => '',
'avatar_url' => $userInfo['avatar_large'] ?? $userInfo['profile_image_url'] ?? '',
'gender' => ($userInfo['gender'] ?? '') === 'm' ? 'male' : (($userInfo['gender'] ?? '') === 'f' ? 'female' : ''),
];
}
private function get_weixin_user_data(array $config, string $code, string $redirect_uri): array {
$oauth = new \Yurun\OAuthLogin\Weixin\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$userInfo = $oauth->getUserInfo();
return [
'provider' => 'weixin',
'provider_id' => (string) ($userInfo['openid'] ?? ''),
'username' => $userInfo['nickname'] ?? '',
'name' => $userInfo['nickname'] ?? '',
'email' => '',
'avatar_url' => $userInfo['headimgurl'] ?? '',
'gender' => ($userInfo['sex'] ?? 0) == 1 ? 'male' : (($userInfo['sex'] ?? 0) == 2 ? 'female' : ''),
];
}
}
return AKSRT_OAuth::get_instance();
@@ -0,0 +1,404 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Moderation_Part_1 {
public function render_comments_custom_column(string $column, int $comment_id): void {
if ($column !== 'aksrt_moderation') {
return;
}
$status = '';
if (function_exists('get_comment')) {
$comment_obj = get_comment($comment_id);
if (is_object($comment_obj) && isset($comment_obj->comment_approved)) {
$status = (string) $comment_obj->comment_approved;
}
}
$status_label = $this->status_label($status);
$source = (string) get_comment_meta($comment_id, 'aksrt_moderation_source', true);
$result_value = (string) get_comment_meta($comment_id, 'aksrt_moderation_result', true);
$reason = (string) get_comment_meta($comment_id, 'aksrt_moderation_reason', true);
$akismet = (string) get_comment_meta($comment_id, 'aksrt_moderation_akismet', true);
$ai_score = (string) get_comment_meta($comment_id, 'aksrt_moderation_ai_score', true);
$ai_flagged = (string) get_comment_meta($comment_id, 'aksrt_moderation_ai_flagged', true);
$ai_top = (string) get_comment_meta($comment_id, 'aksrt_moderation_ai_top_category', true);
$keyword = (string) get_comment_meta($comment_id, 'aksrt_moderation_keyword', true);
$match_type = (string) get_comment_meta($comment_id, 'aksrt_moderation_keyword_match_type', true);
$moderation_user_id = (string) get_comment_meta($comment_id, 'aksrt_moderation_user_id', true);
$moderation_user_ip = (string) get_comment_meta($comment_id, 'aksrt_moderation_user_ip', true);
if ($source === '') {
$source = 'wp_default';
}
if ($result_value === '') {
$result_value = $this->result_by_approved($status);
}
if ($reason === '') {
$reason = 'wp_default_policy';
}
if ($akismet === '') {
$akismet_meta = get_comment_meta($comment_id, 'akismet_result', true);
if ((string) $akismet_meta === 'true') {
$akismet = 'spam';
} elseif ((string) $akismet_meta === 'false') {
$akismet = 'ham';
} else {
$akismet = 'unknown';
}
}
if ($source === 'wp_default' && $akismet === 'ham') {
$source = 'akismet';
if ($result_value === 'wp_default_policy') {
$result_value = 'approved';
}
if ($reason === 'wp_default_policy') {
$reason = 'ham';
}
} elseif ($source === 'wp_default' && $akismet === 'spam') {
$source = 'akismet';
$result_value = 'spam';
if ($reason === 'wp_default_policy') {
$reason = 'spam';
}
}
echo '<strong>' . esc_html($status_label) . '</strong><br>';
echo 'Source: ' . esc_html($source) . ' | ';
echo 'Result: ' . esc_html($result_value) . ' | ';
echo 'Reason: ' . esc_html($reason) . ' | ';
echo 'Akismet: ' . esc_html($akismet);
$extras = [];
if ($ai_score !== '') {
$extras[] = 'AI score: ' . $ai_score;
}
if ($ai_flagged !== '') {
$extras[] = 'AI flagged: ' . $ai_flagged;
}
if ($ai_top !== '') {
$extras[] = 'AI top: ' . $ai_top;
}
if ($keyword !== '') {
$extras[] = 'Keyword: ' . $keyword;
}
if ($match_type !== '') {
$extras[] = 'Match: ' . $match_type;
}
if ($moderation_user_id !== '') {
$extras[] = 'UID: ' . $moderation_user_id;
}
if ($moderation_user_ip !== '') {
$extras[] = 'IP: ' . $moderation_user_ip;
}
if (!empty($extras)) {
echo '<br><small>' . esc_html(implode(' | ', $extras)) . '</small>';
}
}
private function new_default_result(): array {
return [
'result' => 'approved',
'source' => 'wp_default',
'reason' => 'wp_default_policy',
'akismet' => 'unknown',
'ai_score' => '',
'ai_flagged' => '',
'ai_top_category' => '',
'keyword' => '',
'keyword_match_type' => '',
'ip' => '',
];
}
private function remember_result(array $commentdata, array $decision): void {
$fp = $this->comment_fingerprint($commentdata);
if ($fp === '') {
return;
}
$this->moderation_result_map[$fp] = $decision;
if (array_key_exists('comment_approved', $commentdata)) {
$this->forced_approved_map[$fp] = $commentdata['comment_approved'];
}
}
private function resolve_result_from_runtime_or_default(array $commentdata, WP_Comment $comment): array {
$fp_data = $this->comment_fingerprint($commentdata);
if ($fp_data !== '' && isset($this->moderation_result_map[$fp_data])) {
return $this->moderation_result_map[$fp_data];
}
$fp_comment = $this->comment_fingerprint((array) $comment);
if ($fp_comment !== '' && isset($this->moderation_result_map[$fp_comment])) {
return $this->moderation_result_map[$fp_comment];
}
$fallback = $this->new_default_result();
$fallback['result'] = $this->result_by_approved((string) $comment->comment_approved);
$fallback['reason'] = 'wp_default_policy';
$akismet_meta = get_comment_meta($comment->comment_ID, 'akismet_result', true);
if ((string) $akismet_meta === 'true') {
$fallback['source'] = 'akismet';
$fallback['reason'] = 'spam';
$fallback['akismet'] = 'spam';
$fallback['result'] = 'spam';
} elseif ((string) $akismet_meta === 'false') {
$fallback['source'] = 'akismet';
$fallback['reason'] = 'ham';
$fallback['akismet'] = 'ham';
}
return $fallback;
}
private function enrich_moderation_result(array $result, array $commentdata): array {
if (!isset($result['result']) || $result['result'] === '') {
$approved = isset($commentdata['comment_approved']) ? (string) $commentdata['comment_approved'] : '1';
$result['result'] = $this->result_by_approved($approved);
}
if (!isset($result['source']) || $result['source'] === '') {
$result['source'] = 'wp_default';
}
if (!isset($result['reason']) || $result['reason'] === '') {
$result['reason'] = 'wp_default_policy';
}
if (!isset($result['akismet']) || $result['akismet'] === '') {
$result['akismet'] = 'unknown';
}
if (!isset($result['ai_score'])) {
$result['ai_score'] = '';
}
if (!isset($result['ai_flagged'])) {
$result['ai_flagged'] = '';
}
if (!isset($result['ai_top_category'])) {
$result['ai_top_category'] = '';
}
if (!isset($result['keyword'])) {
$result['keyword'] = '';
}
if (!isset($result['keyword_match_type'])) {
$result['keyword_match_type'] = '';
}
return $result;
}
private function result_by_approved(string $approved): string {
if ($approved === 'spam') {
return 'spam';
}
if ($approved === '0') {
return 'pending';
}
if ($approved === 'trash') {
return 'trash';
}
return 'approved';
}
private function status_label(string $approved): string {
if ($approved === '1') {
return 'Approved';
}
if ($approved === '0') {
return 'Pending';
}
if ($approved === 'spam') {
return 'Spam';
}
if ($approved === 'trash') {
return 'Trash';
}
return ucfirst($approved);
}
private function comment_fingerprint(array $data): string {
$post_id = (string) ($data['comment_post_ID'] ?? '');
$author = strtolower(trim((string) ($data['comment_author'] ?? '')));
$email = strtolower(trim((string) ($data['comment_author_email'] ?? '')));
$content_md5 = md5(trim((string) ($data['comment_content'] ?? '')));
$ip = trim((string) ($data['comment_author_IP'] ?? ($data['comment_author_ip'] ?? '')));
if ($post_id === '' && $author === '' && $email === '' && $content_md5 === md5('')) {
return '';
}
return md5(implode('|', [$post_id, $author, $email, $content_md5, $ip]));
}
// ============================================================
// 1. Akismet
// ============================================================
private function check_akismet(array $data, array $config, string $ip) {
if (empty($config['akismet_enabled'])) {
return true;
}
$api_key = trim((string) ($config['akismet_api_key'] ?? ''));
if ($api_key === '') {
return 'error';
}
$site_url = !empty($config['akismet_site_url']) ? (string) $config['akismet_site_url'] : home_url('/');
$payload = [
'blog' => $site_url,
'blog_lang' => $config['akismet_blog_lang'] ?? 'zh-CN',
'blog_charset' => get_bloginfo('charset') ?: 'UTF-8',
'user_ip' => $ip,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'referrer' => $_SERVER['HTTP_REFERER'] ?? '',
'comment_type' => 'comment',
'comment_author' => $data['comment_author'] ?? '',
'comment_author_email' => $data['comment_author_email'] ?? '',
'comment_author_url' => $data['comment_author_URL'] ?? ($data['comment_author_url'] ?? ''),
'comment_content' => $data['comment_content'] ?? '',
'is_test' => defined('WP_DEBUG') && WP_DEBUG ? '1' : '0',
];
$endpoint = sprintf('https://%s.rest.akismet.com/1.1/comment-check', $api_key);
$response = wp_remote_post($endpoint, [
'body' => http_build_query($payload),
'timeout' => 10,
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
]);
if (is_wp_error($response)) {
return 'error';
}
$body = trim((string) wp_remote_retrieve_body($response));
if ($body === 'true') {
return 'spam';
}
if ($body === 'false') {
return 'ham';
}
return 'error';
}
// ============================================================
// 2. AI Moderation
// ============================================================
private function check_ai_moderation(array $data, array $config) {
if (empty($config['ai_enabled']) || empty($config['ai_moderate_comments'])) {
return true;
}
$content = trim((string) ($data['comment_content'] ?? ''));
if ($content === '') {
return true;
}
// 委托给统一内容审核服务
if (!class_exists('AKSRT_Content_Audit')) {
return true;
}
$result = AKSRT_Content_Audit::get_instance()->audit_text($content);
if ($result['passed']) {
return true;
}
// 映射到审核风控的 score 模型
$score = 70;
if (in_array('non_compliant', $result['labels'] ?? [], true)) {
$score = 95;
}
return [
'flagged' => 1,
'score' => $score,
'top_category' => $result['reason'] ?? 'content_audit_flagged',
];
}
/**
* 图片审核 — 委托给统一内容审核服务。
*
* @param string $image_path Absolute path to the local image file.
* @param array $config Optional config override (merged with get_config()).
* @return array ['passed' => bool, 'reason' => string, ...]
*/
public function baidu_moderate_image(string $image_path, array $config = []): array {
$config = array_merge($this->get_config(), $config);
if (empty($config['ai_enabled']) || empty($config['ai_moderate_background'])) {
return ['passed' => true, 'reason' => 'disabled'];
}
if (!class_exists('AKSRT_Content_Audit')) {
return ['passed' => true, 'reason' => 'audit_service_missing'];
}
return AKSRT_Content_Audit::get_instance()->audit_image($image_path);
}
// ============================================================
// 3. Blocked keywords
// ============================================================
private function check_blocked_keywords(array $data, array $config): array {
$keywords_raw = (string) ($config['blocked_keywords'] ?? '');
if (trim($keywords_raw) === '') {
return ['hit' => false, 'keyword' => '', 'match_type' => ''];
}
$parts = preg_split('/[\r\n,;、\x{FF0C}\x{FF1B}]+/u', $keywords_raw);
$keywords = array_values(array_filter(array_map('trim', (array) $parts), static function ($v) {
return $v !== '';
}));
if (empty($keywords)) {
return ['hit' => false, 'keyword' => '', 'match_type' => ''];
}
$text = implode(' ', [
(string) ($data['comment_content'] ?? ''),
(string) ($data['comment_author'] ?? ''),
(string) ($data['comment_author_email'] ?? ''),
(string) ($data['comment_author_URL'] ?? ($data['comment_author_url'] ?? '')),
]);
$text_norm = $this->normalize_text($text);
foreach ($keywords as $keyword) {
if ($keyword === '') {
continue;
}
// User-defined regex keyword like /pattern/flags.
if (preg_match('#^/.+/[imsxuADSUXJ]*$#', $keyword)) {
$matched = @preg_match($keyword, $text);
if ($matched === 1) {
return ['hit' => true, 'keyword' => $keyword, 'match_type' => 'regex'];
}
continue;
}
if ($this->contains_text($text, $keyword)) {
return ['hit' => true, 'keyword' => $keyword, 'match_type' => 'plain'];
}
$keyword_norm = $this->normalize_text($keyword);
if ($keyword_norm !== '' && $this->contains_text($text_norm, $keyword_norm)) {
return ['hit' => true, 'keyword' => $keyword, 'match_type' => 'normalized'];
}
}
return ['hit' => false, 'keyword' => '', 'match_type' => ''];
}
private function contains_text(string $haystack, string $needle): bool {
if ($needle === '') {
return false;
}
if (function_exists('mb_stripos')) {
return mb_stripos($haystack, $needle, 0, 'UTF-8') !== false;
}
return stripos($haystack, $needle) !== false;
}
}
@@ -0,0 +1,163 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Moderation_Part_2 {
private function normalize_text(string $text): string {
$text = preg_replace('/[\x{200B}-\x{200D}\x{FEFF}]/u', '', $text);
$text = preg_replace('/\s+/u', '', (string) $text);
if (function_exists('mb_strtolower')) {
return mb_strtolower((string) $text, 'UTF-8');
}
return strtolower((string) $text);
}
// ============================================================
// 4. Rate limit
// ============================================================
private function check_rate_limit(array $data, array $config, string $ip) {
if (empty($config['rate_limit_enabled'])) {
return true;
}
$min_interval = max(1, (int) ($config['rate_limit_min_interval'] ?? 8));
$max_per_hour = max(1, (int) ($config['rate_limit_max_per_hour'] ?? 15));
$prefix = 'aksrt_rate_';
$last_key = $prefix . md5($ip . '_last');
$hour_key = $prefix . md5($ip . '_hourly');
$exp_key = '_' . $prefix . md5($ip . '_hourly_expiry');
$last_comment_time = get_transient($last_key);
if ($last_comment_time !== false) {
$elapsed = time() - (int) $last_comment_time;
if ($elapsed < $min_interval) {
$wait_time = $min_interval - $elapsed;
return sprintf(__('You are commenting too fast. Please wait %d seconds.', 'aksrt'), $wait_time);
}
}
$hourly_count = get_transient($hour_key);
if ($hourly_count !== false && (int) $hourly_count >= $max_per_hour) {
$reset_time = (int) (get_transient($exp_key) ?: time());
$remaining = max(0, $reset_time - time());
$minutes = (int) ceil($remaining / 60);
return sprintf(__('You have reached the hourly comment limit. Try again in %d minutes.', 'aksrt'), $minutes);
}
if ($hourly_count === false) {
set_transient($hour_key, 1, HOUR_IN_SECONDS);
set_transient($exp_key, time() + HOUR_IN_SECONDS, HOUR_IN_SECONDS);
} else {
set_transient($hour_key, ((int) $hourly_count) + 1, HOUR_IN_SECONDS);
}
set_transient($last_key, time(), $min_interval);
return true;
}
// ============================================================
// 5. GeoIP
// ============================================================
private function check_geoip(array $config, string $ip) {
if (empty($config['geoip_enabled'])) {
return true;
}
$block_countries = $config['geoip_block_countries'] ?? [];
if (!is_array($block_countries) || empty($block_countries)) {
return true;
}
$country_code = $this->lookup_geoip($ip, $config);
if ($country_code === null) {
return true;
}
$country_code = strtoupper($country_code);
$block_upper = array_map('strtoupper', $block_countries);
if (in_array($country_code, $block_upper, true)) {
return sprintf(__('Comments from your region (%s) are currently blocked.', 'aksrt'), esc_html($country_code));
}
return true;
}
private function lookup_geoip(string $ip, array $config): ?string {
// 优先复用 IP 归属地模块的查询能力(含缓存)
if (class_exists('AKSRT_IP_Geo')) {
$result = AKSRT_IP_Geo::get_instance()->lookup($ip);
if (!empty($result['country'])) {
return $result['country'];
}
}
// 回退到内置查询
$ip_geo_config = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_ip_geo_settings()
: [];
$provider = (string) ($ip_geo_config['ip_geo_provider'] ?? $config['geoip_provider'] ?? 'ip-api.com');
$api_key = (string) ($ip_geo_config['ip_geo_api_key'] ?? $config['geoip_api_key'] ?? '');
if ($provider === 'ipinfo' || $provider === 'ipinfo.io') {
return $this->geoip_ipinfo($ip, $api_key);
}
return $this->geoip_ipapi($ip);
}
/**
* Free ip-api.com endpoint.
*/
private function geoip_ipapi(string $ip): ?string {
$url = 'http://ip-api.com/json/' . urlencode($ip) . '?fields=status,countryCode,message';
$response = wp_remote_get($url, [
'timeout' => 5,
'headers' => ['Accept' => 'application/json'],
]);
if (is_wp_error($response)) {
return null;
}
$body = json_decode((string) wp_remote_retrieve_body($response), true);
if (($body['status'] ?? '') === 'success') {
return $body['countryCode'] ?? null;
}
return null;
}
private function geoip_ipinfo(string $ip, string $api_key): ?string {
$url = 'https://ipinfo.io/' . urlencode($ip) . '/json?token=' . urlencode($api_key ?: '');
$response = wp_remote_get($url, [
'timeout' => 5,
'headers' => ['Accept' => 'application/json'],
]);
if (is_wp_error($response)) {
return null;
}
$body = json_decode((string) wp_remote_retrieve_body($response), true);
return $body['country'] ?? null;
}
// ============================================================
// Utilities
// ============================================================
private function get_client_ip(): string {
$keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];
foreach ($keys as $key) {
if (!empty($_SERVER[$key])) {
$parts = explode(',', (string) $_SERVER[$key]);
$ip = trim($parts[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
}
@@ -0,0 +1,396 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_OAuth_Part_1 {
private function get_baidu_user_data(array $config, string $code, string $redirect_uri): array {
$oauth = new \Yurun\OAuthLogin\Baidu\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$userInfo = $oauth->getUserInfo();
return [
'provider' => 'baidu',
'provider_id' => (string) ($userInfo['userid'] ?? ''),
'username' => $userInfo['username'] ?? '',
'name' => $userInfo['realname'] ?? $userInfo['username'] ?? '',
'email' => $userInfo['email'] ?? '',
'avatar_url' => $userInfo['portrait'] ? ('https://himg.bdimg.com/sys/portrait/item/' . $userInfo['portrait'] . '.jpg') : '',
];
}
private function get_alipay_user_data(array $config, string $code, string $redirect_uri): array {
$oauth = new \Yurun\OAuthLogin\Alipay\OAuth2(
$config['client_id'],
$config['client_secret'],
$redirect_uri
);
$oauth->getAccessToken($_GET['state'] ?? '', $code, $_GET['state'] ?? null);
$userInfo = $oauth->getUserInfo();
return [
'provider' => 'alipay',
'provider_id' => (string) ($userInfo['user_id'] ?? ''),
'username' => $userInfo['user_name'] ?? '',
'name' => $userInfo['user_name'] ?? '',
'email' => $userInfo['email'] ?? '',
'avatar_url' => $userInfo['avatar'] ?? '',
'gender' => ($userInfo['gender'] ?? '') === 'm' ? 'male' : (($userInfo['gender'] ?? '') === 'f' ? 'female' : ''),
];
}
private function get_google_auth_url(array $config, string $redirect_uri, string $state): string {
$params = http_build_query([
'client_id' => $config['client_id'],
'redirect_uri' => $redirect_uri,
'response_type' => 'code',
'scope' => 'openid email profile',
'state' => $state,
'access_type' => 'offline',
]);
return 'https://accounts.google.com/o/oauth2/v2/auth?' . $params;
}
private function get_google_user_data(array $config, string $code, string $redirect_uri): array {
// 换取 token
$response = wp_remote_post('https://oauth2.googleapis.com/token', [
'body' => [
'code' => $code,
'client_id' => $config['client_id'],
'client_secret' => $config['client_secret'],
'redirect_uri' => $redirect_uri,
'grant_type' => 'authorization_code',
],
]);
if (is_wp_error($response)) {
throw new \Exception($response->get_error_message());
}
$token_data = json_decode(wp_remote_retrieve_body($response), true);
if (empty($token_data['access_token'])) {
throw new \Exception(__('Google token 获取失败', 'aksrt'));
}
// 获取用户信息
$user_response = wp_remote_get('https://www.googleapis.com/oauth2/v2/userinfo', [
'headers' => ['Authorization' => 'Bearer ' . $token_data['access_token']],
]);
if (is_wp_error($user_response)) {
throw new \Exception($user_response->get_error_message());
}
$user_info = json_decode(wp_remote_retrieve_body($user_response), true);
return [
'provider' => 'google',
'provider_id' => (string) ($user_info['id'] ?? ''),
'username' => $user_info['email'] ?? '',
'name' => $user_info['name'] ?? '',
'email' => $user_info['email'] ?? '',
'email_verified' => !empty($user_info['verified_email']),
'avatar_url' => $user_info['picture'] ?? '',
'locale' => $user_info['locale'] ?? '',
];
}
/**
* 微信公众号 OAuth2 授权 URL
*
* 使用 snsapi_userinfo scope 获取用户信息(需已认证服务号或订阅号+验证)。
*/
private function get_weixingzh_auth_url(array $config, string $redirect_uri, string $state): string {
$params = http_build_query([
'appid' => $config['client_id'],
'redirect_uri' => $redirect_uri,
'response_type' => 'code',
'scope' => 'snsapi_userinfo',
'state' => $state,
]);
return 'https://open.weixin.qq.com/connect/oauth2/authorize?' . $params . '#wechat_redirect';
}
/**
* 微信公众号 OAuth2 获取用户信息
*
* 1. code → access_token + openid
* 2. access_token + openid → userinfo
*/
private function get_weixingzh_user_data(array $config, string $code, string $redirect_uri): array {
// 1. 换取 access_token
$token_url = add_query_arg([
'appid' => $config['client_id'],
'secret' => $config['client_secret'],
'code' => $code,
'grant_type' => 'authorization_code',
], 'https://api.weixin.qq.com/sns/oauth2/access_token');
$response = wp_remote_get($token_url);
if (is_wp_error($response)) {
throw new \Exception($response->get_error_message());
}
$token_data = json_decode(wp_remote_retrieve_body($response), true);
if (empty($token_data['access_token']) || empty($token_data['openid'])) {
$errmsg = $token_data['errmsg'] ?? __('微信公众号 token 获取失败', 'aksrt');
throw new \Exception($errmsg);
}
// 2. 获取用户信息
$user_url = add_query_arg([
'access_token' => $token_data['access_token'],
'openid' => $token_data['openid'],
'lang' => 'zh_CN',
], 'https://api.weixin.qq.com/sns/userinfo');
$user_response = wp_remote_get($user_url);
if (is_wp_error($user_response)) {
throw new \Exception($user_response->get_error_message());
}
$user_info = json_decode(wp_remote_retrieve_body($user_response), true);
return [
'provider' => 'weixingzh',
'provider_id' => (string) ($user_info['openid'] ?? $token_data['openid']),
'username' => $user_info['nickname'] ?? '',
'name' => $user_info['nickname'] ?? '',
'email' => '',
'email_verified' => false,
'avatar_url' => $user_info['headimgurl'] ?? '',
'locale' => 'zh_CN',
'unionid' => $user_info['unionid'] ?? '',
];
}
private function resolve_callback_provider(): string {
$state = $_GET['state'] ?? '';
if ($state === '') return '';
$rest = (strpos($state, 'bind_') === 0) ? substr($state, 5) : $state;
$sep = strpos($rest, '_');
if ($sep !== false && $sep < strlen($rest) - 1) {
return sanitize_key(substr($rest, 0, $sep));
}
return '';
}
private function get_enabled_providers(): array {
if (class_exists('AKSRT_Settings') && AKSRT_Settings::oauth_proxy_mode() === 'client') {
return AKSRT_Settings::oauth_proxy_client_providers();
}
$providers = get_option('aksrt_oauth', []);
$enabled = [];
foreach (['github', 'gitee', 'qq', 'google', 'weibo', 'weixin', 'baidu', 'alipay', 'weixingzh'] as $key) {
if (!empty($providers[$key]['enabled']) && !empty($providers[$key]['client_id'])) {
$enabled[] = $key;
continue;
}
// BC: also check aksrt_settings flat keys
if (class_exists('AKSRT_Settings')) {
$s = AKSRT_Settings::get_all();
$switch_key = 'oauth_' . $key . '_s';
if (!empty($s[$switch_key])) {
$id_key = $key . '_appid';
$sec_key = $key . '_appsecret';
if ($key === 'weixingzh') {
$sec_key = 'weixingzh_appsecret';
} elseif ($key === 'github' || $key === 'gitee' || $key === 'qq') {
$sec_key = $key . '_appkey';
}
if (!empty($s[$id_key]) && !empty($s[$sec_key])) {
$enabled[] = $key;
}
}
}
}
return array_unique($enabled);
}
/**
* 获取 OAuth 提供商数据(供模板使用)
* 返回 [{name, label, icon, url}, ...]
*/
public function get_auth_providers_data(): array {
$platform_names = [
'github' => 'GitHub',
'gitee' => 'Gitee',
'qq' => 'QQ',
'google' => 'Google',
'weibo' => 'Weibo',
'weixin' => 'WeChat',
'baidu' => 'Baidu',
'alipay' => 'Alipay',
'weixingzh' => '微信公众号',
];
$result = [];
foreach ($this->get_enabled_providers() as $key) {
$url = $this->get_auth_url($key);
if (empty($url)) continue;
$result[] = [
'name' => $key,
'label' => $platform_names[$key] ?? ucfirst($key),
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon((string) $key) : '',
'url' => esc_url($url),
];
}
return $result;
}
private function get_provider_config(string $provider): ?array {
$providers = get_option('aksrt_oauth', []);
$config = $providers[$provider] ?? null;
if (!empty($config['client_id']) && !empty($config['client_secret'])) {
return $config;
}
// BC: fallback to aksrt_settings flat keys
if (class_exists('AKSRT_Settings')) {
$s = AKSRT_Settings::get_all();
$id_key = $provider . '_appid';
$sec_key = $provider . '_appsecret';
if ($provider === 'github' || $provider === 'gitee' || $provider === 'qq') {
$sec_key = $provider . '_appkey';
} elseif ($provider === 'google') {
$id_key = 'google_client_id';
$sec_key = 'google_client_secret';
}
$client_id = (string) ($s[$id_key] ?? '');
$client_secret = (string) ($s[$sec_key] ?? '');
if ($client_id !== '' && $client_secret !== '') {
return [
'client_id' => $client_id,
'client_secret' => $client_secret,
'redirect_uri' => home_url('/oauth/callback/'),
];
}
}
return null;
}
private function find_or_create_user($provider, $user_data) {
$email = $user_data['email'] ?? '';
if (!empty($user_data['provider_id'])) {
$by_provider = $this->find_user_by_provider($provider, (string) $user_data['provider_id']);
if ($by_provider instanceof WP_User) {
$bindings = get_user_meta($by_provider->ID, 'aksrt_oauth_bindings', true) ?: [];
if (!is_array($bindings)) $bindings = [];
if (!in_array($provider, $bindings, true)) {
$bindings[] = $provider;
update_user_meta($by_provider->ID, 'aksrt_oauth_bindings', $bindings);
}
if (!empty($user_data['avatar_url'])) {
update_user_meta($by_provider->ID, 'aksrt_oauth_avatar', esc_url_raw($user_data['avatar_url']));
}
return $by_provider;
}
}
if (!empty($email)) {
$existing = get_user_by('email', $email);
if ($existing && !$existing instanceof WP_Error) {
$bindings = get_user_meta($existing->ID, 'aksrt_oauth_bindings', true) ?: [];
if (!is_array($bindings)) $bindings = [];
if (!in_array($provider, $bindings, true)) {
$bindings[] = $provider;
update_user_meta($existing->ID, 'aksrt_oauth_bindings', $bindings);
}
if (!empty($user_data['provider_id'])) {
update_user_meta($existing->ID, "aksrt_{$provider}_id", (string) $user_data['provider_id']);
}
if (!empty($user_data['avatar_url'])) {
update_user_meta($existing->ID, 'aksrt_oauth_avatar', esc_url_raw($user_data['avatar_url']));
}
return $existing;
}
}
return new WP_Error(
'oauth_no_match',
__('需要绑定已有账号', 'aksrt'),
[
'provider' => $provider,
'user_data' => $user_data,
]
);
}
// ============================================
// 前端UI方法
// ============================================
public function login_button(): void {
echo $this->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
}
public function register_button(): void {
echo $this->render_social_buttons(__('使用以下方式快速注册', 'aksrt'));
}
public function render_social_buttons(string $title = ''): string {
$providers = get_option('aksrt_oauth', []);
$enabled_providers = [];
foreach ($providers as $key => $cfg) {
if (!empty($cfg['enabled']) && !empty($cfg['client_id']) && in_array($key, ['github', 'gitee', 'qq', 'google', 'weibo', 'weixin', 'baidu', 'alipay'], true)) {
$enabled_providers[$key] = $cfg;
}
}
if (empty($enabled_providers)) return '';
$icons = [
'github' => aksrt_brand_icon('github'),
'gitee' => aksrt_brand_icon('gitee'),
'qq' => aksrt_brand_icon('qq'),
'google' => aksrt_brand_icon('google'),
'weibo' => aksrt_brand_icon('weibo'),
'weixin' => aksrt_brand_icon('weixin'),
'baidu' => aksrt_brand_icon('baidu'),
'alipay' => aksrt_brand_icon('alipay'),
];
ob_start();
?>
<div class="social-login-divider">
<span><?php echo esc_html($title) ?: esc_html__('or', 'aksrt'); ?></span>
</div>
<div class="oauth-row">
<?php foreach ($enabled_providers as $key => $cfg):
$url = $this->get_auth_url($key);
if (empty($url)) continue;
?>
<a href="<?php echo esc_url($url); ?>"
class="oauth-btn"
aria-label="<?php echo esc_attr(ucfirst($key)); ?>"
title="<?php echo esc_attr(ucfirst($key)); ?>">
<?php echo $icons[$key] ?? ''; ?>
</a>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
public function get_login_buttons(): string {
return $this->render_social_buttons();
}
public function login_message(string $message): string {
return $message . self::get_instance()->render_social_buttons();
}
public function register_message(string $message): string {
return $message . self::get_instance()->render_social_buttons(__('或使用社交账号快速注册', 'aksrt'));
}
}
@@ -0,0 +1,166 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_OAuth_Part_2 {
public function get_frontend_bindings_data(int $user_id): array {
$providers = get_option('aksrt_oauth', []);
$bound_list = get_user_meta($user_id, 'aksrt_oauth_bindings', true) ?: [];
if (!is_array($bound_list)) $bound_list = [];
$platform_names = [
'github' => 'GitHub', 'gitee' => 'Gitee',
'qq' => 'QQ', 'google' => 'Google',
'weibo' => 'Weibo', 'weixin' => 'WeChat',
'baidu' => 'Baidu', 'alipay' => 'Alipay',
];
$icons = [
'github' => aksrt_brand_icon('github'),
'gitee' => aksrt_brand_icon('gitee'),
'qq' => aksrt_brand_icon('qq'),
'google' => aksrt_brand_icon('google'),
'weibo' => aksrt_brand_icon('weibo'),
'weixin' => aksrt_brand_icon('weixin'),
'baidu' => aksrt_brand_icon('baidu'),
'alipay' => aksrt_brand_icon('alipay'),
];
$bindings = [];
foreach ($bound_list as $binding) {
if (in_array($binding, ['github', 'gitee', 'qq', 'google', 'weibo', 'weixin', 'baidu', 'alipay'], true) && !empty($providers[$binding]['enabled'])) {
$bindings[] = [
'key' => $binding,
'name' => $platform_names[$binding] ?? ucfirst($binding),
'icon' => $icons[$binding] ?? '',
];
}
}
$available = [];
foreach (['github', 'gitee', 'qq', 'google', 'weibo', 'weixin', 'baidu', 'alipay'] as $key) {
if (in_array($key, $bound_list, true)) continue;
if (empty($providers[$key]['enabled']) || empty($providers[$key]['client_id'])) continue;
$url = $this->get_auth_url($key, 'bind');
if (empty($url)) continue;
$available[] = [
'key' => $key,
'name' => $platform_names[$key],
'icon' => $icons[$key] ?? '',
'url' => esc_url($url),
];
}
return [
'bindings' => $bindings,
'available' => $available,
];
}
public function profile_bindings(WP_User $user): void {
$bindings = get_user_meta($user->ID, 'aksrt_oauth_bindings', true) ?: [];
if (empty($bindings)) return;
$unbind_nonce = wp_create_nonce('aksrt_oauth_unbind');
?>
<h2><?php _e('社交账号绑定', 'aksrt'); ?></h2>
<table class="form-table">
<?php foreach ($bindings as $binding): ?>
<tr>
<th scope="row"><?php echo esc_html(ucfirst($binding)); ?></th>
<td><span class="badge" style="background:#d1fae5;color:#065f46;padding:4px 12px;border-radius:12px;font-size:12px;">&#10003; <?php esc_html_e('已绑定', 'aksrt'); ?></span></td>
<td>
<button type="button" class="button button-link-delete aksrt-oauth-unbind-btn"
data-provider="<?php echo esc_attr($binding); ?>"
data-user-id="<?php echo esc_attr((string) $user->ID); ?>">
<?php _e('解除绑定', 'aksrt'); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
</table>
<script>
(function(){
var nonce = <?php echo wp_json_encode($unbind_nonce); ?>;
var ajaxUrl = <?php echo wp_json_encode(admin_url('admin-ajax.php')); ?>;
document.querySelectorAll('.aksrt-oauth-unbind-btn').forEach(function(btn){
btn.addEventListener('click', function(){
var msg = <?php echo wp_json_encode(__('确定要解除绑定吗?', 'aksrt')); ?>;
if(!window.confirm(msg)) return;
var fd = new FormData();
fd.append('action', 'aksrt_oauth_unbind');
fd.append('nonce', nonce);
fd.append('provider', btn.getAttribute('data-provider'));
fd.append('userId', btn.getAttribute('data-user-id'));
fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: fd })
.then(function(r){ return r.json(); })
.then(function(d){ if(d && d.success){ location.reload(); } });
});
});
})();
</script>
<?php
}
public function ajax_unbind(): void {
check_ajax_referer('aksrt_oauth_unbind', 'nonce');
$provider = sanitize_key($_POST['provider'] ?? '');
$user_id = isset($_POST['userId']) ? (int) $_POST['userId'] : 0;
$current_id = get_current_user_id();
if (empty($provider) || !in_array($provider, ['github', 'gitee', 'qq', 'google', 'weibo', 'weixin', 'baidu', 'alipay'], true)) {
wp_send_json_error(400);
}
if ($current_id !== $user_id || !current_user_can('read')) {
wp_send_json_error(403);
}
$bindings = get_user_meta($current_id, 'aksrt_oauth_bindings', true) ?: [];
$bindings = array_values(array_diff($bindings, [$provider]));
update_user_meta($current_id, 'aksrt_oauth_bindings', $bindings);
delete_user_meta($current_id, "aksrt_{$provider}_id");
delete_user_meta($current_id, "aksrt_{$provider}_token");
wp_send_json_success(['unbound' => true]);
}
private function find_user_by_provider(string $provider, string $provider_id): ?WP_User {
if (empty($provider_id)) return null;
$users = get_users([
'meta_key' => "aksrt_{$provider}_id",
'meta_value' => $provider_id,
'number' => 1,
]);
return !empty($users[0]) ? $users[0] : null;
}
private function bind_provider_to_user(int $user_id, string $provider, array $user_data): void {
if (!empty($user_data['provider_id'])) {
update_user_meta($user_id, "aksrt_{$provider}_id", $user_data['provider_id']);
}
$bindings = get_user_meta($user_id, 'aksrt_oauth_bindings', true) ?: [];
if (!is_array($bindings)) $bindings = [];
if (!in_array($provider, $bindings, true)) {
$bindings[] = $provider;
update_user_meta($user_id, 'aksrt_oauth_bindings', $bindings);
}
if (!empty($user_data['avatar_url'])) {
update_user_meta($user_id, 'aksrt_oauth_avatar', esc_url_raw($user_data['avatar_url']));
}
}
private function get_profile_url(): string {
if (function_exists('aksrt_get_user_center_url')) {
return aksrt_get_user_center_url();
}
$user = wp_get_current_user();
if ($user->ID) {
return home_url('/user/' . $user->user_nicename . '/');
}
return home_url('/');
}
}