refactor(autoload): migrate theme classes to Composer PSR-4
Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
This commit is contained in:
1 parent
8f349b40fb
commit
aaf458af1c
404 files changed
+13602
-7705
No files matched your search
@@ -0,0 +1,971 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT 人机验证(Cloud Captcha)核心。
|
||||
*
|
||||
* 唯一配置源:option `aksrt_captcha_config`。
|
||||
* 闭环契约:
|
||||
* aksrt_get_captcha_config() -> aksrt_is_captcha_enabled_for(scene)
|
||||
* -> aksrt_render_captcha(scene) -> 客户端 AKSRTCaptchaState + token
|
||||
* -> 业务入口 aksrt_require_captcha(scene, $payload) -> proceed/fail。
|
||||
*
|
||||
* 仅支持云厂商:tencent / aliyun / cloudflare / geetest。
|
||||
* 不含本地图形码 / 本地滑块。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* 支持的验证服务商。
|
||||
*/
|
||||
function aksrt_captcha_providers(): array {
|
||||
return ['tencent', 'aliyun', 'cloudflare', 'geetest'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 人机验证场景白名单。
|
||||
*/
|
||||
function aksrt_captcha_scenes(): array {
|
||||
return [
|
||||
'comment' => __('发表评论', 'aksrt'),
|
||||
'login' => __('用户登录', 'aksrt'),
|
||||
'register' => __('用户注册', 'aksrt'),
|
||||
'bind' => __('绑定第三方', 'aksrt'),
|
||||
'bind_phone' => __('绑定手机', 'aksrt'),
|
||||
'lostpassword' => __('找回密码', 'aksrt'),
|
||||
'change_password' => __('修改密码', 'aksrt'),
|
||||
'change_email' => __('更改邮箱', 'aksrt'),
|
||||
'verify_email' => __('邮箱验证', 'aksrt'),
|
||||
'download' => __('资源下载', 'aksrt'),
|
||||
'signin' => __('每日签到', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认配置结构。
|
||||
*/
|
||||
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' => [],
|
||||
'cloudflare' => [],
|
||||
'geetest' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取人机验证完整配置(规范化,不合并历史脏键)。
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function aksrt_get_captcha_config(): array {
|
||||
$cfg = get_option('aksrt_captcha_config', []);
|
||||
if (!is_array($cfg)) {
|
||||
$cfg = [];
|
||||
}
|
||||
|
||||
$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 = '';
|
||||
}
|
||||
|
||||
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) {
|
||||
$result[$p] = isset($cfg[$p]) && is_array($cfg[$p]) ? $cfg[$p] : [];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 各服务商必填字段。
|
||||
*/
|
||||
function aksrt_captcha_required_fields(string $provider): array {
|
||||
$map = [
|
||||
'tencent' => ['secret_id', 'secret_key', 'app_id', 'app_secret_key'],
|
||||
'aliyun' => ['region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'],
|
||||
'cloudflare' => ['site_key', 'secret_key'],
|
||||
'geetest' => ['captcha_id', 'captcha_key'],
|
||||
];
|
||||
return $map[$provider] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前服务商必填凭据是否齐全(避免空 captcha_id 打到第三方 403)。
|
||||
*/
|
||||
function aksrt_captcha_provider_is_configured(string $provider, array $pcfg = []): bool {
|
||||
if (!in_array($provider, aksrt_captcha_providers(), true)) {
|
||||
return false;
|
||||
}
|
||||
$required = aksrt_captcha_required_fields($provider);
|
||||
if (empty($required)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($required as $field) {
|
||||
if (!isset($pcfg[$field]) || trim((string) $pcfg[$field]) === '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定场景是否启用人机验证。
|
||||
*
|
||||
* @param string $scene 见 aksrt_captcha_scenes()
|
||||
* @return bool
|
||||
*/
|
||||
function aksrt_is_captcha_enabled_for($scene): bool {
|
||||
$scene = sanitize_key((string) $scene);
|
||||
if (!array_key_exists($scene, aksrt_captcha_scenes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cfg = aksrt_get_captcha_config();
|
||||
if (empty($cfg['enabled']) || $cfg['active_provider'] === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$provider = $cfg['active_provider'];
|
||||
$pcfg = $cfg[$provider] ?? [];
|
||||
if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务入口统一调用:未启用放行;启用则校验。
|
||||
*
|
||||
* @param string $scene 场景
|
||||
* @param array $payload 客户端提交的验证字段
|
||||
* @return true|string 成功返回 true,失败返回错误信息字符串
|
||||
*/
|
||||
function aksrt_require_captcha(string $scene, array $payload = []) {
|
||||
if (!aksrt_is_captcha_enabled_for($scene)) {
|
||||
return true;
|
||||
}
|
||||
return aksrt_verify_captcha($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台摘要:enabled / provider / configured / 场景命中状态。
|
||||
*/
|
||||
function aksrt_get_captcha_status_summary(): array {
|
||||
$cfg = aksrt_get_captcha_config();
|
||||
$provider = $cfg['active_provider'];
|
||||
$scenes = [];
|
||||
foreach (aksrt_captcha_scenes() as $key => $label) {
|
||||
$scenes[$key] = aksrt_is_captcha_enabled_for($key);
|
||||
}
|
||||
return [
|
||||
'enabled' => !empty($cfg['enabled']),
|
||||
'provider' => $provider,
|
||||
'configured' => $provider !== '' && aksrt_captcha_provider_is_configured($provider, $cfg[$provider] ?? []),
|
||||
'scenes' => $scenes,
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 客户端真实 IP(穿透 CDN / 代理)
|
||||
// ============================================================
|
||||
|
||||
function aksrt_get_client_ip(): string {
|
||||
$keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];
|
||||
foreach ($keys as $key) {
|
||||
if (!empty($_SERVER[$key])) {
|
||||
$parts = explode(',', (string) $_SERVER[$key]);
|
||||
$ip = trim($parts[0]);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 腾讯云 API 3.0 TC3-HMAC-SHA256 签名
|
||||
// ============================================================
|
||||
|
||||
function aksrt_tencent_tc3_sign($secret_id, $secret_key, $service, $timestamp, $date, $action, $version, $payload): array {
|
||||
$content_type = 'application/json; charset=utf-8';
|
||||
$host = 'captcha.tencentcloudapi.com';
|
||||
$payload_hash = hash('sha256', $payload);
|
||||
$canonical_headers = "content-type:{$content_type}\nhost:{$host}\n";
|
||||
$signed_headers = 'content-type;host';
|
||||
$canonical_request = "POST\n/\n\n{$canonical_headers}\n{$signed_headers}\n{$payload_hash}";
|
||||
$credential_scope = "{$date}/{$service}/tc3_request";
|
||||
$string_to_sign = "TC3-HMAC-SHA256\n{$timestamp}\n{$credential_scope}\n" . hash('sha256', $canonical_request);
|
||||
|
||||
$secret_date = hash_hmac('sha256', $date, 'TC3' . $secret_key, true);
|
||||
$secret_service = hash_hmac('sha256', $service, $secret_date, true);
|
||||
$secret_signing = hash_hmac('sha256', 'tc3_request', $secret_service, true);
|
||||
$signature = hash_hmac('sha256', $string_to_sign, $secret_signing);
|
||||
|
||||
$authorization = 'TC3-HMAC-SHA256 '
|
||||
. "Credential={$secret_id}/{$credential_scope}, "
|
||||
. "SignedHeaders={$signed_headers}, "
|
||||
. "Signature={$signature}";
|
||||
|
||||
return [
|
||||
'Authorization' => $authorization,
|
||||
'Content-Type' => $content_type,
|
||||
'Host' => $host,
|
||||
'X-TC-Action' => $action,
|
||||
'X-TC-Timestamp'=> (string) $timestamp,
|
||||
'X-TC-Version' => $version,
|
||||
'X-TC-Region' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 阿里云 OpenAPI POP Signature v1.0 (HMAC-SHA1)。
|
||||
*/
|
||||
function aksrt_aliyun_afs_sign(array $params, string $secret): string {
|
||||
ksort($params);
|
||||
$canonicalized = '';
|
||||
foreach ($params as $k => $v) {
|
||||
$canonicalized .= '&' . rawurlencode((string) $k) . '=' . rawurlencode((string) $v);
|
||||
}
|
||||
$string_to_sign = 'POST&%2F&' . rawurlencode(substr($canonicalized, 1));
|
||||
return base64_encode(hash_hmac('sha1', $string_to_sign, $secret . '&', true));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 服务端二次校验
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 校验客户端提交的人机验证字段。
|
||||
*
|
||||
* @param array $post_data 客户端字段(captcha_verified + 厂商 token)
|
||||
* @return true|string 成功返回 true,失败返回错误信息
|
||||
*/
|
||||
function aksrt_verify_captcha($post_data) {
|
||||
$cfg = aksrt_get_captcha_config();
|
||||
if (empty($cfg['enabled']) || $cfg['active_provider'] === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$provider = $cfg['active_provider'];
|
||||
$pcfg = $cfg[$provider] ?? [];
|
||||
$client_ip = aksrt_get_client_ip();
|
||||
|
||||
// 客户端验证字段(前端写入 captcha_verified=1 表示用户已完成交互)
|
||||
if (($post_data['captcha_verified'] ?? '') !== '1') {
|
||||
return __('人机验证失败', 'aksrt');
|
||||
}
|
||||
|
||||
switch ($provider) {
|
||||
case 'tencent':
|
||||
$result = aksrt_verify_tencent($pcfg, $post_data, $client_ip);
|
||||
break;
|
||||
case 'cloudflare':
|
||||
$result = aksrt_verify_cloudflare($pcfg, $post_data, $client_ip);
|
||||
break;
|
||||
case 'geetest':
|
||||
$result = aksrt_verify_geetest($pcfg, $post_data);
|
||||
break;
|
||||
case 'aliyun':
|
||||
$result = aksrt_verify_aliyun($pcfg, $post_data);
|
||||
break;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
||||
return $result === true ? true : ($result ?: __('人机验证失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 腾讯云 Captcha 2.0(DescribeCaptchaResult + TC3-HMAC-SHA256)。
|
||||
*
|
||||
* @return true|string
|
||||
*/
|
||||
function aksrt_verify_tencent(array $pcfg, array $post_data, string $client_ip) {
|
||||
$ticket = (string) ($post_data['tencent_ticket'] ?? '');
|
||||
$randstr = (string) ($post_data['tencent_randstr'] ?? '');
|
||||
if ($ticket === '' || $randstr === '') {
|
||||
return __('缺少验证参数', 'aksrt');
|
||||
}
|
||||
if (str_starts_with($ticket, 'trerror_')) {
|
||||
return __('验证码 SDK 异常,请刷新页面重试(容灾票据)', 'aksrt');
|
||||
}
|
||||
|
||||
$secret_id = (string) ($pcfg['secret_id'] ?? '');
|
||||
$secret_key = (string) ($pcfg['secret_key'] ?? '');
|
||||
if ($secret_id === '' || $secret_key === '') {
|
||||
return __('验证码配置不完整:缺少云 API 密钥 (SecretId/SecretKey)', 'aksrt');
|
||||
}
|
||||
|
||||
$timestamp = time();
|
||||
$date = gmdate('Y-m-d', $timestamp);
|
||||
$payload = wp_json_encode([
|
||||
'CaptchaType' => 9,
|
||||
'CaptchaAppId' => (int) ($pcfg['app_id'] ?? 0),
|
||||
'AppSecretKey' => (string) ($pcfg['app_secret_key'] ?? ''),
|
||||
'Ticket' => $ticket,
|
||||
'Randstr' => $randstr,
|
||||
'UserIp' => $client_ip,
|
||||
]);
|
||||
|
||||
$headers = aksrt_tencent_tc3_sign($secret_id, $secret_key, 'captcha', $timestamp, $date, 'DescribeCaptchaResult', '2019-07-22', $payload);
|
||||
|
||||
$curl_headers = [];
|
||||
foreach ($headers as $k => $v) {
|
||||
$curl_headers[] = "{$k}: {$v}";
|
||||
}
|
||||
|
||||
$ch = curl_init('https://captcha.tencentcloudapi.com/');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => $curl_headers,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
]);
|
||||
$response_body = curl_exec($ch);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curl_error) {
|
||||
return '验证码服务请求失败: ' . $curl_error;
|
||||
}
|
||||
|
||||
$body = json_decode((string) $response_body, true);
|
||||
if (isset($body['Response']['Error'])) {
|
||||
$err = $body['Response']['Error'];
|
||||
return '腾讯 API 调用失败: ' . ($err['Message'] ?? $err['Code'] ?? '未知错误');
|
||||
}
|
||||
|
||||
$resp = $body['Response'] ?? [];
|
||||
$captcha_code = (int) ($resp['CaptchaCode'] ?? 0);
|
||||
if ($captcha_code === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$messages = [
|
||||
0 => '验证未通过',
|
||||
7 => 'Randstr 不合法',
|
||||
8 => 'Ticket 已过期(有效期 5 分钟)',
|
||||
9 => 'Ticket 被重复使用',
|
||||
15 => 'Ticket 不合法(解密失败)',
|
||||
16 => 'CaptchaAppId 与 Ticket 不匹配',
|
||||
21 => '票据校验异常(风控拦截)',
|
||||
100 => '参数校验失败(AppId/SecretKey/Ticket 不匹配)',
|
||||
];
|
||||
$msg = $resp['CaptchaMsg'] ?? ($messages[$captcha_code] ?? '未知错误');
|
||||
return '腾讯验证失败: ' . $msg . " (CaptchaCode: {$captcha_code})";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloudflare Turnstile siteverify。
|
||||
*
|
||||
* @return true|string
|
||||
*/
|
||||
function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_ip) {
|
||||
$token = (string) ($post_data['cf_turnstile_response'] ?? '');
|
||||
if ($token === '' || strlen($token) < 10) {
|
||||
return __('缺少验证参数', 'aksrt');
|
||||
}
|
||||
|
||||
$secret = (string) ($pcfg['secret_key'] ?? '');
|
||||
$bodies = [
|
||||
['secret' => $secret, 'response' => $token],
|
||||
['secret' => $secret, 'response' => $token, 'remoteip' => $client_ip],
|
||||
];
|
||||
|
||||
$body = [];
|
||||
foreach ($bodies as $attempt) {
|
||||
$resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
||||
'timeout' => 10,
|
||||
'body' => $attempt,
|
||||
]);
|
||||
if (is_wp_error($resp)) {
|
||||
continue;
|
||||
}
|
||||
$body = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (!empty($body['success'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$codes = isset($body['error-codes']) && is_array($body['error-codes']) ? implode(', ', $body['error-codes']) : '未知错误';
|
||||
return 'Cloudflare 验证失败: ' . $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 极验 GeeTest v4 validate。
|
||||
*
|
||||
* @return true|string
|
||||
*/
|
||||
function aksrt_verify_geetest(array $pcfg, array $post_data) {
|
||||
$lot_number = (string) ($post_data['geetest_lot_number'] ?? '');
|
||||
$captcha_output = (string) ($post_data['geetest_captcha_output'] ?? '');
|
||||
$pass_token = (string) ($post_data['geetest_pass_token'] ?? '');
|
||||
$gen_time = (string) ($post_data['geetest_gen_time'] ?? '');
|
||||
if ($lot_number === '' || $captcha_output === '' || $pass_token === '') {
|
||||
return __('缺少验证参数', 'aksrt');
|
||||
}
|
||||
|
||||
$captcha_key = (string) ($pcfg['captcha_key'] ?? '');
|
||||
$sign_token = hash_hmac('sha256', $lot_number, $captcha_key);
|
||||
|
||||
$resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', [
|
||||
'timeout' => 10,
|
||||
'body' => [
|
||||
'captcha_id' => (string) ($pcfg['captcha_id'] ?? ''),
|
||||
'lot_number' => $lot_number,
|
||||
'captcha_output' => $captcha_output,
|
||||
'pass_token' => $pass_token,
|
||||
'gen_time' => $gen_time,
|
||||
'sign_token' => $sign_token,
|
||||
],
|
||||
]);
|
||||
|
||||
if (is_wp_error($resp)) {
|
||||
return '验证请求失败: ' . $resp->get_error_message();
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (!empty($body['result']) && $body['result'] === 'success') {
|
||||
return true;
|
||||
}
|
||||
$reason = $body['reason'] ?? ($body['msg'] ?? '未知错误');
|
||||
return '极验验证失败: ' . $reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 阿里云 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha)。
|
||||
*
|
||||
* @return true|string
|
||||
*/
|
||||
function aksrt_verify_aliyun(array $pcfg, array $post_data) {
|
||||
$captcha_verify_param = (string) ($post_data['aliyun_captcha_verify_param'] ?? '');
|
||||
if ($captcha_verify_param === '') {
|
||||
return __('缺少验证参数 (captchaVerifyParam)', 'aksrt');
|
||||
}
|
||||
|
||||
$region = (string) ($pcfg['region'] ?? 'cn');
|
||||
$endpoint = $region === 'sgp'
|
||||
? 'captcha.ap-southeast-1.aliyuncs.com'
|
||||
: 'captcha.cn-shanghai.aliyuncs.com';
|
||||
|
||||
$params = [
|
||||
'AccessKeyId' => (string) ($pcfg['access_key_id'] ?? ''),
|
||||
'Action' => 'VerifyIntelligentCaptcha',
|
||||
'Format' => 'JSON',
|
||||
'SignatureMethod' => 'HMAC-SHA1',
|
||||
'SignatureVersion' => '1.0',
|
||||
'SignatureNonce' => uniqid('', true),
|
||||
'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||
'Version' => '2023-03-05',
|
||||
'CaptchaVerifyParam' => $captcha_verify_param,
|
||||
];
|
||||
if (!empty($pcfg['scene_id'])) {
|
||||
$params['SceneId'] = (string) $pcfg['scene_id'];
|
||||
}
|
||||
$params['Signature'] = aksrt_aliyun_afs_sign($params, (string) ($pcfg['access_key_secret'] ?? ''));
|
||||
|
||||
$resp = wp_remote_post('https://' . $endpoint . '/', [
|
||||
'timeout' => 10,
|
||||
'body' => $params,
|
||||
]);
|
||||
|
||||
if (is_wp_error($resp)) {
|
||||
return '验证请求失败: ' . $resp->get_error_message();
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (!empty($body['Success'])) {
|
||||
$verify_result = $body['Result']['VerifyResult'] ?? false;
|
||||
if ($verify_result === true) {
|
||||
return true;
|
||||
}
|
||||
$code_messages = [
|
||||
'F001' => '疑似攻击请求,风险策略不通过',
|
||||
'F002' => '验证参数为空',
|
||||
'F003' => '验证参数格式不合法',
|
||||
'F004' => '测试模式配置为不通过',
|
||||
'F005' => '场景 ID 不合法',
|
||||
'F006' => '场景 ID 与配置不匹配',
|
||||
'F008' => '验证数据重复提交',
|
||||
'F009' => '检测到虚拟设备环境',
|
||||
'F010' => '同 IP 访问频率超限',
|
||||
'F011' => '同设备访问频率超限',
|
||||
'F012' => 'SceneID 与前端配置不一致',
|
||||
'F014' => '无初始化记录或已过期(20 分钟)',
|
||||
'F015' => '验证交互不通过',
|
||||
'F018' => 'CaptchaVerifyParam 重复使用',
|
||||
'F019' => '验证请求超时(90 秒)',
|
||||
'F020' => 'CaptchaVerifyParam 与场景 ID 不匹配',
|
||||
];
|
||||
$verify_code = (string) ($body['Result']['VerifyCode'] ?? '');
|
||||
return '阿里云 V3 验证失败: ' . ($code_messages[$verify_code] ?? '未知错误 (' . ($verify_code ?: 'N/A') . ')');
|
||||
}
|
||||
|
||||
return '阿里云 V3 验证失败: ' . ($body['Message'] ?? '未知错误');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 前端渲染(挂载点 + 内联 SDK + AKSRTCaptchaState)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 渲染人机验证挂载点。未启用场景直接返回。
|
||||
*/
|
||||
function aksrt_render_captcha($scene = 'comment') {
|
||||
$scene = sanitize_key((string) $scene);
|
||||
if (!aksrt_is_captcha_enabled_for($scene)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cfg = aksrt_get_captcha_config();
|
||||
$provider = $cfg['active_provider'];
|
||||
$pcfg = $cfg[$provider] ?? [];
|
||||
|
||||
if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) {
|
||||
if (current_user_can('manage_options')) {
|
||||
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'),
|
||||
$provider
|
||||
));
|
||||
echo '</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$uid = 'aksrt-captcha-' . $scene . '-' . wp_unique_id();
|
||||
$vid = 'captcha_verified_' . $scene . '_' . wp_unique_id();
|
||||
|
||||
$js_config = [
|
||||
'provider' => $provider,
|
||||
'scene' => $scene,
|
||||
'uid' => $uid,
|
||||
'vid' => $vid,
|
||||
'tc' => ['app_id' => (string) ($pcfg['app_id'] ?? '')],
|
||||
'cf' => [
|
||||
'site_key' => (string) ($pcfg['site_key'] ?? ''),
|
||||
'theme' => (string) ($pcfg['theme'] ?? 'auto'),
|
||||
'size' => (string) ($pcfg['size'] ?? 'normal'),
|
||||
],
|
||||
'gt' => [
|
||||
'captcha_id' => trim((string) ($pcfg['captcha_id'] ?? '')),
|
||||
'product' => (string) ($pcfg['product'] ?? 'bind'),
|
||||
],
|
||||
'al' => [
|
||||
'region' => (string) ($pcfg['region'] ?? 'cn'),
|
||||
'prefix' => (string) ($pcfg['prefix'] ?? ''),
|
||||
'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 id="<?php echo esc_attr($uid); ?>" class="aksrt-captcha-box" style="display:none"></div>
|
||||
<input type="hidden" name="captcha_verified" id="<?php echo esc_attr($vid); ?>" value="0">
|
||||
</div>
|
||||
<link rel="stylesheet" href="<?php echo esc_url(AKSRT_URI . '/assets/css/shared/captcha.css'); ?>">
|
||||
<script>(function(){
|
||||
var C=<?php echo wp_json_encode($js_config, JSON_UNESCAPED_UNICODE); ?>;
|
||||
var TOKEN_FIELDS=<?php echo $token_fields_json; ?>;
|
||||
var root=window.AKSRTCaptchaState=window.AKSRTCaptchaState||{loaders:{}, scenes:{}};
|
||||
|
||||
function loadScriptOnce(key, src, readyCheck){
|
||||
if(typeof readyCheck==='function' && readyCheck()){ return Promise.resolve(); }
|
||||
if(root.loaders[key]){ return root.loaders[key]; }
|
||||
root.loaders[key]=new Promise(function(resolve, reject){
|
||||
var existing=document.querySelector('script[data-aksrt-sdk="'+key+'"]');
|
||||
if(existing){
|
||||
if(typeof readyCheck==='function' && readyCheck()){ resolve(); return; }
|
||||
existing.addEventListener('load', function(){ resolve(); }, {once:true});
|
||||
existing.addEventListener('error', function(){ reject(new Error(key+' load failed')); }, {once:true});
|
||||
return;
|
||||
}
|
||||
var script=document.createElement('script');
|
||||
script.src=src; script.async=true; script.setAttribute('data-aksrt-sdk', key);
|
||||
script.onload=function(){ resolve(); };
|
||||
script.onerror=function(){ reject(new Error(key+' load failed')); };
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
return root.loaders[key];
|
||||
}
|
||||
|
||||
var box, hField, form=null;
|
||||
var verified=false;
|
||||
var pendingAction=null;
|
||||
var triggerCaptcha=function(){ showTip('验证码加载中,请稍候', '#e67e22'); };
|
||||
|
||||
function findForm(){
|
||||
form=box.closest('form');
|
||||
if(!form&&C.scene==='comment') form=document.getElementById('commentform');
|
||||
if(!form&&(C.scene==='login'||C.scene==='register')){
|
||||
form=document.getElementById('aksrt-login-form')
|
||||
||document.getElementById('aksrt-modal-login-form')
|
||||
||document.getElementById('sign-login-form')
|
||||
||document.getElementById('aksrt-phone-login-form')
|
||||
||document.getElementById('aksrt-modal-register-form')
|
||||
||document.getElementById('sign-register-form')
|
||||
||document.getElementById('aksrt-phone-register-form')
|
||||
||document.querySelector('.auth-form')
|
||||
||document.querySelector('.sign-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
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
* Creator revenue class lives at AKSRT\Payment\CreatorRevenue (Composer).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT User system module hub (procedural).
|
||||
*
|
||||
* Loads user-parts/*; keeps historical path for bootstrap.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
require_once __DIR__ . '/user-parts/template-registry.php';
|
||||
require_once __DIR__ . '/user-parts/urls.php';
|
||||
require_once __DIR__ . '/user-parts/routing.php';
|
||||
require_once __DIR__ . '/user-parts/account-ajax.php';
|
||||
@@ -0,0 +1,595 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT User Notifications
|
||||
*
|
||||
* Notification CRUD, AJAX handlers, and event-driven notification creators.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('aksrt_get_user_notifications')) :
|
||||
/**
|
||||
* 获取用户提醒列表(即时提醒,不持久化)
|
||||
*
|
||||
* 当前通知源:
|
||||
* - 邮箱待验证
|
||||
* - 评论待审核
|
||||
*
|
||||
* @param int $user_id 用户 ID,默认当前用户
|
||||
* @return array<int, array{type:string,title:string,desc:string,url:string,count:int}>
|
||||
*/
|
||||
function aksrt_get_user_notifications(int $user_id = 0): array {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
|
||||
$email_verified = true;
|
||||
if (class_exists('\AKSRT\Auth\Service')) {
|
||||
$email_verified = \AKSRT\Auth\Service::user_is_email_verified($user_id);
|
||||
}
|
||||
|
||||
if (!$email_verified && (!class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::notification_enabled('verify_email'))) {
|
||||
$items[] = [
|
||||
'type' => 'verify_email',
|
||||
'title' => __('邮箱待验证', 'aksrt'),
|
||||
'desc' => __('请尽快完成邮箱验证,部分功能需要验证后才能使用。', 'aksrt'),
|
||||
'url' => function_exists('aksrt_get_verify_email_url') ? aksrt_get_verify_email_url($user_id) : home_url('/'),
|
||||
'count' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
$pending_comments = (int) get_comments([
|
||||
'user_id' => $user_id,
|
||||
'count' => true,
|
||||
'status' => 'hold',
|
||||
]);
|
||||
|
||||
if ($pending_comments > 0 && (!class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::notification_enabled('pending_comments'))) {
|
||||
$items[] = [
|
||||
'type' => 'pending_comments',
|
||||
'title' => __('评论待审核', 'aksrt'),
|
||||
'desc' => sprintf(__('你有 %d 条评论正在等待审核。', 'aksrt'), $pending_comments),
|
||||
'url' => aksrt_get_user_center_url('comments'),
|
||||
'count' => $pending_comments,
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_notification_type_label')) :
|
||||
/** 通知类型中文标签 */
|
||||
function aksrt_notification_type_label(string $type): string {
|
||||
$map = [
|
||||
'comment_reply' => __('评论回复', 'aksrt'),
|
||||
'comment_approved' => __('评论审核', 'aksrt'),
|
||||
'download_ready' => __('下载就绪', 'aksrt'),
|
||||
'credits_added' => __('积分增加', 'aksrt'),
|
||||
'credits_deducted' => __('积分扣减', 'aksrt'),
|
||||
'verify_email' => __('邮箱验证', 'aksrt'),
|
||||
'pending_comments' => __('待审核评论', 'aksrt'),
|
||||
'notice' => __('通知', 'aksrt'),
|
||||
];
|
||||
return $map[$type] ?? $type;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_notification_count')) :
|
||||
/** 获取用户提醒总数 */
|
||||
function aksrt_get_user_notification_count(int $user_id = 0): int {
|
||||
$items = aksrt_get_user_notifications($user_id);
|
||||
$count = 0;
|
||||
foreach ($items as $item) {
|
||||
$count += max(0, (int) ($item['count'] ?? 0));
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_inbox_notifications')) :
|
||||
/** 获取用户通知收件箱 */
|
||||
function aksrt_get_user_inbox_notifications(int $user_id = 0): array {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0) return [];
|
||||
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = get_user_meta($user_id, 'aksrt_notifications', true);
|
||||
return is_array($items) ? array_values($items) : [];
|
||||
}
|
||||
|
||||
static $migrated = [];
|
||||
if (empty($migrated[$user_id])) {
|
||||
$legacy = get_user_meta($user_id, 'aksrt_notifications', true);
|
||||
if (is_array($legacy) && !empty($legacy)) {
|
||||
foreach ($legacy as $item) {
|
||||
aksrt_add_user_notification($user_id, [
|
||||
'id' => $item['id'] ?? '',
|
||||
'type' => $item['type'] ?? 'notice',
|
||||
'title' => $item['title'] ?? '',
|
||||
'desc' => $item['desc'] ?? '',
|
||||
'url' => $item['url'] ?? '',
|
||||
'read' => !empty($item['read']),
|
||||
'created_at' => $item['created_at'] ?? current_time('mysql', true),
|
||||
]);
|
||||
}
|
||||
delete_user_meta($user_id, 'aksrt_notifications');
|
||||
}
|
||||
$migrated[$user_id] = true;
|
||||
}
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id, type, title, description, target_url, is_read, created_at, read_at
|
||||
FROM {$table}
|
||||
WHERE user_id = %d
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 50",
|
||||
$user_id
|
||||
), ARRAY_A);
|
||||
|
||||
if (!is_array($rows)) return [];
|
||||
|
||||
return array_map(static function ($row) {
|
||||
return [
|
||||
'id' => (string) ($row['id'] ?? ''),
|
||||
'type' => (string) ($row['type'] ?? 'notice'),
|
||||
'title' => (string) ($row['title'] ?? ''),
|
||||
'desc' => (string) ($row['description'] ?? ''),
|
||||
'url' => (string) ($row['target_url'] ?? ''),
|
||||
'read' => !empty($row['is_read']) ? 1 : 0,
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'read_at' => (string) ($row['read_at'] ?? ''),
|
||||
];
|
||||
}, $rows);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_schedule_notification_cleanup')) :
|
||||
/** 注册通知清理定时任务 */
|
||||
function aksrt_schedule_notification_cleanup(): void {
|
||||
if (!wp_next_scheduled('aksrt_notification_cleanup')) {
|
||||
$next = (new DateTimeImmutable('tomorrow 04:10', wp_timezone()))->getTimestamp();
|
||||
wp_schedule_event($next, 'daily', 'aksrt_notification_cleanup');
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_cleanup_notifications')) :
|
||||
/**
|
||||
* 清理通知表:
|
||||
* - 已读通知保留 30 天
|
||||
* - 未读通知保留 180 天
|
||||
* - 每个用户最多保留最近 200 条通知
|
||||
*/
|
||||
function aksrt_cleanup_notifications(): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_notification_settings() : [
|
||||
'read_retention_days' => 30,
|
||||
'unread_retention_days' => 180,
|
||||
'max_per_user' => 200,
|
||||
];
|
||||
|
||||
$read_retention_days = max(1, (int) ($settings['read_retention_days'] ?? 30));
|
||||
$unread_retention_days = max(7, (int) ($settings['unread_retention_days'] ?? 180));
|
||||
$max_per_user = max(20, (int) ($settings['max_per_user'] ?? 200));
|
||||
|
||||
$read_cutoff = (new DateTimeImmutable('-' . $read_retention_days . ' days', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
$unread_cutoff = (new DateTimeImmutable('-' . $unread_retention_days . ' days', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table} WHERE is_read = 1 AND created_at < %s",
|
||||
$read_cutoff
|
||||
));
|
||||
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table} WHERE is_read = 0 AND created_at < %s",
|
||||
$unread_cutoff
|
||||
));
|
||||
|
||||
$user_ids = $wpdb->get_col("SELECT DISTINCT user_id FROM {$table}");
|
||||
if (empty($user_ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($user_ids as $user_id) {
|
||||
$user_id = (int) $user_id;
|
||||
if ($user_id <= 0) continue;
|
||||
|
||||
$ids = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d ORDER BY created_at DESC, id DESC LIMIT %d, 1000",
|
||||
$user_id,
|
||||
$max_per_user
|
||||
));
|
||||
|
||||
if (empty($ids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ids = array_map('absint', $ids);
|
||||
$ids = array_filter($ids, static function ($id) { return $id > 0; });
|
||||
if (empty($ids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$wpdb->query("DELETE FROM {$table} WHERE id IN (" . implode(',', $ids) . ")");
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
add_action('wp', 'aksrt_schedule_notification_cleanup');
|
||||
add_action('aksrt_notification_cleanup', 'aksrt_cleanup_notifications');
|
||||
|
||||
if (!function_exists('aksrt_write_user_inbox_notifications')) :
|
||||
/** 写入用户通知收件箱 */
|
||||
function aksrt_write_user_inbox_notifications(int $user_id, array $items): void {
|
||||
if ($user_id <= 0) return;
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
update_user_meta($user_id, 'aksrt_notifications', array_values(array_slice($items, 0, 50)));
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->delete($table, ['user_id' => $user_id], ['%d']);
|
||||
$items = array_values(array_slice($items, 0, 50));
|
||||
foreach ($items as $item) {
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $user_id,
|
||||
'type' => sanitize_key((string) ($item['type'] ?? 'notice')),
|
||||
'title' => sanitize_text_field((string) ($item['title'] ?? '')),
|
||||
'description' => sanitize_text_field((string) ($item['desc'] ?? '')),
|
||||
'target_url' => esc_url_raw((string) ($item['url'] ?? '')),
|
||||
'is_read' => !empty($item['read']) ? 1 : 0,
|
||||
'created_at' => (string) ($item['created_at'] ?? current_time('mysql', true)),
|
||||
'read_at' => !empty($item['read']) ? current_time('mysql', true) : null,
|
||||
], ['%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s']);
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_add_user_notification')) :
|
||||
/** 添加一条用户通知 */
|
||||
function aksrt_add_user_notification(int $user_id, array $payload): void {
|
||||
if ($user_id <= 0) return;
|
||||
$type = sanitize_key((string) ($payload['type'] ?? 'notice'));
|
||||
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::notification_enabled($type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
$created_at = (string) ($payload['created_at'] ?? current_time('mysql', true));
|
||||
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
array_unshift($items, [
|
||||
'id' => isset($payload['id']) && $payload['id'] !== '' ? sanitize_text_field((string) $payload['id']) : uniqid('notif_', true),
|
||||
'type' => $type,
|
||||
'title' => sanitize_text_field((string) ($payload['title'] ?? '')),
|
||||
'desc' => sanitize_text_field((string) ($payload['desc'] ?? '')),
|
||||
'url' => esc_url_raw((string) ($payload['url'] ?? '')),
|
||||
'read' => !empty($payload['read']) ? 1 : 0,
|
||||
'created_at' => $created_at,
|
||||
]);
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'title' => sanitize_text_field((string) ($payload['title'] ?? '')),
|
||||
'description' => sanitize_text_field((string) ($payload['desc'] ?? '')),
|
||||
'target_url' => esc_url_raw((string) ($payload['url'] ?? '')),
|
||||
'is_read' => !empty($payload['read']) ? 1 : 0,
|
||||
'created_at' => $created_at,
|
||||
'read_at' => !empty($payload['read']) ? current_time('mysql', true) : null,
|
||||
], ['%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s']);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_mark_user_notification_read')) :
|
||||
/** 标记通知已读 */
|
||||
function aksrt_mark_user_notification_read(int $user_id, string $notification_id = '', bool $mark_all = false): void {
|
||||
if ($user_id <= 0) return;
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
foreach ($items as &$item) {
|
||||
if ($mark_all || (($item['id'] ?? '') === $notification_id)) {
|
||||
$item['read'] = 1;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($mark_all) {
|
||||
$wpdb->update(
|
||||
$table,
|
||||
['is_read' => 1, 'read_at' => current_time('mysql', true)],
|
||||
['user_id' => $user_id, 'is_read' => 0],
|
||||
['%d', '%s'],
|
||||
['%d', '%d']
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->update(
|
||||
$table,
|
||||
['is_read' => 1, 'read_at' => current_time('mysql', true)],
|
||||
['id' => absint($notification_id), 'user_id' => $user_id],
|
||||
['%d', '%s'],
|
||||
['%d', '%d']
|
||||
);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_delete_user_notification')) :
|
||||
/** 删除一条用户通知 */
|
||||
function aksrt_delete_user_notification(int $user_id, string $notification_id): void {
|
||||
if ($user_id <= 0 || $notification_id === '') return;
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
$items = array_values(array_filter($items, static function ($item) use ($notification_id) {
|
||||
return (string) ($item['id'] ?? '') !== $notification_id;
|
||||
}));
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
$wpdb->delete($table, ['id' => absint($notification_id), 'user_id' => $user_id], ['%d', '%d']);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_delete_user_notifications')) :
|
||||
/** 批量删除用户通知 */
|
||||
function aksrt_delete_user_notifications(int $user_id, array $notification_ids): void {
|
||||
if ($user_id <= 0 || empty($notification_ids)) return;
|
||||
$ids = array_values(array_filter(array_map('absint', $notification_ids), static function ($id) { return $id > 0; }));
|
||||
if (empty($ids)) return;
|
||||
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
$items = array_values(array_filter($items, static function ($item) use ($ids) {
|
||||
return !in_array(absint($item['id'] ?? 0), $ids, true);
|
||||
}));
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$table} WHERE user_id = " . (int) $user_id . " AND id IN (" . implode(',', $ids) . ")"
|
||||
);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_unread_notification_count')) :
|
||||
/** 获取用户未读通知数 */
|
||||
function aksrt_get_user_unread_notification_count(int $user_id = 0): int {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
$count = 0;
|
||||
foreach ($items as $item) {
|
||||
if (empty($item['read'])) $count++;
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_tab_badge_count')) :
|
||||
/** user 底部 Tab 徽标计数:未读持久化通知 + 即时状态提醒 */
|
||||
function aksrt_get_user_tab_badge_count(int $user_id = 0): int {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::notification_user_badge_enabled()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = aksrt_get_user_unread_notification_count($user_id);
|
||||
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::notification_badge_include_stateful()) {
|
||||
$count += aksrt_get_user_notification_count($user_id);
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_all_notifications')) :
|
||||
/** 获取用户完整通知列表(持久化通知 + 即时提醒) */
|
||||
function aksrt_get_user_all_notifications(int $user_id = 0): array {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0) return [];
|
||||
|
||||
$inbox = aksrt_get_user_inbox_notifications($user_id);
|
||||
$alerts = aksrt_get_user_notifications($user_id);
|
||||
|
||||
$normalized_alerts = [];
|
||||
foreach ($alerts as $alert) {
|
||||
$normalized_alerts[] = [
|
||||
'id' => 'state_' . sanitize_key((string) ($alert['type'] ?? 'notice')),
|
||||
'type' => sanitize_key((string) ($alert['type'] ?? 'notice')),
|
||||
'title' => sanitize_text_field((string) ($alert['title'] ?? '')),
|
||||
'desc' => sanitize_text_field((string) ($alert['desc'] ?? '')),
|
||||
'url' => esc_url_raw((string) ($alert['url'] ?? '')),
|
||||
'read' => 0,
|
||||
'created_at' => current_time('mysql', true),
|
||||
'count' => max(0, (int) ($alert['count'] ?? 0)),
|
||||
'stateful' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
return array_merge($inbox, $normalized_alerts);
|
||||
}
|
||||
endif;
|
||||
|
||||
add_action('wp_ajax_aksrt_mark_notification_read', function () {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
|
||||
}
|
||||
|
||||
$nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$notification_id = sanitize_text_field((string) ($_POST['notification_id'] ?? ''));
|
||||
$mark_all = !empty($_POST['mark_all']);
|
||||
aksrt_mark_user_notification_read(get_current_user_id(), $notification_id, $mark_all);
|
||||
wp_send_json_success([
|
||||
'unread' => aksrt_get_user_unread_notification_count(get_current_user_id()),
|
||||
]);
|
||||
});
|
||||
|
||||
add_action('wp_ajax_aksrt_delete_notification', function () {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
|
||||
}
|
||||
|
||||
$nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$notification_id = sanitize_text_field((string) ($_POST['notification_id'] ?? ''));
|
||||
if ($notification_id === '') {
|
||||
wp_send_json_error(['message' => __('参数错误', 'aksrt')], 400);
|
||||
}
|
||||
|
||||
aksrt_delete_user_notification(get_current_user_id(), $notification_id);
|
||||
wp_send_json_success([
|
||||
'unread' => aksrt_get_user_unread_notification_count(get_current_user_id()),
|
||||
]);
|
||||
});
|
||||
|
||||
add_action('wp_ajax_aksrt_delete_notifications', function () {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
|
||||
}
|
||||
|
||||
$nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$ids = isset($_POST['notification_ids']) ? (array) $_POST['notification_ids'] : [];
|
||||
aksrt_delete_user_notifications(get_current_user_id(), $ids);
|
||||
wp_send_json_success([
|
||||
'unread' => aksrt_get_user_unread_notification_count(get_current_user_id()),
|
||||
]);
|
||||
});
|
||||
|
||||
add_action('wp_ajax_aksrt_check_new_notifications', function () {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
|
||||
}
|
||||
|
||||
$nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$unread = aksrt_get_user_unread_notification_count(get_current_user_id());
|
||||
wp_send_json_success([
|
||||
'count' => $unread,
|
||||
'unread' => $unread,
|
||||
]);
|
||||
});
|
||||
add_action('wp_insert_comment', function ($comment_id, $comment) {
|
||||
if (!$comment instanceof WP_Comment) return;
|
||||
if ((int) $comment->comment_parent <= 0) return;
|
||||
if ((int) $comment->comment_approved !== 1) return;
|
||||
if (get_comment_meta($comment_id, 'aksrt_reply_notified', true)) return;
|
||||
|
||||
$parent = get_comment((int) $comment->comment_parent);
|
||||
if (!$parent instanceof WP_Comment) return;
|
||||
$target_user_id = (int) $parent->user_id;
|
||||
if ($target_user_id <= 0 || $target_user_id === (int) $comment->user_id) return;
|
||||
|
||||
$post_id = (int) $comment->comment_post_ID;
|
||||
$url = get_comment_link($comment);
|
||||
aksrt_add_user_notification($target_user_id, [
|
||||
'type' => 'comment_reply',
|
||||
'title' => __('评论收到回复', 'aksrt'),
|
||||
'desc' => sprintf(__('“%s” 回复了你的评论。', 'aksrt'), get_comment_author($comment)),
|
||||
'url' => $url ?: get_permalink($post_id),
|
||||
]);
|
||||
update_comment_meta($comment_id, 'aksrt_reply_notified', 1);
|
||||
}, 20, 2);
|
||||
|
||||
add_action('transition_comment_status', function ($new_status, $old_status, $comment) {
|
||||
if (!$comment instanceof WP_Comment) return;
|
||||
if ($new_status !== 'approve' || $old_status === 'approve') return;
|
||||
|
||||
$comment_owner_id = (int) $comment->user_id;
|
||||
if ($comment_owner_id > 0 && !get_comment_meta($comment->comment_ID, 'aksrt_approved_notified', true)) {
|
||||
aksrt_add_user_notification($comment_owner_id, [
|
||||
'type' => 'comment_approved',
|
||||
'title' => __('评论已通过审核', 'aksrt'),
|
||||
'desc' => __('你的评论已通过审核,现已公开展示。', 'aksrt'),
|
||||
'url' => get_comment_link($comment),
|
||||
]);
|
||||
update_comment_meta($comment->comment_ID, 'aksrt_approved_notified', 1);
|
||||
}
|
||||
|
||||
if ((int) $comment->comment_parent <= 0) return;
|
||||
if (get_comment_meta($comment->comment_ID, 'aksrt_reply_notified', true)) return;
|
||||
|
||||
$parent = get_comment((int) $comment->comment_parent);
|
||||
if (!$parent instanceof WP_Comment) return;
|
||||
$target_user_id = (int) $parent->user_id;
|
||||
if ($target_user_id <= 0 || $target_user_id === (int) $comment->user_id) return;
|
||||
|
||||
aksrt_add_user_notification($target_user_id, [
|
||||
'type' => 'comment_reply',
|
||||
'title' => __('评论收到回复', 'aksrt'),
|
||||
'desc' => sprintf(__('“%s” 回复了你的评论。', 'aksrt'), get_comment_author($comment)),
|
||||
'url' => get_comment_link($comment),
|
||||
]);
|
||||
update_comment_meta($comment->comment_ID, 'aksrt_reply_notified', 1);
|
||||
}, 20, 3);
|
||||
|
||||
add_action('aksrt_credits_added', function ($user_id, $delta, $reason, $new_balance) {
|
||||
if ($user_id <= 0 || $delta <= 0) return;
|
||||
aksrt_add_user_notification((int) $user_id, [
|
||||
'type' => 'credits_added',
|
||||
'title' => __('积分增加', 'aksrt'),
|
||||
'desc' => sprintf(__('积分变动:+%1$d,当前余额 %2$d。', 'aksrt'), (int) $delta, (int) $new_balance),
|
||||
'url' => aksrt_get_user_center_url('credits'),
|
||||
]);
|
||||
}, 10, 4);
|
||||
|
||||
add_action('aksrt_credits_deducted', function ($user_id, $cost, $reason, $new_balance) {
|
||||
if ($user_id <= 0 || $cost <= 0) return;
|
||||
aksrt_add_user_notification((int) $user_id, [
|
||||
'type' => 'credits_deducted',
|
||||
'title' => __('积分扣减', 'aksrt'),
|
||||
'desc' => sprintf(__('积分变动:-%1$d,当前余额 %2$d。', 'aksrt'), (int) $cost, (int) $new_balance),
|
||||
'url' => aksrt_get_user_center_url('credits'),
|
||||
]);
|
||||
}, 10, 4);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
/**
|
||||
* User profile/password/email AJAX handlers
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// ============================================================
|
||||
add_action('wp_ajax_aksrt_save_profile', function () {
|
||||
if (!isset($_POST['_profile_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_profile_nonce'])), 'aksrt_profile_save')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
|
||||
}
|
||||
|
||||
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
|
||||
$result = function_exists('aksrt_save_user_center_profile')
|
||||
? aksrt_save_user_center_profile($user_id, $_POST)
|
||||
: new WP_Error('aksrt_profile_unavailable', __('保存失败', 'aksrt'));
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['message' => $result->get_error_message()]);
|
||||
}
|
||||
|
||||
wp_send_json_success(['message' => __('保存成功!', 'aksrt')]);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// AJAX: 上传打赏收款码
|
||||
// ============================================================
|
||||
add_action('wp_ajax_aksrt_upload_reward_qr', function () {
|
||||
if (!isset($_POST['_profile_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_profile_nonce'])), 'aksrt_profile_save')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
if (function_exists('aksrt_rewards_enabled') && !aksrt_rewards_enabled()) {
|
||||
wp_send_json_error(['message' => __('打赏功能未启用', 'aksrt')]);
|
||||
}
|
||||
$kind = isset($_POST['kind']) ? sanitize_key((string) $_POST['kind']) : '';
|
||||
if (!in_array($kind, ['weixin', 'alipay'], true)) {
|
||||
wp_send_json_error(['message' => __('无效类型', 'aksrt')]);
|
||||
}
|
||||
if (empty($_FILES['file']['name'])) {
|
||||
wp_send_json_error(['message' => __('请选择图片', 'aksrt')]);
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
|
||||
$file = $_FILES['file'];
|
||||
$check = wp_check_filetype_and_ext($file['tmp_name'], $file['name']);
|
||||
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
$mime = (string) ($check['type'] ?? $file['type'] ?? '');
|
||||
if ($mime === '' || !in_array($mime, $allowed, true)) {
|
||||
wp_send_json_error(['message' => __('仅支持 JPG/PNG/GIF/WebP 图片', 'aksrt')]);
|
||||
}
|
||||
if (!empty($file['size']) && (int) $file['size'] > 2 * 1024 * 1024) {
|
||||
wp_send_json_error(['message' => __('图片不能超过 2MB', 'aksrt')]);
|
||||
}
|
||||
|
||||
$attachment_id = media_handle_upload('file', 0);
|
||||
if (is_wp_error($attachment_id)) {
|
||||
wp_send_json_error(['message' => $attachment_id->get_error_message()]);
|
||||
}
|
||||
$meta = $kind === 'weixin' ? 'aksrt_rewards_weixin' : 'aksrt_rewards_alipay';
|
||||
update_user_meta($user_id, $meta, (int) $attachment_id);
|
||||
$url = wp_get_attachment_image_url((int) $attachment_id, 'full');
|
||||
wp_send_json_success([
|
||||
'message' => __('上传成功', 'aksrt'),
|
||||
'id' => (int) $attachment_id,
|
||||
'url' => is_string($url) ? $url : '',
|
||||
'kind' => $kind,
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// ============================================================
|
||||
// AJAX: 发送邮箱验证码(修改密码/更改邮箱通用)
|
||||
// ============================================================
|
||||
|
||||
if (!function_exists('aksrt_get_email_code_key')) :
|
||||
function aksrt_get_email_code_key(int $user_id, string $scene): string {
|
||||
return 'aksrt_email_code_' . $scene . '_' . $user_id;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_email_code_rate_key')) :
|
||||
function aksrt_get_email_code_rate_key(int $user_id, string $scene, string $target = ''): string {
|
||||
$suffix = $target !== '' ? '_' . md5(strtolower($target)) : '';
|
||||
return 'aksrt_email_code_rate_' . $scene . '_' . $user_id . $suffix;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_store_email_code')) :
|
||||
function aksrt_store_email_code(int $user_id, string $scene, string $code, string $target = ''): void {
|
||||
set_transient(
|
||||
aksrt_get_email_code_key($user_id, $scene),
|
||||
[
|
||||
'hash' => wp_hash_password($code),
|
||||
'target' => $target,
|
||||
'issued_at' => time(),
|
||||
],
|
||||
5 * MINUTE_IN_SECONDS
|
||||
);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_consume_email_code')) :
|
||||
function aksrt_consume_email_code(int $user_id, string $scene, string $code, string $target = ''): bool {
|
||||
$key = aksrt_get_email_code_key($user_id, $scene);
|
||||
$stored = get_transient($key);
|
||||
if (!is_array($stored) || empty($stored['hash']) || !wp_check_password($code, $stored['hash'])) {
|
||||
return false;
|
||||
}
|
||||
if ($target !== '' && strtolower((string) ($stored['target'] ?? '')) !== strtolower($target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete_transient($key);
|
||||
return true;
|
||||
}
|
||||
|
||||
endif;
|
||||
|
||||
add_action('wp_ajax_aksrt_send_email_code', function () {
|
||||
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
$scene = isset( $_POST['scene'] ) ? sanitize_key( (string) $_POST['scene'] ) : '';
|
||||
if ( ! in_array( $scene, array( 'change_password', 'change_email' ), true ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '无效的验证场景', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
if ( function_exists( 'aksrt_require_captcha' ) ) {
|
||||
$captcha_result = aksrt_require_captcha( $scene, wp_unslash( $_POST ) );
|
||||
if ( $captcha_result !== true ) {
|
||||
wp_send_json_error( array( 'message' => $captcha_result ) );
|
||||
}
|
||||
}
|
||||
|
||||
$current_user = wp_get_current_user();
|
||||
$target_email = '';
|
||||
$success_message = '';
|
||||
|
||||
if ( $scene === 'change_password' ) {
|
||||
$target_email = $current_user->user_email;
|
||||
if ( empty( $target_email ) || ! is_email( $target_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '当前账号未绑定有效邮箱', 'aksrt' ) ) );
|
||||
}
|
||||
$success_message = __( '验证码已发送到您的注册邮箱', 'aksrt' );
|
||||
} else {
|
||||
$new_email = isset( $_POST['new_email'] ) ? trim( wp_unslash( $_POST['new_email'] ) ) : '';
|
||||
$current_password = isset( $_POST['current_password'] ) ? trim( wp_unslash( $_POST['current_password'] ) ) : '';
|
||||
|
||||
if ( empty( $current_password ) || ! wp_check_password( $current_password, $current_user->user_pass, $current_user->ID ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '当前密码不正确', 'aksrt' ) ) );
|
||||
}
|
||||
if ( empty( $new_email ) || ! is_email( $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '请输入有效的新邮箱地址', 'aksrt' ) ) );
|
||||
}
|
||||
if ( strtolower( $new_email ) === strtolower( $current_user->user_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '新邮箱与当前邮箱相同', 'aksrt' ) ) );
|
||||
}
|
||||
if ( email_exists( $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '该邮箱已被其他账号使用', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
$target_email = $new_email;
|
||||
$success_message = __( '验证码已发送到新邮箱,请完成验证后再提交', 'aksrt' );
|
||||
}
|
||||
|
||||
// 频率限制:60秒内不能重复发送
|
||||
$rate_key = aksrt_get_email_code_rate_key( $current_user->ID, $scene, $target_email );
|
||||
if ( get_transient( $rate_key ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '操作过于频繁,请稍后再试', 'aksrt' ) ) );
|
||||
}
|
||||
set_transient( $rate_key, true, 60 );
|
||||
|
||||
// 生成验证码并同步发送,只有真正发信成功后才写入验证码缓存。
|
||||
$code = str_pad( (string) random_int( 0, 999999 ), 6, '0', STR_PAD_LEFT );
|
||||
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$default_subject = '[' . $site_name . '] ' . __( '邮箱验证码', 'aksrt' );
|
||||
$default_content = sprintf(
|
||||
__( "您好 %s,\n\n您正在进行敏感操作验证,验证码为:\n\n【%s】\n\n验证码有效期为 5 分钟。如非本人操作,请忽略此邮件。", 'aksrt' ),
|
||||
$current_user->display_name,
|
||||
$code
|
||||
);
|
||||
$sent = class_exists( '\AKSRT\Settings\Settings' )
|
||||
? \AKSRT\Settings\Settings::send_event_email(
|
||||
'change_code', $target_email, $default_subject, $default_content,
|
||||
[ '{username}' => $current_user->display_name, '{site_name}' => $site_name, '{code}' => $code ]
|
||||
)
|
||||
: wp_mail( $target_email, $default_subject, $default_content, [ 'Content-Type: text/plain; charset=UTF-8' ] );
|
||||
|
||||
if ( ! $sent ) {
|
||||
delete_transient( $rate_key );
|
||||
error_log( '[AKSRT] 验证码邮件发送失败 user_id=' . $current_user->ID . ' email=' . $target_email . ' scene=' . $scene );
|
||||
wp_send_json_error( array( 'message' => __( '验证码邮件发送失败,请稍后重试', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
aksrt_store_email_code( $current_user->ID, $scene, $code, $target_email );
|
||||
|
||||
wp_send_json_success( array( 'message' => $success_message ) );
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// AJAX: 修改密码
|
||||
// ============================================================
|
||||
add_action('wp_ajax_aksrt_change_password', function () {
|
||||
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
$old_password = isset( $_POST['old_password'] ) ? trim( wp_unslash( $_POST['old_password'] ) ) : '';
|
||||
$new_password = isset( $_POST['new_password'] ) ? trim( wp_unslash( $_POST['new_password'] ) ) : '';
|
||||
$new_password_confirm = isset( $_POST['new_password_confirm'] ) ? trim( wp_unslash( $_POST['new_password_confirm'] ) ) : '';
|
||||
$email_code = isset( $_POST['email_code'] ) ? sanitize_text_field( $_POST['email_code'] ) : '';
|
||||
|
||||
// 验证原密码
|
||||
$user = get_userdata( $user_id );
|
||||
if ( ! $user || ! wp_check_password( $old_password, $user->data->user_pass, $user_id ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '原密码不正确', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 验证新密码强度
|
||||
if ( mb_strlen( $new_password ) < 6 ) {
|
||||
wp_send_json_error( array( 'message' => __( '新密码长度至少6位', 'aksrt' ) ) );
|
||||
}
|
||||
if ( mb_strlen( $new_password ) > 128 ) {
|
||||
wp_send_json_error( array( 'message' => __( '新密码过长', 'aksrt' ) ) );
|
||||
}
|
||||
if ( $new_password !== $new_password_confirm ) {
|
||||
wp_send_json_error( array( 'message' => __( '两次输入的新密码不一致', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 人机验证
|
||||
if ( function_exists( 'aksrt_require_captcha' ) ) {
|
||||
$captcha_result = aksrt_require_captcha( 'change_password', wp_unslash( $_POST ) );
|
||||
if ( $captcha_result !== true ) {
|
||||
wp_send_json_error( array( 'message' => $captcha_result ) );
|
||||
}
|
||||
}
|
||||
|
||||
// 验证邮箱验证码
|
||||
if ( ! aksrt_consume_email_code( $user_id, 'change_password', $email_code, $user->user_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '邮箱验证码错误或已过期', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 设置新密码
|
||||
wp_set_password( $new_password, $user_id );
|
||||
|
||||
if ( class_exists( 'WP_Session_Tokens' ) ) {
|
||||
WP_Session_Tokens::get_instance( $user_id )->destroy_all();
|
||||
}
|
||||
|
||||
// 清除登录态
|
||||
wp_clear_auth_cookie();
|
||||
|
||||
wp_send_json_success( array(
|
||||
'message' => __( '密码修改成功,即将跳转到登录页…', 'aksrt' ),
|
||||
'redirect_to' => aksrt_get_login_url(),
|
||||
));
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// AJAX: 更改邮箱
|
||||
// ============================================================
|
||||
add_action('wp_ajax_aksrt_change_email', function () {
|
||||
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
$current_password = isset( $_POST['current_password'] ) ? trim( wp_unslash( $_POST['current_password'] ) ) : '';
|
||||
$new_email = isset( $_POST['new_email'] ) ? trim( wp_unslash( $_POST['new_email'] ) ) : '';
|
||||
$email_code = isset( $_POST['email_code'] ) ? sanitize_text_field( $_POST['email_code'] ) : '';
|
||||
|
||||
$current_user = wp_get_current_user();
|
||||
if ( empty( $current_password ) || ! wp_check_password( $current_password, $current_user->user_pass, $current_user->ID ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '当前密码不正确', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 验证新邮箱格式
|
||||
if ( empty( $new_email ) || ! is_email( $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '请输入有效的邮箱地址', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
if ( strtolower( $new_email ) === strtolower( $current_user->user_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '新邮箱与当前邮箱相同', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 检查邮箱是否已被使用
|
||||
if ( email_exists( $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '该邮箱已被其他账号使用', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 人机验证
|
||||
if ( function_exists( 'aksrt_require_captcha' ) ) {
|
||||
$captcha_result = aksrt_require_captcha( 'change_email', wp_unslash( $_POST ) );
|
||||
if ( $captcha_result !== true ) {
|
||||
wp_send_json_error( array( 'message' => $captcha_result ) );
|
||||
}
|
||||
}
|
||||
|
||||
// 验证邮箱验证码
|
||||
if ( ! aksrt_consume_email_code( $user_id, 'change_email', $email_code, $new_email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '邮箱验证码错误或已过期', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
// 更新邮箱
|
||||
$old_email = $current_user->user_email;
|
||||
$update_result = wp_update_user( [
|
||||
'ID' => $user_id,
|
||||
'user_email' => $new_email,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $update_result ) ) {
|
||||
wp_send_json_error( array( 'message' => $update_result->get_error_message() ?: __( '邮箱更新失败,请稍后重试', 'aksrt' ) ) );
|
||||
}
|
||||
|
||||
if ( is_email( $old_email ) ) {
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$default_subject = '[' . $site_name . '] ' . __( '账号邮箱已变更', 'aksrt' );
|
||||
$default_content = sprintf(
|
||||
__( "您好 %s,\n\n您的账号邮箱已成功修改为:%s\n\n如果这不是您的操作,请立即联系站点管理员。", 'aksrt' ),
|
||||
$current_user->display_name,
|
||||
$new_email
|
||||
);
|
||||
if ( class_exists( '\AKSRT\Settings\Settings' ) ) {
|
||||
\AKSRT\Settings\Settings::send_event_email(
|
||||
'change_notice', $old_email, $default_subject, $default_content,
|
||||
[ '{username}' => $current_user->display_name, '{site_name}' => $site_name, '{new_email}' => $new_email ]
|
||||
);
|
||||
} else {
|
||||
wp_mail( $old_email, $default_subject, $default_content, [ 'Content-Type: text/plain; charset=UTF-8' ] );
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 邮箱变更后重置验证状态(阶段 5)======
|
||||
// 1. 写入 14 天回滚窗口 meta(管理员可在此期内还原旧邮箱)
|
||||
// 2. 把 aksrt_email_verified 重置为 0
|
||||
// 3. 若功能启用 → 立即给新邮箱发送验证邮件
|
||||
update_user_meta( $user_id, 'aksrt_email_previous', $old_email );
|
||||
update_user_meta( $user_id, 'aksrt_email_changed_at', time() );
|
||||
|
||||
$verify_url = '';
|
||||
$verify_message = '';
|
||||
if ( class_exists( '\AKSRT\Settings\Settings' ) && class_exists( '\AKSRT\Auth\Service' )
|
||||
&& \AKSRT\Settings\Settings::registration_requires_email_verification() ) {
|
||||
update_user_meta( $user_id, 'aksrt_email_verified', 0 );
|
||||
// 用户对自身的 edit_user 权限映射到 'read',因此 admin_send_verification 会通过权限校验。
|
||||
$verify_result = \AKSRT\Auth\Service::admin_send_verification( $user_id, $user_id );
|
||||
if ( ! empty( $verify_result['success'] ) ) {
|
||||
$verify_url = function_exists('aksrt_get_verify_email_url') ? aksrt_get_verify_email_url( $user_id ) : '';
|
||||
$verify_message = __( '请前往新邮箱完成验证,否则下次登录将被拦截。', 'aksrt' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( class_exists( 'WP_Session_Tokens' ) ) {
|
||||
WP_Session_Tokens::get_instance( $user_id )->destroy_all();
|
||||
}
|
||||
|
||||
// 清除登录态
|
||||
wp_clear_auth_cookie();
|
||||
|
||||
wp_send_json_success( array(
|
||||
'message' => $verify_message !== ''
|
||||
? __( '邮箱修改成功,请前往新邮箱完成验证后重新登录…', 'aksrt' )
|
||||
: __( '邮箱修改成功,即将跳转到登录页…', 'aksrt' ),
|
||||
'redirect_to' => $verify_url !== '' ? $verify_url : aksrt_get_login_url(),
|
||||
'verify_url' => $verify_url,
|
||||
));
|
||||
});
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
/**
|
||||
* User rewrite/routing/bootstrap flush + author redirects
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// ============================================================
|
||||
// 三、URL 路由与模板加载
|
||||
// ============================================================
|
||||
|
||||
// --- 3a. /user/{昵称} 自定义重写 ---
|
||||
add_filter('query_vars', function ($vars) {
|
||||
$vars[] = 'aksrt_user_center';
|
||||
$vars[] = 'user_nicename';
|
||||
return $vars;
|
||||
});
|
||||
|
||||
add_action('template_redirect', function () {
|
||||
if (is_admin() || wp_doing_ajax() || is_feed() || is_embed() || is_preview()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resolved_path = aksrt_get_current_special_template_path();
|
||||
if ($resolved_path === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
status_header(200);
|
||||
load_template($resolved_path, true);
|
||||
exit;
|
||||
}, 100);
|
||||
|
||||
add_action('init', function () {
|
||||
add_rewrite_tag('%aksrt_user_center%', '([^&]+)');
|
||||
add_rewrite_tag('%user_nicename%', '([^/]+)');
|
||||
add_rewrite_rule('^profile/?$', 'index.php?aksrt_user_center=1', 'top');
|
||||
add_rewrite_rule('^user/([^/]+)/?$', 'index.php?user_nicename=$matches[1]', 'top');
|
||||
});
|
||||
|
||||
// --- 3b. 统一模板拦截器(核心)---
|
||||
// 拦截所有使用主题特殊模板的页面请求,确保正确加载
|
||||
add_filter('template_include', function ($template) {
|
||||
$resolved_path = aksrt_get_current_special_template_path();
|
||||
if ($resolved_path !== '') {
|
||||
return $resolved_path;
|
||||
}
|
||||
|
||||
return $template;
|
||||
|
||||
global $_aksrt_special_templates;
|
||||
|
||||
// A) /user/{昵称} 路由 → 加载公开主页模板
|
||||
$nicename = get_query_var('user_nicename');
|
||||
if (!empty($nicename)) {
|
||||
$user = get_user_by('slug', $nicename);
|
||||
if (!$user) return $template; // 不存在的用户 → 默认 404
|
||||
|
||||
$tpl = aksrt_locate_template_file('templates/pages/page-user-home.php');
|
||||
if (!$tpl) {
|
||||
// 兜底:如果新模板不存在,回退到旧 profile 模板
|
||||
$tpl = aksrt_locate_template_file($_aksrt_special_templates['profile']['file']);
|
||||
}
|
||||
if (!$tpl) return $template;
|
||||
|
||||
global $aksrt_profile_user;
|
||||
$aksrt_profile_user = $user;
|
||||
return $tpl;
|
||||
}
|
||||
|
||||
// B) 普通页面 → 检测是否使用了我们的特殊模板,确保加载正确的模板文件
|
||||
// (防止 WP 在某些情况下未能正确解析自定义页面模板)
|
||||
if (is_page()) {
|
||||
$page_id = get_queried_object_id();
|
||||
if ($page_id) {
|
||||
$page_template = get_post_meta($page_id, '_wp_page_template', true);
|
||||
if ($page_template && strpos($page_template, 'templates/pages/') === 0) {
|
||||
$tpl = aksrt_locate_template_file($page_template);
|
||||
if ($tpl) return $tpl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $template;
|
||||
}, 99);
|
||||
|
||||
add_filter('template_include', function ($template) {
|
||||
$resolved_path = aksrt_get_current_special_template_path();
|
||||
if ($resolved_path !== '') {
|
||||
return $resolved_path;
|
||||
}
|
||||
|
||||
if (get_query_var('aksrt_user_center')) {
|
||||
$profile_template = aksrt_locate_template_file('templates/pages/page-profile.php');
|
||||
if ($profile_template) {
|
||||
return $profile_template;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_page()) {
|
||||
$page_id = get_queried_object_id();
|
||||
if ($page_id > 0) {
|
||||
$page_template = get_post_meta($page_id, '_wp_page_template', true);
|
||||
$resolved_template = aksrt_resolve_page_template_file(
|
||||
is_string($page_template) ? $page_template : '',
|
||||
(string) get_post_field('post_name', $page_id)
|
||||
);
|
||||
|
||||
if ($resolved_template !== '') {
|
||||
$resolved_path = aksrt_locate_template_file($resolved_template);
|
||||
if ($resolved_path) {
|
||||
return $resolved_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $template;
|
||||
}, 100);
|
||||
|
||||
// ============================================================
|
||||
// 四、自动维护 + flush
|
||||
// ============================================================
|
||||
add_action('init', function () {
|
||||
global $_aksrt_special_templates;
|
||||
|
||||
// 后台/AJAX:检查并创建缺失页面
|
||||
$pages_bootstrapped = get_option('aksrt_template_pages_bootstrapped');
|
||||
$requested_template_key = (!is_admin() && !wp_doing_ajax()) ? aksrt_get_requested_template_key() : '';
|
||||
if (is_admin() || wp_doing_ajax() || $pages_bootstrapped !== AKSRT_VERSION) {
|
||||
foreach ($_aksrt_special_templates as $key => $cfg) {
|
||||
if ($key === 'profile') {
|
||||
continue;
|
||||
}
|
||||
aksrt_ensure_template_page($key);
|
||||
}
|
||||
|
||||
if ($pages_bootstrapped !== AKSRT_VERSION) {
|
||||
update_option('aksrt_template_pages_bootstrapped', AKSRT_VERSION);
|
||||
}
|
||||
} elseif ($requested_template_key !== '' && $requested_template_key !== 'profile') {
|
||||
aksrt_ensure_template_page($requested_template_key);
|
||||
}
|
||||
|
||||
// 统一 flush 检查
|
||||
$rewrite_version = AKSRT_VERSION . ':sign-profile-routing-v1';
|
||||
$flushed = get_option('aksrt_user_rules_flushed');
|
||||
if ($flushed !== $rewrite_version) {
|
||||
flush_rewrite_rules(false);
|
||||
update_option('aksrt_user_rules_flushed', $rewrite_version);
|
||||
}
|
||||
});
|
||||
|
||||
// 页面变更时清缓存
|
||||
add_action('save_post', function ($pid) { if (get_post_type($pid) === 'page') aksrt_clear_template_page_cache(); });
|
||||
add_action('delete_post', function ($pid) { if (get_post_type($pid) === 'page') aksrt_clear_template_page_cache(); });
|
||||
|
||||
// ============================================================
|
||||
// 六、作者链接改写
|
||||
// ============================================================
|
||||
add_filter('author_link', function ($link, $author_id) {
|
||||
$user = get_userdata($author_id);
|
||||
$slug = $user ? $user->user_nicename : 'user-' . $author_id;
|
||||
return home_url('/user/' . trailingslashit($slug));
|
||||
}, 10, 2);
|
||||
|
||||
add_action('template_redirect', function () {
|
||||
if (!is_author()) return;
|
||||
$author_id = get_queried_object_id();
|
||||
if ($author_id) {
|
||||
$user = get_userdata($author_id);
|
||||
$slug = $user ? $user->user_nicename : 'user-' . $author_id;
|
||||
wp_safe_redirect(home_url('/user/' . trailingslashit($slug)), 301);
|
||||
exit;
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// AJAX: 保存用户个人资料(显示名称、简介、联系方式)
|
||||
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
/**
|
||||
* User template registry + template utils
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// ============================================================
|
||||
// 一、模板注册表 — 所有特殊页面的"身份证"
|
||||
// 修改此处即可增删模板页面,无需改动其他逻辑
|
||||
// ============================================================
|
||||
$_aksrt_special_templates = [
|
||||
'login' => [
|
||||
'file' => 'templates/pages/page-login.php',
|
||||
'slug' => 'login',
|
||||
'title' => '登录',
|
||||
'fallback' => '/login/',
|
||||
],
|
||||
'register' => [
|
||||
'file' => 'templates/pages/page-register.php',
|
||||
'slug' => 'register',
|
||||
'title' => '注册',
|
||||
'fallback' => '/register/',
|
||||
],
|
||||
'bind' => [
|
||||
'file' => 'templates/pages/page-bind-account.php',
|
||||
'slug' => 'bind-account',
|
||||
'title' => '绑定账号',
|
||||
'fallback' => '/bind-account/',
|
||||
],
|
||||
'reset-password' => [
|
||||
'file' => 'templates/pages/page-reset-password.php',
|
||||
'slug' => 'reset-password',
|
||||
'title' => '重置密码',
|
||||
'fallback' => '/reset-password/',
|
||||
],
|
||||
'verify-email' => [
|
||||
'file' => 'templates/pages/page-verify-email.php',
|
||||
'slug' => 'verify-email',
|
||||
'title' => '邮箱验证',
|
||||
'fallback' => '/verify-email/',
|
||||
],
|
||||
'categories' => [
|
||||
'file' => 'templates/pages/page-categories.php',
|
||||
'slug' => 'categories',
|
||||
'title' => '分类目录',
|
||||
'fallback' => '/categories/',
|
||||
],
|
||||
'friends' => [
|
||||
'file' => 'templates/pages/page-friends.php',
|
||||
'slug' => 'friends',
|
||||
'title' => '友情链接',
|
||||
'fallback' => '/friends/',
|
||||
],
|
||||
'profile' => [
|
||||
'file' => 'templates/pages/page-profile.php',
|
||||
'slug' => 'profile',
|
||||
'title' => '个人中心',
|
||||
'fallback' => '/',
|
||||
],
|
||||
];
|
||||
|
||||
// Zibll-style v2 routing keeps only true fixed template pages in the registry.
|
||||
$_aksrt_special_templates = [
|
||||
'sign' => [
|
||||
'file' => 'templates/pages/page-sign.php',
|
||||
'slug' => 'sign',
|
||||
'title' => '登录/注册',
|
||||
'fallback' => '/sign/',
|
||||
],
|
||||
'bind' => [
|
||||
'file' => 'templates/pages/page-bind-account.php',
|
||||
'slug' => 'bind-account',
|
||||
'title' => '绑定账号',
|
||||
'fallback' => '/bind-account/',
|
||||
],
|
||||
'reset-password' => [
|
||||
'file' => 'templates/pages/page-reset-password.php',
|
||||
'slug' => 'reset-password',
|
||||
'title' => '重置密码',
|
||||
'fallback' => '/reset-password/',
|
||||
],
|
||||
'verify-email' => [
|
||||
'file' => 'templates/pages/page-verify-email.php',
|
||||
'slug' => 'verify-email',
|
||||
'title' => '邮箱验证',
|
||||
'fallback' => '/verify-email/',
|
||||
],
|
||||
'categories' => [
|
||||
'file' => 'templates/pages/page-categories.php',
|
||||
'slug' => 'categories',
|
||||
'title' => '分类目录',
|
||||
'fallback' => '/categories/',
|
||||
],
|
||||
'friends' => [
|
||||
'file' => 'templates/pages/page-friends.php',
|
||||
'slug' => 'friends',
|
||||
'title' => '友情链接',
|
||||
'fallback' => '/friends/',
|
||||
],
|
||||
'profile' => [
|
||||
'file' => 'templates/pages/page-profile.php',
|
||||
'slug' => 'profile',
|
||||
'title' => '个人中心',
|
||||
'fallback' => '/profile/',
|
||||
],
|
||||
];
|
||||
|
||||
/** @var array 缓存 key=模板文件路径 => value=WP_Post|null */
|
||||
$_aksrt_template_page_cache = [];
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 二、核心工具函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 查找使用指定模板文件的页面(按模板匹配,自适应任意 slug)
|
||||
*
|
||||
* @param string $template_file 相对于主题的模板路径
|
||||
* @return WP_Post|null
|
||||
*/
|
||||
function aksrt_find_template_page(string $template_file) {
|
||||
global $_aksrt_template_page_cache;
|
||||
if (isset($_aksrt_template_page_cache[$template_file])) {
|
||||
return $_aksrt_template_page_cache[$template_file];
|
||||
}
|
||||
|
||||
$page = null;
|
||||
$pages = get_pages([
|
||||
'meta_key' => '_wp_page_template',
|
||||
'meta_value' => $template_file,
|
||||
'number' => 1,
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
if (!empty($pages)) {
|
||||
$page = $pages[0];
|
||||
}
|
||||
|
||||
$_aksrt_template_page_cache[$template_file] = $page;
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取使用指定模板的页面 URL
|
||||
*
|
||||
* @param string $key 注册表 key(当前仅 profile 使用)
|
||||
* @return string 页面 URL 或回退地址
|
||||
*/
|
||||
function aksrt_get_template_url(string $key): string {
|
||||
global $_aksrt_special_templates;
|
||||
if (!isset($_aksrt_special_templates[$key])) {
|
||||
return home_url('/');
|
||||
}
|
||||
|
||||
$cfg = $_aksrt_special_templates[$key];
|
||||
$page = aksrt_find_template_page($cfg['file']);
|
||||
|
||||
if (!$page) {
|
||||
aksrt_ensure_template_page($key);
|
||||
$page = aksrt_find_template_page($cfg['file']);
|
||||
}
|
||||
|
||||
if ($page) {
|
||||
return get_permalink($page->ID);
|
||||
}
|
||||
|
||||
return home_url($cfg['fallback']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位模板文件的绝对路径
|
||||
*
|
||||
* @param string $template_file 相对于主题的路径
|
||||
* @return string|false 文件绝对路径或 false
|
||||
*/
|
||||
function aksrt_locate_template_file(string $template_file) {
|
||||
$path = locate_template([$template_file]);
|
||||
if ($path) return $path;
|
||||
|
||||
$full = AKSRT_DIR . '/' . $template_file;
|
||||
return file_exists($full) ? $full : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a public-facing page template file from stored meta or page slug.
|
||||
*/
|
||||
function aksrt_resolve_page_template_file(string $template_file = '', string $post_slug = ''): string {
|
||||
$template_file = ltrim($template_file, '/\\');
|
||||
$post_slug = sanitize_title($post_slug);
|
||||
|
||||
$legacy_auth_templates = [
|
||||
'templates/pages/page-login.php' => 'login',
|
||||
'templates/pages/page-register.php' => 'register',
|
||||
'templates/pages/page-bind-account.php' => 'bind',
|
||||
'templates/pages/page-reset-password.php' => 'reset-password',
|
||||
'templates/pages/page-verify-email.php' => 'verify-email',
|
||||
];
|
||||
|
||||
if ($template_file !== '') {
|
||||
if ($template_file === 'templates/pages/page-sign.php') {
|
||||
return aksrt_get_auth_template_file(aksrt_get_auth_tab(), true);
|
||||
}
|
||||
|
||||
if (!empty($legacy_auth_templates[$template_file])) {
|
||||
return aksrt_get_auth_template_file((string) $legacy_auth_templates[$template_file], true);
|
||||
}
|
||||
|
||||
if (strpos($template_file, 'templates/pages/') === 0 && aksrt_locate_template_file($template_file)) {
|
||||
return $template_file;
|
||||
}
|
||||
}
|
||||
|
||||
if ($post_slug !== '') {
|
||||
$auth_slug_map = [
|
||||
'sign' => aksrt_get_auth_tab(),
|
||||
'login' => 'login',
|
||||
'register' => 'register',
|
||||
'bind-account' => 'bind',
|
||||
'reset-password' => 'reset-password',
|
||||
'verify-email' => 'verify-email',
|
||||
];
|
||||
|
||||
if (!empty($auth_slug_map[$post_slug])) {
|
||||
return aksrt_get_auth_template_file((string) $auth_slug_map[$post_slug], true);
|
||||
}
|
||||
|
||||
$slug_map = [
|
||||
'categories' => 'templates/pages/page-categories.php',
|
||||
'friends' => 'templates/pages/page-friends.php',
|
||||
'profile' => 'templates/pages/page-profile.php',
|
||||
];
|
||||
|
||||
if (!empty($slug_map[$post_slug]) && aksrt_locate_template_file($slug_map[$post_slug])) {
|
||||
return $slug_map[$post_slug];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize auth view keys used by the unified sign page.
|
||||
*/
|
||||
function aksrt_normalize_auth_tab(string $tab): string {
|
||||
$tab = sanitize_key($tab);
|
||||
|
||||
switch ($tab) {
|
||||
case 'signin':
|
||||
case 'login':
|
||||
return 'login';
|
||||
case 'signup':
|
||||
case 'register':
|
||||
return 'register';
|
||||
case 'bind':
|
||||
case 'bind-account':
|
||||
return 'bind';
|
||||
case 'reset':
|
||||
case 'lostpassword':
|
||||
case 'reset-password':
|
||||
return 'reset-password';
|
||||
case 'verify':
|
||||
case 'verify-email':
|
||||
return 'verify-email';
|
||||
default:
|
||||
return 'login';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an auth tab should be rendered by the unified sign page.
|
||||
*/
|
||||
function aksrt_auth_tab_uses_unified_sign_page(string $tab): bool {
|
||||
return in_array(
|
||||
aksrt_normalize_auth_tab($tab),
|
||||
['login', 'register'],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the auth template file, preferring the unified sign page when present.
|
||||
*/
|
||||
function aksrt_get_auth_template_file(string $tab = 'login', bool $prefer_sign_page = true): string {
|
||||
$normalized_tab = aksrt_normalize_auth_tab($tab);
|
||||
$sign_template = 'templates/pages/page-sign.php';
|
||||
|
||||
if (
|
||||
$prefer_sign_page
|
||||
&& aksrt_auth_tab_uses_unified_sign_page($normalized_tab)
|
||||
&& aksrt_locate_template_file($sign_template)
|
||||
) {
|
||||
return $sign_template;
|
||||
}
|
||||
|
||||
switch ($normalized_tab) {
|
||||
case 'register':
|
||||
return 'templates/pages/page-register.php';
|
||||
case 'bind':
|
||||
return 'templates/pages/page-bind-account.php';
|
||||
case 'reset-password':
|
||||
return 'templates/pages/page-reset-password.php';
|
||||
case 'verify-email':
|
||||
return 'templates/pages/page-verify-email.php';
|
||||
default:
|
||||
return 'templates/pages/page-login.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the active auth view for sign routes and the sign page template.
|
||||
*/
|
||||
function aksrt_get_auth_tab(): string {
|
||||
$query_tab = get_query_var('aksrt_auth_tab');
|
||||
if (is_string($query_tab) && $query_tab !== '') {
|
||||
return aksrt_normalize_auth_tab($query_tab);
|
||||
}
|
||||
|
||||
if (!empty($_GET['tab'])) {
|
||||
return aksrt_normalize_auth_tab((string) wp_unslash($_GET['tab']));
|
||||
}
|
||||
|
||||
$post = get_queried_object();
|
||||
if ($post instanceof WP_Post) {
|
||||
return aksrt_normalize_auth_tab((string) $post->post_name);
|
||||
}
|
||||
|
||||
return 'login';
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除模板页面缓存
|
||||
*/
|
||||
function aksrt_clear_template_page_cache(): void {
|
||||
global $_aksrt_template_page_cache;
|
||||
$_aksrt_template_page_cache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the public user-home template context and return its template path.
|
||||
*/
|
||||
function aksrt_get_user_home_template_path(): string {
|
||||
global $_aksrt_special_templates, $aksrt_profile_user;
|
||||
|
||||
$nicename = (string) get_query_var('user_nicename');
|
||||
if ($nicename === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$user = get_user_by('slug', $nicename);
|
||||
if (!$user instanceof WP_User) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$template_path = aksrt_locate_template_file('templates/pages/page-user-home.php');
|
||||
|
||||
if (!$template_path) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$aksrt_profile_user = $user;
|
||||
return $template_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active special-page template path for the current request.
|
||||
*/
|
||||
function aksrt_get_current_special_template_path(): string {
|
||||
if (get_query_var('user_nicename')) {
|
||||
return aksrt_get_user_home_template_path();
|
||||
}
|
||||
|
||||
if (get_query_var('aksrt_user_center')) {
|
||||
$profile_template = aksrt_locate_template_file('templates/pages/page-profile.php');
|
||||
return $profile_template ?: '';
|
||||
}
|
||||
|
||||
if (get_query_var('aksrt_sign')) {
|
||||
$auth_template = aksrt_get_auth_template_file((string) get_query_var('aksrt_auth_tab'), true);
|
||||
$auth_template_path = aksrt_locate_template_file($auth_template);
|
||||
return $auth_template_path ?: '';
|
||||
}
|
||||
|
||||
if (is_page()) {
|
||||
$page_id = get_queried_object_id();
|
||||
if ($page_id > 0) {
|
||||
$page_template = get_post_meta($page_id, '_wp_page_template', true);
|
||||
$resolved_template = aksrt_resolve_page_template_file(
|
||||
is_string($page_template) ? $page_template : '',
|
||||
(string) get_post_field('post_name', $page_id)
|
||||
);
|
||||
|
||||
if ($resolved_template !== '') {
|
||||
$resolved_path = aksrt_locate_template_file($resolved_template);
|
||||
if ($resolved_path) {
|
||||
return $resolved_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前请求路径识别系统模板页 key。
|
||||
*
|
||||
* 用于前台直接访问固定 slug 时的自动补页。
|
||||
*/
|
||||
function aksrt_get_requested_template_key(): string {
|
||||
global $_aksrt_special_templates;
|
||||
|
||||
$request_uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
|
||||
if ($request_uri === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$request_path = wp_parse_url($request_uri, PHP_URL_PATH);
|
||||
if (!is_string($request_path) || $request_path === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH);
|
||||
if ($home_path !== '' && str_starts_with($request_path, $home_path)) {
|
||||
$request_path = (string) substr($request_path, strlen($home_path));
|
||||
}
|
||||
|
||||
$request_slug = trim($request_path, '/');
|
||||
if ($request_slug === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ($_aksrt_special_templates as $key => $cfg) {
|
||||
$slug = isset($cfg['slug']) ? sanitize_title((string) $cfg['slug']) : '';
|
||||
if ($slug !== '' && $slug === $request_slug) {
|
||||
return (string) $key;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保使用指定模板的页面存在,缺失则自动创建
|
||||
*
|
||||
* @param string $key 注册表 key
|
||||
*/
|
||||
function aksrt_ensure_template_page(string $key): void {
|
||||
global $_aksrt_special_templates;
|
||||
if (!isset($_aksrt_special_templates[$key])) return;
|
||||
|
||||
$cfg = $_aksrt_special_templates[$key];
|
||||
$desired_slug = sanitize_title($cfg['slug'] ?? $key);
|
||||
|
||||
// 已有页面使用此模板?
|
||||
if (aksrt_find_template_page($cfg['file'])) return;
|
||||
|
||||
if ($desired_slug !== '') {
|
||||
$existing_page = get_page_by_path($desired_slug, OBJECT, 'page');
|
||||
if ($existing_page instanceof WP_Post) {
|
||||
if ($existing_page->post_status !== 'publish') {
|
||||
wp_update_post([
|
||||
'ID' => $existing_page->ID,
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
}
|
||||
|
||||
update_post_meta($existing_page->ID, '_wp_page_template', $cfg['file']);
|
||||
aksrt_clear_template_page_cache();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动创建
|
||||
$page_id = wp_insert_post([
|
||||
'post_title' => $cfg['title'],
|
||||
'post_content' => '',
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'page',
|
||||
'post_name' => $desired_slug !== '' ? $desired_slug : $key,
|
||||
'comment_status' => 'closed',
|
||||
'ping_status' => 'closed',
|
||||
]);
|
||||
|
||||
if (!is_wp_error($page_id) && $page_id > 0) {
|
||||
update_post_meta($page_id, '_wp_page_template', $cfg['file']);
|
||||
aksrt_clear_template_page_cache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
/**
|
||||
* User public URL/page API
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// ============================================================
|
||||
// 五、公开 API 函数(兼容旧代码调用)
|
||||
// ============================================================
|
||||
|
||||
if (!function_exists('aksrt_get_user_center_url')) :
|
||||
/**
|
||||
* 获取本人中心 URL(私有空间入口)
|
||||
*
|
||||
* @param string $tab 可选 tab 参数
|
||||
* @return string
|
||||
*/
|
||||
function aksrt_get_user_center_url(string $tab = ''): string {
|
||||
if (get_option('permalink_structure')) {
|
||||
$url = home_url('/profile/');
|
||||
} else {
|
||||
$url = add_query_arg('aksrt_user_center', '1', home_url('/'));
|
||||
}
|
||||
|
||||
if ($tab !== '') {
|
||||
$url = add_query_arg('tab', $tab, $url);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_orders_page_url')) :
|
||||
/**
|
||||
* 订单中心页面 URL(模板 templates/pages/page-orders.php)。
|
||||
* 未指派页面时返回空串,调用方应隐藏入口。
|
||||
*/
|
||||
function aksrt_get_orders_page_url(): string {
|
||||
return function_exists('aksrt_get_page_url_by_template')
|
||||
? aksrt_get_page_url_by_template('templates/pages/page-orders.php')
|
||||
: '';
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_cart_page_url')) :
|
||||
/**
|
||||
* 购物车页面 URL(模板 templates/pages/page-cart.php)。
|
||||
*/
|
||||
function aksrt_get_cart_page_url(): string {
|
||||
return function_exists('aksrt_get_page_url_by_template')
|
||||
? aksrt_get_page_url_by_template('templates/pages/page-cart.php')
|
||||
: '';
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_revenue_page_url')) :
|
||||
/**
|
||||
* 收益中心页面 URL(模板 templates/pages/page-revenue.php)。
|
||||
*/
|
||||
function aksrt_get_revenue_page_url(): string {
|
||||
return function_exists('aksrt_get_page_url_by_template')
|
||||
? aksrt_get_page_url_by_template('templates/pages/page-revenue.php')
|
||||
: '';
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_messages_page_url')) :
|
||||
/**
|
||||
* 消息中心页面 URL(模板 templates/pages/page-messages.php)。
|
||||
*/
|
||||
function aksrt_get_messages_page_url(): string {
|
||||
return function_exists('aksrt_get_page_url_by_template')
|
||||
? aksrt_get_page_url_by_template('templates/pages/page-messages.php')
|
||||
: '';
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_user_home_url')) :
|
||||
/**
|
||||
* 获取公开用户主页 URL(/user/{nicename}/)
|
||||
*
|
||||
* @param WP_User|int|string|null $user 用户对象、ID、nicename 或 null(当前用户)
|
||||
* @return string
|
||||
*/
|
||||
function aksrt_get_user_home_url($user = null): string {
|
||||
$slug = '';
|
||||
if ($user instanceof WP_User) {
|
||||
$slug = $user->user_nicename;
|
||||
} elseif (is_int($user) && $user > 0) {
|
||||
$u = get_userdata($user);
|
||||
$slug = $u ? $u->user_nicename : '';
|
||||
} elseif (is_string($user) && $user) {
|
||||
$slug = $user;
|
||||
} else {
|
||||
$current = wp_get_current_user();
|
||||
$slug = $current->ID ? $current->user_nicename : '';
|
||||
}
|
||||
if (!$slug) return home_url('/');
|
||||
return home_url('/user/' . trailingslashit($slug));
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_is_user_center_page')) :
|
||||
/**
|
||||
* 判断当前是否为本人中心页面
|
||||
*/
|
||||
function aksrt_is_user_center_page(): bool {
|
||||
return (bool) get_query_var('aksrt_user_center')
|
||||
|| (is_page_template('templates/pages/page-profile.php') && !(bool) get_query_var('user_nicename'));
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_is_user_home_page')) :
|
||||
/**
|
||||
* 判断当前是否为公开用户主页
|
||||
*/
|
||||
function aksrt_is_user_home_page(): bool {
|
||||
return (bool) get_query_var('user_nicename')
|
||||
|| is_page_template('templates/pages/page-user-home.php');
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_profile_url')) :
|
||||
/**
|
||||
* 兼容层:获取用户主页 URL
|
||||
*
|
||||
* @deprecated 使用 aksrt_get_user_center_url() 或 aksrt_get_user_home_url() 替代
|
||||
* @param WP_User|int|string|null $user
|
||||
* @return string
|
||||
*/
|
||||
function aksrt_get_profile_url($user = null): string {
|
||||
// 无参调用 → 当前用户主页(公开页)
|
||||
if ($user === null) {
|
||||
return aksrt_get_user_home_url();
|
||||
}
|
||||
return aksrt_get_user_home_url($user);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_sign_page_url')) :
|
||||
/**
|
||||
* Get the unified auth page URL, with legacy route fallback while the page self-heals.
|
||||
*
|
||||
* @param string $tab
|
||||
* @param array<string, scalar> $args
|
||||
*/
|
||||
function aksrt_get_sign_page_url(string $tab = 'login', array $args = []): string {
|
||||
$tab = aksrt_normalize_auth_tab($tab);
|
||||
|
||||
$legacy_fallbacks = [
|
||||
'login' => home_url('/login/'),
|
||||
'register' => home_url('/register/'),
|
||||
'bind' => home_url('/bind-account/'),
|
||||
'reset-password' => home_url('/reset-password/'),
|
||||
'verify-email' => home_url('/verify-email/'),
|
||||
];
|
||||
|
||||
$page = aksrt_find_template_page('templates/pages/page-sign.php');
|
||||
if (!$page) {
|
||||
aksrt_ensure_template_page('sign');
|
||||
$page = aksrt_find_template_page('templates/pages/page-sign.php');
|
||||
}
|
||||
|
||||
if ($page instanceof WP_Post) {
|
||||
$url = get_permalink($page->ID);
|
||||
$args = ['tab' => $tab] + $args;
|
||||
return add_query_arg($args, $url);
|
||||
}
|
||||
|
||||
$url = $legacy_fallbacks[$tab] ?? home_url('/login/');
|
||||
return !empty($args) ? add_query_arg($args, $url) : $url;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine whether the current request is served by the unified auth experience.
|
||||
*/
|
||||
function aksrt_is_auth_page_request(): bool {
|
||||
return (bool) get_query_var('aksrt_sign')
|
||||
|| is_page_template('templates/pages/page-sign.php')
|
||||
|| is_page_template('templates/pages/page-login.php')
|
||||
|| is_page_template('templates/pages/page-register.php')
|
||||
|| is_page_template('templates/pages/page-bind-account.php')
|
||||
|| is_page_template('templates/pages/page-reset-password.php')
|
||||
|| is_page_template('templates/pages/page-verify-email.php');
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_login_url')) :
|
||||
/** 获取登录页 URL */
|
||||
function aksrt_get_login_url($redirect = '') {
|
||||
$url = aksrt_get_sign_page_url('login');
|
||||
if ($redirect) {
|
||||
$url = add_query_arg('redirect_to', urlencode($redirect), $url);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_register_url')) :
|
||||
/** 获取注册页 URL */
|
||||
function aksrt_get_register_url() {
|
||||
return aksrt_get_sign_page_url('register');
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_bind_url')) :
|
||||
/** 获取绑定账号页面 URL */
|
||||
function aksrt_get_bind_url($transient_key = '') {
|
||||
$url = aksrt_get_template_url('bind');
|
||||
if ($transient_key) {
|
||||
$url = add_query_arg('obk', $transient_key, $url);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_logout_url')) :
|
||||
/** 获取退出登录 URL */
|
||||
function aksrt_get_logout_url($redirect = '') {
|
||||
$target = $redirect ?: home_url('/');
|
||||
$url = add_query_arg('redirect_to', rawurlencode($target), home_url('/logout/'));
|
||||
return wp_nonce_url($url, 'aksrt-logout');
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_reset_password_url')) :
|
||||
/**
|
||||
* 获取重置密码页面 URL
|
||||
*
|
||||
* @param string $token 主题重置 token(可选,设置密码时需要)
|
||||
* @param int $user_id 用户 ID(可选,设置密码时需要)
|
||||
* @return string
|
||||
*/
|
||||
function aksrt_get_reset_password_url(string $token = '', int $user_id = 0): string {
|
||||
$url = aksrt_get_template_url('reset-password');
|
||||
if ($token !== '' && $user_id > 0) {
|
||||
$url = add_query_arg([
|
||||
'token' => $token,
|
||||
'uid' => $user_id,
|
||||
], $url);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_verify_email_url')) :
|
||||
/** 获取邮箱验证页面 URL */
|
||||
function aksrt_get_verify_email_url(int $user_id = 0, array $args = []): string {
|
||||
$url = aksrt_get_template_url('verify-email');
|
||||
if ($user_id > 0) {
|
||||
$args['uid'] = $user_id;
|
||||
}
|
||||
if (!empty($args)) {
|
||||
$url = add_query_arg($args, $url);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
endif;
|
||||
|
||||
// Notifications live in the same user/ domain; bootstrap also loads it.
|
||||
if (!class_exists('AKSRT_User_Notifications', false) /* procedural module */ && file_exists(dirname(__DIR__) . '/user-notifications.php')) {
|
||||
require_once dirname(__DIR__) . '/user-notifications.php';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,726 @@
|
||||
<?php
|
||||
/**
|
||||
* Widget registration and placement rules.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Register custom widgets.
|
||||
*/
|
||||
function aksrt_register_widgets() {
|
||||
register_widget('\AKSRT\Widgets\AboutWidget');
|
||||
register_widget('\AKSRT\Widgets\CategoriesWidget');
|
||||
register_widget('\AKSRT\Widgets\TagCloudWidget');
|
||||
register_widget('\AKSRT\Widgets\HotPostsWidget');
|
||||
register_widget('\AKSRT\Widgets\TabPostsWidget');
|
||||
register_widget('\AKSRT\Widgets\CategoryBlockWidget');
|
||||
register_widget('\AKSRT\Widgets\CategoryTabsWidget');
|
||||
register_widget('\AKSRT\Widgets\CategoryRecommendWidget');
|
||||
|
||||
// 第一批通用 widget
|
||||
register_widget('\AKSRT\Widgets\SearchWidget');
|
||||
register_widget('\AKSRT\Widgets\NoticeWidget');
|
||||
register_widget('\AKSRT\Widgets\RecentCommentsWidget');
|
||||
register_widget('\AKSRT\Widgets\FriendLinksWidget');
|
||||
register_widget('\AKSRT\Widgets\IconCardsWidget');
|
||||
register_widget('\AKSRT\Widgets\ButtonsWidget');
|
||||
register_widget('\AKSRT\Widgets\CustomHtmlGapWidget');
|
||||
|
||||
// 公告跑马灯
|
||||
register_widget('\AKSRT\Widgets\WidgetAnnouncement');
|
||||
|
||||
// 用户卡片
|
||||
register_widget('\AKSRT\Widgets\WidgetUserCard');
|
||||
|
||||
// 用户排行榜
|
||||
register_widget('\AKSRT\Widgets\WidgetUserRanking');
|
||||
|
||||
// 图文封面卡片
|
||||
register_widget('\AKSRT\Widgets\WidgetCoverCards');
|
||||
|
||||
// 视频播放器
|
||||
register_widget('\AKSRT\Widgets\WidgetVideoPlayer');
|
||||
|
||||
// 语言切换器
|
||||
register_widget('\AKSRT\Widgets\WidgetLangSwitch');
|
||||
|
||||
// 一言轮播
|
||||
register_widget('\AKSRT\Widgets\WidgetYiyan');
|
||||
|
||||
// Shop product widgets (§6.9)
|
||||
register_widget('\AKSRT\Widgets\WidgetProductList');
|
||||
register_widget('\AKSRT\Widgets\WidgetProductOneline');
|
||||
register_widget('\AKSRT\Widgets\WidgetProductTab');
|
||||
|
||||
aksrt_maybe_repair_sidebar_blog_widgets();
|
||||
}
|
||||
add_action('widgets_init', 'aksrt_register_widgets');
|
||||
|
||||
if (!function_exists('aksrt_widget_sidebar_area_ids')) {
|
||||
/**
|
||||
* Sidebars intended for sidebar-only widgets.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
function aksrt_widget_sidebar_area_ids(): array {
|
||||
return [
|
||||
'home_sidebar',
|
||||
'sidebar-blog',
|
||||
'sidebar-article',
|
||||
'all_sidebar_top',
|
||||
'all_sidebar_bottom',
|
||||
'cat_sidebar',
|
||||
'tag_sidebar',
|
||||
'search_sidebar',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_archive_area_ids')) {
|
||||
/**
|
||||
* Widget zones for archive pages (cat/tag/search).
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
function aksrt_widget_archive_area_ids(): array {
|
||||
return [
|
||||
'cat_top_fluid',
|
||||
'cat_top_content',
|
||||
'cat_bottom_content',
|
||||
'cat_bottom_fluid',
|
||||
'cat_sidebar',
|
||||
'tag_top_fluid',
|
||||
'tag_top_content',
|
||||
'tag_bottom_content',
|
||||
'tag_bottom_fluid',
|
||||
'tag_sidebar',
|
||||
'search_top_fluid',
|
||||
'search_top_content',
|
||||
'search_bottom_content',
|
||||
'search_bottom_fluid',
|
||||
'search_sidebar',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_homepage_area_ids')) {
|
||||
/**
|
||||
* Homepage widget zones for homepage-only widgets.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
function aksrt_widget_homepage_area_ids(): array {
|
||||
return [
|
||||
'home_top_fluid',
|
||||
'home_top_content',
|
||||
'home_bottom_content',
|
||||
'home_bottom_fluid',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_single_area_ids')) {
|
||||
/**
|
||||
* Single post/page widget zones.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
function aksrt_widget_single_area_ids(): array {
|
||||
return [
|
||||
'single_top_fluid',
|
||||
'single_top_content',
|
||||
'single_bottom_content',
|
||||
'single_bottom_fluid',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_base_from_id')) {
|
||||
/**
|
||||
* Get widget base id from a stored widget instance id.
|
||||
*/
|
||||
function aksrt_widget_base_from_id(string $widget_id): string {
|
||||
return preg_replace('/-\d+$/', '', $widget_id) ?: $widget_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_allowed_sidebars')) {
|
||||
/**
|
||||
* Allowed sidebar ids for a widget base id.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
function aksrt_widget_allowed_sidebars(string $widget_base_id): array {
|
||||
$sidebar_only = [
|
||||
'aksrt_about',
|
||||
'aksrt_categories',
|
||||
'aksrt_tag_cloud',
|
||||
'aksrt_hot_posts',
|
||||
];
|
||||
$homepage_only = [
|
||||
'aksrt_category_recommend',
|
||||
];
|
||||
$hybrid = [
|
||||
'aksrt_tab_posts',
|
||||
'aksrt_category_block',
|
||||
'aksrt_category_tabs',
|
||||
];
|
||||
|
||||
if (in_array($widget_base_id, $sidebar_only, true)) {
|
||||
return aksrt_widget_sidebar_area_ids();
|
||||
}
|
||||
|
||||
if (in_array($widget_base_id, $homepage_only, true)) {
|
||||
return aksrt_widget_homepage_area_ids();
|
||||
}
|
||||
|
||||
if (in_array($widget_base_id, $hybrid, true)) {
|
||||
return array_values(array_unique(array_merge(
|
||||
aksrt_widget_sidebar_area_ids(),
|
||||
aksrt_widget_homepage_area_ids()
|
||||
)));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_is_allowed_in_sidebar')) {
|
||||
/**
|
||||
* Whether a widget base id can be assigned to the given sidebar id.
|
||||
*/
|
||||
function aksrt_widget_is_allowed_in_sidebar(string $widget_base_id, string $sidebar_id): bool {
|
||||
if ($widget_base_id === '' || $sidebar_id === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($sidebar_id === 'wp_inactive_widgets' || strpos($sidebar_id, 'orphaned_widgets') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$allowed = aksrt_widget_allowed_sidebars($widget_base_id);
|
||||
if (empty($allowed)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($sidebar_id, $allowed, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_get_area_context')) {
|
||||
/**
|
||||
* Resolve widget area context from sidebar args.
|
||||
*/
|
||||
function aksrt_widget_get_area_context(array $args): string {
|
||||
$sidebar_id = isset($args['id']) ? (string) $args['id'] : '';
|
||||
|
||||
if (in_array($sidebar_id, aksrt_widget_sidebar_area_ids(), true)) {
|
||||
return 'sidebar';
|
||||
}
|
||||
|
||||
if (in_array($sidebar_id, aksrt_widget_homepage_area_ids(), true)) {
|
||||
return 'homepage';
|
||||
}
|
||||
|
||||
if (in_array($sidebar_id, aksrt_widget_archive_area_ids(), true)) {
|
||||
return 'archive';
|
||||
}
|
||||
|
||||
if (in_array($sidebar_id, aksrt_widget_single_area_ids(), true)) {
|
||||
return 'single';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Shared Widget Form Helper — 公共表单字段与显示规则
|
||||
// ============================================================
|
||||
|
||||
if (!function_exists('aksrt_widget_common_defaults')) {
|
||||
/**
|
||||
* Default values for shared widget instance fields.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function aksrt_widget_common_defaults(): array {
|
||||
return [
|
||||
'subtitle' => '',
|
||||
'title_link' => '',
|
||||
'show_type' => 'all',
|
||||
'show_id_type' => '',
|
||||
'show_ids' => '',
|
||||
'sidebar_affix' => false,
|
||||
'animation_in' => '',
|
||||
'animation_repeat'=> false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_normalize_common')) {
|
||||
/**
|
||||
* Normalize common widget instance fields, filling defaults for legacy instances.
|
||||
*
|
||||
* @param array<string, mixed> $instance
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function aksrt_widget_normalize_common(array $instance): array {
|
||||
$defaults = aksrt_widget_common_defaults();
|
||||
$result = array_merge($defaults, $instance);
|
||||
|
||||
$result['subtitle'] = sanitize_text_field($result['subtitle'] ?? '');
|
||||
$result['title_link'] = esc_url_raw($result['title_link'] ?? '');
|
||||
$result['show_type'] = in_array($result['show_type'] ?? '', ['all', 'pc', 'mobile'], true) ? $result['show_type'] : 'all';
|
||||
$result['show_id_type'] = in_array($result['show_id_type'] ?? '', ['', 'include', 'exclude'], true) ? $result['show_id_type'] : '';
|
||||
$result['show_ids'] = sanitize_text_field($result['show_ids'] ?? '');
|
||||
$result['sidebar_affix'] = !empty($result['sidebar_affix']);
|
||||
$result['animation_in'] = sanitize_key($result['animation_in'] ?? '');
|
||||
$result['animation_repeat']= !empty($result['animation_repeat']);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_render_common_form')) {
|
||||
/**
|
||||
* Render shared widget admin form fields.
|
||||
*
|
||||
* Call this from your widget's form() method after rendering custom fields.
|
||||
*
|
||||
* @param array<string, mixed> $instance Current widget instance.
|
||||
*/
|
||||
function aksrt_widget_render_common_form(array $instance): void {
|
||||
$instance = aksrt_widget_normalize_common($instance);
|
||||
?>
|
||||
<hr style="margin:16px 0 12px;border-color:#e0e0e0;">
|
||||
<p style="font-size:12px;color:#666;margin-bottom:8px;font-weight:600;"><?php esc_html_e('显示规则', 'aksrt'); ?></p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($instance['show_type_id'] ?? ''); ?>"><?php esc_html_e('显示设备', 'aksrt'); ?></label><br>
|
||||
<select name="<?php echo esc_attr($instance['show_type_name'] ?? 'show_type'); ?>" id="<?php echo esc_attr($instance['show_type_id'] ?? ''); ?>" class="widefat">
|
||||
<option value="all" <?php selected($instance['show_type'], 'all'); ?>><?php esc_html_e('全部', 'aksrt'); ?></option>
|
||||
<option value="pc" <?php selected($instance['show_type'], 'pc'); ?>><?php esc_html_e('仅 PC', 'aksrt'); ?></option>
|
||||
<option value="mobile" <?php selected($instance['show_type'], 'mobile'); ?>><?php esc_html_e('仅移动端', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($instance['show_id_type_id'] ?? ''); ?>"><?php esc_html_e('ID 显示规则', 'aksrt'); ?></label><br>
|
||||
<select name="<?php echo esc_attr($instance['show_id_type_name'] ?? 'show_id_type'); ?>" id="<?php echo esc_attr($instance['show_id_type_id'] ?? ''); ?>" class="widefat">
|
||||
<option value="" <?php selected($instance['show_id_type'], ''); ?>><?php esc_html_e('不限制', 'aksrt'); ?></option>
|
||||
<option value="include" <?php selected($instance['show_id_type'], 'include'); ?>><?php esc_html_e('仅指定 ID 显示', 'aksrt'); ?></option>
|
||||
<option value="exclude" <?php selected($instance['show_id_type'], 'exclude'); ?>><?php esc_html_e('指定 ID 隐藏', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($instance['show_ids_id'] ?? ''); ?>"><?php esc_html_e('文章/页面 ID', 'aksrt'); ?></label><br>
|
||||
<input type="text" name="<?php echo esc_attr($instance['show_ids_name'] ?? 'show_ids'); ?>" id="<?php echo esc_attr($instance['show_ids_id'] ?? ''); ?>" class="widefat" value="<?php echo esc_attr($instance['show_ids']); ?>" placeholder="<?php esc_attr_e('逗号分隔,如: 1,2,3', 'aksrt'); ?>">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="<?php echo esc_attr($instance['sidebar_affix_name'] ?? 'sidebar_affix'); ?>" value="1" <?php checked($instance['sidebar_affix']); ?>>
|
||||
<?php esc_html_e('侧栏吸顶', 'aksrt'); ?>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($instance['animation_in_id'] ?? ''); ?>"><?php esc_html_e('入场动画', 'aksrt'); ?></label><br>
|
||||
<select name="<?php echo esc_attr($instance['animation_in_name'] ?? 'animation_in'); ?>" id="<?php echo esc_attr($instance['animation_in_id'] ?? ''); ?>" class="widefat">
|
||||
<option value="" <?php selected($instance['animation_in'], ''); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
|
||||
<option value="fadeIn" <?php selected($instance['animation_in'], 'fadeIn'); ?>><?php esc_html_e('淡入', 'aksrt'); ?></option>
|
||||
<option value="fadeInUp" <?php selected($instance['animation_in'], 'fadeInUp'); ?>><?php esc_html_e('上移淡入', 'aksrt'); ?></option>
|
||||
<option value="fadeInDown" <?php selected($instance['animation_in'], 'fadeInDown'); ?>><?php esc_html_e('下移淡入', 'aksrt'); ?></option>
|
||||
<option value="fadeInLeft" <?php selected($instance['animation_in'], 'fadeInLeft'); ?>><?php esc_html_e('左移淡入', 'aksrt'); ?></option>
|
||||
<option value="fadeInRight" <?php selected($instance['animation_in'], 'fadeInRight'); ?>><?php esc_html_e('右移淡入', 'aksrt'); ?></option>
|
||||
<option value="zoomIn" <?php selected($instance['animation_in'], 'zoomIn'); ?>><?php esc_html_e('缩放进入', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="<?php echo esc_attr($instance['animation_repeat_name'] ?? 'animation_repeat'); ?>" value="1" <?php checked($instance['animation_repeat']); ?>>
|
||||
<?php esc_html_e('动画重复播放', 'aksrt'); ?>
|
||||
</label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_update_common')) {
|
||||
/**
|
||||
* Process shared widget form fields in update().
|
||||
*
|
||||
* @param array<string, mixed> $new_instance
|
||||
* @param array<string, mixed> $old_instance
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function aksrt_widget_update_common(array $new_instance, array $old_instance): array {
|
||||
$merged = array_merge($old_instance, $new_instance);
|
||||
return aksrt_widget_normalize_common($merged);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_render_common_before')) {
|
||||
/**
|
||||
* Render common wrapper attributes and classes before widget content.
|
||||
*
|
||||
* Outputs CSS classes for animation, affix, show_type, etc.
|
||||
* Call this at the start of widget() before rendering content.
|
||||
*
|
||||
* @param array $args Sidebar args.
|
||||
* @param array $instance Widget instance.
|
||||
* @return array Modified args with extra classes.
|
||||
*/
|
||||
function aksrt_widget_get_common_wrapper_classes(array $args, array $instance): array {
|
||||
$instance = aksrt_widget_normalize_common($instance);
|
||||
$classes = [];
|
||||
|
||||
// Show type: PC or mobile
|
||||
if ($instance['show_type'] === 'pc') {
|
||||
$classes[] = 'aksrt-widget--pc-only';
|
||||
} elseif ($instance['show_type'] === 'mobile') {
|
||||
$classes[] = 'aksrt-widget--mobile-only';
|
||||
}
|
||||
|
||||
// Sidebar affix
|
||||
if ($instance['sidebar_affix']) {
|
||||
$classes[] = 'aksrt-widget--affix';
|
||||
}
|
||||
|
||||
// Animation
|
||||
if ($instance['animation_in'] !== '') {
|
||||
$classes[] = 'aksrt-widget--animated';
|
||||
$classes[] = 'aksrt-anim-' . $instance['animation_in'];
|
||||
if ($instance['animation_repeat']) {
|
||||
$classes[] = 'aksrt-anim-repeat';
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_should_display_by_ids')) {
|
||||
/**
|
||||
* Check if the current post/page should display the widget based on show_id_type and show_ids.
|
||||
*
|
||||
* @param array $instance Widget instance.
|
||||
* @return bool
|
||||
*/
|
||||
function aksrt_widget_should_display_by_ids(array $instance): bool {
|
||||
$instance = aksrt_widget_normalize_common($instance);
|
||||
|
||||
if ($instance['show_id_type'] === '' || $instance['show_ids'] === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$current_id = get_the_ID();
|
||||
if (!$current_id) {
|
||||
return true; // Not on a singular page, allow display
|
||||
}
|
||||
|
||||
$ids = array_filter(array_map('absint', explode(',', $instance['show_ids'])));
|
||||
|
||||
if ($instance['show_id_type'] === 'include') {
|
||||
return in_array($current_id, $ids, true);
|
||||
}
|
||||
|
||||
if ($instance['show_id_type'] === 'exclude') {
|
||||
return !in_array($current_id, $ids, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_widget_render_location_help')) {
|
||||
/**
|
||||
* Render placement hint inside widget admin forms.
|
||||
*/
|
||||
function aksrt_widget_render_location_help(string $scope): void {
|
||||
$messages = [
|
||||
'sidebar' => __('仅限侧栏区域:首页侧栏、博客侧栏、文章侧栏、全站侧栏、分类/标签/搜索侧栏。', 'aksrt'),
|
||||
'homepage' => __('仅限首页挂载区域:首页顶部/底部全宽、首页内容上方/下方。', 'aksrt'),
|
||||
'hybrid' => __('支持侧栏、首页和归档页挂载区域。', 'aksrt'),
|
||||
'archive' => __('仅限归档页挂载区域:分类页、标签页、搜索页的顶部/底部/侧栏。', 'aksrt'),
|
||||
];
|
||||
|
||||
if (empty($messages[$scope])) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<p style="margin-top:10px;font-size:12px;color:#666;">%s</p>',
|
||||
esc_html($messages[$scope])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_filter_widget_display_by_area')) {
|
||||
/**
|
||||
* Prevent widgets from rendering outside their allowed areas,
|
||||
* and apply per-widget display rules (show_type, show_id_type/show_ids).
|
||||
*
|
||||
* @param array|false $instance Widget instance.
|
||||
* @param WP_Widget $widget Widget object.
|
||||
* @param array $args Sidebar args.
|
||||
* @return array|false
|
||||
*/
|
||||
function aksrt_filter_widget_display_by_area($instance, WP_Widget $widget, array $args) {
|
||||
$sidebar_id = isset($args['id']) ? (string) $args['id'] : '';
|
||||
$widget_base_id = isset($widget->id_base) ? (string) $widget->id_base : '';
|
||||
|
||||
if ($instance === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Layer 1: Area legality check
|
||||
if (!aksrt_widget_is_allowed_in_sidebar($widget_base_id, $sidebar_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Layer 2: Display rule check (show_type for PC/mobile)
|
||||
if (is_array($instance) && function_exists('aksrt_widget_normalize_common')) {
|
||||
$normalized = aksrt_widget_normalize_common($instance);
|
||||
|
||||
// Show type: PC / mobile filter
|
||||
if ($normalized['show_type'] === 'pc' && wp_is_mobile()) {
|
||||
return false;
|
||||
}
|
||||
if ($normalized['show_type'] === 'mobile' && !wp_is_mobile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show IDs: whitelist / blacklist filter
|
||||
if (!aksrt_widget_should_display_by_ids($normalized)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
add_filter('widget_display_callback', 'aksrt_filter_widget_display_by_area', 10, 3);
|
||||
|
||||
if (!function_exists('aksrt_filter_widget_sidebar_assignments')) {
|
||||
/**
|
||||
* Move disallowed widget assignments back to the inactive widgets bucket.
|
||||
*
|
||||
* @param mixed $new_value New sidebars_widgets option value.
|
||||
* @param mixed $old_value Previous option value.
|
||||
* @return mixed
|
||||
*/
|
||||
function aksrt_filter_widget_sidebar_assignments($new_value, $old_value) {
|
||||
if (!is_array($new_value)) {
|
||||
return $new_value;
|
||||
}
|
||||
|
||||
$removed = [];
|
||||
|
||||
foreach ($new_value as $sidebar_id => $widget_ids) {
|
||||
if (!is_array($widget_ids) || $sidebar_id === 'wp_inactive_widgets') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
foreach ($widget_ids as $widget_id) {
|
||||
if (!is_string($widget_id) || $widget_id === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$widget_base_id = aksrt_widget_base_from_id($widget_id);
|
||||
if (aksrt_widget_is_allowed_in_sidebar($widget_base_id, (string) $sidebar_id)) {
|
||||
$filtered[] = $widget_id;
|
||||
} else {
|
||||
$removed[] = $widget_id;
|
||||
}
|
||||
}
|
||||
|
||||
$new_value[$sidebar_id] = array_values($filtered);
|
||||
}
|
||||
|
||||
if (empty($removed)) {
|
||||
return $new_value;
|
||||
}
|
||||
|
||||
if (!isset($new_value['wp_inactive_widgets']) || !is_array($new_value['wp_inactive_widgets'])) {
|
||||
$new_value['wp_inactive_widgets'] = [];
|
||||
}
|
||||
|
||||
$assigned = [];
|
||||
foreach ($new_value as $widget_ids) {
|
||||
if (!is_array($widget_ids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($widget_ids as $widget_id) {
|
||||
if (is_string($widget_id) && $widget_id !== '') {
|
||||
$assigned[$widget_id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_values(array_unique($removed)) as $widget_id) {
|
||||
if (!isset($assigned[$widget_id]) && !in_array($widget_id, $new_value['wp_inactive_widgets'], true)) {
|
||||
$new_value['wp_inactive_widgets'][] = $widget_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $new_value;
|
||||
}
|
||||
}
|
||||
add_filter('pre_update_option_sidebars_widgets', 'aksrt_filter_widget_sidebar_assignments', 10, 2);
|
||||
|
||||
/**
|
||||
* Ensure widget instance config exists.
|
||||
*/
|
||||
function aksrt_ensure_widget_instance(string $option_name, int $number, array $defaults): void {
|
||||
$instances = get_option($option_name, []);
|
||||
|
||||
if (!is_array($instances)) {
|
||||
$instances = [];
|
||||
}
|
||||
|
||||
if (!isset($instances['_multiwidget'])) {
|
||||
$instances['_multiwidget'] = 1;
|
||||
}
|
||||
|
||||
if (empty($instances[$number]) || !is_array($instances[$number])) {
|
||||
$instances[$number] = $defaults;
|
||||
update_option($option_name, $instances);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Repair the sidebar-blog structure and seed default sidebar widgets on first install.
|
||||
*/
|
||||
function aksrt_maybe_repair_sidebar_blog_widgets(): void {
|
||||
$sidebars = get_option('sidebars_widgets', []);
|
||||
$seeded = (bool) get_option('aksrt_sidebar_blog_seeded', false);
|
||||
|
||||
if (!is_array($sidebars)) {
|
||||
$sidebars = [];
|
||||
}
|
||||
|
||||
$current = $sidebars['sidebar-blog'] ?? null;
|
||||
$needs_repair = false;
|
||||
$normalized = [];
|
||||
|
||||
if (is_array($current)) {
|
||||
foreach ($current as $item) {
|
||||
if (is_array($item)) {
|
||||
$needs_repair = true;
|
||||
}
|
||||
|
||||
if (is_string($item) && $item !== '') {
|
||||
$normalized[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($needs_repair) {
|
||||
$sidebars['sidebar-blog'] = array_values($normalized);
|
||||
update_option('sidebars_widgets', $sidebars);
|
||||
update_option('aksrt_sidebar_blog_seeded', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($current !== null) {
|
||||
if (!$seeded) {
|
||||
update_option('aksrt_sidebar_blog_seeded', true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($seeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
aksrt_ensure_widget_instance('widget_aksrt_about', 2, [
|
||||
'title' => '',
|
||||
]);
|
||||
aksrt_ensure_widget_instance('widget_aksrt_categories', 2, [
|
||||
'title' => __('分类目录', 'aksrt'),
|
||||
'count' => 8,
|
||||
]);
|
||||
aksrt_ensure_widget_instance('widget_aksrt_tag_cloud', 2, [
|
||||
'title' => __('标签云', 'aksrt'),
|
||||
'count' => 15,
|
||||
]);
|
||||
|
||||
$sidebars['sidebar-blog'] = [
|
||||
'aksrt_about-2',
|
||||
'aksrt_categories-2',
|
||||
'aksrt_tag_cloud-2',
|
||||
];
|
||||
update_option('sidebars_widgets', $sidebars);
|
||||
update_option('aksrt_sidebar_blog_seeded', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a block editor collection for AKSRT widgets.
|
||||
*/
|
||||
add_action('enqueue_block_editor_assets', function () {
|
||||
$screen = get_current_screen();
|
||||
if (!$screen || $screen->id !== 'widgets') {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_add_inline_script('wp-blocks', '
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof wp !== "undefined" && wp.blocks && wp.blocks.registerBlockCollection) {
|
||||
wp.blocks.registerBlockCollection("aksrt", {
|
||||
title: "AKSRT 主题小工具",
|
||||
icon: undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
');
|
||||
});
|
||||
|
||||
/**
|
||||
* Assign AKSRT widgets to the AKSRT block category in the widgets editor.
|
||||
*
|
||||
* @param array<string, mixed> $metadata Block metadata.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
add_filter('block_type_metadata', function ($metadata) {
|
||||
$aksrt_widgets = [
|
||||
'aksrt_about',
|
||||
'aksrt_categories',
|
||||
'aksrt_tag_cloud',
|
||||
];
|
||||
|
||||
if (!empty($metadata['name']) && in_array($metadata['name'], $aksrt_widgets, true)) {
|
||||
$metadata['category'] = 'aksrt';
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
});
|
||||
|
||||
/**
|
||||
* Show widget placement hints in the admin form.
|
||||
*/
|
||||
add_action('in_widget_form', function (WP_Widget $widget): void {
|
||||
$map = [
|
||||
'aksrt_about' => 'sidebar',
|
||||
'aksrt_categories' => 'sidebar',
|
||||
'aksrt_tag_cloud' => 'sidebar',
|
||||
'aksrt_hot_posts' => 'sidebar',
|
||||
'aksrt_tab_posts' => 'hybrid',
|
||||
'aksrt_category_block' => 'hybrid',
|
||||
'aksrt_category_tabs' => 'hybrid',
|
||||
'aksrt_category_recommend' => 'homepage',
|
||||
];
|
||||
|
||||
$scope = $map[$widget->id_base] ?? '';
|
||||
if ($scope !== '') {
|
||||
aksrt_widget_render_location_help($scope);
|
||||
}
|
||||
}, 10, 1);
|
||||
Reference in new issue
Block a user