Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class Account_Admin_Page extends Base_Customer_Facing_Admin_Page {
protected $current_customer;

/**
* The return_to URL for sovereign-tenant context.
* The return_to URL for external redirects.
*
* @since 2.0.0
* @var string|null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class Add_New_Site_Admin_Page extends Base_Customer_Facing_Admin_Page {
protected $current_membership;

/**
* The return_to URL for sovereign-tenant context.
* The return_to URL for external redirects.
*
* @since 2.0.0
* @var string|null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class Checkout_Admin_Page extends \WP_Ultimo\Admin_Pages\Base_Customer_Facing_Ad
protected $fold_menu = true;

/**
* The return_to URL for sovereign-tenant context.
* The return_to URL for external redirects.
*
* @since 2.0.0
* @var string|null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class My_Sites_Admin_Page extends Base_Customer_Facing_Admin_Page {
public $current_membership;

/**
* The return_to URL for sovereign-tenant context.
* The return_to URL for external redirects.
*
* @since 2.0.0
* @var string|null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class Template_Switching_Admin_Page extends \WP_Ultimo\Admin_Pages\Base_Customer
protected $menu_settings = false;

/**
* The return_to URL for sovereign-tenant context.
* The return_to URL for external redirects.
*
* @since 2.0.0
* @var string|null
Expand Down Expand Up @@ -166,7 +166,7 @@ public function output(): void {
*
* phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display flag, no state change.
*/
$action = isset($_GET['wu_template_action']) ? sanitize_key(wp_unslash($_GET['wu_template_action'])) : '';
$action = isset($_GET['wu_template_action']) ? sanitize_key(wp_unslash($_GET['wu_template_action'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display flag, no state change.
$message = 'reset' === $action
? __('Template reset successfully!', 'ultimate-multisite')
: __('Template switched successfully!', 'ultimate-multisite');
Expand Down
17 changes: 4 additions & 13 deletions inc/checkout/class-cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,19 +261,6 @@ 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 (wu_is_sovereign_tenant()) {
$this->attributes = (object) [];
$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?
*
Expand Down Expand Up @@ -364,6 +351,10 @@ public function __construct($args) {
*/
$this->attributes = (object) $args;

if (apply_filters('wu_cart_skip_initialization', false, $args, $this)) {
return;
}
Comment on lines +354 to +356
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skipped carts still validate as usable.

If this filter returns true, the constructor exits with $this->errors empty and no cart state built. Cart::is_valid() will still return true, so Checkout::process_order() can keep going with a zero-item cart instead of stopping the flow.

💡 Suggested fix
-		if (apply_filters('wu_cart_skip_initialization', false, $args, $this)) {
+		$skip_initialization = apply_filters('wu_cart_skip_initialization', false, $args, $this);
+
+		if ($skip_initialization) {
+			if ($skip_initialization instanceof \WP_Error) {
+				$this->errors->merge_from($skip_initialization);
+			} elseif ( ! $this->errors->has_errors()) {
+				$this->errors->add('cart_initialization_skipped', __('This checkout is not available.', 'ultimate-multisite'));
+			}
+
 			return;
 		}

As per coding guidelines, "Use WP_Error for validation/operation failures instead of exceptions; check return values with is_wp_error()".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (apply_filters('wu_cart_skip_initialization', false, $args, $this)) {
return;
}
$skip_initialization = apply_filters('wu_cart_skip_initialization', false, $args, $this);
if ($skip_initialization) {
if ($skip_initialization instanceof \WP_Error) {
$this->errors->merge_from($skip_initialization);
} elseif ( ! $this->errors->has_errors()) {
$this->errors->add('cart_initialization_skipped', __('This checkout is not available.', 'ultimate-multisite'));
}
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/checkout/class-cart.php` around lines 354 - 356, The constructor
early-return when apply_filters('wu_cart_skip_initialization', ...) leaves
$this->errors empty so Cart::is_valid() still returns true; instead, when the
filter returns true populate $this->errors with a WP_Error (e.g. 'cart_skipped'
code and a short message) in the same block before returning so downstream
callers like Checkout::process_order() can detect the failure via
is_wp_error()/is_valid checks; modify the block around the apply_filters call in
the Cart class constructor (same scope as the existing apply_filters line) to
set a WP_Error on $this->errors and then return.


/**
* Allow developers to make additional changes to
* the checkout object.
Expand Down
40 changes: 5 additions & 35 deletions inc/checkout/class-checkout-pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,44 +131,20 @@ 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 (wu_is_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/';
add_filter('post_type_link', [$this, 'filter_checkout_urls'], 10, 2);
add_filter('page_link', [$this, 'filter_checkout_urls'], 10, 2);
Comment on lines +134 to +135
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

WordPress hook reference: what arguments do the page_linkandpost_type_linkfilters pass to callbacks, and doespage_linkpass a page ID or aWP_Post object as its second parameter?

💡 Result:

  1. page_link filter: arguments passed to callbacks WordPress applies the page permalink with apply_filters( 'page_link', $link, $post->ID, $sample). That means your callback receives 3 arguments: (1) $link (string) the page’s permalink, (2) $post_id (int) the ID of the page, and (3) $sample (bool) whether it’s a sample permalink. [1][2] 2) post_type_link filter: arguments passed to callbacks WordPress applies the custom post type permalink with apply_filters( 'post_type_link', $post_link, $post, $leavename, $sample). That means your callback receives 4 arguments: (1) $post_link (string) the post’s permalink, (2) $post (WP_Post) the post object, (3) $leavename (bool), and (4) $sample (bool). [3][4] Does page_link pass a page ID or a WP_Post object as its second parameter? It passes the page ID (an int) as the second parameter ($post_id), not a WP_Post object. This is explicit in both the hook reference for page_link and in get_page_link where the filter is invoked with $post->ID as the second argument. [1][2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="inc/checkout/class-checkout-pages.php"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "==> $FILE (lines 110-190)"
nl -ba "$FILE" | sed -n '110,190p'

Repository: Ultimate-Multisite/ultimate-multisite

Length of output: 201


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="inc/checkout/class-checkout-pages.php"

awk 'NR>=110 && NR<=190 {printf "%d\t%s\n", NR, $0}' "$FILE"

Repository: Ultimate-Multisite/ultimate-multisite

Length of output: 2541


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="inc/checkout/class-checkout-pages.php"

rg -n "function\s+get_signup_pages\s*\(" "$FILE"

Repository: Ultimate-Multisite/ultimate-multisite

Length of output: 123


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="inc/checkout/class-checkout-pages.php"
awk 'NR>=960 && NR<=1040 {printf "%d\t%s\n", NR, $0}' "$FILE"

Repository: Ultimate-Multisite/ultimate-multisite

Length of output: 2502


Fix filter_checkout_urls() to support page_link’s callback argument type

page_link passes the page ID (int) as the 2nd argument, but filter_checkout_urls() immediately returns unless the 2nd argument is a \WP_Post. Since get_signup_pages() returns page IDs for signup/login/update/etc., the wu_checkout_pages_checkout_url filter never runs on the normal page permalink path.

💡 Suggested fix
 	public function filter_checkout_urls($permalink, $post) {
+		if (is_numeric($post)) {
+			$post = get_post((int) $post);
+		}
 
 		if (! is_a($post, '\WP_Post')) {
 			return $permalink;
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/checkout/class-checkout-pages.php` around lines 134 - 135,
filter_checkout_urls() currently bails unless the second argument is a \WP_Post,
but the page_link hook passes an int ID; update filter_checkout_urls() to accept
either a \WP_Post or an int by detecting when $post (or the second parameter
name) is numeric and calling get_post($post) to retrieve the WP_Post object
before proceeding, then continue using get_signup_pages() as before so the
wu_checkout_pages_checkout_url filter runs for normal page permalinks too.

}

/**
* Redirects checkout URLs to the main site in sovereign tenant context.
* Filters checkout URLs.
*
* @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) {
public function filter_checkout_urls($permalink, $post) {

if (! is_a($post, '\WP_Post')) {
return $permalink;
Expand All @@ -178,7 +154,7 @@ public function redirect_checkout_urls_in_sovereign_context($permalink, $post) {

// 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 apply_filters('wu_checkout_pages_checkout_url', $permalink, $post, $this);
}

return $permalink;
Expand Down Expand Up @@ -599,8 +575,6 @@ protected function rewrite_subsite_aware_login_url($url) {
public function rewrite_new_user_notification_email($email, $user, $blogname) {
unset($user, $blogname);

unset($user, $blogname);

if (empty($email['message']) || ! is_array($email)) {
return $email;
}
Expand Down Expand Up @@ -634,8 +608,6 @@ function ($matches) {
public function rewrite_password_notification_email($defaults, $key, $user_login, $user_data) {
unset($key, $user_login, $user_data);

unset($key, $user_login, $user_data);

if (empty($defaults['message']) || ! is_array($defaults)) {
return $defaults;
}
Expand Down Expand Up @@ -669,8 +641,6 @@ function ($matches) {
public function rewrite_email_change_content($email_text, $new_user_email) {
unset($new_user_email);

unset($new_user_email);

if (empty($email_text) || ! is_string($email_text)) {
return $email_text;
}
Expand Down
63 changes: 4 additions & 59 deletions inc/checkout/class-checkout.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,25 +232,6 @@ 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.
*
Expand Down Expand Up @@ -582,16 +563,7 @@ public function get_auto_submittable_fields() {
*/
public function maybe_handle_order_submission(): void {

if (wu_is_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
);

if (apply_filters('wu_checkout_skip_order_submission', false, $this)) {
return;
}

Expand Down Expand Up @@ -1876,16 +1848,7 @@ public function validate_form(): void {
*/
public function create_order(): void {

if (wu_is_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
);

if (apply_filters('wu_checkout_skip_create_order', false, $this)) {
return;
}

Expand Down Expand Up @@ -1950,16 +1913,7 @@ public function create_order(): void {
*/
public function check_user_exists(): void {

if (wu_is_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
);

if (apply_filters('wu_checkout_skip_user_exists_check', false, $this)) {
return;
}

Expand Down Expand Up @@ -2015,16 +1969,7 @@ public function check_user_exists(): void {
*/
public function handle_inline_login(): void {

if (wu_is_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
);

if (apply_filters('wu_checkout_skip_inline_login', false, $this)) {
return;
}

Expand Down
1 change: 0 additions & 1 deletion inc/class-wp-ultimo.php
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,6 @@ public function load_public_apis(): void {
require_once wu_path('inc/functions/reflection.php');
require_once wu_path('inc/functions/scheduler.php');
require_once wu_path('inc/functions/session.php');
require_once wu_path('inc/functions/sovereign.php');
require_once wu_path('inc/functions/documentation.php');

/**
Expand Down
2 changes: 1 addition & 1 deletion inc/compat/class-auto-delete-users-compat.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Auto_Delete_Users_Compat {
*/
public function init(): void {

if ( wu_is_sovereign_tenant() ) {
if ( apply_filters('wu_auto_delete_users_skip_compat', false, $this) ) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion inc/compat/class-edit-users-compat.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Edit_Users_Compat {
*/
public function init(): void {

if ( wu_is_sovereign_tenant() ) {
if ( apply_filters('wu_edit_users_skip_compat', false, $this) ) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion inc/compat/class-multiple-accounts-compat.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Multiple_Accounts_Compat {
*/
public function init(): void {

if ( wu_is_sovereign_tenant() ) {
if ( apply_filters('wu_multiple_accounts_skip_compat', false, $this) ) {
return;
}

Expand Down
52 changes: 0 additions & 52 deletions inc/functions/sovereign.php

This file was deleted.

4 changes: 2 additions & 2 deletions inc/managers/class-gateway-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ public function maybe_process_v1_webhooks(): void {
*/
public function process_gateway_confirmations(): void {

if ( wu_is_sovereign_tenant() ) {
if ( apply_filters('wu_gateway_skip_confirmations', false, $this) ) {
return;
}

Expand Down Expand Up @@ -619,7 +619,7 @@ public function get_auto_renewable_gateways() {
*/
public function ajax_check_payment_status(): void {

if ( wu_is_sovereign_tenant() ) {
if ( apply_filters('wu_gateway_skip_payment_status_poll', false, $this) ) {
return;
}

Expand Down
9 changes: 1 addition & 8 deletions inc/ui/class-account-summary-element.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,14 +292,7 @@ public function setup_preview() {
*/
public function output($atts, $content = null) {

if (wu_is_sovereign_tenant()) {
echo wu_get_template_contents( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'elements/sovereign-redirect',
[
'main_site_account_url' => wu_mt_main_site_account_url(),
'element_label' => __('Your account', 'ultimate-multisite'),
]
);
if (apply_filters('wu_account_skip_output', false, $atts, $content, $this)) {
return;
}

Expand Down
Loading
Loading