fix(oauth): actually use the third-party avatar and seed it only once

The OAuth flow wrote the provider avatar to `jio_oauth_avatar`, but nothing
ever read it — every user who signed up through GitHub/QQ saw a Gravatar hash
or the default avatar instead of their own picture. The reference seeds its
`custom_avatar` from the OAuth profile and its resolver reads it back, so both
halves were missing here.

jio_get_custom_avatar_url() now falls back to the stored OAuth avatar when no
uploaded one exists, which puts the chain at uploaded → third-party → external
service → default. The theme already swaps WordPress's get_avatar_data, so one
change covers every avatar surface.

Writes go through a single seed_avatar() guard: it no-ops when an uploaded
avatar or an existing OAuth avatar is present, matching the reference's
`if ($args['avatar'] && !$custom_avatar)`. Previously both the bind path and
the login refresh overwrote the meta unconditionally, which would have wiped a
user's own picture on their next social login.

Because the OAuth avatar now shows through once the upload is gone, both
"restore default" flows (the REST endpoint and the admin profile box) delete
it too; the admin checkbox also appears when only an OAuth avatar is set,
since $avatar_url is now non-empty in that case.

Baidu serves avatars from tb.himg.baidu.com, which 403s — rewritten to
himg.bdimg.com the way the reference does. On top of that the URL is upgraded
to https when the site runs over SSL, because the reference's scheme-relative
form still yields a plain http:// when copied verbatim, and browsers block it
as mixed content.

WeChatShare was reading `custom_avatar` — a reference-theme key this codebase
never writes — so its author branch always fell through to get_avatar_url();
it now uses the shared resolver like every other surface.

Verification: drove the resolver through five states with a stub (none /
OAuth only / both / upload only / Baidu legacy host) and confirmed the
expected URL each time. Added oauth-avatar-contract.test.cjs to pin the
resolution order, the seed guard, and the reset cleanup. 868 widget and
navigation tests pass; the PSR-4 path audit reports 0 errors.
This commit is contained in:
kate522 committed 2026-11-04 17:41:48 +08:00
1 parent 7d78a33a9d
commit 6f29e3d865
6 files changed
+137 -16

No files matched your search

