/** * AKSRT Captcha front-end runtime — cloud providers only. * * Mounts are registered internally by uid and resolved by scene plus DOM scope. * The only public surface is window.AKSRTCaptcha. */ (function () { 'use strict'; var TOKEN_FIELDS = Object.freeze([ 'captcha_verified', 'aksrt_captcha_challenge', 'aksrt_captcha_proof', 'cf_turnstile_response', 'tencent_ticket', 'tencent_randstr', 'geetest_lot_number', 'geetest_captcha_output', 'geetest_pass_token', 'geetest_gen_time', 'aliyun_captcha_verify_param' ]); var loaders = {}; var instances = {}; var scenes = {}; function asElement(scope) { if (!scope) return null; if (scope.nodeType === 1 || scope.nodeType === 9) return scope; if (typeof scope !== 'string') return null; if (instances[scope]) return instances[scope].box; try { return document.querySelector(scope); } catch (_) { return null; } } function scopeBoundary(box, config) { if (config.scope) { try { var configured = document.querySelector(config.scope); if (configured) return configured; } catch (_) {} } return box.closest('[data-aksrt-captcha-scope], form, [data-aksrt-download-box], [data-aksrt-auth], .aksrt-modal, [data-signin-captcha], #comments') || box.parentElement; } function register(instance) { instances[instance.uid] = instance; scenes[instance.scene] = scenes[instance.scene] || []; if (scenes[instance.scene].indexOf(instance) === -1) scenes[instance.scene].push(instance); } function resolve(scene, scope) { scan(); if (typeof scope === 'string' && instances[scope] && instances[scope].scene === scene) { return instances[scope]; } var candidates = scenes[scene] || []; var element = asElement(scope); if (element && element.getAttribute) { var uid = element.getAttribute('data-aksrt-captcha-uid') || element.getAttribute('data-captcha-uid'); if (uid && instances[uid] && instances[uid].scene === scene) return instances[uid]; } if (!element) return scope ? null : (candidates.length === 1 ? candidates[0] : null); var matches = candidates.filter(function (instance) { var boundary = instance.scope; return element === instance.box || element === instance.mount || element === boundary || instance.box.contains(element) || instance.mount.contains(element) || (boundary && boundary.contains(element)) || (element.contains && (element.contains(instance.box) || (boundary && element.contains(boundary)))); }); if (matches.length === 1) return matches[0]; if (!matches.length) return null; matches.sort(function (a, b) { if (a.scope === b.scope) return 0; if (a.scope && b.scope && a.scope.contains(b.scope)) return 1; if (a.scope && b.scope && b.scope.contains(a.scope)) return -1; return 0; }); return matches[0]; } function verify(scene, scope) { var instance = resolve(scene, scope); if (!instance) return Promise.resolve(true); return instance.verify(); } function collect(scene, scope) { var instance = resolve(scene, scope); return instance ? instance.collect() : {}; } function reset(scene, scope) { var instance = resolve(scene, scope); if (instance) instance.reset(); } function releaseRunLock(instance) { instance.runLocked = false; var waiters = instance.lockWaiters.slice(); instance.lockWaiters = []; waiters.forEach(function (resolveWaiter) { resolveWaiter(null); }); } function run(scene, scope) { var instance = resolve(scene, scope); if (!instance) return Promise.resolve({}); if (instance.runLocked) { return new Promise(function (resolveWaiter) { instance.lockWaiters.push(resolveWaiter); }); } instance.runLocked = true; return instance.verify().then(function (passed) { if (!passed) releaseRunLock(instance); return passed ? instance.collect() : null; }).catch(function (error) { releaseRunLock(instance); throw error; }); } window.AKSRTCaptcha = Object.freeze({ verify: verify, collect: collect, reset: reset, run: run, TOKEN_FIELDS: TOKEN_FIELDS }); function scan() { var mounts = document.querySelectorAll('[data-aksrt-captcha-config]'); Array.prototype.forEach.call(mounts, function (mount) { if (mount.dataset.aksrtCaptchaBound) return; var config; try { config = JSON.parse(mount.getAttribute('data-aksrt-captcha-config')); } catch (_) { return; } if (!config || !config.scene || !config.uid || !config.vid) return; mount.dataset.aksrtCaptchaBound = '1'; initMount(config, mount); }); } function initMount(C, mount) { var box = document.getElementById(C.uid); var hField = document.getElementById(C.vid); if (!box || !hField) return; var container = box.closest('form') || mount; mount.setAttribute('data-aksrt-captcha-uid', C.uid); var verified = false; var pending = []; var refreshingChallenge = null; var providerReset = function () {}; var triggerCaptcha = function () { showTip('验证码加载中,请稍候', '#e67e22'); finishPending(false); }; function loadScriptOnce(key, src, readyCheck) { if (typeof readyCheck === 'function' && readyCheck()) return Promise.resolve(); if (loaders[key]) return loaders[key]; loaders[key] = new Promise(function (resolveLoad, rejectLoad) { var existing = document.querySelector('script[data-aksrt-sdk="' + key + '"]'); if (existing) { if (typeof readyCheck === 'function' && readyCheck()) { resolveLoad(); return; } existing.addEventListener('load', resolveLoad, { once: true }); existing.addEventListener('error', function () { rejectLoad(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 = resolveLoad; script.onerror = function () { rejectLoad(new Error(key + ' load failed')); }; document.head.appendChild(script); }); return loaders[key]; } function setField(name, value) { var field = container.querySelector('input[name="' + name + '"]'); if (!field) { field = document.createElement('input'); field.type = 'hidden'; field.name = name; container.appendChild(field); } field.value = value == null ? '' : value; } function collectFields() { var payload = {}; TOKEN_FIELDS.forEach(function (name) { var field = container.querySelector('input[name="' + name + '"]'); if (field && field.value !== '') payload[name] = field.value; }); return payload; } function refreshChallenge() { if (!C.challenge_url || typeof window.fetch !== 'function') return Promise.resolve(); if (refreshingChallenge) return refreshingChallenge; refreshingChallenge = window.fetch(C.challenge_url, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scene: C.scene }) }).then(function (response) { return response.json().catch(function () { return {}; }).then(function (body) { if (!response.ok || !body || body.success === false) throw new Error(body.message || 'challenge refresh failed'); var data = body.data || body; if (!data.challenge) throw new Error('challenge missing'); setField('aksrt_captcha_challenge', data.challenge); }); }).catch(function () { showTip('无法刷新人机验证会话,请刷新页面重试', '#e74c3c'); }).finally(function () { refreshingChallenge = null; }); return refreshingChallenge; } function clearProviderTokens() { TOKEN_FIELDS.forEach(function (name) { if (name === 'captcha_verified' || name === 'aksrt_captcha_challenge' || name === 'aksrt_captcha_proof') return; Array.prototype.forEach.call(container.querySelectorAll('input[name="' + name + '"]'), function (field) { field.remove(); }); }); } function resetVerificationState() { verified = false; releaseRunLock(instance); hField.value = '0'; setField('aksrt_captcha_proof', ''); clearProviderTokens(); try { providerReset(); } catch (_) {} refreshChallenge(); } function finishPending(ok) { var callbacks = pending.slice(); pending = []; callbacks.forEach(function (callback) { callback(!!ok); }); } function cancelPending(message) { if (message) showTip(message, '#e67e22'); finishPending(false); } function providerPayload() { var payload = collectFields(); payload.scene = C.scene; return payload; } function exchangeVerification() { if (!C.exchange_url || typeof window.fetch !== 'function') { cancelPending('人机验证服务不可用,请刷新页面重试'); return; } window.fetch(C.exchange_url, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(providerPayload()) }).then(function (response) { return response.json().catch(function () { return {}; }).then(function (body) { if (!response.ok || !body || body.success === false) throw new Error(body.message || '人机验证失败'); var data = body.data || body; if (!data.proof) throw new Error('人机验证凭证缺失'); setField('aksrt_captcha_proof', data.proof); clearProviderTokens(); completeVerification(); }); }).catch(function (error) { cancelPending(error.message || '人机验证失败,请重试'); resetVerificationState(); }); } function completeProviderVerification() { hField.value = '1'; exchangeVerification(); } function completeVerification() { verified = true; hField.value = '1'; finishPending(true); } function showTip(message, color) { var tip = box.querySelector('.captcha-tip'); if (!tip) { tip = document.createElement('div'); tip.className = 'captcha-tip'; tip.style.cssText = 'font-size:12px;margin-top:4px;text-align:left;'; box.appendChild(tip); } tip.style.color = color || '#e74c3c'; tip.textContent = message; setTimeout(function () { if (tip.parentNode) tip.remove(); }, 4000); } function showLoading() { box.innerHTML = '
验证码加载中...
'; } function clearLoading() { box.innerHTML = ''; } var instance = { uid: C.uid, scene: C.scene, box: box, mount: mount, scope: scopeBoundary(box, C), runLocked: false, lockWaiters: [], collect: collectFields, reset: resetVerificationState, verify: function () { var challengeField = container.querySelector('input[name="aksrt_captcha_challenge"]'); var preparation = challengeField && challengeField.value ? Promise.resolve() : refreshChallenge(); return preparation.then(function () { challengeField = container.querySelector('input[name="aksrt_captcha_challenge"]'); if (!challengeField || !challengeField.value) { showTip('人机验证会话未就绪,请稍后重试', '#e74c3c'); return false; } if (verified || hField.value === '1') return true; return new Promise(function (resolveVerify) { pending.push(resolveVerify); if (pending.length === 1) triggerCaptcha(); }); }); } }; register(instance); refreshingChallenge = refreshChallenge(); function loadTencent() { showLoading(); loadScriptOnce('tencent', 'https://turing.captcha.qcloud.com/TJCaptcha.js', function () { return typeof window.TencentCaptcha !== 'undefined'; }).then(function () { clearLoading(); var captchaInstance = null; triggerCaptcha = function () { if (!captchaInstance) { captchaInstance = new window.TencentCaptcha(C.tc.app_id, function (result) { if (result.ret === 2) { cancelPending('已取消验证'); return; } if (result.ret === 0 && result.ticket) { if (result.ticket.indexOf('trerror_') === 0) { cancelPending('验证码服务异常,请刷新页面重试'); return; } setField('tencent_ticket', result.ticket); setField('tencent_randstr', result.randstr); completeProviderVerification(); return; } var errorMap = { 1001: '验证码组件加载失败', 1002: '验证码超时', 1006: '验证码配置拉取失败', 1008: '验证码页面加载异常', 1012: '验证码刷新次数过多', 1013: '验证码网络异常' }; cancelPending(errorMap[result.errorCode] || '验证码异常,请重试'); }, { userLanguage: 'zh-cn' }); } captchaInstance.show(); }; }).catch(function () { clearLoading(); cancelPending('验证码组件加载失败,请刷新重试'); }); } function loadCloudflare() { var widgetId = null; var verifying = false; var modal = null; function closeModal() { if (widgetId !== null && typeof window.turnstile !== 'undefined') { try { window.turnstile.remove(widgetId); } catch (_) {} } widgetId = null; if (modal && modal.parentNode) modal.parentNode.removeChild(modal); modal = null; } function renderWidget() { if (typeof window.turnstile === 'undefined') { setTimeout(renderWidget, 200); return; } widgetId = window.turnstile.render('#modalWidget_' + C.uid, { sitekey: C.cf.site_key, theme: C.cf.theme, size: C.cf.size, action: 'aksrt-verify', cdata: C.scene, callback: function (token) { if (!verifying) return; verifying = false; setField('cf_turnstile_response', token); closeModal(); completeProviderVerification(); }, 'error-callback': function () { verifying = false; closeModal(); cancelPending('验证码失败,请重试'); }, 'expired-callback': function () { verifying = false; closeModal(); cancelPending('验证码已过期,请重新验证'); } }); } function createModal() { modal = document.createElement('div'); modal.className = 'aksrt-captcha-modal-overlay'; modal.innerHTML = '
' + '
' + '人机验证' + '
' + '

请完成验证后继续操作

' + '
'; document.body.appendChild(modal); var cancel = function () { verifying = false; closeModal(); cancelPending('已取消验证'); }; modal.querySelector('.aksrt-captcha-modal-backdrop').addEventListener('click', cancel); modal.querySelector('.aksrt-captcha-modal-close').addEventListener('click', cancel); renderWidget(); } window['onTurnstileLoad_' + C.uid] = function () {}; if (typeof window.turnstile === 'undefined') { loadScriptOnce('cloudflare', 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileLoad_' + C.uid + '&render=explicit', function () { return typeof window.turnstile !== 'undefined'; }).catch(function () { cancelPending('验证码组件加载失败,请刷新重试'); }); } triggerCaptcha = function () { if (verifying) return; verifying = true; createModal(); }; providerReset = closeModal; } 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(); cancelPending('极验 Captcha ID 未配置'); return; } function render() { clearLoading(); window.initGeetest4({ captchaId: captchaId, product: product, hideSuccess: true }, function (captchaInstance) { function onSuccess() { var result = captchaInstance.getValidate(); if (!result) { cancelPending('验证码失败,请重试'); 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); completeProviderVerification(); } captchaInstance.onReady(function () {}).onSuccess(onSuccess).onClose(function () { cancelPending('已取消验证'); }); if (product !== 'bind' && product !== 'popup') captchaInstance.appendTo('#' + C.uid); triggerCaptcha = function () { if (product === 'bind' || product === 'popup') captchaInstance.showCaptcha(); else showTip('请先完成人机验证', '#e67e22'); }; providerReset = function () { if (typeof captchaInstance.reset === 'function') captchaInstance.reset(); }; }); } if (typeof window.initGeetest4 === 'undefined') { loadScriptOnce('geetest', 'https://static.geetest.com/v4/gt4.js', function () { return typeof window.initGeetest4 !== 'undefined'; }).then(render).catch(function () { clearLoading(); cancelPending('极验组件加载失败'); }); } else { render(); } } function loadAliyun() { showLoading(); window.AliyunCaptchaConfig = { region: C.al.region || 'cn', prefix: C.al.prefix }; function render() { clearLoading(); var captchaInstance = null; window.initAliyunCaptcha({ SceneId: C.al.scene_id, mode: 'popup', element: '#' + C.uid, success: function (verifyParam) { setField('aliyun_captcha_verify_param', verifyParam); completeProviderVerification(); }, fail: function () { cancelPending('验证码失败,请重试'); }, getInstance: function (value) { captchaInstance = value; }, slideStyle: { width: 360, height: 40 }, language: 'cn' }); triggerCaptcha = function () { if (captchaInstance && captchaInstance.show) captchaInstance.show(); else cancelPending('验证码组件加载中,请稍候再试'); }; providerReset = function () { if (captchaInstance && typeof captchaInstance.reset === 'function') captchaInstance.reset(); }; } 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(); cancelPending('验证码组件加载失败,请刷新重试'); }); } else { render(); } } switch (C.provider) { case 'tencent': loadTencent(); break; case 'cloudflare': loadCloudflare(); break; case 'geetest': loadGeetest(); break; case 'aliyun': loadAliyun(); break; } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan); else setTimeout(scan, 0); // AJAX 注入新内容后由 auto_fun 重扫(dataset.aksrtCaptchaBound 标记保证幂等)。 if (window.jQuery) window.jQuery(document).on('auto_fun', scan); if (window.MutationObserver) { new MutationObserver(function (mutations) { var shouldScan = mutations.some(function (mutation) { return mutation.addedNodes && mutation.addedNodes.length; }); if (shouldScan) scan(); }).observe(document.documentElement, { childList: true, subtree: true }); } })();