true]; $GLOBALS['aksrt_account_user_id'] = 7; $GLOBALS['aksrt_account_user'] = null; $GLOBALS['aksrt_account_meta'] = []; $GLOBALS['aksrt_account_transients'] = []; $GLOBALS['aksrt_account_actions'] = []; $GLOBALS['aksrt_account_mail'] = []; $GLOBALS['aksrt_account_existing_emails'] = []; $GLOBALS['aksrt_account_captcha_calls'] = []; $GLOBALS['aksrt_account_captcha_fail'] = false; $GLOBALS['aksrt_account_update_fail'] = false; final class WP_User { public function __construct( public int $ID, public string $user_email, public string $display_name, public string $user_pass ) {} } final class WP_Error { public function __construct( private string $code = '', private string $message = '', private mixed $data = null ) {} public function get_error_code(): string { return $this->code; } public function get_error_message(): string { return $this->message; } public function get_error_data(): mixed { return $this->data; } } final class JsonResponseException extends \RuntimeException { public function __construct(public array $payload) { parent::__construct('json_response'); } } function add_action(string $hook, callable $callback): void { $GLOBALS['aksrt_account_actions'][$hook] = $callback; } function __(string $text, string $domain = ''): string { return $text; } function is_wp_error(mixed $value): bool { return $value instanceof WP_Error; } function is_email(string $email): bool { return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); } function sanitize_email(string $email): string { return trim($email); } function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; } function sanitize_text_field(string $value): string { return trim(strip_tags($value)); } function wp_unslash(mixed $value): mixed { return $value; } function wp_verify_nonce(string $nonce, string $action): bool { return $nonce === 'valid' && $action === 'aksrt_account_settings'; } function get_current_user_id(): int { return (int) $GLOBALS['aksrt_account_user_id']; } function wp_get_current_user(): mixed { return $GLOBALS['aksrt_account_user']; } function get_userdata(int $user_id): ?WP_User { return $user_id === 7 ? $GLOBALS['aksrt_account_user'] : null; } function get_bloginfo(string $show = ''): string { return 'AKSRT Test'; } function wp_hash_password(string $password): string { return 'hash:' . $password; } function wp_check_password(string $password, string $hash, int $user_id = 0): bool { return hash_equals('hash:' . $password, $hash); } function email_exists(string $email): int|false { return $GLOBALS['aksrt_account_existing_emails'][strtolower($email)] ?? false; } function set_transient(string $key, mixed $value, int $expiration = 0): bool { $GLOBALS['aksrt_account_transients'][$key] = [ 'value' => $value, 'expires' => $expiration > 0 ? time() + $expiration : 0, ]; return true; } function get_transient(string $key): mixed { $row = $GLOBALS['aksrt_account_transients'][$key] ?? null; if (!is_array($row)) return false; if (!empty($row['expires']) && $row['expires'] <= time()) { unset($GLOBALS['aksrt_account_transients'][$key]); return false; } return $row['value'] ?? false; } function delete_transient(string $key): bool { unset($GLOBALS['aksrt_account_transients'][$key]); return true; } function get_user_meta(int $user_id, string $key, bool $single = false): mixed { return $GLOBALS['aksrt_account_meta'][$user_id][$key] ?? ''; } function update_user_meta(int $user_id, string $key, mixed $value): bool { $GLOBALS['aksrt_account_meta'][$user_id][$key] = $value; return true; } function delete_user_meta(int $user_id, string $key): bool { unset($GLOBALS['aksrt_account_meta'][$user_id][$key]); return true; } function wp_update_user(array $data): int|WP_Error { if ($GLOBALS['aksrt_account_update_fail']) { return new WP_Error('update_failed', 'update failed'); } $user = $GLOBALS['aksrt_account_user']; if (isset($data['user_email'])) $user->user_email = (string) $data['user_email']; if (isset($data['user_pass'])) $user->user_pass = wp_hash_password((string) $data['user_pass']); return (int) $user->ID; } function aksrt_require_captcha(string $scene, array $payload): true|WP_Error { $GLOBALS['aksrt_account_captcha_calls'][] = $scene; return $GLOBALS['aksrt_account_captcha_fail'] ? new WP_Error('captcha_failed', 'captcha failed') : true; } function aksrt_get_auth_agreement_view(): array { return ['enabled' => true, 'required' => true]; } function wp_send_json_error(array $data): never { throw new JsonResponseException(['success' => false, 'data' => $data]); } function wp_send_json_success(array $data): never { throw new JsonResponseException(['success' => true, 'data' => $data]); } function account_settings_request(string $action, array $data = []): array { $callback = $GLOBALS['aksrt_account_actions'][$action] ?? null; if (!is_callable($callback)) throw new \RuntimeException('Missing AJAX action: ' . $action); $_POST = array_merge(['_settings_nonce' => 'valid'], $data); try { $callback(); } catch (JsonResponseException $response) { return $response->payload; } throw new \RuntimeException('AJAX action returned without a JSON response: ' . $action); } function account_settings_check(bool $condition, string $message): void { if (!$condition) throw new \RuntimeException($message); } function account_settings_code_key(string $scene, string $target): string { return 'aksrt_account_code_7_' . $scene . '_' . md5(strtolower(trim($target))); } function account_settings_send_cooldown_key(string $target): string { return 'aksrt_send_cd_' . md5(strtolower(trim($target))); } function account_settings_error_frequency_key(): string { return 'aksrt_err_freq_test'; } function account_settings_code(string $scene, string $target): string { $stored = get_transient(account_settings_code_key($scene, $target)); account_settings_check(is_array($stored), 'expected verification code was not stored for ' . $scene); return substr((string) ($stored['hash'] ?? ''), strlen('hash:')); } function account_settings_proof_key(): string { return 'aksrt_account_proof_email_old_7'; } require dirname(__DIR__, 2) . '/inc/modules/user-parts/account-ajax.php'; } namespace AKSRT\Settings { final class Settings { public static function get_all(): array { return $GLOBALS['aksrt_account_settings']; } public static function send_event_email(string $event, string $to, array $values): bool { $GLOBALS['aksrt_account_mail'][] = ['event' => $event, 'to' => $to, 'values' => $values]; return true; } } } namespace AKSRT\Auth { final class Service { public static function validate_password(string $password, string $confirm = ''): true|string { if (strlen($password) < 6) return 'password too short'; if ($confirm !== '' && $password !== $confirm) return 'password mismatch'; return true; } } } namespace AKSRT\Extensions { final class SendLimit { public static function check(string $target): array { $sent_at = (int) get_transient('aksrt_send_cd_' . md5(strtolower(trim($target)))); if ($sent_at > 0 && time() - $sent_at < 60) { return ['ok' => false, 'message' => 'cooldown', 'remaining' => 60 - (time() - $sent_at)]; } return ['ok' => true, 'message' => '', 'remaining' => 0]; } public static function record(string $target): int { set_transient('aksrt_send_cd_' . md5(strtolower(trim($target))), time(), 60); return 60; } } final class SecurityHardening { public static function error_frequency_wait(): int { $state = get_transient('aksrt_err_freq_test'); $count = is_array($state) ? (int) ($state['count'] ?? 0) : 0; if ($count < 5) return 0; return max(0, ($count >= 10 ? 180 : 30) - (time() - (int) ($state['last'] ?? 0))); } public static function error_frequency_record(): void { $state = get_transient('aksrt_err_freq_test'); $count = is_array($state) ? (int) ($state['count'] ?? 0) + 1 : 1; set_transient('aksrt_err_freq_test', ['count' => $count, 'last' => time()], 600); } public static function error_frequency_clear(): void { delete_transient('aksrt_err_freq_test'); } } } namespace { $GLOBALS['aksrt_account_user'] = new WP_User(7, 'old@example.test', 'Test User', 'hash:OldPass123'); $send_action = 'wp_ajax_aksrt_send_account_email_code'; $verify_action = 'wp_ajax_aksrt_verify_account_email'; $change_email_action = 'wp_ajax_aksrt_change_email'; $change_password_action = 'wp_ajax_aksrt_change_password'; $saved_user = $GLOBALS['aksrt_account_user']; $GLOBALS['aksrt_account_user'] = null; account_settings_check(empty(account_settings_request($send_action, ['scene' => 'verify_email'])['success']), 'email-code request did not handle a missing user object'); account_settings_check(empty(account_settings_request($verify_action, ['code' => '123456'])['success']), 'old-email verification did not handle a missing user object'); account_settings_check(empty(account_settings_request($change_email_action, ['new_email' => 'new@example.test'])['success']), 'email update did not handle a missing user object'); account_settings_check(empty(account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123'])['success']), 'password update did not handle a missing user object'); $GLOBALS['aksrt_account_user'] = $saved_user; $old_send = account_settings_request($send_action, ['scene' => 'verify_email']); account_settings_check(!empty($old_send['success']), 'old email verification code could not be sent'); account_settings_check($old_send['data']['countdown'] === 60, 'old email code send did not surface the cooldown countdown'); $cooldown_send = account_settings_request($send_action, ['scene' => 'verify_email']); account_settings_check(empty($cooldown_send['success']), 'email code resend was not blocked by the send cooldown'); $old_code = account_settings_code('verify_email', 'old@example.test'); $bad_old_code = account_settings_request($verify_action, ['code' => 'wrong']); account_settings_check(empty($bad_old_code['success']), 'email verification accepted an incorrect old-email code'); $old_code_row = get_transient(account_settings_code_key('verify_email', 'old@example.test')); account_settings_check(($old_code_row['attempts'] ?? 0) === 1, 'incorrect old-email attempt was not recorded'); $GLOBALS['aksrt_account_transients'][account_settings_code_key('verify_email', 'old@example.test')]['expires'] = time() - 1; $expired_old_code = account_settings_request($verify_action, ['code' => $old_code]); account_settings_check(empty($expired_old_code['success']), 'email verification accepted an expired old-email code'); account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'expired old-email code was not discarded'); account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired old-email code issued an old-email proof'); delete_transient(account_settings_send_cooldown_key('old@example.test')); $old_send = account_settings_request($send_action, ['scene' => 'verify_email']); account_settings_check(!empty($old_send['success']), 'old email code could not be resent after expiry'); $old_code = account_settings_code('verify_email', 'old@example.test'); $verified_old = account_settings_request($verify_action, ['code' => $old_code]); account_settings_check(!empty($verified_old['success']), 'email verification rejected the correct old-email code'); account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'old-email proof was not issued'); account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'valid old-email code was not consumed'); $new_email = 'new@example.test'; $new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]); account_settings_check(!empty($new_send['success']), 'new email verification code could not be sent'); $new_code = account_settings_code('change_email', $new_email); $GLOBALS['aksrt_account_existing_emails']['taken@example.test'] = 19; $duplicate = account_settings_request($change_email_action, ['new_email' => 'taken@example.test', 'code' => $new_code, 'agree_terms' => '1']); account_settings_check(empty($duplicate['success']), 'email update accepted an address already owned by another account'); account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'duplicate-email validation consumed the old-email proof'); account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'duplicate-email validation consumed the new-email code'); unset($GLOBALS['aksrt_account_existing_emails']['taken@example.test']); $bad_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => 'wrong', 'agree_terms' => '1']); account_settings_check(empty($bad_new_code['success']), 'email update accepted an incorrect new-email code'); account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'incorrect-code validation consumed the old-email proof'); account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'incorrect-code validation consumed the new-email code'); $GLOBALS['aksrt_account_transients'][account_settings_code_key('change_email', $new_email)]['expires'] = time() - 1; $expired_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']); account_settings_check(empty($expired_new_code['success']), 'email update accepted an expired new-email code'); account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired new-email code validation consumed the old-email proof'); account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'expired new-email code was not discarded'); delete_transient(account_settings_send_cooldown_key($new_email)); $new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]); account_settings_check(!empty($new_send['success']), 'new email code could not be resent after expiry'); $new_code = account_settings_code('change_email', $new_email); $GLOBALS['aksrt_account_update_fail'] = true; $write_failure = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']); account_settings_check(empty($write_failure['success']), 'email update reported success after the user write failed'); account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'failed database write consumed the old-email proof'); account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'failed database write consumed the new-email code'); $GLOBALS['aksrt_account_update_fail'] = false; $changed = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']); account_settings_check(!empty($changed['success']), 'email update rejected valid old and new proofs'); account_settings_check($GLOBALS['aksrt_account_user']->user_email === $new_email, 'email update did not persist the new address'); account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'successful update retained the old-email proof'); account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'successful update retained the new-email code'); account_settings_check((int) get_user_meta(7, 'aksrt_email_verified', true) === 1, 'successful update did not mark the email verified'); account_settings_check(end($GLOBALS['aksrt_account_mail'])['event'] === 'change_notice', 'successful update did not notify the previous email'); $GLOBALS['aksrt_account_settings']['email_set_captch'] = false; $direct_email = account_settings_request($change_email_action, ['new_email' => 'direct@example.test', 'agree_terms' => '1']); account_settings_check(!empty($direct_email['success']), 'email update without configured verification still required a code'); account_settings_check($GLOBALS['aksrt_account_user']->user_email === 'direct@example.test', 'direct email update did not persist'); $GLOBALS['aksrt_account_settings']['email_set_captch'] = true; $GLOBALS['aksrt_account_user']->user_email = ''; $first_email = 'first@example.test'; $first_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $first_email]); account_settings_check(!empty($first_send['success']), 'first email binding did not send a new-address code'); $first_code = account_settings_code('change_email', $first_email); $first_bind = account_settings_request($change_email_action, ['new_email' => $first_email, 'code' => $first_code, 'agree_terms' => '1']); account_settings_check(!empty($first_bind['success']), 'first email binding incorrectly required old-email verification'); account_settings_check($GLOBALS['aksrt_account_user']->user_email === $first_email, 'first email binding did not persist'); $GLOBALS['aksrt_account_captcha_fail'] = true; $captcha_rejected = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']); account_settings_check(empty($captcha_rejected['success']), 'password change bypassed human verification'); account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:OldPass123', 'captcha rejection changed the password'); $GLOBALS['aksrt_account_captcha_fail'] = false; for ($attempt = 0; $attempt < 8; $attempt++) { $wrong_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'wrong']); account_settings_check(empty($wrong_password['success']), 'password change accepted an incorrect old password'); } $limited_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']); account_settings_check(empty($limited_password['success']), 'password change did not apply its old-password attempt limit'); delete_transient(account_settings_error_frequency_key()); $changed_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']); account_settings_check(!empty($changed_password['success']), 'password change rejected the correct old password'); account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:NewPass123', 'password change did not persist the new password'); account_settings_check(get_transient(account_settings_error_frequency_key()) === false, 'successful password change did not reset its error limit'); update_user_meta(7, 'oauth_new', 1); $oauth_password = account_settings_request($change_password_action, ['password' => 'OAuthPass123', 'password2' => 'OAuthPass123']); account_settings_check(!empty($oauth_password['success']), 'OAuth first-time password setup incorrectly required an old password'); account_settings_check(get_user_meta(7, 'oauth_new', true) === '', 'OAuth first-time password setup retained the new-account marker'); echo "Account settings harness passed\n"; }