From 6ce3970b9433472ddb4124717ed78d535180ddbb Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 30 Apr 2026 03:20:01 -0600 Subject: [PATCH 1/3] GH#1007: fix: initialize Site_Exporter before the requirements/setup gate Move Site_Exporter::get_instance() from the fully-loaded section (~line 584) to before the Requirements::met() / run_setup() early-return (~line 188). This ensures that all Site_Exporter functionality is available even when Ultimate Multisite is not fully configured: - Sites > Export & Import admin menu item is registered - Export/import row actions appear on the WordPress Sites page - Export/import form handlers and download endpoint are hooked - WP Ultimo-specific hooks (wu_export_site, wu_import_site, etc.) are also available for when WP Ultimo IS set up (no change in behaviour) The Singleton trait ensures init() runs exactly once, so hoisting the call is safe even though the fully-loaded boot path no longer calls it a second time. Resolves #1007 --- inc/class-wp-ultimo.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 06459413..b68ca1cb 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -173,6 +173,20 @@ public function init(): void { WP_Ultimo\Newsletter::get_instance(); \WP_Ultimo\Credits::get_instance(); + /* + * Loads the Site Exporter early so export/import is available even when + * Ultimate Multisite is not fully set up (e.g. during migration from + * other multisite solutions). The Site Exporter's WordPress-native + * integration (Sites page row actions, Export & Import admin menu) uses + * only WordPress core functions and has no dependency on WP Ultimo being + * configured. The Singleton trait guarantees init() runs only once even + * when the boot sequence reaches the component a second time. + * + * All helper functions it depends on (wu_request, wu_maybe_create_folder, + * wu_exporter_*) are loaded above via load_public_apis() and inc/functions/fs.php. + */ + \WP_Ultimo\Site_Exporter\Site_Exporter::get_instance(); + /* * Check if the Ultimate Multisite requirements are present. * @@ -578,11 +592,6 @@ protected function load_extra_components(): void { */ \WP_Ultimo\Tax\Tax::get_instance(); - /* - * Loads the Site Exporter - */ - \WP_Ultimo\Site_Exporter\Site_Exporter::get_instance(); - /* * Loads the template placeholders */ From 2a555e4b279b1b0c12623fc8a99aa95d7c83ec69 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 30 Apr 2026 03:20:43 -0600 Subject: [PATCH 2/3] GH#1006: fix: register mu-migration WP-CLI commands early in Site_Exporter::setup() WP-CLI performs command discovery early in its bootstrap process. The mu-migration command class files each end with a bare WP_CLI::add_command() call that fires at file-include time, but load_dependencies() was only ever called lazily from handle_site_export() / handle_site_import() - far too late for WP-CLI to discover them. Add an early call to load_dependencies() in setup() when WP_CLI is defined and truthy. This mirrors the pattern in inc/apis/trait-wp-cli.php (enable_wp_cli()) used by every other manager that registers WP-CLI commands. The require_once calls inside load_dependencies() are idempotent, so the lazy call from handle_site_export() remains harmless. Combined with the GH#1007 fix (Site_Exporter now loads before the requirements gate), this makes the commands available even when Ultimate Multisite is not fully set up - which is the primary migration use case. Resolves #1006 --- inc/site-exporter/class-site-exporter.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/inc/site-exporter/class-site-exporter.php b/inc/site-exporter/class-site-exporter.php index 2ae4c6a6..40491c41 100644 --- a/inc/site-exporter/class-site-exporter.php +++ b/inc/site-exporter/class-site-exporter.php @@ -68,6 +68,28 @@ public function init(): void { */ public function setup(): void { + /* + * Register the mu-migration WP-CLI commands early so WP-CLI discovers + * them during the command bootstrap phase. + * + * Each mu-migration class file ends with a bare `WP_CLI::add_command()` + * call that executes at file-include time. Those files are only loaded + * via `load_dependencies()`, which is normally called lazily from + * `handle_site_export()` / `handle_site_import()` — far too late for + * WP-CLI's command discovery. Calling `load_dependencies()` here when + * WP-CLI is active means the commands are registered before WP-CLI + * finishes bootstrapping. + * + * The `require_once` calls inside `load_dependencies()` are idempotent, + * so calling it again later from `handle_site_export()` is harmless. + * + * This check mirrors the pattern used by the `WP_CLI` trait in + * `inc/apis/trait-wp-cli.php` (see `enable_wp_cli()`). + */ + if ( defined('WP_CLI') && WP_CLI ) { + $this->load_dependencies(); + } + add_action('wu_export_site', [$this, 'handle_site_export'], 10, 3); add_action('wu_import_site', [$this, 'handle_site_import']); From bcee8d4bc86c250d60cfe8927df7b34c535457a2 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 30 Apr 2026 05:10:19 -0600 Subject: [PATCH 3/3] fix: add _reload_done circuit-breaker to thank-you.js stopped branch Add a one-time guard (_reload_done) around the window.location.reload() call in the Case 1 'stopped' branch of check_site_created(). Multiple setTimeout callbacks can be queued before the first reload fires during async polling overlap, potentially triggering redundant reloads. The guard is initialised to false in data() and set to true immediately before window.location.reload() is invoked, ensuring only the first queued callback performs the reload. Resolves #1038 --- assets/js/thank-you.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/assets/js/thank-you.js b/assets/js/thank-you.js index ea49fa7d..dd693d62 100644 --- a/assets/js/thank-you.js +++ b/assets/js/thank-you.js @@ -105,7 +105,12 @@ document.addEventListener("DOMContentLoaded", () => { // navigation and break the infinite-reload loop that occurred when the // "stopped" branch fired window.location.reload() on every page load // after site creation had already finished. - is_post_redirect: new URLSearchParams(window.location.search).has("_t") + is_post_redirect: new URLSearchParams(window.location.search).has("_t"), + // One-time guard for the Case 1 "stopped" reload. Multiple + // check_site_created() callbacks can be queued before the first reload + // fires (async polling overlap), so we set this flag before calling + // window.location.reload() and bail out of subsequent calls. + _reload_done: false }; }, computed: { @@ -205,8 +210,11 @@ document.addEventListener("DOMContentLoaded", () => { this.creating = false; this.stopped_count++; if (this.stopped_count >= 3) { - if (this.running_count > 0) { + if (this.running_count > 0 && !this._reload_done) { // Case 1: was running this session, now stopped — one-time reload. + // _reload_done guards against multiple queued callbacks all firing + // reload before the first navigation clears the page. + this._reload_done = true; window.location.reload(); } else if (!wu_thank_you.creating) { // Case 2: PHP already reported creation complete — mark ready.