feat(user): add local avatar management flow

This commit is contained in:
2026-07-29 13:35:17 +08:00
parent 1b4ba8938a
commit e94437ef60
20 changed files with 289 additions and 186 deletions
-1
View File
@@ -3,7 +3,6 @@
* AKSRT Theme Core Hooks
*
* 从 functions.php 中抽离的核心功能钩子:
* - Gravatar 头像源替换(4层拦截)
* - SMTP 邮件配置注入
* - 评论开关控制
* - Banner CPT 注册
-50
View File
@@ -20,56 +20,6 @@ add_filter('show_admin_bar', '__return_false');
add_filter('pre_option_link_manager_enabled', '__return_true');
// ============================================================
// Gravatar 头像源替换(4层拦截:get_avatar_url / pre_get_avatar_data / avatar_defaults / ob_start 兜底)
// ============================================================
function aksrt_get_gravatar_source() {
if (class_exists('\AKSRT\Settings\Settings')) {
return \AKSRT\Settings\Settings::avatar_source();
}
$source = aksrt_get_setting('avatar_source', 'cn.cravatar.com');
$allowed = array('cn.cravatar.com', 'cn.gravatar.com', 'gravatar.com');
return in_array($source, $allowed, true) ? $source : 'cn.cravatar.com';
}
function aksrt_replace_gravatar_domain($url) {
$source = aksrt_get_gravatar_source();
if ($source === 'gravatar.com') return $url;
return preg_replace('#^https?://([0-9]+\.)?(secure\.)?gravatar\.com#i', 'https://' . $source, $url);
}
add_filter('get_avatar_url', function ($url) {
return aksrt_replace_gravatar_domain($url);
});
add_filter('pre_get_avatar_data', function ($args) {
if (!empty($args['url'])) {
$args['url'] = aksrt_replace_gravatar_domain($args['url']);
}
return $args;
}, 10, 1);
add_filter('avatar_defaults', function ($defaults) {
foreach ($defaults as &$url) {
$url = aksrt_replace_gravatar_domain($url);
}
unset($url);
return $defaults;
});
// 全局输出缓冲兜底(覆盖 JS/API 直接请求的 URL)
add_action('init', function () {
if (is_admin()) ob_start(function ($html) {
return preg_replace_callback(
'#https?://([0-9]+\.)?(secure\.)?gravatar\.com#i',
function () { return 'https://' . aksrt_get_gravatar_source(); },
$html
);
});
});
// ============================================================
// SMTP 邮件配置注入
// ============================================================
+133 -13
View File
@@ -37,9 +37,21 @@ if (!function_exists('aksrt_resolve_avatar_user_id')) {
}
}
if (!function_exists('aksrt_get_default_avatar_url')) {
/**
* Return the bundled avatar used when a user has no custom image.
*/
function aksrt_get_default_avatar_url(): string {
return (string) apply_filters(
'aksrt_default_avatar_url',
get_theme_file_uri('assets/img/user/default avatar.svg')
);
}
}
if (!function_exists('aksrt_get_custom_avatar_url')) {
/**
* Return the locally uploaded or OAuth avatar URL.
* Return the locally uploaded avatar URL.
*/
function aksrt_get_custom_avatar_url(int $user_id, int $size = 96): string {
if ($user_id <= 0) {
@@ -47,15 +59,17 @@ if (!function_exists('aksrt_get_custom_avatar_url')) {
}
$attachment_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
if ($attachment_id > 0) {
$image = wp_get_attachment_image_url($attachment_id, [$size, $size]);
if (is_string($image) && $image !== '') {
return $image;
}
if ($attachment_id <= 0) {
return '';
}
$oauth_avatar = esc_url_raw((string) get_user_meta($user_id, 'aksrt_oauth_avatar', true));
return $oauth_avatar;
$attachment = get_post($attachment_id);
if (!$attachment instanceof WP_Post || $attachment->post_type !== 'attachment' || (int) $attachment->post_author !== $user_id || !wp_attachment_is_image($attachment_id)) {
return '';
}
$image = wp_get_attachment_image_url($attachment_id, [$size, $size]);
return is_string($image) ? $image : '';
}
}
@@ -77,7 +91,7 @@ if (!function_exists('aksrt_get_avatar_url')) {
if ($custom !== '') {
return $custom;
}
return (string) get_avatar_url($subject, array_merge($args, ['size' => $size]));
return aksrt_get_default_avatar_url();
}
}
@@ -119,9 +133,115 @@ if (!function_exists('aksrt_get_avatar_html')) {
add_filter('get_avatar_data', static function (array $args, $subject): array {
$user_id = aksrt_resolve_avatar_user_id($subject);
$custom = aksrt_get_custom_avatar_url($user_id, (int) ($args['size'] ?? 96));
if ($custom !== '') {
$args['url'] = $custom;
$args['found_avatar'] = true;
}
$args['url'] = $custom !== '' ? $custom : aksrt_get_default_avatar_url();
$args['found_avatar'] = true;
return $args;
}, 20, 2);
add_action('show_user_profile', 'aksrt_render_admin_avatar_field');
add_action('edit_user_profile', 'aksrt_render_admin_avatar_field');
if (!function_exists('aksrt_render_admin_avatar_field')) {
/**
* Render local avatar controls on the native WordPress user profile screen.
*/
function aksrt_render_admin_avatar_field(WP_User $user): void {
if (!current_user_can('edit_user', $user->ID)) {
return;
}
?>
<h2><?php esc_html_e('AKSRT 头像', 'aksrt'); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e('当前头像', 'aksrt'); ?></th>
<td>
<?php echo aksrt_get_avatar_html($user->ID, 96, ['loading' => 'eager']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<p><input type="file" name="aksrt_avatar_file" accept="image/jpeg,image/png,image/webp,image/gif"></p>
<?php if (aksrt_get_custom_avatar_url((int) $user->ID, 96) !== '') : ?>
<label><input type="checkbox" name="aksrt_avatar_reset" value="1"> <?php esc_html_e('恢复项目默认头像并删除当前头像文件', 'aksrt'); ?></label>
<?php endif; ?>
<p class="description"><?php esc_html_e('支持 JPG、PNG、GIF、WebP;大小上限由“权限 → 附件权限”控制。', 'aksrt'); ?></p>
<?php wp_nonce_field('aksrt_save_admin_avatar_' . $user->ID, 'aksrt_admin_avatar_nonce'); ?>
</td>
</tr>
</table>
<?php
}
}
add_action('personal_options_update', 'aksrt_save_admin_avatar_field');
add_action('edit_user_profile_update', 'aksrt_save_admin_avatar_field');
if (!function_exists('aksrt_save_admin_avatar_field')) {
/**
* Save local avatar changes from the native WordPress profile screen.
*/
function aksrt_save_admin_avatar_field(int $user_id): void {
if (!current_user_can('edit_user', $user_id)) {
return;
}
$nonce = isset($_POST['aksrt_admin_avatar_nonce']) ? sanitize_text_field(wp_unslash($_POST['aksrt_admin_avatar_nonce'])) : '';
if (!wp_verify_nonce($nonce, 'aksrt_save_admin_avatar_' . $user_id)) {
return;
}
$previous_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
if (!empty($_POST['aksrt_avatar_reset'])) {
delete_user_meta($user_id, 'aksrt_avatar_attachment_id');
aksrt_delete_owned_avatar_attachment($previous_id, $user_id);
do_action('aksrt_user_avatar_changed', $user_id, 0, '');
return;
}
if (empty($_FILES['aksrt_avatar_file']['name'])) {
return;
}
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$max_size = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2))) * MB_IN_BYTES;
$file = $_FILES['aksrt_avatar_file'];
if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || (int) ($file['size'] ?? 0) <= 0 || (int) $file['size'] > $max_size) {
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$attachment_id = media_handle_upload('aksrt_avatar_file', 0, [], [
'test_form' => false,
'mimes' => [
'jpg|jpeg|jpe' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
],
]);
if (is_wp_error($attachment_id) || !wp_attachment_is_image((int) $attachment_id)) {
if (!is_wp_error($attachment_id)) {
wp_delete_attachment((int) $attachment_id, true);
}
return;
}
$attachment_id = (int) $attachment_id;
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
update_post_meta($attachment_id, '_aksrt_profile_media_user_id', $user_id);
update_post_meta($attachment_id, '_aksrt_profile_media_type', 'avatar');
update_user_meta($user_id, 'aksrt_avatar_attachment_id', $attachment_id);
aksrt_delete_owned_avatar_attachment($previous_id, $user_id);
do_action('aksrt_user_avatar_changed', $user_id, $attachment_id, (string) wp_get_attachment_image_url($attachment_id, 'thumbnail'));
}
}
if (!function_exists('aksrt_delete_owned_avatar_attachment')) {
/**
* Delete an avatar attachment only when it belongs to the target user.
*/
function aksrt_delete_owned_avatar_attachment(int $attachment_id, int $user_id): bool {
$attachment = $attachment_id > 0 ? get_post($attachment_id) : null;
if (!$attachment instanceof WP_Post || $attachment->post_type !== 'attachment' || (int) $attachment->post_author !== $user_id) {
return false;
}
return (bool) wp_delete_attachment($attachment_id, true);
}
}
+20 -13
View File
@@ -373,20 +373,27 @@ if (!function_exists('aksrt_user_center_avatar_upload_config')) {
if (!$enabled) {
return [];
}
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$max_size = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2))) * MB_IN_BYTES;
return [
'enabled' => true,
'restUrl' => rest_url('aksrt/v1/user/avatar'),
'restNonce' => wp_create_nonce('wp_rest'),
'buttonId' => 'aksrt-upload-avatar-btn',
'inputId' => 'aksrt-avatar-file-input',
'imageId' => 'aksrt-user-avatar-image',
'fieldName' => 'avatar',
'maxSize' => 2 * MB_IN_BYTES,
'idleText' => __('更换头像', 'aksrt'),
'loadingText' => __('上传中…', 'aksrt'),
'tooLargeText' => __('图片大小不能超过 2MB', 'aksrt'),
'successText' => __('头像已更新', 'aksrt'),
'errorText' => __('头像上传失败', 'aksrt'),
'enabled' => true,
'restUrl' => rest_url('aksrt/v1/user/avatar'),
'restNonce' => wp_create_nonce('wp_rest'),
'buttonId' => 'aksrt-upload-avatar-btn',
'removeButtonId' => 'aksrt-remove-avatar-btn',
'inputId' => 'aksrt-avatar-file-input',
'imageId' => 'aksrt-user-avatar-image',
'fieldName' => 'avatar',
'maxSize' => $max_size,
'idleText' => __('更换头像', 'aksrt'),
'loadingText' => __('上传中…', 'aksrt'),
'removeText' => __('恢复默认头像', 'aksrt'),
'removingText' => __('恢复中…', 'aksrt'),
'confirmRemoveText' => __('确定恢复为默认头像?当前上传的头像文件将被删除。', 'aksrt'),
'tooLargeText' => sprintf(__('图片大小不能超过 %dMB', 'aksrt'), (int) ($max_size / MB_IN_BYTES)),
'successText' => __('头像已更新', 'aksrt'),
'removeSuccessText' => __('已恢复默认头像', 'aksrt'),
'errorText' => __('头像操作失败', 'aksrt'),
];
}
}
@@ -94,7 +94,10 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
<button type="button" id="aksrt-upload-avatar-btn" class="btn-ghost btn-sm">
<i class="fas fa-camera" aria-hidden="true"></i> <span><?php esc_html_e('更换头像', 'aksrt'); ?></span>
</button>
<input type="file" id="aksrt-avatar-file-input" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
<input type="file" id="aksrt-avatar-file-input" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
<button type="button" id="aksrt-remove-avatar-btn" class="btn-ghost btn-sm"<?php echo get_user_meta($user_id, 'aksrt_avatar_attachment_id', true) ? '' : ' hidden'; ?>>
<i class="fas fa-undo" aria-hidden="true"></i> <span><?php esc_html_e('恢复默认头像', 'aksrt'); ?></span>
</button>
<?php endif; ?>
<a href="<?php echo esc_url(aksrt_get_user_home_url()); ?>" class="btn-ghost btn-sm">
<i class="fas fa-map-marker-alt" aria-hidden="true"></i> <?php esc_html_e('我的主页', 'aksrt'); ?>