#!/bin/bash # # Script to automate WordPress installation. # Includes dependency checks, WP-CLI installation, database setup, # WordPress core installation, and configuration. # v2.1 - Fixes WP-CLI execution path issues when run as root. # # --- Configuration --- # Exit immediately if a command exits with a non-zero status. set -e # Treat unset variables as an error when substituting. set -u # Pipe commands return the exit status of the last command in the pipe set -o pipefail # Colors for output messages RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # --- Default Values --- WP_ADMIN_USER="" WP_ADMIN_PASS="" WP_ADMIN_EMAIL="" WP_ROOT="/var/www/webroot/ROOT" # Default WordPress root directory DOMAIN="" # Domain will be determined or required DB_HOST="127.0.0.1" DB_ROOT_USER="root" DB_ROOT_PASS="" # Require user to provide this for security # PERFORM_DB_ROOT_RESET is set during arg parsing WEB_USER="litespeed" # Web server user WEB_GROUP="litespeed" # Web server group WP_CLI_PATH="/usr/local/bin/wp" # Path to WP-CLI executable # --- Helper Functions --- # Print informational messages info() { printf "${BLUE}[INFO] %s${NC}\n" "$@" } # Print success messages success() { printf "${GREEN}[SUCCESS] %s${NC}\n" "$@" } # Print warning messages warning() { printf "${YELLOW}[WARNING] %s${NC}\n" "$@" } # Print error messages and exit error_exit() { printf "${RED}[ERROR] %s${NC}\n" "$@" >&2 # Attempt cleanup before exiting cleanup &> /dev/null || true exit 1 } # Function to display usage information usage() { printf "Usage: %s [OPTIONS]\n" "$0" printf "\n" printf "Automates the installation of WordPress.\n" printf "\n" printf "Required Options:\n" printf " --wpusername=USERNAME WordPress admin username (mandatory)\n" printf " --wppassword=PASSWORD WordPress admin password (mandatory)\n" printf " --wpemail=EMAIL WordPress admin email (mandatory)\n" # Make dbrootpass conditional in description printf " --dbrootpass=PASSWORD Current MySQL/MariaDB root password (required IF NOT using --reset-db-root-pass)\n" printf "\n" printf "Optional Options:\n" printf " --wproot=PATH WordPress installation directory (default: %s)\n" "$WP_ROOT" printf " --domain=DOMAIN Domain name for the site (default: auto-detected from hostname)\n" printf " --webuser=USER Web server user (default: %s)\n" "$WEB_USER" printf " --webgroup=GROUP Web server group (default: %s)\n" "$WEB_GROUP" printf " --dbhost=HOST Database host (default: %s)\n" "$DB_HOST" printf " --reset-db-root-pass Perform the risky root password reset (requires script runner with root privileges)\n" printf " -h, --help Display this help message\n" printf "\n" printf "Example:\n" printf " %s --wpusername=myuser --wppassword='securePass' --wpemail=me@example.com --dbrootpass='currentRootPass'\n" "$0" printf " %s --wpusername=myuser --wppassword='securePass' --wpemail=me@example.com --reset-db-root-pass --domain=example.com\n" "$0" exit 1 } # Function to check if a command exists command_exists() { command -v "$1" &> /dev/null } # Function to generate a random secure password generate_password() { openssl rand -base64 16 } # Function to clean up temporary files # Define WP_CLI_CONFIG_PATH early so cleanup function knows about it WP_CLI_CONFIG_PATH="/tmp/wp-cli-config-$RANDOM.yml" cleanup() { # Check if file exists before trying to remove if [[ -n "${WP_CLI_CONFIG_PATH-}" && -f "$WP_CLI_CONFIG_PATH" ]]; then info "Cleaning up temporary WP-CLI config: $WP_CLI_CONFIG_PATH" rm -f "$WP_CLI_CONFIG_PATH" fi } # --- Argument Parsing --- TEMP=$(getopt -o h --longoptions help,wpusername:,wppassword:,wpemail:,wproot:,domain:,dbhost:,dbrootpass:,reset-db-root-pass,webuser:,webgroup: -n "$0" -- "$@") if [ $? != 0 ]; then error_exit "Terminating... Invalid arguments provided." fi eval set -- "$TEMP" unset TEMP PERFORM_DB_ROOT_RESET="false" # Default value while true; do case "$1" in --wpusername) WP_ADMIN_USER="$2"; shift 2 ;; --wppassword) WP_ADMIN_PASS="$2"; shift 2 ;; --wpemail) WP_ADMIN_EMAIL="$2"; shift 2 ;; --wproot) WP_ROOT="$2"; shift 2 ;; --domain) DOMAIN="$2"; shift 2 ;; --dbhost) DB_HOST="$2"; shift 2 ;; --dbrootpass) DB_ROOT_PASS="$2"; shift 2 ;; --reset-db-root-pass) PERFORM_DB_ROOT_RESET="true"; shift 1 ;; --webuser) WEB_USER="$2"; shift 2 ;; --webgroup) WEB_GROUP="$2"; shift 2 ;; -h|--help) usage ;; --) shift ; break ;; *) error_exit "Internal error parsing options! Unexpected option: $1";; esac done # Set trap *after* WP_CLI_CONFIG_PATH is defined and potentially used trap cleanup EXIT SIGINT SIGTERM # --- Validation --- info "Validating parameters..." if [[ -z "$WP_ADMIN_USER" ]]; then error_exit "WordPress admin username (--wpusername) is required."; fi if [[ -z "$WP_ADMIN_PASS" ]]; then error_exit "WordPress admin password (--wppassword) is required."; fi if [[ -z "$WP_ADMIN_EMAIL" ]]; then error_exit "WordPress admin email (--wpemail) is required."; fi if [[ ! "$WP_ADMIN_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then error_exit "Invalid email format for --wpemail: '$WP_ADMIN_EMAIL'"; fi if [[ "$PERFORM_DB_ROOT_RESET" == "false" && -z "$DB_ROOT_PASS" ]]; then error_exit "Database root password (--dbrootpass) is required unless --reset-db-root-pass is used."; fi if [[ "$PERFORM_DB_ROOT_RESET" == "true" && -n "$DB_ROOT_PASS" ]]; then warning "Both --reset-db-root-pass and --dbrootpass provided. Reset will be performed, provided password ignored."; fi if [[ ! -d "$WP_ROOT" ]]; then error_exit "WordPress root directory '$WP_ROOT' does not exist or is not a directory."; fi if ! id "$WEB_USER" &>/dev/null; then error_exit "Web user '$WEB_USER' does not exist."; fi if ! getent group "$WEB_GROUP" &>/dev/null; then error_exit "Web group '$WEB_GROUP' does not exist."; fi success "Parameters validated." # --- Determine Domain --- if [[ -z "$DOMAIN" ]]; then if ! command_exists hostname; then error_exit "'hostname' command not found. Please install it or specify --domain."; fi FULL_HOSTNAME=$(hostname -f) DOMAIN=$(echo "$FULL_HOSTNAME" | sed -E 's/^(node[0-9]*-|wp-|web-|host-|localhost)//') # Slightly more aggressive cleaning if [[ -z "$DOMAIN" || "$DOMAIN" == "$FULL_HOSTNAME" || "$DOMAIN" == "localdomain" ]]; then # Handle cases where sed didn't change much warning "Could not reliably determine a public domain from hostname '$FULL_HOSTNAME'. Using it as is." DOMAIN="$FULL_HOSTNAME" # Consider erroring out if domain detection is critical and fails # error_exit "Failed to determine domain automatically. Please specify using --domain." fi info "Auto-detected domain: $DOMAIN (Use --domain to override if incorrect)" else info "Using specified domain: $DOMAIN" fi # --- Dependency Checks --- info "Checking dependencies..." # Group related checks if ! command_exists php; then error_exit "'php' command not found. Please install PHP."; fi if ! command_exists mysql; then error_exit "'mysql' command (client) not found. Please install MySQL/MariaDB client."; fi if ! command_exists curl; then error_exit "'curl' command not found. Please install curl."; fi if ! command_exists openssl; then error_exit "'openssl' command not found. Please install openssl."; fi if ! command_exists getopt; then error_exit "'getopt' command not found. Please install getopt."; fi if ! command_exists hostname; then error_exit "'hostname' command not found. Please install hostname or provide --domain."; fi if ! command_exists sed; then error_exit "'sed' command not found. Please install sed."; fi # Sudo needed for WP-CLI install/move and potentially for DB reset/permissions if ! command_exists sudo; then error_exit "'sudo' command not found. Sudo is required."; fi # Checks specific to DB reset path if [[ "$PERFORM_DB_ROOT_RESET" == "true" ]]; then if ! command_exists systemctl; then error_exit "'systemctl' command not found, but required for --reset-db-root-pass."; fi if ! command_exists pkill; then error_exit "'pkill' command not found, but required for --reset-db-root-pass."; fi # Check for mysqld_safe OR the actual daemon binary, as mysqld_safe might be deprecated if ! command_exists mysqld_safe && ! command_exists mariadbd && ! command_exists mysqld; then error_exit "'mysqld_safe' or 'mariadbd'/'mysqld' not found, required for --reset-db-root-pass."; fi # Check if we actually *can* use sudo for systemctl - requires root privileges if [[ "$(id -u)" -ne 0 ]] && ! sudo -n systemctl is-active mariadb &>/dev/null; then warning "Cannot run 'sudo systemctl' without password. DB reset requires script runner with root privileges or passwordless sudo for systemctl." # Consider making this an error_exit depending on strictness fi fi success "All essential dependencies found." # --- WP-CLI Setup --- # Check if WP-CLI executable exists at the defined path if [[ ! -x "$WP_CLI_PATH" ]]; then info "WP-CLI not found at $WP_CLI_PATH or not executable. Attempting installation..." TEMP_WP_CLI="./wp-cli.phar" if ! curl -o "$TEMP_WP_CLI" https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar; then error_exit "Failed to download WP-CLI." fi chmod +x "$TEMP_WP_CLI" # Attempt to move using sudo. This requires sudo privileges. if ! sudo mv "$TEMP_WP_CLI" "$WP_CLI_PATH"; then # Cleanup downloaded file if move failed rm -f "$TEMP_WP_CLI" error_exit "Failed to move WP-CLI to $WP_CLI_PATH. Check sudo permissions for the move command." fi # Verify installation by checking the target path again if [[ ! -x "$WP_CLI_PATH" ]]; then error_exit "WP-CLI installation failed unexpectedly. $WP_CLI_PATH not found or not executable after move." fi success "WP-CLI installed successfully to $WP_CLI_PATH" else success "WP-CLI is already installed at $WP_CLI_PATH." fi # --- WP-CLI Execution Context Setup --- # Determine how WP-CLI commands should be run (user, flags) # This logic prioritizes running as root if available, to avoid sudo PATH issues. WP_RUN_ARGS=("--path=$WP_ROOT") SUDO_CMD="" # Command prefix (e.g., sudo -u user) - empty by default WP_EXECUTABLE="$WP_CLI_PATH" # Use the full path to wp-cli # Check if the script is running as root (UID 0) if [[ "$(id -u)" -eq 0 ]]; then info "Script is running as root. Using --allow-root for WP-CLI commands." # Avoid adding flag if somehow already present (e.g., future modification) if [[ ! " ${WP_RUN_ARGS[@]} " =~ " --allow-root " ]]; then WP_RUN_ARGS+=("--allow-root") fi # No SUDO_CMD needed, root executes directly else # Script is NOT running as root. Check if it's running as the target web user. if [[ "$(id -u)" -eq "$(id -u "$WEB_USER")" ]]; then info "Script is running as the web user ('$WEB_USER'). No sudo or --allow-root needed." # No SUDO_CMD needed, correct user executes directly else # Running as a different non-root user. Need to try `sudo -u WEB_USER`. info "Script running as non-root user '$(id -un)'. Attempting to run WP-CLI as '$WEB_USER' via sudo." if sudo -n -u "$WEB_USER" "$WP_EXECUTABLE" --info --skip-update --quiet "${WP_RUN_ARGS[@]}" &>/dev/null; then info "Successfully verified ability to run WP-CLI as '$WEB_USER' using sudo." SUDO_CMD="sudo -u $WEB_USER" # Keep WP_EXECUTABLE as full path else # Cannot run as root, cannot run as web_user, cannot sudo -u web_user without password. error_exit "Script lacks permissions. Run as root, as '$WEB_USER', or ensure user '$(id -un)' has passwordless sudo access to run '$WP_EXECUTABLE' as '$WEB_USER'." fi fi fi info "WP-CLI execution command prefix: [${SUDO_CMD:-direct}] using executable [$WP_EXECUTABLE]" info "WP-CLI arguments: ${WP_RUN_ARGS[*]}" # --- Temporary WP-CLI Config for Domain --- # Create config file for WP-CLI to potentially pick up domain context # This helps if WP-CLI struggles to identify the site URL, especially with --allow-root # Might not be strictly necessary if WP_HOME/WP_SITEURL are defined early enough # but provides extra robustness. info "Creating temporary WP-CLI config at $WP_CLI_CONFIG_PATH" cat > "$WP_CLI_CONFIG_PATH" < /dev/null FLUSH PRIVILEGES; ALTER USER 'root'@'localhost' IDENTIFIED BY '$new_root_password'; ALTER USER 'root'@'127.0.0.1' IDENTIFIED BY '$new_root_password'; FLUSH PRIVILEGES; EXIT EOF then warning "Failed 'ALTER USER' reset attempt (may be normal). Trying alternative syntax..." # Try the mysql_native_password plugin explicitly if ! sudo mysql --protocol=socket -u root <<-EOF &> /dev/null FLUSH PRIVILEGES; ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '$new_root_password'; ALTER USER 'root'@'127.0.0.1' IDENTIFIED WITH mysql_native_password BY '$new_root_password'; FLUSH PRIVILEGES; EXIT EOF then warning "Failed with native password plugin. Trying legacy 'UPDATE' method..." # Fallback for older MySQL/MariaDB versions if ! sudo mysql --protocol=socket -u root <<-EOF &> /dev/null FLUSH PRIVILEGES; UPDATE mysql.user SET Password=PASSWORD('$new_root_password') WHERE User='root' AND Host='localhost'; UPDATE mysql.user SET Password=PASSWORD('$new_root_password') WHERE User='root' AND Host='127.0.0.1'; FLUSH PRIVILEGES; EXIT EOF then warning "Failed legacy 'UPDATE' reset method. Trying direct 'authentication_string' update (MariaDB/newer MySQL)..." if ! sudo mysql --protocol=socket -u root <<-EOF &> /dev/null FLUSH PRIVILEGES; UPDATE mysql.user SET authentication_string=PASSWORD('$new_root_password') WHERE User='root'; FLUSH PRIVILEGES; EXIT EOF then error_exit "All attempts to reset the root password in safe mode failed. Check MySQL/MariaDB logs. Manual intervention required." fi fi fi fi success "Root password likely reset in safe mode." info "Stopping MariaDB safe mode process (PID: $MYSQLD_SAFE_PID)..." # Use sudo to kill processes started by root sudo kill "$MYSQLD_SAFE_PID" || warning "Failed to kill specific PID $MYSQLD_SAFE_PID (maybe already stopped?)." sleep 2 info "Attempting broader pkill for lingering safe mode processes..." sudo pkill -f mysqld_safe || sudo pkill -f mariadbd-safe || true # Ignore errors if not found sleep 2 info "Attempting broader pkill for lingering database daemons..." sudo pkill -f mariadbd || sudo pkill -f mysqld || true # Ignore errors if not found sleep 3 success "Safe mode processes likely stopped." info "Starting $DB_SERVICE_NAME service normally..." if ! sudo systemctl start "$DB_SERVICE_NAME"; then error_exit "Failed to start $DB_SERVICE_NAME service after password reset. Check status/logs."; fi sleep 5 # Allow time for service to initialize info "Verifying $DB_SERVICE_NAME service status..." if ! sudo systemctl is-active --quiet "$DB_SERVICE_NAME"; then error_exit "$DB_SERVICE_NAME service failed to start or become active. Check service status."; fi success "$DB_SERVICE_NAME service started successfully with new root password." DB_ROOT_PASS="$new_root_password" # Use the newly set password for subsequent operations else info "Using provided root password to create database and user." # Test connection with provided root password if ! mysql -u "$DB_ROOT_USER" -p"$DB_ROOT_PASS" -h "$DB_HOST" -e "SELECT 1;" &> /dev/null; then error_exit "Failed to connect to database using provided root credentials. Check user '$DB_ROOT_USER', password, and host '$DB_HOST'." fi success "Database root connection successful." fi # --- Create WordPress Database and User --- info "Creating WordPress database '$DB_NAME' and user '$DB_USER'..." # Use printf for safer password injection into the command SQL_COMMAND=$(printf "CREATE DATABASE IF NOT EXISTS \`%s\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS '%s'@'%s' IDENTIFIED BY '%s'; GRANT ALL PRIVILEGES ON \`%s\`.* TO '%s'@'%s'; FLUSH PRIVILEGES;" \ "$DB_NAME" "$DB_USER" "$DB_HOST" "$DB_PASSWORD" "$DB_NAME" "$DB_USER" "$DB_HOST") # Try connecting to localhost via socket first if TCP connection fails if ! mysql -u "$DB_ROOT_USER" -p"$DB_ROOT_PASS" -h "$DB_HOST" -e "$SQL_COMMAND"; then warning "Failed to connect via TCP ($DB_HOST). Trying socket connection to localhost..." if ! mysql -u "$DB_ROOT_USER" -p"$DB_ROOT_PASS" --protocol=socket -e "$SQL_COMMAND"; then # If socket fails too, try with no password (in case reset made blank password) warning "Socket connection failed too. Trying without password..." if ! mysql -u "$DB_ROOT_USER" --protocol=socket -e "$SQL_COMMAND"; then error_exit "Failed to execute SQL command to create WordPress database/user. Check MySQL/MariaDB logs and permissions for '$DB_ROOT_USER'." else warning "Connected without password! MySQL root has no password now." # Update the root password again to be sure SECURE_ROOT_SQL="ALTER USER 'root'@'localhost' IDENTIFIED BY '$DB_ROOT_PASS'; FLUSH PRIVILEGES;" mysql -u "$DB_ROOT_USER" --protocol=socket -e "$SECURE_ROOT_SQL" || warning "Could not secure root user with password!" fi fi success "Database '$DB_NAME' and user '$DB_USER' created successfully via socket connection." else success "Database '$DB_NAME' and user '$DB_USER' created successfully." fi # --- WordPress Core File Setup --- info "Ensuring WordPress files are present in: $WP_ROOT" cd "$WP_ROOT" || error_exit "Failed to change directory to $WP_ROOT" # Backup existing wp-config.php if it exists if [[ -f "wp-config.php" ]]; then BACKUP_NAME="wp-config.php.bak.$(date +%Y%m%d_%H%M%S)" # Use underscore in timestamp info "Backing up existing wp-config.php to $BACKUP_NAME" cp wp-config.php "$BACKUP_NAME" || warning "Failed to backup wp-config.php" fi # Download WordPress core files ONLY if key files/dirs are missing if [[ ! -f "index.php" || ! -f "wp-includes/version.php" || ! -d "wp-admin" ]]; then info "WordPress core files seem missing or incomplete. Downloading..." # Use determined $SUDO_CMD, $WP_EXECUTABLE, and $WP_RUN_ARGS if ! $SUDO_CMD $WP_EXECUTABLE core download "${WP_RUN_ARGS[@]}" --version=latest; then error_exit "Failed to download WordPress core files using WP-CLI." fi success "WordPress core downloaded." else info "WordPress core files seem to exist. Skipping download." fi # --- Create wp-config.php --- info "Creating wp-config.php..." # Generate Salts using WP-CLI info "Generating WordPress salts using WP-CLI..." # Use determined $SUDO_CMD, $WP_EXECUTABLE, and $WP_RUN_ARGS SALTS=$($SUDO_CMD $WP_EXECUTABLE config salt generate --raw "${WP_RUN_ARGS[@]}" 2>/dev/null) || { warning "Could not generate salts using WP-CLI. Falling back to openssl (less standard format)." SALTS=$(cat < wp-config.php < .htaccess <<'EOF' || warning "Failed to create .htaccess file" # BEGIN LSCACHE ## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ## RewriteEngine on CacheLookup on RewriteRule .* - [E=Cache-Control:no-autoflush] RewriteRule litespeed/debug/.*\.log$ - [F,L] RewriteRule \.litespeed_conf\.dat - [F,L] ### marker ASYNC start ### RewriteCond %{REQUEST_URI} /wp-admin/admin-ajax\.php RewriteCond %{QUERY_STRING} action=async_litespeed RewriteRule .* - [E=noabort:1] ### marker ASYNC end ### ### marker CACHE RESOURCE start ### RewriteRule wp-content/.*/[^/]*(responsive|css|js|dynamic|loader|fonts)\.php - [E=cache-control:max-age=3600] ### marker CACHE RESOURCE end ### ### marker DROPQS start ### CacheKeyModify -qs:fbclid CacheKeyModify -qs:gclid CacheKeyModify -qs:utm* CacheKeyModify -qs:_ga ### marker DROPQS end ### ## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ## # END LSCACHE # BEGIN NON_LSCACHE ## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ## ## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ## # END NON_LSCACHE # BEGIN WordPress # The directives (lines) between "BEGIN WordPress" and "END WordPress" are # dynamically generated, and should only be modified via WordPress filters. # Any changes to the directives between these markers will be overwritten. RewriteEngine On RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] # END WordPress # BEGIN LiteSpeed # The directives (lines) between "BEGIN LiteSpeed" and "END LiteSpeed" are # dynamically generated, and should only be modified via WordPress filters. # Any changes to the directives between these markers will be overwritten. SetEnv noabort 1 # END LiteSpeed EOF # Set appropriate permissions for .htaccess if [[ -f ".htaccess" ]]; then sudo chmod 644 .htaccess sudo chown "${WEB_USER}:${WEB_GROUP}" .htaccess success ".htaccess file created and configured for LiteSpeed." fi else info ".htaccess file already exists. Skipping creation." fi # Set up pretty permalinks using WP-CLI info "Configuring WordPress permalink structure..." $SUDO_CMD $WP_EXECUTABLE rewrite structure '/%postname%/' "${WP_RUN_ARGS[@]}" || warning "Could not set permalink structure" $SUDO_CMD $WP_EXECUTABLE rewrite flush "${WP_RUN_ARGS[@]}" || warning "Could not flush rewrite rules" success "WordPress installed successfully via WP-CLI." else info "WordPress is already installed according to WP-CLI." # Optionally update URL if needed (be careful with this, can break site if proxy/etc involved) # info "Verifying site URL options..." # $SUDO_CMD $WP_EXECUTABLE option update siteurl "https://$DOMAIN" "${WP_RUN_ARGS[@]}" || warning "Failed to update siteurl option" # $SUDO_CMD $WP_EXECUTABLE option update home "https://$DOMAIN" "${WP_RUN_ARGS[@]}" || warning "Failed to update home option" fi # --- Let's Encrypt SSL Certificate Setup --- info "Setting up Let's Encrypt SSL certificate..." # First, validate that the domain is publicly accessible info "Validating domain accessibility for SSL certificate..." # Check if domain resolves to this server DOMAIN_VALIDATION_FAILED=false # Get server's public IP SERVER_IP=$(curl -s ifconfig.me 2>/dev/null || curl -s ipinfo.io/ip 2>/dev/null || curl -s icanhazip.com 2>/dev/null) if [[ -z "$SERVER_IP" ]]; then warning "Could not determine server's public IP address." DOMAIN_VALIDATION_FAILED=true else info "Server public IP: $SERVER_IP" fi # Check if domain resolves if [[ "$DOMAIN_VALIDATION_FAILED" == "false" ]]; then DOMAIN_IP=$(dig +short "$DOMAIN" 2>/dev/null | tail -n1) if [[ -z "$DOMAIN_IP" ]]; then warning "Domain '$DOMAIN' does not resolve to any IP address." warning "Domain needs to be publicly accessible and point to this server for SSL to work." DOMAIN_VALIDATION_FAILED=true elif [[ "$DOMAIN_IP" != "$SERVER_IP" ]]; then warning "Domain '$DOMAIN' resolves to $DOMAIN_IP, but server IP is $SERVER_IP." warning "Domain must point to this server for Let's Encrypt validation to work." DOMAIN_VALIDATION_FAILED=true else success "Domain validation passed: $DOMAIN resolves to $SERVER_IP" fi fi # Check if domain is accessible via HTTP if [[ "$DOMAIN_VALIDATION_FAILED" == "false" ]]; then info "Testing HTTP accessibility for domain validation..." HTTP_TEST=$(curl -s -o /dev/null -w "%{http_code}" "http://$DOMAIN" --connect-timeout 10 --max-time 30 2>/dev/null) if [[ "$HTTP_TEST" != "200" && "$HTTP_TEST" != "301" && "$HTTP_TEST" != "302" ]]; then warning "Domain '$DOMAIN' is not accessible via HTTP (got status: ${HTTP_TEST:-'timeout/error'})." warning "Let's Encrypt needs HTTP access for domain validation." DOMAIN_VALIDATION_FAILED=true else success "Domain is accessible via HTTP (status: $HTTP_TEST)" fi fi # Proceed with SSL only if domain validation passed if [[ "$DOMAIN_VALIDATION_FAILED" == "true" ]]; then warning "Domain validation failed. Skipping SSL certificate generation." info "To get SSL later:" info "1. Ensure your domain '$DOMAIN' points to this server ($SERVER_IP)" info "2. Make sure port 80 is open and accessible from the internet" info "3. Run: sudo certbot --webroot -w '$WP_ROOT' -d '$DOMAIN' --email '$WP_ADMIN_EMAIL' --agree-tos" info "WordPress will work without SSL, but HTTPS is recommended for production sites." else # Install certbot if not present if ! command_exists certbot; then info "Installing certbot for Let's Encrypt certificate management..." if command_exists apt-get; then # Debian/Ubuntu sudo apt-get update -qq sudo apt-get install -y certbot python3-certbot-apache || error_exit "Failed to install certbot via apt-get" elif command_exists yum; then # CentOS/RHEL 7 sudo yum install -y epel-release sudo yum install -y certbot python3-certbot-apache || error_exit "Failed to install certbot via yum" elif command_exists dnf; then # CentOS/RHEL 8+/Fedora sudo dnf install -y certbot python3-certbot-apache || error_exit "Failed to install certbot via dnf" else warning "Package manager not detected. Please install certbot manually." info "You can install certbot using: wget https://dl.eff.org/certbot-auto && chmod a+x certbot-auto" fi else success "Certbot is already installed." fi # Generate SSL certificate if command_exists certbot; then info "Generating Let's Encrypt SSL certificate for domain: $DOMAIN" # Create a simple verification file for webroot authentication WEBROOT_PATH="$WP_ROOT" ACME_CHALLENGE_DIR="$WEBROOT_PATH/.well-known/acme-challenge" sudo mkdir -p "$ACME_CHALLENGE_DIR" sudo chown -R "${WEB_USER}:${WEB_GROUP}" "$WEBROOT_PATH/.well-known" sudo chmod -R 755 "$WEBROOT_PATH/.well-known" # Try webroot method first (non-interactive) info "Attempting SSL certificate generation using webroot method..." if sudo certbot certonly \ --webroot \ --webroot-path="$WEBROOT_PATH" \ --email="$WP_ADMIN_EMAIL" \ --agree-tos \ --non-interactive \ --domains="$DOMAIN" \ --expand; then success "SSL certificate generated successfully for $DOMAIN" # Set up automatic renewal info "Setting up automatic SSL certificate renewal..." # Create renewal cron job if it doesn't exist CRON_JOB="0 12 * * * /usr/bin/certbot renew --quiet --post-hook \"systemctl reload lshttpd || systemctl reload apache2 || systemctl reload nginx\"" if ! sudo crontab -l 2>/dev/null | grep -q "certbot renew"; then (sudo crontab -l 2>/dev/null; echo "$CRON_JOB") | sudo crontab - success "Automatic SSL renewal configured (daily check at 12:00 PM)" else info "SSL renewal cron job already exists." fi # For LiteSpeed, we need to restart the service to pick up new certificates info "Restarting LiteSpeed web server to apply SSL certificate..." if sudo systemctl is-active lshttpd &>/dev/null; then sudo systemctl restart lshttpd || warning "Failed to restart lshttpd service" success "LiteSpeed restarted successfully" elif sudo systemctl is-active litespeed &>/dev/null; then sudo systemctl restart litespeed || warning "Failed to restart litespeed service" success "LiteSpeed restarted successfully" else warning "LiteSpeed service not detected or not running. You may need to manually configure SSL in LiteSpeed admin panel." info "SSL certificate location: /etc/letsencrypt/live/$DOMAIN/" info "Certificate file: /etc/letsencrypt/live/$DOMAIN/fullchain.pem" info "Private key file: /etc/letsencrypt/live/$DOMAIN/privkey.pem" fi else warning "SSL certificate generation failed. You can manually run:" warning "sudo certbot --webroot -w '$WP_ROOT' -d '$DOMAIN' --email '$WP_ADMIN_EMAIL' --agree-tos" info "Or configure SSL manually in your web server control panel." fi else warning "Certbot not available. SSL certificate not generated." info "Please install certbot manually and run: sudo certbot --webroot -w '$WP_ROOT' -d '$DOMAIN'" fi fi # --- Final Summary --- success "WordPress setup process completed!" printf "\n--- ${YELLOW}Installation Summary${NC} ---\n" printf "Site URL: ${GREEN}https://%s${NC}\n" "$DOMAIN" printf "WP Root: ${GREEN}%s${NC}\n" "$WP_ROOT" printf "Web User: ${GREEN}%s${NC}\n" "$WEB_USER" printf "Web Group: ${GREEN}%s${NC}\n" "$WEB_GROUP" printf "\n" printf "${YELLOW}Admin Credentials:${NC}\n" printf " Username: ${GREEN}%s${NC}\n" "$WP_ADMIN_USER" printf " Password: ${YELLOW}%s${NC} (Keep this safe!)\n" "$WP_ADMIN_PASS" printf " Email: ${GREEN}%s${NC}\n" "$WP_ADMIN_EMAIL" printf "\n" printf "${YELLOW}Database Credentials:${NC}\n" printf " Database Name: ${GREEN}%s${NC}\n" "$DB_NAME" printf " Username: ${GREEN}%s${NC}\n" "$DB_USER" printf " Password: ${YELLOW}%s${NC} (Keep this safe!)\n" "$DB_PASSWORD" printf " Host: ${GREEN}%s${NC}\n" "$DB_HOST" if [[ "$PERFORM_DB_ROOT_RESET" == "true" ]]; then printf "\n${RED}IMPORTANT: The MySQL/MariaDB root password was reset during this process.${NC}\n" printf " New Root Pass: ${YELLOW}%s${NC} (Keep this safe!)\n" "$DB_ROOT_PASS" fi printf "%s\n" "---------------------------" # Explicitly call cleanup before final exit (trap should also handle it) cleanup exit 0