#!/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) 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 # Prefer running WP-CLI as the designated web user so that any files it # creates/updates (e.g. .htaccess) are owned by that user instead of root. if [[ "$(id -u)" -eq 0 ]]; then info "Script is running as root. Attempting to run WP-CLI as '$WEB_USER' for correct file ownership." if sudo -n -u "$WEB_USER" "$WP_EXECUTABLE" --info --skip-update --quiet "${WP_RUN_ARGS[@]}" &>/dev/null; then SUDO_CMD="sudo -u $WEB_USER" info "WP-CLI will be executed via sudo as '$WEB_USER'." else warning "Failed to execute WP-CLI as '$WEB_USER' without password. Falling back to running as root with --allow-root. Resulting files may be owned by root." WP_RUN_ARGS+=("--allow-root") fi else # Script is NOT running as root. if [[ "$(id -u)" -eq "$(id -u "$WEB_USER")" ]]; then info "Script is already running as the web user ('$WEB_USER'). No sudo or --allow-root needed." else 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 SUDO_CMD="sudo -u $WEB_USER" info "Successfully configured sudo execution for WP-CLI as '$WEB_USER'." else error_exit "Unable to execute WP-CLI as '$WEB_USER'. Ensure the current user has passwordless sudo access, or run this script as root." 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 < "$TEMP_HTACCESS" <<'EOF' || error_exit "Failed to write .htaccess content to temporary 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 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 EOF fi # Move the temporary file to the final location with proper ownership sudo mv "$TEMP_HTACCESS" ".htaccess" || error_exit "Failed to move .htaccess file to final location" # Set appropriate permissions and ownership for .htaccess sudo chmod 644 .htaccess || error_exit "Failed to set permissions on .htaccess" sudo chown "${WEB_USER}:${WEB_GROUP}" .htaccess || error_exit "Failed to set ownership on .htaccess" # Verify the ownership was set correctly if [[ "$(stat -c '%U:%G' .htaccess)" == "${WEB_USER}:${WEB_GROUP}" ]]; then success ".htaccess file created and configured for LiteSpeed with proper ownership (${WEB_USER}:${WEB_GROUP})." else warning ".htaccess file created but ownership verification failed. Current ownership: $(stat -c '%U:%G' .htaccess)" 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" # WP-CLI operations above might have recreated or modified .htaccess as the user executing WP-CLI. # To enforce consistent ownership, reset it to the designated web user/group. if [[ -f ".htaccess" ]]; then sudo chown "${WEB_USER}:${WEB_GROUP}" .htaccess || warning "Failed to reset ownership on .htaccess after WP-CLI operations." fi success "WordPress installed successfully via WP-CLI." # ------------------------------------------------------------------ # Final check: ensure .htaccess has the WordPress rewrite directives # Some plugins (e.g., LiteSpeed Cache) may recreate .htaccess and drop # the default WordPress block. If it's missing, append it now. # ------------------------------------------------------------------ if [[ -f ".htaccess" ]] && ! grep -q "# BEGIN WordPress" .htaccess; then warning ".htaccess is missing WordPress rewrite rules – adding them." # Re-establish the path to the template in case this block is far # from the earlier variable scope. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HTACCESS_TEMPLATE="$SCRIPT_DIR/templates/htaccess.template" WORDPRESS_BLOCK="" if [[ -f "$HTACCESS_TEMPLATE" ]]; then WORDPRESS_BLOCK=$(awk '/# BEGIN WordPress/{flag=1} flag{print} /# END WordPress/{flag=0}' "$HTACCESS_TEMPLATE") fi # Fallback inline snippet if template missing or awk failed if [[ -z "$WORDPRESS_BLOCK" ]]; then read -r -d '' WORDPRESS_BLOCK <<'WPBLOCK' || true # 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 WPBLOCK fi # Append the block to .htaccess printf "%s\n" "$WORDPRESS_BLOCK" | sudo tee -a .htaccess > /dev/null || warning "Failed to append WordPress rules to .htaccess" sudo chown "${WEB_USER}:${WEB_GROUP}" .htaccess || warning "Failed to set ownership after appending WordPress rules." fi 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..." # Validate domain is properly set before proceeding if [[ -z "$DOMAIN" ]]; then error_exit "Domain variable is empty. Cannot proceed with SSL certificate generation." fi if [[ "$DOMAIN" == "localhost" || "$DOMAIN" == "localdomain" ]]; then warning "Domain is '$DOMAIN' which is not suitable for SSL certificates. Skipping SSL setup." info "You can manually configure SSL later or re-run with --domain=your-actual-domain.com" # Skip SSL section entirely SSL_SKIPPED=true else info "Using domain for SSL certificate: $DOMAIN" SSL_SKIPPED=false fi # Only proceed with SSL setup if domain is valid if [[ "$SSL_SKIPPED" != "true" ]]; then # 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..." # Check if certificate already exists if [[ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]]; then info "SSL certificate already exists for $DOMAIN. Checking if renewal is needed..." if sudo certbot renew --cert-name="$DOMAIN" --dry-run 2>/dev/null; then info "Existing SSL certificate is valid and not due for renewal." SSL_SUCCESS=true else info "Existing certificate needs renewal. Attempting to renew..." if sudo certbot renew --cert-name="$DOMAIN" --force-renewal 2>/dev/null; then SSL_SUCCESS=true else warning "Failed to renew existing SSL certificate." SSL_SUCCESS=false fi fi else # Generate new certificate if sudo certbot certonly \ --webroot \ --webroot-path="$WEBROOT_PATH" \ --email="$WP_ADMIN_EMAIL" \ --agree-tos \ --non-interactive \ --domains="$DOMAIN"; then SSL_SUCCESS=true else SSL_SUCCESS=false fi fi if [[ "$SSL_SUCCESS" == "true" ]]; then success "SSL certificate is ready 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 if (sudo crontab -l 2>/dev/null; echo "$CRON_JOB") | sudo crontab - 2>/dev/null; then success "Automatic SSL renewal configured (daily check at 12:00 PM)" else warning "Failed to configure automatic SSL renewal cron job" fi 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..." LITESPEED_RESTARTED=false if sudo systemctl is-active lshttpd &>/dev/null; then if sudo systemctl restart lshttpd 2>/dev/null; then success "LiteSpeed (lshttpd) restarted successfully" LITESPEED_RESTARTED=true else warning "Failed to restart lshttpd service" fi elif sudo systemctl is-active litespeed &>/dev/null; then if sudo systemctl restart litespeed 2>/dev/null; then success "LiteSpeed (litespeed) restarted successfully" LITESPEED_RESTARTED=true else warning "Failed to restart litespeed service" fi else warning "LiteSpeed service not detected or not running." fi if [[ "$LITESPEED_RESTARTED" != "true" ]]; then warning "LiteSpeed service restart failed or not attempted. 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 else info "SSL certificate setup skipped due to invalid domain." 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" printf "\n" printf "${YELLOW}LiteSpeed Cache Plugin:${NC}\n" printf " Status: ${GREEN}Installed and Activated${NC}\n" printf " Cache Enabled: ${GREEN}Yes${NC}\n" printf " Cache TTL: ${GREEN}1 week (604800 seconds)${NC}\n" printf " Optimizations: ${GREEN}CSS/JS minification, WebP, Lazy loading${NC}\n" printf " Admin Panel: ${GREEN}https://%s/wp-admin/admin.php?page=litespeed${NC}\n" "$DOMAIN" 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