From 1bf315fd109ed4e83d30fffbafea91956a90387e Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 7 May 2026 13:52:42 -0600 Subject: [PATCH] fix(checkout): send 'set your password' email on auto-generated password signup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a checkout form auto-generates the customer password (the simple preset and any form with the password field's auto_generate_password flag enabled), the previous flow passed the random 16-char password straight to wpmu_create_user(), which silently suppresses the user notification email. The customer was left with a password they did not know and no email telling them how to set one. Now we keep the auto-generated password local to the checkout method and pass an empty value to wu_create_customer(), so the user-creation path falls through to register_new_user(). That fires wp_send_new_user_notifications($user_id, 'both') — the same flow WordPress uses when an admin adds a user without a password from wp-admin/user-new.php — so the customer receives the standard 'set your password' email. After the user exists we apply the generated password via wp_set_password() so the immediate same-request auto-login (login_customer_after_checkout) and any code path that needs a known credential within this request keeps working. The reset-password link in the email remains valid because it is keyed on a fresh activation key, not on the stored password hash. Adds a regression test that asserts the register_new_user action fires for the new user and that the user record ends up with a non-empty password hash. --- inc/checkout/class-checkout.php | 46 +++++++++++- tests/WP_Ultimo/Checkout/Checkout_Test.php | 87 ++++++++++++++++++++++ 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index c43b0160..f6eb470e 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -1136,11 +1136,27 @@ protected function maybe_create_customer() { /* * Resolve the password: use the submitted value, or generate one * when the auto_generate_password flag is present in the session. + * + * When the password is auto-generated (e.g. the simple checkout + * preset), we deliberately leave $password empty in the data + * passed to wu_create_customer() so the user-creation path falls + * through to register_new_user(). That mirrors what WordPress + * does when an admin adds a user without a password from + * wp-admin/user-new.php — the user receives the standard + * "set your password" notification email. Once the user exists + * we apply the auto-generated password via wp_set_password() + * below so the immediate auto-login path still works for the + * rest of this request. */ - $password = $this->request_or_session('password'); + $submitted_password = $this->request_or_session('password'); + $auto_generate_password = (bool) $this->request_or_session('auto_generate_password'); + $generated_password = ''; - if ($this->request_or_session('auto_generate_password')) { - $password = wp_generate_password(16, true, false); + if ($auto_generate_password) { + $generated_password = wp_generate_password(16, true, false); + $password_for_user = ''; // Triggers register_new_user() path with notification email. + } else { + $password_for_user = $submitted_password; } /* @@ -1152,7 +1168,7 @@ protected function maybe_create_customer() { $customer_data = [ 'username' => $username, 'email' => $this->request_or_session('email_address'), - 'password' => $password, + 'password' => $password_for_user, 'email_verification' => $this->get_customer_email_verification_status(), 'signup_form' => $form_slug, 'meta' => [], @@ -1197,6 +1213,28 @@ protected function maybe_create_customer() { if (is_wp_error($customer)) { return $customer; } + + /* + * If the password was auto-generated, apply it to the user we + * just created so the same-request auto-login (see + * login_customer_after_checkout()) and any code that needs + * a known credential within this request keeps working. + * + * The "set your password" notification email has already been + * dispatched by register_new_user() inside wu_create_customer(). + * The reset-password link in that email remains valid because + * it is keyed on the user_login + a fresh activation key, not + * on the user's stored password hash. + * + * @since 2.6.0 + */ + if ($auto_generate_password && $generated_password && ! is_wp_error($customer)) { + $new_user_id = $customer->get_user_id(); + + if ($new_user_id) { + wp_set_password($generated_password, $new_user_id); + } + } } /* diff --git a/tests/WP_Ultimo/Checkout/Checkout_Test.php b/tests/WP_Ultimo/Checkout/Checkout_Test.php index 4e05c81f..0cca688c 100644 --- a/tests/WP_Ultimo/Checkout/Checkout_Test.php +++ b/tests/WP_Ultimo/Checkout/Checkout_Test.php @@ -2945,6 +2945,93 @@ public function test_maybe_create_customer_auto_generate_username_from_email(): unset($_REQUEST['email_address'], $_REQUEST['auto_generate_username'], $_REQUEST['password']); } + /** + * Test maybe_create_customer with auto_generate_password sends the + * standard WordPress "set your password" notification email and applies + * the auto-generated password to the new user record. + * + * Regression test: previously, the auto-generated password was passed + * straight to wpmu_create_user(), which silently suppresses the user + * notification email. Now we route the empty-password path through + * register_new_user() so WP fires wp_send_new_user_notifications() with + * the 'both' flag, then apply the generated password via wp_set_password(). + */ + public function test_maybe_create_customer_auto_generate_password_sends_notification(): void { + + wp_set_current_user(0); + + $checkout = Checkout::get_instance(); + $reflection = new \ReflectionClass($checkout); + $method = $reflection->getMethod('maybe_create_customer'); + + if (PHP_VERSION_ID < 80100) { + $method->setAccessible(true); + } + + $order_prop = $this->get_order_prop($reflection); + $order_prop->setValue($checkout, new Cart(['products' => []])); + + $this->ensure_session($checkout); + + $unique_suffix = time() . '_' . wp_rand(1000, 9999); + $email = 'autopwd_' . $unique_suffix . '@example.com'; + $username = 'autopwd_' . $unique_suffix; + + // Capture wp_new_user_notification firing for the user role. + $notification_fired = false; + $captured_user_id = 0; + $user_callback = static function ($user_id) use (&$notification_fired, &$captured_user_id) { + $notification_fired = true; + $captured_user_id = $user_id; + }; + add_action('register_new_user', $user_callback); + + // Block actual mail delivery during the test. + add_filter('pre_wp_mail', '__return_true', 1); + + $_REQUEST['email_address'] = $email; + $_REQUEST['username'] = $username; + $_REQUEST['auto_generate_password'] = '1'; + $_REQUEST['password'] = ''; + + $result = $method->invoke($checkout); + + // Cleanup hooks/filters before assertions so a failure doesn't leak. + remove_action('register_new_user', $user_callback); + remove_filter('pre_wp_mail', '__return_true', 1); + + if (is_wp_error($result)) { + $this->markTestSkipped('Customer creation failed: ' . $result->get_error_message()); + } + + $this->assertInstanceOf(\WP_Ultimo\Models\Customer::class, $result); + $this->assertTrue( + $notification_fired, + 'register_new_user action should fire so WP sends the "set your password" email when auto_generate_password is set.' + ); + $this->assertSame( + (int) $result->get_user_id(), + (int) $captured_user_id, + 'register_new_user should fire for the same user that the customer record links to.' + ); + + // The user record exists and has a non-empty password hash (set by wp_set_password()). + $user = get_user_by('id', $result->get_user_id()); + $this->assertInstanceOf(\WP_User::class, $user); + $this->assertNotEmpty($user->user_pass, 'Auto-generated password should be applied to the user record.'); + + // Cleanup + wpmu_delete_user($result->get_user_id()); + $result->delete(); + $order_prop->setValue($checkout, null); + unset( + $_REQUEST['email_address'], + $_REQUEST['username'], + $_REQUEST['auto_generate_password'], + $_REQUEST['password'] + ); + } + // ------------------------------------------------------------------------- // maybe_create_site — additional branches // -------------------------------------------------------------------------