The theme shipped six direct providers while the reference carries fourteen, and its aggregate-login channel offered three of sixteen platforms. OAuthProviderClient now owns the providers the bundled SDK does not cover. Douyin, Microsoft, Facebook, Twitter and Apple talk to their documented OAuth2 endpoints directly; Baidu and Alipay reuse yurunsoft, which already ships both (Alipay needs its application private key, so provider_config learned an extra_keys map to surface it). The broker dispatches through handles(), so the existing start/callback routes stay provider-agnostic. Three provider quirks needed handling rather than a plain copy: Apple returns the callback as response_mode=form_post, and handle_callback only read $_GET, so it now reads $_POST when the request is a POST — otherwise every Apple login would fail with code_missing. Its client secret is built as an ES256 JWT from Team ID / Key ID / .p8 rather than a static string. Twitter requires PKCE. The verifier is derived from the state token, so it travels with the existing transaction row and needs no extra storage. Facebook hands back protocol-less avatar URLs, which are upgraded to https the same way the OAuth avatar resolver does. Settings gained matching toggles and credential groups on both sides — the PHP schema and the Vue schema — and the aggregate platform list went from three to sixteen, with CLoginClient's hard-coded allowlist replaced by the catalog constant. Brand icons were extended so each new provider resolves to its sprite; facebook and twitter moved to their colour sprites, and the `twitter -> x` alias was dropped since it bypassed the new mapping. The oauth-security contract previously asserted that Baidu and Alipay must not appear on the OAuth surface. That was the old intent, so the assertion is replaced with a positive one, keeping the separate check that the Alipay payment gateway still exists. Verification: a new harness stubs the HTTP layer and drives all five custom clients — authorization URLs carry redirect/state (Twitter also a PKCE challenge), identities normalize to the shared shape, Facebook's avatar is upgraded to https, Apple's private-relay email survives, and an error payload throws instead of yielding an empty identity. 871 widget/navigation tests pass and the PSR-4 audit reports 0 errors. Not covered: no live handshake happened — every provider needs real AppID and secret pairs, and Apple/Douyin require developer-account review before the full round trip can be exercised.
528 lines
20 KiB
PHP
528 lines
20 KiB
PHP
<?php
|
||
/**
|
||
* @package Jio
|
||
*/
|
||
|
||
namespace Jio\Auth;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
final class OAuth {
|
||
|
||
private const BIND_PASS_KIND = 'oauth_bind_pass';
|
||
private const BIND_PASS_TTL = 5 * MINUTE_IN_SECONDS;
|
||
|
||
private static $instance = null;
|
||
private OAuthProviderBroker $broker;
|
||
|
||
private function __construct() {
|
||
$this->broker = new OAuthProviderBroker();
|
||
}
|
||
|
||
public static function get_instance(): self {
|
||
if (null === self::$instance) {
|
||
self::$instance = new self();
|
||
}
|
||
return self::$instance;
|
||
}
|
||
|
||
public function init(): void {
|
||
add_action('init', [$this, 'add_rewrite_rules']);
|
||
add_action('init', [$this, 'maybe_flush_rewrite_rules'], 20);
|
||
add_action('init', static function (): void {
|
||
if (wp_rand(1, 100) === 1) {
|
||
OAuthTransactionStore::cleanup();
|
||
}
|
||
}, 30);
|
||
add_action('template_redirect', [$this, 'handle_routes'], 8);
|
||
add_action('rest_api_init', [$this, 'register_rest_routes']);
|
||
add_action('wp_ajax_jio_oauth_unbind', [$this, 'ajax_unbind']);
|
||
}
|
||
|
||
public function add_rewrite_rules(): void {
|
||
add_rewrite_rule('^oauth/start/([a-z0-9_-]+)/?$', 'index.php?jio_oauth=start&jio_oauth_provider=$matches[1]', 'top');
|
||
add_rewrite_rule('^oauth/callback/?$', 'index.php?jio_oauth=callback', 'top');
|
||
add_rewrite_rule('^oauth/clogin/?$', 'index.php?jio_oauth=clogin', 'top');
|
||
add_rewrite_rule('^oauth/clogin/callback/?$', 'index.php?jio_oauth=clogin_callback', 'top');
|
||
add_filter('query_vars', static function (array $vars): array {
|
||
$vars[] = 'jio_oauth';
|
||
$vars[] = 'jio_oauth_provider';
|
||
return $vars;
|
||
});
|
||
}
|
||
|
||
public function register_rest_routes(): void {
|
||
register_rest_route('jio/v1', '/oauth/bind-pass', [
|
||
'methods' => 'POST',
|
||
'callback' => [$this, 'rest_issue_bind_pass'],
|
||
'permission_callback' => static function (): bool {
|
||
return is_user_logged_in();
|
||
},
|
||
]);
|
||
}
|
||
|
||
public function rest_issue_bind_pass(\WP_REST_Request $request): \WP_REST_Response {
|
||
$user_id = get_current_user_id();
|
||
$nonce = sanitize_text_field((string) $request->get_header('X-WP-Nonce'));
|
||
if ($user_id <= 0 || $nonce === '' || !wp_verify_nonce($nonce, 'wp_rest')) {
|
||
return new \WP_REST_Response([
|
||
'success' => false,
|
||
'code' => 'oauth_bind_nonce_invalid',
|
||
'message' => __('安全验证失败,请刷新页面重试', 'jio'),
|
||
], 403);
|
||
}
|
||
|
||
$payload = $request->get_json_params();
|
||
$payload = is_array($payload) ? $payload : [];
|
||
$provider = sanitize_key((string) ($payload['provider'] ?? ''));
|
||
if (!in_array($provider, OAuthProviderCatalog::enabled_login_keys(), true)) {
|
||
return new \WP_REST_Response([
|
||
'success' => false,
|
||
'code' => 'oauth_bind_provider_invalid',
|
||
'message' => __('该第三方登录方式不可用', 'jio'),
|
||
], 400);
|
||
}
|
||
|
||
$url = $this->get_auth_url($provider, 'bind');
|
||
if ($url === '') {
|
||
return new \WP_REST_Response([
|
||
'success' => false,
|
||
'code' => 'oauth_bind_provider_invalid',
|
||
'message' => __('该第三方登录方式不可用', 'jio'),
|
||
], 400);
|
||
}
|
||
$redirect = OAuthUrlPolicy::validated_redirect((string) ($payload['redirect_to'] ?? ''), home_url('/'));
|
||
$url = add_query_arg('redirect_to', $redirect, $url);
|
||
|
||
if (!jio_is_captcha_enabled_for('bind')) {
|
||
return new \WP_REST_Response([
|
||
'success' => true,
|
||
'data' => ['url' => $url],
|
||
], 200);
|
||
}
|
||
|
||
$result = jio_require_captcha('bind', $payload);
|
||
if ($result !== true) {
|
||
return new \WP_REST_Response([
|
||
'success' => false,
|
||
'code' => is_wp_error($result) ? $result->get_error_code() : 'jio_captcha_failed',
|
||
'message' => is_wp_error($result) ? $result->get_error_message() : __('人机验证失败,请重试', 'jio'),
|
||
], 400);
|
||
}
|
||
|
||
try {
|
||
$pass = OAuthTransactionStore::issue(self::BIND_PASS_KIND, [
|
||
'user_id' => $user_id,
|
||
'provider' => $provider,
|
||
], self::BIND_PASS_TTL, true);
|
||
} catch (\Throwable $e) {
|
||
return new \WP_REST_Response([
|
||
'success' => false,
|
||
'code' => 'oauth_bind_pass_issue_failed',
|
||
'message' => __('无法创建绑定会话,请稍后重试', 'jio'),
|
||
], 500);
|
||
}
|
||
|
||
$url = add_query_arg('bind_pass', $pass, $url);
|
||
|
||
return new \WP_REST_Response([
|
||
'success' => true,
|
||
'data' => ['url' => $url],
|
||
], 200);
|
||
}
|
||
|
||
public static function validate_bind_pass(string $token, string $provider, int $user_id, bool $consume = true): bool {
|
||
if (!jio_is_captcha_enabled_for('bind')) {
|
||
return true;
|
||
}
|
||
if ($token === '' || $user_id <= 0) {
|
||
return false;
|
||
}
|
||
$matches = static function (?array $row) use ($provider, $user_id): bool {
|
||
$payload = is_array($row['payload'] ?? null) ? $row['payload'] : [];
|
||
return $row !== null
|
||
&& (int) ($payload['user_id'] ?? 0) === $user_id
|
||
&& hash_equals(sanitize_key($provider), sanitize_key((string) ($payload['provider'] ?? '')));
|
||
};
|
||
$row = OAuthTransactionStore::peek($token, self::BIND_PASS_KIND, true);
|
||
if (!$matches($row)) {
|
||
return false;
|
||
}
|
||
if (!$consume) {
|
||
return true;
|
||
}
|
||
return $matches(OAuthTransactionStore::claim($token, self::BIND_PASS_KIND, true));
|
||
}
|
||
|
||
public function maybe_flush_rewrite_rules(): void {
|
||
$version = defined('JIO_VERSION') ? JIO_VERSION . ':oauth-v2' : 'oauth-v2';
|
||
if (get_option('jio_oauth_routes_flushed') !== $version) {
|
||
flush_rewrite_rules(false);
|
||
update_option('jio_oauth_routes_flushed', $version, false);
|
||
}
|
||
}
|
||
|
||
public function handle_routes(): void {
|
||
$route = sanitize_key((string) get_query_var('jio_oauth'));
|
||
if ($route === '') {
|
||
return;
|
||
}
|
||
switch ($route) {
|
||
case 'start':
|
||
$this->handle_start();
|
||
break;
|
||
case 'callback':
|
||
$this->handle_callback();
|
||
break;
|
||
case 'clogin':
|
||
$this->handle_clogin_start();
|
||
break;
|
||
case 'clogin_callback':
|
||
$this->handle_clogin_callback();
|
||
break;
|
||
}
|
||
}
|
||
|
||
public function get_auth_url(string $provider, string $action = 'login'): string {
|
||
$provider = sanitize_key($provider);
|
||
$action = $action === 'bind' ? 'bind' : 'login';
|
||
if (!in_array($provider, OAuthProviderCatalog::enabled_login_keys(), true)) {
|
||
return '';
|
||
}
|
||
$redirect = isset($_GET['redirect_to'])
|
||
? OAuthUrlPolicy::validated_redirect((string) wp_unslash($_GET['redirect_to']), home_url('/'))
|
||
: '';
|
||
if ($provider === 'weixingzh') {
|
||
$url = function_exists('jio_get_sign_page_url') ? jio_get_sign_page_url('oauth') : home_url('/sign/?tab=oauth');
|
||
$url = add_query_arg('oauth_intent', $action, $url);
|
||
return $redirect !== '' ? add_query_arg('redirect_to', $redirect, $url) : $url;
|
||
}
|
||
if (str_starts_with($provider, 'clogin_')) {
|
||
$url = add_query_arg([
|
||
'type' => substr($provider, 7),
|
||
'action' => $action,
|
||
], home_url('/oauth/clogin/'));
|
||
return $redirect !== '' ? add_query_arg('redirect_to', $redirect, $url) : $url;
|
||
}
|
||
$url = add_query_arg('action', $action, home_url('/oauth/start/' . $provider . '/'));
|
||
return $redirect !== '' ? add_query_arg('redirect_to', $redirect, $url) : $url;
|
||
}
|
||
|
||
public function get_auth_providers_data(): array {
|
||
$result = [];
|
||
foreach (OAuthProviderCatalog::enabled_login_keys() as $provider) {
|
||
$url = $this->get_auth_url($provider);
|
||
if ($url === '') {
|
||
continue;
|
||
}
|
||
$result[] = [
|
||
'name' => $provider,
|
||
'label' => OAuthProviderCatalog::label($provider),
|
||
'icon' => function_exists('jio_brand_icon') ? jio_brand_icon(OAuthProviderCatalog::icon_key($provider)) : '',
|
||
// Raw, matching get_frontend_bindings_data(): every surface escapes
|
||
// once on output. esc_url() is idempotent on its own output, so
|
||
// escaping here too was redundant rather than broken.
|
||
'url' => $url,
|
||
];
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Social login block.
|
||
*
|
||
* Emits the muted separator plus a .social_loginbar of .social-login-item
|
||
* buttons, one per enabled provider. Returns '' when nothing is enabled.
|
||
*
|
||
* @param string $title Separator label; an empty title drops the separator.
|
||
*/
|
||
public function render_social_buttons(string $title = ''): string {
|
||
$items = $this->get_auth_providers_data();
|
||
if ($items === []) {
|
||
return '';
|
||
}
|
||
|
||
$large = (bool) \Jio\Settings\Settings::get_value('oauth_button_lg', false);
|
||
$html = $title !== ''
|
||
? '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html($title) . '</p>'
|
||
: '';
|
||
$html .= '<div class="social_loginbar">';
|
||
foreach ($items as $item) {
|
||
$name = sanitize_html_class((string) ($item['name'] ?? ''));
|
||
$type = str_starts_with($name, 'clogin_') ? substr($name, 7) : $name;
|
||
$label = trim((string) ($item['label'] ?? ''));
|
||
$url = (string) ($item['url'] ?? '');
|
||
if ($name === '' || $type === '' || $label === '' || $url === '') {
|
||
continue;
|
||
}
|
||
$class = 'social-login-item ' . $type . ($large ? ' button-lg' : ' toggle-radius');
|
||
if ($name === 'weixingzh') {
|
||
// Reference parity: the WeChat entry opens the QR panel instead of
|
||
// navigating, so auth.js routes it to the modal's oauth tab.
|
||
$class .= ' qrcode-signin';
|
||
}
|
||
$html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="' . esc_attr($class) . '"'
|
||
. ' data-provider="' . esc_attr($name) . '"'
|
||
. ' aria-label="' . esc_attr(sprintf(__('%s登录', 'jio'), $label)) . '"'
|
||
. ' title="' . esc_attr(sprintf(__('%s登录', 'jio'), $label)) . '">'
|
||
. $item['icon'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||
. ($large ? '<span>' . esc_html(sprintf(__('%s登录', 'jio'), $label)) . '</span>' : '')
|
||
. '</a>';
|
||
}
|
||
return $html . '</div>';
|
||
}
|
||
|
||
public function get_frontend_bindings_data(int $user_id): array {
|
||
$bound = OAuthAccountService::bindings($user_id);
|
||
$bindings = [];
|
||
foreach ($bound as $provider) {
|
||
$bindings[] = $this->binding_row($provider, true);
|
||
}
|
||
$available = [];
|
||
foreach (OAuthProviderCatalog::enabled_login_keys() as $provider) {
|
||
if (in_array($provider, $bound, true)) {
|
||
continue;
|
||
}
|
||
$url = $this->get_auth_url($provider, 'bind');
|
||
if ($url !== '') {
|
||
$row = $this->binding_row($provider, false);
|
||
$row['url'] = $url;
|
||
$available[] = $row;
|
||
}
|
||
}
|
||
return ['bindings' => $bindings, 'available' => $available];
|
||
}
|
||
|
||
public function ajax_unbind(): void {
|
||
check_ajax_referer('jio_oauth_unbind', 'nonce');
|
||
$provider = sanitize_key((string) ($_POST['provider'] ?? ''));
|
||
$user_id = isset($_POST['userId']) ? (int) $_POST['userId'] : 0;
|
||
$current_id = get_current_user_id();
|
||
if ($current_id <= 0 || $current_id !== $user_id || !current_user_can('read')) {
|
||
wp_send_json_error(['message' => __('无权执行此操作', 'jio')], 403);
|
||
}
|
||
$result = OAuthAccountService::unbind($current_id, $provider);
|
||
if (is_wp_error($result)) {
|
||
$status = $result->get_error_code() === 'oauth_last_login_method' ? 409 : 400;
|
||
wp_send_json_error(['message' => $result->get_error_message()], $status);
|
||
}
|
||
wp_send_json_success(['unbound' => true, 'message' => __('解除绑定成功', 'jio')]);
|
||
}
|
||
|
||
private function handle_start(): void {
|
||
$provider = sanitize_key((string) get_query_var('jio_oauth_provider'));
|
||
$action = isset($_GET['action']) && sanitize_key((string) $_GET['action']) === 'bind' ? 'bind' : 'login';
|
||
if (!OAuthProviderCatalog::master_enabled() || !in_array($provider, OAuthProviderCatalog::enabled_login_keys(), true)) {
|
||
$this->redirect_error('provider_disabled', $provider, $action);
|
||
}
|
||
$user_id = get_current_user_id();
|
||
if ($action === 'bind' && $user_id <= 0) {
|
||
wp_safe_redirect(function_exists('jio_get_login_url') ? jio_get_login_url() : wp_login_url());
|
||
exit;
|
||
}
|
||
if ($action === 'bind') {
|
||
$bind_pass = sanitize_text_field((string) ($_GET['bind_pass'] ?? ''));
|
||
if (!self::validate_bind_pass($bind_pass, $provider, $user_id, true)) {
|
||
$this->redirect_error('bind_verification_required', $provider, $action);
|
||
}
|
||
}
|
||
if (OAuthProviderCatalog::proxy_mode() === 'client') {
|
||
$url = OAuthProxyClient::get_instance()->get_proxy_auth_url($provider, $action);
|
||
if ($url === '') {
|
||
$this->redirect_error('proxy_config', $provider, $action);
|
||
}
|
||
wp_redirect($url); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
|
||
exit;
|
||
}
|
||
$redirect = isset($_GET['redirect_to']) ? OAuthUrlPolicy::validated_redirect((string) $_GET['redirect_to'], home_url('/')) : home_url('/');
|
||
$redirect_uri = home_url('/oauth/callback/');
|
||
try {
|
||
$state = OAuthTransactionStore::issue('direct', [
|
||
'provider' => $provider,
|
||
'action' => $action,
|
||
'user_id' => $user_id,
|
||
'redirect_to' => $redirect,
|
||
'redirect_uri' => $redirect_uri,
|
||
], 10 * MINUTE_IN_SECONDS);
|
||
wp_redirect($this->broker->auth_url($provider, $redirect_uri, $state)); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
|
||
exit;
|
||
} catch (\Throwable $e) {
|
||
$this->redirect_error('provider_unavailable', $provider, $action);
|
||
}
|
||
}
|
||
|
||
private function handle_callback(): void {
|
||
// Apple 使用 response_mode=form_post,state/code 走 POST 而不是 query。
|
||
$input = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET;
|
||
$state = sanitize_text_field((string) ($input['state'] ?? ''));
|
||
$row = $state !== '' ? OAuthTransactionStore::claim($state, 'direct') : null;
|
||
$payload = is_array($row['payload'] ?? null) ? $row['payload'] : [];
|
||
$provider = sanitize_key((string) ($payload['provider'] ?? ''));
|
||
$action = (string) ($payload['action'] ?? 'login');
|
||
if (!$row || !OAuthProviderCatalog::is_direct($provider)) {
|
||
$this->redirect_error('state_invalid', $provider, $action);
|
||
}
|
||
if ($action === 'bind' && (int) ($payload['user_id'] ?? 0) !== get_current_user_id()) {
|
||
$this->redirect_error('state_user_mismatch', $provider, $action);
|
||
}
|
||
if (!empty($input['error'])) {
|
||
$this->redirect_error('provider_cancelled', $provider, $action);
|
||
}
|
||
$code = sanitize_text_field((string) ($input['code'] ?? ''));
|
||
if ($code === '') {
|
||
$this->redirect_error('code_missing', $provider, $action);
|
||
}
|
||
try {
|
||
$identity = $this->broker->identity($provider, $code, (string) $payload['redirect_uri'], $state);
|
||
if (empty($identity['provider_id'])) {
|
||
throw new \RuntimeException('missing_provider_id');
|
||
}
|
||
if ($action === 'bind') {
|
||
$result = OAuthAccountService::bind(get_current_user_id(), $provider, $identity);
|
||
if (is_wp_error($result)) {
|
||
throw new \RuntimeException($result->get_error_code());
|
||
}
|
||
$this->redirect_success('bind', $provider, function_exists('jio_get_user_center_url') ? jio_get_user_center_url('settings') : home_url('/'));
|
||
}
|
||
$this->complete_identity_login($provider, $identity, (string) ($payload['redirect_to'] ?? home_url('/')));
|
||
} catch (\Throwable $e) {
|
||
$this->redirect_error($e->getMessage() === 'oauth_binding_conflict' ? 'binding_conflict' : 'callback_failed', $provider, $action);
|
||
}
|
||
}
|
||
|
||
private function handle_clogin_start(): void {
|
||
$config = OAuthProviderCatalog::clogin_settings();
|
||
$type = sanitize_key((string) ($_GET['type'] ?? ''));
|
||
$action = sanitize_key((string) ($_GET['action'] ?? '')) === 'bind' ? 'bind' : 'login';
|
||
if (!$config['enabled'] || !in_array($type, $config['types'], true)) {
|
||
$this->redirect_error('provider_disabled', 'clogin_' . $type, $action);
|
||
}
|
||
if ($action === 'bind' && !is_user_logged_in()) {
|
||
wp_safe_redirect(function_exists('jio_get_login_url') ? jio_get_login_url() : wp_login_url());
|
||
exit;
|
||
}
|
||
if ($action === 'bind') {
|
||
$bind_pass = sanitize_text_field((string) ($_GET['bind_pass'] ?? ''));
|
||
if (!self::validate_bind_pass($bind_pass, 'clogin_' . $type, get_current_user_id(), true)) {
|
||
$this->redirect_error('bind_verification_required', 'clogin_' . $type, $action);
|
||
}
|
||
}
|
||
$redirect = isset($_GET['redirect_to']) ? OAuthUrlPolicy::validated_redirect((string) $_GET['redirect_to'], home_url('/')) : home_url('/');
|
||
try {
|
||
$state = OAuthTransactionStore::issue('clogin', [
|
||
'type' => $type,
|
||
'action' => $action,
|
||
'user_id' => get_current_user_id(),
|
||
'redirect_to' => $redirect,
|
||
], 10 * MINUTE_IN_SECONDS);
|
||
$client = new CLoginClient([
|
||
'apiurl' => $config['url'],
|
||
'appid' => $config['id'],
|
||
'appkey' => $config['key'],
|
||
'callback' => home_url('/oauth/clogin/callback/'),
|
||
]);
|
||
$response = $client->login($type, $state);
|
||
if (!empty($response['code']) || empty($response['url'])) {
|
||
throw new \RuntimeException('clogin_unavailable');
|
||
}
|
||
wp_redirect((string) $response['url']); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
|
||
exit;
|
||
} catch (\Throwable $e) {
|
||
$this->redirect_error('provider_unavailable', 'clogin_' . $type, $action);
|
||
}
|
||
}
|
||
|
||
private function handle_clogin_callback(): void {
|
||
$state = sanitize_text_field((string) ($_GET['state'] ?? ''));
|
||
$row = $state !== '' ? OAuthTransactionStore::claim($state, 'clogin') : null;
|
||
$payload = is_array($row['payload'] ?? null) ? $row['payload'] : [];
|
||
$type = sanitize_key((string) ($payload['type'] ?? ''));
|
||
$provider = 'clogin_' . $type;
|
||
$action = (string) ($payload['action'] ?? 'login');
|
||
if (!$row || !in_array($type, OAuthProviderCatalog::clogin_settings()['types'], true)) {
|
||
$this->redirect_error('state_invalid', $provider, $action);
|
||
}
|
||
if ($action === 'bind' && (int) ($payload['user_id'] ?? 0) !== get_current_user_id()) {
|
||
$this->redirect_error('state_user_mismatch', $provider, $action);
|
||
}
|
||
$code = sanitize_text_field((string) ($_GET['code'] ?? ''));
|
||
if ($code === '') {
|
||
$this->redirect_error('code_missing', $provider, $action);
|
||
}
|
||
$config = OAuthProviderCatalog::clogin_settings();
|
||
$client = new CLoginClient(['apiurl' => $config['url'], 'appid' => $config['id'], 'appkey' => $config['key']]);
|
||
$info = $client->callback($code);
|
||
$returned_type = $client->from_api_type((string) ($info['type'] ?? ''));
|
||
if ((isset($info['code']) && (int) $info['code'] !== 0) || $returned_type !== $type || empty($info['social_uid'])) {
|
||
$this->redirect_error('callback_failed', $provider, $action);
|
||
}
|
||
$identity = [
|
||
'provider' => $provider,
|
||
'provider_id' => (string) $info['social_uid'],
|
||
'username' => sanitize_text_field((string) ($info['nickname'] ?? '')),
|
||
'name' => sanitize_text_field((string) ($info['nickname'] ?? '')),
|
||
'email' => '',
|
||
'email_verified' => false,
|
||
'avatar_url' => esc_url_raw((string) ($info['faceimg'] ?? '')),
|
||
];
|
||
if ($action === 'bind') {
|
||
$result = OAuthAccountService::bind(get_current_user_id(), $provider, $identity);
|
||
if (is_wp_error($result)) {
|
||
$this->redirect_error('binding_conflict', $provider, $action);
|
||
}
|
||
$this->redirect_success('bind', $provider, function_exists('jio_get_user_center_url') ? jio_get_user_center_url('settings') : home_url('/'));
|
||
}
|
||
$this->complete_identity_login($provider, $identity, (string) ($payload['redirect_to'] ?? home_url('/')));
|
||
}
|
||
|
||
private function complete_identity_login(string $provider, array $identity, string $redirect): void {
|
||
$user = OAuthAccountService::resolve($provider, $identity);
|
||
if (is_wp_error($user) && $user->get_error_code() === 'oauth_no_match') {
|
||
$obk = OAuthTransactionStore::issue('obk', [
|
||
'provider' => $provider,
|
||
'provider_id' => (string) ($identity['provider_id'] ?? ''),
|
||
'user_data' => $identity,
|
||
'redirect_to' => OAuthUrlPolicy::validated_redirect($redirect, home_url('/')),
|
||
], 15 * MINUTE_IN_SECONDS);
|
||
$url = function_exists('jio_get_bind_url') ? jio_get_bind_url($obk) : home_url('/sign/?tab=bind&obk=' . rawurlencode($obk));
|
||
wp_safe_redirect($url);
|
||
exit;
|
||
}
|
||
if (!$user instanceof \WP_User) {
|
||
$this->redirect_error('account_match_failed', $provider, 'login');
|
||
}
|
||
OAuthAccountService::complete_login($user, true);
|
||
$this->redirect_success('login', $provider, OAuthUrlPolicy::validated_redirect($redirect, home_url('/')));
|
||
}
|
||
|
||
private function binding_row(string $provider, bool $bound): array {
|
||
return [
|
||
'key' => $provider,
|
||
'name' => OAuthProviderCatalog::label($provider),
|
||
'icon' => function_exists('jio_brand_icon') ? jio_brand_icon(OAuthProviderCatalog::icon_key($provider)) : '',
|
||
'is_bound' => $bound,
|
||
'url' => '',
|
||
];
|
||
}
|
||
|
||
private function redirect_success(string $action, string $provider, string $target): void {
|
||
wp_safe_redirect(add_query_arg([
|
||
'oauth_action' => $action,
|
||
'oauth_provider' => $provider,
|
||
], $target));
|
||
exit;
|
||
}
|
||
|
||
private function redirect_error(string $code, string $provider = '', string $action = 'login'): void {
|
||
$target = $action === 'bind' && is_user_logged_in() && function_exists('jio_get_user_center_url')
|
||
? jio_get_user_center_url('settings')
|
||
: (function_exists('jio_get_login_url') ? jio_get_login_url() : wp_login_url());
|
||
wp_safe_redirect(add_query_arg([
|
||
'oauth_action' => 'error',
|
||
'oauth_provider' => sanitize_key($provider),
|
||
'oauth_error' => sanitize_key($code),
|
||
], $target));
|
||
exit;
|
||
}
|
||
}
|