@@ -27,6 +27,8 @@ trait UserControllerProfileMediaOperations {
$attachment_id = (int) get_user_meta($user_id, 'jio_avatar_attachment_id', true);
delete_user_meta($user_id, 'jio_avatar_attachment_id');
// 第三方头像会在上传头像被清除后重新顶上来,恢复默认时必须一并丢弃。
delete_user_meta($user_id, 'jio_oauth_avatar');
$this->delete_owned_profile_attachment($attachment_id, $user_id);
do_action('jio_user_avatar_changed', $user_id, 0, '');
+21 -5
View File
@@ -71,9 +71,7 @@ final class OAuthAccountService {
$bindings[] = $provider;
}
update_user_meta($user_id, 'jio_oauth_bindings', array_values(array_unique($bindings)));
if (!empty($identity['avatar_url'])) {
update_user_meta($user_id, 'jio_oauth_avatar', esc_url_raw((string) $identity['avatar_url']));
}
self::seed_avatar($user_id, $identity);
return true;
} finally {
OAuthTransactionStore::release_lock($scope, (string) $lock);
@@ -114,9 +112,27 @@ final class OAuthAccountService {
$bindings[] = $provider;
update_user_meta($user_id, 'jio_oauth_bindings', array_values(array_unique($bindings)));
}
if (!empty($identity['avatar_url'])) {
update_user_meta($user_id, 'jio_oauth_avatar', esc_url_raw((string) $identity['avatar_url']));
self::seed_avatar($user_id, $identity);
}
/**
* 建档或首次绑定时以第三方头像补齐,之后不再覆盖。
*
* 参考主题只在用户没有自定义头像时写入(`if ($args['avatar'] && !$custom_avatar)`),
* 这样用户换过的头像不会被后续第三方登录冲掉。
*/
private static function seed_avatar(int $user_id, array $identity): void {
$avatar = trim((string) ($identity['avatar_url'] ?? ''));
if ($user_id <= 0 || $avatar === '') {
return;
}
if ((int) get_user_meta($user_id, 'jio_avatar_attachment_id', true) > 0) {
return;
}
if (trim((string) get_user_meta($user_id, 'jio_oauth_avatar', true)) !== '') {
return;
}
update_user_meta($user_id, 'jio_oauth_avatar', esc_url_raw($avatar));
}
private static function has_recovery_after_unbind(int $user_id, string $provider): bool {
+4 -4
View File
@@ -166,10 +166,10 @@ window.WeChatShareDate = <?php echo wp_json_encode($payload, JSON_UNESCAPED_UNIC
} elseif (is_author()) {
$author = get_queried_object();
if ($author && !empty($author->ID)) {
$pic = (string) get_user_meta((int) $author->ID, 'custom_avatar', true);
if ($pic === '') {
$pic = get_avatar_url((int) $author->ID, ['size' => 300]);
}
// 与本主题其余头像位一致:上传头像 → 第三方头像 → 外部服务。
$pic = function_exists('jio_get_avatar_url')
? (string) jio_get_avatar_url((int) $author->ID, ['size' => 300])
: get_avatar_url((int) $author->ID, ['size' => 300]);
}
} elseif (is_category() || is_tag() || is_tax()) {
$term = get_queried_object();
+5 -1
View File
@@ -33,6 +33,8 @@ final class ProfileAdmin {
? jio_get_user_profile_details($user_id)
: ['gender' => 'secret', 'website' => '', 'qq' => '', 'wechat' => '', 'weibo' => '', 'github' => '', 'address' => ''];
$avatar_url = function_exists('jio_get_custom_avatar_url') ? jio_get_custom_avatar_url($user_id, 96) : '';
$avatar_custom = (int) get_user_meta($user_id, 'jio_avatar_attachment_id', true) > 0
|| trim((string) get_user_meta($user_id, 'jio_oauth_avatar', true)) !== '';
$cover_id = (int) get_user_meta($user_id, 'jio_background_attachment_id', true);
$cover_url = esc_url_raw((string) get_user_meta($user_id, 'jio_background_image', true));
if ($cover_url === '' && $cover_id > 0) $cover_url = (string) wp_get_attachment_image_url($cover_id, 'full');
@@ -45,7 +47,7 @@ final class ProfileAdmin {
<td>
<?php echo get_avatar($user_id, 96, '', $user->display_name, ['loading' => 'eager']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<p><input id="jio_avatar_file" type="file" name="jio_avatar_file" accept="image/jpeg,image/png,image/webp,image/gif"></p>
<?php if ($avatar_url !== '') : ?><label><input type="checkbox" name="jio_avatar_reset" value="1"> <?php esc_html_e('恢复项目默认头像并删除当前头像文件', 'jio'); ?></label><?php endif; ?>
<?php if ($avatar_custom) : ?><label><input type="checkbox" name="jio_avatar_reset" value="1"> <?php esc_html_e('恢复项目默认头像并清除当前头像', 'jio'); ?></label><?php endif; ?>
<p class="description"><?php esc_html_e('支持 JPG、PNG、GIF、WebP;大小上限由“权限 → 附件权限”控制。', 'jio'); ?></p>
<?php wp_nonce_field('jio_save_admin_avatar_' . $user_id, 'jio_admin_avatar_nonce'); ?>
</td>
@@ -192,6 +194,8 @@ final class ProfileAdmin {
$previous_id = (int) get_user_meta($user_id, 'jio_avatar_attachment_id', true);
if (!empty($_POST['jio_avatar_reset'])) {
delete_user_meta($user_id, 'jio_avatar_attachment_id');
// 上传头像被清除后第三方头像会重新生效,恢复默认时一并丢弃。
delete_user_meta($user_id, 'jio_oauth_avatar');
if (function_exists('jio_delete_owned_avatar_attachment')) jio_delete_owned_avatar_attachment($previous_id, $user_id);
do_action('jio_user_avatar_changed', $user_id, 0, '');
return;