diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index e47eb9e30..f6aa55c96 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -989,7 +989,11 @@ public function process_order() { * let's check if the user is logged in, * and if not, let's do that. */ - $this->login_customer_after_checkout(); + $login_result = $this->login_customer_after_checkout(); + + if (is_wp_error($login_result)) { + return $login_result; + } /* * Action time. @@ -2402,7 +2406,7 @@ protected function form_has_auto_generate_password(): bool { * just created in this very request and the credential is not available. * * @since 2.6.0 - * @return void + * @return \WP_Error|null */ protected function login_customer_after_checkout() { @@ -2426,7 +2430,24 @@ protected function login_customer_after_checkout() { ); // Sign in the user as if they used the login form. - wp_signon($user_credentials, is_ssl()); + $signed_in_user = wp_signon($user_credentials, is_ssl()); + + if (is_wp_error($signed_in_user)) { + $login_error_message = wp_strip_all_tags($signed_in_user->get_error_message()); + + if (empty($login_error_message)) { + $login_error_message = __('Unknown login error.', 'ultimate-multisite'); + } + + return new \WP_Error( + 'checkout_login_failed', + sprintf( + /* translators: %s is the login failure message returned by WordPress or another plugin. */ + __('We could not log you in automatically during checkout. Please try again or contact support before continuing. Login error: %s', 'ultimate-multisite'), + $login_error_message + ) + ); + } return; } diff --git a/tests/WP_Ultimo/Checkout/Checkout_Test.php b/tests/WP_Ultimo/Checkout/Checkout_Test.php index 155c99963..e3f7ca921 100644 --- a/tests/WP_Ultimo/Checkout/Checkout_Test.php +++ b/tests/WP_Ultimo/Checkout/Checkout_Test.php @@ -5010,7 +5010,7 @@ public function test_login_customer_after_checkout_noop_when_logged_in(): void { $method = $this->get_login_method($reflection); $login_fired = false; - add_action('wp_login', function() use (&$login_fired) { + add_action('wp_login', function () use (&$login_fired) { $login_fired = true; }); @@ -5128,7 +5128,7 @@ public function test_login_customer_after_checkout_with_password_fires_wp_login( $_REQUEST['password'] = $password; $login_fired = false; - add_action('wp_login', function() use (&$login_fired) { + add_action('wp_login', function () use (&$login_fired) { $login_fired = true; }); @@ -5147,6 +5147,69 @@ public function test_login_customer_after_checkout_with_password_fires_wp_login( wp_delete_user($user_id); } + /** + * Test login_customer_after_checkout returns a visible error when wp_signon fails. + * + * Security plugins can hook into the normal WordPress authentication flow and + * reject the credential round-trip. Checkout must stop instead of silently + * handing the customer to a payment gateway while logged out. + */ + public function test_login_customer_after_checkout_with_password_returns_error_when_signon_fails(): void { + + $unique = uniqid('badpw_', true); + $password = 'TestP@ssw0rd!'; + $user_id = self::factory()->user->create([ + 'user_login' => $unique, + 'user_pass' => $password, + 'user_email' => $unique . '@example.com', + ]); + + wp_set_current_user(0); + wp_clear_auth_cookie(); + + $customer = wu_create_customer([ + 'user_id' => $user_id, + 'username' => $unique, + 'email' => $unique . '@example.com', + ]); + + if (is_wp_error($customer)) { + require_once ABSPATH . 'wp-admin/includes/user.php'; + wp_delete_user($user_id); + $this->markTestSkipped('Customer creation failed: ' . $customer->get_error_message()); + } + + $checkout = Checkout::get_instance(); + $reflection = new \ReflectionClass($checkout); + + $this->inject_customer($checkout, $reflection, $customer); + $this->ensure_session($checkout); + + $_REQUEST['password'] = 'WrongP@ssw0rd!'; + + $login_fired = false; + add_action('wp_login', function () use (&$login_fired) { + $login_fired = true; + }); + + $method = $this->get_login_method($reflection); + $result = $method->invoke($checkout); + + remove_all_actions('wp_login'); + unset($_REQUEST['password']); + + $this->assertWPError($result); + $this->assertSame('checkout_login_failed', $result->get_error_code()); + $this->assertStringContainsString('Login error:', $result->get_error_message()); + $this->assertFalse($login_fired, 'wp_login must not fire when wp_signon returns an error'); + + // Cleanup. + wp_set_current_user(0); + $customer->delete(); + require_once ABSPATH . 'wp-admin/includes/user.php'; + wp_delete_user($user_id); + } + /** * Test login_customer_after_checkout handles a customer with user_id = 0 gracefully. *