From 7c03b93ec770fbd27b10b2c2f66775be2b5d920c Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 16 Jul 2026 15:54:36 +0530 Subject: [PATCH 1/5] 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/5] 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 ); } From 0f7f0a295903c5069801b353fd18bab50a1dff01 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 24 Sep 2026 10:41:58 +0000 Subject: [PATCH 3/5] fix(clone): surface unparseable site list in parent-site check validate_parent_site_present_on_host() treated any non-JSON 'ee site list' output with exit code 0 as an empty host, so garbled output (e.g. stderr noise merged into stdout under ssh -t) was still reported as 'Parent site not found'. Only the 'No sites found!' error now counts as an empty host. --- src/clone/Cloner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clone/Cloner.php b/src/clone/Cloner.php index 86d2b4a3..b11538bb 100644 --- a/src/clone/Cloner.php +++ b/src/clone/Cloner.php @@ -86,7 +86,7 @@ public function validate_parent_site_present_on_host( string $site ): void { 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 ) ) { + if ( ! $this->is_no_sites_error( $list_run ) ) { throw new \Exception( 'Unable to get site list on ' . $this->user . '@' . $this->host ); } $list_result = []; From 4df2cbe0a0d936b01668f6402665dcca5de839f6 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 24 Sep 2026 16:45:50 +0000 Subject: [PATCH 4/5] fix(clone): treat an unparseable site list as an error in site_exists site_exists() read `ee site list` output that exited 0 but wasn't JSON (e.g. shell-rc noise under ssh -t) as "site doesn't exist". For an existing destination site, the clone then ran `ee site create`, which failed, and the create step's rollback ran `ee site delete --yes` on that existing site. Throw "Unable to get site list" instead, like the parent-site check does. --- src/clone/Cloner.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/clone/Cloner.php b/src/clone/Cloner.php index b11538bb..972e4c4f 100644 --- a/src/clone/Cloner.php +++ b/src/clone/Cloner.php @@ -320,6 +320,11 @@ public function site_exists(): bool { $sites = json_decode( $site_list->stdout, true ); + // Unparseable output must not read as "site doesn't exist": a clone rollback would then delete an existing destination site. + if ( ! is_array( $sites ) ) { + throw new \Exception( 'Unable to get site list on ' . $this->user . '@' . $this->host ); + } + foreach ( $sites as $site ) { if ( $site['site'] === $this->name ) { if ( 'disabled' === $site['status'] ) { From be6aab870f1f7776eaea654b9dfa47406a9a65f3 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 24 Sep 2026 16:46:19 +0000 Subject: [PATCH 5/5] fix(clone): accept an int wildcard flag in the parent-site check On PHP 8.1+ PDO SQLite returns native ints, so `ee site info --format=json` gives `site_ssl_wildcard: 1` and the strict `=== '1'` check never matched. Same-name `--ssl=inherit` clones to another host then always failed with "Parent site not found", even when the parent had wildcard SSL. Check both fields with `empty()` instead. --- src/clone/Cloner.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clone/Cloner.php b/src/clone/Cloner.php index 972e4c4f..f2cc55f1 100644 --- a/src/clone/Cloner.php +++ b/src/clone/Cloner.php @@ -100,7 +100,8 @@ public function validate_parent_site_present_on_host( string $site ): void { if ( explode( '.', $site, 2 )[1] === $parent_site ) { $info_result = $this->execute( 'ee site info ' . $parent_site . ' --format=json' ); $info_result = json_decode( $info_result->stdout, true ); - if ( $info_result['site_ssl'] !== '' && $info_result['site_ssl_wildcard'] === '1' ) { + // site_ssl_wildcard is int 1 or string '1' depending on the PHP version (PDO SQLite typing). + if ( ! empty( $info_result['site_ssl'] ) && ! empty( $info_result['site_ssl_wildcard'] ) ) { return; } }