Files
wordpress-theme-aksrt/inc/helpers/aksrt-avatar.php
T
kate522 7f520803ff feat(ban): mirror reference theme ban/restriction prompt surface
修复公开主页禁封提示框为空的问题,并对齐参考主题(Zibll)全部前台禁封提示入口:
- fix(profile): 资料卡下方治理提示框渲染为空——模板读取的 tone/message 键与
  数据层实际输出的 state/label/text 契约不匹配;现按禁封类型展示
  「违规已封号/小黑屋禁封中/禁言中」+ 可配置说明文案
- feat(ban): 用户中心页头展示治理状态徽章 + 查看详情入口,直达禁封申诉页
  (参考主题 user_page_header_desc / zib_get_user_ban_info_link)
- feat(ban): 评论拒绝态区分禁封/小黑屋/禁言,附状态徽章与申诉链接
  (参考主题 zib_get_user_ban_nocan_info,原为笼统的无权限文案)
- feat(ban): 禁封申诉页补全状态信息卡(状态/开始时间/结束时间/原因/说明/
  申诉权限),对齐 zib_get_ban_info_modal;此前仅有「禁封原因」一行且无入口
- feat(ban): 昵称旁禁封徽章 + 头像禁封角标(覆盖 VIP 位,参考主题
  user_name_badge / user_avatar_badge 钩子语义),main.css 与入队的
  main.min.css 同步新增角标样式
- fix(ban): 举报入口对已封禁/小黑屋目标隐藏(评论、文章下拉、封面菜单三处,
  对齐 zib_get_report_link 排除语义;禁言为轻量限制不隐藏)
- refactor(ban): 治理展示态收敛为单一数据源 Restriction::get_display_state()
  + state_labels(),资料卡/用户中心/评论拒绝态/昵称徽章共用
- feat(ban): 小黑屋用户开放申诉(对齐参考主题 zib_user_is_ban 双类型语义),
  禁封站内信附加申诉页链接
- test: report-ban 契约测试 +3(展示态单一数据源、提示面入口矩阵、举报排除),
  45 个测试文件全部通过
2026-09-22 13:28:52 +08:00

458 lines
17 KiB
PHP

