mb-admin/scripts/install-wordpress.sh

1072 lines
49 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/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
NEEDS_OWNERSHIP_CORRECTION="false" # Flag to track if ownership correction is needed
# --- 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 correct ownership after WP-CLI operations if needed
correct_ownership_if_needed() {
if [[ "$NEEDS_OWNERSHIP_CORRECTION" == "true" ]]; then
info "Correcting file ownership after WP-CLI operation..."
if ! sudo chown -R "${WEB_USER}:${WEB_GROUP}" "$WP_ROOT" 2>/dev/null; then
warning "Failed to correct some file ownership. Some files may still be owned by root."
fi
fi
}
# 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. Files will be corrected to proper ownership after creation."
WP_RUN_ARGS+=("--allow-root")
# Set flag to indicate we need ownership correction after WP-CLI operations
NEEDS_OWNERSHIP_CORRECTION="true"
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" <<EOF || error_exit "Failed to create temporary WP-CLI config file."
# Temporary config for WP-CLI execution context
# Ensures WP-CLI understands the target domain, especially when run via cron or as root.
apache_modules:
- mod_rewrite
_:
server:
HTTP_HOST: $DOMAIN
HTTPS: on # Assume HTTPS, adjust if site uses HTTP
# If using --allow-root, pass it here too, though it's also in WP_RUN_ARGS
# allow-root: true # Uncomment if needed, but should be covered by WP_RUN_ARGS
EOF
# Ensure the config file is readable by the user executing WP-CLI
if [[ -n "$SUDO_CMD" ]]; then
# If using sudo -u, the target user needs to read it. Group readability might suffice.
chmod 640 "$WP_CLI_CONFIG_PATH" # Owner read/write, group read
# Attempt to chown group, ignore failure as permissions might be sufficient
chgrp "$WEB_GROUP" "$WP_CLI_CONFIG_PATH" || true
else
# Running as current user (root or web_user), user readability is enough
chmod 600 "$WP_CLI_CONFIG_PATH" # Owner read/write only
fi
# --- Database Setup ---
info "Setting up database..."
DB_NAME="wpdb_$(openssl rand -hex 4)"
DB_USER="wpuser_$(openssl rand -hex 4)"
DB_PASSWORD=$(generate_password)
if [[ "$PERFORM_DB_ROOT_RESET" == "true" ]]; then
# --- Risky Root Password Reset ---
# This section requires the script runner to have root privileges or passwordless sudo for systemctl etc.
warning "Attempting to reset MariaDB/MySQL root password. This is risky!"
new_root_password=$(generate_password)
info "New root password will be: $new_root_password"
DB_SERVICE_NAME="mariadb" # Common default, might need adjustment (e.g., mysql)
info "Attempting to stop $DB_SERVICE_NAME service..."
# Use sudo explicitly as service management requires root
if ! sudo systemctl stop "$DB_SERVICE_NAME"; then error_exit "Failed to stop $DB_SERVICE_NAME service. Check status/logs."; fi
info "$DB_SERVICE_NAME service stopped."
sleep 3
info "Starting $DB_SERVICE_NAME in safe mode (skip-grant-tables)..."
# Find appropriate command
MYSQLD_SAFE_CMD=""
if command_exists mysqld_safe; then MYSQLD_SAFE_CMD="mysqld_safe";
elif command_exists mariadbd-safe; then MYSQLD_SAFE_CMD="mariadbd-safe"; # Some distros use this
elif command_exists mariadbd; then MYSQLD_SAFE_CMD="mariadbd"; # Daemon directly might work
elif command_exists mysqld; then MYSQLD_SAFE_CMD="mysqld";
else error_exit "Cannot find mysqld_safe, mariadbd-safe, mariadbd, or mysqld command for safe mode."; fi
# Use sudo explicitly for safe mode start
sudo "$MYSQLD_SAFE_CMD" --skip-grant-tables --skip-networking &
MYSQLD_SAFE_PID=$!
info "$MYSQLD_SAFE_CMD started in background (PID: $MYSQLD_SAFE_PID). Waiting for it to initialize..."
sleep 10 # Allow generous time for safe mode startup
# Generate a simpler password without special characters to avoid auth issues
new_root_password=$(openssl rand -base64 12 | tr -dc 'a-zA-Z0-9' | head -c 16)
info "Using simplified password format: $new_root_password"
info "Attempting to reset root password using mysql client..."
# Use sudo for mysql command connecting via socket as root
if ! sudo mysql --protocol=socket -u root <<-EOF &> /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."
correct_ownership_if_needed
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 <<EOF
define( 'AUTH_KEY', '$(generate_password)' );
define( 'SECURE_AUTH_KEY', '$(generate_password)' );
define( 'LOGGED_IN_KEY', '$(generate_password)' );
define( 'NONCE_KEY', '$(generate_password)' );
define( 'AUTH_SALT', '$(generate_password)' );
define( 'SECURE_AUTH_SALT', '$(generate_password)' );
define( 'LOGGED_IN_SALT', '$(generate_password)' );
define( 'NONCE_SALT', '$(generate_password)' );
EOF
)
}
# Check if salts were actually generated
if [[ -z "$SALTS" ]]; then
error_exit "Failed to generate WordPress salts using both WP-CLI and fallback."
fi
success "WordPress salts generated."
# Use cat with heredoc for wp-config.php creation
cat > wp-config.php <<EOF || error_exit "Failed to write wp-config.php file."
<?php
/**
* The base configuration for WordPress
* @link https://wordpress.org/support/article/editing-wp-config-php/
* @package WordPress
*/
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', '${DB_NAME}' );
/** Database username */
define( 'DB_USER', '${DB_USER}' );
/** Database password */
define( 'DB_PASSWORD', '${DB_PASSWORD}' );
/** Database hostname */
define( 'DB_HOST', '${DB_HOST}' );
/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );
/** The database collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', '' );
/**#@+
* Authentication unique keys and salts.
* @since 2.6.0
*/
${SALTS}
/**#@-*/
/**
* WordPress database table prefix.
*/
\$table_prefix = 'wp_'; // Default prefix
/**
* For developers: WordPress debugging mode.
*/
define( 'WP_DEBUG', false ); // Set to true for development
/* Add any custom values between this line and the "stop editing" line. */
// Memory Limits
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' ); // For admin area
// Updates
define( 'WP_AUTO_UPDATE_CORE', false ); // Recommended: 'minor' or true for security
// File Editing
define( 'DISALLOW_FILE_EDIT', true ); // Security enhancement
// Define site URL and home URL (ensure protocol matches server setup)
define( 'WP_HOME', 'https://${DOMAIN}' );
define( 'WP_SITEURL', 'https://${DOMAIN}' );
// If using a reverse proxy handling SSL, uncomment the following:
// if (isset(\$_SERVER['HTTP_X_FORWARDED_PROTO']) && \$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
// \$_SERVER['HTTPS'] = 'on';
// }
// Fix missing HTTP_HOST for CLI operations (redundant if using WP_CLI_CONFIG_PATH, but safe)
if ( defined( 'WP_CLI' ) && WP_CLI && ! isset( \$_SERVER['HTTP_HOST'] ) ) {
\$_SERVER['HTTP_HOST'] = '${DOMAIN}';
}
/* That's all, stop editing! Happy publishing. */
/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
/** Sets up WordPress vars and included files. */
require_once ABSPATH . 'wp-settings.php';
EOF
success "wp-config.php created successfully."
# --- Set Permissions ---
info "Setting file permissions for $WP_ROOT..."
# Use sudo explicitly as permissions/ownership changes usually require root
# Set ownership first - recursively
info "Setting ownership to ${WEB_USER}:${WEB_GROUP}..."
if ! sudo chown -R "${WEB_USER}:${WEB_GROUP}" "$WP_ROOT"; then
# Don't exit, maybe some files failed, but core install might still work
warning "Failed to set ownership on some files/dirs within $WP_ROOT. Check permissions."
fi
# Set directory and file permissions
info "Setting directory permissions to 755 and file permissions to 644..."
# Use sudo find. Requires runner (root) to have read access everywhere.
if ! sudo find "$WP_ROOT" -type d -exec chmod 755 {} \; ; then
warning "Could not set directory permissions using find. Check permissions."
fi
if ! sudo find "$WP_ROOT" -type f -exec chmod 644 {} \; ; then
warning "Could not set file permissions using find. Check permissions."
fi
# Ensure wp-config.php is more secure (readable only by owner and group)
info "Setting specific permissions for wp-config.php to 640..."
if [[ -f "wp-config.php" ]]; then
if ! sudo chmod 640 wp-config.php; then
warning "Could not set specific permissions (640) on wp-config.php."
fi
else
warning "wp-config.php not found during permission setting phase."
fi
success "File permissions set (potential warnings noted)."
# --- WordPress Core Installation ---
info "Checking if WordPress is already installed..."
# Use determined $SUDO_CMD, $WP_EXECUTABLE, and $WP_RUN_ARGS
if ! $SUDO_CMD $WP_EXECUTABLE core is-installed "${WP_RUN_ARGS[@]}"; then
info "WordPress is not installed. Proceeding with installation..."
# Use determined $SUDO_CMD, $WP_EXECUTABLE, and $WP_RUN_ARGS
if ! $SUDO_CMD $WP_EXECUTABLE core install \
--url="https://${DOMAIN}" \
--title="My WordPress Site on $DOMAIN" \
--admin_user="$WP_ADMIN_USER" \
--admin_password="$WP_ADMIN_PASS" \
--admin_email="$WP_ADMIN_EMAIL" \
--skip-email \
"${WP_RUN_ARGS[@]}"; then
error_exit "WordPress core installation failed using WP-CLI."
fi
correct_ownership_if_needed
info "Removing default plugins (Akismet, Hello Dolly)..."
# Use determined $SUDO_CMD, $WP_EXECUTABLE, and $WP_RUN_ARGS
$SUDO_CMD $WP_EXECUTABLE plugin delete akismet hello "${WP_RUN_ARGS[@]}" --quiet || warning "Could not delete default plugins (might not exist)."
correct_ownership_if_needed
# Install LiteSpeed Cache plugin (without activating yet)
info "Installing LiteSpeed Cache plugin..."
if $SUDO_CMD $WP_EXECUTABLE plugin install litespeed-cache "${WP_RUN_ARGS[@]}"; then
success "LiteSpeed Cache plugin installed successfully."
# Create necessary LiteSpeed Cache directories BEFORE activation
info "Creating LiteSpeed Cache directories before activation..."
LITESPEED_CACHE_DIR="$WP_ROOT/wp-content/litespeed"
if [[ ! -d "$LITESPEED_CACHE_DIR" ]]; then
sudo mkdir -p "$LITESPEED_CACHE_DIR"
sudo chown -R "${WEB_USER}:${WEB_GROUP}" "$LITESPEED_CACHE_DIR"
sudo chmod -R 755 "$LITESPEED_CACHE_DIR"
success "LiteSpeed Cache directory created: $LITESPEED_CACHE_DIR"
else
info "LiteSpeed Cache directory already exists: $LITESPEED_CACHE_DIR"
fi
# Create subdirectories that the plugin commonly uses
for subdir in css js img tmp; do
SUBDIR_PATH="$LITESPEED_CACHE_DIR/$subdir"
if [[ ! -d "$SUBDIR_PATH" ]]; then
sudo mkdir -p "$SUBDIR_PATH"
sudo chown -R "${WEB_USER}:${WEB_GROUP}" "$SUBDIR_PATH"
sudo chmod -R 755 "$SUBDIR_PATH"
fi
done
# Now activate the plugin after directories are ready
info "Activating LiteSpeed Cache plugin..."
if $SUDO_CMD $WP_EXECUTABLE plugin activate litespeed-cache "${WP_RUN_ARGS[@]}"; then
success "LiteSpeed Cache plugin activated successfully."
correct_ownership_if_needed
else
warning "Failed to activate LiteSpeed Cache plugin after installation."
fi
# Configure basic LiteSpeed Cache settings
info "Configuring basic LiteSpeed Cache settings..."
# Enable cache
$SUDO_CMD $WP_EXECUTABLE option update litespeed.conf.cache 1 "${WP_RUN_ARGS[@]}" || warning "Could not enable LiteSpeed cache"
# Set cache TTL to 1 week (604800 seconds)
$SUDO_CMD $WP_EXECUTABLE option update litespeed.conf.cache-ttl_pub 604800 "${WP_RUN_ARGS[@]}" || warning "Could not set public cache TTL"
# Enable CSS/JS optimization
$SUDO_CMD $WP_EXECUTABLE option update litespeed.conf.optm-css_min 1 "${WP_RUN_ARGS[@]}" || warning "Could not enable CSS minification"
$SUDO_CMD $WP_EXECUTABLE option update litespeed.conf.optm-js_min 1 "${WP_RUN_ARGS[@]}" || warning "Could not enable JS minification"
# Enable image optimization (WebP)
$SUDO_CMD $WP_EXECUTABLE option update litespeed.conf.media-webp 1 "${WP_RUN_ARGS[@]}" || warning "Could not enable WebP conversion"
# Enable lazy loading for images
$SUDO_CMD $WP_EXECUTABLE option update litespeed.conf.media-lazy 1 "${WP_RUN_ARGS[@]}" || warning "Could not enable lazy loading"
# Enable browser cache
$SUDO_CMD $WP_EXECUTABLE option update litespeed.conf.cache-browser 1 "${WP_RUN_ARGS[@]}" || warning "Could not enable browser cache"
success "LiteSpeed Cache basic configuration completed."
else
warning "Failed to install LiteSpeed Cache plugin. You can install it manually from the WordPress admin."
fi
# Ensure a default theme is activated (uses Twenty Twenty-Four or fallback to any available theme)
info "Ensuring a default theme is activated..."
if $SUDO_CMD $WP_EXECUTABLE theme is-installed twentytwentyfour "${WP_RUN_ARGS[@]}"; then
$SUDO_CMD $WP_EXECUTABLE theme activate twentytwentyfour "${WP_RUN_ARGS[@]}" || warning "Could not activate Twenty Twenty-Four theme"
else
# Get first available theme and activate it
FIRST_THEME=$($SUDO_CMD $WP_EXECUTABLE theme list --status=inactive --field=name --format=csv "${WP_RUN_ARGS[@]}" | head -n 1)
if [[ -n "$FIRST_THEME" ]]; then
info "Twenty Twenty-Four not found. Activating $FIRST_THEME theme instead."
$SUDO_CMD $WP_EXECUTABLE theme activate "$FIRST_THEME" "${WP_RUN_ARGS[@]}" || warning "Could not activate $FIRST_THEME theme"
fi
fi
correct_ownership_if_needed
# Create .htaccess file for WordPress permalink functionality
info "Creating .htaccess file for URL rewriting..."
if [[ ! -f ".htaccess" ]]; then
# Define the path to the .htaccess template file
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HTACCESS_TEMPLATE="$SCRIPT_DIR/templates/htaccess.template"
# Create temporary .htaccess file
TEMP_HTACCESS=$(mktemp) || error_exit "Failed to create temporary file for .htaccess"
# Check if the template file exists and use it, otherwise use inline fallback
if [[ -f "$HTACCESS_TEMPLATE" ]]; then
info "Using external .htaccess template: $HTACCESS_TEMPLATE"
cp "$HTACCESS_TEMPLATE" "$TEMP_HTACCESS" || error_exit "Failed to copy .htaccess template to temporary file"
else
warning "External template not found at $HTACCESS_TEMPLATE, using inline template"
cat > "$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! ##
<IfModule LiteSpeed>
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 ###
</IfModule>
## 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.
<IfModule mod_rewrite.c>
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]
</IfModule>
# 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"
correct_ownership_if_needed
# 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.
<IfModule mod_rewrite.c>
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]
</IfModule>
# 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!"
# --- Final Ownership Correction ---
info "Performing final ownership correction to ensure all files are owned by ${WEB_USER}:${WEB_GROUP}..."
cd "$WP_ROOT" || error_exit "Failed to change directory to $WP_ROOT for final ownership correction"
# Comprehensive ownership fix for all WordPress files and directories
if ! sudo chown -R "${WEB_USER}:${WEB_GROUP}" "$WP_ROOT"; then
warning "Failed to set ownership on some files during final correction. Some files may still be owned by root."
else
success "Final ownership correction completed successfully."
fi
# Specifically ensure critical files are properly owned
critical_files=("wp-config.php" ".htaccess" "index.php")
for file in "${critical_files[@]}"; do
if [[ -f "$file" ]]; then
sudo chown "${WEB_USER}:${WEB_GROUP}" "$file" || warning "Failed to set ownership on $file"
fi
done
# Ensure LiteSpeed Cache directories have correct ownership if they exist
if [[ -d "wp-content/litespeed" ]]; then
info "Correcting LiteSpeed Cache directory ownership..."
sudo chown -R "${WEB_USER}:${WEB_GROUP}" "wp-content/litespeed" || warning "Failed to set ownership on LiteSpeed Cache directory"
fi
# Ensure uploads directory has correct ownership if it exists
if [[ -d "wp-content/uploads" ]]; then
info "Correcting uploads directory ownership..."
sudo chown -R "${WEB_USER}:${WEB_GROUP}" "wp-content/uploads" || warning "Failed to set ownership on uploads directory"
fi
# Final verification - show file ownership status
info "Verifying file ownership in WordPress root directory..."
if command_exists ls; then
info "Current file ownership in $WP_ROOT:"
ls -la "$WP_ROOT" | head -20 # Show first 20 files
# Count files with wrong ownership
wrong_ownership_count=$(find "$WP_ROOT" -maxdepth 2 \( ! -user "$WEB_USER" -o ! -group "$WEB_GROUP" \) -type f 2>/dev/null | wc -l)
if [[ "$wrong_ownership_count" -gt 0 ]]; then
warning "$wrong_ownership_count files still have incorrect ownership. You may need to run: sudo chown -R ${WEB_USER}:${WEB_GROUP} $WP_ROOT"
else
success "All files now have correct ownership (${WEB_USER}:${WEB_GROUP})"
fi
fi
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