Skip to content

Architectural fix: lazy-load Current::load_currents() so wu-ajax handlers see populated currents #1114

@superdav42

Description

@superdav42

Summary

Current::load_currents() is hooked to the init and wp actions and populates the cached $site / $customer / $membership properties read by WP_Ultimo()->currents->get_*(). The ?wu-ajax=1 light-ajax pipeline (Light_Ajax::process_light_ajax) dispatches its wu_ajax_* action handlers at plugins_loaded priority 20 and calls die() immediately afterwards, so init never fires and the cache stays null.

Every call site that reads WP_Ultimo()->currents->get_customer() or ->get_membership() from inside a light-ajax-reachable code path therefore sees null even when the user IS logged in as a real customer with the right ownership. That has produced two visible bugs already and an audit found three more dormant ones — see the predecessor PR #1113 for the full list and the local fallbacks that were applied site-by-site.

The right long-term fix is to make Current resilient to being read before init: the first time a get_*() accessor is called against a null cache, run load_currents() on demand, then return the populated value.

Why a separate issue

The local fallbacks in #1113 are intentionally minimal and per-call-site — they were the right shape for an urgent customer-visible regression and a clean audit-derived companion fix. But they're band-aids: every new call site that reads currents->get_* from a wu-ajax path has to remember the same pattern, and a future regression won't surface as a clean error.

A lazy-load of Current::load_currents() would eliminate the bug class and let us revert the per-site fallbacks. The blast radius is non-trivial though — every WP_Ultimo()->currents->get_* caller in the entire codebase changes timing characteristics, including third-party addons that hook wu_current_set_* filters. That deserves its own change with deliberate thought, ideally a beta cycle, and ideally a quick scan of the public addon ecosystem for callers that would observe the new firing order.

Files to inspect

  • inc/class-current.phpload_currents() and the get_site() / get_customer() / get_membership() accessors. The lazy-load hook would land in the accessors, gated by a $loaded flag so we don't re-run on every read.
  • inc/class-light-ajax.phpprocess_light_ajax runs at plugins_loaded:20 and calls die(); this is what guarantees init won't fire.
  • inc/models/class-site.phpis_customer_allowed() site-local fallback added in fix(template-switch+light-ajax+stripe+settings): four follow-ups from end-to-end UX validation #1113 that the architectural fix should be able to revert.
  • inc/models/class-membership.php — same.
  • inc/models/class-checkout-form.phpmembership_change_form_fields() and add_new_site_form_fields() site-local fallbacks.
  • inc/managers/class-site-manager.phpmaybe_validate_add_new_site() site-local fallback.

Implementation sketch

// inc/class-current.php
public function get_customer() {
    if ( ! $this->loaded && null === $this->customer) {
        $this->load_currents();
    }
    return $this->customer;
}

load_currents() already short-circuits cleanly — it sets $this->customer regardless of whether the cache was hit, so a second call is idempotent. We'd add a $loaded flag to track first-call to avoid the cost of repeated lookups when the user genuinely has no customer record.

Verification

  • After the lazy-load lands, revert the five site-local fallbacks (Site::is_customer_allowed(), Membership::is_customer_allowed(), Checkout_Form::*_form_fields() x 2, Site_Manager::maybe_validate_add_new_site()).
  • Re-run the regression flow that surfaced this: log in as a customer, hit the customer-panel template-switching page, click Apply on the same template, confirm the AJAX call returns success (not not_authorized).
  • Re-run a delete-site form action from the customer panel; confirm no spurious "not allowed" error.
  • Manual check that any addon hooking wu_current_set_customer / _membership / _site still observes the filter in a sensible order — the filter will now fire later for wu-ajax requests (at first-read instead of at init).

Pattern reference

The fallback pattern used in #1113 mirrors Current::load_currents()'s own heuristic — wu_get_current_customer() for the customer (no Current dependency) and (array) $customer->get_memberships()[0] for the membership. The lazy-load implementation should call the same load_currents() to keep both code paths converged.


aidevops.sh v3.14.72 plugin for OpenCode v1.14.33 with claude-sonnet-4-6 spent 1d 18h on this as a headless worker.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestorigin:workerAuto-created by pulse labelless backfill (t2112)priority:mediumMedium severity — moderate quality issuesolved:workerTask was solved by a headless workerstatus:in-reviewPR open, awaiting review/merge

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions