/** * 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(root, reason) { var scope = root && root.querySelectorAll ? root : document; var mounts = []; if (scope.nodeType === 1 && scope.matches('[data-aksrt-captcha-config]')) mounts.push(scope); Array.prototype.push.apply(mounts, scope.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 = '
请完成验证后继续操作
' + '