feat(ux): forum pay-hide depth, clogin OAuth, ban appeals admin, posts-navs widget
- Forum compose/save/draft: VIP prices, partial hide, coupons + shortcode tools - Rainbow clogin aggregate login (rewrite + schema + CLoginClient) - Native ban appeals tab: approve/reject + lift ban + notify - WidgetPostsNavs + register missing forum widgets - Submit editor shortcodes: login/reply/vip/payshow
This commit is contained in:
1 parent
25aabb71a3
commit
24330653f0
15 files changed
+834
-35
No files matched your search
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* CaiHong / rainbow aggregate OAuth client (zibll clogin parity).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Auth;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class CLoginClient {
|
||||
|
||||
private string $apiurl;
|
||||
private string $appid;
|
||||
private string $appkey;
|
||||
private string $callback;
|
||||
public string $state = '';
|
||||
|
||||
/**
|
||||
* @param array{apiurl:string,appid:string,appkey:string,callback?:string} $config
|
||||
*/
|
||||
public function __construct(array $config) {
|
||||
$this->apiurl = rtrim((string) ($config['apiurl'] ?? ''), '/') . '/connect.php';
|
||||
$this->appid = (string) ($config['appid'] ?? '');
|
||||
$this->appkey = (string) ($config['appkey'] ?? '');
|
||||
$this->callback = (string) ($config['callback'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function login(string $type): array {
|
||||
$this->state = md5(uniqid((string) wp_rand(), true));
|
||||
$params = [
|
||||
'act' => 'login',
|
||||
'appid' => $this->appid,
|
||||
'appkey' => $this->appkey,
|
||||
'type' => $this->to_api_type($type),
|
||||
'redirect_uri' => $this->callback,
|
||||
'state' => $this->state,
|
||||
];
|
||||
return $this->request($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function callback(string $code): array {
|
||||
return $this->request([
|
||||
'act' => 'callback',
|
||||
'appid' => $this->appid,
|
||||
'appkey' => $this->appkey,
|
||||
'code' => $code,
|
||||
]);
|
||||
}
|
||||
|
||||
public function to_api_type(string $type): string {
|
||||
switch (sanitize_key($type)) {
|
||||
case 'weixin':
|
||||
case 'wechat':
|
||||
return 'wx';
|
||||
case 'weibo':
|
||||
return 'sina';
|
||||
default:
|
||||
return sanitize_key($type);
|
||||
}
|
||||
}
|
||||
|
||||
public function from_api_type(string $type): string {
|
||||
switch (sanitize_key($type)) {
|
||||
case 'wx':
|
||||
return 'weixin';
|
||||
case 'sina':
|
||||
return 'weibo';
|
||||
default:
|
||||
return sanitize_key($type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $params
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function request(array $params): array {
|
||||
$url = $this->apiurl . '?' . http_build_query($params);
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => 15,
|
||||
'headers' => [
|
||||
'User-Agent' => 'AKSRT-CLogin/1.0',
|
||||
],
|
||||
]);
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
'code' => 1,
|
||||
'msg' => $response->get_error_message(),
|
||||
];
|
||||
}
|
||||
$body = (string) wp_remote_retrieve_body($response);
|
||||
$data = json_decode($body, true);
|
||||
return is_array($data) ? $data : [
|
||||
'code' => 1,
|
||||
'msg' => __('聚合登录接口返回异常', 'aksrt'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,15 @@ class OAuth {
|
||||
public function init(): void {
|
||||
add_action('init', [$this, 'add_rewrite_rules']);
|
||||
add_action('template_redirect', [$this, 'handle_callback']);
|
||||
add_action('template_redirect', [$this, 'handle_clogin_routes'], 9);
|
||||
add_action('show_user_profile', [$this, 'profile_bindings'], 10, 1);
|
||||
add_action('wp_ajax_aksrt_oauth_unbind', [$this, 'ajax_unbind']);
|
||||
}
|
||||
|
||||
public function add_rewrite_rules(): void {
|
||||
add_rewrite_rule('^oauth/callback/?$', 'index.php?aksrt_oauth=callback', 'top');
|
||||
add_rewrite_rule('^oauth/clogin/?$', 'index.php?aksrt_oauth=clogin', 'top');
|
||||
add_rewrite_rule('^oauth/clogin/callback/?$', 'index.php?aksrt_oauth=clogin_callback', 'top');
|
||||
add_filter('query_vars', function($vars) {
|
||||
$vars[] = 'aksrt_oauth';
|
||||
return $vars;
|
||||
@@ -40,6 +43,15 @@ class OAuth {
|
||||
* 生成OAuth授权URL
|
||||
*/
|
||||
public function get_auth_url(string $provider, string $action = 'login'): string {
|
||||
$provider = sanitize_key($provider);
|
||||
if (str_starts_with($provider, 'clogin_') || str_starts_with($provider, 'clogin-')) {
|
||||
$type = str_replace(['clogin_', 'clogin-'], '', $provider);
|
||||
return $this->get_clogin_login_url($type, $action);
|
||||
}
|
||||
if ($provider === 'clogin') {
|
||||
return $this->get_clogin_login_url('qq', $action);
|
||||
}
|
||||
|
||||
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::oauth_proxy_mode() === 'client') {
|
||||
if (class_exists('AKSRT\\Auth\\OAuthProxyClient')) {
|
||||
return OAuthProxyClient::get_instance()->get_proxy_auth_url($provider, $action);
|
||||
|
||||
+242
-29
@@ -217,7 +217,28 @@ trait OAuthPart1 {
|
||||
}
|
||||
}
|
||||
}
|
||||
return array_unique($enabled);
|
||||
// Rainbow aggregate (clogin) providers as virtual keys: clogin_qq, clogin_weixin...
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
$s = \AKSRT\Settings\Settings::get_all();
|
||||
if (!empty($s['clogin_s']) && !empty($s['clogin_url']) && !empty($s['clogin_id']) && !empty($s['clogin_key'])) {
|
||||
$raw = $s['clogin_oauth_s'] ?? $s['clogin_types'] ?? ['qq', 'weixin', 'weibo', 'alipay'];
|
||||
if (is_string($raw)) {
|
||||
$raw = preg_split('/[,,\s]+/', $raw) ?: [];
|
||||
}
|
||||
if (!is_array($raw)) {
|
||||
$raw = [];
|
||||
}
|
||||
foreach ($raw as $ctype) {
|
||||
$ctype = sanitize_key((string) $ctype);
|
||||
if ($ctype === '') {
|
||||
continue;
|
||||
}
|
||||
$enabled[] = 'clogin_' . $ctype;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($enabled));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,10 +263,20 @@ trait OAuthPart1 {
|
||||
$url = $this->get_auth_url($key);
|
||||
if (empty($url)) continue;
|
||||
|
||||
$label = $platform_names[$key] ?? '';
|
||||
$icon_key = $key;
|
||||
if (str_starts_with($key, 'clogin_')) {
|
||||
$ctype = substr($key, 7);
|
||||
$label = ($platform_names[$ctype] ?? strtoupper($ctype)) . ' · 聚合';
|
||||
$icon_key = $ctype === 'weixin' ? 'weixin' : $ctype;
|
||||
}
|
||||
if ($label === '') {
|
||||
$label = ucfirst($key);
|
||||
}
|
||||
$result[] = [
|
||||
'name' => $key,
|
||||
'label' => $platform_names[$key] ?? ucfirst($key),
|
||||
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon((string) $key) : '',
|
||||
'label' => $label,
|
||||
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon((string) $icon_key) : '',
|
||||
'url' => esc_url($url),
|
||||
];
|
||||
}
|
||||
@@ -345,42 +376,28 @@ trait OAuthPart1 {
|
||||
}
|
||||
|
||||
public function render_social_buttons(string $title = ''): string {
|
||||
$providers = get_option('aksrt_oauth', []);
|
||||
$enabled_providers = [];
|
||||
foreach ($providers as $key => $cfg) {
|
||||
if (!empty($cfg['enabled']) && !empty($cfg['client_id']) && in_array($key, ['github', 'gitee', 'qq', 'google', 'weibo', 'weixin', 'baidu', 'alipay'], true)) {
|
||||
$enabled_providers[$key] = $cfg;
|
||||
}
|
||||
$items = $this->get_auth_providers_data();
|
||||
if (empty($items)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (empty($enabled_providers)) return '';
|
||||
|
||||
$icons = [
|
||||
'github' => aksrt_brand_icon('github'),
|
||||
'gitee' => aksrt_brand_icon('gitee'),
|
||||
'qq' => aksrt_brand_icon('qq'),
|
||||
'google' => aksrt_brand_icon('google'),
|
||||
'weibo' => aksrt_brand_icon('weibo'),
|
||||
'weixin' => aksrt_brand_icon('weixin'),
|
||||
'baidu' => aksrt_brand_icon('baidu'),
|
||||
'alipay' => aksrt_brand_icon('alipay'),
|
||||
];
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="social-login-divider">
|
||||
<span><?php echo esc_html($title) ?: esc_html__('or', 'aksrt'); ?></span>
|
||||
</div>
|
||||
<div class="oauth-row">
|
||||
<?php foreach ($enabled_providers as $key => $cfg):
|
||||
$url = $this->get_auth_url($key);
|
||||
if (empty($url)) continue;
|
||||
<?php foreach ($items as $item):
|
||||
$key = (string) ($item['name'] ?? '');
|
||||
$url = (string) ($item['url'] ?? '');
|
||||
if ($key === '' || $url === '') continue;
|
||||
?>
|
||||
<a href="<?php echo esc_url($url); ?>"
|
||||
class="oauth-btn"
|
||||
aria-label="<?php echo esc_attr(ucfirst($key)); ?>"
|
||||
title="<?php echo esc_attr(ucfirst($key)); ?>">
|
||||
<?php echo $icons[$key] ?? ''; ?>
|
||||
class="oauth-btn oauth-btn--<?php echo esc_attr($key); ?>"
|
||||
data-provider="<?php echo esc_attr($key); ?>"
|
||||
aria-label="<?php echo esc_attr((string) ($item['label'] ?? $key)); ?>"
|
||||
title="<?php echo esc_attr((string) ($item['label'] ?? $key)); ?>">
|
||||
<?php echo $item['icon'] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
@@ -399,4 +416,200 @@ trait OAuthPart1 {
|
||||
public function register_message(string $message): string {
|
||||
return $message . self::get_instance()->render_social_buttons(__('或使用社交账号快速注册', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled:bool,url:string,id:string,key:string,types:string[]}
|
||||
*/
|
||||
private function get_clogin_settings(): array {
|
||||
$s = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$raw = $s['clogin_oauth_s'] ?? $s['clogin_types'] ?? ['qq', 'weixin', 'weibo', 'alipay'];
|
||||
if (is_string($raw)) {
|
||||
$raw = preg_split('/[,,\s]+/', $raw) ?: [];
|
||||
}
|
||||
if (!is_array($raw)) {
|
||||
$raw = [];
|
||||
}
|
||||
$types = [];
|
||||
foreach ($raw as $t) {
|
||||
$t = sanitize_key((string) $t);
|
||||
if ($t !== '') {
|
||||
$types[] = $t;
|
||||
}
|
||||
}
|
||||
return [
|
||||
'enabled' => !empty($s['clogin_s']),
|
||||
'url' => esc_url_raw((string) ($s['clogin_url'] ?? '')),
|
||||
'id' => (string) ($s['clogin_id'] ?? ''),
|
||||
'key' => (string) ($s['clogin_key'] ?? ''),
|
||||
'types' => array_values(array_unique($types)),
|
||||
];
|
||||
}
|
||||
|
||||
private function get_clogin_login_url(string $type, string $action = 'login'): string {
|
||||
$cfg = $this->get_clogin_settings();
|
||||
$type = sanitize_key($type);
|
||||
if (!$cfg['enabled'] || $cfg['url'] === '' || $cfg['id'] === '' || $cfg['key'] === '' || $type === '') {
|
||||
return '';
|
||||
}
|
||||
if (!empty($cfg['types']) && !in_array($type, $cfg['types'], true)) {
|
||||
return '';
|
||||
}
|
||||
$args = ['type' => $type];
|
||||
if ($action === 'bind' && is_user_logged_in()) {
|
||||
$args['bind'] = 1;
|
||||
}
|
||||
$rurl = isset($_GET['redirect_to']) ? esc_url_raw(wp_unslash((string) $_GET['redirect_to'])) : '';
|
||||
if ($rurl !== '') {
|
||||
$args['rurl'] = $rurl;
|
||||
}
|
||||
return add_query_arg($args, home_url('/oauth/clogin/'));
|
||||
}
|
||||
|
||||
public function handle_clogin_routes(): void {
|
||||
$route = get_query_var('aksrt_oauth');
|
||||
if ($route === 'clogin') {
|
||||
$this->handle_clogin_login();
|
||||
return;
|
||||
}
|
||||
if ($route === 'clogin_callback') {
|
||||
$this->handle_clogin_callback();
|
||||
}
|
||||
}
|
||||
|
||||
private function handle_clogin_login(): void {
|
||||
$cfg = $this->get_clogin_settings();
|
||||
$type = isset($_GET['type']) ? sanitize_key(wp_unslash((string) $_GET['type'])) : '';
|
||||
if (!$cfg['enabled'] || $cfg['url'] === '' || $cfg['id'] === '' || $cfg['key'] === '' || $type === '') {
|
||||
wp_die(esc_html__('暂未启用此登录方式', 'aksrt'));
|
||||
}
|
||||
if (!empty($cfg['types']) && !in_array($type, $cfg['types'], true)) {
|
||||
wp_die(esc_html__('此登录方式未启用', 'aksrt'));
|
||||
}
|
||||
if (!class_exists(CLoginClient::class)) {
|
||||
wp_die(esc_html__('聚合登录组件未加载', 'aksrt'));
|
||||
}
|
||||
|
||||
$client = new CLoginClient([
|
||||
'apiurl' => $cfg['url'],
|
||||
'appid' => $cfg['id'],
|
||||
'appkey' => $cfg['key'],
|
||||
'callback' => home_url('/oauth/clogin/callback'),
|
||||
]);
|
||||
$resp = $client->login($type);
|
||||
if (!empty($resp['code']) && (int) $resp['code'] !== 0 && !empty($resp['msg'])) {
|
||||
wp_die(esc_html(sprintf(__('处理失败:%s', 'aksrt'), (string) $resp['msg'])));
|
||||
}
|
||||
if (empty($resp['url'])) {
|
||||
wp_die(esc_html__('无法获取聚合登录地址', 'aksrt'));
|
||||
}
|
||||
|
||||
if (!session_id()) {
|
||||
@session_start();
|
||||
}
|
||||
$_SESSION['AKSRT_CLOGIN_STATE'] = $client->state;
|
||||
$_SESSION['AKSRT_CLOGIN_TYPE'] = $type;
|
||||
$_SESSION['AKSRT_CLOGIN_RURL'] = !empty($_GET['rurl']) ? esc_url_raw(wp_unslash((string) $_GET['rurl'])) : '';
|
||||
$_SESSION['AKSRT_CLOGIN_BIND'] = !empty($_GET['bind']) ? 1 : 0;
|
||||
|
||||
wp_redirect(esc_url_raw((string) $resp['url']));
|
||||
exit;
|
||||
}
|
||||
|
||||
private function handle_clogin_callback(): void {
|
||||
if (!session_id()) {
|
||||
@session_start();
|
||||
}
|
||||
$state = isset($_GET['state']) ? (string) wp_unslash($_GET['state']) : '';
|
||||
$code = isset($_GET['code']) ? (string) wp_unslash($_GET['code']) : '';
|
||||
if ($code === '' || $state === '' || empty($_SESSION['AKSRT_CLOGIN_STATE']) || !hash_equals((string) $_SESSION['AKSRT_CLOGIN_STATE'], $state)) {
|
||||
wp_die(esc_html__('环境异常,state 验证失败,请刷新后重试', 'aksrt'));
|
||||
}
|
||||
|
||||
$cfg = $this->get_clogin_settings();
|
||||
if (!$cfg['enabled']) {
|
||||
wp_die(esc_html__('暂未启用此功能', 'aksrt'));
|
||||
}
|
||||
$client = new CLoginClient([
|
||||
'apiurl' => $cfg['url'],
|
||||
'appid' => $cfg['id'],
|
||||
'appkey' => $cfg['key'],
|
||||
]);
|
||||
$info = $client->callback($code);
|
||||
if (!empty($info['code']) && (int) $info['code'] !== 0 && !empty($info['msg'])) {
|
||||
wp_die(esc_html((string) $info['msg']));
|
||||
}
|
||||
$openid = (string) ($info['social_uid'] ?? '');
|
||||
$api_type = (string) ($info['type'] ?? ($_SESSION['AKSRT_CLOGIN_TYPE'] ?? ''));
|
||||
$provider_type = $client->from_api_type($api_type);
|
||||
if ($openid === '' || $provider_type === '') {
|
||||
wp_die(esc_html__('无法获取用户信息', 'aksrt'));
|
||||
}
|
||||
|
||||
$provider = 'clogin_' . sanitize_key($provider_type);
|
||||
$user_data = [
|
||||
'provider' => $provider,
|
||||
'provider_id' => $openid,
|
||||
'username' => (string) ($info['nickname'] ?? ''),
|
||||
'name' => (string) ($info['nickname'] ?? ''),
|
||||
'email' => '',
|
||||
'avatar_url' => (string) ($info['faceimg'] ?? ''),
|
||||
];
|
||||
if ($user_data['avatar_url'] !== '' && str_contains($user_data['avatar_url'], 'no_portrait')) {
|
||||
$user_data['avatar_url'] = '';
|
||||
}
|
||||
|
||||
$is_bind = !empty($_SESSION['AKSRT_CLOGIN_BIND']);
|
||||
$rurl = !empty($_SESSION['AKSRT_CLOGIN_RURL']) ? (string) $_SESSION['AKSRT_CLOGIN_RURL'] : '';
|
||||
unset($_SESSION['AKSRT_CLOGIN_STATE'], $_SESSION['AKSRT_CLOGIN_TYPE'], $_SESSION['AKSRT_CLOGIN_RURL'], $_SESSION['AKSRT_CLOGIN_BIND']);
|
||||
|
||||
try {
|
||||
if ($is_bind) {
|
||||
$uid = get_current_user_id();
|
||||
if ($uid <= 0) {
|
||||
wp_safe_redirect(function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url());
|
||||
exit;
|
||||
}
|
||||
$existing = $this->find_user_by_provider($provider, $openid);
|
||||
if ($existing && (int) $existing->ID !== $uid) {
|
||||
throw new \Exception(__('该第三方账号已被其他用户绑定', 'aksrt'));
|
||||
}
|
||||
$this->bind_provider_to_user($uid, $provider, $user_data);
|
||||
$redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/');
|
||||
wp_safe_redirect(add_query_arg(['tab' => 'settings', 'oauth_action' => 'bind', 'oauth_bound' => $provider], $redirect));
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = $this->find_or_create_user($provider, $user_data);
|
||||
if (is_wp_error($user) && $user->get_error_code() === 'oauth_no_match') {
|
||||
// Auto-create account for aggregate login when no match (zibll-like).
|
||||
$login = 'u' . wp_generate_password(10, false, false);
|
||||
$uid = wp_insert_user([
|
||||
'user_login' => $login,
|
||||
'user_pass' => wp_generate_password(24, true, true),
|
||||
'display_name' => $user_data['name'] !== '' ? $user_data['name'] : $login,
|
||||
'nickname' => $user_data['name'] !== '' ? $user_data['name'] : $login,
|
||||
'role' => get_option('default_role', 'subscriber'),
|
||||
]);
|
||||
if (is_wp_error($uid)) {
|
||||
throw new \Exception($uid->get_error_message());
|
||||
}
|
||||
$this->bind_provider_to_user((int) $uid, $provider, $user_data);
|
||||
$user = get_user_by('id', (int) $uid);
|
||||
}
|
||||
if (is_wp_error($user) || !$user instanceof \WP_User) {
|
||||
throw new \Exception(is_wp_error($user) ? $user->get_error_message() : __('登录失败', 'aksrt'));
|
||||
}
|
||||
wp_set_current_user($user->ID);
|
||||
wp_set_auth_cookie($user->ID, true);
|
||||
do_action('wp_login', $user->user_login, $user);
|
||||
|
||||
$redirect = $rurl !== '' ? $rurl : (function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'));
|
||||
wp_safe_redirect(add_query_arg(['oauth_action' => 'login', 'oauth_bound' => $provider], $redirect));
|
||||
exit;
|
||||
} catch (\Exception $e) {
|
||||
wp_die(esc_html__('登录失败:', 'aksrt') . '<br>' . esc_html($e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user