#!/bin/bash # # Script to automate WordPress installation. # Includes dependency checks, WP-CLI installation, database setup, # WordPress core installation, and configuration. # v2.1 - Fixes WP-CLI execution path issues when run as root. # # --- Configuration --- # Exit immediately if a command exits with a non-zero status. set -e # Treat unset variables as an error when substituting. set -u # Pipe commands return the exit status of the last command in the pipe set -o pipefail # Colors for output messages RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # --- Default Values --- WP_ADMIN_USER="" WP_ADMIN_PASS="" WP_ADMIN_EMAIL="" WP_ROOT="/var/www/webroot/ROOT" # Default WordPress root directory DOMAIN="" # Domain will be determined or required DB_HOST="127.0.0.1" DB_ROOT_USER="root" DB_ROOT_PASS="" # Require user to provide this for security # PERFORM_DB_ROOT_RESET is set during arg parsing WEB_USER="litespeed" # Web server user WEB_GROUP="litespeed" # Web server group WP_CLI_PATH="/usr/local/bin/wp" # Path to WP-CLI executable # --- Helper Functions --- # Print informational messages info() { printf "${BLUE}[INFO] %s${NC}\n" "$@" } # Print success messages success() { printf "${GREEN}[SUCCESS] %s${NC}\n" "$@" } # Print warning messages warning() { printf "${YELLOW}[WARNING] %s${NC}\n" "$@" } # Print error messages and exit error_exit() { printf "${RED}[ERROR] %s${NC}\n" "$@" >&2 # Attempt cleanup before exiting cleanup &> /dev/null || true exit 1 } # Function to display usage information usage() { printf "Usage: %s [OPTIONS]\n" "$0" printf "\n" printf "Automates the installation of WordPress.\n" printf "\n" printf "Required Options:\n" printf " --wpusername=USERNAME WordPress admin username (mandatory)\n" printf " --wppassword=PASSWORD WordPress admin password (mandatory)\n" printf " --wpemail=EMAIL WordPress admin email (mandatory)\n" # Make dbrootpass conditional in description printf " --dbrootpass=PASSWORD Current MySQL/MariaDB root password (required IF NOT using --reset-db-root-pass)\n" printf "\n" printf "Optional Options:\n" printf " --wproot=PATH WordPress installation directory (default: %s)\n" "$WP_ROOT" printf " --domain=DOMAIN Domain name for the site (default: auto-detected from hostname)\n" printf " --webuser=USER Web server user (default: %s)\n" "$WEB_USER" printf " --webgroup=GROUP Web server group (default: %s)\n" "$WEB_GROUP" printf " --dbhost=HOST Database host (default: %s)\n" "$DB_HOST" printf " --reset-db-root-pass Perform the risky root password reset (requires script runner with root privileges)\n" printf " -h, --help Display this help message\n" printf "\n" printf "Example:\n" printf " %s --wpusername=myuser --wppassword='securePass' --wpemail=me@example.com --dbrootpass='currentRootPass'\n" "$0" printf " %s --wpusername=myuser --wppassword='securePass' --wpemail=me@example.com --reset-db-root-pass --domain=example.com\n" "$0" exit 1 } # Function to check if a command exists command_exists() { command -v "$1" &> /dev/null } # Function to generate a random secure password generate_password() { openssl rand -base64 16 } # Function to clean up temporary files # Define WP_CLI_CONFIG_PATH early so cleanup function knows about it WP_CLI_CONFIG_PATH="/tmp/wp-cli-config-$RANDOM.yml" cleanup() { # Check if file exists before trying to remove if [[ -n "${WP_CLI_CONFIG_PATH-}" && -f "$WP_CLI_CONFIG_PATH" ]]; then info "Cleaning up temporary WP-CLI config: $WP_CLI_CONFIG_PATH" rm -f "$WP_CLI_CONFIG_PATH" fi } # --- Argument Parsing --- TEMP=$(getopt -o h --longoptions help,wpusername:,wppassword:,wpemail:,wproot:,domain:,dbhost:,dbrootpass:,reset-db-root-pass,webuser:,webgroup: -n "$0" -- "$@") if [ $? != 0 ]; then error_exit "Terminating... Invalid arguments provided." fi eval set -- "$TEMP" unset TEMP PERFORM_DB_ROOT_RESET="false" # Default value while true; do case "$1" in --wpusername) WP_ADMIN_USER="$2"; shift 2 ;; --wppassword) WP_ADMIN_PASS="$2"; shift 2 ;; --wpemail) WP_ADMIN_EMAIL="$2"; shift 2 ;; --wproot) WP_ROOT="$2"; shift 2 ;; --domain) DOMAIN="$2"; shift 2 ;; --dbhost) DB_HOST="$2"; shift 2 ;; --dbrootpass) DB_ROOT_PASS="$2"; shift 2 ;; --reset-db-root-pass) PERFORM_DB_ROOT_RESET="true"; shift 1 ;; --webuser) WEB_USER="$2"; shift 2 ;; --webgroup) WEB_GROUP="$2"; shift 2 ;; -h|--help) usage ;; --) shift ; break ;; *) error_exit "Internal error parsing options! Unexpected option: $1";; esac done # Set trap *after* WP_CLI_CONFIG_PATH is defined and potentially used trap cleanup EXIT SIGINT SIGTERM # --- Validation --- info "Validating parameters..." if [[ -z "$WP_ADMIN_USER" ]]; then error_exit "WordPress admin username (--wpusername) is required."; fi if [[ -z "$WP_ADMIN_PASS" ]]; then error_exit "WordPress admin password (--wppassword) is required."; fi if [[ -z "$WP_ADMIN_EMAIL" ]]; then error_exit "WordPress admin email (--wpemail) is required."; fi if [[ ! "$WP_ADMIN_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then error_exit "Invalid email format for --wpemail: '$WP_ADMIN_EMAIL'"; fi if [[ "$PERFORM_DB_ROOT_RESET" == "false" && -z "$DB_ROOT_PASS" ]]; then error_exit "Database root password (--dbrootpass) is required unless --reset-db-root-pass is used."; fi if [[ "$PERFORM_DB_ROOT_RESET" == "true" && -n "$DB_ROOT_PASS" ]]; then warning "Both --reset-db-root-pass and --dbrootpass provided. Reset will be performed, provided password ignored."; fi if [[ ! -d "$WP_ROOT" ]]; then error_exit "WordPress root directory '$WP_ROOT' does not exist or is not a directory."; fi if ! id "$WEB_USER" &>/dev/null; then error_exit "Web user '$WEB_USER' does not exist."; fi if ! getent group "$WEB_GROUP" &>/dev/null; then error_exit "Web group '$WEB_GROUP' does not exist."; fi success "Parameters validated." # --- Determine Domain --- if [[ -z "$DOMAIN" ]]; then if ! command_exists hostname; then error_exit "'hostname' command not found. Please install it or specify --domain."; fi FULL_HOSTNAME=$(hostname -f) DOMAIN=$(echo "$FULL_HOSTNAME" | sed -E 's/^(node[0-9]*-|wp-|web-|host-|localhost)//') # Slightly more aggressive cleaning if [[ -z "$DOMAIN" || "$DOMAIN" == "$FULL_HOSTNAME" || "$DOMAIN" == "localdomain" ]]; then # Handle cases where sed didn't change much warning "Could not reliably determine a public domain from hostname '$FULL_HOSTNAME'. Using it as is." DOMAIN="$FULL_HOSTNAME" # Consider erroring out if domain detection is critical and fails # error_exit "Failed to determine domain automatically. Please specify using --domain." fi info "Auto-detected domain: $DOMAIN (Use --domain to override if incorrect)" else info "Using specified domain: $DOMAIN" fi # --- Dependency Checks --- info "Checking dependencies..." # Group related checks if ! command_exists php; then error_exit "'php' command not found. Please install PHP."; fi if ! command_exists mysql; then error_exit "'mysql' command (client) not found. Please install MySQL/MariaDB client."; fi if ! command_exists curl; then error_exit "'curl' command not found. Please install curl."; fi if ! command_exists openssl; then error_exit "'openssl' command not found. Please install openssl."; fi if ! command_exists getopt; then error_exit "'getopt' command not found. Please install getopt."; fi if ! command_exists hostname; then error_exit "'hostname' command not found. Please install hostname or provide --domain."; fi if ! command_exists sed; then error_exit "'sed' command not found. Please install sed."; fi # Sudo needed for WP-CLI install/move and potentially for DB reset/permissions if ! command_exists sudo; then error_exit "'sudo' command not found. Sudo is required."; fi # Checks specific to DB reset path if [[ "$PERFORM_DB_ROOT_RESET" == "true" ]]; then if ! command_exists systemctl; then error_exit "'systemctl' command not found, but required for --reset-db-root-pass."; fi if ! command_exists pkill; then error_exit "'pkill' command not found, but required for --reset-db-root-pass."; fi # Check for mysqld_safe OR the actual daemon binary, as mysqld_safe might be deprecated if ! command_exists mysqld_safe && ! command_exists mariadbd && ! command_exists mysqld; then error_exit "'mysqld_safe' or 'mariadbd'/'mysqld' not found, required for --reset-db-root-pass."; fi # Check if we actually *can* use sudo for systemctl - requires root privileges if [[ "$(id -u)" -ne 0 ]] && ! sudo -n systemctl is-active mariadb &>/dev/null; then warning "Cannot run 'sudo systemctl' without password. DB reset requires script runner with root privileges or passwordless sudo for systemctl." # Consider making this an error_exit depending on strictness fi fi success "All essential dependencies found." # --- WP-CLI Setup --- # Check if WP-CLI executable exists at the defined path if [[ ! -x "$WP_CLI_PATH" ]]; then info "WP-CLI not found at $WP_CLI_PATH or not executable. Attempting installation..." TEMP_WP_CLI="./wp-cli.phar" if ! curl -o "$TEMP_WP_CLI" https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar; then error_exit "Failed to download WP-CLI." fi chmod +x "$TEMP_WP_CLI" # Attempt to move using sudo. This requires sudo privileges. if ! sudo mv "$TEMP_WP_CLI" "$WP_CLI_PATH"; then # Cleanup downloaded file if move failed rm -f "$TEMP_WP_CLI" error_exit "Failed to move WP-CLI to $WP_CLI_PATH. Check sudo permissions for the move command." fi # Verify installation by checking the target path again if [[ ! -x "$WP_CLI_PATH" ]]; then error_exit "WP-CLI installation failed unexpectedly. $WP_CLI_PATH not found or not executable after move." fi success "WP-CLI installed successfully to $WP_CLI_PATH" else success "WP-CLI is already installed at $WP_CLI_PATH." fi # --- WP-CLI Execution Context Setup --- # Determine how WP-CLI commands should be run (user, flags) # This logic prioritizes running as root if available, to avoid sudo PATH issues. WP_RUN_ARGS=("--path=$WP_ROOT") SUDO_CMD="" # Command prefix (e.g., sudo -u user) - empty by default WP_EXECUTABLE="$WP_CLI_PATH" # Use the full path to wp-cli # Check if the script is running as root (UID 0) if [[ "$(id -u)" -eq 0 ]]; then info "Script is running as root. Using --allow-root for WP-CLI commands." # Avoid adding flag if somehow already present (e.g., future modification) if [[ ! " ${WP_RUN_ARGS[@]} " =~ " --allow-root " ]]; then WP_RUN_ARGS+=("--allow-root") fi # No SUDO_CMD needed, root executes directly else # Script is NOT running as root. Check if it's running as the target web user. if [[ "$(id -u)" -eq "$(id -u "$WEB_USER")" ]]; then info "Script is running as the web user ('$WEB_USER'). No sudo or --allow-root needed." # No SUDO_CMD needed, correct user executes directly else # Running as a different non-root user. Need to try `sudo -u WEB_USER`. info "Script running as non-root user '$(id -un)'. Attempting to run WP-CLI as '$WEB_USER' via sudo." if sudo -n -u "$WEB_USER" "$WP_EXECUTABLE" --info --skip-update --quiet "${WP_RUN_ARGS[@]}" &>/dev/null; then info "Successfully verified ability to run WP-CLI as '$WEB_USER' using sudo." SUDO_CMD="sudo -u $WEB_USER" # Keep WP_EXECUTABLE as full path else # Cannot run as root, cannot run as web_user, cannot sudo -u web_user without password. error_exit "Script lacks permissions. Run as root, as '$WEB_USER', or ensure user '$(id -un)' has passwordless sudo access to run '$WP_EXECUTABLE' as '$WEB_USER'." fi fi fi info "WP-CLI execution command prefix: [${SUDO_CMD:-direct}] using executable [$WP_EXECUTABLE]" info "WP-CLI arguments: ${WP_RUN_ARGS[*]}" # --- Temporary WP-CLI Config for Domain --- # Create config file for WP-CLI to potentially pick up domain context # This helps if WP-CLI struggles to identify the site URL, especially with --allow-root # Might not be strictly necessary if WP_HOME/WP_SITEURL are defined early enough # but provides extra robustness. info "Creating temporary WP-CLI config at $WP_CLI_CONFIG_PATH" cat > "$WP_CLI_CONFIG_PATH" < /dev/null FLUSH PRIVILEGES; ALTER USER 'root'@'localhost' IDENTIFIED BY '$new_root_password'; ALTER USER 'root'@'127.0.0.1' IDENTIFIED BY '$new_root_password'; FLUSH PRIVILEGES; EXIT EOF then warning "Failed 'ALTER USER' reset attempt (may be normal). Trying alternative syntax..." # Try the mysql_native_password plugin explicitly if ! sudo mysql --protocol=socket -u root <<-EOF &> /dev/null FLUSH PRIVILEGES; ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '$new_root_password'; ALTER USER 'root'@'127.0.0.1' IDENTIFIED WITH mysql_native_password BY '$new_root_password'; FLUSH PRIVILEGES; EXIT EOF then warning "Failed with native password plugin. Trying legacy 'UPDATE' method..." # Fallback for older MySQL/MariaDB versions if ! sudo mysql --protocol=socket -u root <<-EOF &> /dev/null FLUSH PRIVILEGES; UPDATE mysql.user SET Password=PASSWORD('$new_root_password') WHERE User='root' AND Host='localhost'; UPDATE mysql.user SET Password=PASSWORD('$new_root_password') WHERE User='root' AND Host='127.0.0.1'; FLUSH PRIVILEGES; EXIT EOF then warning "Failed legacy 'UPDATE' reset method. Trying direct 'authentication_string' update (MariaDB/newer MySQL)..." if ! sudo mysql --protocol=socket -u root <<-EOF &> /dev/null FLUSH PRIVILEGES; UPDATE mysql.user SET authentication_string=PASSWORD('$new_root_password') WHERE User='root'; FLUSH PRIVILEGES; EXIT EOF then error_exit "All attempts to reset the root password in safe mode failed. Check MySQL/MariaDB logs. Manual intervention required." fi fi fi fi success "Root password likely reset in safe mode." info "Stopping MariaDB safe mode process (PID: $MYSQLD_SAFE_PID)..." # Use sudo to kill processes started by root sudo kill "$MYSQLD_SAFE_PID" || warning "Failed to kill specific PID $MYSQLD_SAFE_PID (maybe already stopped?)." sleep 2 info "Attempting broader pkill for lingering safe mode processes..." sudo pkill -f mysqld_safe || sudo pkill -f mariadbd-safe || true # Ignore errors if not found sleep 2 info "Attempting broader pkill for lingering database daemons..." sudo pkill -f mariadbd || sudo pkill -f mysqld || true # Ignore errors if not found sleep 3 success "Safe mode processes likely stopped." info "Starting $DB_SERVICE_NAME service normally..." if ! sudo systemctl start "$DB_SERVICE_NAME"; then error_exit "Failed to start $DB_SERVICE_NAME service after password reset. Check status/logs."; fi sleep 5 # Allow time for service to initialize info "Verifying $DB_SERVICE_NAME service status..." if ! sudo systemctl is-active --quiet "$DB_SERVICE_NAME"; then error_exit "$DB_SERVICE_NAME service failed to start or become active. Check service status."; fi success "$DB_SERVICE_NAME service started successfully with new root password." DB_ROOT_PASS="$new_root_password" # Use the newly set password for subsequent operations else info "Using provided root password to create database and user." # Test connection with provided root password if ! mysql -u "$DB_ROOT_USER" -p"$DB_ROOT_PASS" -h "$DB_HOST" -e "SELECT 1;" &> /dev/null; then error_exit "Failed to connect to database using provided root credentials. Check user '$DB_ROOT_USER', password, and host '$DB_HOST'." fi success "Database root connection successful." fi # --- Create WordPress Database and User --- info "Creating WordPress database '$DB_NAME' and user '$DB_USER'..." # Use printf for safer password injection into the command SQL_COMMAND=$(printf "CREATE DATABASE IF NOT EXISTS \`%s\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS '%s'@'%s' IDENTIFIED BY '%s'; GRANT ALL PRIVILEGES ON \`%s\`.* TO '%s'@'%s'; FLUSH PRIVILEGES;" \ "$DB_NAME" "$DB_USER" "$DB_HOST" "$DB_PASSWORD" "$DB_NAME" "$DB_USER" "$DB_HOST") # Try connecting to localhost via socket first if TCP connection fails if ! mysql -u "$DB_ROOT_USER" -p"$DB_ROOT_PASS" -h "$DB_HOST" -e "$SQL_COMMAND"; then warning "Failed to connect via TCP ($DB_HOST). Trying socket connection to localhost..." if ! mysql -u "$DB_ROOT_USER" -p"$DB_ROOT_PASS" --protocol=socket -e "$SQL_COMMAND"; then # If socket fails too, try with no password (in case reset made blank password) warning "Socket connection failed too. Trying without password..." if ! mysql -u "$DB_ROOT_USER" --protocol=socket -e "$SQL_COMMAND"; then error_exit "Failed to execute SQL command to create WordPress database/user. Check MySQL/MariaDB logs and permissions for '$DB_ROOT_USER'." else warning "Connected without password! MySQL root has no password now." # Update the root password again to be sure SECURE_ROOT_SQL="ALTER USER 'root'@'localhost' IDENTIFIED BY '$DB_ROOT_PASS'; FLUSH PRIVILEGES;" mysql -u "$DB_ROOT_USER" --protocol=socket -e "$SECURE_ROOT_SQL" || warning "Could not secure root user with password!" fi fi success "Database '$DB_NAME' and user '$DB_USER' created successfully via socket connection." else success "Database '$DB_NAME' and user '$DB_USER' created successfully." fi # --- WordPress Core File Setup --- info "Ensuring WordPress files are present in: $WP_ROOT" cd "$WP_ROOT" || error_exit "Failed to change directory to $WP_ROOT" # Backup existing wp-config.php if it exists if [[ -f "wp-config.php" ]]; then BACKUP_NAME="wp-config.php.bak.$(date +%Y%m%d_%H%M%S)" # Use underscore in timestamp info "Backing up existing wp-config.php to $BACKUP_NAME" cp wp-config.php "$BACKUP_NAME" || warning "Failed to backup wp-config.php" fi # Download WordPress core files ONLY if key files/dirs are missing if [[ ! -f "index.php" || ! -f "wp-includes/version.php" || ! -d "wp-admin" ]]; then info "WordPress core files seem missing or incomplete. Downloading..." # Use determined $SUDO_CMD, $WP_EXECUTABLE, and $WP_RUN_ARGS if ! $SUDO_CMD $WP_EXECUTABLE core download "${WP_RUN_ARGS[@]}" --skip-content --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 <