优化了一些问题fix(captcha)
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# 第三方人机验证优化改造计划
|
||||
|
||||
约束:不写双路/兼容迁移代码;顺手清理死代码;不做真实视觉效果验收(用语法检查、契约测试、构建产物校验代替)。
|
||||
已确认决策:范围全做(P0+P1+P2);下载场景 pass 放行 10 分钟;凭据不齐时仅加后台警告、前台保持放行。
|
||||
|
||||
## 改动文件总览
|
||||
|
||||
| 文件 | 动作 |
|
||||
|---|---|
|
||||
| `inc/modules/captcha.php` | 重写校验层 + 瘦身渲染 + 后台警告 + 静态缓存 |
|
||||
| `assets/js/captcha-front.js` | 新建(从内联脚本迁移,~340 行) |
|
||||
| `assets/css/shared/captcha.css` | 保留不动(改为进 bundle) |
|
||||
| `tools/build-public-assets.mjs` | publicScripts 加 captcha-front.js;public.css 加 captcha.css |
|
||||
| `src/Admin/Schema/SchemaPermissions.php` | 删除 'mode' 死字段(L32-34) |
|
||||
| `assets/admin/src/schema/sections/permissions/index.js` | 删除 'mode' 死字段(L18 defaults、L50-56) |
|
||||
| `src/Admin/SchemaProvider.php` | L403 白名单删 'mode';保存后 flush 配置静态缓存 |
|
||||
| `tests/widget-system/captcha-cloud-contract.test.cjs` | 新建契约测试 |
|
||||
|
||||
不动的文件及原因:`aksrt-header-userbox.php`(toast-contract 测试锁定 signin 接线)、`data-aksrt-download-captcha` 挂载点(`templates/parts/resource-download-box.php:467`、`inc/helpers/aksrt-pay.php:1586`)、所有业务 JS(auth/signin/comments/cms-download/user-center/front-submit 只消费 `AKSRTCaptchaState.scenes[scene]` 与 mount 内 token input,接口不变即零改动)。
|
||||
|
||||
## 1. inc/modules/captcha.php — 校验层
|
||||
|
||||
**P0-1 腾讯云改走 WP HTTP API**:`aksrt_verify_tencent()` 删除 curl 块(L374-390,含 `CURLOPT_SSL_VERIFYPEER => false`),改用 `wp_remote_post('https://captcha.tencentcloudapi.com/', ['timeout'=>5, 'headers'=>aksrt_tencent_tc3_sign(...)生成的一维数组, 'body'=>$payload])`,与另外三家一致。签名函数不动。
|
||||
|
||||
**P1-5 错误信息收敛**:四家服务商的传输层错误(wp_remote_post 返回 WP_Error、响应不可解析)统一 `error_log('[AKSRT Captcha] {provider}: ' . 详情)` 后返回通用文案 `__('人机验证服务暂不可用,请稍后重试', 'aksrt')`;保留对用户有行动意义的映射文案(腾讯 CaptchaCode 表、阿里 F0xx 表)。腾讯 API Error 分支(签名错误等)也走 log+通用文案。
|
||||
|
||||
**P1-4**:阿里云 `SignatureNonce` 由 `uniqid('', true)` 改 `wp_generate_uuid4()`。
|
||||
|
||||
**P1-8**:四家 `'timeout'` 统一 5 秒(现 10 秒)。
|
||||
|
||||
**P1-6 Turnstile 绑定场景**:`aksrt_verify_captcha($post_data, $scene = '')` 增加第二参数(唯一调用方是 `aksrt_require_captcha`,无散落调用点);Cloudflare 成功响应校验:`action !== 'aksrt-verify'` 或 `cdata !== $scene`(存在才比对,lenient)→ 失败。客户端配套见 §2。
|
||||
|
||||
**P0-2 下载 pass 放行(10 分钟)**:`aksrt_require_captcha()` 内,scene 命中放行名单时先查 transient、验证通过后写 transient:
|
||||
- `aksrt_captcha_pass_ttl($scene)`:仅 `download` 返回 `10 * MINUTE_IN_SECONDS`,其余返回 0(单一代码路径,无散落分支)
|
||||
- key = `'aksrt_captcha_pass_' . md5($scene . '|' . get_current_user_id() . '|' . aksrt_get_client_ip() . '|' . md5(UA))`,仿 `aksrt_auth_rate_` 既有模式
|
||||
|
||||
## 2. assets/js/captcha-front.js — 新建(P2-9 + P1-7 客户端)
|
||||
|
||||
把 `aksrt_render_captcha()` 内联 `<script>`(L632-969)整体迁移为静态文件,逻辑不改名不重写:`loadScriptOnce`、四家 `load*`、`AKSRTCaptchaState.scenes[scene].verify/reset` 注册、`setField/resetVerificationState/syncDownloadSiblings` 等原样搬移。仅两处行为修正:
|
||||
- 极验 `onClose` 空回调改为 `cancelPending('已取消验证')`(P1-7,与腾讯/CF 取消体验对齐)
|
||||
- Turnstile render 配置加 `cdata: C.scene`(P1-6 客户端配套)
|
||||
|
||||
激活模型:扫描 `document.querySelectorAll('[data-aksrt-captcha-config]')`,`JSON.parse(el.getAttribute(...))` 逐个 init;DOMContentLoaded 后再补扫一次并暴露 `window.AKSRTCaptchaState.rescan()` 供未来 ajax 注入使用。保持 vanilla JS、无新依赖。
|
||||
|
||||
对应 PHP 渲染端瘦身:`aksrt_render_captcha()` 只输出
|
||||
1. 原 wrapper 结构不变(`.aksrt-captcha-wrap` + `#uid` box + `#vid` captcha_verified 隐藏域),
|
||||
2. wrapper 上挂 `data-aksrt-captcha-config`(`esc_attr(wp_json_encode($js_config))`),
|
||||
3. 删除内联 `<script>` 与逐次 `<link rel="stylesheet">`。
|
||||
|
||||
TOKEN_FIELDS 常量、i18n 字符串原样进 JS 文件(与仓库其它 bundle 内硬编码中文一致)。
|
||||
|
||||
## 3. 构建与后台(P2-10 + bundle 接线)
|
||||
|
||||
- `tools/build-public-assets.mjs`:`publicScripts` 追加 `'assets/js/captcha-front.js'`;`cssBundles['public.css']` 追加 `'assets/css/shared/captcha.css'`。走既有 bundle 版本机制,无需新增 enqueue 代码。
|
||||
- `SchemaPermissions.php`:删除 'mode' 字段定义(L32-34,单选项 always 的死字段)。
|
||||
- `assets/admin/src/schema/sections/permissions/index.js`:删除 `mode` 的 defaults(L18)与字段定义(L50-56)。
|
||||
- `SchemaProvider.php` L403:归一化白名单去掉 `'mode'`;`write_section_settings()` 更新 `aksrt_captcha_config` 后调用新助手 `aksrt_captcha_config_reset()` 清静态缓存(function_exists 守卫,与现有 captcha 专用块并排)。
|
||||
- DB 里残留的 `mode` 键不做迁移清洗(读路径白名单已忽略它,符合"不做迁移"约束)。
|
||||
|
||||
## 4. inc/modules/captcha.php — 后台警告与缓存
|
||||
|
||||
- **P0-3**:新增顶层 `add_action('admin_notices', ...)`:`manage_options` 权限 + `enabled` 为真 + `active_provider` 非空 + `aksrt_captcha_provider_is_configured()` 为假时,输出 `notice-error` 横幅,文案指向「功能&权限 → 人机验证」补全配置(复用现有 misconfig 提示文案)。前台行为不变(保持放行)。
|
||||
- **P2-11**:`aksrt_get_captcha_config()` 加 `static $cache`;新增 `aksrt_captcha_config_reset()`;写入路径(SchemaProvider)调用 reset。`aksrt_get_captcha_status_summary()` 的 12 场景循环随之受益。
|
||||
|
||||
## 5. 新契约测试 tests/widget-system/captcha-cloud-contract.test.cjs
|
||||
|
||||
仿 toast-contract 的正则断言风格:
|
||||
- `captcha.php`:不匹配 `curl_init|CURLOPT_SSL_VERIFYPEER|machine-verification|slidercaptcha|imagecaptcha`;匹配 `wp_remote_post`(≥4 处)、`wp_generate_uuid4`、`'timeout'\s*=>\s*5`、`aksrt_captcha_pass_`;渲染函数不匹配 `/<script|<link/`(内联抽取完成)
|
||||
- `captcha-front.js` 存在且含 `AKSRTCaptchaState`、`data-aksrt-captcha-config`
|
||||
- `build-public-assets.mjs` 的 publicScripts 含 captcha-front.js、public.css 含 shared/captcha.css
|
||||
- `SchemaPermissions.php` 与 admin `index.js` 的 captcha 段不匹配 `mode`
|
||||
- `SchemaProvider.php` 不匹配 `'mode'`
|
||||
|
||||
## 6. 验证(不做视觉验收)
|
||||
|
||||
1. `php -l` 全部改动的 PHP 文件
|
||||
2. `node --check` captcha-front.js
|
||||
3. `node tools/build-public-assets.mjs` 重建 bundle
|
||||
4. `grep` 全仓清扫:无 `curl_init`、无 `'mode'`(captcha 相关文件)、无 legacy 标记
|
||||
5. `node --test tests/widget-system/*.test.cjs` 全量跑,预期仅存量 page-display-contract 一项失败(与本次无关,已验证在 HEAD 上同样失败)
|
||||
|
||||
## 明确不做
|
||||
|
||||
- 不做视觉验收/浏览器实测(按要求)
|
||||
- 不写新旧配置迁移、不清洗 DB 残留 `mode` 键
|
||||
- 不动腾讯 trerror 容灾票据策略(维持拒绝)
|
||||
- 不做阿里云签名 V3 升级、Turnstile appearance/retry 配置项(列为后续可选项)
|
||||
- 不动 `ExtensionController` 的 extensions 状态端点(SPA 未消费属另一话题)
|
||||
|
||||
## 建议提交拆分(如需提交)
|
||||
|
||||
1. `fix(captcha): use WP HTTP API with TLS verification for tencent`
|
||||
2. `feat(captcha): short-lived download pass + turnstile binding + error hardening`
|
||||
3. `refactor(captcha): extract inline runtime to captcha-front.js bundle`
|
||||
4. `chore(captcha): drop dead mode field, config cache, contract tests`
|
||||
@@ -15,7 +15,6 @@ export const permissionsSchemas = {
|
||||
defaults: {
|
||||
enabled: false,
|
||||
active_provider: '',
|
||||
mode: 'always',
|
||||
enabled_scenarios: [
|
||||
'comment', 'login', 'register', 'bind', 'bind_phone',
|
||||
'lostpassword', 'change_password', 'change_email', 'download', 'signin',
|
||||
@@ -46,16 +45,6 @@ export const permissionsSchemas = {
|
||||
],
|
||||
dependsOn: { key: 'enabled', value: true },
|
||||
},
|
||||
{
|
||||
key: 'mode',
|
||||
label: '触发模式',
|
||||
type: 'radio',
|
||||
buttonStyle: false,
|
||||
options: [
|
||||
{ label: '始终显示', value: 'always' },
|
||||
],
|
||||
dependsOn: { key: 'enabled', value: true },
|
||||
},
|
||||
{
|
||||
key: 'enabled_scenarios',
|
||||
label: '启用场景',
|
||||
|
||||
@@ -7748,3 +7748,27 @@ body.aksrt-modal-runtime-open {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* source: assets/css/shared/captcha.css */
|
||||
/**
|
||||
* AKSRT Captcha Styles
|
||||
* 人机验证组件样式(从 functions.php 内联 CSS 抽取)
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
.aksrt-captcha-box{display:none}.aksrt-captcha-wrap{text-align:center}
|
||||
|
||||
/* Cloudflare Turnstile Modal */
|
||||
.aksrt-captcha-modal-overlay{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center}
|
||||
.aksrt-captcha-modal-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.55);animation:aksrtFadeIn .2s ease}
|
||||
.aksrt-captcha-modal-box{position:relative;background:var(--surface-page,#fff);border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.3);width:90%;max-width:380px;animation:aksrtSlideUp .25s ease;overflow:hidden}
|
||||
.aksrt-captcha-modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--color-line,#e5e7eb)}
|
||||
.aksrt-captcha-modal-title{font-size:15px;font-weight:600;color:var(--color-ink,#1f2937)}
|
||||
.aksrt-captcha-modal-close{background:none;border:none;font-size:22px;color:var(--color-ink-4,#9ca3af);cursor:pointer;padding:0;line-height:1;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:6px}.aksrt-captcha-modal-close:hover{background:var(--color-bg-hover,#f3f4f6);color:var(--color-ink,#1f2937)}
|
||||
.aksrt-captcha-modal-body{padding:20px}
|
||||
.aksrt-captcha-modal-hint{font-size:13px;color:var(--color-ink-4,#9ca3af);margin:0 0 16px;text-align:center}
|
||||
.aksrt-captcha-modal-widget{display:flex;justify-content:center}
|
||||
|
||||
@keyframes aksrtFadeIn{from{opacity:0}to{opacity:1}}
|
||||
@keyframes aksrtSlideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* AKSRT Captcha front-end runtime — cloud providers only (tencent / aliyun / cloudflare / geetest).
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var TOKEN_FIELDS = [
|
||||
'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'
|
||||
];
|
||||
|
||||
var root = (window.AKSRTCaptchaState = window.AKSRTCaptchaState || { loaders: {}, scenes: {} });
|
||||
root.rescan = scan;
|
||||
|
||||
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;
|
||||
try {
|
||||
config = JSON.parse(el.getAttribute('data-aksrt-captcha-config'));
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (config && config.scene && config.uid && config.vid) {
|
||||
initMount(config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
var verified = false;
|
||||
var pendingAction = null;
|
||||
var triggerCaptcha = function () { showTip('验证码加载中,请稍候', '#e67e22'); };
|
||||
|
||||
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) {
|
||||
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 });
|
||||
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')); };
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
el.value = value;
|
||||
}
|
||||
|
||||
function resetVerificationState() {
|
||||
verified = false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 = '<div style="text-align:center;padding:20px;color:#9ca3af;">验证码加载中...</div>'; }
|
||||
function clearLoading() { box.innerHTML = ''; }
|
||||
|
||||
function finishPending(ok) {
|
||||
var action = pendingAction;
|
||||
pendingAction = null;
|
||||
if (typeof action === 'function') { action(!!ok); }
|
||||
}
|
||||
|
||||
function cancelPending(msg) {
|
||||
if (msg) showTip(msg, '#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
|
||||
};
|
||||
|
||||
// ============ 腾讯云 Captcha 2.0 ============
|
||||
function loadTencent() {
|
||||
showLoading();
|
||||
loadScriptOnce('tencent', 'https://turing.captcha.qcloud.com/TJCaptcha.js', function () {
|
||||
return typeof TencentCaptcha !== 'undefined';
|
||||
}).then(function () {
|
||||
clearLoading();
|
||||
var capIns = 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');
|
||||
return;
|
||||
}
|
||||
setField('tencent_ticket', res.ticket);
|
||||
setField('tencent_randstr', res.randstr);
|
||||
setField('tencent_sid', res.sid || '');
|
||||
submitForm();
|
||||
return;
|
||||
}
|
||||
if (res.errorCode) {
|
||||
var errorMap = { 1001: '验证码组件加载失败', 1002: '验证码超时', 1006: '验证码配置拉取失败', 1008: '验证码页面加载异常', 1012: '验证码刷新次数过多', 1013: '验证码网络异常' };
|
||||
showTip(errorMap[res.errorCode] || ('验证码异常(errorCode:' + res.errorCode + ')'), '#e74c3c');
|
||||
}
|
||||
}, { userLanguage: 'zh-cn' });
|
||||
}
|
||||
capIns.show();
|
||||
};
|
||||
}).catch(function () { clearLoading(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
|
||||
}
|
||||
|
||||
// ============ Cloudflare Turnstile(弹窗) ============
|
||||
function loadCloudflare() {
|
||||
var cfWidgetId = null, cfVerifying = false, cfModal = 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 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,
|
||||
callback: function (token) {
|
||||
if (!cfVerifying) return;
|
||||
cfVerifying = false;
|
||||
setField('cf_turnstile_response', token);
|
||||
closeModal();
|
||||
submitForm();
|
||||
},
|
||||
'error-callback': function () { showTip('验证码失败,请重试', '#e74c3c'); cfWidgetId = null; setTimeout(renderWidget, 500); },
|
||||
'expired-callback': function () { showTip('验证码已过期,请重新验证', '#e67e22'); cfWidgetId = null; setTimeout(renderWidget, 500); }
|
||||
});
|
||||
}
|
||||
|
||||
function createModal() {
|
||||
cfModal = document.createElement('div');
|
||||
cfModal.className = 'aksrt-captcha-modal-overlay';
|
||||
cfModal.innerHTML = '<div class="aksrt-captcha-modal-backdrop"></div>' +
|
||||
'<div class="aksrt-captcha-modal-box"><div class="aksrt-captcha-modal-header">' +
|
||||
'<span class="aksrt-captcha-modal-title">人机验证</span>' +
|
||||
'<button type="button" class="aksrt-captcha-modal-close">×</button></div>' +
|
||||
'<div class="aksrt-captcha-modal-body"><p class="aksrt-captcha-modal-hint">请完成验证后继续操作</p>' +
|
||||
'<div id="modalWidget_' + C.uid + '" class="aksrt-captcha-modal-widget"></div></div></div></div>';
|
||||
document.body.appendChild(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);
|
||||
renderWidget();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (cfWidgetId) { try { turnstile.remove(cfWidgetId); } catch (e) {} cfWidgetId = null; }
|
||||
if (cfModal && cfModal.parentNode) { cfModal.parentNode.removeChild(cfModal); }
|
||||
cfModal = null;
|
||||
}
|
||||
|
||||
triggerCaptcha = function () {
|
||||
if (cfVerifying) { showTip('验证码进行中,请稍候', '#e67e22'); return; }
|
||||
cfVerifying = true;
|
||||
createModal();
|
||||
};
|
||||
}
|
||||
|
||||
// ============ 极验 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';
|
||||
if (!captchaId) {
|
||||
clearLoading();
|
||||
showTip('极验 Captcha ID 未配置,请到后台「功能&权限 → 人机验证」填写并保存', '#e74c3c');
|
||||
return;
|
||||
}
|
||||
function render() {
|
||||
clearLoading();
|
||||
initGeetest4({ captchaId: captchaId, product: product, hideSuccess: true }, function (capIns) {
|
||||
window['__gt_' + C.uid] = capIns;
|
||||
function onSuccess() {
|
||||
var result = capIns.getValidate();
|
||||
if (!result) 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('已取消验证'); });
|
||||
}
|
||||
});
|
||||
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') {
|
||||
loadScriptOnce('geetest', 'https://static.geetest.com/v4/gt4.js', function () {
|
||||
return typeof initGeetest4 !== 'undefined';
|
||||
}).then(render).catch(function () { clearLoading(); showTip('极验组件加载失败', '#e74c3c'); });
|
||||
return;
|
||||
}
|
||||
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;
|
||||
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();
|
||||
},
|
||||
fail: function (result) { showTip('验证码失败: ' + (result || '请重试'), '#e74c3c'); captchaIns = null; },
|
||||
getInstance: function (instance) { captchaIns = instance; },
|
||||
slideStyle: { width: 360, height: 40 }, language: 'cn'
|
||||
});
|
||||
triggerCaptcha = function () {
|
||||
if (captchaIns && captchaIns.show) { captchaIns.show(); return; }
|
||||
showTip('验证码组件加载中,请稍候再试', '#e67e22');
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', scan);
|
||||
} else {
|
||||
setTimeout(scan, 50);
|
||||
}
|
||||
})();
|
||||
+133
-391
@@ -5,7 +5,8 @@
|
||||
* 唯一配置源:option `aksrt_captcha_config`。
|
||||
* 闭环契约:
|
||||
* aksrt_get_captcha_config() -> aksrt_is_captcha_enabled_for(scene)
|
||||
* -> aksrt_render_captcha(scene) -> 客户端 AKSRTCaptchaState + token
|
||||
* -> aksrt_render_captcha(scene) -> [data-aksrt-captcha-config] 挂载点
|
||||
* -> captcha-front.js(public bundle)-> 客户端 AKSRTCaptchaState + token
|
||||
* -> 业务入口 aksrt_require_captcha(scene, $payload) -> proceed/fail。
|
||||
*
|
||||
* 仅支持云厂商:tencent / aliyun / cloudflare / geetest。
|
||||
@@ -81,9 +82,16 @@ function aksrt_captcha_sanitize_scene_list($raw): array {
|
||||
/**
|
||||
* 获取人机验证完整配置(规范化,不合并历史脏键)。
|
||||
*
|
||||
* 结果在同一请求内缓存;后台保存后调用 aksrt_captcha_config_reset() 失效。
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function aksrt_get_captcha_config(): array {
|
||||
$cache =& $GLOBALS['aksrt_captcha_config_cache'];
|
||||
if (is_array($cache)) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$stored = get_option('aksrt_captcha_config', []);
|
||||
$cfg = array_merge(aksrt_captcha_default_config(), is_array($stored) ? $stored : []);
|
||||
|
||||
@@ -103,9 +111,17 @@ function aksrt_get_captcha_config(): array {
|
||||
$result[$p] = isset($cfg[$p]) && is_array($cfg[$p]) ? $cfg[$p] : [];
|
||||
}
|
||||
|
||||
$cache = $result;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使请求内的配置缓存失效(后台保存路径调用)。
|
||||
*/
|
||||
function aksrt_captcha_config_reset(): void {
|
||||
$GLOBALS['aksrt_captcha_config_cache'] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 各服务商必填字段。
|
||||
*/
|
||||
@@ -164,6 +180,25 @@ function aksrt_is_captcha_enabled_for($scene): bool {
|
||||
return in_array($scene, $cfg['enabled_scenarios'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景验证通过后的服务端放行时长(秒),0 表示不启用。
|
||||
*
|
||||
* 下载页多个卡片共享一次验证结果(前端会把 token 同步到兄弟挂载点),
|
||||
* 而厂商 token 是一次性的,因此验证通过后由服务端记录短时放行,
|
||||
* 后续下载请求直接命中放行,避免票据被判定重复使用。
|
||||
*/
|
||||
function aksrt_captcha_pass_ttl(string $scene): int {
|
||||
return $scene === 'download' ? 10 * MINUTE_IN_SECONDS : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务端放行记录的 transient key:场景 + 用户 + IP + UA。
|
||||
*/
|
||||
function aksrt_captcha_pass_key(string $scene): string {
|
||||
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
|
||||
return 'aksrt_captcha_pass_' . md5($scene . '|' . get_current_user_id() . '|' . aksrt_get_client_ip() . '|' . md5($ua));
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务入口统一调用:未启用放行;启用则校验。
|
||||
*
|
||||
@@ -175,7 +210,17 @@ function aksrt_require_captcha(string $scene, array $payload = []) {
|
||||
if (!aksrt_is_captcha_enabled_for($scene)) {
|
||||
return true;
|
||||
}
|
||||
return aksrt_verify_captcha($payload);
|
||||
|
||||
$ttl = aksrt_captcha_pass_ttl($scene);
|
||||
if ($ttl > 0 && get_transient(aksrt_captcha_pass_key($scene))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = aksrt_verify_captcha($payload, $scene);
|
||||
if ($result === true && $ttl > 0) {
|
||||
set_transient(aksrt_captcha_pass_key($scene), time(), $ttl);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,6 +241,28 @@ function aksrt_get_captcha_status_summary(): array {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 凭据未配齐时的后台警告(前台保持放行,验证码不是准入门槛)。
|
||||
*/
|
||||
add_action('admin_notices', static function () {
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
$cfg = aksrt_get_captcha_config();
|
||||
if (empty($cfg['enabled']) || $cfg['active_provider'] === '') {
|
||||
return;
|
||||
}
|
||||
$provider = $cfg['active_provider'];
|
||||
if (aksrt_captcha_provider_is_configured($provider, $cfg[$provider] ?? [])) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="notice notice-error"><p>' . esc_html(sprintf(
|
||||
/* translators: %s: captcha provider key */
|
||||
__('人机验证服务商「%s」已启用但凭据未配齐,前台人机验证未生效。请到「功能&权限 → 人机验证」补全配置。', 'aksrt'),
|
||||
$provider
|
||||
)) . '</p></div>';
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 客户端真实 IP(穿透 CDN / 代理)
|
||||
// ============================================================
|
||||
@@ -255,7 +322,7 @@ function aksrt_tencent_tc3_sign($secret_id, $secret_key, $service, $timestamp, $
|
||||
|
||||
$secret_date = hash_hmac('sha256', $date, 'TC3' . $secret_key, true);
|
||||
$secret_service = hash_hmac('sha256', $service, $secret_date, true);
|
||||
$secret_signing = hash_hmac('sha256', 'tc3_request', $secret_service, true);
|
||||
$secret_signing = hash_hmac('sha256', 'tc3_request', $secret_signing, true);
|
||||
$signature = hash_hmac('sha256', $string_to_sign, $secret_signing);
|
||||
|
||||
$authorization = 'TC3-HMAC-SHA256 '
|
||||
@@ -291,13 +358,21 @@ function aksrt_aliyun_afs_sign(array $params, string $secret): string {
|
||||
// 服务端二次校验
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 记录传输层 / 厂商原始错误细节,供站点排查;对用户只暴露通用文案。
|
||||
*/
|
||||
function aksrt_captcha_log(string $provider, string $detail): void {
|
||||
error_log('[AKSRT Captcha] ' . $provider . ': ' . $detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验客户端提交的人机验证字段。
|
||||
*
|
||||
* @param array $post_data 客户端字段(captcha_verified + 厂商 token)
|
||||
* @param array $post_data 客户端字段(captcha_verified + 厂商 token)
|
||||
* @param string $scene 触发场景(用于 Cloudflare cdata 绑定校验)
|
||||
* @return true|string 成功返回 true,失败返回错误信息
|
||||
*/
|
||||
function aksrt_verify_captcha($post_data) {
|
||||
function aksrt_verify_captcha($post_data, string $scene = '') {
|
||||
$cfg = aksrt_get_captcha_config();
|
||||
if (empty($cfg['enabled']) || $cfg['active_provider'] === '') {
|
||||
return true;
|
||||
@@ -317,7 +392,7 @@ function aksrt_verify_captcha($post_data) {
|
||||
$result = aksrt_verify_tencent($pcfg, $post_data, $client_ip);
|
||||
break;
|
||||
case 'cloudflare':
|
||||
$result = aksrt_verify_cloudflare($pcfg, $post_data, $client_ip);
|
||||
$result = aksrt_verify_cloudflare($pcfg, $post_data, $client_ip, $scene);
|
||||
break;
|
||||
case 'geetest':
|
||||
$result = aksrt_verify_geetest($pcfg, $post_data);
|
||||
@@ -366,37 +441,29 @@ function aksrt_verify_tencent(array $pcfg, array $post_data, string $client_ip)
|
||||
|
||||
$headers = aksrt_tencent_tc3_sign($secret_id, $secret_key, 'captcha', $timestamp, $date, 'DescribeCaptchaResult', '2019-07-22', $payload);
|
||||
|
||||
$curl_headers = [];
|
||||
foreach ($headers as $k => $v) {
|
||||
$curl_headers[] = "{$k}: {$v}";
|
||||
}
|
||||
|
||||
$ch = curl_init('https://captcha.tencentcloudapi.com/');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => $curl_headers,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
$resp = wp_remote_post('https://captcha.tencentcloudapi.com/', [
|
||||
'timeout' => 5,
|
||||
'headers' => $headers,
|
||||
'body' => $payload,
|
||||
]);
|
||||
$response_body = curl_exec($ch);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curl_error) {
|
||||
return '验证码服务请求失败: ' . $curl_error;
|
||||
if (is_wp_error($resp)) {
|
||||
aksrt_captcha_log('tencent', $resp->get_error_message());
|
||||
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
|
||||
}
|
||||
|
||||
$body = json_decode((string) $response_body, true);
|
||||
$body = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (!is_array($body)) {
|
||||
aksrt_captcha_log('tencent', 'unparsable response');
|
||||
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
|
||||
}
|
||||
if (isset($body['Response']['Error'])) {
|
||||
$err = $body['Response']['Error'];
|
||||
return '腾讯 API 调用失败: ' . ($err['Message'] ?? $err['Code'] ?? '未知错误');
|
||||
aksrt_captcha_log('tencent', ($err['Code'] ?? 'unknown') . ': ' . ($err['Message'] ?? ''));
|
||||
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
|
||||
}
|
||||
|
||||
$resp = $body['Response'] ?? [];
|
||||
$captcha_code = (int) ($resp['CaptchaCode'] ?? 0);
|
||||
$resp_data = $body['Response'] ?? [];
|
||||
$captcha_code = (int) ($resp_data['CaptchaCode'] ?? 0);
|
||||
if ($captcha_code === 1) {
|
||||
return true;
|
||||
}
|
||||
@@ -411,16 +478,16 @@ function aksrt_verify_tencent(array $pcfg, array $post_data, string $client_ip)
|
||||
21 => '票据校验异常(风控拦截)',
|
||||
100 => '参数校验失败(AppId/SecretKey/Ticket 不匹配)',
|
||||
];
|
||||
$msg = $resp['CaptchaMsg'] ?? ($messages[$captcha_code] ?? '未知错误');
|
||||
$msg = $resp_data['CaptchaMsg'] ?? ($messages[$captcha_code] ?? '未知错误');
|
||||
return '腾讯验证失败: ' . $msg . " (CaptchaCode: {$captcha_code})";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloudflare Turnstile siteverify。
|
||||
* Cloudflare Turnstile siteverify。校验 action 与 cdata(场景)绑定,防 token 转用。
|
||||
*
|
||||
* @return true|string
|
||||
*/
|
||||
function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_ip) {
|
||||
function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_ip, string $scene = '') {
|
||||
$token = (string) ($post_data['cf_turnstile_response'] ?? '');
|
||||
if ($token === '' || strlen($token) < 10) {
|
||||
return __('缺少验证参数', 'aksrt');
|
||||
@@ -435,7 +502,7 @@ function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_i
|
||||
$body = [];
|
||||
foreach ($bodies as $attempt) {
|
||||
$resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
||||
'timeout' => 10,
|
||||
'timeout' => 5,
|
||||
'body' => $attempt,
|
||||
]);
|
||||
if (is_wp_error($resp)) {
|
||||
@@ -443,12 +510,22 @@ function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_i
|
||||
}
|
||||
$body = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (!empty($body['success'])) {
|
||||
if (isset($body['action']) && (string) $body['action'] !== 'aksrt-verify') {
|
||||
aksrt_captcha_log('cloudflare', 'action mismatch: ' . (string) $body['action']);
|
||||
return __('人机验证未通过,请重试', 'aksrt');
|
||||
}
|
||||
if ($scene !== '' && isset($body['cdata']) && (string) $body['cdata'] !== $scene) {
|
||||
aksrt_captcha_log('cloudflare', 'cdata mismatch: ' . (string) $body['cdata'] . ' != ' . $scene);
|
||||
return __('人机验证未通过,请重试', 'aksrt');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$codes = isset($body['error-codes']) && is_array($body['error-codes']) ? implode(', ', $body['error-codes']) : '未知错误';
|
||||
return 'Cloudflare 验证失败: ' . $codes;
|
||||
if (is_array($body) && isset($body['error-codes'])) {
|
||||
aksrt_captcha_log('cloudflare', 'siteverify failed: ' . implode(', ', (array) $body['error-codes']));
|
||||
}
|
||||
return __('人机验证未通过,请重试', 'aksrt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -469,7 +546,7 @@ function aksrt_verify_geetest(array $pcfg, array $post_data) {
|
||||
$sign_token = hash_hmac('sha256', $lot_number, $captcha_key);
|
||||
|
||||
$resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', [
|
||||
'timeout' => 10,
|
||||
'timeout' => 5,
|
||||
'body' => [
|
||||
'captcha_id' => (string) ($pcfg['captcha_id'] ?? ''),
|
||||
'lot_number' => $lot_number,
|
||||
@@ -481,15 +558,16 @@ function aksrt_verify_geetest(array $pcfg, array $post_data) {
|
||||
]);
|
||||
|
||||
if (is_wp_error($resp)) {
|
||||
return '验证请求失败: ' . $resp->get_error_message();
|
||||
aksrt_captcha_log('geetest', $resp->get_error_message());
|
||||
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (!empty($body['result']) && $body['result'] === 'success') {
|
||||
if (is_array($body) && !empty($body['result']) && $body['result'] === 'success') {
|
||||
return true;
|
||||
}
|
||||
$reason = $body['reason'] ?? ($body['msg'] ?? '未知错误');
|
||||
return '极验验证失败: ' . $reason;
|
||||
aksrt_captcha_log('geetest', 'validate failed: ' . (string) ($body['reason'] ?? ($body['msg'] ?? 'unknown')));
|
||||
return __('人机验证未通过,请重试', 'aksrt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -514,7 +592,7 @@ function aksrt_verify_aliyun(array $pcfg, array $post_data) {
|
||||
'Format' => 'JSON',
|
||||
'SignatureMethod' => 'HMAC-SHA1',
|
||||
'SignatureVersion' => '1.0',
|
||||
'SignatureNonce' => uniqid('', true),
|
||||
'SignatureNonce' => wp_generate_uuid4(),
|
||||
'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||
'Version' => '2023-03-05',
|
||||
'CaptchaVerifyParam' => $captcha_verify_param,
|
||||
@@ -525,15 +603,20 @@ function aksrt_verify_aliyun(array $pcfg, array $post_data) {
|
||||
$params['Signature'] = aksrt_aliyun_afs_sign($params, (string) ($pcfg['access_key_secret'] ?? ''));
|
||||
|
||||
$resp = wp_remote_post('https://' . $endpoint . '/', [
|
||||
'timeout' => 10,
|
||||
'timeout' => 5,
|
||||
'body' => $params,
|
||||
]);
|
||||
|
||||
if (is_wp_error($resp)) {
|
||||
return '验证请求失败: ' . $resp->get_error_message();
|
||||
aksrt_captcha_log('aliyun', $resp->get_error_message());
|
||||
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (!is_array($body)) {
|
||||
aksrt_captcha_log('aliyun', 'unparsable response');
|
||||
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
|
||||
}
|
||||
if (!empty($body['Success'])) {
|
||||
$verify_result = $body['Result']['VerifyResult'] ?? false;
|
||||
if ($verify_result === true) {
|
||||
@@ -561,15 +644,18 @@ function aksrt_verify_aliyun(array $pcfg, array $post_data) {
|
||||
return '阿里云 V3 验证失败: ' . ($code_messages[$verify_code] ?? '未知错误 (' . ($verify_code ?: 'N/A') . ')');
|
||||
}
|
||||
|
||||
return '阿里云 V3 验证失败: ' . ($body['Message'] ?? '未知错误');
|
||||
aksrt_captcha_log('aliyun', 'api failed: ' . (string) ($body['Message'] ?? 'unknown'));
|
||||
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 前端渲染(挂载点 + 内联 SDK + AKSRTCaptchaState)
|
||||
// 前端渲染(挂载点 + data 配置,运行时在 public bundle 的 captcha-front.js)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 渲染人机验证挂载点。未启用场景直接返回。
|
||||
*
|
||||
* 挂载点自带 JSON 配置,由 captcha-front.js(public bundle)扫描激活。
|
||||
*/
|
||||
function aksrt_render_captcha($scene = 'comment') {
|
||||
$scene = sanitize_key((string) $scene);
|
||||
@@ -618,354 +704,10 @@ function aksrt_render_captcha($scene = 'comment') {
|
||||
'scene_id' => (string) ($pcfg['scene_id'] ?? ''),
|
||||
],
|
||||
];
|
||||
|
||||
$token_fields = ['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'];
|
||||
$token_fields_json = wp_json_encode($token_fields, JSON_UNESCAPED_UNICODE);
|
||||
?>
|
||||
<div class="aksrt-captcha-wrap" data-provider="<?php echo esc_attr($provider); ?>" data-scene="<?php echo esc_attr($scene); ?>">
|
||||
<div class="aksrt-captcha-wrap" data-provider="<?php echo esc_attr($provider); ?>" data-scene="<?php echo esc_attr($scene); ?>" data-aksrt-captcha-config="<?php echo esc_attr(wp_json_encode($js_config, JSON_UNESCAPED_UNICODE)); ?>">
|
||||
<div id="<?php echo esc_attr($uid); ?>" class="aksrt-captcha-box" style="display:none"></div>
|
||||
<input type="hidden" name="captcha_verified" id="<?php echo esc_attr($vid); ?>" value="0">
|
||||
</div>
|
||||
<link rel="stylesheet" href="<?php echo esc_url(AKSRT_URI . '/assets/css/shared/captcha.css'); ?>">
|
||||
<script>(function(){
|
||||
var C=<?php echo wp_json_encode($js_config, JSON_UNESCAPED_UNICODE); ?>;
|
||||
var TOKEN_FIELDS=<?php echo $token_fields_json; ?>;
|
||||
var root=window.AKSRTCaptchaState=window.AKSRTCaptchaState||{loaders:{}, scenes:{}};
|
||||
|
||||
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){
|
||||
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});
|
||||
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')); };
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
return root.loaders[key];
|
||||
}
|
||||
|
||||
var box, hField, form=null;
|
||||
var verified=false;
|
||||
var pendingAction=null;
|
||||
var triggerCaptcha=function(){ showTip('验证码加载中,请稍候', '#e67e22'); };
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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); }
|
||||
el.value=value;
|
||||
}
|
||||
|
||||
function resetVerificationState(){
|
||||
verified=false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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='<div style="text-align:center;padding:20px;color:#9ca3af;">验证码加载中...</div>'; }
|
||||
function clearLoading(){ box.innerHTML=''; }
|
||||
|
||||
function finishPending(ok){
|
||||
var action=pendingAction; pendingAction=null;
|
||||
if(typeof action==='function'){ action(!!ok); }
|
||||
}
|
||||
|
||||
function cancelPending(msg){
|
||||
if(msg) showTip(msg, '#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
|
||||
};
|
||||
|
||||
// ============ 腾讯云 Captcha 2.0 ============
|
||||
function loadTencent(){
|
||||
showLoading();
|
||||
loadScriptOnce('tencent', 'https://turing.captcha.qcloud.com/TJCaptcha.js', function(){
|
||||
return typeof TencentCaptcha!=='undefined';
|
||||
}).then(function(){
|
||||
clearLoading();
|
||||
var capIns=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'); return;
|
||||
}
|
||||
setField('tencent_ticket', res.ticket);
|
||||
setField('tencent_randstr', res.randstr);
|
||||
setField('tencent_sid', res.sid||'');
|
||||
submitForm();
|
||||
return;
|
||||
}
|
||||
if(res.errorCode){
|
||||
var errorMap={1001:'验证码组件加载失败',1002:'验证码超时',1006:'验证码配置拉取失败',1008:'验证码页面加载异常',1012:'验证码刷新次数过多',1013:'验证码网络异常'};
|
||||
showTip(errorMap[res.errorCode]||('验证码异常(errorCode:'+res.errorCode+')'), '#e74c3c');
|
||||
}
|
||||
}, {userLanguage:'zh-cn'});
|
||||
}
|
||||
capIns.show();
|
||||
};
|
||||
}).catch(function(){ clearLoading(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
|
||||
}
|
||||
|
||||
// ============ Cloudflare Turnstile(弹窗) ============
|
||||
function loadCloudflare(){
|
||||
var cfWidgetId=null, cfVerifying=false, cfModal=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 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',
|
||||
callback:function(token){
|
||||
if(!cfVerifying) return;
|
||||
cfVerifying=false;
|
||||
setField('cf_turnstile_response', token);
|
||||
closeModal(); submitForm();
|
||||
},
|
||||
'error-callback':function(){ showTip('验证码失败,请重试', '#e74c3c'); cfWidgetId=null; setTimeout(renderWidget, 500); },
|
||||
'expired-callback':function(){ showTip('验证码已过期,请重新验证', '#e67e22'); cfWidgetId=null; setTimeout(renderWidget, 500); }
|
||||
});
|
||||
}
|
||||
|
||||
function createModal(){
|
||||
cfModal=document.createElement('div');
|
||||
cfModal.className='aksrt-captcha-modal-overlay';
|
||||
cfModal.innerHTML='<div class="aksrt-captcha-modal-backdrop"></div>'+
|
||||
'<div class="aksrt-captcha-modal-box"><div class="aksrt-captcha-modal-header">'+
|
||||
'<span class="aksrt-captcha-modal-title">人机验证</span>'+
|
||||
'<button type="button" class="aksrt-captcha-modal-close">×</button></div>'+
|
||||
'<div class="aksrt-captcha-modal-body"><p class="aksrt-captcha-modal-hint">请完成验证后继续操作</p>'+
|
||||
'<div id="modalWidget_'+C.uid+'" class="aksrt-captcha-modal-widget"></div></div></div>';
|
||||
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);
|
||||
renderWidget();
|
||||
}
|
||||
|
||||
function closeModal(){
|
||||
if(cfWidgetId){ try { turnstile.remove(cfWidgetId); } catch(e) {} cfWidgetId=null; }
|
||||
if(cfModal && cfModal.parentNode){ cfModal.parentNode.removeChild(cfModal); }
|
||||
cfModal=null;
|
||||
}
|
||||
|
||||
triggerCaptcha=function(){
|
||||
if(cfVerifying){ showTip('验证码进行中,请稍候', '#e67e22'); return; }
|
||||
cfVerifying=true; createModal();
|
||||
};
|
||||
}
|
||||
|
||||
// ============ 极验 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';
|
||||
if(!captchaId){
|
||||
clearLoading();
|
||||
showTip('极验 Captcha ID 未配置,请到后台「功能&权限 → 人机验证」填写并保存', '#e74c3c');
|
||||
return;
|
||||
}
|
||||
function render(){
|
||||
clearLoading();
|
||||
initGeetest4({captchaId:captchaId, product:product, hideSuccess:true}, function(capIns){
|
||||
window['__gt_'+C.uid]=capIns;
|
||||
function onSuccess(){
|
||||
var result=capIns.getValidate();
|
||||
if(!result) 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(){});
|
||||
}else{
|
||||
capIns.appendTo('#'+C.uid).onSuccess(onSuccess).onClose(function(){});
|
||||
}
|
||||
});
|
||||
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'){
|
||||
loadScriptOnce('geetest', 'https://static.geetest.com/v4/gt4.js', function(){
|
||||
return typeof initGeetest4!=='undefined';
|
||||
}).then(render).catch(function(){ clearLoading(); showTip('极验组件加载失败', '#e74c3c'); });
|
||||
return;
|
||||
}
|
||||
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;
|
||||
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();
|
||||
},
|
||||
fail:function(result){ showTip('验证码失败: ' + (result || '请重试'), '#e74c3c'); captchaIns=null; },
|
||||
getInstance:function(instance){ captchaIns=instance; },
|
||||
slideStyle:{width:360, height:40}, language:'cn'
|
||||
});
|
||||
triggerCaptcha=function(){
|
||||
if(captchaIns && captchaIns.show){ captchaIns.show(); return; }
|
||||
showTip('验证码组件加载中,请稍候再试', '#e67e22');
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', init); }
|
||||
else { setTimeout(init, 50); }
|
||||
})();</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ class SchemaPermissions {
|
||||
['label' => 'Cloudflare Turnstile', 'value' => 'cloudflare'],
|
||||
['label' => '极验 GeeTest', 'value' => 'geetest'],
|
||||
]],
|
||||
['key' => 'mode', 'label' => '触发模式', 'type' => 'radio', 'default' => 'always', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [
|
||||
['label' => '始终显示', 'value' => 'always'],
|
||||
]],
|
||||
['key' => 'enabled_scenarios', 'label' => '启用场景', 'type' => 'checkbox', 'default' => ['comment', 'login', 'register', 'bind', 'bind_phone', 'lostpassword', 'change_password', 'change_email', 'download', 'signin'], 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [
|
||||
['label' => '发表评论', 'value' => 'comment'],
|
||||
['label' => '用户登录', 'value' => 'login'],
|
||||
|
||||
@@ -400,7 +400,7 @@ class SchemaProvider {
|
||||
// Captcha: always surface the normalized positive scene list.
|
||||
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_get_captcha_config')) {
|
||||
$normalized = aksrt_get_captcha_config();
|
||||
foreach (['enabled', 'active_provider', 'enabled_scenarios', 'tencent', 'aliyun', 'cloudflare', 'geetest', 'mode'] as $key) {
|
||||
foreach (['enabled', 'active_provider', 'enabled_scenarios', 'tencent', 'aliyun', 'cloudflare', 'geetest'] as $key) {
|
||||
if (array_key_exists($key, $normalized)) {
|
||||
$data[$key] = $normalized[$key];
|
||||
}
|
||||
@@ -437,6 +437,9 @@ class SchemaProvider {
|
||||
}
|
||||
|
||||
$updated = update_option($option_key, $stored);
|
||||
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_captcha_config_reset')) {
|
||||
aksrt_captcha_config_reset();
|
||||
}
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
if (isset($stored['site_favicon'])) {
|
||||
|
||||
@@ -38,6 +38,7 @@ const cssBundles = {
|
||||
'assets/css/frontend/ajax-spinner.css',
|
||||
'assets/css/frontend/modal-runtime.css',
|
||||
'assets/css/frontend/notyf.css',
|
||||
'assets/css/shared/captcha.css',
|
||||
],
|
||||
'homepage.css': [
|
||||
'assets/css/frontend/post-card.css',
|
||||
@@ -88,6 +89,7 @@ const publicScripts = [
|
||||
'assets/js/color-extract.js',
|
||||
'assets/js/effects.js',
|
||||
'assets/js/runtime/main.min.js',
|
||||
'assets/js/captcha-front.js',
|
||||
'assets/js/instant-page.js',
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user