From ed44ddda1e37df65866cffaef64bb24e77ee065e Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 20 May 2026 13:13:05 -0600 Subject: [PATCH] fix(domain): skip async enqueue of wu_add_domain/wu_add_subdomain when no listener is hooked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Domain_Manager unconditionally enqueued the 'wu_add_domain' and 'wu_add_subdomain' Action Scheduler jobs on every site creation and custom-domain change. Those hooks are extension points consumed only by host-provider integration modules (Cloudflare, cPanel, Hostinger, RunCloud, Hestia, Plesk, BunnyCDN, etc.) — each one registers its listener inside register_hooks() only when the integration is enabled. In environments where no host integration is active, Action Scheduler ran each enqueued job with zero callbacks and logged 'no callbacks are registered' on every site/domain operation. Site creation and the wu_async_process_domain_stage DNS/SSL chain were never affected by this — the domain stage progression has its own dedicated listener (class-domain-manager.php:155). Guard both enqueue sites with has_action(...). Host providers register their listeners at plugin init, well before Action Scheduler claims async jobs, so has_action() is authoritative at enqueue time. Any enabled integration continues to receive both hooks as before. Adds four regression tests covering both the skip path (no listener registered) and the enqueue path (listener registered) for each hook. --- inc/managers/class-domain-manager.php | 22 +- .../Managers/Domain_Manager_Test.php | 236 ++++++++++++++++++ 2 files changed, 256 insertions(+), 2 deletions(-) diff --git a/inc/managers/class-domain-manager.php b/inc/managers/class-domain-manager.php index 248a8ffd6..032df80c0 100644 --- a/inc/managers/class-domain-manager.php +++ b/inc/managers/class-domain-manager.php @@ -336,7 +336,16 @@ public function handle_site_created($site): void { 'site_id' => $site->blog_id, ]; - wu_enqueue_async_action('wu_add_subdomain', $args, 'domain'); + /* + * Only enqueue the async action when at least one host-provider integration + * has hooked into wu_add_subdomain. Otherwise Action Scheduler runs the job + * with zero callbacks and logs "no callbacks are registered" on every site + * creation, which is noise — the wu_async_process_domain_stage chain handles + * DNS/SSL progression independently. + */ + if (has_action('wu_add_subdomain')) { + wu_enqueue_async_action('wu_add_subdomain', $args, 'domain'); + } // Create a domain record for the site $this->create_domain_record_for_site($site); @@ -764,7 +773,16 @@ public function send_domain_to_host($old_value, $new_value, $item_id): void { 'site_id' => $site_id, ]; - wu_enqueue_async_action('wu_add_domain', $args, 'domain'); + /* + * Only enqueue the async action when at least one host-provider + * integration has hooked into wu_add_domain. Otherwise Action Scheduler + * runs the job with zero callbacks and logs "no callbacks are + * registered" on every domain change — see handle_site_created() for + * the equivalent wu_add_subdomain guard. + */ + if (has_action('wu_add_domain')) { + wu_enqueue_async_action('wu_add_domain', $args, 'domain'); + } /** * Fires when a custom domain is added to the network. diff --git a/tests/WP_Ultimo/Managers/Domain_Manager_Test.php b/tests/WP_Ultimo/Managers/Domain_Manager_Test.php index 3697f5ab3..4b26212cf 100644 --- a/tests/WP_Ultimo/Managers/Domain_Manager_Test.php +++ b/tests/WP_Ultimo/Managers/Domain_Manager_Test.php @@ -1132,6 +1132,242 @@ public function test_send_domain_to_host_different_values(): void { $this->assertTrue(true); // If we got here, no exception was thrown } + // ---------------------------------------------------------------- + // NEW: wu_add_domain / wu_add_subdomain are skipped when no host + // provider integration is listening. Regression for the "no callbacks + // are registered" Action Scheduler warning logged on every site + // creation when no host integration is enabled. + // ---------------------------------------------------------------- + + /** + * Test send_domain_to_host does not enqueue wu_add_domain when no listener is hooked. + */ + public function test_send_domain_to_host_skips_enqueue_without_listener(): void { + $unique_domain = 'no-listener-' . uniqid('', true) . '.example.com'; + + // Stash any pre-existing listeners so this test can deterministically + // assert "nothing got enqueued" — the production guard must skip the + // async enqueue when no host-provider integration is listening. + $prior_callbacks = $GLOBALS['wp_filter']['wu_add_domain'] ?? null; + unset($GLOBALS['wp_filter']['wu_add_domain']); + + try { + $domain = wu_create_domain([ + 'blog_id' => $this->get_blog_id(), + 'domain' => $unique_domain, + 'stage' => Domain_Stage::DONE, + ]); + + $this->assertNotWPError($domain); + + $this->assertFalse( + has_action('wu_add_domain'), + 'precondition: wu_add_domain must have no listeners' + ); + + $this->domain_manager->send_domain_to_host( + 'old-' . $unique_domain, + $unique_domain, + $domain->get_id() + ); + + $enqueued_args = [ + 'domain' => $unique_domain, + 'site_id' => $domain->get_site_id(), + ]; + + $matching_actions = wu_get_scheduled_actions( + [ + 'hook' => 'wu_add_domain', + 'status' => \ActionScheduler_Store::STATUS_PENDING, + 'args' => $enqueued_args, + 'per_page' => 5, + ], + 'ids' + ); + + $this->assertEmpty( + $matching_actions, + sprintf( + 'wu_add_domain must not be enqueued for args %s; found action ids: %s', + var_export($enqueued_args, true), + var_export($matching_actions, true) + ) + ); + } finally { + if (null !== $prior_callbacks) { + $GLOBALS['wp_filter']['wu_add_domain'] = $prior_callbacks; + } + } + } + + /** + * Test send_domain_to_host enqueues wu_add_domain when a listener is hooked. + */ + public function test_send_domain_to_host_enqueues_when_listener_registered(): void { + $callback = function () { + // no-op listener — its presence is what we are testing + }; + + $prior_callbacks = $GLOBALS['wp_filter']['wu_add_domain'] ?? null; + unset($GLOBALS['wp_filter']['wu_add_domain']); + + try { + $domain = wu_create_domain([ + 'blog_id' => $this->get_blog_id(), + 'domain' => 'with-listener-host.example.com', + 'stage' => Domain_Stage::DONE, + ]); + + $this->assertNotWPError($domain); + + wu_unschedule_all_actions('wu_add_domain'); + + add_action('wu_add_domain', $callback); + + $this->domain_manager->send_domain_to_host( + 'old-with-listener.example.com', + 'with-listener-host.example.com', + $domain->get_id() + ); + + $enqueued_args = [ + 'domain' => 'with-listener-host.example.com', + 'site_id' => $domain->get_site_id(), + ]; + + $matching_actions = wu_get_scheduled_actions( + [ + 'hook' => 'wu_add_domain', + 'status' => \ActionScheduler_Store::STATUS_PENDING, + 'args' => $enqueued_args, + 'per_page' => 5, + ], + 'ids' + ); + + $this->assertNotEmpty( + $matching_actions, + 'wu_add_domain must be enqueued when a host-provider integration is listening.' + ); + } finally { + remove_action('wu_add_domain', $callback); + if (null !== $prior_callbacks) { + $GLOBALS['wp_filter']['wu_add_domain'] = $prior_callbacks; + } + wu_unschedule_all_actions('wu_add_domain'); + } + } + + /** + * Test handle_site_created does not enqueue wu_add_subdomain when no listener is hooked. + */ + public function test_handle_site_created_skips_subdomain_enqueue_without_listener(): void { + global $current_site; + + $unique_sub = 'no-listener-' . uniqid('', false) . '.' . $current_site->domain; + + $prior_callbacks = $GLOBALS['wp_filter']['wu_add_subdomain'] ?? null; + unset($GLOBALS['wp_filter']['wu_add_subdomain']); + + try { + $blog_id = $this->create_test_blog([ + 'domain' => $unique_sub, + ]); + + $site = get_blog_details($blog_id); + + $this->assertFalse( + has_action('wu_add_subdomain'), + 'precondition: wu_add_subdomain must have no listeners' + ); + + $this->domain_manager->handle_site_created($site); + + $enqueued_args = [ + 'subdomain' => $site->domain, + 'site_id' => $site->blog_id, + ]; + + $matching_actions = wu_get_scheduled_actions( + [ + 'hook' => 'wu_add_subdomain', + 'status' => \ActionScheduler_Store::STATUS_PENDING, + 'args' => $enqueued_args, + 'per_page' => 5, + ], + 'ids' + ); + + $this->assertEmpty( + $matching_actions, + sprintf( + 'wu_add_subdomain must not be enqueued for args %s; found action ids: %s', + var_export($enqueued_args, true), + var_export($matching_actions, true) + ) + ); + } finally { + if (null !== $prior_callbacks) { + $GLOBALS['wp_filter']['wu_add_subdomain'] = $prior_callbacks; + } + } + } + + /** + * Test handle_site_created enqueues wu_add_subdomain when a listener is hooked. + */ + public function test_handle_site_created_enqueues_subdomain_when_listener_registered(): void { + global $current_site; + + $callback = function () { + // no-op listener — its presence is what we are testing + }; + + $prior_callbacks = $GLOBALS['wp_filter']['wu_add_subdomain'] ?? null; + unset($GLOBALS['wp_filter']['wu_add_subdomain']); + + try { + $blog_id = $this->create_test_blog([ + 'domain' => 'with-listener-sub.' . $current_site->domain, + ]); + + $site = get_blog_details($blog_id); + + wu_unschedule_all_actions('wu_add_subdomain'); + + add_action('wu_add_subdomain', $callback); + + $this->domain_manager->handle_site_created($site); + + $enqueued_args = [ + 'subdomain' => $site->domain, + 'site_id' => $site->blog_id, + ]; + + $matching_actions = wu_get_scheduled_actions( + [ + 'hook' => 'wu_add_subdomain', + 'status' => \ActionScheduler_Store::STATUS_PENDING, + 'args' => $enqueued_args, + 'per_page' => 5, + ], + 'ids' + ); + + $this->assertNotEmpty( + $matching_actions, + 'wu_add_subdomain must be enqueued when a host-provider integration is listening.' + ); + } finally { + remove_action('wu_add_subdomain', $callback); + if (null !== $prior_callbacks) { + $GLOBALS['wp_filter']['wu_add_subdomain'] = $prior_callbacks; + } + wu_unschedule_all_actions('wu_add_subdomain'); + } + } + // ---------------------------------------------------------------- // NEW: Domain set_domain normalizes to lowercase // ----------------------------------------------------------------