diff --git a/inc/checkout/class-cart.php b/inc/checkout/class-cart.php index 7a5f54db..1e2ab213 100644 --- a/inc/checkout/class-cart.php +++ b/inc/checkout/class-cart.php @@ -261,6 +261,18 @@ class Cart implements \JsonSerializable { * @param array $args An array containing the cart arguments. */ public function __construct($args) { + /* + * Guard against instantiation in sovereign tenant context. + * Checkout should only run on the main site. + */ + if (defined('WU_MT_SOVEREIGN_TENANT') && WU_MT_SOVEREIGN_TENANT) { + $this->errors = new \WP_Error( + 'sovereign_checkout_disabled', + __('Checkout is disabled in sovereign tenant context.', 'ultimate-multisite') + ); + return; + } + /* * Why are we using shortcode atts, you might ask? * @@ -854,7 +866,6 @@ protected function build_from_membership($membership_id): bool { ]; if (in_array($membership->get_status(), $inactive_statuses, true)) { - $this->cart_type = 'reactivation'; /* diff --git a/inc/checkout/class-checkout-pages.php b/inc/checkout/class-checkout-pages.php index 27c1e06a..8acbf700 100644 --- a/inc/checkout/class-checkout-pages.php +++ b/inc/checkout/class-checkout-pages.php @@ -130,6 +130,58 @@ public function init(): void { add_action('save_post', [$this, 'handle_compat_mode_setting']); } + + /* + * In sovereign tenant context, redirect checkout URLs to the main site. + */ + if (defined('WU_MT_SOVEREIGN_TENANT') && WU_MT_SOVEREIGN_TENANT) { + add_filter('post_type_link', [$this, 'redirect_checkout_urls_in_sovereign_context'], 10, 2); + add_filter('page_link', [$this, 'redirect_checkout_urls_in_sovereign_context'], 10, 2); + } + } + + /** + * Gets the main site checkout URL. + * + * Used to redirect sovereign tenants to the main site for checkout. + * + * @since 2.5.2 + * @return string The main site checkout URL. + */ + public function get_main_site_checkout_url(): string { + + $main_site = get_blog_details(get_network()->site_id); + + if (! $main_site) { + return network_site_url('/register/'); + } + + return trailingslashit($main_site->siteurl) . 'register/'; + } + + /** + * Redirects checkout URLs to the main site in sovereign tenant context. + * + * @since 2.5.2 + * + * @param string $permalink The post permalink. + * @param \WP_Post $post The post object. + * @return string The modified permalink. + */ + public function redirect_checkout_urls_in_sovereign_context($permalink, $post) { + + if (! is_a($post, '\WP_Post')) { + return $permalink; + } + + $signup_pages = $this->get_signup_pages(); + + // Check if this post is a checkout-related page + if (in_array($post->ID, array_filter($signup_pages), true)) { + return $this->get_main_site_checkout_url(); + } + + return $permalink; } /** @@ -425,7 +477,6 @@ public function replace_reset_password_link($message, $key, $user_login, $user_d ); $new_url = set_url_scheme($new_url, null); - } else { /* diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index 8da7ea56..6ea2fc09 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -232,6 +232,25 @@ public function init(): void { add_action('wu_checkout_errors', [$this, 'maybe_display_checkout_errors']); } + /** + * Gets the main site checkout URL. + * + * Used to redirect sovereign tenants to the main site for checkout. + * + * @since 2.5.2 + * @return string The main site checkout URL. + */ + public function get_main_site_checkout_url(): string { + + $main_site = get_blog_details(get_network()->site_id); + + if (! $main_site) { + return network_site_url('/register/'); + } + + return trailingslashit($main_site->siteurl) . 'register/'; + } + /** * Add checkout rewrite rules. * @@ -563,6 +582,19 @@ public function get_auto_submittable_fields() { */ public function maybe_handle_order_submission(): void { + if (defined('WU_MT_SOVEREIGN_TENANT') && WU_MT_SOVEREIGN_TENANT) { + wp_send_json_error( + [ + 'code' => 'sovereign_checkout_disabled', + 'message' => __('Checkout runs on the main site.', 'ultimate-multisite'), + 'main_site_url' => $this->get_main_site_checkout_url(), + ], + 400 + ); + + return; + } + $this->setup_checkout(); check_ajax_referer('wu_checkout'); @@ -750,15 +782,17 @@ public function process_order() { $existing_customer = wu_get_current_customer(); if ($existing_customer) { - $active_memberships = wu_get_memberships([ - 'customer_id' => $existing_customer->get_id(), - 'status__in' => [ - Membership_Status::ACTIVE, - Membership_Status::TRIALING, - Membership_Status::ON_HOLD, - ], - 'number' => 1, - ]); + $active_memberships = wu_get_memberships( + [ + 'customer_id' => $existing_customer->get_id(), + 'status__in' => [ + Membership_Status::ACTIVE, + Membership_Status::TRIALING, + Membership_Status::ON_HOLD, + ], + 'number' => 1, + ] + ); if ( ! empty($active_memberships)) { $existing_membership = reset($active_memberships); @@ -1838,6 +1872,19 @@ public function validate_form(): void { */ public function create_order(): void { + if (defined('WU_MT_SOVEREIGN_TENANT') && WU_MT_SOVEREIGN_TENANT) { + wp_send_json_error( + [ + 'code' => 'sovereign_checkout_disabled', + 'message' => __('Checkout runs on the main site.', 'ultimate-multisite'), + 'main_site_url' => $this->get_main_site_checkout_url(), + ], + 400 + ); + + return; + } + $this->setup_checkout(); // Set billing address to be used on the order @@ -1898,6 +1945,19 @@ public function create_order(): void { */ public function check_user_exists(): void { + if (defined('WU_MT_SOVEREIGN_TENANT') && WU_MT_SOVEREIGN_TENANT) { + wp_send_json_error( + [ + 'code' => 'sovereign_checkout_disabled', + 'message' => __('Checkout runs on the main site.', 'ultimate-multisite'), + 'main_site_url' => $this->get_main_site_checkout_url(), + ], + 400 + ); + + return; + } + check_ajax_referer('wu_checkout'); $field_type = wu_request('field_type'); @@ -1950,6 +2010,19 @@ public function check_user_exists(): void { */ public function handle_inline_login(): void { + if (defined('WU_MT_SOVEREIGN_TENANT') && WU_MT_SOVEREIGN_TENANT) { + wp_send_json_error( + [ + 'code' => 'sovereign_checkout_disabled', + 'message' => __('Checkout runs on the main site.', 'ultimate-multisite'), + 'main_site_url' => $this->get_main_site_checkout_url(), + ], + 400 + ); + + return; + } + check_ajax_referer('wu_checkout'); $username_or_email = sanitize_text_field(wu_request('username_or_email')); @@ -2091,15 +2164,15 @@ public function get_checkout_variables() { /* translators: %s: field label */ 'field_invalid_email' => __('%s must be a valid email address.', 'ultimate-multisite'), /* translators: 1: field label, 2: minimum character count */ - 'field_min_length' => __('%s must be at least %d characters.', 'ultimate-multisite'), + 'field_min_length' => __('%1$s must be at least %2$d characters.', 'ultimate-multisite'), /* translators: 1: field label, 2: maximum character count */ - 'field_max_length' => __('%s must not exceed %d characters.', 'ultimate-multisite'), + 'field_max_length' => __('%1$s must not exceed %2$d characters.', 'ultimate-multisite'), /* translators: %s: field label */ 'field_alpha_dash' => __('%s may only contain letters, numbers, dashes, and underscores.', 'ultimate-multisite'), /* translators: %s: field label */ 'field_lowercase' => __('%s must be lowercase.', 'ultimate-multisite'), /* translators: 1: field label, 2: other field label */ - 'field_same' => __('%s must match %s.', 'ultimate-multisite'), + 'field_same' => __('%1$s must match %2$s.', 'ultimate-multisite'), /* translators: %s: field label */ 'field_integer' => __('%s must be a whole number.', 'ultimate-multisite'), /* translators: %s: field label */ diff --git a/inc/ui/class-checkout-element.php b/inc/ui/class-checkout-element.php index bb4e43fc..8a612df3 100644 --- a/inc/ui/class-checkout-element.php +++ b/inc/ui/class-checkout-element.php @@ -698,6 +698,18 @@ public function inject_inline_auto_submittable_field($auto_submittable_field): v */ public function output($atts, $content = null) { + // In sovereign tenant context, render a link to the main site checkout instead + if (defined('WU_MT_SOVEREIGN_TENANT') && WU_MT_SOVEREIGN_TENANT) { + $checkout_pages = \WP_Ultimo\Checkout\Checkout_Pages::get_instance(); + $main_site_url = $checkout_pages->get_main_site_checkout_url(); + ?> + + + + apply_filters('wu_membership_update_form', 'wu-checkout'), diff --git a/tests/WP_Ultimo/Checkout/Checkout_Test.php b/tests/WP_Ultimo/Checkout/Checkout_Test.php index 0cca688c..fa2a3211 100644 --- a/tests/WP_Ultimo/Checkout/Checkout_Test.php +++ b/tests/WP_Ultimo/Checkout/Checkout_Test.php @@ -5340,6 +5340,150 @@ public function test_cleanup_expired_drafts_cleans_up_pending_site(): void { $found_membership->delete(); } + // ------------------------------------------------------------------------- + // Sovereign Tenant Checkout Guards + // ------------------------------------------------------------------------- + + /** + * Test that create_order returns error in sovereign tenant context. + */ + public function test_create_order_returns_error_in_sovereign_context(): void { + + // Define the sovereign tenant constant + if (!defined('WU_MT_SOVEREIGN_TENANT')) { + define('WU_MT_SOVEREIGN_TENANT', true); + } + + $checkout = Checkout::get_instance(); + + // Mock the AJAX request + $_POST['wu_nonce'] = wp_create_nonce('wu_checkout'); + + // Capture the JSON response + ob_start(); + $checkout->create_order(); + $response = ob_get_clean(); + + $data = json_decode($response, true); + + $this->assertFalse($data['success']); + $this->assertEquals('sovereign_checkout_disabled', $data['data']['code']); + $this->assertStringContainsString('main site', $data['data']['message']); + $this->assertNotEmpty($data['data']['main_site_url']); + } + + /** + * Test that maybe_handle_order_submission returns error in sovereign tenant context. + */ + public function test_maybe_handle_order_submission_returns_error_in_sovereign_context(): void { + + // Define the sovereign tenant constant + if (!defined('WU_MT_SOVEREIGN_TENANT')) { + define('WU_MT_SOVEREIGN_TENANT', true); + } + + $checkout = Checkout::get_instance(); + + // Mock the AJAX request + $_POST['wu_nonce'] = wp_create_nonce('wu_checkout'); + + // Capture the JSON response + ob_start(); + $checkout->maybe_handle_order_submission(); + $response = ob_get_clean(); + + $data = json_decode($response, true); + + $this->assertFalse($data['success']); + $this->assertEquals('sovereign_checkout_disabled', $data['data']['code']); + } + + /** + * Test that check_user_exists returns error in sovereign tenant context. + */ + public function test_check_user_exists_returns_error_in_sovereign_context(): void { + + // Define the sovereign tenant constant + if (!defined('WU_MT_SOVEREIGN_TENANT')) { + define('WU_MT_SOVEREIGN_TENANT', true); + } + + $checkout = Checkout::get_instance(); + + // Mock the AJAX request + $_POST['wu_nonce'] = wp_create_nonce('wu_checkout'); + $_POST['field_type'] = 'email'; + $_POST['value'] = 'test@example.com'; + + // Capture the JSON response + ob_start(); + $checkout->check_user_exists(); + $response = ob_get_clean(); + + $data = json_decode($response, true); + + $this->assertFalse($data['success']); + $this->assertEquals('sovereign_checkout_disabled', $data['data']['code']); + } + + /** + * Test that handle_inline_login returns error in sovereign tenant context. + */ + public function test_handle_inline_login_returns_error_in_sovereign_context(): void { + + // Define the sovereign tenant constant + if (!defined('WU_MT_SOVEREIGN_TENANT')) { + define('WU_MT_SOVEREIGN_TENANT', true); + } + + $checkout = Checkout::get_instance(); + + // Mock the AJAX request + $_POST['wu_nonce'] = wp_create_nonce('wu_checkout'); + $_POST['username_or_email'] = 'test@example.com'; + $_POST['password'] = 'password123'; + + // Capture the JSON response + ob_start(); + $checkout->handle_inline_login(); + $response = ob_get_clean(); + + $data = json_decode($response, true); + + $this->assertFalse($data['success']); + $this->assertEquals('sovereign_checkout_disabled', $data['data']['code']); + } + + /** + * Test that Cart constructor sets error in sovereign tenant context. + */ + public function test_cart_constructor_sets_error_in_sovereign_context(): void { + + // Define the sovereign tenant constant + if (!defined('WU_MT_SOVEREIGN_TENANT')) { + define('WU_MT_SOVEREIGN_TENANT', true); + } + + $cart = new Cart([ + 'products' => [], + ]); + + $this->assertTrue(is_wp_error($cart->errors)); + $this->assertEquals('sovereign_checkout_disabled', $cart->errors->get_error_code()); + } + + /** + * Test that get_main_site_checkout_url returns valid URL. + */ + public function test_get_main_site_checkout_url_returns_valid_url(): void { + + $checkout = Checkout::get_instance(); + $url = $checkout->get_main_site_checkout_url(); + + $this->assertNotEmpty($url); + $this->assertStringContainsString('register', $url); + } + // ------------------------------------------------------------------------- // Teardown // -------------------------------------------------------------------------