From 011c1c4bf4f657a018ec1bd3075bd63c2fe5c6a3 Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 13 Jun 2026 00:30:18 -0600 Subject: [PATCH] fix: harden checkout thank-you flow --- inc/checkout/class-checkout.php | 26 ++++++++++++++-- inc/models/class-membership.php | 7 +++++ tests/WP_Ultimo/Checkout/Checkout_Test.php | 35 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index b835a17e..51a19d86 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -3383,10 +3383,14 @@ public function request_or_session($key, $default_value = false) { */ public function get_next_step_name() { - $steps = $this->steps; + $steps = $this->get_steps_or_empty_array(); $keys = array_column($steps, 'id'); + if (empty($keys)) { + return $this->step_name; + } + $current_step_index = array_search($this->step_name, array_values($keys), true); /* @@ -3411,7 +3415,7 @@ public function get_next_step_name() { */ public function is_first_step() { - $step_names = array_column($this->steps, 'id'); + $step_names = array_column($this->get_steps_or_empty_array(), 'id'); if (empty($step_names)) { return true; @@ -3449,7 +3453,7 @@ public function is_last_step() { return false; } - $step_names = array_column($this->steps, 'id'); + $step_names = array_column($this->get_steps_or_empty_array(), 'id'); if (empty($step_names)) { return true; @@ -3458,6 +3462,22 @@ public function is_last_step() { return array_pop($step_names) === $this->step_name; } + /** + * Returns checkout steps as an array. + * + * Payment return and thank-you requests can enqueue checkout scripts after the + * checkout form context has been cleared, leaving the public steps property + * unset/null. Treat that state as an empty one-step flow instead of fataling + * when navigation helpers call array_column(). + * + * @since 2.13.2 + * @return array + */ + protected function get_steps_or_empty_array() { + + return is_array($this->steps) ? $this->steps : []; + } + /** * Decides if we should display errors on the checkout screen. * diff --git a/inc/models/class-membership.php b/inc/models/class-membership.php index ddc48ea0..44827208 100644 --- a/inc/models/class-membership.php +++ b/inc/models/class-membership.php @@ -2128,6 +2128,13 @@ public function publish_pending_site_async(): void { $use_loopback = (bool) apply_filters('wu_publish_pending_site_use_loopback', true, $this); $can_finish_request = function_exists('litespeed_finish_request') || function_exists('fastcgi_finish_request'); + $server_software = isset($_SERVER['SERVER_SOFTWARE']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_SOFTWARE'])) : ''; + $is_frankenphp = 'frankenphp' === PHP_SAPI || false !== stripos($server_software, 'frankenphp'); + + if ($is_frankenphp) { + $can_finish_request = false; + } + $can_finish_request = (bool) apply_filters('wu_publish_pending_site_can_finish_request', $can_finish_request, $this); $loopback_started = false; $args = ['membership_id' => $this->get_id()]; diff --git a/tests/WP_Ultimo/Checkout/Checkout_Test.php b/tests/WP_Ultimo/Checkout/Checkout_Test.php index b8801263..1a0cf5e1 100644 --- a/tests/WP_Ultimo/Checkout/Checkout_Test.php +++ b/tests/WP_Ultimo/Checkout/Checkout_Test.php @@ -535,6 +535,17 @@ public function test_is_first_step_empty_steps(): void { $this->assertTrue($checkout->is_first_step()); } + /** + * Test is_first_step with null steps. + */ + public function test_is_first_step_null_steps(): void { + + $checkout = Checkout::get_instance(); + $checkout->steps = null; + + $this->assertTrue($checkout->is_first_step()); + } + /** * Test is_first_step when on first step. */ @@ -624,6 +635,18 @@ public function test_is_last_step_empty_steps(): void { $this->assertTrue($checkout->is_last_step()); } + /** + * Test is_last_step with null steps returns true. + */ + public function test_is_last_step_null_steps(): void { + + $checkout = Checkout::get_instance(); + $checkout->steps = null; + $checkout->step_name = null; + + $this->assertTrue($checkout->is_last_step()); + } + /** * Test is_last_step returns false when pre-flight param is set. */ @@ -704,6 +727,18 @@ public function test_get_next_step_name_no_current_step(): void { $this->assertEquals('step-2', $checkout->get_next_step_name()); } + /** + * Test get_next_step_name with null steps returns current step. + */ + public function test_get_next_step_name_null_steps_returns_current(): void { + + $checkout = Checkout::get_instance(); + $checkout->steps = null; + $checkout->step_name = 'thank-you'; + + $this->assertEquals('thank-you', $checkout->get_next_step_name()); + } + /** * Test get_next_step_name from middle step. */