Files
wordpress-theme-aksrt/inc/classes/class-aksrt-captcha.php
T
kate522 7a4419d0dd fix(captcha): 1:1 align bind-phone and real status contracts
Add bind_phone captcha scene across schema/runtime/UI, require captcha on
phone bind, return real captcha status objects instead of legacy option
aliases, and harden form discovery for page-sign auth forms.
2026-07-13 21:14:31 +08:00

934 lines
31 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 {
return [
'enabled' => false,
'active_provider' => '',
'disabled_scenarios' => [],
'tencent' => [],
'aliyun' => [],
'cloudflare' => [],
'geetest' => [],
];
}
/**
* 获取人机验证完整配置(规范化,不合并历史脏键)。
*
* @return array
*/
function aksrt_get_captcha_config(): array {
$cfg = get_option('aksrt_captcha_config', []);
if (!is_array($cfg)) {
$cfg = [];
}
$default = aksrt_captcha_default_config();
$provider = isset($cfg['active_provider']) ? sanitize_key((string) $cfg['active_provider']) : '';
if (!in_array($provider, aksrt_captcha_providers(), true)) {
$provider = '';
}
$disabled = isset($cfg['disabled_scenarios']) ? (array) $cfg['disabled_scenarios'] : [];
$disabled = array_values(array_filter(array_map('sanitize_key', $disabled), function ($s) {
return array_key_exists($s, aksrt_captcha_scenes());
}));
$result = [
'enabled' => !empty($cfg['enabled']),
'active_provider' => $provider,
'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;
}
return !in_array($scene, $cfg['disabled_scenarios'], 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'){
var action=pendingAction; pendingAction=null; action(); 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=''; }
root.scenes[C.scene]={
verify:function(callback){
var retry=0;
function run(){
if(verified || hField.value==='1'){
if(typeof callback==='function') callback();
return;
}
pendingAction=(typeof callback==='function')?callback:null;
triggerCaptcha();
}
run();
},
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) 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(); showTip('验证码已取消', '#e67e22'); };
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
}