Files
wordpress-theme-aksrt/inc/classes/class-aksrt-captcha.php
T

1271 lines
51 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
/**
* Restored captcha adapter monolith.
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_get_captcha_config')):
/**
* 获取人机验证完整配置
*/
function aksrt_get_captcha_config() {
$default = array(
'enabled' => false,
'active_provider' => '',
'mode' => 'always',
'disabled_scenarios'=> array(),
);
return get_option('aksrt_captcha_config', $default);
}
endif;
if (!function_exists('aksrt_is_captcha_enabled_for')):
/**
* 检查指定场景是否启用人机验证
*
* @param string $scene comment / login / register / lostpassword
* @return bool
*/
function aksrt_is_captcha_enabled_for($scene) {
$cfg = aksrt_get_captcha_config();
if (empty($cfg['enabled'])) return false;
if (empty($cfg['active_provider'])) return false;
$disabled = isset($cfg['disabled_scenarios']) ? $cfg['disabled_scenarios'] : array();
return !in_array($scene, $disabled);
}
endif;
if (!function_exists('aksrt_is_native_auth_admin_whitelist')):
/**
* 判断当前请求是否属于原生认证后台白名单。
*
* 前台登录/注册/找回密码已经由主题认证路由接管,
* 这里仅保留 wp-login.php 场景作为后台认证白名单,
* 避免影响 WordPress 后台登录、重新认证等原生流程。
*
* @return bool
*/
function aksrt_is_native_auth_admin_whitelist() {
return isset($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'wp-login.php';
}
endif;
// ============================================================
// PHP 7.x 兼容辅助函数(替代 PHP 8.0+ 的 match 表达式)
// ============================================================
if (!function_exists('aksrt_captcha_field_names')):
/**
* 获取指定验证服务商的字段名列表(替代 match)
*
* @param string $provider 服务商标识
* @return array 字段名数组
*/
function aksrt_captcha_field_names($provider) {
$map = array(
'tencent' => array('secret_id', 'secret_key', 'app_id', 'app_secret_key'),
'aliyun' => array('region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'),
'cloudflare' => array('site_key', 'secret_key', 'domain', 'theme', 'size'),
'geetest' => array('captcha_id', 'captcha_key', 'product'),
);
return isset($map[$provider]) ? $map[$provider] : array();
}
endif;
if (!function_exists('aksrt_captcha_required_fields')):
/**
* 获取指定验证服务商的必填字段列表(替代 match)
*
* @param string $provider 服务商标识
* @return array 必填字段名数组
*/
function aksrt_captcha_required_fields($provider) {
$map = array(
'tencent' => array('secret_id', 'secret_key', 'app_id', 'app_secret_key'),
'aliyun' => array('region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'),
'cloudflare' => array('site_key', 'secret_key'),
'geetest' => array('captcha_id', 'captcha_key'),
);
return isset($map[$provider]) ? $map[$provider] : array();
}
endif;
// ============================================================
// 人机验证:获取客户端真实IP(穿透CDN/代理)
// ============================================================
if (!function_exists('aksrt_get_client_ip')) :
function aksrt_get_client_ip() {
// 按优先级检查代理头
$keys = array('HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP');
foreach ($keys as $key) {
if (!empty($_SERVER[$key])) {
$parts = explode(',', $_SERVER[$key]);
$ip = trim($parts[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
}
}
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
}
endif;
// ============================================================
// 腾讯云 API 3.0 TC3-HMAC-SHA256 签名函数
// ============================================================
if (!function_exists('aksrt_tencent_tc3_sign')) :
function aksrt_tencent_tc3_sign($secret_id, $secret_key, $service, $timestamp, $date, $action, $version, $payload) {
// 1. 拼接签名原文
$http_request_method = 'POST';
$canonical_uri = '/';
$canonical_querystring = '';
$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 = "{$http_request_method}\n{$canonical_uri}\n{$canonical_querystring}\n{$canonical_headers}\n{$signed_headers}\n{$payload_hash}";
$algorithm = 'TC3-HMAC-SHA256';
$credential_scope = "{$date}/{$service}/tc3_request";
$string_to_sign = "{$algorithm}\n{$timestamp}\n{$credential_scope}\n" . hash('sha256', $canonical_request);
// 2. 计算签名密钥链
$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);
// 3. 计算签名
$signature = hash_hmac('sha256', $string_to_sign, $secret_signing);
// 4. 组装 Authorization Header(包含 SecretId)
$authorization = "{$algorithm} "
. "Credential={$secret_id}/{$credential_scope}, "
. "SignedHeaders={$signed_headers}, "
. "Signature={$signature}";
return array(
'Authorization' => $authorization,
'Content-Type' => $content_type,
'Host' => $host,
'X-TC-Action' => $action,
'X-TC-Timestamp'=> (string) $timestamp,
'X-TC-Version' => $version,
'X-TC-Region' => '',
);
}
function aksrt_verify_captcha($post_data) {
$cfg = aksrt_get_captcha_config();
if (empty($cfg['enabled']) || empty($cfg['active_provider'])) {
return true;
}
$provider = $cfg['active_provider'];
$pcfg = isset($cfg[$provider]) && is_array($cfg[$provider]) ? $cfg[$provider] : array();
$client_ip = aksrt_get_client_ip();
// 快速路径:客户端验证字段检查
$captcha_verified = isset($post_data['captcha_verified']) ? $post_data['captcha_verified'] : '';
if ($captcha_verified !== '1') {
return __('人机验证失败', 'aksrt');
}
$result = false;
$error_msg = '';
switch ($provider) {
// --- 腾讯云验证码 2.0 (API 3.0 DescribeCaptchaResult + TC3-HMAC-SHA256 签名) ---
case 'tencent':
$ticket = isset($post_data['tencent_ticket']) ? $post_data['tencent_ticket'] : '';
$randstr = isset($post_data['tencent_randstr']) ? $post_data['tencent_randstr'] : '';
if (!$ticket || !$randstr) {
$error_msg = '缺少验证参数';
break;
}
// 检测容灾票据 (trerror_ 前缀表示 SDK 加载失败或异常)
if (strpos($ticket, 'trerror_') === 0) {
$error_msg = '验证码SDK异常,请刷新页面重试 (容灾票据)';
break;
}
// 检查是否配置了云 API 密钥(TC3 签名必须)
$cloud_secret_id = isset($pcfg['secret_id']) ? $pcfg['secret_id'] : '';
$cloud_secret_key = isset($pcfg['secret_key']) ? $pcfg['secret_key'] : '';
if (!$cloud_secret_id || !$cloud_secret_key) {
$error_msg = '验证码配置不完整:缺少云 API 密钥 (SecretId/SecretKey)';
break;
}
// API 3.0 DescribeCaptchaResult 请求参数
$tc_action = 'DescribeCaptchaResult';
$tc_version = '2019-07-22';
$tc_service = 'captcha';
$tc_host = 'captcha.tencentcloudapi.com';
$tc_timestamp = time();
$tc_date = gmdate('Y-m-d', $tc_timestamp);
$tc_payload = wp_json_encode(array(
'CaptchaType' => 9,
'CaptchaAppId' => (int) (isset($pcfg['app_id']) ? $pcfg['app_id'] : 0),
'AppSecretKey' => isset($pcfg['app_secret_key']) ? $pcfg['app_secret_key'] : '',
'Ticket' => $ticket,
'Randstr' => $randstr,
'UserIp' => $client_ip,
));
// 生成 TC3-HMAC-SHA256 签名(使用云 API 密钥)
$headers = aksrt_tencent_tc3_sign(
$cloud_secret_id,
$cloud_secret_key,
$tc_service,
$tc_timestamp,
$tc_date,
$tc_action,
$tc_version,
$tc_payload
);
// 使用 cURL 发起签名请求(避免 WordPress HTTP API 干扰 headers)
$ch = curl_init("https://{$tc_host}/");
$curl_headers = array();
foreach ($headers as $k => $v) {
$curl_headers[] = "{$k}: {$v}";
}
curl_setopt_array($ch, array(
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => $tc_payload,
CURLOPT_HTTPHEADER => $curl_headers,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_FOLLOWLOCATION => false,
));
$response_body = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_error) {
$error_msg = '验证码服务请求失败: ' . $curl_error;
break;
}
$body = json_decode(!empty($response_body) ? $response_body : '{}', true);
// 检查 API 错误
if (isset($body['Response']['Error'])) {
$api_err = $body['Response']['Error'];
$err_code = isset($api_err['Code']) ? $api_err['Code'] : '';
$err_msg = isset($api_err['Message']) ? $api_err['Message'] : '';
$error_msg = '腾讯 API 调用失败: ' . (!empty($err_msg) ? $err_msg : (!empty($err_code) ? $err_code : '未知错误'));
break;
}
$resp = isset($body['Response']) && is_array($body['Response']) ? $body['Response'] : array();
$captcha_code = (int) (isset($resp['CaptchaCode']) ? $resp['CaptchaCode'] : 0);
// CaptchaCode: 1=验证通过, 其他=失败
if ($captcha_code === 1) {
$result = true;
} else {
$last_code = $captcha_code;
$last_msg = isset($resp['CaptchaMsg']) ? $resp['CaptchaMsg'] : '';
$messages = array(
0 => '验证未通过',
7 => 'Randstr不合法',
8 => 'Ticket已过期(有效期5分钟)',
9 => 'Ticket被重复使用',
15 => 'Ticket不合法(解密失败)',
16 => 'CaptchaAppId与Ticket不匹配',
21 => '票据校验异常(风控拦截)',
100 => '参数校验失败(AppId/SecretKey/Ticket不匹配)',
);
$msg_text = !empty($last_msg) ? $last_msg : (isset($messages[$last_code]) ? $messages[$last_code] : '未知错误');
$error_msg = '腾讯验证失败: ' . $msg_text . " (CaptchaCode: {$last_code})";
}
break;
// --- Cloudflare Turnstile ---
case 'cloudflare':
$token = isset($post_data['cf_turnstile_response']) ? $post_data['cf_turnstile_response'] : '';
if (!$token) {
$error_msg = '缺少验证参数';
break;
}
// Token 格式检查 (Turnstile token 通常是长字符串)
if (strlen($token) < 10) {
$error_msg = '验证码 token 格式异常';
break;
}
// 尝试方式1: 不带 remoteip(Cloudflare 自动使用服务器 IP)
// 如果失败再尝试方式2: 带 remoteip
$cf_attempts = array(
'without-remoteip' => array(
'body' => array(
'secret' => isset($pcfg['secret_key']) ? $pcfg['secret_key'] : '',
'response' => $token,
)
),
'with-remoteip' => array(
'body' => array(
'secret' => isset($pcfg['secret_key']) ? $pcfg['secret_key'] : '',
'response' => $token,
'remoteip' => $client_ip,
)
),
);
foreach ($cf_attempts as $mode => $attempt) {
$resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', array(
'timeout' => 10,
'body' => $attempt['body'],
));
if (is_wp_error($resp)) {
continue;
}
$raw_body = wp_remote_retrieve_body($resp);
$body = json_decode($raw_body, true);
if (!empty($body['success'])) {
$result = true;
break;
}
}
if (!$result) {
$error_codes = isset($body['error-codes']) && is_array($body['error-codes']) ? $body['error-codes'] : array();
$error_codes_str = implode(', ', $error_codes);
$error_msg = 'Cloudflare验证失败: ' . (!empty($error_codes_str) ? $error_codes_str : '未知错误');
}
break;
// --- 极验 GeeTest v4 ---
case 'geetest':
$lot_number = isset($post_data['geetest_lot_number']) ? $post_data['geetest_lot_number'] : '';
$captcha_output = isset($post_data['geetest_captcha_output']) ? $post_data['geetest_captcha_output'] : '';
$pass_token = isset($post_data['geetest_pass_token']) ? $post_data['geetest_pass_token'] : '';
$gen_time = isset($post_data['geetest_gen_time']) ? $post_data['geetest_gen_time'] : '';
if (!$lot_number || !$captcha_output || !$pass_token) {
$error_msg = '缺少验证参数';
break;
}
// 极验 v4 要求使用 HMAC-SHA256 签名(非普通 SHA256)
$captcha_key_val = isset($pcfg['captcha_key']) ? $pcfg['captcha_key'] : '';
$sign_token = hash_hmac('sha256', $lot_number, $captcha_key_val);
$resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', array(
'timeout' => 10,
'body' => array(
'captcha_id' => isset($pcfg['captcha_id']) ? $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)) {
$error_msg = '验证请求失败: ' . $resp->get_error_message();
break;
}
$raw_body = wp_remote_retrieve_body($resp);
$body = json_decode($raw_body, true);
if (!empty($body['result']) && $body['result'] === 'success') {
$result = true;
} else {
$reason = isset($body['reason']) ? $body['reason'] : '';
$bmsg = isset($body['msg']) ? $body['msg'] : '';
$error_msg = '极验验证失败: ' . (!empty($reason) ? $reason : (!empty($bmsg) ? $bmsg : '未知错误'));
}
break;
// --- 阿里云 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha) ---
case 'aliyun':
$captcha_verify_param = isset($post_data['aliyun_captcha_verify_param']) ? $post_data['aliyun_captcha_verify_param'] : '';
if (!$captcha_verify_param) {
$error_msg = '缺少验证参数 (captchaVerifyParam)';
break;
}
// 确定地域对应的 API 端点
$region = isset($pcfg['region']) ? $pcfg['region'] : 'cn';
$endpoint = $region === 'sgp'
? 'captcha.ap-southeast-1.aliyuncs.com'
: 'captcha.cn-shanghai.aliyuncs.com';
$params = array(
'AccessKeyId' => isset($pcfg['access_key_id']) ? $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,
);
// 可选: 传入 SceneId 校验一致性(防止前端篡改)
if (!empty($pcfg['scene_id'])) {
$params['SceneId'] = $pcfg['scene_id'];
}
$ak_secret = isset($pcfg['access_key_secret']) ? $pcfg['access_key_secret'] : '';
$params['Signature'] = aksrt_aliyun_afs_sign($params, $ak_secret);
$resp = wp_remote_post('https://' . $endpoint . '/', array(
'timeout' => 10,
'body' => $params,
));
if (is_wp_error($resp)) {
$error_msg = '验证请求失败: ' . $resp->get_error_message();
break;
}
$body = json_decode(wp_remote_retrieve_body($resp), true);
if (!empty($body['Success'])) {
$verify_result = isset($body['Result']['VerifyResult']) ? $body['Result']['VerifyResult'] : false;
$verify_code = isset($body['Result']['VerifyCode']) ? $body['Result']['VerifyCode'] : '';
if ($verify_result === true) {
$result = true;
} else {
// 根据 VerifyCode 映射错误信息
$code_messages = array(
'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不匹配',
);
$vc_display = !empty($verify_code) ? $verify_code : 'N/A';
$code_msg = isset($code_messages[$verify_code]) ? $code_messages[$verify_code] : ('未知错误 (' . $vc_display . ')');
$error_msg = '阿里云V3验证失败: ' . $code_msg;
}
} else {
$amsg = isset($body['Message']) ? $body['Message'] : '';
$error_msg = '阿里云V3验证失败: ' . (!empty($amsg) ? $amsg : '未知错误');
}
break;
default:
return true;
}
if ($result) {
return true;
}
return __('人机验证失败', 'aksrt');
}
endif;
/**
* 阿里云 OpenAPI 签名函数 (POP Signature v1.0 / HMAC-SHA1)
* 同时适用于 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha) 和旧版 AFS
*/
if (!function_exists('aksrt_aliyun_afs_sign')) :
function aksrt_aliyun_afs_sign($params, $secret) {
ksort($params);
$canonicalized = '';
foreach ($params as $k => $v) {
$canonicalized .= '&' . rawurlencode($k) . '=' . rawurlencode($v);
}
$string_to_sign = 'POST&%2F&' . rawurlencode(substr($canonicalized, 1));
$signature = base64_encode(hash_hmac('sha1', $string_to_sign, $secret . '&', true));
return $signature;
}
function aksrt_render_captcha($scene = 'comment') {
$cfg = aksrt_get_captcha_config();
if (empty($cfg['enabled']) || empty($cfg['active_provider'])) return;
if (!aksrt_is_captcha_enabled_for($scene)) return;
$provider = $cfg['active_provider'];
$pcfg = isset($cfg[$provider]) && is_array($cfg[$provider]) ? $cfg[$provider] : array();
$uid = 'aksrt-captcha-' . $scene . '-' . wp_unique_id();
$vid = 'captcha_verified_' . $scene . '_' . wp_unique_id();
// 构建 JS 配置
$js_config = array(
'provider' => $provider,
'scene' => $scene,
'uid' => $uid,
'vid' => $vid,
'tc' => array('app_id' => isset($pcfg['app_id']) ? $pcfg['app_id'] : ''),
'cf' => array(
'site_key' => isset($pcfg['site_key']) ? $pcfg['site_key'] : '',
'theme' => isset($pcfg['theme']) ? $pcfg['theme'] : 'auto',
'size' => isset($pcfg['size']) ? $pcfg['size'] : 'normal'
),
'gt' => array(
'captcha_id' => isset($pcfg['captcha_id']) ? $pcfg['captcha_id'] : '',
'product' => isset($pcfg['product']) ? $pcfg['product'] : 'popup'
),
'al' => array(
'region' => isset($pcfg['region']) ? $pcfg['region'] : 'cn',
'prefix' => isset($pcfg['prefix']) ? $pcfg['prefix'] : '',
'scene_id' => isset($pcfg['scene_id']) ? $pcfg['scene_id'] : ''
),
);
?>
<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>
<script>(function(){
var C=<?php echo wp_json_encode($js_config, JSON_UNESCAPED_UNICODE); ?>;
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];
}
function init(){
var box=document.getElementById(C.uid);
var h =document.getElementById(C.vid);
if(!box||!h) return;
// ---- 查找表单(含重试) ----
var form=null, tries=0, maxTries=10;
function findForm(){
form=box.closest('form');
if(!form&&C.scene==='comment') form=document.getElementById('commentform');
if(!form&&C.scene==='login') form=document.getElementById('aksrt-login-form')||document.querySelector('.auth-form');
return form;
}
(function retryFind(){
findForm();
if(!form && ++tries < maxTries){ setTimeout(retryFind, 150); }
else {
if(form){ bindSubmit(); }
setTimeout(function(){ loadProvider(); }, 0);
}
})();
// ---- 提交拦截(capture 阶段确保最先触发) ----
var verified=false;
var pendingAction=null;
function bindSubmit(){
form.addEventListener('submit', function(e){
if(verified || h.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 submitForm(){
verified=true; h.value='1';
// 重置:如果页面未跳转(如评论被拒/审核),下次提交需重新验证
setTimeout(function(){
verified=false;
h.value='0';
// 清理已消耗的token字段,避免复用
['cf_turnstile_response','tencent_ticket','tencent_randstr','tencent_sid',
'geetest_lot_number','geetest_captcha_output','geetest_pass_token','geetest_gen_time',
'aliyun_afs_token','aliyun_session_id'].forEach(function(n){
var f=form.querySelector('input[name="'+n+'"]');
if(f) f.remove();
});
}, 3000);
if(form.requestSubmit){ form.requestSubmit(); }
else{ HTMLFormElement.prototype.submit.call(form); }
}
// ---- 提示信息 ----
function resetVerificationState(){
verified=false;
h.value='0';
var container = form || box;
['cf_turnstile_response','tencent_ticket','tencent_randstr','tencent_sid',
'geetest_lot_number','geetest_captcha_output','geetest_pass_token','geetest_gen_time',
'aliyun_afs_token','aliyun_session_id','aliyun_captcha_verify_param'].forEach(function(n){
var f=container.querySelector('input[name="'+n+'"]');
if(f) f.remove();
});
if(C.scene==='download'){
var myContainer=box.closest('[data-aksrt-download-captcha]');
if(myContainer){
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='0';
['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'].forEach(function(n){
var f=target.querySelector('input[name="'+n+'"]');
if(f) f.remove();
});
});
}
}
}
function submitNativeForm(){
if(!form) return;
if(form.requestSubmit){ form.requestSubmit(); }
else{ HTMLFormElement.prototype.submit.call(form); }
}
submitForm=function(){
verified=true;
h.value='1';
if(C.scene==='download'){
var myContainer=box.closest('[data-aksrt-download-captcha]');
if(myContainer){
var tokenFields=['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'];
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='1';
tokenFields.forEach(function(name){
var srcEl=myContainer.querySelector('input[name="'+name+'"]');
if(srcEl&&srcEl.value!==''){
var tgtEl=target.querySelector('input[name="'+name+'"]');
if(!tgtEl){tgtEl=document.createElement('input');tgtEl.type='hidden';tgtEl.name=name;target.appendChild(tgtEl);}
tgtEl.value=srcEl.value;
}
});
});
}
}
if(typeof pendingAction==='function'){
var action=pendingAction;
pendingAction=null;
action();
return;
}
setTimeout(function(){
resetVerificationState();
}, 3000);
submitNativeForm();
};
root.scenes[C.scene]={
verify:function(callback){
var retry=0;
function run(){
if(verified || h.value==='1'){
if(typeof callback==='function') callback();
return;
}
if(typeof triggerCaptcha==='function'){
pendingAction=(typeof callback==='function')?callback:null;
triggerCaptcha();
return;
}
if(retry++ < 20){
setTimeout(run, 150);
return;
}
showTip('验证码组件加载中,请稍候再试', '#e67e22');
}
run();
},
reset:resetVerificationState
};
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=''; }
// ---- 各供应商回调入口 ----
var triggerCaptcha; // 由 loadProvider 赋值
function loadProvider(){
switch(C.provider){
case 'tencent': loadTencent(); break;
case 'cloudflare': loadCloudflare(); break;
case 'geetest': loadGeetest(); break;
case 'aliyun': loadAliyun(); break;
}
}
// ============ 腾讯云 Captcha 2.0 ============
function loadTencent(){
showLoading();
var script=document.createElement('script');
// 腾讯云验证码 2.0 SDK 地址
script.src='https://turing.captcha.qcloud.com/TJCaptcha.js';
script.onload=function(){
clearLoading();
var capIns=null;
triggerCaptcha=function(){
if(!capIns){
capIns=new TencentCaptcha(C.tc.app_id, function(res){
// res.ret: 0=验证成功, 2=用户主动关闭
// res.errorCode: 错误码(存在时为容灾场景)
// res.ticket: 票据, res.randstr: 随机串, res.sid: 链路ID
if(res.ret===2){
// 用户主动关闭,不处理
return;
}
// 容灾票据检测 (trerror_ 前缀 = SDK异常)
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();
}
// errorCode 细分处理
if(res.errorCode){
var errorMap={
1001:'验证码组件加载失败',
1002:'验证超时',
1006:'验证码配置拉取失败',
1008:'验证码页面加载异常',
1012:'验证刷新次数过多',
1013:'验证网络异常'
};
showTip(errorMap[res.errorCode]||'验证码异常(errorCode:'+res.errorCode+')','#e74c3c');
}
}, {userLanguage:'zh-cn'});
}
capIns.show();
};
};
script.onerror=function(){
clearLoading();
showTip('验证码组件加载失败,请刷新重试','#e74c3c');
};
document.head.appendChild(script);
}
// ============ Cloudflare Turnstile (Modal-based) ============
function loadCloudflare(){
var cfWidgetId=null;
var cfVerifying=false;
var cfModal=null;
// Pre-load Turnstile API (defer rendering to when user clicks submit)
if(typeof turnstile==='undefined'){
window['onTurnstileLoad_'+C.uid]=function(){ /* api ready, widget rendered on demand */ };
var s=document.createElement('script');
s.src='https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileLoad_'+C.uid+'&render=explicit';
document.head.appendChild(s);
}
// Render Turnstile widget inside the modal
function renderWidget(){
// Clean up any previous widget before rendering new one
if(cfWidgetId){
try { turnstile.remove(cfWidgetId); } catch(e) {}
cfWidgetId=null;
}
var widgetId='modalWidget_'+C.uid;
var widgetDiv=document.getElementById(widgetId);
if(!widgetDiv) return;
if(typeof turnstile==='undefined'){ setTimeout(renderWidget, 200); return; }
cfWidgetId=turnstile.render('#'+widgetId, {
sitekey: C.cf.site_key,
theme: C.cf.theme,
size: C.cf.size,
action: 'comment-verify',
callback: function(token){
if(!cfVerifying) return;
cfVerifying=false;
setField('cf_turnstile_response', token);
closeModal();
submitForm();
},
'error-callback': function(){
showTip('验证失败,请重试','#e74c3c');
cfWidgetId=null;
setTimeout(function(){ renderWidget(); }, 500);
},
'expired-callback': function(){
showTip('验证已过期,请重新验证','#e67e22');
cfWidgetId=null;
setTimeout(function(){ renderWidget(); }, 500);
}
});
}
// Create modal overlay
function createModal(){
var widgetId='modalWidget_'+C.uid;
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="'+widgetId+'" class="aksrt-captcha-modal-widget"></div>'+
'</div>'+
'</div>';
document.body.appendChild(cfModal);
var backdrop=cfModal.querySelector('.aksrt-captcha-modal-backdrop');
var closeBtn=cfModal.querySelector('.aksrt-captcha-modal-close');
var cancelFn=function(){
cfVerifying=false;
closeModal();
showTip('验证已取消','#e67e22');
};
backdrop.addEventListener('click', cancelFn);
closeBtn.addEventListener('click', cancelFn);
renderWidget();
}
function closeModal(){
if(cfWidgetId){
try { turnstile.remove(cfWidgetId); } catch(e) {}
cfWidgetId=null;
}
if(cfModal){
if(cfModal.parentNode) cfModal.parentNode.removeChild(cfModal);
cfModal=null;
}
}
triggerCaptcha=function(){
if(cfVerifying){
showTip('验证进行中,请稍候...','#e67e22');
return;
}
cfVerifying=true;
createModal();
};
}
// ============ 极验 GeeTest v4 ============
function loadGeetest(){
showLoading();
var product=C.gt.product||'bind';
function render(){
clearLoading();
initGeetest4({captchaId:C.gt.captcha_id, product:product, hideSuccess:true}, function(capIns){
window['__gt_'+C.uid]=capIns;
function onSuccess(){
var r=capIns.getValidate();
if(r){
setField('geetest_lot_number', r.lot_number);
setField('geetest_captcha_output', r.captcha_output);
setField('geetest_pass_token', r.pass_token);
setField('geetest_gen_time', r.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 ci=window['__gt_'+C.uid];
if(ci){
if(product==='bind' || product==='popup') ci.showCaptcha();
else showTip('请先完成人机验证','#e67e22');
}else{ showTip('验证码加载中,请稍候...','#e67e22'); }
};
}
if(typeof initGeetest4==='undefined'){
var s=document.createElement('script');
s.src='https://static.geetest.com/v4/gt4.js';
s.onload=render;
s.onerror=function(){ clearLoading(); showTip('极验组件加载失败','#e74c3c'); };
document.head.appendChild(s);
}else{ render(); }
}
// ============ 阿里云 CAPTCHA 2.0 V3 (AliyunCaptcha) ============
function loadAliyun(){
showLoading();
// 设置全局配置(必须在引入 JS 之前)
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(){
showTip('请先完成人机验证', '#e67e22');
if (captchaIns && captchaIns.show) {
captchaIns.show();
}
};
}
if (typeof window.initAliyunCaptcha === 'undefined') {
var s = document.createElement('script');
s.src = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js';
s.onload = render;
s.onerror = function(){
clearLoading();
showTip('验证码组件加载失败,请刷新重试', '#e74c3c');
};
document.head.appendChild(s);
} else {
render();
}
}
loadTencent=function(){
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');
});
};
loadCloudflare=function(){
var cfWidgetId=null;
var cfVerifying=false;
var 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:'comment-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();
};
};
loadGeetest=function(){
showLoading();
var product=C.gt.product||'bind';
function render(){
clearLoading();
initGeetest4({captchaId:C.gt.captcha_id, 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();
};
loadAliyun=function(){
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();
};
}
if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', init); }
else{ setTimeout(init, 50); }
})();</script>
<?php echo '<link rel="stylesheet" href="' . esc_url(AKSRT_URI . '/assets/css/shared/captcha.css') . '">'; ?>
<?php
}
endif;