mb-admin/scripts/install-wordpress.sh

861 lines
39 KiB
Bash

#!/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" <<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."
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
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)."
# Install and activate LiteSpeed Cache plugin
info "Installing LiteSpeed Cache plugin..."
if $SUDO_CMD $WP_EXECUTABLE plugin install litespeed-cache "${WP_RUN_ARGS[@]}" --activate; then
success "LiteSpeed Cache plugin installed and activated successfully."
# 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
# Create .htaccess file for WordPress permalink functionality
info "Creating .htaccess file for URL rewriting..."
if [[ ! -f ".htaccess" ]]; then
cat > .htaccess <<'EOF' || warning "Failed to create .htaccess 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 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 ###
</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
# 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.
<IfModule Litespeed>
SetEnv noabort 1
</IfModule>
# 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..."
# 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
# --- 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