fix(captcha): wire scene switches and rename menu to 人机验证

- Admin: 登录权限 → 人机验证; 停用场景 → 启用场景 (opt-in)
- Normalize enabled_scenarios with legacy disabled_scenarios BC
- Sign-in: never soft-bypass captcha; cancel unlocks button
- Captcha JS: cancel path finishes pending callback as false
This commit is contained in:
2026-07-15 16:48:57 +08:00
parent 808d46664f
commit bc96edf384
10 changed files with 143 additions and 54 deletions
@@ -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();
@@ -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],
@@ -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为不限制'],
]],
],
+59 -21
View File
@@ -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<int, string>
*/
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 '<div class="aksrt-captcha-misconfig" style="font-size:12px;color:#d03050;margin:8px 0;">';
echo esc_html(sprintf(
/* translators: %s: captcha provider key */
__('人机验证服务商「%s」已启用但凭据未配齐,前台已跳过渲染。请到「功能&权限 → 登录权限」补全配置。', 'aksrt'),
__('人机验证服务商「%s」已启用但凭据未配齐,前台已跳过渲染。请到「功能&权限 → 人机验证」补全配置。', 'aksrt'),
$provider
));
echo '</div>';
@@ -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='<div style="text-align:center;padding:20px;color:#9ca3af;">验证码加载中...</div>'; }
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(){
'<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(); 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(){