From 7c03b93ec770fbd27b10b2c2f66775be2b5d920c Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 16 Jul 2026 15:54:36 +0530 Subject: [PATCH 1/2] fix(clone): detect empty destination host in site_exists `ee site clone`/`ee site sync` aborted with "Unable to get site list" whenever the destination host had zero existing EE sites. On an empty host `ee site list` calls \EE::error('No sites found!'), which writes to stderr and exits 1, but site_exists() inspected stdout for the sentinel, so the special-case never matched and the generic error path threw. Match the sentinel against stderr . stdout (covering both the local separate-streams case and the ssh -t merged-output case) and use strpos to tolerate trailing whitespace/debug noise. Also guard validate_parent_site_present_on_host() against a null json_decode on an empty host, which previously triggered a PHP warning from foreach(null). Refs rtCamp/EasyDash#4135 --- src/clone/Cloner.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/clone/Cloner.php b/src/clone/Cloner.php index 49980fd0..521697aa 100644 --- a/src/clone/Cloner.php +++ b/src/clone/Cloner.php @@ -84,6 +84,11 @@ public function validate_parent_site_present_on_host( string $site ): void { $list_result = $this->execute( 'ee site list --format=json' ); $list_result = json_decode( $list_result->stdout, true ); + // Empty host: `ee site list` returns no JSON ("No sites found!" on stderr), so there is no parent to match. + if ( ! is_array( $list_result ) ) { + $list_result = []; + } + foreach ( $list_result as $site_details ) { $parent_site = $site_details['site']; $substr_match = strpos( $site, $parent_site ); @@ -290,14 +295,13 @@ public function create_site( Site $source_site, $assoc_args ): EE\ProcessRun { public function site_exists(): bool { $site_list = $this->execute( 'ee site list --format=json --no-color' ); - if ( 1 === $site_list->return_code ) { - $error = trim ( preg_replace( '#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $site_list->stdout ) ); - if ( 'Error: No sites found!' === $error ) { + if ( 0 !== $site_list->return_code ) { + // "No sites found!" is emitted on stderr (merged into stdout under `ssh -t`). + $output = trim( preg_replace( '#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $site_list->stderr . $site_list->stdout ) ); + if ( false !== strpos( $output, 'Error: No sites found!' ) ) { return false; } - } - if ( 0 !== $site_list->return_code ) { throw new \Exception( 'Unable to get site list on ' . $this->user . '@' . $this->host ); } From 41aee5692d5eec2ef3f901e47eefb99f5f4d3f56 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 16 Jul 2026 16:13:07 +0530 Subject: [PATCH 2/2] fix(clone): harden empty-host detection after review - Extract is_no_sites_error() so site_exists() and validate_parent_site_present_on_host() share one sentinel check. - Narrow the gate back to exit code 1 (the code \EE::error always exits with) so an unrelated non-zero failure can no longer be mistaken for 'no sites' via a substring match. - validate_parent_site_present_on_host() now distinguishes an empty host from a genuine 'ee site list' failure, surfacing the latter as 'Unable to get site list' instead of masking it as 'parent not found'. --- src/clone/Cloner.php | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/clone/Cloner.php b/src/clone/Cloner.php index 521697aa..86d2b4a3 100644 --- a/src/clone/Cloner.php +++ b/src/clone/Cloner.php @@ -81,11 +81,14 @@ public function validate_ee_version(): void { } public function validate_parent_site_present_on_host( string $site ): void { - $list_result = $this->execute( 'ee site list --format=json' ); - $list_result = json_decode( $list_result->stdout, true ); + $list_run = $this->execute( 'ee site list --format=json' ); + $list_result = json_decode( $list_run->stdout, true ); - // Empty host: `ee site list` returns no JSON ("No sites found!" on stderr), so there is no parent to match. if ( ! is_array( $list_result ) ) { + // An empty host has no parent to match; any other failure must surface, not be masked as "parent not found". + if ( 0 !== $list_run->return_code && ! $this->is_no_sites_error( $list_run ) ) { + throw new \Exception( 'Unable to get site list on ' . $this->user . '@' . $this->host ); + } $list_result = []; } @@ -292,16 +295,26 @@ public function create_site( Site $source_site, $assoc_args ): EE\ProcessRun { return $new_site; } + // True when `ee site list` failed specifically because the host has no sites. + // `\EE::error( 'No sites found!' )` exits 1 and writes to stderr (merged into stdout under `ssh -t`). + private function is_no_sites_error( EE\ProcessRun $result ): bool { + if ( 1 !== $result->return_code ) { + return false; + } + + $output = trim( preg_replace( '#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $result->stderr . $result->stdout ) ); + + return false !== strpos( $output, 'Error: No sites found!' ); + } + public function site_exists(): bool { $site_list = $this->execute( 'ee site list --format=json --no-color' ); - if ( 0 !== $site_list->return_code ) { - // "No sites found!" is emitted on stderr (merged into stdout under `ssh -t`). - $output = trim( preg_replace( '#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $site_list->stderr . $site_list->stdout ) ); - if ( false !== strpos( $output, 'Error: No sites found!' ) ) { - return false; - } + if ( $this->is_no_sites_error( $site_list ) ) { + return false; + } + if ( 0 !== $site_list->return_code ) { throw new \Exception( 'Unable to get site list on ' . $this->user . '@' . $this->host ); }