diff --git a/assets/admin/src/pages/SiteSettings.vue b/assets/admin/src/pages/SiteSettings.vue index 5dc1a724..3b53488b 100644 --- a/assets/admin/src/pages/SiteSettings.vue +++ b/assets/admin/src/pages/SiteSettings.vue @@ -85,22 +85,6 @@ - - - - - Gravatar 头像源 - - - - - - 国内站点建议优先选 Cravatar。 - - - - - @@ -140,7 +124,6 @@ const saving = ref(false) const basic = reactive({ comment_enabled: true, comment_requires_registration: false, - avatar_source: 'cn.cravatar.com', icp_number: '', police_number: '', lazyload_enabled: true, @@ -149,12 +132,6 @@ const basic = reactive({ comment_pagination_mode: 'numbered', }) -const avatarSourceOptions = [ - { label: 'Cravatar (国内推荐)', value: 'cn.cravatar.com' }, - { label: 'Gravatar 国内镜像', value: 'cn.gravatar.com' }, - { label: 'Gravatar 官方', value: 'gravatar.com' }, -] - async function loadAll() { loading.value = true try { @@ -163,7 +140,6 @@ async function loadAll() { const d = basicRes.data basic.comment_enabled = d.comment_enabled !== false basic.comment_requires_registration = !!d.comment_requires_registration - basic.avatar_source = d.avatar_source || 'cn.cravatar.com' basic.icp_number = d.icp_number || '' basic.police_number = d.police_number || '' basic.lazyload_enabled = d.lazyload_enabled !== false @@ -184,7 +160,6 @@ async function handleSave() { const basicPayload = { comment_enabled: basic.comment_enabled, comment_requires_registration: basic.comment_requires_registration, - avatar_source: basic.avatar_source, icp_number: basic.icp_number, police_number: basic.police_number, lazyload_enabled: basic.lazyload_enabled, diff --git a/assets/admin/src/pages/permissions/AttachmentPermissions.vue b/assets/admin/src/pages/permissions/AttachmentPermissions.vue index 5f55cce3..2867dc52 100644 --- a/assets/admin/src/pages/permissions/AttachmentPermissions.vue +++ b/assets/admin/src/pages/permissions/AttachmentPermissions.vue @@ -40,7 +40,7 @@ - 关闭后用户只能使用 Gravatar/Cravatar 自动头像。 + 关闭后用户使用项目内置默认头像,且不能自行更换。 diff --git a/assets/admin/src/pages/users/UsersFeatures.vue b/assets/admin/src/pages/users/UsersFeatures.vue index c7c71592..1b256bc7 100644 --- a/assets/admin/src/pages/users/UsersFeatures.vue +++ b/assets/admin/src/pages/users/UsersFeatures.vue @@ -65,13 +65,6 @@ 公开资料编辑位 - - - - 开启后,用户中心会显示跳转到 WordPress 原生资料页的头像修改入口。 - - - @@ -121,7 +114,6 @@ const homeSummary = reactive({ const form = reactive({ dynamicTabs: ['posts', 'comments', 'favorites'], - user_avatar_upload: true, user_cover_upload: true, user_social_links: true, }) @@ -160,7 +152,6 @@ async function load() { const permissionData = extractData(permissionRes) form.dynamicTabs = parseTabs(featureData.user_center_tabs || 'profile,posts,comments,favorites') - form.user_avatar_upload = featureData.user_avatar_upload !== false form.user_cover_upload = featureData.user_cover_upload !== false form.user_social_links = featureData.user_social_links !== false @@ -179,7 +170,6 @@ async function handleSave() { try { await api.saveSchemaPage('users', 'features', { user_center_tabs: ['profile', ...form.dynamicTabs].join(','), - user_avatar_upload: form.user_avatar_upload, user_cover_upload: form.user_cover_upload, user_social_links: form.user_social_links, }) diff --git a/assets/admin/src/schema/sections/global/common.js b/assets/admin/src/schema/sections/global/common.js index 74cd6123..6f9e9390 100644 --- a/assets/admin/src/schema/sections/global/common.js +++ b/assets/admin/src/schema/sections/global/common.js @@ -2,7 +2,7 @@ export const commonSchema = { key: 'global.common', title: '常用功能', icon: 'fas fa-cog', - description: '管理站点常用开关、懒加载、头像源与备案信息等基础配置。', + description: '管理站点常用开关、懒加载与备案信息等基础配置。', defaults: { comment_enabled: true, lazyload_enabled: true, @@ -17,7 +17,6 @@ export const commonSchema = { lazyload_cover: true, lazyload_other: true, lazyload_widget: true, - avatar_source: 'cn.cravatar.com', icp_filing: '', footer_text: '', }, @@ -67,25 +66,6 @@ export const commonSchema = { { key: 'lazyload_widget', label: '小工具图片懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } }, ], }, - { - key: 'avatar', - title: '头像服务', - icon: 'fas fa-user-circle', - fields: [ - { - key: 'avatar_source', - label: '头像源', - type: 'select', - default: 'cn.cravatar.com', - options: [ - { label: 'Cravatar (推荐)', value: 'cn.cravatar.com' }, - { label: 'Gravatar 中国', value: 'cn.gravatar.com' }, - { label: 'Gravatar 国际', value: 'gravatar.com' }, - ], - help: 'Cravatar 是国内最快的头像服务,推荐使用', - }, - ], - }, { key: 'filing', title: '备案信息', diff --git a/assets/admin/src/schema/sections/users/features.js b/assets/admin/src/schema/sections/users/features.js index 35f39740..84b663ad 100644 --- a/assets/admin/src/schema/sections/users/features.js +++ b/assets/admin/src/schema/sections/users/features.js @@ -5,14 +5,12 @@ export const featuresSchema = { description: '配置用户中心和个人主页的功能模块。', defaults: { user_center_tabs: 'profile,posts,comments,favorites,vip,credits,auth', - user_avatar_upload: true, user_cover_upload: true, user_social_links: true, user_desc_std: '这家伙很懒…', user_city_s: true, user_city_type: 'province', user_join_day_my_name: '本站', - user_avatar_default: '', user_cover_img: '', user_rewrite_slug: 'user', user_follow_s: true, @@ -27,7 +25,6 @@ export const featuresSchema = { icon: 'fas fa-user-shield', fields: [ { key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,posts,comments,favorites,vip,credits,auth', help: '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入' }, - { key: 'user_avatar_upload', label: '头像上传', type: 'switch', default: true }, { key: 'user_cover_upload', label: '封面上传', type: 'switch', default: true }, { key: 'user_social_links', label: '社交链接', type: 'switch', default: true }, { key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' }, @@ -45,7 +42,6 @@ export const featuresSchema = { dependsOn: { key: 'user_city_s', value: true }, }, { key: 'user_join_day_my_name', label: '站名', type: 'input', default: '本站', help: '用于「已加入 X 天」显示中的站点名称' }, - { key: 'user_avatar_default', label: '默认头像', type: 'media', default: '' }, { key: 'user_cover_img', label: '默认封面', type: 'media', default: '' }, { key: 'user_rewrite_slug', label: '用户页 slug', type: 'input', default: 'user' }, { key: 'user_follow_s', label: '启用关注功能', type: 'switch', default: true }, diff --git a/assets/img/user/default avatar.svg b/assets/img/user/default avatar.svg new file mode 100644 index 00000000..43ec19ec --- /dev/null +++ b/assets/img/user/default avatar.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/js/author.js b/assets/js/author.js index f42a30de..338b9d25 100644 --- a/assets/js/author.js +++ b/assets/js/author.js @@ -206,6 +206,7 @@ var button = document.getElementById(upload.buttonId || 'aksrt-upload-avatar-btn'); var input = document.getElementById(upload.inputId || 'aksrt-avatar-file-input'); var image = document.getElementById(upload.imageId || 'aksrt-user-avatar-image'); + var removeButton = document.getElementById(upload.removeButtonId || 'aksrt-remove-avatar-btn'); if (!button || !input || button.dataset.aksrtAvatarBound === '1') return; button.dataset.aksrtAvatarBound = '1'; @@ -246,6 +247,7 @@ var url = payload.url || (payload.data && payload.data.url) || ''; if (!url) throw payload; if (image) image.src = url; + if (removeButton) removeButton.hidden = false; notify(payload.message || upload.successText || '头像已更新', 'success'); }) .catch(function (error) { @@ -256,6 +258,40 @@ input.value = ''; }); }); + + if (removeButton) { + var removeLabel = removeButton.querySelector('span'); + var removeIdleText = upload.removeText || (removeLabel ? removeLabel.textContent : '恢复默认头像'); + removeButton.addEventListener('click', function () { + if (!window.confirm(upload.confirmRemoveText || '确定恢复为默认头像?')) return; + removeButton.disabled = true; + if (removeLabel) removeLabel.textContent = upload.removingText || '恢复中…'; + + fetch(upload.restUrl, { + method: 'DELETE', + credentials: 'same-origin', + headers: { 'X-WP-Nonce': upload.restNonce || '' } + }) + .then(function (response) { + return response.json().then(function (payload) { + if (!response.ok || !payload || !payload.success) throw payload || {}; + return payload; + }); + }) + .then(function (payload) { + if (image && payload.url) image.src = payload.url; + removeButton.hidden = true; + notify(payload.message || upload.removeSuccessText || '已恢复默认头像', 'success'); + }) + .catch(function (error) { + notify((error && error.message) || upload.errorText || '头像操作失败', 'error'); + }) + .finally(function () { + removeButton.disabled = false; + if (removeLabel) removeLabel.textContent = removeIdleText; + }); + }); + } } function init() { diff --git a/inc/core-hooks/part-1.php b/inc/core-hooks/part-1.php index 70d3a44d..98dfa457 100644 --- a/inc/core-hooks/part-1.php +++ b/inc/core-hooks/part-1.php @@ -3,7 +3,6 @@ * AKSRT Theme Core Hooks * * 从 functions.php 中抽离的核心功能钩子: - * - Gravatar 头像源替换(4层拦截) * - SMTP 邮件配置注入 * - 评论开关控制 * - Banner CPT 注册 diff --git a/inc/core-hooks/part-2.php b/inc/core-hooks/part-2.php index f86f6bc4..6822138c 100644 --- a/inc/core-hooks/part-2.php +++ b/inc/core-hooks/part-2.php @@ -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 邮件配置注入 // ============================================================ diff --git a/inc/helpers/aksrt-avatar.php b/inc/helpers/aksrt-avatar.php index d2a25b9f..3da78046 100644 --- a/inc/helpers/aksrt-avatar.php +++ b/inc/helpers/aksrt-avatar.php @@ -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; + } + ?> + + + + + + ID, 96, ['loading' => 'eager']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + + ID, 96) !== '') : ?> + + + + ID, 'aksrt_admin_avatar_nonce'); ?> + + + + $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); + } +} diff --git a/inc/helpers/aksrt-user-center.php b/inc/helpers/aksrt-user-center.php index 41d2962d..98d69595 100644 --- a/inc/helpers/aksrt-user-center.php +++ b/inc/helpers/aksrt-user-center.php @@ -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'), ]; } } diff --git a/inc/helpers/user-center-renderers/profile-header.php b/inc/helpers/user-center-renderers/profile-header.php index f006d1ef..a3dbc015 100644 --- a/inc/helpers/user-center-renderers/profile-header.php +++ b/inc/helpers/user-center-renderers/profile-header.php @@ -94,7 +94,10 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_ - + + > + + diff --git a/src/AKSRT/Admin/ImportExportController.php b/src/AKSRT/Admin/ImportExportController.php index e138a7c5..9b9c0212 100644 --- a/src/AKSRT/Admin/ImportExportController.php +++ b/src/AKSRT/Admin/ImportExportController.php @@ -371,14 +371,6 @@ class ImportExportController extends ControllerBase { 'status' => $backfill_done ? 'ok' : 'info', ]; - // Avatar source - $avatar_source = \AKSRT\Settings\Settings::get_all()['avatar_source'] ?? 'cn.cravatar.com'; - $checks['avatar_source'] = [ - 'label' => '头像服务', - 'value' => $avatar_source, - 'status' => $avatar_source === 'cn.cravatar.com' ? 'ok' : 'info', - ]; - // Option count $option_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'aksrt_%'" diff --git a/src/AKSRT/Admin/Schema/GlobalSchema.php b/src/AKSRT/Admin/Schema/GlobalSchema.php index 97297aa2..cef6031c 100644 --- a/src/AKSRT/Admin/Schema/GlobalSchema.php +++ b/src/AKSRT/Admin/Schema/GlobalSchema.php @@ -26,13 +26,6 @@ class GlobalSchema { ['key' => 'performance', 'title' => '性能优化', 'fields' => [ ['key' => 'lazyload_enabled', 'label' => '图片懒加载', 'type' => 'switch', 'default' => true], ]], - ['key' => 'avatar', 'title' => '头像服务', 'fields' => [ - ['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [ - ['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'], - ['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'], - ['label' => 'Gravatar 官方', 'value' => 'gravatar.com'], - ]], - ]], ['key' => 'filing', 'title' => '备案信息', 'fields' => [ ['key' => 'icp_filing', 'label' => 'ICP 备案号', 'type' => 'input', 'default' => ''], ['key' => 'footer_text', 'label' => '页脚附加文字', 'type' => 'input', 'default' => ''], @@ -203,13 +196,6 @@ class GlobalSchema { ['label' => '样式 10', 'value' => 'no10'], ]], ]], - ['key' => 'avatar', 'title' => '头像设置', 'fields' => [ - ['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [ - ['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'], - ['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'], - ['label' => 'Gravatar 官方', 'value' => 'gravatar.com'], - ], 'help' => '国内站点建议优先选 Cravatar'], - ]], ], ], 'code-highlight' => [ diff --git a/src/AKSRT/Admin/Schema/SchemaUsers.php b/src/AKSRT/Admin/Schema/SchemaUsers.php index 2029db1f..d41e4097 100644 --- a/src/AKSRT/Admin/Schema/SchemaUsers.php +++ b/src/AKSRT/Admin/Schema/SchemaUsers.php @@ -516,7 +516,6 @@ class SchemaUsers { 'groups' => [ ['key' => 'center', 'title' => '用户中心', 'fields' => [ ['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,posts,comments,favorites,vip,credits,auth', 'help' => '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入'], - ['key' => 'user_avatar_upload', 'label' => '头像上传', 'type' => 'switch', 'default' => true], ['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true], ['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true], ['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'], @@ -527,7 +526,6 @@ class SchemaUsers { ['label' => '国家+省份', 'value' => 'country_province'], ]], ['key' => 'user_join_day_my_name', 'label' => '站名', 'type' => 'input', 'default' => '本站', 'desc' => '用于「已加入 X 天」显示中的站点名称'], - ['key' => 'user_avatar_default', 'label' => '默认头像', 'type' => 'media', 'default' => ''], ['key' => 'user_cover_img', 'label' => '默认封面', 'type' => 'media', 'default' => ''], ['key' => 'user_rewrite_slug', 'label' => '用户页slug', 'type' => 'input', 'default' => 'user'], ['key' => 'user_follow_s', 'label' => '启用关注功能', 'type' => 'switch', 'default' => true], diff --git a/src/AKSRT/Admin/UserController.php b/src/AKSRT/Admin/UserController.php index b35fbd82..06201886 100644 --- a/src/AKSRT/Admin/UserController.php +++ b/src/AKSRT/Admin/UserController.php @@ -52,9 +52,16 @@ class UserController extends ControllerBase { 'permission_callback' => function () { return is_user_logged_in(); }, ]); register_rest_route(self::API_NAMESPACE, '/user/avatar', [ - 'methods' => 'POST', - 'callback' => [$this, 'rest_upload_user_avatar'], - 'permission_callback' => function () { return is_user_logged_in(); }, + [ + 'methods' => 'POST', + 'callback' => [$this, 'rest_upload_user_avatar'], + 'permission_callback' => function () { return is_user_logged_in(); }, + ], + [ + 'methods' => 'DELETE', + 'callback' => [$this, 'rest_delete_user_avatar'], + 'permission_callback' => function () { return is_user_logged_in(); }, + ], ]); // Comment inline image upload (logged-in users) diff --git a/src/AKSRT/Admin/UserControllerProfileMediaOperations.php b/src/AKSRT/Admin/UserControllerProfileMediaOperations.php index 446c2f4a..db96ca5d 100644 --- a/src/AKSRT/Admin/UserControllerProfileMediaOperations.php +++ b/src/AKSRT/Admin/UserControllerProfileMediaOperations.php @@ -19,6 +19,23 @@ trait UserControllerProfileMediaOperations { ); } + public function rest_delete_user_avatar(\WP_REST_Request $request): \WP_REST_Response { + $user_id = get_current_user_id(); + if ($user_id <= 0) { + return $this->error(__('请先登录', 'aksrt'), 401); + } + + $attachment_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true); + delete_user_meta($user_id, 'aksrt_avatar_attachment_id'); + $this->delete_owned_profile_attachment($attachment_id, $user_id); + do_action('aksrt_user_avatar_changed', $user_id, 0, ''); + + return $this->success([ + 'message' => __('已恢复默认头像', 'aksrt'), + 'url' => function_exists('aksrt_get_default_avatar_url') ? aksrt_get_default_avatar_url() : '', + ]); + } + public function rest_upload_user_background(\WP_REST_Request $request): \WP_REST_Response { return $this->upload_profile_image( ['background', 'file'], @@ -58,15 +75,39 @@ trait UserControllerProfileMediaOperations { return $this->error(__('请选择要上传的图片', 'aksrt'), 400); } - $file = $_FILES[$file_key]; - if (!empty($file['size']) && (int) $file['size'] > 2 * MB_IN_BYTES) { - return $this->error(__('图片大小不能超过 2MB', 'aksrt'), 413); - } - 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_key]; + $upload_error = isset($file['error']) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE; + if ($upload_error !== UPLOAD_ERR_OK) { + return $this->error(__('头像文件上传失败,请重新选择', 'aksrt'), 400); + } + + $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; + $max_size_mb = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2))); + $max_size = $max_size_mb * MB_IN_BYTES; + $actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0; + if ($actual_size <= 0 || $actual_size > $max_size) { + return $this->error(sprintf(__('图片大小必须在 1 字节至 %dMB 之间', 'aksrt'), $max_size_mb), 413); + } + + $checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], [ + 'jpg|jpeg|jpe' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + ]); + if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type'])) { + return $this->error(__('仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt'), 415); + } + + $image_size = wp_getimagesize($file['tmp_name']); + if (!$image_size || empty($image_size[0]) || empty($image_size[1]) || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) { + return $this->error(__('图片无效或尺寸过大', 'aksrt'), 415); + } + $attachment_id = media_handle_upload($file_key, 0, [], [ 'test_form' => false, 'mimes' => [ @@ -81,6 +122,14 @@ trait UserControllerProfileMediaOperations { } $attachment_id = (int) $attachment_id; + wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]); + if (!wp_attachment_is_image($attachment_id)) { + wp_delete_attachment($attachment_id, true); + return $this->error(__('上传文件不是有效图片', 'aksrt'), 415); + } + update_post_meta($attachment_id, '_aksrt_profile_media_user_id', $user_id); + update_post_meta($attachment_id, '_aksrt_profile_media_type', $is_avatar ? 'avatar' : 'background'); + $url = wp_get_attachment_image_url($attachment_id, $is_avatar ? 'thumbnail' : 'full'); if (!$url) { wp_delete_attachment($attachment_id, true); @@ -94,10 +143,11 @@ trait UserControllerProfileMediaOperations { } if ($previous_id > 0 && $previous_id !== $attachment_id) { - $previous = get_post($previous_id); - if ($previous instanceof \WP_Post && (int) $previous->post_author === $user_id) { - wp_delete_attachment($previous_id, true); - } + $this->delete_owned_profile_attachment($previous_id, $user_id); + } + + if ($is_avatar) { + do_action('aksrt_user_avatar_changed', $user_id, $attachment_id, $url); } return $this->success([ @@ -109,4 +159,22 @@ trait UserControllerProfileMediaOperations { ], ]); } + + private function delete_owned_profile_attachment(int $attachment_id, int $user_id): bool { + if ($attachment_id <= 0) { + return false; + } + + $attachment = get_post($attachment_id); + if (!$attachment instanceof \WP_Post || $attachment->post_type !== 'attachment' || (int) $attachment->post_author !== $user_id) { + return false; + } + + $managed_user_id = (int) get_post_meta($attachment_id, '_aksrt_profile_media_user_id', true); + if ($managed_user_id > 0 && $managed_user_id !== $user_id) { + return false; + } + + return (bool) wp_delete_attachment($attachment_id, true); + } } diff --git a/src/AKSRT/Settings/DisplayPageGroups.php b/src/AKSRT/Settings/DisplayPageGroups.php index 97ee381b..578433b3 100644 --- a/src/AKSRT/Settings/DisplayPageGroups.php +++ b/src/AKSRT/Settings/DisplayPageGroups.php @@ -98,7 +98,6 @@ trait DisplayPageGroups { 'logo_text' => self::logo_text(), 'logo_width' => self::logo_width(), 'site_favicon' => self::favicon_url(), - 'avatar_source' => self::avatar_source(), 'icp_filing' => self::icp_filing(), 'footer_text' => self::footer_text(), 'theme_mode' => self::theme_mode(), diff --git a/src/AKSRT/Settings/FacadeBulk.php b/src/AKSRT/Settings/FacadeBulk.php index a5c76f90..214bee42 100644 --- a/src/AKSRT/Settings/FacadeBulk.php +++ b/src/AKSRT/Settings/FacadeBulk.php @@ -502,15 +502,6 @@ trait FacadeBulk { - /** 获取头像镜像源 */ - public static function avatar_source(): string { - $source = sanitize_text_field((string) self::get_value('avatar_source', 'cn.cravatar.com')); - $allowed = ['cn.cravatar.com', 'cn.gravatar.com', 'gravatar.com']; - return in_array($source, $allowed, true) ? $source : 'cn.cravatar.com'; - } - - - /** 获取SEO关键词 */ public static function seo_keywords(): string { $s = self::get_all(); diff --git a/src/AKSRT/Settings/UserRuntime.php b/src/AKSRT/Settings/UserRuntime.php index 06bd7a9d..ac3fc77b 100644 --- a/src/AKSRT/Settings/UserRuntime.php +++ b/src/AKSRT/Settings/UserRuntime.php @@ -132,7 +132,7 @@ class UserRuntime { return [ 'tabs' => $normalized_tabs, - 'avatar_upload' => !isset($settings['user_avatar_upload']) || !empty($settings['user_avatar_upload']), + 'avatar_upload' => !isset($settings['allow_avatar_upload']) || !empty($settings['allow_avatar_upload']), 'cover_upload' => !isset($settings['user_cover_upload']) || !empty($settings['user_cover_upload']), 'social_links' => !isset($settings['user_social_links']) || !empty($settings['user_social_links']), ];