Files
wordpress-theme-aksrt/inc/modules/captcha.php
T
2026-09-01 18:43:21 +08:00

713 lines
24 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) -> [data-aksrt-captcha-config] 挂载点
* -> captcha-front.js(public bundle)-> 客户端 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'),
'download' => __('资源下载', 'aksrt'),
'signin' => __('每日签到', 'aksrt'),
'front_submit' => __('前台投稿', 'aksrt'),
];
}
/**
* 默认配置结构。
*/
function aksrt_captcha_default_config(): array {
$default_scenes = array_values(array_diff(array_keys(aksrt_captcha_scenes()), ['front_submit']));
return [
'enabled' => false,
'active_provider' => '',
// Positive list: checked scenes require captcha. Front submissions stay opt-in.
'enabled_scenarios' => $default_scenes,
'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);
}
/**
* 获取人机验证完整配置(规范化,不合并历史脏键)。
*
* 结果在同一请求内缓存;后台保存后调用 aksrt_captcha_config_reset() 失效。
*
* @return array
*/
function aksrt_get_captcha_config(): array {
$cache =& $GLOBALS['aksrt_captcha_config_cache'];
if (is_array($cache)) {
return $cache;
}
$stored = get_option('aksrt_captcha_config', []);
$cfg = array_merge(aksrt_captcha_default_config(), is_array($stored) ? $stored : []);
$provider = isset($cfg['active_provider']) ? sanitize_key((string) $cfg['active_provider']) : '';
if (!in_array($provider, aksrt_captcha_providers(), true)) {
$provider = '';
}
$enabled = aksrt_captcha_sanitize_scene_list($cfg['enabled_scenarios'] ?? []);
$result = [
'enabled' => !empty($cfg['enabled']),
'active_provider' => $provider,
'enabled_scenarios' => $enabled,
];
foreach (aksrt_captcha_providers() as $p) {
$result[$p] = isset($cfg[$p]) && is_array($cfg[$p]) ? $cfg[$p] : [];
}
$cache = $result;
return $result;
}
/**
* 使请求内的配置缓存失效(后台保存路径调用)。
*/
function aksrt_captcha_config_reset(): void {
$GLOBALS['aksrt_captcha_config_cache'] = null;
}
/**
* 各服务商必填字段。
*/
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['enabled_scenarios'], true);
}
/**
* 场景验证通过后的服务端放行时长(秒),0 表示不启用。
*
* 下载页多个卡片共享一次验证结果(前端会把 token 同步到兄弟挂载点),
* 而厂商 token 是一次性的,因此验证通过后由服务端记录短时放行,
* 后续下载请求直接命中放行,避免票据被判定重复使用。
*/
function aksrt_captcha_pass_ttl(string $scene): int {
return $scene === 'download' ? 10 * MINUTE_IN_SECONDS : 0;
}
/**
* 服务端放行记录的 transient key:场景 + 用户 + IP + UA。
*/
function aksrt_captcha_pass_key(string $scene): string {
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
return 'aksrt_captcha_pass_' . md5($scene . '|' . get_current_user_id() . '|' . aksrt_get_client_ip() . '|' . md5($ua));
}
/**
* 业务入口统一调用:未启用放行;启用则校验。
*
* @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;
}
$ttl = aksrt_captcha_pass_ttl($scene);
if ($ttl > 0 && get_transient(aksrt_captcha_pass_key($scene))) {
return true;
}
$result = aksrt_verify_captcha($payload, $scene);
if ($result === true && $ttl > 0) {
set_transient(aksrt_captcha_pass_key($scene), time(), $ttl);
}
return $result;
}
/**
* 后台摘要: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,
];
}
/**
* 凭据未配齐时的后台警告(前台保持放行,验证码不是准入门槛)。
*/
add_action('admin_notices', static function () {
if (!current_user_can('manage_options')) {
return;
}
$cfg = aksrt_get_captcha_config();
if (empty($cfg['enabled']) || $cfg['active_provider'] === '') {
return;
}
$provider = $cfg['active_provider'];
if (aksrt_captcha_provider_is_configured($provider, $cfg[$provider] ?? [])) {
return;
}
echo '<div class="notice notice-error"><p>' . esc_html(sprintf(
/* translators: %s: captcha provider key */
__('人机验证服务商「%s」已启用但凭据未配齐,前台人机验证未生效。请到「功能&权限 → 人机验证」补全配置。', 'aksrt'),
$provider
)) . '</p></div>';
});
// ============================================================
// 客户端真实 IP(穿透 CDN / 代理)
// ============================================================
function aksrt_get_client_ip(): string {
// Prefer CDN / reverse-proxy headers (Cloudflare first), then generic proxies.
$keys = [
'HTTP_CF_CONNECTING_IP',
'HTTP_TRUE_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'HTTP_CLIENT_IP',
];
foreach ($keys as $key) {
if (empty($_SERVER[$key])) {
continue;
}
// X-Forwarded-For may be a chain: client, proxy1, proxy2 — take first valid.
$parts = explode(',', (string) $_SERVER[$key]);
foreach ($parts as $part) {
$ip = trim($part);
// Strip IPv6 brackets / port noise occasionally injected by proxies.
if (str_starts_with($ip, '[') && str_contains($ip, ']')) {
$ip = substr($ip, 1, strpos($ip, ']') - 1);
} elseif (substr_count($ip, ':') === 1 && str_contains($ip, '.')) {
// IPv4:port
$ip = explode(':', $ip, 2)[0];
}
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
}
// Native wp-comments-post + any path that omits comment_author_IP.
// Ensures comment_author_IP (and thus aksrt_comment_addr) is the real client IP behind CDN.
add_filter('pre_comment_user_ip', static function ($ip) {
$resolved = aksrt_get_client_ip();
return ($resolved !== '' && $resolved !== '0.0.0.0') ? $resolved : $ip;
}, 5);
// ============================================================
// 腾讯云 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_signing, 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));
}
// ============================================================
// 服务端二次校验
// ============================================================
/**
* 记录传输层 / 厂商原始错误细节,供站点排查;对用户只暴露通用文案。
*/
function aksrt_captcha_log(string $provider, string $detail): void {
error_log('[AKSRT Captcha] ' . $provider . ': ' . $detail);
}
/**
* 校验客户端提交的人机验证字段。
*
* @param array $post_data 客户端字段(captcha_verified + 厂商 token)
* @param string $scene 触发场景(用于 Cloudflare cdata 绑定校验)
* @return true|string 成功返回 true,失败返回错误信息
*/
function aksrt_verify_captcha($post_data, string $scene = '') {
$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, $scene);
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);
$resp = wp_remote_post('https://captcha.tencentcloudapi.com/', [
'timeout' => 5,
'headers' => $headers,
'body' => $payload,
]);
if (is_wp_error($resp)) {
aksrt_captcha_log('tencent', $resp->get_error_message());
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
}
$body = json_decode(wp_remote_retrieve_body($resp), true);
if (!is_array($body)) {
aksrt_captcha_log('tencent', 'unparsable response');
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
}
if (isset($body['Response']['Error'])) {
$err = $body['Response']['Error'];
aksrt_captcha_log('tencent', ($err['Code'] ?? 'unknown') . ': ' . ($err['Message'] ?? ''));
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
}
$resp_data = $body['Response'] ?? [];
$captcha_code = (int) ($resp_data['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_data['CaptchaMsg'] ?? ($messages[$captcha_code] ?? '未知错误');
return '腾讯验证失败: ' . $msg . " (CaptchaCode: {$captcha_code})";
}
/**
* Cloudflare Turnstile siteverify。校验 action 与 cdata(场景)绑定,防 token 转用。
*
* @return true|string
*/
function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_ip, string $scene = '') {
$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' => 5,
'body' => $attempt,
]);
if (is_wp_error($resp)) {
continue;
}
$body = json_decode(wp_remote_retrieve_body($resp), true);
if (!empty($body['success'])) {
if (isset($body['action']) && (string) $body['action'] !== 'aksrt-verify') {
aksrt_captcha_log('cloudflare', 'action mismatch: ' . (string) $body['action']);
return __('人机验证未通过,请重试', 'aksrt');
}
if ($scene !== '' && isset($body['cdata']) && (string) $body['cdata'] !== $scene) {
aksrt_captcha_log('cloudflare', 'cdata mismatch: ' . (string) $body['cdata'] . ' != ' . $scene);
return __('人机验证未通过,请重试', 'aksrt');
}
return true;
}
}
if (is_array($body) && isset($body['error-codes'])) {
aksrt_captcha_log('cloudflare', 'siteverify failed: ' . implode(', ', (array) $body['error-codes']));
}
return __('人机验证未通过,请重试', 'aksrt');
}
/**
* 极验 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' => 5,
'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)) {
aksrt_captcha_log('geetest', $resp->get_error_message());
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
}
$body = json_decode(wp_remote_retrieve_body($resp), true);
if (is_array($body) && !empty($body['result']) && $body['result'] === 'success') {
return true;
}
aksrt_captcha_log('geetest', 'validate failed: ' . (string) ($body['reason'] ?? ($body['msg'] ?? 'unknown')));
return __('人机验证未通过,请重试', 'aksrt');
}
/**
* 阿里云 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' => wp_generate_uuid4(),
'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' => 5,
'body' => $params,
]);
if (is_wp_error($resp)) {
aksrt_captcha_log('aliyun', $resp->get_error_message());
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
}
$body = json_decode(wp_remote_retrieve_body($resp), true);
if (!is_array($body)) {
aksrt_captcha_log('aliyun', 'unparsable response');
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
}
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') . ')');
}
aksrt_captcha_log('aliyun', 'api failed: ' . (string) ($body['Message'] ?? 'unknown'));
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
}
// ============================================================
// 前端渲染(挂载点 + data 配置,运行时在 public bundle 的 captcha-front.js)
// ============================================================
/**
* 渲染人机验证挂载点。未启用场景直接返回。
*
* 挂载点自带 JSON 配置,由 captcha-front.js(public bundle)扫描激活。
*/
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'] ?? ''),
],
];
?>
<div class="aksrt-captcha-wrap" data-provider="<?php echo esc_attr($provider); ?>" data-scene="<?php echo esc_attr($scene); ?>" data-aksrt-captcha-config="<?php echo esc_attr(wp_json_encode($js_config, JSON_UNESCAPED_UNICODE)); ?>">
<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>
<?php
}