Files
wordpress-theme-aksrt/assets/js/captcha-front.js
T
kate522 5fd7c212ed fix(runtime): 按参考主题硬切换为 loader/main/auto_fun 单一 DOM 生命周期
- 删除 AksrtFrontend registry/rehydrate 双路架构及 build/public.js 拼包入口,
  前台唯一初始路径为 PHP 本地栈(jQuery 1.9.1 → Bootstrap 3.4.1)
  → loader_js(_win/aksrtData 唯一注入)→ tbquire(['main']) → runtime/main.min.js
- window.auto_fun = debounce(z_auto_fun, 100) 为唯一重扫入口;所有模块改为
  执行一次 + off/on('auto_fun.aksrt-*') 命名空间重扫,data/class 标记幂等
- 局部更新协议统一:HTML 注入 → post_ajax.ed → auto_fun();
  远程 modal → loaded.bs.modal → auto_fun();JSON 成功 → zib_ajax.success
- 灯箱恢复 Miniimgbox/imgbox 唯一 owner(imgbox-container、a[data-imgbox]、
  imgbox_data),删除 AksrtLightbox/data-aksrt-lightbox 第二实现及 CSS/overlay,
  _win.imgbox* 重新接上 post-display 设置
- 死代码清理:runtime 下 11 组被自有实现替代的旧模块副本、无消费 vendored 库、
  顶层 grade.min.js、revenue.js、无调用方的 aksrt_get_win_runtime_config(),
  以及 loader 中 16 条死 mapping
- PHP 入队改为本地栈 + loader_js,routed/widget/submit 依赖统一换绑;
  用户卡片发布按钮恢复 nav_user_newposts_btn 设置门控
- 测试对齐:新增 frontend-lifecycle-contract,重写 navigation/lazyload/toast/
  auth-source/comment/page-display/post-visual 等断言为目标架构契约

验证:node --test widget-system+navigation 223 通过 0 失败;
构建脚本重建 main/imgbox min 与 CSS bundles;改动 PHP php -l、JS node --check
全部通过;git diff --check 干净;credits 重入 harness 通过。
2026-09-03 12:44:31 +08:00

561 lines
21 KiB
JavaScript

/**
* 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 = '<div style="text-align:center;padding:20px;color:#9ca3af;">验证码加载中...</div>';
}
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 = '<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></div>';
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 });
}
})();