<?php
/**
* Unified frontend avatar helpers.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_resolve_avatar_user_id')) {
/**
* Resolve a WordPress user ID from an avatar subject.
*
* @param mixed $subject User ID, WP_User, comment, post, or email.
*/
function aksrt_resolve_avatar_user_id($subject): int {
if ($subject instanceof WP_User) {
return (int) $subject->ID;
}
if ($subject instanceof WP_Post) {
return (int) $subject->post_author;
}
if ($subject instanceof WP_Comment) {
return (int) $subject->user_id;
}
if (is_object($subject) && isset($subject->user_id)) {
return (int) $subject->user_id;
}
if (is_numeric($subject)) {
return max(0, (int) $subject);
}
if (is_string($subject) && is_email($subject)) {
$user = get_user_by('email', $subject);
return $user instanceof WP_User ? (int) $user->ID : 0;
}
return 0;
}
}
if (!function_exists('aksrt_resolve_avatar_email')) {
/**
* Resolve an email address from an avatar subject.
*
* @param mixed $subject User ID, WP_User, comment, post, or email.
*/
function aksrt_resolve_avatar_email($subject): string {
if ($subject instanceof WP_User) {
return sanitize_email((string) $subject->user_email);
}
if ($subject instanceof WP_Comment && is_email((string) $subject->comment_author_email)) {
return sanitize_email((string) $subject->comment_author_email);
}
if (is_object($subject) && isset($subject->comment_author_email) && is_email((string) $subject->comment_author_email)) {
return sanitize_email((string) $subject->comment_author_email);
}
if (is_string($subject) && is_email($subject)) {
return sanitize_email($subject);
}
$user_id = aksrt_resolve_avatar_user_id($subject);
$user = $user_id > 0 ? get_userdata($user_id) : false;
return $user instanceof WP_User ? sanitize_email((string) $user->user_email) : '';
}
}
if (!function_exists('aksrt_get_user_feature_settings')) {
/**
* Return the canonical user media and signature settings.
*
* @return array{avatar_upload_s:bool,avatar_external:string,avatar_default_img:string,user_cover_upload_s:bool,user_cover_img:string,user_desc_std:string}
*/
function aksrt_get_user_feature_settings(): array {
if (class_exists('\AKSRT\Settings\Settings')) {
return \AKSRT\Settings\Settings::get_user_feature_settings();
}
return [
'avatar_upload_s' => true,
'avatar_external' => '',
'avatar_default_img' => get_theme_file_uri('assets/img/avatar-default.png'),
'user_cover_upload_s' => true,
'user_cover_img' => get_theme_file_uri('assets/img/user_t.jpg'),
'user_desc_std' => '这家伙很懒,什么都没有写...',
];
}
}
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 {
$settings = aksrt_get_user_feature_settings();
return (string) apply_filters(
'aksrt_default_avatar_url',
(string) $settings['avatar_default_img']
);
}
}
if (!function_exists('aksrt_get_external_avatar_url')) {
/**
* Return the configured external avatar for a subject.
*
* @param mixed $subject User subject.
*/
function aksrt_get_external_avatar_url($subject, int $size = 96): string {
$settings = aksrt_get_user_feature_settings();
$hosts = [
'gravatar' => 'https://www.gravatar.com/avatar/',
'weavatar' => 'https://weavatar.com/avatar/',
'cravatar' => 'https://cn.cravatar.com/avatar/',
'geekzu' => 'https://sdn.geekzu.org/avatar/',
'loli' => 'https://gravatar.loli.net/avatar/',
'sep' => 'https://cdn.sep.cc/avatar/',
'v2ex' => 'https://cdn.v2ex.com/gravatar/',
];
$host = $hosts[(string) $settings['avatar_external']] ?? '';
$email = aksrt_resolve_avatar_email($subject);
if ($host === '' || $email === '') {
return '';
}
$size = max(1, min(2048, $size));
return add_query_arg([
's' => $size,
'd' => aksrt_get_default_avatar_url(),
], $host . hash('sha256', strtolower(trim($email))));
}
}
if (!function_exists('aksrt_get_custom_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) {
return '';
}
$attachment_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
if ($attachment_id <= 0) {
return '';
}
$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 : '';
}
}
if (!function_exists('aksrt_get_avatar_url')) {
/**
* Resolve the canonical avatar URL used by all AKSRT frontend surfaces.
*
* @param mixed $subject User subject accepted by get_avatar_url().
* @param array|int $args WordPress avatar args or a numeric size.
*/
function aksrt_get_avatar_url($subject, $args = []): string {
if (is_numeric($args)) {
$args = ['size' => (int) $args];
}
$args = is_array($args) ? $args : [];
$size = max(16, min(512, (int) ($args['size'] ?? 96)));
$user_id = aksrt_resolve_avatar_user_id($subject);
$custom = aksrt_get_custom_avatar_url($user_id, $size);
if ($custom !== '') {
return $custom;
}
$external = aksrt_get_external_avatar_url($subject, $size);
if ($external !== '') {
return $external;
}
return aksrt_get_default_avatar_url();
}
}
if (!function_exists('aksrt_get_avatar_html')) {
/**
* Render a canonical avatar image.
*
* @param mixed $subject User subject.
* @param int $size Pixel size.
* @param array<string,mixed> $attrs Additional safe image attributes.
*/
function aksrt_get_avatar_html($subject, int $size = 96, array $attrs = []): string {
$size = max(16, min(512, $size));
$url = aksrt_get_avatar_url($subject, ['size' => $size]);
if ($url === '') {
return '';
}
$defaults = [
'class' => 'avatar avatar-' . $size . ' photo',
'alt' => '',
'width' => (string) $size,
'height' => (string) $size,
];
$attrs = array_merge($defaults, $attrs);
unset($attrs['loading']);
$class = (string) ($attrs['class'] ?? $defaults['class']);
unset($attrs['class']);
$html_attrs = '';
foreach ($attrs as $name => $value) {
$name = sanitize_key((string) $name);
if ($name === '' || $value === false || $value === null) {
continue;
}
$html_attrs .= ' ' . $name . '="' . esc_attr((string) $value) . '"';
}
return '<img' . aksrt_get_lazy_attr('lazy_avatar', $url, $class, aksrt_get_default_avatar_url()) . $html_attrs . '>';
}
}
add_filter('get_avatar_data', static function (array $args, $subject): array {
$args['url'] = aksrt_get_avatar_url($subject, ['size' => (int) ($args['size'] ?? 96)]);
$args['found_avatar'] = true;
return $args;
}, 20, 2);
if (!function_exists('aksrt_get_avatar_box')) {
/**
* legacy parity: 头像框(span.avatar-img + VIP 角标 + 可选主页链接)。
*
* @param mixed $id_or_email 用户 ID / WP_User / 邮箱。
* @param string $class 外层 span 类(avatar-img avatar-lg 等)。
* @param bool $link 是否包一层用户主页链接。
* @param bool $vip 是否叠加 VIP 角标图。
*/
function aksrt_get_avatar_box($id_or_email, string $class = 'avatar-img', bool $link = true, bool $vip = true): string {
$user_id = aksrt_resolve_avatar_user_id($id_or_email);
$subject = $user_id > 0 ? $user_id : $id_or_email;
$img = aksrt_get_avatar_html($subject, 80, ['class' => 'avatar']);
$badge = '';
if ($vip && $user_id > 0) {
// 禁封角标覆盖 VIP 角标位(参考主题 user_avatar_badge 过滤器覆盖语义)。
$ban_badge = function_exists('aksrt_get_user_ban_avatar_badge')
? aksrt_get_user_ban_avatar_badge($user_id)
: '';
if ($ban_badge !== '') {
$badge = $ban_badge;
} elseif (function_exists('aksrt_get_user_vip_level') && aksrt_get_user_vip_level($user_id) > 0) {
$icon_url = '';
if (class_exists('\AKSRT\Settings\Settings')) {
$vip_settings = \AKSRT\Settings\Settings::get_payment_vip_settings();
$level = aksrt_get_user_vip_level($user_id);
$icon_url = esc_url((string) ($vip_settings['levels'][$level]['img_icon'] ?? ''));
}
if ($icon_url !== '') {
$badge_text = function_exists('aksrt_get_vip_badge_text') ? aksrt_get_vip_badge_text($user_id) : 'VIP';
$badge = '<img class="avatar-badge" src="' . $icon_url . '" title="' . esc_attr($badge_text) . '" alt="' . esc_attr($badge_text) . '">';
}
}
}
$html = '<span class="' . esc_attr($class) . '">' . $img . $badge . '</span>';
if ($link && $user_id > 0) {
$home = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user_id) : get_author_posts_url($user_id);
$html = '<a href="' . esc_url($home) . '">' . $html . '</a>';
}
return $html;
}
}
add_action('show_user_profile', 'aksrt_render_admin_avatar_field');
add_action('edit_user_profile', 'aksrt_render_admin_avatar_field');
add_action('user_edit_form_tag', static function (): void {
echo ' enctype="multipart/form-data"';
});
if (!function_exists('aksrt_is_avatar_upload_enabled')) {
/**
* Whether local avatar uploads are enabled by attachment policy.
*/
function aksrt_is_avatar_upload_enabled(): bool {
$settings = aksrt_get_user_feature_settings();
return !empty($settings['avatar_upload_s']);
}
}
if (!function_exists('aksrt_is_user_cover_upload_enabled')) {
function aksrt_is_user_cover_upload_enabled(): bool {
$settings = aksrt_get_user_feature_settings();
return !empty($settings['user_cover_upload_s']);
}
}
if (!function_exists('aksrt_get_user_cover_url')) {
function aksrt_get_user_cover_url(int $user_id): string {
$custom = $user_id > 0 ? esc_url_raw((string) get_user_meta($user_id, 'aksrt_background_image', true)) : '';
if ($custom !== '') {
return $custom;
}
$settings = aksrt_get_user_feature_settings();
return (string) $settings['user_cover_img'];
}
}
if (!function_exists('aksrt_get_user_description')) {
function aksrt_get_user_description(int $user_id): string {
$description = $user_id > 0 ? trim((string) get_user_meta($user_id, 'description', true)) : '';
if ($description !== '') {
return $description;
}
$settings = aksrt_get_user_feature_settings();
return (string) $settings['user_desc_std'];
}
}
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) || !aksrt_is_avatar_upload_enabled()) {
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 get_avatar($user->ID, 96, '', $user->display_name, ['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_filter('user_profile_update_errors', static function (WP_Error $errors, bool $update): void {
if (empty($_FILES['aksrt_avatar_file']['name']) || !aksrt_is_avatar_upload_enabled()) {
return;
}
$file = $_FILES['aksrt_avatar_file'];
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$max_size_mb = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2)));
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || $actual_size <= 0 || $actual_size > $max_size_mb * MB_IN_BYTES) {
$errors->add('aksrt_avatar_size', sprintf(__('头像文件无效或超过 %dMB', 'aksrt'), $max_size_mb));
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
$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',
]);
$image_size = wp_getimagesize($file['tmp_name']);
if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type']) || !$image_size || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
$errors->add('aksrt_avatar_type', __('头像必须是有效的 JPG、PNG、GIF 或 WebP 图片,且总像素不能超过 4000 万。', 'aksrt'));
}
}, 10, 2);
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) || !aksrt_is_avatar_upload_enabled()) {
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'];
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || $actual_size <= 0 || $actual_size > $max_size) {
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
$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',
]);
$image_size = wp_getimagesize($file['tmp_name']);
if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type']) || !$image_size || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
return;
}
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);
}
}