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
35 changes: 33 additions & 2 deletions inc/class-current.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,33 @@ public function load_currents(): void {
return;
}

/*
* `get_query_var()` reads from the global `$wp_query`, which
* WordPress only populates during `parse_query` (just before
* `wp` fires). The lazy-load entry path added in PR #1118 can
* call `load_currents()` from `Site::is_customer_allowed()` /
* `Membership::is_customer_allowed()` while still on
* `plugins_loaded` — for example when the wu-ajax pipeline
* (`Light_Ajax::process_light_ajax`) dispatches
* `wu_ajax_wu_switch_template` at `plugins_loaded` priority 20
* and the handler reads `is_customer_allowed()`. At that point
* `$wp_query` is still null and `get_query_var()` fatals with
* `Call to a member function get() on null` on PHP 8+, which
* crashes the AJAX handler with a 500 — surfacing in the
* customer-panel template-switching UI as a generic
* "A network error occurred. Please check your connection and
* try again." banner.
*
* Skip the pretty-URL hash overrides when we know the query
* cannot have been parsed yet. The wu-ajax pipeline is a flat
* `?wu-ajax=1` POST with no rewrite-driven `site_hash` /
* `membership_hash` query vars, so there is nothing to lose
* by skipping them here. Frontend pretty-URL requests still
* hit `load_currents()` via the `wp` action where `$wp_query`
* is populated and these reads succeed normally.
*/
$query_vars_ready = did_action('parse_query') && isset($GLOBALS['wp_query']) && $GLOBALS['wp_query'] instanceof \WP_Query;

$site = false;

/**
Expand All @@ -250,7 +277,9 @@ public function load_currents(): void {
*/
$site_url_param = self::param_key('site');

$site_hash = wu_request($site_url_param, get_query_var('site_hash'));
$site_hash_default = $query_vars_ready ? get_query_var('site_hash') : '';

$site_hash = wu_request($site_url_param, $site_hash_default);

$site_from_url = wu_get_site_by_hash($site_hash);

Expand Down Expand Up @@ -296,7 +325,9 @@ public function load_currents(): void {
*/
$membership_url_param = self::param_key('membership');

$membership_hash = wu_request($membership_url_param, get_query_var('membership_hash'));
$membership_hash_default = $query_vars_ready ? get_query_var('membership_hash') : '';

$membership_hash = wu_request($membership_url_param, $membership_hash_default);

if ($membership_hash) {
$this->membership_set_via_request = true;
Expand Down
56 changes: 56 additions & 0 deletions tests/WP_Ultimo/Current_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,62 @@ protected function set_loaded_state($loaded): void {
$property->setValue($this->current, $loaded);
}

/**
* Regression test for the wu-ajax `plugins_loaded` fatal.
*
* The light-ajax pipeline (`Light_Ajax::process_light_ajax`) dispatches
* `wu_ajax_*` handlers at `plugins_loaded` priority 20. The handler for
* `wu_switch_template` calls `Site::is_customer_allowed()`, which (after
* the lazy-load entry from PR #1118) calls `Current::load_currents()`
* before the `wp` action has fired. At that point the global
* `$wp_query` is null, and any `get_query_var()` call inside
* `load_currents()` throws `Call to a member function get() on null`
* on PHP 8+ — surfacing in the customer-panel template-switching UI as
* a generic "A network error occurred" banner because the AJAX call
* died with a 500 instead of returning JSON.
*
* This test temporarily nulls `$GLOBALS['wp_query']` to simulate the
* early-call condition, then asserts `load_currents()` runs to
* completion without fataling. The hash-override branches must be
* skipped via the `$query_vars_ready` guard so the lazy-load can
* still populate the customer/membership fields.
*/
public function test_load_currents_survives_when_wp_query_is_null(): void {

set_current_screen('front');

$user_id = $this->factory()->user->create(['role' => 'subscriber']);

wu_create_customer(
[
'user_id' => $user_id,
'email_address' => 'current-no-wp-query@example.com',
]
);

wp_set_current_user($user_id);

$this->current->set_site(null);
$this->current->set_customer(null);
$this->current->set_membership(null);
$this->set_loaded_state(false);

// Save and null out the global wp_query to simulate the
// plugins_loaded entry path where it has not yet been set up.
$saved_wp_query = $GLOBALS['wp_query'] ?? null;
$GLOBALS['wp_query'] = null;

try {
$this->current->load_currents();

// Sanity assertion — if get_query_var() had been called we
// would have fataled before reaching this line.
$this->assertTrue(true, 'load_currents survived a null $wp_query.');
} finally {
$GLOBALS['wp_query'] = $saved_wp_query;
}
}

/**
* Test param_key returns expected defaults.
*/
Expand Down
Loading