diff --git a/assets/admin/src/constants/sections/permissions.js b/assets/admin/src/constants/sections/permissions.js index a555a1bd..dccb2621 100644 --- a/assets/admin/src/constants/sections/permissions.js +++ b/assets/admin/src/constants/sections/permissions.js @@ -42,10 +42,10 @@ export const permissionsSection = { schemaPage({ key: 'permissions-login', name: 'PermissionsLogin', - label: '登录权限', + label: '人机验证', route: '/permissions/login', - title: '登录权限', - desc: '登录验证、人机验证、注册限制配置。', + title: '人机验证', + desc: '云验证码服务商、场景开关与密钥配置。', canonicalDomain: 'permissions', section: 'login', }), diff --git a/assets/admin/src/pages/permissions/CommentPermissions.vue b/assets/admin/src/pages/permissions/CommentPermissions.vue index 8a35a9ae..45a9d030 100644 --- a/assets/admin/src/pages/permissions/CommentPermissions.vue +++ b/assets/admin/src/pages/permissions/CommentPermissions.vue @@ -115,12 +115,16 @@ async function load() { const comment = commentData?.data ?? commentData ?? {} const captcha = captchaRes?.data ?? captchaRes ?? {} const moderation = moderationRes?.data ?? moderationRes ?? {} + const enabledScenarios = Array.isArray(captcha.enabled_scenarios) ? captcha.enabled_scenarios : null const disabledScenarios = Array.isArray(captcha.disabled_scenarios) ? captcha.disabled_scenarios : [] + const commentSceneOn = enabledScenarios + ? enabledScenarios.includes('comment') + : !disabledScenarios.includes('comment') form.comment_require_login = !!comment.comment_require_login form.comment_allow_reply = comment.comment_allow_reply !== false - status.commentCaptchaEnabled = !!captcha.enabled && !!captcha.active_provider && !disabledScenarios.includes('comment') + status.commentCaptchaEnabled = !!captcha.enabled && !!captcha.active_provider && commentSceneOn status.moderationEnabled = !!moderation.enabled status.rateLimitEnabled = !!moderation.rate_limit_enabled status.keywordRuleCount = splitKeywords(moderation.blocked_keywords).length diff --git a/assets/admin/src/pages/permissions/DownloadPermissions.vue b/assets/admin/src/pages/permissions/DownloadPermissions.vue index 333b572d..173b1d00 100644 --- a/assets/admin/src/pages/permissions/DownloadPermissions.vue +++ b/assets/admin/src/pages/permissions/DownloadPermissions.vue @@ -54,7 +54,7 @@ diff --git a/assets/admin/src/pages/users/UsersSignin.vue b/assets/admin/src/pages/users/UsersSignin.vue index 35599db1..219783d6 100644 --- a/assets/admin/src/pages/users/UsersSignin.vue +++ b/assets/admin/src/pages/users/UsersSignin.vue @@ -227,9 +227,16 @@ async function load() { const captchaData = extractData(captchaRes) const dailySignin = creditData.daily_signin || {} const inviteReward = creditData.invite_reward || {} + const enabledScenarios = Array.isArray(captchaData.enabled_scenarios) + ? captchaData.enabled_scenarios + : null const disabledScenarios = Array.isArray(captchaData.disabled_scenarios) ? captchaData.disabled_scenarios : [] + const sceneOn = (key) => { + if (enabledScenarios) return enabledScenarios.includes(key) + return !disabledScenarios.includes(key) + } creditSnapshot.value = creditData authSnapshot.value = authData @@ -249,8 +256,8 @@ async function load() { form.invite_register_field = authData.invite_register_field || 'optional' form.invite_reject_invalid = authData.invite_reject_invalid !== false - captchaSummary.register = !!captchaData.enabled && !!captchaData.active_provider && !disabledScenarios.includes('register') - captchaSummary.signin = !!captchaData.enabled && !!captchaData.active_provider && !disabledScenarios.includes('signin') + captchaSummary.register = !!captchaData.enabled && !!captchaData.active_provider && sceneOn('register') + captchaSummary.signin = !!captchaData.enabled && !!captchaData.active_provider && sceneOn('signin') } catch (error) { message.error(error.message || '加载签到奖励配置失败') } finally { diff --git a/assets/admin/src/schema/sections/permissions/index.js b/assets/admin/src/schema/sections/permissions/index.js index df4b0b8f..734971fc 100644 --- a/assets/admin/src/schema/sections/permissions/index.js +++ b/assets/admin/src/schema/sections/permissions/index.js @@ -9,13 +9,17 @@ export const permissionsSchemas = { */ login: { key: 'permissions.login', - title: '登录权限', + title: '人机验证', icon: 'fas fa-shield-alt', - description: '管理登录、注册、评论等场景的人机验证与访问限制。', + description: '配置云验证码服务商,并选择需要强制人机验证的前端场景。', defaults: { enabled: false, active_provider: '', mode: 'always', + enabled_scenarios: [ + 'comment', 'login', 'register', 'bind', 'bind_phone', + 'lostpassword', 'change_password', 'change_email', 'verify_email', 'download', 'signin', + ], disabled_scenarios: [], tencent: {}, aliyun: {}, @@ -28,7 +32,7 @@ export const permissionsSchemas = { title: '基础设置', icon: 'fas fa-cog', fields: [ - { key: 'enabled', label: '启用人机验证', type: 'switch', help: '开启后将在指定场景要求用户完成人机验证' }, + { key: 'enabled', label: '启用人机验证', type: 'switch', help: '总开关:关闭后所有场景均不验证' }, { key: 'active_provider', label: '当前服务商', @@ -54,10 +58,10 @@ export const permissionsSchemas = { dependsOn: { key: 'enabled', value: true }, }, { - key: 'disabled_scenarios', - label: '停用场景', + key: 'enabled_scenarios', + label: '启用场景', type: 'checkbox', - help: '勾选后表示该场景不启用人机验证;未勾选的场景会启用。', + help: '勾选 = 该场景需要人机验证。取消勾选则该场景放行。', dependsOn: { key: 'enabled', value: true }, options: [ { label: '发表评论', value: 'comment' }, @@ -297,7 +301,7 @@ export const permissionsSchemas = { key: 'permissions.download', title: '下载权限', icon: 'fas fa-download', - description: '下载门槛由 DownloadPermissions 页维护(cms_features);人机验证服务商在登录权限页。', + description: '下载门槛由 DownloadPermissions 页维护(cms_features);人机验证服务商在「人机验证」页。', defaults: {}, groups: [], }, diff --git a/assets/js/credits.js b/assets/js/credits.js index 959f5f99..40a4d3ed 100644 --- a/assets/js/credits.js +++ b/assets/js/credits.js @@ -114,14 +114,28 @@ } catch (_) {} } - function triggerCaptcha(callback) { + function triggerCaptcha(onSuccess, onFail) { + var mount = document.querySelector('[data-signin-captcha]'); var state = window.AKSRTCaptchaState; var scene = state && state.scenes && state.scenes.signin; + if (scene && typeof scene.verify === 'function') { - scene.verify(callback); + scene.verify(function (ok) { + if (ok === false) { + if (typeof onFail === 'function') onFail('已取消验证'); + return; + } + onSuccess(); + }); return; } - callback(); + + if (mount) { + if (typeof onFail === 'function') onFail('人机验证加载中,请稍候再试'); + return; + } + + onSuccess(); } btn.addEventListener('click', function () { @@ -129,19 +143,13 @@ btn.innerHTML = '…'; setTip(''); - var captchaDone = false; - var timeoutId = setTimeout(function () { - if (!captchaDone) { - captchaDone = true; - doSignin(); - } - }, 30000); - triggerCaptcha(function () { - clearTimeout(timeoutId); - if (captchaDone) return; - captchaDone = true; doSignin(); + }, function (msg) { + resetCaptcha(); + if (msg) setTip(msg); + btn.disabled = false; + btn.innerHTML = '签到'; }); function doSignin() { diff --git a/inc/classes/admin/class-aksrt-admin-schema-provider.php b/inc/classes/admin/class-aksrt-admin-schema-provider.php index 555cb6e1..e0ab8b72 100644 --- a/inc/classes/admin/class-aksrt-admin-schema-provider.php +++ b/inc/classes/admin/class-aksrt-admin-schema-provider.php @@ -282,6 +282,16 @@ class AKSRT_Admin_Schema_Provider { } } + // Captcha: always surface normalized enabled_scenarios (legacy disabled list inverted). + if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_get_captcha_config')) { + $normalized = aksrt_get_captcha_config(); + foreach (['enabled', 'active_provider', 'enabled_scenarios', 'disabled_scenarios', 'tencent', 'aliyun', 'cloudflare', 'geetest', 'mode'] as $key) { + if (array_key_exists($key, $normalized)) { + $data[$key] = $normalized[$key]; + } + } + } + return $data; } @@ -302,6 +312,24 @@ class AKSRT_Admin_Schema_Provider { $stored[$key] = $value; } + // Captcha: keep enabled_scenarios / disabled_scenarios mirrors in sync. + if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_captcha_scenes')) { + $all = array_keys(aksrt_captcha_scenes()); + if (array_key_exists('enabled_scenarios', $stored)) { + $enabled = function_exists('aksrt_captcha_sanitize_scene_list') + ? aksrt_captcha_sanitize_scene_list($stored['enabled_scenarios']) + : (array) $stored['enabled_scenarios']; + $stored['enabled_scenarios'] = $enabled; + $stored['disabled_scenarios'] = array_values(array_diff($all, $enabled)); + } elseif (array_key_exists('disabled_scenarios', $stored)) { + $disabled = function_exists('aksrt_captcha_sanitize_scene_list') + ? aksrt_captcha_sanitize_scene_list($stored['disabled_scenarios']) + : (array) $stored['disabled_scenarios']; + $stored['disabled_scenarios'] = $disabled; + $stored['enabled_scenarios'] = array_values(array_diff($all, $disabled)); + } + } + $updated = update_option($option_key, $stored); AKSRT_Settings::flush_settings_cache(); diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php index dd03ac18..7f5914aa 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php @@ -53,7 +53,7 @@ class AKSRT_Admin_Schema_Extensions_Security { ['key' => 'remove_more_wp_head', 'label' => '移除WP head多余代码', 'type' => 'switch', 'default' => true], ]], ['key' => 'captcha-note', 'title' => '人机验证', 'fields' => [ - ['key' => 'captcha_redirect_hint', 'label' => '配置入口', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '人机验证(腾讯云 / 阿里云 / Cloudflare / 极验)请到「功能&权限 → 登录权限」统一配置;本页不再维护验证码字段。'], + ['key' => 'captcha_redirect_hint', 'label' => '配置入口', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '人机验证(腾讯云 / 阿里云 / Cloudflare / 极验)请到「功能&权限 → 人机验证」统一配置;本页不再维护验证码字段。'], ]], ['key' => 'rate-limit', 'title' => '频率限制', 'fields' => [ ['key' => 'brush_limit_post_enabled', 'label' => '文章发布限制', 'type' => 'switch', 'default' => false], diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php b/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php index 459b9639..0e229553 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php @@ -16,8 +16,8 @@ class AKSRT_Admin_Schema_Permissions { 'sections' => [ 'login' => [ 'key' => 'login', - 'title' => '登录权限', - 'version' => '1.0.0', + 'title' => '人机验证', + 'version' => '1.1.0', 'option_key' => 'aksrt_captcha_config', 'groups' => [ ['key' => 'basic', 'title' => '基础开关', 'fields' => [ @@ -32,7 +32,7 @@ class AKSRT_Admin_Schema_Permissions { ['key' => 'mode', 'label' => '触发模式', 'type' => 'radio', 'default' => 'always', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [ ['label' => '始终显示', 'value' => 'always'], ]], - ['key' => 'disabled_scenarios', 'label' => '停用场景', 'type' => 'checkbox', 'default' => [], 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [ + ['key' => 'enabled_scenarios', 'label' => '启用场景', 'type' => 'checkbox', 'default' => ['comment', 'login', 'register', 'bind', 'bind_phone', 'lostpassword', 'change_password', 'change_email', 'verify_email', 'download', 'signin'], 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [ ['label' => '发表评论', 'value' => 'comment'], ['label' => '用户登录', 'value' => 'login'], ['label' => '用户注册', 'value' => 'register'], @@ -138,7 +138,7 @@ class AKSRT_Admin_Schema_Permissions { ['key' => 'download_role_size', 'label' => '角色文件大小限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|max_size_mb,每行一条'], ]], ['key' => 'captcha', 'title' => '安全设置', 'fields' => [ - ['key' => 'download_captcha_hint', 'label' => '下载验证码', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '下载验证码门槛在「功能&权限 → 下载权限」页(cms_features.download_captcha)维护;服务商与场景在「登录权限」。'], + ['key' => 'download_captcha_hint', 'label' => '下载验证码', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '下载验证码门槛在「功能&权限 → 下载权限」页(cms_features.download_captcha)维护;服务商与场景在「人机验证」。'], ['key' => 'download_ip_limit', 'label' => 'IP下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '同一IP每小时最大下载次数,0为不限制'], ]], ], diff --git a/inc/classes/class-aksrt-captcha.php b/inc/classes/class-aksrt-captcha.php index 2303f9d7..78421136 100644 --- a/inc/classes/class-aksrt-captcha.php +++ b/inc/classes/class-aksrt-captcha.php @@ -46,9 +46,13 @@ function aksrt_captcha_scenes(): array { * 默认配置结构。 */ function aksrt_captcha_default_config(): array { + $all_scenes = array_keys(aksrt_captcha_scenes()); return [ 'enabled' => false, 'active_provider' => '', + // Positive list: checked scenes require captcha. Default all scenes. + 'enabled_scenarios' => $all_scenes, + // Legacy mirror kept for BC consumers / admin summaries. 'disabled_scenarios' => [], 'tencent' => [], 'aliyun' => [], @@ -57,6 +61,24 @@ function aksrt_captcha_default_config(): array { ]; } +/** + * Sanitize a list of captcha scene keys against the whitelist. + * + * @param mixed $raw + * @return array + */ +function aksrt_captcha_sanitize_scene_list($raw): array { + $list = is_array($raw) ? $raw : []; + $clean = []; + foreach ($list as $scene) { + $scene = sanitize_key((string) $scene); + if ($scene !== '' && array_key_exists($scene, aksrt_captcha_scenes()) && !in_array($scene, $clean, true)) { + $clean[] = $scene; + } + } + return array_values($clean); +} + /** * 获取人机验证完整配置(规范化,不合并历史脏键)。 * @@ -68,20 +90,26 @@ function aksrt_get_captcha_config(): array { $cfg = []; } - $default = aksrt_captcha_default_config(); + $all_scenes = array_keys(aksrt_captcha_scenes()); $provider = isset($cfg['active_provider']) ? sanitize_key((string) $cfg['active_provider']) : ''; if (!in_array($provider, aksrt_captcha_providers(), true)) { $provider = ''; } - $disabled = isset($cfg['disabled_scenarios']) ? (array) $cfg['disabled_scenarios'] : []; - $disabled = array_values(array_filter(array_map('sanitize_key', $disabled), function ($s) { - return array_key_exists($s, aksrt_captcha_scenes()); - })); + if (array_key_exists('enabled_scenarios', $cfg)) { + // Explicit positive list (may be empty = no scenes). + $enabled = aksrt_captcha_sanitize_scene_list($cfg['enabled_scenarios']); + } else { + // Legacy: only disabled list stored → all minus disabled. + $disabled_legacy = aksrt_captcha_sanitize_scene_list($cfg['disabled_scenarios'] ?? []); + $enabled = array_values(array_diff($all_scenes, $disabled_legacy)); + } + $disabled = array_values(array_diff($all_scenes, $enabled)); $result = [ 'enabled' => !empty($cfg['enabled']), 'active_provider' => $provider, + 'enabled_scenarios' => $enabled, 'disabled_scenarios' => $disabled, ]; foreach (aksrt_captcha_providers() as $p) { @@ -146,7 +174,11 @@ function aksrt_is_captcha_enabled_for($scene): bool { return false; } - return !in_array($scene, $cfg['disabled_scenarios'], true); + $enabled = isset($cfg['enabled_scenarios']) && is_array($cfg['enabled_scenarios']) + ? $cfg['enabled_scenarios'] + : array_values(array_diff(array_keys(aksrt_captcha_scenes()), (array) ($cfg['disabled_scenarios'] ?? []))); + + return in_array($scene, $enabled, true); } /** @@ -546,7 +578,7 @@ function aksrt_render_captcha($scene = 'comment') { echo '
'; echo esc_html(sprintf( /* translators: %s: captcha provider key */ - __('人机验证服务商「%s」已启用但凭据未配齐,前台已跳过渲染。请到「功能&权限 → 登录权限」补全配置。', 'aksrt'), + __('人机验证服务商「%s」已启用但凭据未配齐,前台已跳过渲染。请到「功能&权限 → 人机验证」补全配置。', 'aksrt'), $provider )); echo '
'; @@ -701,7 +733,7 @@ function submitForm(){ verified=true; hField.value='1'; if(C.scene==='download') syncDownloadSiblings(true); if(typeof pendingAction==='function'){ - var action=pendingAction; pendingAction=null; action(); return; + finishPending(true); return; } setTimeout(resetVerificationState, 3000); submitNativeForm(); @@ -717,18 +749,24 @@ function showTip(msg, color){ function showLoading(){ box.innerHTML='
验证码加载中...
'; } function clearLoading(){ box.innerHTML=''; } -root.scenes[C.scene]={ +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){ - var retry=0; - function run(){ - if(verified || hField.value==='1'){ - if(typeof callback==='function') callback(); - return; - } - pendingAction=(typeof callback==='function')?callback:null; - triggerCaptcha(); + if(verified || hField.value==='1'){ + if(typeof callback==='function') callback(true); + return; } - run(); + pendingAction=(typeof callback==='function')?function(ok){ callback(ok!==false); }:null; + triggerCaptcha(); }, reset:resetVerificationState }; @@ -744,7 +782,7 @@ function loadTencent(){ triggerCaptcha=function(){ if(!capIns){ capIns=new TencentCaptcha(C.tc.app_id, function(res){ - if(res.ret===2) return; + if(res.ret===2){ cancelPending('已取消验证'); return; } if(res.ret===0 && res.ticket){ if(res.ticket.indexOf('trerror_')===0){ showTip('验证码服务异常,请刷新页面重试', '#e67e22'); return; @@ -803,7 +841,7 @@ function loadCloudflare(){ '

请完成验证后继续操作

'+ '
'; document.body.appendChild(cfModal); - var cancelFn=function(){ cfVerifying=false; closeModal(); showTip('验证码已取消', '#e67e22'); }; + 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(); @@ -828,7 +866,7 @@ function loadGeetest(){ var product=(C.gt && C.gt.product) ? String(C.gt.product) : 'bind'; if(!captchaId){ clearLoading(); - showTip('极验 Captcha ID 未配置,请到后台「功能&权限 → 登录权限」填写并保存', '#e74c3c'); + showTip('极验 Captcha ID 未配置,请到后台「功能&权限 → 人机验证」填写并保存', '#e74c3c'); return; } function render(){