diff --git a/functions.php b/functions.php
index 7c817caf..21ca21fb 100644
--- a/functions.php
+++ b/functions.php
@@ -374,6 +374,9 @@ if (class_exists('\AKSRT\User\Level')) {
if (class_exists('\AKSRT\User\LevelAdmin')) {
\AKSRT\User\LevelAdmin::get_instance()->init();
}
+if (class_exists('\AKSRT\User\ProfileAdmin')) {
+ \AKSRT\User\ProfileAdmin::init();
+}
if (class_exists('\AKSRT\User\Metrics')) {
\AKSRT\User\Metrics::get_instance()->init();
}
diff --git a/inc/helpers/aksrt-avatar.php b/inc/helpers/aksrt-avatar.php
index 17b317c7..2fc58aa2 100644
--- a/inc/helpers/aksrt-avatar.php
+++ b/inc/helpers/aksrt-avatar.php
@@ -267,8 +267,6 @@ if (!function_exists('aksrt_get_avatar_box')) {
}
}
-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"';
});
@@ -312,137 +310,6 @@ if (!function_exists('aksrt_get_user_description')) {
}
}
-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;
- }
- ?>
-
-
- $max_size) {
- $errors->add('aksrt_avatar_size', sprintf(__('头像文件无效或超过 %s', 'aksrt'), size_format($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) {
- $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;
- }
-
- // 头像上限并入「前端图片上传限制」统一治理(参考主题同源)。
- $max_size = class_exists('\AKSRT\Attachment\UploadLimits') ? \AKSRT\Attachment\UploadLimits::limit_bytes('image') : (int) wp_max_upload_size();
- $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.
diff --git a/inc/helpers/aksrt-user-center.php b/inc/helpers/aksrt-user-center.php
index 529dba05..5d55aa6a 100644
--- a/inc/helpers/aksrt-user-center.php
+++ b/inc/helpers/aksrt-user-center.php
@@ -12,7 +12,7 @@ if (!function_exists('aksrt_get_user_profile_details')) {
* Read the fixed profile details. Social fields are deliberately kept in one
* structured meta value so public rendering can filter them server-side.
*
- * @return array{privacy:string,gender:string,website:string,qq:string,wechat:string,github:string}
+ * @return array{privacy:string,gender:string,website:string,qq:string,wechat:string,weibo:string,github:string,address:string}
*/
function aksrt_get_user_profile_details(int $user_id): array {
$raw = get_user_meta($user_id, 'aksrt_profile_details', true);
@@ -32,13 +32,15 @@ if (!function_exists('aksrt_get_user_profile_details')) {
'website' => esc_url_raw((string) ($raw['website'] ?? '')),
'qq' => sanitize_text_field((string) ($raw['qq'] ?? '')),
'wechat' => sanitize_text_field((string) ($raw['wechat'] ?? '')),
+ 'weibo' => esc_url_raw((string) ($raw['weibo'] ?? '')),
'github' => esc_url_raw((string) ($raw['github'] ?? '')),
+ 'address' => sanitize_text_field((string) ($raw['address'] ?? '')),
];
}
}
if (!function_exists('aksrt_get_visible_user_profile_details')) {
- /** @return array{gender:string,website:string,qq:string,wechat:string,github:string} */
+ /** @return array{gender:string,website:string,qq:string,wechat:string,weibo:string,github:string,address:string} */
function aksrt_get_visible_user_profile_details(int $user_id, int $viewer_id = 0): array {
$details = aksrt_get_user_profile_details($user_id);
$visible = $viewer_id === $user_id || $details['privacy'] === 'public' || ($details['privacy'] === 'members' && $viewer_id > 0);
@@ -47,7 +49,9 @@ if (!function_exists('aksrt_get_visible_user_profile_details')) {
'website' => $visible ? $details['website'] : '',
'qq' => $visible ? $details['qq'] : '',
'wechat' => $visible ? $details['wechat'] : '',
+ 'weibo' => $visible ? $details['weibo'] : '',
'github' => $visible ? $details['github'] : '',
+ 'address' => $visible ? $details['address'] : '',
];
}
}
@@ -86,28 +90,32 @@ if (!function_exists('aksrt_save_user_center_profile')) {
}
}
- $privacy = isset($input['privacy']) ? sanitize_key(wp_unslash($input['privacy'])) : 'private';
+ $existing_details = aksrt_get_user_profile_details($user_id);
+ $privacy = isset($input['privacy']) ? sanitize_key(wp_unslash($input['privacy'])) : $existing_details['privacy'];
if (!in_array($privacy, ['private', 'public', 'members'], true)) {
$errors[] = __('无效的隐私设置。', 'aksrt');
}
- $gender = isset($input['gender']) ? sanitize_key(wp_unslash($input['gender'])) : 'secret';
+ $gender = isset($input['gender']) ? sanitize_key(wp_unslash($input['gender'])) : $existing_details['gender'];
if (!in_array($gender, ['secret', 'male', 'female'], true)) {
$errors[] = __('无效的性别设置。', 'aksrt');
}
$details = [
'privacy' => $privacy,
'gender' => $gender,
- 'website' => isset($input['website']) ? esc_url_raw(wp_unslash($input['website'])) : '',
- 'qq' => isset($input['qq']) ? sanitize_text_field(wp_unslash($input['qq'])) : '',
- 'wechat' => isset($input['wechat']) ? sanitize_text_field(wp_unslash($input['wechat'])) : '',
- 'github' => isset($input['github']) ? esc_url_raw(wp_unslash($input['github'])) : '',
+ 'website' => isset($input['website']) ? esc_url_raw(wp_unslash($input['website'])) : $existing_details['website'],
+ 'qq' => isset($input['qq']) ? sanitize_text_field(wp_unslash($input['qq'])) : $existing_details['qq'],
+ 'wechat' => isset($input['wechat']) ? sanitize_text_field(wp_unslash($input['wechat'])) : $existing_details['wechat'],
+ 'weibo' => isset($input['weibo']) ? esc_url_raw(wp_unslash($input['weibo'])) : $existing_details['weibo'],
+ 'github' => isset($input['github']) ? esc_url_raw(wp_unslash($input['github'])) : $existing_details['github'],
+ 'address' => isset($input['address']) ? sanitize_text_field(wp_unslash($input['address'])) : $existing_details['address'],
];
- foreach (['website', 'github'] as $field) {
+ foreach (['website', 'weibo', 'github'] as $field) {
if (!empty($input[$field]) && $details[$field] === '') {
- $errors[] = sprintf(__('无效的%s链接。', 'aksrt'), $field === 'website' ? __('个人网站', 'aksrt') : 'GitHub');
+ $field_label = ['website' => __('个人网站', 'aksrt'), 'weibo' => __('微博', 'aksrt'), 'github' => 'GitHub'][$field];
+ $errors[] = sprintf(__('无效的%s链接。', 'aksrt'), $field_label);
}
}
- if (mb_strlen($details['qq']) > 50 || mb_strlen($details['wechat']) > 100) {
+ if (mb_strlen($details['qq']) > 50 || mb_strlen($details['wechat']) > 100 || mb_strlen($details['address']) > 200) {
$errors[] = __('社交资料长度超过限制。', 'aksrt');
}
if ($errors) {
diff --git a/inc/helpers/aksrt-user-dashboard.php b/inc/helpers/aksrt-user-dashboard.php
index 729e08fe..0a4d3d88 100644
--- a/inc/helpers/aksrt-user-dashboard.php
+++ b/inc/helpers/aksrt-user-dashboard.php
@@ -66,7 +66,7 @@ function aksrt_get_user_dashboard_view(int $user_id, array $args = []): array {
$profile_details = function_exists('aksrt_get_visible_user_profile_details')
? aksrt_get_visible_user_profile_details($user_id, $viewer_id)
- : ['gender' => '', 'website' => '', 'qq' => '', 'wechat' => '', 'github' => ''];
+ : ['gender' => '', 'website' => '', 'qq' => '', 'wechat' => '', 'weibo' => '', 'github' => '', 'address' => ''];
$background_url = function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url($user_id) : '';
diff --git a/inc/helpers/user-center-renderers/profile-form.php b/inc/helpers/user-center-renderers/profile-form.php
index 97e1b065..79d14408 100644
--- a/inc/helpers/user-center-renderers/profile-form.php
+++ b/inc/helpers/user-center-renderers/profile-form.php
@@ -55,7 +55,9 @@ $profile_details = isset($profile_details) && is_array($profile_details) ? $prof
+
+
diff --git a/inc/helpers/user-center-renderers/settings-account.php b/inc/helpers/user-center-renderers/settings-account.php
index b30a4334..1bf58e22 100644
--- a/inc/helpers/user-center-renderers/settings-account.php
+++ b/inc/helpers/user-center-renderers/settings-account.php
@@ -12,7 +12,7 @@ $account_user = isset($target_user) && $target_user instanceof WP_User
: wp_get_current_user();
$account_user_id = $account_user instanceof WP_User ? (int) $account_user->ID : (int) ($user_id ?? 0);
$account_email = $account_user instanceof WP_User ? (string) $account_user->user_email : '';
-$account_phone = $account_user_id > 0 ? (string) get_user_meta($account_user_id, 'aksrt_phone', true) : '';
+$account_phone = $account_user_id > 0 ? (string) get_user_meta($account_user_id, 'phone_number', true) : '';
$user_settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$sms_settings = class_exists('\\AKSRT\\Sms\\SMS') ? \AKSRT\Sms\SMS::get_instance()->get_provider_config() : [];
$phone_available = !empty($user_settings['bind_phone']) && (($sms_settings['sms_sdk'] ?? 'null') !== 'null');
diff --git a/src/Admin/UserControllerAuthPhoneOperations.php b/src/Admin/UserControllerAuthPhoneOperations.php
index 96c289ed..f378673c 100644
--- a/src/Admin/UserControllerAuthPhoneOperations.php
+++ b/src/Admin/UserControllerAuthPhoneOperations.php
@@ -131,7 +131,7 @@ trait UserControllerAuthPhoneOperations {
private function auth_find_user(string $target, string $channel) {
if ($channel === 'phone') {
$users = get_users([
- 'meta_key' => 'aksrt_phone',
+ 'meta_key' => 'phone_number',
'meta_value' => $target,
'number' => 1,
]);
diff --git a/src/Auth/Service.php b/src/Auth/Service.php
index 07ad8973..c454b0f0 100644
--- a/src/Auth/Service.php
+++ b/src/Auth/Service.php
@@ -50,7 +50,7 @@ final class Service {
&& \AKSRT\Settings\Settings::signin_phone_enabled()
) {
$users = get_users([
- 'meta_key' => 'aksrt_phone',
+ 'meta_key' => 'phone_number',
'meta_value' => $login,
'number' => 1,
]);
diff --git a/src/Auth/ServicePart1b.php b/src/Auth/ServicePart1b.php
index 15735eb3..b0732e04 100644
--- a/src/Auth/ServicePart1b.php
+++ b/src/Auth/ServicePart1b.php
@@ -127,7 +127,7 @@ trait ServicePart1b {
}
// 绑定手机号
- update_user_meta($user_id, 'aksrt_phone', $phone);
+ update_user_meta($user_id, 'phone_number', $phone);
update_user_meta($user_id, 'aksrt_phone_verified', '1');
// 自动登录
@@ -173,21 +173,23 @@ trait ServicePart1b {
return ['success' => false, 'error' => __('验证码错误或已过期', 'aksrt')];
}
- // 检查手机号是否已被其他用户绑定
+ $user_id = get_current_user_id();
$existing = self::get_user_by_phone($phone);
- if ($existing && (int) $existing->ID !== get_current_user_id()) {
+ if ($existing && (int) $existing->ID !== $user_id) {
return ['success' => false, 'error' => __('该手机号已被其他账号绑定', 'aksrt')];
}
+ if ($existing && (int) $existing->ID === $user_id) {
+ return ['success' => false, 'error' => __('手机号不能与当前绑定号码相同', 'aksrt')];
+ }
- $user_id = get_current_user_id();
- update_user_meta($user_id, 'aksrt_phone', $phone);
- update_user_meta($user_id, 'aksrt_phone_verified', '1');
- do_action('aksrt_user_phone_changed', $user_id, $phone);
-
- return [
+ $old_phone = (string) get_user_meta($user_id, 'phone_number', true);
+ update_user_meta($user_id, 'phone_number', $phone);
+ update_user_meta($user_id, 'aksrt_phone_verified', '1');
+ do_action('aksrt_user_phone_changed', $user_id, $phone, $old_phone);
+ return [
'success' => true,
- 'message' => __('手机号绑定成功', 'aksrt'),
+ 'message' => $old_phone !== '' ? __('手机号修改成功', 'aksrt') : __('手机号绑定成功', 'aksrt'),
];
}
@@ -196,7 +198,7 @@ trait ServicePart1b {
*/
private static function get_user_by_phone(string $phone): ?\WP_User {
$users = get_users([
- 'meta_key' => 'aksrt_phone',
+ 'meta_key' => 'phone_number',
'meta_value' => $phone,
'number' => 1,
'fields' => 'all',
diff --git a/src/User/ProfileAdmin.php b/src/User/ProfileAdmin.php
new file mode 100644
index 00000000..7e0cc5e6
--- /dev/null
+++ b/src/User/ProfileAdmin.php
@@ -0,0 +1,282 @@
+ID)) {
+ return;
+ }
+
+ $user_id = (int) $user->ID;
+ $details = function_exists('aksrt_get_user_profile_details')
+ ? aksrt_get_user_profile_details($user_id)
+ : ['gender' => 'secret', 'website' => '', 'qq' => '', 'wechat' => '', 'weibo' => '', 'github' => '', 'address' => ''];
+ $avatar_url = function_exists('aksrt_get_custom_avatar_url') ? aksrt_get_custom_avatar_url($user_id, 96) : '';
+ $cover_id = (int) get_user_meta($user_id, 'aksrt_background_attachment_id', true);
+ $cover_url = esc_url_raw((string) get_user_meta($user_id, 'aksrt_background_image', true));
+ if ($cover_url === '' && $cover_id > 0) $cover_url = (string) wp_get_attachment_image_url($cover_id, 'full');
+ $phone = (string) get_user_meta($user_id, self::PHONE_META, true);
+ ?>
+
+
+
+ ID : (int) ($_POST['user_id'] ?? 0);
+ if ($user_id <= 0 || !current_user_can('edit_user', $user_id) || empty($_POST[self::PROFILE_NONCE])) {
+ return;
+ }
+ $nonce = sanitize_text_field(wp_unslash($_POST[self::PROFILE_NONCE]));
+ if (!wp_verify_nonce($nonce, self::PROFILE_NONCE . '_' . $user_id)) {
+ $errors->add('aksrt_profile_nonce', __('更多资料安全验证失败,请刷新页面重试。', 'aksrt'));
+ return;
+ }
+ $phone = trim(sanitize_text_field(wp_unslash($_POST['phone_number'] ?? '')));
+ $old_phone = (string) get_user_meta($user_id, self::PHONE_META, true);
+ $unlocked = !empty($_POST['aksrt_phone_edit_unlocked']);
+ if ($phone !== '' && $phone !== $old_phone && $unlocked) {
+ if (!class_exists('AKSRT\\Sms\\SMS') || !\AKSRT\Sms\SMS::is_phonenumber($phone)) {
+ $errors->add('aksrt_profile_phone', __('绑定手机号格式不正确。', 'aksrt'));
+ } else {
+ $users = get_users(['meta_key' => self::PHONE_META, 'meta_value' => $phone, 'exclude' => [$user_id], 'number' => 1, 'fields' => 'ids']);
+ if (!empty($users)) {
+ $errors->add('aksrt_profile_phone_exists', __('该手机号已被其他账号绑定。', 'aksrt'));
+ }
+ }
+ }
+ foreach (['aksrt_profile_weibo' => '微博', 'aksrt_profile_github' => 'GitHub', 'aksrt_profile_website' => '个人网站'] as $key => $label) {
+ $value = trim((string) wp_unslash($_POST[$key] ?? ''));
+ if ($value !== '' && !esc_url_raw($value)) {
+ $errors->add('aksrt_profile_' . sanitize_key($label), sprintf(__('%s地址格式不正确。', 'aksrt'), $label));
+ }
+ }
+ $gender = sanitize_key(wp_unslash($_POST['aksrt_profile_gender'] ?? 'secret'));
+ if (!in_array($gender, ['secret', 'male', 'female'], true)) {
+ $errors->add('aksrt_profile_gender', __('性别选项无效。', 'aksrt'));
+ }
+ $details_limits = ['aksrt_profile_qq' => 50, 'aksrt_profile_wechat' => 100, 'aksrt_profile_address' => 200];
+ foreach ($details_limits as $key => $limit) {
+ if (mb_strlen((string) wp_unslash($_POST[$key] ?? '')) > $limit) {
+ $errors->add('aksrt_profile_' . $key, sprintf(__('资料字段不能超过 %d 个字符。', 'aksrt'), $limit));
+ }
+ }
+ if (!function_exists('aksrt_is_avatar_upload_enabled') || aksrt_is_avatar_upload_enabled()) {
+ self::validate_image_upload($errors, 'aksrt_avatar_file', __('头像', 'aksrt'), $user_id);
+ }
+ if (!function_exists('aksrt_is_user_cover_upload_enabled') || aksrt_is_user_cover_upload_enabled()) {
+ self::validate_image_upload($errors, 'aksrt_cover_file', __('封面', 'aksrt'), $user_id);
+ }
+ }
+
+ public static function save(int $user_id): void {
+ if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
+ return;
+ }
+ $nonce = sanitize_text_field(wp_unslash($_POST[self::PROFILE_NONCE] ?? ''));
+ if (!wp_verify_nonce($nonce, self::PROFILE_NONCE . '_' . $user_id)) {
+ return;
+ }
+
+ $details = function_exists('aksrt_get_user_profile_details') ? aksrt_get_user_profile_details($user_id) : [];
+ if (isset($_POST['aksrt_profile_gender'])) $details['gender'] = sanitize_key(wp_unslash($_POST['aksrt_profile_gender']));
+ if (isset($_POST['aksrt_profile_qq'])) $details['qq'] = sanitize_text_field(wp_unslash($_POST['aksrt_profile_qq']));
+ if (isset($_POST['aksrt_profile_wechat'])) $details['wechat'] = sanitize_text_field(wp_unslash($_POST['aksrt_profile_wechat']));
+ if (isset($_POST['aksrt_profile_weibo'])) $details['weibo'] = esc_url_raw(wp_unslash($_POST['aksrt_profile_weibo']));
+ if (isset($_POST['aksrt_profile_github'])) $details['github'] = esc_url_raw(wp_unslash($_POST['aksrt_profile_github']));
+ if (isset($_POST['aksrt_profile_website'])) $details['website'] = esc_url_raw(wp_unslash($_POST['aksrt_profile_website']));
+ if (isset($_POST['aksrt_profile_address'])) $details['address'] = sanitize_text_field(wp_unslash($_POST['aksrt_profile_address']));
+ if (function_exists('aksrt_get_user_profile_details')) update_user_meta($user_id, 'aksrt_profile_details', $details);
+
+ $old_phone = (string) get_user_meta($user_id, self::PHONE_META, true);
+ $new_phone = trim(sanitize_text_field(wp_unslash($_POST['phone_number'] ?? '')));
+ if (!empty($_POST['aksrt_phone_edit_unlocked']) && $new_phone !== '' && $new_phone !== $old_phone && class_exists('AKSRT\\Sms\\SMS') && \AKSRT\Sms\SMS::is_phonenumber($new_phone)) {
+ $users = get_users(['meta_key' => self::PHONE_META, 'meta_value' => $new_phone, 'exclude' => [$user_id], 'number' => 1, 'fields' => 'ids']);
+ if (empty($users)) {
+ update_user_meta($user_id, self::PHONE_META, $new_phone);
+ update_user_meta($user_id, 'aksrt_phone_verified', '1');
+ do_action('aksrt_user_phone_changed', $user_id, $new_phone, $old_phone);
+ }
+ }
+
+ self::save_avatar($user_id);
+ self::save_cover($user_id);
+ }
+
+ private static function validate_image_upload(\WP_Error $errors, string $field, string $label, int $user_id): void {
+ if (empty($_FILES[$field]['name'])) return;
+ $file = $_FILES[$field];
+ $max_size = class_exists('AKSRT\\Attachment\\UploadLimits')
+ ? \AKSRT\Attachment\UploadLimits::limit_bytes('image', $user_id)
+ : (int) wp_max_upload_size();
+ $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) {
+ $errors->add('aksrt_profile_' . sanitize_key($field) . '_size', sprintf(__('%1$s文件无效或超过 %2$s。', 'aksrt'), $label, size_format($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) {
+ $errors->add('aksrt_profile_' . sanitize_key($field) . '_type', sprintf(__('%s必须是有效的 JPG、PNG、GIF 或 WebP 图片,且总像素不能超过 4000 万。', 'aksrt'), $label));
+ }
+ }
+
+ private static function save_avatar(int $user_id): void {
+ if (function_exists('aksrt_is_avatar_upload_enabled') && !aksrt_is_avatar_upload_enabled()) return;
+ $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');
+ if (function_exists('aksrt_delete_owned_avatar_attachment')) 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;
+ 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);
+ if (function_exists('aksrt_delete_owned_avatar_attachment')) 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'));
+ }
+
+ private static function save_cover(int $user_id): void {
+ if (function_exists('aksrt_is_user_cover_upload_enabled') && !aksrt_is_user_cover_upload_enabled()) return;
+ $old_id = (int) get_user_meta($user_id, 'aksrt_background_attachment_id', true);
+ if (!empty($_POST['aksrt_background_reset'])) {
+ delete_user_meta($user_id, 'aksrt_background_attachment_id');
+ delete_user_meta($user_id, 'aksrt_background_image');
+ self::delete_attachment($old_id, $user_id);
+ return;
+ }
+ if (empty($_FILES['aksrt_cover_file']['name'])) return;
+ $file = $_FILES['aksrt_cover_file'];
+ $max_size = class_exists('AKSRT\\Attachment\\UploadLimits') ? \AKSRT\Attachment\UploadLimits::limit_bytes('image', $user_id) : (int) wp_max_upload_size();
+ if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || empty($file['tmp_name']) || filesize($file['tmp_name']) > $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';
+ $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']) || !$image_size || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) return;
+ $attachment_id = media_handle_upload('aksrt_cover_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', 'background');
+ $url = wp_get_attachment_image_url($attachment_id, 'full');
+ if (!$url) {
+ wp_delete_attachment($attachment_id, true);
+ return;
+ }
+ update_user_meta($user_id, 'aksrt_background_attachment_id', $attachment_id);
+ update_user_meta($user_id, 'aksrt_background_image', esc_url_raw($url));
+ self::delete_attachment($old_id, $user_id);
+ }
+
+ private static function delete_attachment(int $attachment_id, int $user_id): void {
+ $attachment = $attachment_id > 0 ? get_post($attachment_id) : null;
+ $managed_user_id = $attachment_id > 0 ? (int) get_post_meta($attachment_id, '_aksrt_profile_media_user_id', true) : 0;
+ $managed_type = $attachment_id > 0 ? (string) get_post_meta($attachment_id, '_aksrt_profile_media_type', true) : '';
+ if ($attachment instanceof \WP_Post && $attachment->post_type === 'attachment' && (int) $attachment->post_author === $user_id && $managed_user_id === $user_id && $managed_type === 'background') {
+ wp_delete_attachment($attachment_id, true);
+ }
+ }
+
+ public static function unlock_script(): void {
+ if (!get_current_screen() || !in_array(get_current_screen()->base, ['profile', 'user-edit'], true)) return;
+ ?>
+
+ user_registered, 'Y-m-d H:i:s');
$last_login_ts = (int) get_user_meta($user_id, 'aksrt_last_login', true);
$last_login_time = $last_login_ts > 0 ? wp_date('Y-m-d H:i:s', $last_login_ts) : '';
-$has_more_details = $registered_time !== '' || $last_login_time !== '' || !empty($profile_details['website']) || !empty($profile_details['qq']) || !empty($profile_details['wechat']) || !empty($profile_details['github']);
+$has_more_details = $registered_time !== '' || $last_login_time !== '' || !empty($profile_details['website']) || !empty($profile_details['qq']) || !empty($profile_details['wechat']) || !empty($profile_details['weibo']) || !empty($profile_details['github']) || !empty($profile_details['address']);
$signals = $dashboard['profile_signals'] ?? [];
$show_report = !empty($dashboard['show_report']);
$bg_url = function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url($user_id) : '';
@@ -214,11 +214,13 @@ $show_cover_menu = $has_more_details || $show_cover_report || $cover_ban_html !=
-
+ - __('男', 'aksrt'), 'female' => __('女', 'aksrt'), 'secret' => __('保密', 'aksrt')][$profile_details['gender']] ?? __('保密', 'aksrt')); ?>
+
- QQ
+
diff --git a/tests/widget-system/user-features-contract.test.cjs b/tests/widget-system/user-features-contract.test.cjs
index 536d3fc3..19d71500 100644
--- a/tests/widget-system/user-features-contract.test.cjs
+++ b/tests/widget-system/user-features-contract.test.cjs
@@ -184,13 +184,14 @@ test('native user profile omits ledger tables and renders canonical images eager
const credits = read('src/User/CreditsAdmin.php');
const balance = read('src/User/BalanceAdmin.php');
const avatar = read('inc/helpers/aksrt-avatar.php');
+ const profileAdmin = read('src/User/ProfileAdmin.php');
const level = read('src/User/LevelAdmin.php');
assert.doesNotMatch(credits, /LedgerQuery::get_instance\(\)->history/);
assert.doesNotMatch(credits, /近期%s流水|近期积分流水/);
assert.doesNotMatch(balance, /LedgerQuery::get_instance\(\)->history/);
assert.doesNotMatch(balance, /近期流水/);
- assert.match(avatar, /get_avatar\(\$user->ID, 96, '', \$user->display_name, \['loading' => 'eager'\]\)/);
+ assert.match(profileAdmin, /get_avatar\(\$user_id, 96, '', \$user->display_name, \['loading' => 'eager'\]\)/);
assert.doesNotMatch(avatar, /aksrt_get_avatar_html\(\$user->ID, 96/);
assert.match(level, /Level::get_level_icon_url\(\$level\)/);
assert.match(level, /
{
+ const admin = read('src/User/ProfileAdmin.php');
+ const avatar = read('inc/helpers/aksrt-avatar.php');
+ const profile = read('inc/helpers/aksrt-user-center.php');
+ const form = read('inc/helpers/user-center-renderers/profile-form.php');
+ const publicProfile = read('templates/parts/user/author-hero.php');
+
+ ['自定义头像', '自定义封面', '性别', 'QQ', '微信', '微博', 'GitHub', '住址', '绑定手机号'].forEach((label) => assert.ok(admin.includes(label), `missing ${label}`));
+ assert.match(admin, /'aksrt_avatar_attachment_id'/);
+ assert.match(admin, /'aksrt_background_attachment_id'/);
+ assert.match(admin, /'aksrt_profile_details'/);
+ assert.match(profile, /'weibo'\s*=>/);
+ assert.match(profile, /'address'\s*=>/);
+ assert.match(profile, /\$existing_details\['weibo'\]/);
+ assert.match(profile, /\$existing_details\['address'\]/);
+ assert.match(form, /name="weibo"/);
+ assert.match(form, /name="address"/);
+ assert.match(publicProfile, /\$profile_details\['weibo'\]/);
+ assert.match(publicProfile, /\$profile_details\['address'\]/);
+ assert.doesNotMatch(avatar, /aksrt_render_admin_avatar_field|aksrt_save_admin_avatar_field/);
+});
diff --git a/tests/widget-system/user-profile-phone-harness.php b/tests/widget-system/user-profile-phone-harness.php
new file mode 100644
index 00000000..31c4ed1a
--- /dev/null
+++ b/tests/widget-system/user-profile-phone-harness.php
@@ -0,0 +1,238 @@
+errors[$code][] = $message; }
+ public function get_error_codes(): array { return array_keys($this->errors); }
+ }
+ final class WP_REST_Request {
+ public function __construct(private array $params = [], private string $nonce = 'ok') {}
+ public function get_json_params(): array { return $this->params; }
+ public function get_header(string $name): string { return $name === 'X-WP-Nonce' ? $this->nonce : ''; }
+ }
+ final class WP_REST_Response {
+ public function __construct(public array $data, public int $status = 200) {}
+ }
+ function is_wp_error($value): bool { return $value instanceof WP_Error; }
+
+ function is_admin(): bool { return true; }
+ function current_user_can(string $capability, ...$args): bool { return $GLOBALS['aksrt_can_edit_user']; }
+ function is_user_logged_in(): bool { return true; }
+ function get_current_user_id(): int { return 7; }
+ function get_user_meta(int $user_id, string $key, bool $single = false) {
+ $value = $GLOBALS['aksrt_profile_meta'][$user_id][$key] ?? '';
+ return $single ? $value : ($value === '' ? [] : [$value]);
+ }
+ function update_user_meta(int $user_id, string $key, $value): bool {
+ $GLOBALS['aksrt_profile_meta'][$user_id][$key] = $value;
+ return true;
+ }
+ function get_users(array $args = []): array {
+ $results = [];
+ foreach ($GLOBALS['aksrt_profile_meta'] as $user_id => $meta) {
+ if (in_array((int) $user_id, array_map('intval', $args['exclude'] ?? []), true)) continue;
+ if (($meta[$args['meta_key'] ?? ''] ?? null) !== ($args['meta_value'] ?? null)) continue;
+ $results[] = ($args['fields'] ?? '') === 'ids' ? (int) $user_id : new WP_User((int) $user_id);
+ if (count($results) >= (int) ($args['number'] ?? 1)) break;
+ }
+ return $results;
+ }
+ function sanitize_text_field($value): string { return trim(strip_tags((string) $value)); }
+ function sanitize_textarea_field($value): string { return sanitize_text_field($value); }
+ function sanitize_key($value): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $value)) ?? ''; }
+ function wp_unslash($value) { return $value; }
+ function esc_url_raw($value): string {
+ $value = trim((string) $value);
+ return $value === '' || filter_var($value, FILTER_VALIDATE_URL) ? $value : '';
+ }
+ function wp_verify_nonce(string $nonce, string $action): bool { return $GLOBALS['aksrt_nonce_valid'] && $nonce === 'ok'; }
+ function aksrt_require_captcha(string $scene, array $payload): bool { return true; }
+ function get_transient(string $key) { return $GLOBALS['aksrt_transients'][$key] ?? false; }
+ function set_transient(string $key, $value, int $expiration = 0): bool { $GLOBALS['aksrt_transients'][$key] = $value; return true; }
+ function delete_transient(string $key): bool { unset($GLOBALS['aksrt_transients'][$key]); return true; }
+ function wp_check_password(string $password, string $hash): bool { return $hash === 'fixture-hash' && $password === $GLOBALS['aksrt_expected_sms_code']; }
+ function wp_update_user(array $data) { return (int) ($data['ID'] ?? 0); }
+ function do_action(...$args): void {}
+ function add_action(...$args): void {}
+ function add_filter(...$args): void {}
+ function __(string $text, string $domain = ''): string { return $text; }
+ function esc_attr(string $value): string { return htmlspecialchars($value, ENT_QUOTES); }
+ function esc_html_e(string $text, string $domain = ''): void { echo htmlspecialchars($text, ENT_QUOTES); }
+ function esc_attr_e(string $text, string $domain = ''): void { echo htmlspecialchars($text, ENT_QUOTES); }
+ function esc_url(string $value): string { return $value; }
+ function selected($selected, $current = true, bool $echo = true): string { return (string) $selected === (string) $current ? 'selected="selected"' : ''; }
+ function wp_nonce_field(string $action, string $name): void {}
+ function get_avatar(...$args): string { return '
'; }
+ function wp_max_upload_size(): int { return 2097152; }
+ function size_format(int $bytes): string { return (string) $bytes; }
+ function wp_check_filetype_and_ext(...$args): array { return []; }
+ function wp_match_mime_types(...$args): bool { return true; }
+ function wp_getimagesize(...$args): array|false { return false; }
+ function wp_get_attachment_image_url(...$args): string|false { return false; }
+ function get_post(...$args) { return null; }
+ function get_post_meta(...$args) { return ''; }
+ function wp_delete_attachment(...$args): bool { return true; }
+ function get_current_screen() { return null; }
+
+ require dirname(__DIR__, 2) . '/inc/helpers/aksrt-user-center.php';
+}
+
+namespace AKSRT\Sms {
+ final class SMS {
+ public static function is_phonenumber(string $phone): bool { return (bool) preg_match('/^1[3-9]\d{9}$/', $phone); }
+ }
+}
+
+namespace AKSRT\Auth {
+ require dirname(__DIR__, 2) . '/src/Auth/ServicePart1b.php';
+ final class PhoneHarnessService { use ServicePart1b; }
+ final class Service { use ServicePart1b; }
+}
+
+namespace AKSRT\Admin {
+ require dirname(__DIR__, 2) . '/src/Admin/UserControllerAuthPhoneOperations.php';
+ final class PhoneOperationsHarness {
+ use UserControllerAuthPhoneOperations;
+ public function get_json_params(\WP_REST_Request $request): array { return $request->get_json_params(); }
+ public function error(string $message, int $status = 400): \WP_REST_Response { return new \WP_REST_Response(['success' => false, 'message' => $message], $status); }
+ public function success(array $data): \WP_REST_Response { return new \WP_REST_Response(array_merge(['success' => true], $data), 200); }
+ }
+}
+
+namespace {
+ require dirname(__DIR__, 2) . '/src/User/ProfileAdmin.php';
+
+ function check(bool $condition, string $message): void {
+ if (!$condition) throw new RuntimeException($message);
+ }
+
+ $GLOBALS['aksrt_profile_meta'][7] = [
+ 'aksrt_phone' => '13900000000',
+ 'aksrt_profile_details' => [
+ 'privacy' => 'public', 'gender' => 'female', 'website' => 'https://example.test',
+ 'qq' => '123456', 'wechat' => 'wxid', 'weibo' => 'https://weibo.com/user',
+ 'github' => 'https://github.com/user', 'address' => 'Shanghai',
+ ],
+ ];
+
+ $bad_format = \AKSRT\Auth\PhoneHarnessService::bind_phone(['phone' => 'bad'], true);
+ check(empty($bad_format['success']), 'phone binding accepted malformed number');
+ $unverified = \AKSRT\Auth\PhoneHarnessService::bind_phone(['phone' => '13800000007'], false);
+ check(empty($unverified['success']), 'phone binding accepted missing verification');
+
+ $GLOBALS['aksrt_profile_meta'][8]['phone_number'] = '13800000008';
+ $duplicate = \AKSRT\Auth\PhoneHarnessService::bind_phone(['phone' => '13800000008'], true);
+ check(empty($duplicate['success']), 'phone binding accepted a number owned by another user');
+
+ $bound = \AKSRT\Auth\PhoneHarnessService::bind_phone(['phone' => '13800000007'], true);
+ check(!empty($bound['success']), 'verified phone binding failed');
+ check(get_user_meta(7, 'phone_number', true) === '13800000007', 'verified phone was not stored in canonical meta');
+ check(get_user_meta(7, 'aksrt_phone_verified', true) === '1', 'verified marker was not stored');
+ check(get_user_meta(7, 'aksrt_phone', true) === '13900000000', 'legacy phone key was unexpectedly migrated or rewritten');
+
+ $same_phone = \AKSRT\Auth\PhoneHarnessService::bind_phone(['phone' => '13800000007'], true);
+ check(empty($same_phone['success']), 'binding accepted the current phone as a change');
+
+ $phone_to_bind = '13800000010';
+ $otp_key = 'aksrt_auth_code_' . md5('bind_phone|' . strtolower(trim($phone_to_bind)));
+ $operations = new \AKSRT\Admin\PhoneOperationsHarness();
+ $GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
+ $GLOBALS['aksrt_expected_sms_code'] = '246810';
+ $wrong_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '000000']));
+ check(empty($wrong_code->data['success']), 'REST phone binding accepted an incorrect SMS code');
+ check(($GLOBALS['aksrt_transients'][$otp_key]['attempts'] ?? 0) === 1, 'incorrect SMS code attempt was not recorded');
+ $GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() - 1];
+ $expired_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '246810']));
+ check(empty($expired_code->data['success']), 'REST phone binding accepted an expired SMS code');
+ check(!isset($GLOBALS['aksrt_transients'][$otp_key]), 'expired SMS code was not discarded');
+ $GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
+ $valid_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '246810']));
+ check(!empty($valid_code->data['success']), 'REST phone binding rejected the correct SMS code');
+ check(get_user_meta(7, 'phone_number', true) === $phone_to_bind, 'REST phone binding did not store the canonical phone meta');
+ check(!isset($GLOBALS['aksrt_transients'][$otp_key]), 'valid SMS code was not consumed');
+
+ $_POST = [
+ 'aksrt_profile_admin_nonce' => 'ok',
+ 'aksrt_admin_avatar_nonce' => 'ok',
+ 'aksrt_profile_gender' => 'male',
+ 'aksrt_profile_qq' => '7654321',
+ 'aksrt_profile_wechat' => 'new-wechat',
+ 'aksrt_profile_weibo' => 'https://weibo.com/new-user',
+ 'aksrt_profile_github' => 'https://github.com/new-user',
+ 'aksrt_profile_website' => 'https://new.example.test',
+ 'aksrt_profile_address' => 'Beijing',
+ 'phone_number' => '13800000007',
+ 'aksrt_phone_edit_unlocked' => '0',
+ ];
+ $_FILES = [];
+ \AKSRT\User\ProfileAdmin::save(7);
+ $details = aksrt_get_user_profile_details(7);
+ check($details['privacy'] === 'public', 'admin profile update erased social privacy setting');
+ check($details['gender'] === 'male' && $details['weibo'] === 'https://weibo.com/new-user' && $details['address'] === 'Beijing', 'admin profile fields did not persist');
+ check($details['website'] === 'https://new.example.test', 'personal website field did not persist');
+
+ $front_save = aksrt_save_user_center_profile(7, ['nickname' => 'Profile User']);
+ check($front_save === true, 'user-center profile save failed');
+ $details = aksrt_get_user_profile_details(7);
+ check($details['weibo'] === 'https://weibo.com/new-user' && $details['address'] === 'Beijing', 'front profile save erased admin-added fields');
+ $details['privacy'] = 'private';
+ update_user_meta(7, 'aksrt_profile_details', $details);
+ $private_view = aksrt_get_visible_user_profile_details(7, 9);
+ check($private_view['weibo'] === '' && $private_view['address'] === '', 'private social details leaked to another viewer');
+ $owner_view = aksrt_get_visible_user_profile_details(7, 7);
+ check($owner_view['weibo'] === 'https://weibo.com/new-user' && $owner_view['address'] === 'Beijing', 'owner cannot read private social details');
+ $details['privacy'] = 'public';
+ update_user_meta(7, 'aksrt_profile_details', $details);
+ $public_view = aksrt_get_visible_user_profile_details(7, 9);
+ check($public_view['weibo'] === 'https://weibo.com/new-user' && $public_view['address'] === 'Beijing', 'public social details are not visible to another viewer');
+
+ $_POST = ['aksrt_profile_admin_nonce' => 'ok', 'phone_number' => 'not-a-phone', 'aksrt_phone_edit_unlocked' => '1'];
+ $invalid_errors = new WP_Error();
+ \AKSRT\User\ProfileAdmin::validate($invalid_errors, true, new WP_User(7));
+ check(in_array('aksrt_profile_phone', $invalid_errors->get_error_codes(), true), 'invalid admin phone format was not rejected');
+
+ $_POST = ['aksrt_profile_admin_nonce' => 'ok', 'phone_number' => '13800000008', 'aksrt_phone_edit_unlocked' => '1'];
+ $duplicate_errors = new WP_Error();
+ \AKSRT\User\ProfileAdmin::validate($duplicate_errors, true, new WP_User(7));
+ check(in_array('aksrt_profile_phone_exists', $duplicate_errors->get_error_codes(), true), 'admin profile accepted a duplicate phone');
+
+ $_POST = ['aksrt_profile_admin_nonce' => 'wrong', 'phone_number' => '13800000009', 'aksrt_phone_edit_unlocked' => '1'];
+ $before_phone = get_user_meta(7, 'phone_number', true);
+ $before_details = get_user_meta(7, 'aksrt_profile_details', true);
+ \AKSRT\User\ProfileAdmin::save(7);
+ check(get_user_meta(7, 'phone_number', true) === $before_phone, 'invalid admin nonce changed the phone');
+ check(get_user_meta(7, 'aksrt_profile_details', true) === $before_details, 'invalid admin nonce changed profile details');
+
+ $_POST = ['aksrt_profile_admin_nonce' => 'ok', 'phone_number' => '13800000009', 'aksrt_phone_edit_unlocked' => '1'];
+ \AKSRT\User\ProfileAdmin::save(7);
+ check(get_user_meta(7, 'phone_number', true) === '13800000009', 'unlocked admin phone change was not saved');
+ check(get_user_meta(7, 'aksrt_phone_verified', true) === '1', 'admin phone change cleared its verified marker');
+ $_POST['phone_number'] = '';
+ \AKSRT\User\ProfileAdmin::save(7);
+ check(get_user_meta(7, 'phone_number', true) === '13800000009', 'blank admin phone unexpectedly cleared the binding');
+
+ $before_details = get_user_meta(7, 'aksrt_profile_details', true);
+ $GLOBALS['aksrt_can_edit_user'] = false;
+ $_POST = ['aksrt_profile_admin_nonce' => 'ok', 'aksrt_profile_address' => 'Unauthorized change'];
+ \AKSRT\User\ProfileAdmin::save(7);
+ $GLOBALS['aksrt_can_edit_user'] = true;
+ check(get_user_meta(7, 'aksrt_profile_details', true) === $before_details, 'user without edit permission changed profile details');
+
+ echo "User profile phone harness passed\n";
+}
diff --git a/uninstall.php b/uninstall.php
index d561209a..38c1b706 100644
--- a/uninstall.php
+++ b/uninstall.php
@@ -63,6 +63,10 @@ $meta_keys = [
'aksrt_avatar_attachment_id',
'aksrt_background_attachment_id',
'aksrt_background_image',
+ 'phone_number',
+ 'aksrt_phone_verified',
+ 'aksrt_profile_details',
+ 'aksrt_phone',
'aksrt_email_verified',
'aksrt_unread_message_count',
'_aksrt_active_restrictions',