优化了一些问题fix(captcha)
This commit is contained in:
1 parent
41da33bb13
commit
88eb29c645
10 files changed
+1073
-406
No files matched your search
@@ -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: {
|
defaults: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
active_provider: '',
|
active_provider: '',
|
||||||
mode: 'always',
|
|
||||||
enabled_scenarios: [
|
enabled_scenarios: [
|
||||||
'comment', 'login', 'register', 'bind', 'bind_phone',
|
'comment', 'login', 'register', 'bind', 'bind_phone',
|
||||||
'lostpassword', 'change_password', 'change_email', 'download', 'signin',
|
'lostpassword', 'change_password', 'change_email', 'download', 'signin',
|
||||||
@@ -46,16 +45,6 @@ export const permissionsSchemas = {
|
|||||||
],
|
],
|
||||||
dependsOn: { key: 'enabled', value: true },
|
dependsOn: { key: 'enabled', value: true },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'mode',
|
|
||||||
label: '触发模式',
|
|
||||||
type: 'radio',
|
|
||||||
buttonStyle: false,
|
|
||||||
options: [
|
|
||||||
{ label: '始终显示', value: 'always' },
|
|
||||||
],
|
|
||||||
dependsOn: { key: 'enabled', value: true },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'enabled_scenarios',
|
key: 'enabled_scenarios',
|
||||||
label: '启用场景',
|
label: '启用场景',
|
||||||
|
|||||||
@@ -7748,3 +7748,27 @@ body.aksrt-modal-runtime-open {
|
|||||||
transform: rotate(360deg);
|
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
@@ -5,4 +5,4 @@
|
|||||||
*//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" ("attr(href) ")"}abbr[title]:after{content:" ("attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}*{box-sizing:border-box}:after,:before{box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-Line truncated
|
*//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" ("attr(href) ")"}abbr[title]:after{content:" ("attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}*{box-sizing:border-box}:after,:before{box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-Line truncated
|
||||||
* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
|
* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
|
||||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:""}.fa-accessible-icon:before{content:""}.fa-accusoft:before{content:""}.fa-acquisitions-incorporated:before{content:""}.fa-ad:before{content:""}.fa-address-book:before{content:""}.fa-address-card:before{content:""}.fa-adjust:before{content:""}.fa-adn:before{content:""}.fa-adversal:before{content:""}.fa-affiliatetheme:before{content:""}.fa-air-freshener:before{content:""}.fa-airbnb:before{content:""}.fa-algolia:before{content:""}.fa-align-center:before{content:""}.fa-align-justify:before{content:""}.fa-align-left:before{content:""}.fa-align-right:before{content:""}.fa-alipay:before{content:""}.fa-allergies:before{content:""}.fa-amazon:before{content:""}.fa-amazon-pay:before{content:""}.fa-ambulance:before{content:""}.fa-american-sign-language-interpreting:before{content:""}.fa-amilia:before{content:""}.fa-anchor:before{content:""}.fa-android:before{content:""}.fa-angellist:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angry:before{content:""}.fa-angrycreative:before{content:""}.fa-angular:before{content:""}.fa-ankh:before{content:""}.fa-app-store:before{content:""}.fa-app-store-ios:before{content:""}.fa-apper:before{content:""}.fa-apple:before{content:""}.fa-apple-alt:before{content:""}.fa-apple-pay:before{content:""}.fa-archive:before{content:""}.fa-archway:before{content:""}.fa-arrow-alt-circle-down:before{content:""}.fa-arrow-alt-circle-left:before{content:""}.fa-arrow-alt-circle-right:before{content:""}.fa-arrow-alt-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-down:before{content:""}.fLine truncated
|
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:""}.fa-accessible-icon:before{content:""}.fa-accusoft:before{content:""}.fa-acquisitions-incorporated:before{content:""}.fa-ad:before{content:""}.fa-address-book:before{content:""}.fa-address-card:before{content:""}.fa-adjust:before{content:""}.fa-adn:before{content:""}.fa-adversal:before{content:""}.fa-affiliatetheme:before{content:""}.fa-air-freshener:before{content:""}.fa-airbnb:before{content:""}.fa-algolia:before{content:""}.fa-align-center:before{content:""}.fa-align-justify:before{content:""}.fa-align-left:before{content:""}.fa-align-right:before{content:""}.fa-alipay:before{content:""}.fa-allergies:before{content:""}.fa-amazon:before{content:""}.fa-amazon-pay:before{content:""}.fa-ambulance:before{content:""}.fa-american-sign-language-interpreting:before{content:""}.fa-amilia:before{content:""}.fa-anchor:before{content:""}.fa-android:before{content:""}.fa-angellist:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angry:before{content:""}.fa-angrycreative:before{content:""}.fa-angular:before{content:""}.fa-ankh:before{content:""}.fa-app-store:before{content:""}.fa-app-store-ios:before{content:""}.fa-apper:before{content:""}.fa-apple:before{content:""}.fa-apple-alt:before{content:""}.fa-apple-pay:before{content:""}.fa-archive:before{content:""}.fa-archway:before{content:""}.fa-arrow-alt-circle-down:before{content:""}.fa-arrow-alt-circle-left:before{content:""}.fa-arrow-alt-circle-right:before{content:""}.fa-arrow-alt-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-down:before{content:""}.fLine truncated
|
||||||
@@ -5605,6 +5605,415 @@ tbquire.config({
|
|||||||
jQuery.cookie=function(e,o,t){if(o===void 0){var i=null;if(document.cookie&&document.cookie!="")for(var r=document.cookie.split(";"),n=0;n<r.length;n++){var c=jQuery.trim(r[n]);if(c.substring(0,e.length+1)==e+"="){i=decodeURIComponent(c.substring(e.length+1));break}}return i}t=t||{},o===null&&(o="",t.expires=-1);var a="";if(t.expires&&(typeof t.expires=="number"||t.expires.toUTCString)){var l2;typeof t.expires=="number"?(l2=new Date,l2.setTime(l2.getTime()+24*t.expires*60*60*1e3)):l2=t.expires,a="; expires="+l2.toUTCString()}var u=t.path?"; path="+t.path:"",s=t.domain?"; domain="+t.domain:"",m=t.secure?"; secure":"";document.cookie=[e,"=",encodeURIComponent(o),a,u,s,m].join("")};var lcs={get:function(e){return window.localStorage?localStorage.getItem(e):jQuery.cookie(e)},set:function(e,o){window.localStorage?localStorage[e]=o:jQuery.cookie(e,o)},remove:function(e){window.localStorage?localStorage.removeItem(e):jQuery.cookie(e,"")}};function aksrt__(key){var map=window._win&&window._win.i18n;return map&&Object.prototype.hasOwnProperty.call(map,key)?map[key]:key}function aksrt_is_fatal_error_response(text){return text&&(text.indexOf("Fatal error")>-1||text.indexOf("致命错误")>-1)}function aksrt_sprintf(key){var args=Array.prototype.slice.call(arguments,1),str=aksrt__(key),autoIndex=0;return str.replace(/%(\d+\$)?s/g,function(match,num){var idx=num?parseInt(num,10)-1:autoIndex++;return args[idx]!==void 0&&args[idx]!==null?args[idx]:match})}(function(window2,factory){var lazySizes=factory(window2,window2.document,Date);window2.lazySizes=lazySizes,typeof module=="object"&&module.exports&&(module.exports=lazySizes)})(typeof window<"u"?window:{},function(window2,document2,Date2){"use strict";var lazysizes,lazySizesCfg;if((function(){var prop,lazySizesDefaults={lazyClass:"lazyload",loadedClass:"lazyloaded",loadingClass:"lazyloading",preloadClass:"lazypreload",loadafterClass:"lazyloadafter",errorClass:"lazyerror",autosizesClass:"lazyautosizes",srcAttr:"data-src",srcsetAttr:"data-srcset",sizesAttr:"data-sizes",minSize:40,customMedia:{},init:!0,expFactor:1.5,hFac:.8,loadMode:2,loadHidden:!0,ricTimeout:0,throttleDelay:125};lazySizesCfg=window2.lazySizesConfig||window2.lazysizesConfig||{};for(prop in lazySizesDefaults)prop in lazySizesCfg||(lazySizesCfg[prop]=lazySizesDefaults[prop])})(),!document2||!document2.getElementsByClassName)return{init:function(){},cfg:lazySizesCfg,noSupport:!0};var docElem=document2.documentElement,supportPicture=window2.HTMLPictureElement,_addEventListener="addEventListener",_getAttribute="getAttribute",addEventListener=window2[_addEventListener].bind(window2),setTimeout2=window2.setTimeout,requestAnimationFrame2=window2.requestAnimationFrame||setTimeout2,requestIdleCallback=window2.requestIdleCallback,regPicture=/^picture$/i,loadEvents=["load","error","lazyincluded","_lazyloaded"],regClassCache={},forEach=Array.prototype.forEach,hasClass=function(ele,cls){return regClassCache[cls]||(regClassCache[cls]=new RegExp("(\\s|^)"+cls+"(\\s|$)")),regClassCache[cls].test(ele[_getAttribute]("class")||"")&®ClassCache[cls]},addClass=function(ele,cls){hasClass(ele,cls)||ele.setAttribute("class",(ele[_getAttribute]("class")||"").trim()+" "+cls)},removeClass=function(ele,cls){var reg2;(reg2=hasClass(ele,cls))&&ele.setAttribute("class",(ele[_getAttribute]("class")||"").replace(reg2," "))},addRemoveLoadEvents=function(dom,fn,add){var action=add?_addEventListener:"removeEventListener";add&&addRemoveLoadEvents(dom,fn),loadEvents.forEach(function(evt){dom[action](evt,fn)})},triggerEvent=function(elem,name,detail,noBubbles,noCancelable){var event=document2.createEvent("Event");return detail||(detail={}),detail.instance=lazysizes,event.initEvent(name,!noBubbles,!noCancelable),event.detail=detail,elem.dispatchEvent(event),event},updatePolyfill=function(el,full){var polyfill;!supportPicture&&(polyfill=window2.picturefill||lazySizesCfg.pf)?(full&&full.src&&!el[_getAttribute]("srcset")&&el.setAttribute("srcset",full.src),polyfill({reevaluate:!0,elements:[el]})):full&&full.src&&(el.src=full.src)},getCSS=function(elem,style){return(getComputedStyle(elem,null)||{})[style]},getWidth=function(elem,parent,width){for(width=width||elem.offsetWidth;width<lazySizesCfg.minSize&&parent&&!elem._lazysizesWidth;)width=parent.offsetWidth,parent=parent.parentNode;return width},rAF=(function(){var running,waiting,firstFns=[],secondFns=[],fns=firstFns,run=function(){var runFns=fns;for(fns=firstFns.length?secondFns:firstFns,running=!0,waiting=!1;runFns.length;)runFns.shift()();running=!1},rafBatch=function(fn,queue){running&&!queue?fn.apply(this,arguments):(fns.push(fn),waiting||(waiting=!0,(document2.hidden?setTimeout2:requestAnimationFrame2)(run)))};return rafBatch._lsFlush=run,rafBatch})(),rAFIt=function(fn,simple){return simple?function(){rAF(fn)}:function(){var that=this,args=arguments;rAF(function(){fn.apply(that,args)})}},throttle2=function(fn){var running,lastTime=0,gDelay=lazySizesCfg.throttleDelay,rICTimeout=lazySizesCfLine truncated
|
jQuery.cookie=function(e,o,t){if(o===void 0){var i=null;if(document.cookie&&document.cookie!="")for(var r=document.cookie.split(";"),n=0;n<r.length;n++){var c=jQuery.trim(r[n]);if(c.substring(0,e.length+1)==e+"="){i=decodeURIComponent(c.substring(e.length+1));break}}return i}t=t||{},o===null&&(o="",t.expires=-1);var a="";if(t.expires&&(typeof t.expires=="number"||t.expires.toUTCString)){var l2;typeof t.expires=="number"?(l2=new Date,l2.setTime(l2.getTime()+24*t.expires*60*60*1e3)):l2=t.expires,a="; expires="+l2.toUTCString()}var u=t.path?"; path="+t.path:"",s=t.domain?"; domain="+t.domain:"",m=t.secure?"; secure":"";document.cookie=[e,"=",encodeURIComponent(o),a,u,s,m].join("")};var lcs={get:function(e){return window.localStorage?localStorage.getItem(e):jQuery.cookie(e)},set:function(e,o){window.localStorage?localStorage[e]=o:jQuery.cookie(e,o)},remove:function(e){window.localStorage?localStorage.removeItem(e):jQuery.cookie(e,"")}};function aksrt__(key){var map=window._win&&window._win.i18n;return map&&Object.prototype.hasOwnProperty.call(map,key)?map[key]:key}function aksrt_is_fatal_error_response(text){return text&&(text.indexOf("Fatal error")>-1||text.indexOf("致命错误")>-1)}function aksrt_sprintf(key){var args=Array.prototype.slice.call(arguments,1),str=aksrt__(key),autoIndex=0;return str.replace(/%(\d+\$)?s/g,function(match,num){var idx=num?parseInt(num,10)-1:autoIndex++;return args[idx]!==void 0&&args[idx]!==null?args[idx]:match})}(function(window2,factory){var lazySizes=factory(window2,window2.document,Date);window2.lazySizes=lazySizes,typeof module=="object"&&module.exports&&(module.exports=lazySizes)})(typeof window<"u"?window:{},function(window2,document2,Date2){"use strict";var lazysizes,lazySizesCfg;if((function(){var prop,lazySizesDefaults={lazyClass:"lazyload",loadedClass:"lazyloaded",loadingClass:"lazyloading",preloadClass:"lazypreload",loadafterClass:"lazyloadafter",errorClass:"lazyerror",autosizesClass:"lazyautosizes",srcAttr:"data-src",srcsetAttr:"data-srcset",sizesAttr:"data-sizes",minSize:40,customMedia:{},init:!0,expFactor:1.5,hFac:.8,loadMode:2,loadHidden:!0,ricTimeout:0,throttleDelay:125};lazySizesCfg=window2.lazySizesConfig||window2.lazysizesConfig||{};for(prop in lazySizesDefaults)prop in lazySizesCfg||(lazySizesCfg[prop]=lazySizesDefaults[prop])})(),!document2||!document2.getElementsByClassName)return{init:function(){},cfg:lazySizesCfg,noSupport:!0};var docElem=document2.documentElement,supportPicture=window2.HTMLPictureElement,_addEventListener="addEventListener",_getAttribute="getAttribute",addEventListener=window2[_addEventListener].bind(window2),setTimeout2=window2.setTimeout,requestAnimationFrame2=window2.requestAnimationFrame||setTimeout2,requestIdleCallback=window2.requestIdleCallback,regPicture=/^picture$/i,loadEvents=["load","error","lazyincluded","_lazyloaded"],regClassCache={},forEach=Array.prototype.forEach,hasClass=function(ele,cls){return regClassCache[cls]||(regClassCache[cls]=new RegExp("(\\s|^)"+cls+"(\\s|$)")),regClassCache[cls].test(ele[_getAttribute]("class")||"")&®ClassCache[cls]},addClass=function(ele,cls){hasClass(ele,cls)||ele.setAttribute("class",(ele[_getAttribute]("class")||"").trim()+" "+cls)},removeClass=function(ele,cls){var reg2;(reg2=hasClass(ele,cls))&&ele.setAttribute("class",(ele[_getAttribute]("class")||"").replace(reg2," "))},addRemoveLoadEvents=function(dom,fn,add){var action=add?_addEventListener:"removeEventListener";add&&addRemoveLoadEvents(dom,fn),loadEvents.forEach(function(evt){dom[action](evt,fn)})},triggerEvent=function(elem,name,detail,noBubbles,noCancelable){var event=document2.createEvent("Event");return detail||(detail={}),detail.instance=lazysizes,event.initEvent(name,!noBubbles,!noCancelable),event.detail=detail,elem.dispatchEvent(event),event},updatePolyfill=function(el,full){var polyfill;!supportPicture&&(polyfill=window2.picturefill||lazySizesCfg.pf)?(full&&full.src&&!el[_getAttribute]("srcset")&&el.setAttribute("srcset",full.src),polyfill({reevaluate:!0,elements:[el]})):full&&full.src&&(el.src=full.src)},getCSS=function(elem,style){return(getComputedStyle(elem,null)||{})[style]},getWidth=function(elem,parent,width){for(width=width||elem.offsetWidth;width<lazySizesCfg.minSize&&parent&&!elem._lazysizesWidth;)width=parent.offsetWidth,parent=parent.parentNode;return width},rAF=(function(){var running,waiting,firstFns=[],secondFns=[],fns=firstFns,run=function(){var runFns=fns;for(fns=firstFns.length?secondFns:firstFns,running=!0,waiting=!1;runFns.length;)runFns.shift()();running=!1},rafBatch=function(fn,queue){running&&!queue?fn.apply(this,arguments):(fns.push(fn),waiting||(waiting=!0,(document2.hidden?setTimeout2:requestAnimationFrame2)(run)))};return rafBatch._lsFlush=run,rafBatch})(),rAFIt=function(fn,simple){return simple?function(){rAF(fn)}:function(){var that=this,args=arguments;rAF(function(){fn.apply(that,args)})}},throttle2=function(fn){var running,lastTime=0,gDelay=lazySizesCfg.throttleDelay,rICTimeout=lazySizesCfLine truncated
|
||||||
;
|
;
|
||||||
|
|
||||||
|
/* source: assets/js/captcha-front.js */
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
;
|
||||||
|
|
||||||
/* source: assets/js/instant-page.js */
|
/* source: assets/js/instant-page.js */
|
||||||
/**
|
/**
|
||||||
* AKSRT Instant Page
|
* AKSRT Instant Page
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
+132
-390
@@ -5,7 +5,8 @@
|
|||||||
* 唯一配置源:option `aksrt_captcha_config`。
|
* 唯一配置源:option `aksrt_captcha_config`。
|
||||||
* 闭环契约:
|
* 闭环契约:
|
||||||
* aksrt_get_captcha_config() -> aksrt_is_captcha_enabled_for(scene)
|
* 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。
|
* -> 业务入口 aksrt_require_captcha(scene, $payload) -> proceed/fail。
|
||||||
*
|
*
|
||||||
* 仅支持云厂商:tencent / aliyun / cloudflare / geetest。
|
* 仅支持云厂商:tencent / aliyun / cloudflare / geetest。
|
||||||
@@ -81,9 +82,16 @@ function aksrt_captcha_sanitize_scene_list($raw): array {
|
|||||||
/**
|
/**
|
||||||
* 获取人机验证完整配置(规范化,不合并历史脏键)。
|
* 获取人机验证完整配置(规范化,不合并历史脏键)。
|
||||||
*
|
*
|
||||||
|
* 结果在同一请求内缓存;后台保存后调用 aksrt_captcha_config_reset() 失效。
|
||||||
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
function aksrt_get_captcha_config(): 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', []);
|
$stored = get_option('aksrt_captcha_config', []);
|
||||||
$cfg = array_merge(aksrt_captcha_default_config(), is_array($stored) ? $stored : []);
|
$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] : [];
|
$result[$p] = isset($cfg[$p]) && is_array($cfg[$p]) ? $cfg[$p] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cache = $result;
|
||||||
return $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);
|
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)) {
|
if (!aksrt_is_captcha_enabled_for($scene)) {
|
||||||
return true;
|
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 / 代理)
|
// 客户端真实 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_date = hash_hmac('sha256', $date, 'TC3' . $secret_key, true);
|
||||||
$secret_service = hash_hmac('sha256', $service, $secret_date, 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);
|
$signature = hash_hmac('sha256', $string_to_sign, $secret_signing);
|
||||||
|
|
||||||
$authorization = 'TC3-HMAC-SHA256 '
|
$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,失败返回错误信息
|
* @return true|string 成功返回 true,失败返回错误信息
|
||||||
*/
|
*/
|
||||||
function aksrt_verify_captcha($post_data) {
|
function aksrt_verify_captcha($post_data, string $scene = '') {
|
||||||
$cfg = aksrt_get_captcha_config();
|
$cfg = aksrt_get_captcha_config();
|
||||||
if (empty($cfg['enabled']) || $cfg['active_provider'] === '') {
|
if (empty($cfg['enabled']) || $cfg['active_provider'] === '') {
|
||||||
return true;
|
return true;
|
||||||
@@ -317,7 +392,7 @@ function aksrt_verify_captcha($post_data) {
|
|||||||
$result = aksrt_verify_tencent($pcfg, $post_data, $client_ip);
|
$result = aksrt_verify_tencent($pcfg, $post_data, $client_ip);
|
||||||
break;
|
break;
|
||||||
case 'cloudflare':
|
case 'cloudflare':
|
||||||
$result = aksrt_verify_cloudflare($pcfg, $post_data, $client_ip);
|
$result = aksrt_verify_cloudflare($pcfg, $post_data, $client_ip, $scene);
|
||||||
break;
|
break;
|
||||||
case 'geetest':
|
case 'geetest':
|
||||||
$result = aksrt_verify_geetest($pcfg, $post_data);
|
$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);
|
$headers = aksrt_tencent_tc3_sign($secret_id, $secret_key, 'captcha', $timestamp, $date, 'DescribeCaptchaResult', '2019-07-22', $payload);
|
||||||
|
|
||||||
$curl_headers = [];
|
$resp = wp_remote_post('https://captcha.tencentcloudapi.com/', [
|
||||||
foreach ($headers as $k => $v) {
|
'timeout' => 5,
|
||||||
$curl_headers[] = "{$k}: {$v}";
|
'headers' => $headers,
|
||||||
}
|
'body' => $payload,
|
||||||
|
|
||||||
$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,
|
|
||||||
]);
|
]);
|
||||||
$response_body = curl_exec($ch);
|
if (is_wp_error($resp)) {
|
||||||
$curl_error = curl_error($ch);
|
aksrt_captcha_log('tencent', $resp->get_error_message());
|
||||||
curl_close($ch);
|
return __('人机验证服务暂不可用,请稍后重试', 'aksrt');
|
||||||
|
|
||||||
if ($curl_error) {
|
|
||||||
return '验证码服务请求失败: ' . $curl_error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$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'])) {
|
if (isset($body['Response']['Error'])) {
|
||||||
$err = $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'] ?? [];
|
$resp_data = $body['Response'] ?? [];
|
||||||
$captcha_code = (int) ($resp['CaptchaCode'] ?? 0);
|
$captcha_code = (int) ($resp_data['CaptchaCode'] ?? 0);
|
||||||
if ($captcha_code === 1) {
|
if ($captcha_code === 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -411,16 +478,16 @@ function aksrt_verify_tencent(array $pcfg, array $post_data, string $client_ip)
|
|||||||
21 => '票据校验异常(风控拦截)',
|
21 => '票据校验异常(风控拦截)',
|
||||||
100 => '参数校验失败(AppId/SecretKey/Ticket 不匹配)',
|
100 => '参数校验失败(AppId/SecretKey/Ticket 不匹配)',
|
||||||
];
|
];
|
||||||
$msg = $resp['CaptchaMsg'] ?? ($messages[$captcha_code] ?? '未知错误');
|
$msg = $resp_data['CaptchaMsg'] ?? ($messages[$captcha_code] ?? '未知错误');
|
||||||
return '腾讯验证失败: ' . $msg . " (CaptchaCode: {$captcha_code})";
|
return '腾讯验证失败: ' . $msg . " (CaptchaCode: {$captcha_code})";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cloudflare Turnstile siteverify。
|
* Cloudflare Turnstile siteverify。校验 action 与 cdata(场景)绑定,防 token 转用。
|
||||||
*
|
*
|
||||||
* @return true|string
|
* @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'] ?? '');
|
$token = (string) ($post_data['cf_turnstile_response'] ?? '');
|
||||||
if ($token === '' || strlen($token) < 10) {
|
if ($token === '' || strlen($token) < 10) {
|
||||||
return __('缺少验证参数', 'aksrt');
|
return __('缺少验证参数', 'aksrt');
|
||||||
@@ -435,7 +502,7 @@ function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_i
|
|||||||
$body = [];
|
$body = [];
|
||||||
foreach ($bodies as $attempt) {
|
foreach ($bodies as $attempt) {
|
||||||
$resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
$resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
||||||
'timeout' => 10,
|
'timeout' => 5,
|
||||||
'body' => $attempt,
|
'body' => $attempt,
|
||||||
]);
|
]);
|
||||||
if (is_wp_error($resp)) {
|
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);
|
$body = json_decode(wp_remote_retrieve_body($resp), true);
|
||||||
if (!empty($body['success'])) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$codes = isset($body['error-codes']) && is_array($body['error-codes']) ? implode(', ', $body['error-codes']) : '未知错误';
|
if (is_array($body) && isset($body['error-codes'])) {
|
||||||
return 'Cloudflare 验证失败: ' . $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);
|
$sign_token = hash_hmac('sha256', $lot_number, $captcha_key);
|
||||||
|
|
||||||
$resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', [
|
$resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', [
|
||||||
'timeout' => 10,
|
'timeout' => 5,
|
||||||
'body' => [
|
'body' => [
|
||||||
'captcha_id' => (string) ($pcfg['captcha_id'] ?? ''),
|
'captcha_id' => (string) ($pcfg['captcha_id'] ?? ''),
|
||||||
'lot_number' => $lot_number,
|
'lot_number' => $lot_number,
|
||||||
@@ -481,15 +558,16 @@ function aksrt_verify_geetest(array $pcfg, array $post_data) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (is_wp_error($resp)) {
|
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);
|
$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;
|
return true;
|
||||||
}
|
}
|
||||||
$reason = $body['reason'] ?? ($body['msg'] ?? '未知错误');
|
aksrt_captcha_log('geetest', 'validate failed: ' . (string) ($body['reason'] ?? ($body['msg'] ?? 'unknown')));
|
||||||
return '极验验证失败: ' . $reason;
|
return __('人机验证未通过,请重试', 'aksrt');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -514,7 +592,7 @@ function aksrt_verify_aliyun(array $pcfg, array $post_data) {
|
|||||||
'Format' => 'JSON',
|
'Format' => 'JSON',
|
||||||
'SignatureMethod' => 'HMAC-SHA1',
|
'SignatureMethod' => 'HMAC-SHA1',
|
||||||
'SignatureVersion' => '1.0',
|
'SignatureVersion' => '1.0',
|
||||||
'SignatureNonce' => uniqid('', true),
|
'SignatureNonce' => wp_generate_uuid4(),
|
||||||
'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
|
'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||||
'Version' => '2023-03-05',
|
'Version' => '2023-03-05',
|
||||||
'CaptchaVerifyParam' => $captcha_verify_param,
|
'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'] ?? ''));
|
$params['Signature'] = aksrt_aliyun_afs_sign($params, (string) ($pcfg['access_key_secret'] ?? ''));
|
||||||
|
|
||||||
$resp = wp_remote_post('https://' . $endpoint . '/', [
|
$resp = wp_remote_post('https://' . $endpoint . '/', [
|
||||||
'timeout' => 10,
|
'timeout' => 5,
|
||||||
'body' => $params,
|
'body' => $params,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (is_wp_error($resp)) {
|
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);
|
$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'])) {
|
if (!empty($body['Success'])) {
|
||||||
$verify_result = $body['Result']['VerifyResult'] ?? false;
|
$verify_result = $body['Result']['VerifyResult'] ?? false;
|
||||||
if ($verify_result === true) {
|
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 验证失败: ' . ($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') {
|
function aksrt_render_captcha($scene = 'comment') {
|
||||||
$scene = sanitize_key((string) $scene);
|
$scene = sanitize_key((string) $scene);
|
||||||
@@ -618,354 +704,10 @@ function aksrt_render_captcha($scene = 'comment') {
|
|||||||
'scene_id' => (string) ($pcfg['scene_id'] ?? ''),
|
'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>
|
<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">
|
<input type="hidden" name="captcha_verified" id="<?php echo esc_attr($vid); ?>" value="0">
|
||||||
</div>
|
</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
|
<?php
|
||||||
}
|
}
|
||||||
@@ -29,9 +29,6 @@ class SchemaPermissions {
|
|||||||
['label' => 'Cloudflare Turnstile', 'value' => 'cloudflare'],
|
['label' => 'Cloudflare Turnstile', 'value' => 'cloudflare'],
|
||||||
['label' => '极验 GeeTest', 'value' => 'geetest'],
|
['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' => [
|
['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' => 'comment'],
|
||||||
['label' => '用户登录', 'value' => 'login'],
|
['label' => '用户登录', 'value' => 'login'],
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ class SchemaProvider {
|
|||||||
// Captcha: always surface the normalized positive scene list.
|
// Captcha: always surface the normalized positive scene list.
|
||||||
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_get_captcha_config')) {
|
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_get_captcha_config')) {
|
||||||
$normalized = 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)) {
|
if (array_key_exists($key, $normalized)) {
|
||||||
$data[$key] = $normalized[$key];
|
$data[$key] = $normalized[$key];
|
||||||
}
|
}
|
||||||
@@ -437,6 +437,9 @@ class SchemaProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$updated = update_option($option_key, $stored);
|
$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();
|
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||||
|
|
||||||
if (isset($stored['site_favicon'])) {
|
if (isset($stored['site_favicon'])) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const cssBundles = {
|
|||||||
'assets/css/frontend/ajax-spinner.css',
|
'assets/css/frontend/ajax-spinner.css',
|
||||||
'assets/css/frontend/modal-runtime.css',
|
'assets/css/frontend/modal-runtime.css',
|
||||||
'assets/css/frontend/notyf.css',
|
'assets/css/frontend/notyf.css',
|
||||||
|
'assets/css/shared/captcha.css',
|
||||||
],
|
],
|
||||||
'homepage.css': [
|
'homepage.css': [
|
||||||
'assets/css/frontend/post-card.css',
|
'assets/css/frontend/post-card.css',
|
||||||
@@ -88,6 +89,7 @@ const publicScripts = [
|
|||||||
'assets/js/color-extract.js',
|
'assets/js/color-extract.js',
|
||||||
'assets/js/effects.js',
|
'assets/js/effects.js',
|
||||||
'assets/js/runtime/main.min.js',
|
'assets/js/runtime/main.min.js',
|
||||||
|
'assets/js/captcha-front.js',
|
||||||
'assets/js/instant-page.js',
|
'assets/js/instant-page.js',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user