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 (int) ($GLOBALS['aksrt_current_user_id'] ?? 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[] = in_array(($args['fields'] ?? ''), ['ids', 'ID'], true) ? (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\Settings { final class Settings { public static function get_all(): array { return ['bind_phone' => true]; } } } 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('account_bind_new_phone_7|' . strtolower(trim($phone_to_bind))); $operations = new \AKSRT\Admin\PhoneOperationsHarness(); $old_phone = '13800000007'; $old_otp_key = 'aksrt_auth_code_' . md5('account_verify_old_phone_7|' . strtolower(trim($old_phone))); $GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300]; $GLOBALS['aksrt_expected_sms_code'] = '246810'; $wrong_old_code = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '000000'])); check(empty($wrong_old_code->data['success']), 'REST phone replacement accepted an incorrect old-number code'); check(($GLOBALS['aksrt_transients'][$old_otp_key]['attempts'] ?? 0) === 1, 'incorrect old-number code attempt was not recorded'); $GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() - 1]; $expired_old_code = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '246810'])); check(empty($expired_old_code->data['success']), 'REST phone replacement accepted an expired old-number code'); check(!isset($GLOBALS['aksrt_transients'][$old_otp_key]), 'expired old-number code was not discarded'); $GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300]; $verified_old_phone = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '246810'])); check(!empty($verified_old_phone->data['success']), 'REST phone replacement rejected the correct old-number code'); check(!isset($GLOBALS['aksrt_transients'][$old_otp_key]), 'valid old-number code was not consumed'); check(($GLOBALS['aksrt_transients']['aksrt_account_phone_old_7']['phone'] ?? '') === $old_phone, 'old-number proof was not issued for the current account'); $GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300]; $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'); $duplicate_phone = '13800000008'; $duplicate_otp_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_7|' . strtolower(trim($duplicate_phone))); $GLOBALS['aksrt_transients'][$duplicate_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300]; $duplicate_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $duplicate_phone, 'code' => '246810'])); check(empty($duplicate_code->data['success']), 'REST phone binding accepted a number owned by another user'); check(isset($GLOBALS['aksrt_transients'][$duplicate_otp_key]), 'duplicate-number validation consumed the valid SMS code'); $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'); check(!isset($GLOBALS['aksrt_transients']['aksrt_account_phone_old_7']), 'successful phone change did not consume the old-number proof'); $initial_bind_user_id = 9; $initial_bind_phone = '13800000011'; $initial_bind_code_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_9|' . strtolower(trim($initial_bind_phone))); $GLOBALS['aksrt_current_user_id'] = $initial_bind_user_id; $GLOBALS['aksrt_transients'][$initial_bind_code_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300]; $initial_bind = $operations->user_bind_phone(new WP_REST_Request(['phone' => $initial_bind_phone, 'code' => '246810'])); check(!empty($initial_bind->data['success']), 'first-time phone binding incorrectly required an old-number proof'); check(get_user_meta($initial_bind_user_id, 'phone_number', true) === $initial_bind_phone, 'first-time phone binding did not store the canonical phone meta'); check(!isset($GLOBALS['aksrt_transients'][$initial_bind_code_key]), 'successful first-time phone binding did not consume its SMS code'); $GLOBALS['aksrt_current_user_id'] = 7; $_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"; }