diff --git a/.gitignore b/.gitignore
index 1e827085..63bbc3c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,9 @@
!/tests/widget-system/page-display-save-harness.php
!/tests/widget-system/admin-ui-contract.test.cjs
!/tests/widget-system/auth-source-contract.test.cjs
+!/tests/widget-system/captcha-business-contract.test.cjs
+!/tests/widget-system/captcha-security-harness.php
+!/tests/widget-system/captcha-runtime-harness.test.cjs
!/tests/widget-system/oauth-security-contract.test.cjs
!/tests/widget-system/oauth-security-harness.php
!/tests/widget-system/user-badges-contract.test.cjs
diff --git a/assets/admin/src/api/modules/schema.js b/assets/admin/src/api/modules/schema.js
index 98789349..6a4004c2 100644
--- a/assets/admin/src/api/modules/schema.js
+++ b/assets/admin/src/api/modules/schema.js
@@ -3,6 +3,12 @@ export const schemaApiMethods = {
getSchemaPage(domain, section) {
return this.request(`/settings/schema/${this.normalizeDomainKey(domain)}/${encodeURIComponent(section)}`)
},
+ testCaptcha(provider, token) {
+ return this.request('/captcha/test', {
+ method: 'POST',
+ body: JSON.stringify({ provider, token }),
+ })
+ },
saveSchemaPage(domain, section, data, version = '1.0.0') {
return this.request(`/settings/schema/${this.normalizeDomainKey(domain)}/${encodeURIComponent(section)}`, {
method: 'POST',
diff --git a/assets/admin/src/components/captcha/AliyunConfig.vue b/assets/admin/src/components/captcha/AliyunConfig.vue
index f216b1ac..4c1f5d1c 100644
--- a/assets/admin/src/components/captcha/AliyunConfig.vue
+++ b/assets/admin/src/components/captcha/AliyunConfig.vue
@@ -40,9 +40,11 @@
v-model:value="model.access_key_secret"
type="password"
show-password-on="click"
- autocomplete="current-password"
+ autocomplete="new-password"
placeholder="阿里云 RAM 用户 AccessKey Secret"
+ @update:value="onSecretInput('access_key_secret', $event)"
/>
+ 清除
请使用 RAM 子账号密钥并授权 AliyunYundunAFSFullAccess 权限,切勿使用主账号密钥。
@@ -58,6 +60,16 @@
import { computed } from 'vue'
const model = defineModel({ type: Object, default: () => ({}) })
+const emit = defineEmits(['clear-secret', 'restore-secret'])
+
+function clearSecret(field) {
+ emit('clear-secret', field)
+ model.value = { ...model.value, [field]: '' }
+}
+
+function onSecretInput(field, value) {
+ if (value !== '') emit('restore-secret', field)
+}
const isConfigured = computed(() => {
const c = model.value
diff --git a/assets/admin/src/components/captcha/CloudflareConfig.vue b/assets/admin/src/components/captcha/CloudflareConfig.vue
index 38b3bf9f..fe78c690 100644
--- a/assets/admin/src/components/captcha/CloudflareConfig.vue
+++ b/assets/admin/src/components/captcha/CloudflareConfig.vue
@@ -5,10 +5,8 @@
-
-
-
-
+
+ 清除
@@ -26,6 +24,16 @@
import { computed } from 'vue'
const model = defineModel({ type: Object, default: () => ({}) })
+const emit = defineEmits(['clear-secret', 'restore-secret'])
+
+function clearSecret(field) {
+ emit('clear-secret', field)
+ model.value = { ...model.value, [field]: '' }
+}
+
+function onSecretInput(field, value) {
+ if (value !== '') emit('restore-secret', field)
+}
const isConfigured = computed(() => {
const c = model.value
diff --git a/assets/admin/src/components/captcha/GeetestConfig.vue b/assets/admin/src/components/captcha/GeetestConfig.vue
index caf96c8f..e367eb15 100644
--- a/assets/admin/src/components/captcha/GeetestConfig.vue
+++ b/assets/admin/src/components/captcha/GeetestConfig.vue
@@ -5,7 +5,8 @@
-
+
+ 清除
✓ 已完整配置
未完成配置
@@ -17,6 +18,16 @@
import { computed } from 'vue'
const model = defineModel({ type: Object, default: () => ({ product: 'bind' }) })
+const emit = defineEmits(['clear-secret', 'restore-secret'])
+
+function clearSecret(field) {
+ emit('clear-secret', field)
+ model.value = { ...model.value, [field]: '' }
+}
+
+function onSecretInput(field, value) {
+ if (value !== '') emit('restore-secret', field)
+}
const isConfigured = computed(() => {
const c = model.value
diff --git a/assets/admin/src/components/captcha/TencentConfig.vue b/assets/admin/src/components/captcha/TencentConfig.vue
index 9effbfb5..9ccf6041 100644
--- a/assets/admin/src/components/captcha/TencentConfig.vue
+++ b/assets/admin/src/components/captcha/TencentConfig.vue
@@ -9,7 +9,8 @@
-
+
+ 清除
验证码密钥
@@ -17,7 +18,8 @@
-
+
+ 清除
请从「验证管理 → 基础配置」完整复制,注意区分 I/l/1/O/0
@@ -34,6 +36,16 @@
import { computed } from 'vue'
const model = defineModel({ type: Object, default: () => ({}) })
+const emit = defineEmits(['clear-secret', 'restore-secret'])
+
+function clearSecret(field) {
+ emit('clear-secret', field)
+ model.value = { ...model.value, [field]: '' }
+}
+
+function onSecretInput(field, value) {
+ if (value !== '') emit('restore-secret', field)
+}
const isConfigured = computed(() => {
const c = model.value
diff --git a/assets/admin/src/pages/SchemaPage.vue b/assets/admin/src/pages/SchemaPage.vue
index 56bd0886..9f650219 100644
--- a/assets/admin/src/pages/SchemaPage.vue
+++ b/assets/admin/src/pages/SchemaPage.vue
@@ -19,28 +19,28 @@
v-if="isPermissionsLogin"
#field-tencent="{ value, setValue }"
>
-
+
-
+
-
+
-
+
normalizeSchemaDomain(props.domain || route.meta?.canonicalDomain || route.meta?.schemaDomain || resolveDomainFromPath(route.path)))
@@ -210,8 +211,12 @@ const {
}
})
if (clearSecrets.length) payload.__clear_secrets = clearSecrets
+ if (isPermissionsLogin.value && captchaSecretsToClear.value.size) {
+ payload.__clear_secrets = [...(payload.__clear_secrets || []), ...captchaSecretsToClear.value]
+ }
const response = await api.saveSchemaPage(resolvedDomain.value, resolvedSection.value, payload)
clearSecrets.forEach((key) => { data[key] = '' })
+ if (isPermissionsLogin.value) captchaSecretsToClear.value = new Set()
return response
},
defaults: defaults.value,
@@ -230,10 +235,26 @@ async function handleSave() {
function handleReload() {
closeTestModal()
+ captchaSecretsToClear.value = new Set()
reload()
message.info('已放弃更改')
}
+function markCaptchaSecretForClear(provider, field) {
+ const path = `${provider}.${field}`
+ const next = new Set(captchaSecretsToClear.value)
+ next.add(path)
+ captchaSecretsToClear.value = next
+}
+
+function unmarkCaptchaSecretForClear(provider, field) {
+ const path = `${provider}.${field}`
+ if (!captchaSecretsToClear.value.has(path)) return
+ const next = new Set(captchaSecretsToClear.value)
+ next.delete(path)
+ captchaSecretsToClear.value = next
+}
+
function getCaptchaProviderConfig(provider) {
if (!formData.value || !provider) return {}
const cfg = formData.value[provider]
@@ -256,9 +277,8 @@ function handleCaptchaTest() {
message.warning(`请先填写必填字段: ${missing.join(', ')}`)
return
}
-
- if (provider === 'tencent') {
- loadTencentTest(cfg)
+ if (dirty.value) {
+ message.warning('请先保存当前配置,再执行真实服务端验签。')
return
}
@@ -267,52 +287,20 @@ function handleCaptchaTest() {
testLoading.value = true
nextTick(() => {
- createTestIframe(provider, cfg)
+ loadProviderSDK(provider, cfg)
})
}
-function loadTencentTest(cfg) {
- const sdkSrc = 'https://turing.captcha.qcloud.com/TJCaptcha.js'
-
- if (typeof window.TencentCaptcha !== 'undefined') {
- try {
- new window.TencentCaptcha(cfg.app_id, function(res) {
- if (res.ret === 0) message.success('腾讯云验证码测试成功!配置正确。')
- else message.warning('验证已取消。')
- }, { userLanguage: 'zh-cn' }).show()
- } catch (error) {
- message.error('初始化失败: ' + error.message)
- }
- return
- }
-
- const existingScript = document.querySelector('script[src="' + sdkSrc + '"]')
- if (existingScript) {
- existingScript.addEventListener('load', function() { loadTencentTest(cfg) }, { once: true })
- return
- }
-
- const script = document.createElement('script')
- script.src = sdkSrc
- script.onload = function () {
- try {
- new window.TencentCaptcha(cfg.app_id, function(res) {
- if (res.ret === 0) message.success('腾讯云验证码测试成功!')
- else message.warning('验证已取消。')
- }, { userLanguage: 'zh-cn' }).show()
- } catch (error) {
- message.error('初始化失败: ' + error.message)
- }
- }
- script.onerror = function () { message.error('加载腾讯验证码SDK失败') }
- document.head.appendChild(script)
-}
-
-function createTestIframe(provider, cfg) {
- setTimeout(() => {
+async function submitCaptchaTest(provider, token) {
+ testLoading.value = true
+ try {
+ const response = await api.testCaptcha(provider, token)
+ testResult.value = { success: true, message: response?.message || '服务端同源验签通过' }
+ } catch (error) {
+ testResult.value = { success: false, message: error?.message || '服务端验签失败' }
+ } finally {
testLoading.value = false
- loadProviderSDK(provider, cfg)
- }, 100)
+ }
}
function loadProviderSDK(provider, cfg) {
@@ -331,28 +319,37 @@ function loadProviderSDK(provider, cfg) {
case 'tencent':
script.src = 'https://turing.captcha.qcloud.com/TJCaptcha.js'
script.onload = function () {
+ testLoading.value = false
try {
new window.TencentCaptcha(cfg.app_id, function(res) {
- testResult.value = { success: res.ret === 0, message: res.ret === 0 ? '验证成功!' : '验证已取消。' }
+ if (res.ret === 0) {
+ submitCaptchaTest('tencent', {
+ tencent_ticket: res.ticket || '',
+ tencent_randstr: res.randstr || '',
+ })
+ } else {
+ testResult.value = { success: false, message: '验证已取消。' }
+ }
}, { userLanguage: 'zh-cn' }).show()
} catch (error) {
testResult.value = { success: false, message: '初始化失败: ' + error.message }
}
}
- script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
+ script.onerror = function () { testLoading.value = false; testResult.value = { success: false, message: '加载SDK失败' } }
break
case 'cloudflare':
window.onTurnstileTestLoad = function() {
+ testLoading.value = false
try {
window.turnstile.render('#captcha-test-box', {
sitekey: cfg.site_key,
- theme: 'auto',
+ theme: cfg.theme || 'auto',
size: cfg.size || 'normal',
- callback: function() {
- testResult.value = { success: true, message: '验证成功!' }
- message.success('Cloudflare Turnstile 测试成功!')
- setTimeout(closeTestModal, 1500)
+ action: 'aksrt-admin-test',
+ cdata: 'admin-captcha-test',
+ callback: function(token) {
+ submitCaptchaTest('cloudflare', { cf_turnstile_response: token })
},
'error-callback': function() {
testResult.value = { success: false, message: '验证失败' }
@@ -363,19 +360,28 @@ function loadProviderSDK(provider, cfg) {
}
}
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileTestLoad'
- script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
+ script.onerror = function () { testLoading.value = false; testResult.value = { success: false, message: '加载SDK失败' } }
break
case 'geetest':
script.src = 'https://static.geetest.com/v4/gt4.js'
script.onload = function () {
+ testLoading.value = false
try {
window.initGeetest4({ captchaId: cfg.captcha_id, product: cfg.product || 'float' }, function(captchaObj) {
window.__gtTestCaptcha = captchaObj
const onSuccess = function() {
- testResult.value = { success: true, message: '验证成功!' }
- message.success('极验 GeeTest 测试成功!')
- setTimeout(closeTestModal, 1500)
+ const result = captchaObj.getValidate ? captchaObj.getValidate() : null
+ if (!result) {
+ testResult.value = { success: false, message: '未取得极验验证参数' }
+ return
+ }
+ submitCaptchaTest('geetest', {
+ geetest_lot_number: result.lot_number || '',
+ geetest_captcha_output: result.captcha_output || '',
+ geetest_pass_token: result.pass_token || '',
+ geetest_gen_time: result.gen_time || '',
+ })
}
const onError = function(error) {
testResult.value = { success: false, message: '出错: ' + (error.msg || '?') }
@@ -393,22 +399,21 @@ function loadProviderSDK(provider, cfg) {
testResult.value = { success: false, message: '初始化失败: ' + error.message }
}
}
- script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
+ script.onerror = function () { testLoading.value = false; testResult.value = { success: false, message: '加载SDK失败' } }
break
case 'aliyun':
window.AliyunCaptchaConfig = { region: cfg.region || 'cn', prefix: cfg.prefix }
script.src = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js'
script.onload = function () {
+ testLoading.value = false
try {
window.initAliyunCaptcha({
SceneId: cfg.scene_id,
mode: 'embed',
element: '#captcha-test-box',
- success: function() {
- testResult.value = { success: true, message: '验证成功!' }
- message.success('阿里云测试成功!')
- setTimeout(closeTestModal, 1500)
+ success: function(captchaVerifyParam) {
+ submitCaptchaTest('aliyun', { aliyun_captcha_verify_param: String(captchaVerifyParam || '') })
},
fail: function(result) {
testResult.value = { success: false, message: String(result) }
@@ -421,7 +426,7 @@ function loadProviderSDK(provider, cfg) {
testResult.value = { success: false, message: '初始化失败: ' + error.message }
}
}
- script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
+ script.onerror = function () { testLoading.value = false; testResult.value = { success: false, message: '加载SDK失败' } }
break
default:
diff --git a/assets/admin/src/schema/sections/permissions/index.js b/assets/admin/src/schema/sections/permissions/index.js
index 6b62b218..14b9897e 100644
--- a/assets/admin/src/schema/sections/permissions/index.js
+++ b/assets/admin/src/schema/sections/permissions/index.js
@@ -15,10 +15,7 @@ export const permissionsSchemas = {
defaults: {
enabled: false,
active_provider: '',
- enabled_scenarios: [
- 'comment', 'login', 'register', 'bind', 'bind_phone',
- 'lostpassword', 'change_password', 'change_email', 'download', 'signin',
- ],
+ enabled_scenarios: [],
tencent: {},
aliyun: {},
cloudflare: {},
@@ -81,10 +78,10 @@ export const permissionsSchemas = {
alertContent: '下方会渲染当前验证码服务商的专属配置面板。保存仍通过统一 schema contract 进行。',
dependsOn: { key: 'enabled', value: true },
},
- { key: 'tencent', label: '腾讯云配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'tencent' } },
- { key: 'aliyun', label: '阿里云配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'aliyun' } },
- { key: 'cloudflare', label: 'Cloudflare 配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'cloudflare' } },
- { key: 'geetest', label: '极验配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'geetest' } },
+ { key: 'tencent', label: '腾讯云配置', type: 'custom', default: { secret_id: '', secret_key: '', app_id: '', app_secret_key: '' }, secretFields: ['secret_key', 'app_secret_key'], dependsOn: { key: 'active_provider', value: 'tencent' } },
+ { key: 'aliyun', label: '阿里云配置', type: 'custom', default: { region: 'cn', prefix: '', scene_id: '', access_key_id: '', access_key_secret: '' }, secretFields: ['access_key_secret'], dependsOn: { key: 'active_provider', value: 'aliyun' } },
+ { key: 'cloudflare', label: 'Cloudflare 配置', type: 'custom', default: { site_key: '', secret_key: '', theme: 'auto', size: 'normal' }, secretFields: ['secret_key'], dependsOn: { key: 'active_provider', value: 'cloudflare' } },
+ { key: 'geetest', label: '极验配置', type: 'custom', default: { captcha_id: '', captcha_key: '', product: 'bind' }, secretFields: ['captcha_key'], dependsOn: { key: 'active_provider', value: 'geetest' } },
],
},
],
diff --git a/assets/js/auth.js b/assets/js/auth.js
index a4e44d7f..f02f864f 100644
--- a/assets/js/auth.js
+++ b/assets/js/auth.js
@@ -26,6 +26,7 @@
account_match_failed: '第三方账号匹配失败,请重试',
login_failed: '第三方登录失败,请重试',
bind_failed: '第三方账号绑定失败,请重试',
+ bind_verification_required: '请先完成人机验证后再绑定',
binding_conflict: '该第三方账号已绑定其他账号',
account_conflict: '该第三方账号无法绑定到当前账号',
already_bound: '该第三方账号已绑定其他账号',
@@ -297,10 +298,12 @@
nodes.box.dataset.loading = '1';
setQrStatus(root, '正在获取二维码');
- qrRequest(root, 'qrcode-login', {
+ var qrPayload = {
action: root.dataset.oauthIntent || 'login',
redirect_to: root.dataset.redirectTo || window.location.href,
- }).then(function (result) {
+ };
+ if (root.dataset.bindPass) qrPayload.bind_pass = root.dataset.bindPass;
+ qrRequest(root, 'qrcode-login', qrPayload).then(function (result) {
if (!result || !isRootActive(root)) return;
nodes.box.dataset.loading = '0';
nodes.box.dataset.loaded = '1';
@@ -347,6 +350,7 @@
var root = form.closest('[data-aksrt-auth]');
data.action = root ? (root.dataset.oauthIntent || 'login') : 'login';
data.redirect_to = root ? (root.dataset.redirectTo || window.location.href) : window.location.href;
+ if (root && root.dataset.bindPass) data.bind_pass = root.dataset.bindPass;
var button = form.querySelector('button[type="submit"]');
if (button) button.disabled = true;
request('weixingzh-code', data).then(function (result) {
@@ -391,23 +395,20 @@
}
function captchaScene(scene) {
- return scene === 'resetpassword' ? 'lostpassword' : scene;
+ return scene === 'resetpassword' || scene === 'reset-password' ? 'lostpassword' : scene;
}
- function verifyCaptcha(scene, callback) {
- var state = window.AKSRTCaptchaState;
- var captcha = state && state.scenes ? state.scenes[captchaScene(scene)] : null;
- if (!captcha || typeof captcha.verify !== 'function') {
- callback(true);
- return;
- }
- captcha.verify(function (verified) { callback(verified !== false); });
+ function captchaFormScene(form, fallback) {
+ var mount = form && form.querySelector ? form.querySelector('[data-aksrt-captcha-config][data-scene]') : null;
+ return mount ? (mount.getAttribute('data-scene') || captchaScene(fallback)) : captchaScene(fallback);
}
- function resetCaptcha(scene) {
- var state = window.AKSRTCaptchaState;
- var captcha = state && state.scenes ? state.scenes[captchaScene(scene)] : null;
- if (captcha && typeof captcha.reset === 'function') captcha.reset();
+ function captchaScope(form) {
+ return form || document;
+ }
+
+ function resetCaptcha(scene, scope) {
+ if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset(captchaScene(scene), captchaScope(scope));
}
function sendCode(button) {
@@ -422,39 +423,63 @@
payload.scene = button.dataset.sendCode;
payload.target = target.value.trim();
payload.channel = (form.querySelector('[name="channel"]') || {}).value || '';
- verifyCaptcha(payload.scene, function (verified) {
- if (!verified) {
+ var scene = captchaFormScene(form, payload.scene);
+ var captcha = window.AKSRTCaptcha;
+ var ticket = captcha ? captcha.run(captchaScene(scene), captchaScope(form)) : Promise.resolve({});
+ ticket.then(function (captchaPayload) {
+ if (captchaPayload === null) {
button.disabled = false;
notify('请先完成人机验证', 'warning');
- return;
+ return null;
}
payload = formData(form);
payload.scene = button.dataset.sendCode;
payload.target = target.value.trim();
payload.channel = (form.querySelector('[name="channel"]') || {}).value || '';
- request('code', payload).then(function (result) {
+ Object.assign(payload, captchaPayload || {});
+ return request('code', payload).then(function (result) {
notify(result.message || '验证码已发送', 'success');
startCountdown(button, result.countdown || 60);
}).catch(function (error) {
button.disabled = false;
notify(error.message, 'danger');
}).finally(function () {
- resetCaptcha(payload.scene);
+ resetCaptcha(scene, form);
});
+ }).catch(function (error) {
+ button.disabled = false;
+ resetCaptcha(scene, form);
+ notify(error.message || '人机验证失败,请重试', 'danger');
});
}
function submit(form) {
var action = form.dataset.authForm;
- var payload = formData(form);
- if (action === 'register' && payload.channel !== 'phone') payload.user_email = payload.target;
+ var scene = captchaFormScene(form, action);
+ var captcha = window.AKSRTCaptcha;
setBusy(form, true);
- request(action, payload).then(function (result) {
- notify(result.message || '操作成功', 'success');
- if (result.redirect) window.setTimeout(function () { window.location.assign(result.redirect); }, 350);
+ (captcha ? captcha.run(scene, captchaScope(form)) : Promise.resolve({})).then(function (captchaPayload) {
+ if (captchaPayload === null) {
+ setBusy(form, false);
+ notify('请先完成人机验证', 'warning');
+ return null;
+ }
+ var payload = formData(form);
+ if (action === 'register' && payload.channel !== 'phone') payload.user_email = payload.target;
+ Object.assign(payload, captchaPayload || {});
+ return request(action, payload).then(function (result) {
+ notify(result.message || '操作成功', 'success');
+ if (result.redirect) window.setTimeout(function () { window.location.assign(result.redirect); }, 350);
+ }).catch(function (error) {
+ notify(error.message, 'danger');
+ setBusy(form, false);
+ }).finally(function () {
+ resetCaptcha(scene, form);
+ });
}).catch(function (error) {
- notify(error.message, 'danger');
setBusy(form, false);
+ resetCaptcha(scene, form);
+ notify(error.message || '人机验证失败,请重试', 'danger');
});
}
diff --git a/assets/js/build/public.js b/assets/js/build/public.js
index 86c26d0a..8ab28b63 100644
--- a/assets/js/build/public.js
+++ b/assets/js/build/public.js
@@ -2375,8 +2375,8 @@ tbquire.config({
}
}
- // 执行实际的 AJAX 请求 — local insert instead of full-page re-fetch
- function performSubmit() {
+ // 执行实际的 REST 请求 — local insert instead of full-page re-fetch
+ function performSubmit(captchaPayload) {
setLoading(true);
var formData = collectFormData();
@@ -2388,6 +2388,7 @@ tbquire.config({
// Try REST endpoint for local-insert HTML response
var restBody = {};
formData.forEach(function (v, k) { restBody[k] = v; });
+ Object.assign(restBody, captchaPayload || {});
if (isEdit) {
restBody.comment_id = parseInt(editId.value, 10);
restBody.content = restBody.comment || restBody.content || '';
@@ -2428,15 +2429,6 @@ tbquire.config({
var replyInfo = document.getElementById('comment-reply-info');
if (replyInfo) replyInfo.style.display = 'none';
- // Reset captcha after successful submit
- try {
- var captchaScene = window.AKSRTCaptchaState &&
- window.AKSRTCaptchaState.scenes &&
- window.AKSRTCaptchaState.scenes.comment;
- if (captchaScene && typeof captchaScene.reset === 'function') {
- captchaScene.reset();
- }
- } catch (e) { /* ignore */ }
// Wait time countdown
if (json.wait_time && json.wait_time > 0) {
@@ -2465,14 +2457,6 @@ tbquire.config({
if (json && json.code) {
notyf(json.message || '评论提交失败', 'danger');
- try {
- var failScene = window.AKSRTCaptchaState &&
- window.AKSRTCaptchaState.scenes &&
- window.AKSRTCaptchaState.scenes.comment;
- if (failScene && typeof failScene.reset === 'function') {
- failScene.reset();
- }
- } catch (e) { /* ignore */ }
if (json.wait_time) startCountdown(json.wait_time);
else setLoading(false);
return;
@@ -2482,9 +2466,14 @@ tbquire.config({
setLoading(false);
setTimeout(function () { reloadCommentList(); }, 500);
})
- .catch(function () {
- // Fallback: submit to wp-comments-post.php directly
- performLegacySubmit();
+ .catch(function (error) {
+ console.error('[AKSRT Comment] 提交失败:', error);
+ notyf((error && error.message) || '网络连接失败,请检查网络后重试', 'danger');
+ setLoading(false);
+ })
+ .finally(function () {
+ if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('comment', form);
+ if (!countdownTimer && isSubmitting) setLoading(false);
});
}
@@ -2556,44 +2545,6 @@ tbquire.config({
placeholder.remove();
}
}
- // Legacy fallback: POST to wp-comments-post.php
- function performLegacySubmit() {
- var formData = collectFormData();
- var actionUrl = form.getAttribute('action') || '/wp-comments-post.php';
-
- fetch(actionUrl, {
- method: 'POST',
- body: formData,
- credentials: 'same-origin',
- headers: { 'X-Requested-With': 'XMLHttpRequest' }
- })
- .then(function (response) {
- return response.text().then(function (text) {
- return { status: response.status, text: text, ok: response.ok };
- });
- })
- .then(function (result) {
- handleResponse(result.text, result.status);
- })
- .catch(function (error) {
- console.error('[AKSRT Comment] 提交失败:', error);
-
- if (error.message === 'Failed to fetch' || error.name === 'TypeError') {
- notyf('网络连接失败,请检查网络后重试', 'danger');
- } else {
- notyf(error.message || '评论提交失败,请重试', 'danger');
- }
-
- setLoading(false);
- })
- .finally(function () {
- // 兜底:避免异常分支漏掉状态恢复导致按钮卡在"发送中"
- if (!countdownTimer && isSubmitting) {
- setLoading(false);
- }
- });
- }
-
// 核心表单提交事件处理器
function handleSubmit(e) {
// 如果已经在提交中或倒计时中,防止重复提交
@@ -2606,29 +2557,20 @@ tbquire.config({
// 阻止默认表单提交
e.preventDefault();
e.stopImmediatePropagation();
- // 检查验证码状态(如果有验证码组件)
- var captchaScene = window.AKSRTCaptchaState &&
- window.AKSRTCaptchaState.scenes &&
- window.AKSRTCaptchaState.scenes.comment;
-
- if (captchaScene && typeof captchaScene.verify === 'function') {
- // 有验证码:先触发验证流程
- setLoading(true);
-
- captchaScene.verify(function (verified) {
- if (verified !== false) {
- // 验证通过,执行 AJAX 提交
- performSubmit();
- } else {
- // 验证失败
- notyf('人机验证未通过,请重试', 'warning');
- setLoading(false);
- }
- });
- } else {
- // 无验证码或已经验证过:直接提交
- performSubmit();
- }
+ var captcha = window.AKSRTCaptcha;
+ setLoading(true);
+ (captcha ? captcha.run('comment', form) : Promise.resolve({})).then(function (captchaPayload) {
+ if (captchaPayload === null) {
+ notyf('人机验证未通过,请重试', 'warning');
+ setLoading(false);
+ return;
+ }
+ performSubmit(captchaPayload);
+ }).catch(function () {
+ notyf('人机验证未通过,请重试', 'warning');
+ setLoading(false);
+ if (captcha) captcha.reset('comment', form);
+ });
return false;
}
@@ -5692,409 +5634,560 @@ jQuery.cookie=function(e,o,t){if(o===void 0){var i=null;if(document.cookie&&docu
/* source: assets/js/captcha-front.js */
/**
- * AKSRT Captcha front-end runtime — cloud providers only (tencent / aliyun / cloudflare / geetest).
+ * AKSRT Captcha front-end runtime — cloud providers only.
*
- * PHP renders passive mounts via [data-aksrt-captcha-config] (JSON config attribute);
- * this script scans them, registers AKSRTCaptchaState.scenes[scene], loads the provider
- * SDK once per page, and writes verified tokens back into the surrounding form.
- * Bundled into assets/js/build/public.js by tools/build-public-assets.mjs.
+ * 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 = [
+ var TOKEN_FIELDS = Object.freeze([
+ 'captcha_verified',
+ 'aksrt_captcha_challenge',
+ 'aksrt_captcha_proof',
'cf_turnstile_response',
- 'tencent_ticket', 'tencent_randstr', 'tencent_sid',
+ '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 = {};
- var root = (window.AKSRTCaptchaState = window.AKSRTCaptchaState || { loaders: {}, scenes: {} });
- root.rescan = scan;
+ 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 scan() {
- var mounts = document.querySelectorAll('[data-aksrt-captcha-config]');
- Array.prototype.forEach.call(mounts, function (el) {
- if (el.dataset.aksrtCaptchaBound) {
- return;
- }
- el.dataset.aksrtCaptchaBound = '1';
- var config;
+ function scopeBoundary(box, config) {
+ if (config.scope) {
try {
- config = JSON.parse(el.getAttribute('data-aksrt-captcha-config'));
- } catch (e) {
- return;
- }
- if (config && config.scene && config.uid && config.vid) {
- initMount(config);
- }
+ 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;
});
}
- /**
- * One mount = one scene instance. Mirrors the scene semantics of the previous
- * per-render inline script: the last initialized mount wins scenes[scene].
- */
- function initMount(C) {
- var root = window.AKSRTCaptchaState;
- var box, hField, form = null;
+ 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 pendingAction = null;
- var triggerCaptcha = function () { showTip('验证码加载中,请稍候', '#e67e22'); };
+ 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 (root.loaders[key]) { return root.loaders[key]; }
- root.loaders[key] = new Promise(function (resolve, reject) {
+ 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()) { resolve(); return; }
- existing.addEventListener('load', function () { resolve(); }, { once: true });
- existing.addEventListener('error', function () { reject(new Error(key + ' load failed')); }, { once: true });
+ 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 = function () { resolve(); };
- script.onerror = function () { reject(new Error(key + ' load failed')); };
+ script.onload = resolveLoad;
+ script.onerror = function () { rejectLoad(new Error(key + ' load failed')); };
document.head.appendChild(script);
});
- return root.loaders[key];
- }
-
- function findForm() {
- form = box.closest('form');
- if (!form && C.scene === 'comment') form = document.getElementById('commentform');
- if (!form && (C.scene === 'login' || C.scene === 'register')) {
- form = box.closest('[data-auth-form]') || document.querySelector('[data-auth-form]');
- }
- if (!form && C.scene === 'bind_phone') {
- form = document.getElementById('aksrt-bind-phone-form')
- || document.querySelector('[data-aksrt-bind-phone-form]')
- || document.querySelector('.auth-form');
- }
- return form;
- }
-
- function bindSubmit() {
- if (!form) return;
- form.addEventListener('submit', function (e) {
- if (verified || hField.value === '1') return;
- e.preventDefault(); e.stopImmediatePropagation();
- pendingAction = null; triggerCaptcha();
- }, true);
+ return loaders[key];
}
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);
+ var field = container.querySelector('input[name="' + name + '"]');
+ if (!field) {
+ field = document.createElement('input');
+ field.type = 'hidden';
+ field.name = name;
+ container.appendChild(field);
}
- el.value = value;
+ 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';
- var container = form || box;
- TOKEN_FIELDS.forEach(function (n) {
- var f = container.querySelector('input[name="' + n + '"]');
- if (f) f.remove();
- });
- if (C.scene === 'download') {
- syncDownloadSiblings(false);
- }
+ setField('aksrt_captcha_proof', '');
+ clearProviderTokens();
+ try { providerReset(); } catch (_) {}
+ refreshChallenge();
}
- function syncDownloadSiblings(passed) {
- var myContainer = box.closest('[data-aksrt-download-captcha]');
- if (!myContainer) return;
- 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 = passed ? '1' : '0';
- TOKEN_FIELDS.forEach(function (name) {
- var f = target.querySelector('input[name="' + name + '"]');
- if (f) f.remove();
- if (passed) {
- var srcEl = myContainer.querySelector('input[name="' + name + '"]');
- if (srcEl && srcEl.value !== '') {
- var tgtEl = document.createElement('input');
- tgtEl.type = 'hidden';
- tgtEl.name = name;
- target.appendChild(tgtEl);
- tgtEl.value = srcEl.value;
- }
- }
- });
- });
- }
-
- function submitNativeForm() {
- if (!form) return;
- if (form.requestSubmit) { form.requestSubmit(); }
- else { HTMLFormElement.prototype.submit.call(form); }
- }
-
- function submitForm() {
- verified = true; hField.value = '1';
- if (C.scene === 'download') syncDownloadSiblings(true);
- if (typeof pendingAction === 'function') {
- finishPending(true);
- return;
- }
- setTimeout(resetVerificationState, 3000);
- submitNativeForm();
- }
-
- 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 = '验证码加载中...
'; }
- function clearLoading() { box.innerHTML = ''; }
-
function finishPending(ok) {
- var action = pendingAction;
- pendingAction = null;
- if (typeof action === 'function') { action(!!ok); }
+ var callbacks = pending.slice();
+ pending = [];
+ callbacks.forEach(function (callback) { callback(!!ok); });
}
- function cancelPending(msg) {
- if (msg) showTip(msg, '#e67e22');
+ function cancelPending(message) {
+ if (message) showTip(message, '#e67e22');
finishPending(false);
}
- root.scenes[C.scene] = {
- verify: function (callback) {
- if (verified || hField.value === '1') {
- if (typeof callback === 'function') callback(true);
- return;
- }
- pendingAction = (typeof callback === 'function') ? function (ok) { callback(ok !== false); } : null;
- triggerCaptcha();
- },
- reset: resetVerificationState
+ 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();
- // ============ 腾讯云 Captcha 2.0 ============
function loadTencent() {
showLoading();
loadScriptOnce('tencent', 'https://turing.captcha.qcloud.com/TJCaptcha.js', function () {
- return typeof TencentCaptcha !== 'undefined';
+ return typeof window.TencentCaptcha !== 'undefined';
}).then(function () {
clearLoading();
- var capIns = null;
+ var captchaInstance = null;
triggerCaptcha = function () {
- if (!capIns) {
- capIns = new TencentCaptcha(C.tc.app_id, function (res) {
- if (res.ret === 2) { cancelPending('已取消验证'); return; }
- if (res.ret === 0 && res.ticket) {
- if (res.ticket.indexOf('trerror_') === 0) {
- showTip('验证码服务异常,请刷新页面重试', '#e67e22');
+ 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', res.ticket);
- setField('tencent_randstr', res.randstr);
- setField('tencent_sid', res.sid || '');
- submitForm();
+ setField('tencent_ticket', result.ticket);
+ setField('tencent_randstr', result.randstr);
+ completeProviderVerification();
return;
}
- if (res.errorCode) {
- var errorMap = { 1001: '验证码组件加载失败', 1002: '验证码超时', 1006: '验证码配置拉取失败', 1008: '验证码页面加载异常', 1012: '验证码刷新次数过多', 1013: '验证码网络异常' };
- showTip(errorMap[res.errorCode] || ('验证码异常(errorCode:' + res.errorCode + ')'), '#e74c3c');
- }
+ var errorMap = { 1001: '验证码组件加载失败', 1002: '验证码超时', 1006: '验证码配置拉取失败', 1008: '验证码页面加载异常', 1012: '验证码刷新次数过多', 1013: '验证码网络异常' };
+ cancelPending(errorMap[result.errorCode] || '验证码异常,请重试');
}, { userLanguage: 'zh-cn' });
}
- capIns.show();
+ captchaInstance.show();
};
- }).catch(function () { clearLoading(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
+ }).catch(function () {
+ clearLoading();
+ cancelPending('验证码组件加载失败,请刷新重试');
+ });
}
- // ============ Cloudflare Turnstile(弹窗) ============
function loadCloudflare() {
- var cfWidgetId = null, cfVerifying = false, cfModal = null;
+ var widgetId = null;
+ var verifying = false;
+ var modal = 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 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 (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: 'aksrt-verify', cdata: C.scene,
+ 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 (!cfVerifying) return;
- cfVerifying = false;
+ if (!verifying) return;
+ verifying = false;
setField('cf_turnstile_response', token);
closeModal();
- submitForm();
+ completeProviderVerification();
},
- 'error-callback': function () { showTip('验证码失败,请重试', '#e74c3c'); cfWidgetId = null; setTimeout(renderWidget, 500); },
- 'expired-callback': function () { showTip('验证码已过期,请重新验证', '#e67e22'); cfWidgetId = null; setTimeout(renderWidget, 500); }
+ 'error-callback': function () {
+ verifying = false;
+ closeModal();
+ cancelPending('验证码失败,请重试');
+ },
+ 'expired-callback': function () {
+ verifying = false;
+ closeModal();
+ cancelPending('验证码已过期,请重新验证');
+ }
});
}
function createModal() {
- cfModal = document.createElement('div');
- cfModal.className = 'aksrt-captcha-modal-overlay';
- cfModal.innerHTML = '' +
+ modal = document.createElement('div');
+ modal.className = 'aksrt-captcha-modal-overlay';
+ modal.innerHTML = '' +
'';
- document.body.appendChild(cfModal);
- var cancelFn = function () { cfVerifying = false; closeModal(); cancelPending('已取消验证'); };
- cfModal.querySelector('.aksrt-captcha-modal-backdrop').addEventListener('click', cancelFn);
- cfModal.querySelector('.aksrt-captcha-modal-close').addEventListener('click', cancelFn);
+ 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();
}
- function closeModal() {
- if (cfWidgetId) { try { turnstile.remove(cfWidgetId); } catch (e) {} cfWidgetId = null; }
- if (cfModal && cfModal.parentNode) { cfModal.parentNode.removeChild(cfModal); }
- cfModal = null;
+ 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 (cfVerifying) { showTip('验证码进行中,请稍候', '#e67e22'); return; }
- cfVerifying = true;
+ if (verifying) return;
+ verifying = true;
createModal();
};
+ providerReset = closeModal;
}
- // ============ 极验 GeeTest v4 ============
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';
+ 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();
- showTip('极验 Captcha ID 未配置,请到后台「功能&权限 → 人机验证」填写并保存', '#e74c3c');
+ cancelPending('极验 Captcha ID 未配置');
return;
}
+
function render() {
clearLoading();
- initGeetest4({ captchaId: captchaId, product: product, hideSuccess: true }, function (capIns) {
- window['__gt_' + C.uid] = capIns;
+ window.initGeetest4({ captchaId: captchaId, product: product, hideSuccess: true }, function (captchaInstance) {
function onSuccess() {
- var result = capIns.getValidate();
- if (!result) return;
+ 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);
- submitForm();
- }
- if (product === 'bind' || product === 'popup') {
- capIns.onReady(function () {}).onSuccess(onSuccess).onClose(function () { cancelPending('已取消验证'); });
- } else {
- capIns.appendTo('#' + C.uid).onSuccess(onSuccess).onClose(function () { cancelPending('已取消验证'); });
+ 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();
+ };
});
- 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') {
+
+ if (typeof window.initGeetest4 === 'undefined') {
loadScriptOnce('geetest', 'https://static.geetest.com/v4/gt4.js', function () {
- return typeof initGeetest4 !== 'undefined';
- }).then(render).catch(function () { clearLoading(); showTip('极验组件加载失败', '#e74c3c'); });
- return;
+ return typeof window.initGeetest4 !== 'undefined';
+ }).then(render).catch(function () {
+ clearLoading();
+ cancelPending('极验组件加载失败');
+ });
+ } else {
+ render();
}
- render();
}
- // ============ 阿里云 CAPTCHA 2.0 V3 ============
function loadAliyun() {
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;
+ var captchaInstance = 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();
+ SceneId: C.al.scene_id,
+ mode: 'popup',
+ element: '#' + C.uid,
+ success: function (verifyParam) {
+ setField('aliyun_captcha_verify_param', verifyParam);
+ completeProviderVerification();
},
- fail: function (result) { showTip('验证码失败: ' + (result || '请重试'), '#e74c3c'); captchaIns = null; },
- getInstance: function (instance) { captchaIns = instance; },
- slideStyle: { width: 360, height: 40 }, language: 'cn'
+ fail: function () { cancelPending('验证码失败,请重试'); },
+ getInstance: function (value) { captchaInstance = value; },
+ slideStyle: { width: 360, height: 40 },
+ language: 'cn'
});
triggerCaptcha = function () {
- if (captchaIns && captchaIns.show) { captchaIns.show(); return; }
- showTip('验证码组件加载中,请稍候再试', '#e67e22');
+ 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(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
- return;
- }
- render();
- }
-
- function loadProvider() {
- switch (C.provider) {
- case 'tencent': loadTencent(); break;
- case 'cloudflare': loadCloudflare(); break;
- case 'geetest': loadGeetest(); break;
- case 'aliyun': loadAliyun(); break;
+ }).then(render).catch(function () {
+ clearLoading();
+ cancelPending('验证码组件加载失败,请刷新重试');
+ });
+ } else {
+ render();
}
}
- function init() {
- box = document.getElementById(C.uid);
- hField = document.getElementById(C.vid);
- if (!box || !hField) return;
-
- var tries = 0;
- (function retryFind() {
- findForm();
- if (!form && ++tries < 10) { setTimeout(retryFind, 150); }
- else {
- if (form) bindSubmit();
- setTimeout(loadProvider, 0);
- }
- })();
+ switch (C.provider) {
+ case 'tencent': loadTencent(); break;
+ case 'cloudflare': loadCloudflare(); break;
+ case 'geetest': loadGeetest(); break;
+ case 'aliyun': loadAliyun(); break;
}
-
- init();
}
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', scan);
- } else {
- setTimeout(scan, 50);
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan);
+ else setTimeout(scan, 0);
+
+ 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 });
}
})();
;
diff --git a/assets/js/captcha-front.js b/assets/js/captcha-front.js
index 0d91cc3a..646b3d82 100644
--- a/assets/js/captcha-front.js
+++ b/assets/js/captcha-front.js
@@ -1,406 +1,557 @@
/**
- * AKSRT Captcha front-end runtime — cloud providers only (tencent / aliyun / cloudflare / geetest).
+ * AKSRT Captcha front-end runtime — cloud providers only.
*
- * PHP renders passive mounts via [data-aksrt-captcha-config] (JSON config attribute);
- * this script scans them, registers AKSRTCaptchaState.scenes[scene], loads the provider
- * SDK once per page, and writes verified tokens back into the surrounding form.
- * Bundled into assets/js/build/public.js by tools/build-public-assets.mjs.
+ * 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 = [
+ var TOKEN_FIELDS = Object.freeze([
+ 'captcha_verified',
+ 'aksrt_captcha_challenge',
+ 'aksrt_captcha_proof',
'cf_turnstile_response',
- 'tencent_ticket', 'tencent_randstr', 'tencent_sid',
+ '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 = {};
- var root = (window.AKSRTCaptchaState = window.AKSRTCaptchaState || { loaders: {}, scenes: {} });
- root.rescan = scan;
+ 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 scan() {
- var mounts = document.querySelectorAll('[data-aksrt-captcha-config]');
- Array.prototype.forEach.call(mounts, function (el) {
- if (el.dataset.aksrtCaptchaBound) {
- return;
- }
- el.dataset.aksrtCaptchaBound = '1';
- var config;
+ function scopeBoundary(box, config) {
+ if (config.scope) {
try {
- config = JSON.parse(el.getAttribute('data-aksrt-captcha-config'));
- } catch (e) {
- return;
- }
- if (config && config.scene && config.uid && config.vid) {
- initMount(config);
- }
+ 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;
});
}
- /**
- * One mount = one scene instance. Mirrors the scene semantics of the previous
- * per-render inline script: the last initialized mount wins scenes[scene].
- */
- function initMount(C) {
- var root = window.AKSRTCaptchaState;
- var box, hField, form = null;
+ 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 pendingAction = null;
- var triggerCaptcha = function () { showTip('验证码加载中,请稍候', '#e67e22'); };
+ 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 (root.loaders[key]) { return root.loaders[key]; }
- root.loaders[key] = new Promise(function (resolve, reject) {
+ 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()) { resolve(); return; }
- existing.addEventListener('load', function () { resolve(); }, { once: true });
- existing.addEventListener('error', function () { reject(new Error(key + ' load failed')); }, { once: true });
+ 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 = function () { resolve(); };
- script.onerror = function () { reject(new Error(key + ' load failed')); };
+ script.onload = resolveLoad;
+ script.onerror = function () { rejectLoad(new Error(key + ' load failed')); };
document.head.appendChild(script);
});
- return root.loaders[key];
- }
-
- function findForm() {
- form = box.closest('form');
- if (!form && C.scene === 'comment') form = document.getElementById('commentform');
- if (!form && (C.scene === 'login' || C.scene === 'register')) {
- form = box.closest('[data-auth-form]') || document.querySelector('[data-auth-form]');
- }
- if (!form && C.scene === 'bind_phone') {
- form = document.getElementById('aksrt-bind-phone-form')
- || document.querySelector('[data-aksrt-bind-phone-form]')
- || document.querySelector('.auth-form');
- }
- return form;
- }
-
- function bindSubmit() {
- if (!form) return;
- form.addEventListener('submit', function (e) {
- if (verified || hField.value === '1') return;
- e.preventDefault(); e.stopImmediatePropagation();
- pendingAction = null; triggerCaptcha();
- }, true);
+ return loaders[key];
}
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);
+ var field = container.querySelector('input[name="' + name + '"]');
+ if (!field) {
+ field = document.createElement('input');
+ field.type = 'hidden';
+ field.name = name;
+ container.appendChild(field);
}
- el.value = value;
+ 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';
- var container = form || box;
- TOKEN_FIELDS.forEach(function (n) {
- var f = container.querySelector('input[name="' + n + '"]');
- if (f) f.remove();
- });
- if (C.scene === 'download') {
- syncDownloadSiblings(false);
- }
+ setField('aksrt_captcha_proof', '');
+ clearProviderTokens();
+ try { providerReset(); } catch (_) {}
+ refreshChallenge();
}
- function syncDownloadSiblings(passed) {
- var myContainer = box.closest('[data-aksrt-download-captcha]');
- if (!myContainer) return;
- 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 = passed ? '1' : '0';
- TOKEN_FIELDS.forEach(function (name) {
- var f = target.querySelector('input[name="' + name + '"]');
- if (f) f.remove();
- if (passed) {
- var srcEl = myContainer.querySelector('input[name="' + name + '"]');
- if (srcEl && srcEl.value !== '') {
- var tgtEl = document.createElement('input');
- tgtEl.type = 'hidden';
- tgtEl.name = name;
- target.appendChild(tgtEl);
- tgtEl.value = srcEl.value;
- }
- }
- });
- });
- }
-
- function submitNativeForm() {
- if (!form) return;
- if (form.requestSubmit) { form.requestSubmit(); }
- else { HTMLFormElement.prototype.submit.call(form); }
- }
-
- function submitForm() {
- verified = true; hField.value = '1';
- if (C.scene === 'download') syncDownloadSiblings(true);
- if (typeof pendingAction === 'function') {
- finishPending(true);
- return;
- }
- setTimeout(resetVerificationState, 3000);
- submitNativeForm();
- }
-
- 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 = '验证码加载中...
'; }
- function clearLoading() { box.innerHTML = ''; }
-
function finishPending(ok) {
- var action = pendingAction;
- pendingAction = null;
- if (typeof action === 'function') { action(!!ok); }
+ var callbacks = pending.slice();
+ pending = [];
+ callbacks.forEach(function (callback) { callback(!!ok); });
}
- function cancelPending(msg) {
- if (msg) showTip(msg, '#e67e22');
+ function cancelPending(message) {
+ if (message) showTip(message, '#e67e22');
finishPending(false);
}
- root.scenes[C.scene] = {
- verify: function (callback) {
- if (verified || hField.value === '1') {
- if (typeof callback === 'function') callback(true);
- return;
- }
- pendingAction = (typeof callback === 'function') ? function (ok) { callback(ok !== false); } : null;
- triggerCaptcha();
- },
- reset: resetVerificationState
+ 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();
- // ============ 腾讯云 Captcha 2.0 ============
function loadTencent() {
showLoading();
loadScriptOnce('tencent', 'https://turing.captcha.qcloud.com/TJCaptcha.js', function () {
- return typeof TencentCaptcha !== 'undefined';
+ return typeof window.TencentCaptcha !== 'undefined';
}).then(function () {
clearLoading();
- var capIns = null;
+ var captchaInstance = null;
triggerCaptcha = function () {
- if (!capIns) {
- capIns = new TencentCaptcha(C.tc.app_id, function (res) {
- if (res.ret === 2) { cancelPending('已取消验证'); return; }
- if (res.ret === 0 && res.ticket) {
- if (res.ticket.indexOf('trerror_') === 0) {
- showTip('验证码服务异常,请刷新页面重试', '#e67e22');
+ 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', res.ticket);
- setField('tencent_randstr', res.randstr);
- setField('tencent_sid', res.sid || '');
- submitForm();
+ setField('tencent_ticket', result.ticket);
+ setField('tencent_randstr', result.randstr);
+ completeProviderVerification();
return;
}
- if (res.errorCode) {
- var errorMap = { 1001: '验证码组件加载失败', 1002: '验证码超时', 1006: '验证码配置拉取失败', 1008: '验证码页面加载异常', 1012: '验证码刷新次数过多', 1013: '验证码网络异常' };
- showTip(errorMap[res.errorCode] || ('验证码异常(errorCode:' + res.errorCode + ')'), '#e74c3c');
- }
+ var errorMap = { 1001: '验证码组件加载失败', 1002: '验证码超时', 1006: '验证码配置拉取失败', 1008: '验证码页面加载异常', 1012: '验证码刷新次数过多', 1013: '验证码网络异常' };
+ cancelPending(errorMap[result.errorCode] || '验证码异常,请重试');
}, { userLanguage: 'zh-cn' });
}
- capIns.show();
+ captchaInstance.show();
};
- }).catch(function () { clearLoading(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
+ }).catch(function () {
+ clearLoading();
+ cancelPending('验证码组件加载失败,请刷新重试');
+ });
}
- // ============ Cloudflare Turnstile(弹窗) ============
function loadCloudflare() {
- var cfWidgetId = null, cfVerifying = false, cfModal = null;
+ var widgetId = null;
+ var verifying = false;
+ var modal = 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 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 (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: 'aksrt-verify', cdata: C.scene,
+ 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 (!cfVerifying) return;
- cfVerifying = false;
+ if (!verifying) return;
+ verifying = false;
setField('cf_turnstile_response', token);
closeModal();
- submitForm();
+ completeProviderVerification();
},
- 'error-callback': function () { showTip('验证码失败,请重试', '#e74c3c'); cfWidgetId = null; setTimeout(renderWidget, 500); },
- 'expired-callback': function () { showTip('验证码已过期,请重新验证', '#e67e22'); cfWidgetId = null; setTimeout(renderWidget, 500); }
+ 'error-callback': function () {
+ verifying = false;
+ closeModal();
+ cancelPending('验证码失败,请重试');
+ },
+ 'expired-callback': function () {
+ verifying = false;
+ closeModal();
+ cancelPending('验证码已过期,请重新验证');
+ }
});
}
function createModal() {
- cfModal = document.createElement('div');
- cfModal.className = 'aksrt-captcha-modal-overlay';
- cfModal.innerHTML = '' +
+ modal = document.createElement('div');
+ modal.className = 'aksrt-captcha-modal-overlay';
+ modal.innerHTML = '' +
'';
- document.body.appendChild(cfModal);
- var cancelFn = function () { cfVerifying = false; closeModal(); cancelPending('已取消验证'); };
- cfModal.querySelector('.aksrt-captcha-modal-backdrop').addEventListener('click', cancelFn);
- cfModal.querySelector('.aksrt-captcha-modal-close').addEventListener('click', cancelFn);
+ 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();
}
- function closeModal() {
- if (cfWidgetId) { try { turnstile.remove(cfWidgetId); } catch (e) {} cfWidgetId = null; }
- if (cfModal && cfModal.parentNode) { cfModal.parentNode.removeChild(cfModal); }
- cfModal = null;
+ 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 (cfVerifying) { showTip('验证码进行中,请稍候', '#e67e22'); return; }
- cfVerifying = true;
+ if (verifying) return;
+ verifying = true;
createModal();
};
+ providerReset = closeModal;
}
- // ============ 极验 GeeTest v4 ============
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';
+ 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();
- showTip('极验 Captcha ID 未配置,请到后台「功能&权限 → 人机验证」填写并保存', '#e74c3c');
+ cancelPending('极验 Captcha ID 未配置');
return;
}
+
function render() {
clearLoading();
- initGeetest4({ captchaId: captchaId, product: product, hideSuccess: true }, function (capIns) {
- window['__gt_' + C.uid] = capIns;
+ window.initGeetest4({ captchaId: captchaId, product: product, hideSuccess: true }, function (captchaInstance) {
function onSuccess() {
- var result = capIns.getValidate();
- if (!result) return;
+ 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);
- submitForm();
- }
- if (product === 'bind' || product === 'popup') {
- capIns.onReady(function () {}).onSuccess(onSuccess).onClose(function () { cancelPending('已取消验证'); });
- } else {
- capIns.appendTo('#' + C.uid).onSuccess(onSuccess).onClose(function () { cancelPending('已取消验证'); });
+ 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();
+ };
});
- 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') {
+
+ if (typeof window.initGeetest4 === 'undefined') {
loadScriptOnce('geetest', 'https://static.geetest.com/v4/gt4.js', function () {
- return typeof initGeetest4 !== 'undefined';
- }).then(render).catch(function () { clearLoading(); showTip('极验组件加载失败', '#e74c3c'); });
- return;
+ return typeof window.initGeetest4 !== 'undefined';
+ }).then(render).catch(function () {
+ clearLoading();
+ cancelPending('极验组件加载失败');
+ });
+ } else {
+ render();
}
- render();
}
- // ============ 阿里云 CAPTCHA 2.0 V3 ============
function loadAliyun() {
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;
+ var captchaInstance = 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();
+ SceneId: C.al.scene_id,
+ mode: 'popup',
+ element: '#' + C.uid,
+ success: function (verifyParam) {
+ setField('aliyun_captcha_verify_param', verifyParam);
+ completeProviderVerification();
},
- fail: function (result) { showTip('验证码失败: ' + (result || '请重试'), '#e74c3c'); captchaIns = null; },
- getInstance: function (instance) { captchaIns = instance; },
- slideStyle: { width: 360, height: 40 }, language: 'cn'
+ fail: function () { cancelPending('验证码失败,请重试'); },
+ getInstance: function (value) { captchaInstance = value; },
+ slideStyle: { width: 360, height: 40 },
+ language: 'cn'
});
triggerCaptcha = function () {
- if (captchaIns && captchaIns.show) { captchaIns.show(); return; }
- showTip('验证码组件加载中,请稍候再试', '#e67e22');
+ 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(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
- return;
- }
- render();
- }
-
- function loadProvider() {
- switch (C.provider) {
- case 'tencent': loadTencent(); break;
- case 'cloudflare': loadCloudflare(); break;
- case 'geetest': loadGeetest(); break;
- case 'aliyun': loadAliyun(); break;
+ }).then(render).catch(function () {
+ clearLoading();
+ cancelPending('验证码组件加载失败,请刷新重试');
+ });
+ } else {
+ render();
}
}
- function init() {
- box = document.getElementById(C.uid);
- hField = document.getElementById(C.vid);
- if (!box || !hField) return;
-
- var tries = 0;
- (function retryFind() {
- findForm();
- if (!form && ++tries < 10) { setTimeout(retryFind, 150); }
- else {
- if (form) bindSubmit();
- setTimeout(loadProvider, 0);
- }
- })();
+ switch (C.provider) {
+ case 'tencent': loadTencent(); break;
+ case 'cloudflare': loadCloudflare(); break;
+ case 'geetest': loadGeetest(); break;
+ case 'aliyun': loadAliyun(); break;
}
-
- init();
}
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', scan);
- } else {
- setTimeout(scan, 50);
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan);
+ else setTimeout(scan, 0);
+
+ 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 });
}
})();
diff --git a/assets/js/cms-download.js b/assets/js/cms-download.js
index 148d7526..f720f287 100644
--- a/assets/js/cms-download.js
+++ b/assets/js/cms-download.js
@@ -326,26 +326,12 @@
}
// ===== 主流程 =====
- function collectCaptchaPayload(sourceEl) {
- const box = sourceEl ? sourceEl.closest('[data-aksrt-download-box]') : null;
- const container = box ? box.querySelector('[data-aksrt-download-captcha]') : null;
- if (!container) return {};
- const out = {};
- const keys = [
- 'captcha_verified',
- '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',
- ];
- keys.forEach(function (k) {
- const el = container.querySelector('input[name="' + k + '"]');
- if (el && el.value !== '') out[k] = el.value;
- });
- return out;
- }
+ function captchaScope(sourceEl) {
+ const box = sourceEl ? sourceEl.closest('[data-aksrt-download-box]') : null;
+ return box ? (box.querySelector('[data-aksrt-download-captcha]') || box) : sourceEl;
+ }
- function isCaptchaActive(sourceEl) {
+ function isCaptchaActive(sourceEl) {
const card = sourceEl ? sourceEl.closest('[data-aksrt-download-box]') : null;
if (!card) return false;
return card.getAttribute('data-captcha') === '1';
@@ -370,22 +356,20 @@
);
}
- function ensureCaptchaThen(sourceEl, callback) {
- if (!isCaptchaActive(sourceEl)) { callback(); return; }
- const state = window.AKSRTCaptchaState;
- const scene = state && state.scenes && state.scenes.download;
- if (!scene || typeof scene.verify !== 'function') {
- // captcha 组件未就绪:不直发,避免绕过服务端门槛
- if (window.console && console.warn) {
- console.warn('[AKSRT] download captcha not ready');
+ function ensureCaptchaThen(sourceEl, callback) {
+ if (!isCaptchaActive(sourceEl)) { callback({}); return; }
+ const captcha = window.AKSRTCaptcha;
+ if (!captcha) {
+ if (window.console && console.warn) console.warn('[AKSRT] download captcha not ready');
+ alert('验证码组件加载中,请稍候再试');
+ return;
}
- alert('验证码组件加载中,请稍候再试');
- return;
+ captcha.run('download', captchaScope(sourceEl)).then(function (payload) {
+ if (payload !== null) callback(payload);
+ });
}
- scene.verify(function () { callback(); });
- }
- function performDownloadRequest(button) {
+ function performDownloadRequest(button, captchaPayload) {
const card = button.closest('[data-aksrt-download-box]');
if (!card) return;
const postId = parseInt(card.getAttribute('data-post-id'), 10);
@@ -396,10 +380,9 @@
const original = button.innerHTML;
button.innerHTML = ' 处理中…';
- const restNonce = window.aksrtData.restNonce || '';
- const captchaPayload = collectCaptchaPayload(button);
+ const restNonce = window.aksrtData.restNonce || '';
- fetch(REST_BASE + '/download/request', {
+ fetch(REST_BASE + '/download/request', {
method: 'POST',
credentials: 'same-origin',
headers: {
@@ -416,28 +399,26 @@
.then(function (r) {
button.disabled = false;
button.innerHTML = original;
- if (!r.ok || (r.json && r.json.code)) {
- renderError(r.json.message || '请稍后重试', r.json.data || {}, button, r.json.code);
- // 验证码已被消费,重置以便用户重试
- try {
- const scene = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes && window.AKSRTCaptchaState.scenes.download;
- if (scene && typeof scene.reset === 'function') scene.reset();
- } catch (_) { /* ignore */ }
- return;
- }
+ if (!r.ok || (r.json && r.json.code)) {
+ renderError(r.json.message || '请稍后重试', r.json.data || {}, button, r.json.code);
+ return;
+ }
renderSuccess(r.json, button);
})
- .catch(function () {
- button.disabled = false;
- button.innerHTML = original;
- renderError('网络错误,请稍后重试', {}, button);
- });
- }
+ .catch(function () {
+ button.disabled = false;
+ button.innerHTML = original;
+ renderError('网络错误,请稍后重试', {}, button);
+ })
+ .finally(function () {
+ if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('download', captchaScope(button));
+ });
+ }
- function requestDownload(button) {
- ensureCaptchaThen(button, function () {
- performDownloadRequest(button);
- });
+ function requestDownload(button) {
+ ensureCaptchaThen(button, function (captchaPayload) {
+ performDownloadRequest(button, captchaPayload);
+ });
}
function pollOrderPaid(orderNumber, returnFocusEl) {
diff --git a/assets/js/core/comments.js b/assets/js/core/comments.js
index 5e09132e..6ca3e8e0 100644
--- a/assets/js/core/comments.js
+++ b/assets/js/core/comments.js
@@ -192,8 +192,8 @@
}
}
- // 执行实际的 AJAX 请求 — local insert instead of full-page re-fetch
- function performSubmit() {
+ // 执行实际的 REST 请求 — local insert instead of full-page re-fetch
+ function performSubmit(captchaPayload) {
setLoading(true);
var formData = collectFormData();
@@ -205,6 +205,7 @@
// Try REST endpoint for local-insert HTML response
var restBody = {};
formData.forEach(function (v, k) { restBody[k] = v; });
+ Object.assign(restBody, captchaPayload || {});
if (isEdit) {
restBody.comment_id = parseInt(editId.value, 10);
restBody.content = restBody.comment || restBody.content || '';
@@ -245,15 +246,6 @@
var replyInfo = document.getElementById('comment-reply-info');
if (replyInfo) replyInfo.style.display = 'none';
- // Reset captcha after successful submit
- try {
- var captchaScene = window.AKSRTCaptchaState &&
- window.AKSRTCaptchaState.scenes &&
- window.AKSRTCaptchaState.scenes.comment;
- if (captchaScene && typeof captchaScene.reset === 'function') {
- captchaScene.reset();
- }
- } catch (e) { /* ignore */ }
// Wait time countdown
if (json.wait_time && json.wait_time > 0) {
@@ -282,14 +274,6 @@
if (json && json.code) {
notyf(json.message || '评论提交失败', 'danger');
- try {
- var failScene = window.AKSRTCaptchaState &&
- window.AKSRTCaptchaState.scenes &&
- window.AKSRTCaptchaState.scenes.comment;
- if (failScene && typeof failScene.reset === 'function') {
- failScene.reset();
- }
- } catch (e) { /* ignore */ }
if (json.wait_time) startCountdown(json.wait_time);
else setLoading(false);
return;
@@ -299,9 +283,14 @@
setLoading(false);
setTimeout(function () { reloadCommentList(); }, 500);
})
- .catch(function () {
- // Fallback: submit to wp-comments-post.php directly
- performLegacySubmit();
+ .catch(function (error) {
+ console.error('[AKSRT Comment] 提交失败:', error);
+ notyf((error && error.message) || '网络连接失败,请检查网络后重试', 'danger');
+ setLoading(false);
+ })
+ .finally(function () {
+ if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('comment', form);
+ if (!countdownTimer && isSubmitting) setLoading(false);
});
}
@@ -373,44 +362,6 @@
placeholder.remove();
}
}
- // Legacy fallback: POST to wp-comments-post.php
- function performLegacySubmit() {
- var formData = collectFormData();
- var actionUrl = form.getAttribute('action') || '/wp-comments-post.php';
-
- fetch(actionUrl, {
- method: 'POST',
- body: formData,
- credentials: 'same-origin',
- headers: { 'X-Requested-With': 'XMLHttpRequest' }
- })
- .then(function (response) {
- return response.text().then(function (text) {
- return { status: response.status, text: text, ok: response.ok };
- });
- })
- .then(function (result) {
- handleResponse(result.text, result.status);
- })
- .catch(function (error) {
- console.error('[AKSRT Comment] 提交失败:', error);
-
- if (error.message === 'Failed to fetch' || error.name === 'TypeError') {
- notyf('网络连接失败,请检查网络后重试', 'danger');
- } else {
- notyf(error.message || '评论提交失败,请重试', 'danger');
- }
-
- setLoading(false);
- })
- .finally(function () {
- // 兜底:避免异常分支漏掉状态恢复导致按钮卡在"发送中"
- if (!countdownTimer && isSubmitting) {
- setLoading(false);
- }
- });
- }
-
// 核心表单提交事件处理器
function handleSubmit(e) {
// 如果已经在提交中或倒计时中,防止重复提交
@@ -423,29 +374,20 @@
// 阻止默认表单提交
e.preventDefault();
e.stopImmediatePropagation();
- // 检查验证码状态(如果有验证码组件)
- var captchaScene = window.AKSRTCaptchaState &&
- window.AKSRTCaptchaState.scenes &&
- window.AKSRTCaptchaState.scenes.comment;
-
- if (captchaScene && typeof captchaScene.verify === 'function') {
- // 有验证码:先触发验证流程
- setLoading(true);
-
- captchaScene.verify(function (verified) {
- if (verified !== false) {
- // 验证通过,执行 AJAX 提交
- performSubmit();
- } else {
- // 验证失败
- notyf('人机验证未通过,请重试', 'warning');
- setLoading(false);
- }
- });
- } else {
- // 无验证码或已经验证过:直接提交
- performSubmit();
- }
+ var captcha = window.AKSRTCaptcha;
+ setLoading(true);
+ (captcha ? captcha.run('comment', form) : Promise.resolve({})).then(function (captchaPayload) {
+ if (captchaPayload === null) {
+ notyf('人机验证未通过,请重试', 'warning');
+ setLoading(false);
+ return;
+ }
+ performSubmit(captchaPayload);
+ }).catch(function () {
+ notyf('人机验证未通过,请重试', 'warning');
+ setLoading(false);
+ if (captcha) captcha.reset('comment', form);
+ });
return false;
}
diff --git a/assets/js/front-submit.js b/assets/js/front-submit.js
index 2e04b312..e1e4c198 100644
--- a/assets/js/front-submit.js
+++ b/assets/js/front-submit.js
@@ -196,10 +196,20 @@
submit: function (btn, action) {
var self = this;
- if (action !== 'posts_draft' && !this.requestCaptcha(function () {
- self.submit(btn, action);
- })) {
- return;
+ if (action !== 'posts_draft') {
+ var captcha = window.AKSRTCaptcha;
+ if (captcha && !btn.dataset.aksrtCaptchaReady) {
+ captcha.run('front_submit', this.form).then(function (payload) {
+ if (payload === null) return;
+ btn.dataset.aksrtCaptchaReady = '1';
+ self.submit(btn, action);
+ }).catch(function () {
+ captcha.reset('front_submit', self.form);
+ notyf(i18n.error || '人机验证失败,请重试', 'danger');
+ });
+ return;
+ }
+ delete btn.dataset.aksrtCaptchaReady;
}
this.isSubmitting = true;
var toastId = action === 'posts_draft'
@@ -292,28 +302,14 @@
btn.disabled = false;
btn.innerHTML = original;
self.isSubmitting = false;
+ })
+ .finally(function () {
+ if (action !== 'posts_draft' && window.AKSRTCaptcha) {
+ window.AKSRTCaptcha.reset('front_submit', self.form);
+ }
});
},
- requestCaptcha: function (onVerified) {
- var verified = this.form.querySelector('input[name="captcha_verified"]');
- if (verified && verified.value === '1') {
- return true;
- }
- var captcha = window.AKSRTCaptchaState &&
- window.AKSRTCaptchaState.scenes &&
- window.AKSRTCaptchaState.scenes.front_submit;
- if (!captcha || typeof captcha.verify !== 'function') {
- return true;
- }
- captcha.verify(function (passed) {
- if (passed) {
- onVerified();
- }
- });
- return false;
- },
-
bindDraftDelete: function () {
document.addEventListener('click', function (e) {
var btn = e.target.closest('.aksrt-submit-drafts__delete');
diff --git a/assets/js/routes/user-center.js b/assets/js/routes/user-center.js
index de849401..2ddef274 100644
--- a/assets/js/routes/user-center.js
+++ b/assets/js/routes/user-center.js
@@ -22,6 +22,24 @@
var tabAliases = cfg.tabAliases || {};
var tabUrls = cfg.tabUrls || {};
+ function captchaAvailable(scene, scope) {
+ var captcha = window.AKSRTCaptcha;
+ if (!captcha) return false;
+ var element = scope && scope.nodeType ? scope : null;
+ if (element && element.matches && element.matches('[data-aksrt-captcha-config][data-scene="' + scene + '"]')) return true;
+ if (element && element.querySelector && element.querySelector('[data-aksrt-captcha-config][data-scene="' + scene + '"]')) return true;
+ var mount = element && element.closest ? element.closest('[data-aksrt-captcha-config][data-scene="' + scene + '"]') : null;
+ return !!mount;
+ }
+
+ function runCaptcha(scene, scope) {
+ return window.AKSRTCaptcha ? window.AKSRTCaptcha.run(scene, scope) : Promise.resolve({});
+ }
+
+ function resetCaptcha(scene, scope) {
+ if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset(scene, scope);
+ }
+
/* ==================== Dashboard 导航 ==================== */
function initDashboardNavigation() {
var tabContainer = document.querySelector('.uc-mobile-tabs');
@@ -653,7 +671,6 @@
var f = m.querySelector('form');
var sceneMap = { 'pwd-modal': 'change_password', 'email-modal': 'change_email', 'phone-modal': 'bind_phone' };
var scene = m.id && sceneMap[m.id] ? sceneMap[m.id] : '';
- var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes ? window.AKSRTCaptchaState.scenes : null;
if (f) {
f.reset();
var bar = document.getElementById('pwd-strength-bar');
@@ -661,9 +678,7 @@
if (bar && bar.firstChild) { bar.style.width = '0'; bar.firstChild.className = 'pwd-strength__fill'; }
if (txt) { txt.textContent = ''; txt.className = 'pwd-strength__text'; }
}
- if (scene && store && store[scene] && typeof store[scene].reset === 'function') {
- store[scene].reset();
- }
+ if (scene) resetCaptcha(scene, f || m);
}
root.querySelectorAll('.security-trigger').forEach(function(btn) {
@@ -725,16 +740,6 @@
}
// 验证码发送
- function triggerSceneCaptcha(scene, callback) {
- var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes ? window.AKSRTCaptchaState.scenes : null;
- var instance = store && store[scene] ? store[scene] : null;
- if (instance && typeof instance.verify === 'function') {
- instance.verify(callback);
- return;
- }
- callback();
- }
-
function getSendBtnIdleText(btnId) {
if (btnId === 'pwd-send-code') return i18n.sendToCurrentEmail || '发送到当前邮箱';
if (btnId === 'email-send-code') return i18n.sendToNewEmail || '发送到新邮箱';
@@ -753,20 +758,22 @@
var toastId = scene === 'change_password'
? 'aksrt-settings-password-code-toast'
: 'aksrt-settings-email-code-toast';
- triggerSceneCaptcha(scene, function() {
- self.disabled = true;
+ runCaptcha(scene, form).then(function(captchaPayload) {
+ if (captchaPayload === null) return;
+ self.disabled = true;
self.textContent = i18n.sending || '发送中...';
notyf(i18n.sending || '正在发送验证码…', 'load', '', toastId);
var data = new URLSearchParams();
data.set('action', 'aksrt_send_email_code');
data.set('_settings_nonce', settingsNonce);
data.set('scene', scene || '');
- if (form) {
- new FormData(form).forEach(function(value, key) {
- if (data.has(key) || value === '' || value == null) return;
- data.set(key, value);
- });
- }
+ if (form) {
+ new FormData(form).forEach(function(value, key) {
+ if (data.has(key) || value === '' || value == null) return;
+ data.set(key, value);
+ });
+ }
+ Object.keys(captchaPayload || {}).forEach(function(key) { data.set(key, captchaPayload[key]); });
fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data.toString() })
.then(function(r) { return r.json(); }).then(function(res) {
if (res.success) {
@@ -791,12 +798,18 @@
self.textContent = getSendBtnIdleText(btnId);
}
})
- .catch(function() {
- notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
- self.disabled = false;
- self.textContent = getSendBtnIdleText(btnId);
+ .catch(function() {
+ notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
+ self.disabled = false;
+ self.textContent = getSendBtnIdleText(btnId);
+ })
+ .finally(function() {
+ resetCaptcha(scene, form);
+ });
+ }).catch(function() {
+ resetCaptcha(scene, form);
+ notyf('人机验证失败,请重试', 'danger', 5, toastId);
});
- });
});
}
@@ -829,15 +842,24 @@
return;
}
}
- if (btn) { btn.disabled = true; btn.textContent = i18n.submitting || '提交中...'; }
- notyf(i18n.submitting || '正在提交…', 'load', '', toastId);
- var d = new FormData(fm);
- d.set('action', act);
- d.set('_settings_nonce', settingsNonce);
- fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: d })
- .then(function(r) { return r.json(); })
- .then(function(r) {
- if (btn) { btn.disabled = false; btn.textContent = orig; }
+ if (btn) { btn.disabled = true; btn.textContent = i18n.submitting || '提交中...'; }
+ notyf(i18n.submitting || '正在提交…', 'load', '', toastId);
+ var scene = fid === 'pwd-form' ? 'change_password' : 'change_email';
+ runCaptcha(scene, fm).then(function(captchaPayload) {
+ if (captchaPayload === null) {
+ if (btn) { btn.disabled = false; btn.textContent = orig; }
+ return null;
+ }
+ var d = new FormData(fm);
+ d.set('action', act);
+ d.set('_settings_nonce', settingsNonce);
+ Object.keys(captchaPayload || {}).forEach(function(key) { d.set(key, captchaPayload[key]); });
+ return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: d });
+ })
+ .then(function(r) { return r ? r.json() : null; })
+ .then(function(r) {
+ if (!r) return;
+ if (btn) { btn.disabled = false; btn.textContent = orig; }
if (r.success) {
notyf((r.data && r.data.message) || '操作成功', 'success', 5, toastId);
closeModal(fm.closest('.aksrt-modal'));
@@ -846,17 +868,64 @@
notyf((r.data && r.data.message) || i18n.operationFailed || '操作失败', 'danger', 5, toastId);
}
})
- .catch(function() {
- if (btn) { btn.disabled = false; btn.textContent = orig; }
- notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
- });
+ .catch(function() {
+ if (btn) { btn.disabled = false; btn.textContent = orig; }
+ notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
+ })
+ .finally(function() {
+ resetCaptcha(scene, fm);
+ });
});
}
submitForm('pwd-form', 'aksrt_change_password');
submitForm('email-form', 'aksrt_change_email');
- // 解绑
+ root.querySelectorAll('.binding-item--linkable').forEach(function(link) {
+ if (link.dataset.aksrtBindCaptchaInitialized === 'true') return;
+ link.dataset.aksrtBindCaptchaInitialized = 'true';
+ link.addEventListener('click', function(event) {
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
+ var startUrl = link.getAttribute('data-bind-start-url') || link.getAttribute('href') || '';
+ var provider = link.getAttribute('data-provider') || link.getAttribute('data-bind-provider') || '';
+ var scope = link.closest('[data-aksrt-captcha-scope]') || root;
+ if (!startUrl || !captchaAvailable('bind', scope)) return;
+ event.preventDefault();
+ var restUrl = String(cfg.restUrl || (window.aksrtData && (aksrtData.restUrl || aksrtData.apiUrl)) || '').replace(/\/$/, '');
+ var nonce = cfg.restNonce || (window.aksrtData && (aksrtData.restNonce || aksrtData.nonce)) || '';
+ var redirectTo = (cfg.tabUrls && cfg.tabUrls.settings) || window.location.href;
+ if (!restUrl) { window.location.assign(startUrl); return; }
+ link.classList.add('is-loading');
+ runCaptcha('bind', scope).then(function(captchaPayload) {
+ if (captchaPayload === null) return null;
+ return fetch(restUrl + '/oauth/bind-pass', {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
+ body: JSON.stringify(Object.assign({
+ provider: provider,
+ redirect_to: redirectTo
+ }, captchaPayload || {}))
+ });
+ }).then(function(response) {
+ if (!response) return null;
+ return response.json().catch(function() { return {}; }).then(function(payload) {
+ if (!response.ok || !payload || payload.success === false) throw new Error(payload.message || '绑定验证失败');
+ return payload.data || payload;
+ });
+ }).then(function(payload) {
+ if (payload && payload.url) window.location.assign(payload.url);
+ }).catch(function(error) {
+ notyf(error.message || '绑定验证失败,请重试', 'danger');
+ }).finally(function() {
+ link.classList.remove('is-loading');
+ resetCaptcha('bind', scope);
+ });
+ });
+ });
+
+ // 解绑
+
root.querySelectorAll('.binding-unbind-btn').forEach(function(b) {
if (b.dataset.aksrtSettingsInitialized === 'true') return;
b.dataset.aksrtSettingsInitialized = 'true';
@@ -939,8 +1008,9 @@
provider_error: '第三方授权失败,请稍后重试',
callback_failed: '第三方授权失败,请稍后重试',
login_failed: '第三方登录失败,请重试',
- bind_failed: '第三方账号绑定失败,请重试',
- account_conflict: '该第三方账号无法绑定到当前账号',
+ bind_failed: '第三方账号绑定失败,请重试',
+ bind_verification_required: '请先完成人机验证后再绑定',
+ account_conflict: '该第三方账号无法绑定到当前账号',
already_bound: '该第三方账号已绑定其他账号',
timeout: '第三方授权已超时,请重试'
};
@@ -1016,16 +1086,24 @@
var nonce = (window.aksrtData && aksrtData.restNonce) || '';
sendButton.disabled = true;
notyf('正在发送验证码…', 'load', '', toastId);
- fetch(String(restRoot).replace(/\/$/, '') + '/user/phone-code', {
- method: 'POST', credentials: 'same-origin',
- headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
- body: JSON.stringify({ phone: phone })
- }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, json: j }; }); }).then(function (res) {
+ runCaptcha('bind_phone', form).then(function(captchaPayload) {
+ if (captchaPayload === null) {
+ sendButton.disabled = false;
+ return null;
+ }
+ return fetch(String(restRoot).replace(/\/$/, '') + '/user/phone-code', {
+ method: 'POST', credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
+ body: JSON.stringify(Object.assign({ phone: phone }, captchaPayload || {}))
+ });
+ }).then(function (r) { return r ? r.json().then(function (j) { return { ok: r.ok, json: j }; }) : null; }).then(function (res) {
+ if (!res) return;
if (!res.ok || !res.json.success) throw new Error(res.json.message || '发送失败');
notyf(res.json.message || '验证码已发送', 'success', 5, toastId);
var left = Number((res.json.data && res.json.data.countdown) || 60);
var timer = setInterval(function () { left -= 1; sendButton.textContent = left + 's'; if (left <= 0) { clearInterval(timer); sendButton.disabled = false; sendButton.textContent = '获取验证码'; } }, 1000);
- }).catch(function (error) { sendButton.disabled = false; notyf(error.message || '发送失败', 'danger', 5, toastId); });
+ }).catch(function (error) { sendButton.disabled = false; notyf(error.message || '发送失败', 'danger', 5, toastId); })
+ .finally(function () { resetCaptcha('bind_phone', form); });
});
form.addEventListener('submit', function (e) {
e.preventDefault();
@@ -1046,33 +1124,12 @@
submitBtn.disabled = true;
submitBtn.textContent = '绑定中...';
}
- function collectCaptcha(formEl) {
- var out = {};
- [
- 'captcha_verified', 'tencent_ticket', 'tencent_randstr', 'tencent_sid',
- 'cf_turnstile_response', 'geetest_lot_number', 'geetest_captcha_output',
- 'geetest_pass_token', 'geetest_gen_time', 'aliyun_captcha_verify_param'
- ].forEach(function (name) {
- var el = formEl.querySelector('[name="' + name + '"]');
- if (el && el.value) out[name] = el.value;
- });
- return out;
- }
- function resetCaptcha() {
- try {
- var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes;
- if (store && store.bind_phone && typeof store.bind_phone.reset === 'function') {
- store.bind_phone.reset();
- }
- } catch (err) {}
- }
- function doBind() {
- var toastId = 'aksrt-settings-phone-bind-toast';
- notyf('正在绑定手机…', 'load', '', toastId);
- var captcha = collectCaptcha(form);
- var restRoot = (window.aksrtData && aksrtData.apiUrl) || '/wp-json/aksrt/v1';
- var nonce = (window.aksrtData && aksrtData.restNonce) || '';
- fetch(String(restRoot).replace(/\/$/, '') + '/user/phone', {
+ function doBind(captcha) {
+ var toastId = 'aksrt-settings-phone-bind-toast';
+ notyf('正在绑定手机…', 'load', '', toastId);
+ var restRoot = (window.aksrtData && aksrtData.apiUrl) || '/wp-json/aksrt/v1';
+ var nonce = (window.aksrtData && aksrtData.restNonce) || '';
+ return fetch(String(restRoot).replace(/\/$/, '') + '/user/phone', {
method: 'POST',
credentials: 'same-origin',
headers: {
@@ -1088,28 +1145,32 @@
setTimeout(function () { window.location.reload(); }, 600);
return;
}
- resetCaptcha();
notyf((res.json && (res.json.message || res.json.error)) || '绑定失败', 'danger', 5, toastId);
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '确认绑定';
- }
- })
- .catch(function () {
- resetCaptcha();
- notyf('网络错误', 'danger', 5, toastId);
+ }
+ })
+ .catch(function () {
+ notyf('网络错误', 'danger', 5, toastId);
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '确认绑定';
}
- });
- }
- var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes;
- if (store && store.bind_phone && typeof store.bind_phone.verify === 'function') {
- store.bind_phone.verify(doBind);
- } else {
- doBind();
- }
+ });
+ }
+ runCaptcha('bind_phone', form).then(function(captchaPayload) {
+ if (captchaPayload === null) {
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = '确认绑定';
+ }
+ return null;
+ }
+ return doBind(captchaPayload);
+ }).finally(function() {
+ resetCaptcha('bind_phone', form);
+ });
});
}
diff --git a/assets/js/signin.js b/assets/js/signin.js
index adba6991..a6ea0a4e 100644
--- a/assets/js/signin.js
+++ b/assets/js/signin.js
@@ -101,60 +101,14 @@
return document.querySelector('[data-signin-captcha]');
}
- function collectCaptcha() {
- var mount = captchaMount();
- if (!mount) return {};
- var payload = {};
- var keys = [
- 'captcha_verified', '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'
- ];
- keys.forEach(function (key) {
- var input = mount.querySelector('input[name="' + key + '"]');
- if (input && input.value !== '') payload[key] = input.value;
- });
- return payload;
- }
-
- function resetCaptcha() {
- var state = window.AKSRTCaptchaState;
- var scene = state && state.scenes && state.scenes.signin;
- if (scene && typeof scene.reset === 'function') scene.reset();
- }
-
- function triggerCaptcha(onSuccess, onFail) {
- var mount = captchaMount();
- var state = window.AKSRTCaptchaState;
- var scene = state && state.scenes && state.scenes.signin;
-
- if (scene && typeof scene.verify === 'function') {
- scene.verify(function (ok) {
- if (ok === false) {
- onFail('已取消验证');
- return;
- }
- onSuccess();
- });
- return;
- }
- if (mount) {
- onFail('人机验证加载中,请稍候再试');
- return;
- }
- onSuccess();
- }
-
- function doSignin() {
+ function doSignin(captchaPayload) {
notyf('正在签到…', 'load', '', 'aksrt-balance-signin-toast');
api('/signin', {
method: 'POST',
- body: JSON.stringify({ captcha: collectCaptcha() })
+ body: JSON.stringify({ captcha: captchaPayload || {} })
}).then(function (result) {
signinBusy = false;
if (!result.ok) {
- resetCaptcha();
notyf((result.json && result.json.message) || '签到失败', 'danger', 5, 'aksrt-balance-signin-toast');
setButtonState('idle');
return;
@@ -166,6 +120,8 @@
});
setButtonState('done');
refresh();
+ }).finally(function () {
+ if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('signin', captchaMount());
});
}
@@ -176,10 +132,15 @@
signinBusy = true;
setButtonState('busy');
- triggerCaptcha(doSignin, function (message) {
+ var captcha = window.AKSRTCaptcha;
+ var mount = captchaMount();
+ (captcha ? captcha.run('signin', mount) : Promise.resolve({})).then(function (payload) {
+ if (payload === null) throw new Error('已取消验证');
+ doSignin(payload);
+ }).catch(function (error) {
signinBusy = false;
- resetCaptcha();
- if (message) notyf(message, 'danger');
+ if (captcha) captcha.reset('signin', mount);
+ notyf((error && error.message) || '人机验证加载中,请稍候再试', 'danger');
setButtonState('idle');
});
});
diff --git a/inc/helpers/user-center-renderers/settings-bindings.php b/inc/helpers/user-center-renderers/settings-bindings.php
index f8974c16..258838ba 100644
--- a/inc/helpers/user-center-renderers/settings-bindings.php
+++ b/inc/helpers/user-center-renderers/settings-bindings.php
@@ -48,7 +48,7 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
];
}
?>
-