From b3676314feefd7e29a062e941bd50dfe7b819c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Thu, 20 Nov 2025 15:25:16 +0200 Subject: [PATCH 1/5] feat(site): add site:https command for enabling HTTPS - Add SiteHttpsCommand class with domain selection and playbook execution - Add site-https.sh playbook that configures Caddy for automatic HTTPS - Support different WWW redirect modes (redirect-to-root, redirect-to-www) - Display HTTPS-enabled URL after successful configuration --- app/Console/Site/SiteAddCommand.php | 41 ++++++- app/Console/Site/SiteHttpsCommand.php | 152 +++++++++++++++++++++++++ app/SymfonyApp.php | 2 + app/Traits/ServersTrait.php | 57 ++++++++++ app/Traits/SitesTrait.php | 16 +++ playbooks/server-info.sh | 83 ++++++++++++++ playbooks/site-add.sh | 91 +++++++++++---- playbooks/site-https.sh | 157 ++++++++++++++++++++++++++ 8 files changed, 571 insertions(+), 28 deletions(-) create mode 100644 app/Console/Site/SiteHttpsCommand.php create mode 100644 playbooks/site-https.sh diff --git a/app/Console/Site/SiteAddCommand.php b/app/Console/Site/SiteAddCommand.php index d5f56091..21fe7a2b 100644 --- a/app/Console/Site/SiteAddCommand.php +++ b/app/Console/Site/SiteAddCommand.php @@ -38,7 +38,8 @@ protected function configure(): void ->addOption('repo', null, InputOption::VALUE_REQUIRED, 'Git repository URL') ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'Git branch name') ->addOption('server', null, InputOption::VALUE_REQUIRED, 'Server name') - ->addOption('php-version', null, InputOption::VALUE_REQUIRED, 'PHP version to use'); + ->addOption('php-version', null, InputOption::VALUE_REQUIRED, 'PHP version to use') + ->addOption('www-mode', null, InputOption::VALUE_REQUIRED, 'WWW handling mode (redirect-to-root, redirect-to-www)'); } // ---- @@ -106,6 +107,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'repo' => $repo, 'branch' => $branch, 'phpVersion' => $phpVersion, + 'wwwMode' => $wwwMode, ] = $siteInfo; // @@ -134,6 +136,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'DEPLOYER_PERMS' => $permissions, 'DEPLOYER_SITE_DOMAIN' => $domain, 'DEPLOYER_PHP_VERSION' => $phpVersion, + 'DEPLOYER_WWW_MODE' => $wwwMode, ], true ); @@ -162,11 +165,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Display next steps // ---- + $displayUrl = ($wwwMode === 'redirect-to-www') + ? 'http://www.' . $domain + : 'http://' . $domain; + $this->io->writeln([ 'Next steps:', - ' • Site is accessible at https://' . $domain . '', - ' • Update DNS records to point ' . $domain . ' to ' . $server->host . '', + ' • Site is accessible at ' . $displayUrl . '', + ' • Update DNS records:', + ' - Point @ (root) to ' . $server->host . '', + ' - Point www to ' . $server->host . '', + ' • Run site:https to enable HTTPS once you have your DNS records set up', ' • Deploy your application with site:deploy', + '', ]); // @@ -179,6 +190,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'branch' => $branch, 'server' => $server->name, 'php-version' => $phpVersion, + 'www-mode' => $wwwMode, ]); return Command::SUCCESS; @@ -293,7 +305,7 @@ private function selectPhpVersion(array $info): string|int * Gather site details from user input or CLI options. * * @param array $info Server information from serverInfo() - * @return array{domain: string, repo: string, branch: string, phpVersion: string}|null + * @return array{domain: string, repo: string, branch: string, phpVersion: string, wwwMode: string}|null */ protected function gatherSiteInfo(array $info): ?array { @@ -313,6 +325,26 @@ protected function gatherSiteInfo(array $info): ?array return null; } + // Normalize immediately after input + $domain = $this->normalizeDomain($domain); + + // + // Determine WWW handling + // ---- + + /** @var string $wwwMode */ + $wwwMode = $this->io->getOptionOrPrompt( + 'www-mode', + fn () => $this->io->promptSelect( + label: "How should 'www.{$domain}' be handled?", + options: [ + 'redirect-to-root' => 'Redirect www to non-www', + 'redirect-to-www' => 'Redirect non-www to www', + ], + default: 'redirect-to-root' + ) + ); + // // Gather git details // ---- @@ -370,6 +402,7 @@ protected function gatherSiteInfo(array $info): ?array 'repo' => $repo, 'branch' => $branch, 'phpVersion' => $phpVersion, + 'wwwMode' => $wwwMode, ]; } } diff --git a/app/Console/Site/SiteHttpsCommand.php b/app/Console/Site/SiteHttpsCommand.php new file mode 100644 index 00000000..5cc7c562 --- /dev/null +++ b/app/Console/Site/SiteHttpsCommand.php @@ -0,0 +1,152 @@ +addOption('domain', null, InputOption::VALUE_REQUIRED, 'Domain name'); + } + + // ---- + // Execution + // ---- + + protected function execute(InputInterface $input, OutputInterface $output): int + { + parent::execute($input, $output); + + $this->heading('Enable HTTPS'); + + // + // Select site + // ---- + + $site = $this->selectSite() + ; + if (is_int($site)) { + return $site; + } + + // + // Get server for site + // ---- + + $server = $this->getServerForSite($site); + + if (is_int($server)) { + return $server; + } + + $this->displayServerDeets($server); + + // + // Get server info (verifies SSH connection and validates distribution & permissions) + // ---- + + $info = $this->serverInfo($server); + + if (is_int($info)) { + return $info; + } + + [ + 'distro' => $distro, + 'permissions' => $permissions, + ] = $info; + + /** @var string $distro */ + /** @var string $permissions */ + + // + // Get site configuration + // ---- + + $config = $this->getSiteConfig($info, $site->domain); + + if ($config === null) { + $this->io->warning("Site '{$site->domain}' configuration not found on server"); + $this->io->writeln([ + '', + 'It looks like this site has not been provisioned yet.', + 'Run site:add to provision the site first.', + '', + ]); + + return Command::SUCCESS; + } + + $this->io->writeln([ + " • PHP Version: {$config['php_version']}", + " • WWW Mode: {$config['www_mode']}", + '', + ]); + + // + // Execute playbook + // ---- + + $result = $this->executePlaybook( + $server, + 'site-https', + 'Enabling HTTPS...', + [ + 'DEPLOYER_DISTRO' => $distro, + 'DEPLOYER_PERMS' => $permissions, + 'DEPLOYER_SITE_DOMAIN' => $site->domain, + 'DEPLOYER_PHP_VERSION' => $config['php_version'], + 'DEPLOYER_WWW_MODE' => $config['www_mode'], + ], + true + ); + + if (is_int($result)) { + return $result; + } + + $this->yay('HTTPS enabled successfully'); + + // + // Display next steps + // ---- + + $displayUrl = ($config['www_mode'] === 'redirect-to-www') + ? 'https://www.' . $site->domain + : 'https://' . $site->domain; + + $this->io->writeln([ + 'Your site is now accessible over HTTPS:', + ' ' . $displayUrl . '', + '', + ]); + + return Command::SUCCESS; + } +} diff --git a/app/SymfonyApp.php b/app/SymfonyApp.php index 32cf27d8..cfcfb8b1 100644 --- a/app/SymfonyApp.php +++ b/app/SymfonyApp.php @@ -19,6 +19,7 @@ use Bigpixelrocket\DeployerPHP\Console\Server\ServerRunCommand; use Bigpixelrocket\DeployerPHP\Console\Site\SiteAddCommand; use Bigpixelrocket\DeployerPHP\Console\Site\SiteDeleteCommand; +use Bigpixelrocket\DeployerPHP\Console\Site\SiteHttpsCommand; use Bigpixelrocket\DeployerPHP\Console\Site\SiteListCommand; use Bigpixelrocket\DeployerPHP\Console\Site\SiteSharedPullCommand; use Bigpixelrocket\DeployerPHP\Console\Site\SiteSharedPushCommand; @@ -166,6 +167,7 @@ private function registerCommands(): void SiteListCommand::class, SiteSharedPushCommand::class, SiteSharedPullCommand::class, + SiteHttpsCommand::class, ]; foreach ($commands as $command) { diff --git a/app/Traits/ServersTrait.php b/app/Traits/ServersTrait.php index 22c0209e..2bc8162f 100644 --- a/app/Traits/ServersTrait.php +++ b/app/Traits/ServersTrait.php @@ -70,6 +70,37 @@ protected function serverInfo(ServerDTO $server): array|int return $info; } + /** + * Get configuration for a specific site from server info. + * + * @param array $info Server information array + * @param string $domain Site domain + * @return array{php_version: string, www_mode: string, https_enabled: bool}|null Returns config or null if not found + */ + protected function getSiteConfig(array $info, string $domain): ?array + { + if (! isset($info['sites_config']) || ! is_array($info['sites_config'])) { + return null; + } + + $config = $info['sites_config'][$domain] ?? null; + + if (! is_array($config)) { + return null; + } + + /** @var mixed $phpVer */ + $phpVer = $config['php_version'] ?? 'unknown'; + /** @var mixed $mode */ + $mode = $config['www_mode'] ?? 'unknown'; + + return [ + 'php_version' => is_scalar($phpVer) ? (string) $phpVer : 'unknown', + 'www_mode' => is_scalar($mode) ? (string) $mode : 'unknown', + 'https_enabled' => filter_var($config['https_enabled'] ?? false, FILTER_VALIDATE_BOOLEAN), + ]; + } + /** * Validate that server is running a supported distribution. * @@ -390,6 +421,32 @@ protected function displayServerInfo(array $info): void } } } + + // Display Sites Configuration if available + if (isset($info['sites_config']) && is_array($info['sites_config']) && count($info['sites_config']) > 0) { + $sitesItems = []; + foreach ($info['sites_config'] as $domain => $config) { + if (! is_array($config)) { + continue; + } + + /** @var mixed $phpVal */ + $phpVal = $config['php_version'] ?? '?'; + /** @var mixed $modeVal */ + $modeVal = $config['www_mode'] ?? '?'; + + $php = is_scalar($phpVal) ? (string) $phpVal : '?'; + $mode = is_scalar($modeVal) ? (string) $modeVal : '?'; + $https = filter_var($config['https_enabled'] ?? false, FILTER_VALIDATE_BOOLEAN) ? 'HTTPS' : 'HTTP'; + + $sitesItems[] = "{$domain}: PHP {$php}, {$mode}, {$https}"; + } + + if (count($sitesItems) > 0) { + $this->io->displayDeets(['Sites Config' => $sitesItems]); + $this->io->writeln(''); + } + } } /** diff --git a/app/Traits/SitesTrait.php b/app/Traits/SitesTrait.php index 0f7a77e5..4c6f2269 100644 --- a/app/Traits/SitesTrait.php +++ b/app/Traits/SitesTrait.php @@ -140,6 +140,8 @@ protected function validateSiteDomain(mixed $domain): ?string return 'Domain must be a string'; } + $domain = $this->normalizeDomain($domain); + // Check format $isValid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; if (! $isValid) { @@ -155,6 +157,20 @@ protected function validateSiteDomain(mixed $domain): ?string return null; } + /** + * Normalize domain name (lowercase and strip www.). + */ + protected function normalizeDomain(string $domain): string + { + $domain = strtolower(trim($domain)); + + if (str_starts_with($domain, 'www.')) { + $domain = substr($domain, 4); + } + + return $domain; + } + /** * Validate branch name is not empty. * diff --git a/playbooks/server-info.sh b/playbooks/server-info.sh index 1d5b6943..c814052a 100755 --- a/playbooks/server-info.sh +++ b/playbooks/server-info.sh @@ -376,6 +376,61 @@ get_php_fpm_metrics() { printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$pool" "$process_manager" "$start_since" "$accepted_conn" "$listen_queue" "$idle_processes" "$active_processes" "$total_processes" "$max_children_reached" "$slow_requests" } +# +# Sites Configuration Detection +# ---- + +get_sites_config() { + local sites_dir="/etc/caddy/conf.d/sites" + + if [[ ! -d "$sites_dir" ]]; then + return + fi + + for config_file in "$sites_dir"/*.caddy; do + [[ -f "$config_file" ]] || continue + + local domain + domain=$(basename "$config_file" .caddy) + + # Read file content + local content + content=$(cat "$config_file") + + # PHP Version + local php_version="unknown" + if [[ $content =~ php([0-9]+\.[0-9]+)-fpm\.sock ]]; then + php_version="${BASH_REMATCH[1]}" + fi + + # WWW Mode + local www_mode="unknown" + if [[ $content =~ "Redirect www -> root" ]]; then + www_mode="redirect-to-root" + elif [[ $content =~ "Redirect root -> www" ]]; then + www_mode="redirect-to-www" + fi + + # HTTPS Status + local https_enabled="true" + if [[ $www_mode == "redirect-to-root" ]]; then + if [[ $content =~ http://${domain} ]]; then + https_enabled="false" + fi + elif [[ $www_mode == "redirect-to-www" ]]; then + if [[ $content =~ http://www.${domain} ]]; then + https_enabled="false" + fi + else + if [[ $content =~ http://${domain} || $content =~ http://www.${domain} ]]; then + https_enabled="false" + fi + fi + + printf '%s\t%s\t%s\t%s\n' "$domain" "$php_version" "$www_mode" "$https_enabled" + done +} + # ---- # Main Execution # ---- @@ -536,6 +591,34 @@ main() { exit 1 fi fi + + echo "→ Detecting sites configuration..." + local sites_config_yaml="" has_sites_config=false + + while IFS=$'\t' read -r domain php_ver mode https_status; do + has_sites_config=true + sites_config_yaml+=" ${domain}: + php_version: ${php_ver} + www_mode: ${mode} + https_enabled: ${https_status} +" + done < <(get_sites_config) + + if [[ $has_sites_config == true ]]; then + if ! echo "sites_config:" >> "$DEPLOYER_OUTPUT_FILE"; then + echo "Error: Failed to write sites_config header" >&2 + exit 1 + fi + if ! echo "$sites_config_yaml" >> "$DEPLOYER_OUTPUT_FILE"; then + echo "Error: Failed to write sites_config body" >&2 + exit 1 + fi + else + if ! echo "sites_config: {}" >> "$DEPLOYER_OUTPUT_FILE"; then + echo "Error: Failed to write empty sites_config" >&2 + exit 1 + fi + fi } main "$@" diff --git a/playbooks/site-add.sh b/playbooks/site-add.sh index ee6c668c..38270058 100644 --- a/playbooks/site-add.sh +++ b/playbooks/site-add.sh @@ -31,6 +31,7 @@ export DEBIAN_FRONTEND=noninteractive [[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 [[ -z $DEPLOYER_SITE_DOMAIN ]] && echo "Error: DEPLOYER_SITE_DOMAIN required" && exit 1 [[ -z $DEPLOYER_PHP_VERSION ]] && echo "Error: DEPLOYER_PHP_VERSION required" && exit 1 +[[ -z $DEPLOYER_WWW_MODE ]] && echo "Error: DEPLOYER_WWW_MODE required" && exit 1 export DEPLOYER_PERMS # Shared helpers are automatically inlined when executing playbooks remotely @@ -51,7 +52,7 @@ setup_site_directories() { local domain=$1 local site_path="/home/deployer/sites/${domain}" - echo "→ Creating directory structure for ${domain}..." + echo "→ Creating directory structure..." # Create main site directory if ! run_cmd test -d "$site_path"; then @@ -102,11 +103,15 @@ setup_demo_page() { local domain=$1 local index_file="/home/deployer/sites/${domain}/current/public/index.php" - echo "→ Creating default page for ${domain}..." + echo "→ Creating default page..." if ! run_cmd test -f "$index_file"; then if ! run_cmd tee "$index_file" > /dev/null <<- 'EOF'; then - site:deploy to deploy your application'; + '; + echo '
  • Run site:https to enable HTTPS
  • '; + echo '
  • Deploy your application with site:deploy
  • '; + echo ''; EOF echo "Error: Failed to create index.php" >&2 exit 1 @@ -136,8 +141,9 @@ setup_demo_page() { configure_caddy_vhost() { local domain=$1 local site_path="/home/deployer/sites/${domain}" + local www_mode=$DEPLOYER_WWW_MODE - echo "→ Creating Caddy configuration for ${domain}..." + echo "→ Creating Caddy configuration..." # Use specified PHP version local php_version=$DEPLOYER_PHP_VERSION @@ -160,31 +166,68 @@ configure_caddy_vhost() { exit 1 fi - # Create vhost configuration file - local vhost_file="/etc/caddy/conf.d/sites/${domain}.caddy" + # Generate Caddy configuration content + local caddy_config="" - if ! run_cmd tee "$vhost_file" > /dev/null <<- EOF; then - # Site: ${domain} - ${domain} { - root * ${site_path}/current/public - encode gzip - - log { - output file /var/log/caddy/${domain}-access.log { - roll_size 100mb - roll_keep 5 - roll_keep_for 720h - } - format json + # Common site block configuration + read -r -d '' site_block_config <<- EOF + root * ${site_path}/current/public + encode gzip + + log { + output file /var/log/caddy/${domain}-access.log { + roll_size 100mb + roll_keep 5 + roll_keep_for 720h } + format json + } - # Serve PHP files through FPM - php_fastcgi unix//${php_fpm_socket} + # Serve PHP files through FPM + php_fastcgi unix//${php_fpm_socket} - # Serve static files directly (more efficient than passing through PHP-FPM) - file_server - } + # Serve static files directly (more efficient than passing through PHP-FPM) + file_server EOF + + # Build configuration based on WWW mode + case $www_mode in + redirect-to-root) + # Redirect www to non-www + read -r -d '' caddy_config <<- EOF + # Site: ${domain} (Redirect www -> root) + www.${domain} { + redir http://${domain}{uri} permanent + } + + http://${domain} { + ${site_block_config} + } + EOF + ;; + redirect-to-www) + # Redirect non-www to www + read -r -d '' caddy_config <<- EOF + # Site: ${domain} (Redirect root -> www) + ${domain} { + redir http://www.${domain}{uri} permanent + } + + http://www.${domain} { + ${site_block_config} + } + EOF + ;; + *) + echo "Error: Invalid WWW mode: ${www_mode}" >&2 + exit 1 + ;; + esac + + # Create vhost configuration file + local vhost_file="/etc/caddy/conf.d/sites/${domain}.caddy" + + if ! echo "$caddy_config" | run_cmd tee "$vhost_file" > /dev/null; then echo "Error: Failed to create Caddy configuration" >&2 exit 1 fi diff --git a/playbooks/site-https.sh b/playbooks/site-https.sh new file mode 100644 index 00000000..46d559dd --- /dev/null +++ b/playbooks/site-https.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash + +# +# Site HTTPS Playbook - Ubuntu/Debian Only +# +# Enable HTTPS for an existing site using Caddy's automatic certificates. +# ---- +# +# This playbook only supports Ubuntu and Debian distributions (debian family). +# Requires site to be already provisioned. +# +# Required Environment Variables: +# DEPLOYER_OUTPUT_FILE - Output file path +# DEPLOYER_DISTRO - Exact distribution: ubuntu|debian +# DEPLOYER_PERMS - Permissions: root|sudo +# DEPLOYER_SITE_DOMAIN - Site domain name +# DEPLOYER_PHP_VERSION - PHP version to use (preserved) +# DEPLOYER_WWW_MODE - WWW handling mode (preserved) +# +# Returns YAML with: +# - status: success +# - https_enabled: true +# + +set -o pipefail +export DEBIAN_FRONTEND=noninteractive + +[[ -z $DEPLOYER_OUTPUT_FILE ]] && echo "Error: DEPLOYER_OUTPUT_FILE required" && exit 1 +[[ -z $DEPLOYER_DISTRO ]] && echo "Error: DEPLOYER_DISTRO required" && exit 1 +[[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 +[[ -z $DEPLOYER_SITE_DOMAIN ]] && echo "Error: DEPLOYER_SITE_DOMAIN required" && exit 1 +[[ -z $DEPLOYER_PHP_VERSION ]] && echo "Error: DEPLOYER_PHP_VERSION required" && exit 1 +[[ -z $DEPLOYER_WWW_MODE ]] && echo "Error: DEPLOYER_WWW_MODE required" && exit 1 +export DEPLOYER_PERMS + +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" + +# ---- +# Helper Functions +# ---- + +# +# Update Caddy Configuration +# ---- + +update_caddy_config() { + local domain=$1 + local site_path="/home/deployer/sites/${domain}" + local www_mode=$DEPLOYER_WWW_MODE + local php_version=$DEPLOYER_PHP_VERSION + local php_fpm_socket="/run/php/php${php_version}-fpm.sock" + local vhost_file="/etc/caddy/conf.d/sites/${domain}.caddy" + + echo "→ Updating Caddy configuration for HTTPS..." + + if ! run_cmd test -f "$vhost_file"; then + echo "Error: Site configuration file not found: $vhost_file" >&2 + exit 1 + fi + + # Common site block configuration + # Note: No http:// prefix here triggers Auto HTTPS + read -r -d '' site_block_config <<- EOF + root * ${site_path}/current/public + encode gzip + + log { + output file /var/log/caddy/${domain}-access.log { + roll_size 100mb + roll_keep 5 + roll_keep_for 720h + } + format json + } + + # Serve PHP files through FPM + php_fastcgi unix//${php_fpm_socket} + + # Serve static files directly + file_server + EOF + + # Build configuration based on WWW mode + local caddy_config="" + + case $www_mode in + redirect-to-root) + # Redirect www to non-www + read -r -d '' caddy_config <<- EOF + # Site: ${domain} (Redirect www -> root) + www.${domain} { + redir https://${domain}{uri} permanent + } + + ${domain} { + ${site_block_config} + } + EOF + ;; + redirect-to-www) + # Redirect non-www to www + read -r -d '' caddy_config <<- EOF + # Site: ${domain} (Redirect root -> www) + ${domain} { + redir https://www.${domain}{uri} permanent + } + + www.${domain} { + ${site_block_config} + } + EOF + ;; + *) + echo "Error: Invalid WWW mode: ${www_mode}" >&2 + exit 1 + ;; + esac + + if ! echo "$caddy_config" | run_cmd tee "$vhost_file" > /dev/null; then + echo "Error: Failed to update Caddy configuration" >&2 + exit 1 + fi +} + +# +# Reload Services +# ---- + +reload_services() { + echo "→ Reloading Caddy..." + if ! run_cmd systemctl reload caddy; then + echo "Error: Failed to reload Caddy" >&2 + exit 1 + fi +} + +# ---- +# Main Execution +# ---- + +main() { + local domain=$DEPLOYER_SITE_DOMAIN + + update_caddy_config "$domain" + reload_services + + if ! cat > "$DEPLOYER_OUTPUT_FILE" <<- EOF; then + status: success + https_enabled: true + EOF + echo "Error: Failed to write output file" >&2 + exit 1 + fi +} + +main "$@" From 8cbbad3b9e35c7529b0e070fca84e50bfa981eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Thu, 20 Nov 2025 15:46:13 +0200 Subject: [PATCH 2/5] fix(site:https): guard against unknown PHP version from server config Prevent the site-https command from proceeding with playbook execution when PHP version cannot be detected from server configuration, avoiding confusing failures during playbook execution with invalid phpunknown-fpm.sock paths. --- app/Console/Site/SiteHttpsCommand.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Console/Site/SiteHttpsCommand.php b/app/Console/Site/SiteHttpsCommand.php index 5cc7c562..04f3cb24 100644 --- a/app/Console/Site/SiteHttpsCommand.php +++ b/app/Console/Site/SiteHttpsCommand.php @@ -103,11 +103,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } - $this->io->writeln([ - " • PHP Version: {$config['php_version']}", - " • WWW Mode: {$config['www_mode']}", - '', - ]); + if ($config['php_version'] === 'unknown') { + $this->nay("Could not detect PHP version for '{$site->domain}' from server config; re-provision the site or run server:info to debug."); + + return Command::FAILURE; + } // // Execute playbook From 8473dd61690b10855ed4a4f17d50606fc274789e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Thu, 20 Nov 2025 15:52:11 +0200 Subject: [PATCH 3/5] refactor(servers): centralize site config parsing in displayServerInfo Use getSiteConfig() method instead of duplicating extraction logic when building Sites Config display lines. This reduces code duplication and ensures consistent parsing behavior. --- app/Traits/ServersTrait.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/Traits/ServersTrait.php b/app/Traits/ServersTrait.php index 2bc8162f..7e10a53f 100644 --- a/app/Traits/ServersTrait.php +++ b/app/Traits/ServersTrait.php @@ -425,19 +425,16 @@ protected function displayServerInfo(array $info): void // Display Sites Configuration if available if (isset($info['sites_config']) && is_array($info['sites_config']) && count($info['sites_config']) > 0) { $sitesItems = []; - foreach ($info['sites_config'] as $domain => $config) { - if (! is_array($config)) { + foreach (array_keys($info['sites_config']) as $domain) { + $config = $this->getSiteConfig($info, (string) $domain); + + if ($config === null) { continue; } - /** @var mixed $phpVal */ - $phpVal = $config['php_version'] ?? '?'; - /** @var mixed $modeVal */ - $modeVal = $config['www_mode'] ?? '?'; - - $php = is_scalar($phpVal) ? (string) $phpVal : '?'; - $mode = is_scalar($modeVal) ? (string) $modeVal : '?'; - $https = filter_var($config['https_enabled'] ?? false, FILTER_VALIDATE_BOOLEAN) ? 'HTTPS' : 'HTTP'; + $php = $config['php_version'] === 'unknown' ? '?' : $config['php_version']; + $mode = $config['www_mode'] === 'unknown' ? '?' : $config['www_mode']; + $https = $config['https_enabled'] ? 'HTTPS' : 'HTTP'; $sitesItems[] = "{$domain}: PHP {$php}, {$mode}, {$https}"; } From 8c03b53325887b52f9cec22058af32fb94f9da82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Thu, 20 Nov 2025 16:03:19 +0200 Subject: [PATCH 4/5] fix(site:add): add validation for WWW mode selection - Extract WWW mode options into reusable array - Switch to getValidatedOptionOrPrompt for proper validation - Add validation logic to ensure only valid WWW modes are accepted --- app/Console/Site/SiteAddCommand.php | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/app/Console/Site/SiteAddCommand.php b/app/Console/Site/SiteAddCommand.php index 21fe7a2b..4771ee85 100644 --- a/app/Console/Site/SiteAddCommand.php +++ b/app/Console/Site/SiteAddCommand.php @@ -332,19 +332,29 @@ protected function gatherSiteInfo(array $info): ?array // Determine WWW handling // ---- - /** @var string $wwwMode */ - $wwwMode = $this->io->getOptionOrPrompt( + $wwwModes = [ + 'redirect-to-root' => 'Redirect www to non-www', + 'redirect-to-www' => 'Redirect non-www to www', + ]; + + /** @var string|null $wwwMode */ + $wwwMode = $this->io->getValidatedOptionOrPrompt( 'www-mode', - fn () => $this->io->promptSelect( + fn ($validate) => $this->io->promptSelect( label: "How should 'www.{$domain}' be handled?", - options: [ - 'redirect-to-root' => 'Redirect www to non-www', - 'redirect-to-www' => 'Redirect non-www to www', - ], - default: 'redirect-to-root' - ) + options: $wwwModes, + default: 'redirect-to-root', + validate: $validate + ), + fn ($value) => in_array($value, array_keys($wwwModes), true) + ? null + : sprintf("Invalid WWW mode '%s'. Allowed: %s", is_scalar($value) ? $value : gettype($value), implode(', ', array_keys($wwwModes))) ); + if ($wwwMode === null) { + return null; + } + // // Gather git details // ---- From ab185772e91b1f6451a2e288ba3dea9516ae2885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Thu, 20 Nov 2025 16:03:22 +0200 Subject: [PATCH 5/5] fix(server:info): quote YAML values in sites config output - Add quotes around php_version, www_mode, and https_enabled values - Add documentation comment for sites_config field in output --- playbooks/server-info.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/playbooks/server-info.sh b/playbooks/server-info.sh index c814052a..ce88fbab 100755 --- a/playbooks/server-info.sh +++ b/playbooks/server-info.sh @@ -16,6 +16,7 @@ # - caddy: Caddy metrics (available, version, sites_count, domains, uptime_seconds, active_requests, total_requests, memory_mb) # - php_fpm: map of PHP versions to metrics (pool, process_manager, uptime_seconds, accepted_conn, listen_queue, idle_processes, active_processes, total_processes, max_children_reached, slow_requests) # - ports: map of port numbers to process names +# - sites_config: map of domain to config (php_version, www_mode, https_enabled) set -o pipefail export DEBIAN_FRONTEND=noninteractive @@ -598,9 +599,9 @@ main() { while IFS=$'\t' read -r domain php_ver mode https_status; do has_sites_config=true sites_config_yaml+=" ${domain}: - php_version: ${php_ver} - www_mode: ${mode} - https_enabled: ${https_status} + php_version: \"${php_ver}\" + www_mode: \"${mode}\" + https_enabled: \"${https_status}\" " done < <(get_sites_config)