#!/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 SKIP_OBJCACHE="false" # Flag to skip automatic Object Cache configuration # --- 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 " --skip-objcache Skip automatic LiteSpeed Object Cache with Redis configuration\n" printf " -h, --help Display this help message\n" printf "\n" printf "Examples:\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" printf " %s --wpusername=myuser --wppassword='securePass' --wpemail=me@example.com --dbrootpass='currentRootPass' --skip-objcache\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:,skip-objcache -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 ;; --skip-objcache) SKIP_OBJCACHE="true"; shift 1 ;; -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" < /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 < wp-config.php </dev/null 2>&1; then info "Redis socket available. Configuring with socket connection (optimal for single node)..." if bash "$OBJCACHE_SCRIPT" --enable --connection-type=socket; then success "LiteSpeed Object Cache configured with Redis socket successfully." else warning "Failed to configure LiteSpeed Object Cache with Redis socket. You can configure it manually later." fi elif redis-cli -h 127.0.0.1 -p 6379 ping >/dev/null 2>&1; then info "Redis TCP available. Configuring with TCP connection..." if bash "$OBJCACHE_SCRIPT" --enable --connection-type=tcp --redis-host=127.0.0.1 --redis-port=6379; then success "LiteSpeed Object Cache configured with Redis TCP successfully." else warning "Failed to configure LiteSpeed Object Cache with Redis TCP. You can configure it manually later." fi else warning "Redis is not available. LiteSpeed Object Cache not configured." warning "To configure later: bash $OBJCACHE_SCRIPT --enable" fi else warning "LiteSpeed Object Cache configuration script not found at: $OBJCACHE_SCRIPT" warning "You can configure Object Cache manually through the LiteSpeed admin panel." fi else info "Skipping automatic LiteSpeed Object Cache configuration (--skip-objcache specified)." info "You can configure Object Cache later using:" info "bash $(dirname "${BASH_SOURCE[0]}")/configure_litespeed_redis_object_cache.sh --enable" fi 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! ## RewriteEngine on CacheLookup on RewriteRule .* - [E=Cache-Control:no-autoflush] RewriteRule litespeed/debug/.*\.log$ - [F,L] RewriteRule \.litespeed_conf\.dat - [F,L] ### marker ASYNC start ### RewriteCond %{REQUEST_URI} /wp-admin/admin-ajax\.php RewriteCond %{QUERY_STRING} action=async_litespeed RewriteRule .* - [E=noabort:1] ### marker ASYNC end ### ### marker DROPQS start ### CacheKeyModify -qs:fbclid CacheKeyModify -qs:gclid CacheKeyModify -qs:utm* CacheKeyModify -qs:_ga ### marker DROPQS end ### ## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ## # END LSCACHE # BEGIN NON_LSCACHE ## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ## ## LITESPEED WP CACHE PLUGIN - Do not edit the contents of this block! ## # END NON_LSCACHE # BEGIN WordPress # The directives (lines) between "BEGIN WordPress" and "END WordPress" are # dynamically generated, and should only be modified via WordPress filters. # Any changes to the directives between these markers will be overwritten. RewriteEngine On RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] # END WordPress EOF fi # Move the temporary file to the final location with proper ownership sudo mv "$TEMP_HTACCESS" ".htaccess" || error_exit "Failed to move .htaccess file to final location" # Set appropriate permissions and ownership for .htaccess sudo chmod 644 .htaccess || error_exit "Failed to set permissions on .htaccess" sudo chown "${WEB_USER}:${WEB_GROUP}" .htaccess || error_exit "Failed to set ownership on .htaccess" # Verify the ownership was set correctly if [[ "$(stat -c '%U:%G' .htaccess)" == "${WEB_USER}:${WEB_GROUP}" ]]; then success ".htaccess file created and configured for LiteSpeed with proper ownership (${WEB_USER}:${WEB_GROUP})." else warning ".htaccess file created but ownership verification failed. Current ownership: $(stat -c '%U:%G' .htaccess)" fi else info ".htaccess file already exists. Skipping creation." fi # Set up pretty permalinks using WP-CLI info "Configuring WordPress permalink structure..." $SUDO_CMD $WP_EXECUTABLE rewrite structure '/%postname%/' "${WP_RUN_ARGS[@]}" || warning "Could not set permalink structure" $SUDO_CMD $WP_EXECUTABLE rewrite flush "${WP_RUN_ARGS[@]}" || warning "Could not flush rewrite rules" 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. RewriteEngine On RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] # END WordPress WPBLOCK fi # Append the block to .htaccess printf "%s\n" "$WORDPRESS_BLOCK" | sudo tee -a .htaccess > /dev/null || warning "Failed to append WordPress rules to .htaccess" sudo chown "${WEB_USER}:${WEB_GROUP}" .htaccess || warning "Failed to set ownership after appending WordPress rules." fi else info "WordPress is already installed according to WP-CLI." # Optionally update URL if needed (be careful with this, can break site if proxy/etc involved) # info "Verifying site URL options..." # $SUDO_CMD $WP_EXECUTABLE option update siteurl "https://$DOMAIN" "${WP_RUN_ARGS[@]}" || warning "Failed to update siteurl option" # $SUDO_CMD $WP_EXECUTABLE option update home "https://$DOMAIN" "${WP_RUN_ARGS[@]}" || warning "Failed to update home option" fi # --- Let's Encrypt SSL Certificate Setup --- info "Setting up Let's Encrypt SSL certificate..." # Validate domain is properly set before proceeding if [[ -z "$DOMAIN" ]]; then error_exit "Domain variable is empty. Cannot proceed with SSL certificate generation." fi if [[ "$DOMAIN" == "localhost" || "$DOMAIN" == "localdomain" ]]; then warning "Domain is '$DOMAIN' which is not suitable for SSL certificates. Skipping SSL setup." info "You can manually configure SSL later or re-run with --domain=your-actual-domain.com" # Skip SSL section entirely SSL_SKIPPED=true else info "Using domain for SSL certificate: $DOMAIN" SSL_SKIPPED=false fi # Only proceed with SSL setup if domain is valid if [[ "$SSL_SKIPPED" != "true" ]]; then # Install certbot if not present if ! command_exists certbot; then info "Installing certbot for Let's Encrypt certificate management..." if command_exists apt-get; then # Debian/Ubuntu sudo apt-get update -qq sudo apt-get install -y certbot python3-certbot-apache || error_exit "Failed to install certbot via apt-get" elif command_exists yum; then # CentOS/RHEL 7 sudo yum install -y epel-release sudo yum install -y certbot python3-certbot-apache || error_exit "Failed to install certbot via yum" elif command_exists dnf; then # CentOS/RHEL 8+/Fedora sudo dnf install -y certbot python3-certbot-apache || error_exit "Failed to install certbot via dnf" else warning "Package manager not detected. Please install certbot manually." info "You can install certbot using: wget https://dl.eff.org/certbot-auto && chmod a+x certbot-auto" fi else success "Certbot is already installed." fi # Generate SSL certificate if command_exists certbot; then info "Generating Let's Encrypt SSL certificate for domain: $DOMAIN" # Create a simple verification file for webroot authentication WEBROOT_PATH="$WP_ROOT" ACME_CHALLENGE_DIR="$WEBROOT_PATH/.well-known/acme-challenge" sudo mkdir -p "$ACME_CHALLENGE_DIR" sudo chown -R "${WEB_USER}:${WEB_GROUP}" "$WEBROOT_PATH/.well-known" sudo chmod -R 755 "$WEBROOT_PATH/.well-known" # Try webroot method first (non-interactive) info "Attempting SSL certificate generation using webroot method..." # Check if certificate already exists if [[ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]]; then info "SSL certificate already exists for $DOMAIN. Checking if renewal is needed..." if sudo certbot renew --cert-name="$DOMAIN" --dry-run 2>/dev/null; then info "Existing SSL certificate is valid and not due for renewal." SSL_SUCCESS=true else info "Existing certificate needs renewal. Attempting to renew..." if sudo certbot renew --cert-name="$DOMAIN" --force-renewal 2>/dev/null; then SSL_SUCCESS=true else warning "Failed to renew existing SSL certificate." SSL_SUCCESS=false fi fi else # Generate new certificate if sudo certbot certonly \ --webroot \ --webroot-path="$WEBROOT_PATH" \ --email="$WP_ADMIN_EMAIL" \ --agree-tos \ --non-interactive \ --domains="$DOMAIN"; then SSL_SUCCESS=true else SSL_SUCCESS=false fi fi if [[ "$SSL_SUCCESS" == "true" ]]; then success "SSL certificate is ready for $DOMAIN" # Set up automatic renewal info "Setting up automatic SSL certificate renewal..." # Create renewal cron job if it doesn't exist CRON_JOB="0 12 * * * /usr/bin/certbot renew --quiet --post-hook \"systemctl reload lshttpd || systemctl reload apache2 || systemctl reload nginx\"" if ! sudo crontab -l 2>/dev/null | grep -q "certbot renew"; then if (sudo crontab -l 2>/dev/null; echo "$CRON_JOB") | sudo crontab - 2>/dev/null; then success "Automatic SSL renewal configured (daily check at 12:00 PM)" else warning "Failed to configure automatic SSL renewal cron job" fi else info "SSL renewal cron job already exists." fi # For LiteSpeed, we need to restart the service to pick up new certificates info "Restarting LiteSpeed web server to apply SSL certificate..." LITESPEED_RESTARTED=false if sudo systemctl is-active lshttpd &>/dev/null; then if sudo systemctl restart lshttpd 2>/dev/null; then success "LiteSpeed (lshttpd) restarted successfully" LITESPEED_RESTARTED=true else warning "Failed to restart lshttpd service" fi elif sudo systemctl is-active litespeed &>/dev/null; then if sudo systemctl restart litespeed 2>/dev/null; then success "LiteSpeed (litespeed) restarted successfully" LITESPEED_RESTARTED=true else warning "Failed to restart litespeed service" fi else warning "LiteSpeed service not detected or not running." fi if [[ "$LITESPEED_RESTARTED" != "true" ]]; then warning "LiteSpeed service restart failed or not attempted. You may need to manually configure SSL in LiteSpeed admin panel." info "SSL certificate location: /etc/letsencrypt/live/$DOMAIN/" info "Certificate file: /etc/letsencrypt/live/$DOMAIN/fullchain.pem" info "Private key file: /etc/letsencrypt/live/$DOMAIN/privkey.pem" fi else warning "SSL certificate generation failed. You can manually run:" warning "sudo certbot --webroot -w '$WP_ROOT' -d '$DOMAIN' --email '$WP_ADMIN_EMAIL' --agree-tos" info "Or configure SSL manually in your web server control panel." fi else warning "Certbot not available. SSL certificate not generated." info "Please install certbot manually and run: sudo certbot --webroot -w '$WP_ROOT' -d '$DOMAIN'" fi else info "SSL certificate setup skipped due to invalid domain." fi # --- Final Summary --- success "WordPress setup process completed!" # --- 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" printf "\n" printf "${YELLOW}LiteSpeed Object Cache (Single Node):${NC}\n" if [[ "$SKIP_OBJCACHE" == "true" ]]; then printf " Status: ${YELLOW}Skipped (--skip-objcache used)${NC}\n" printf " Manual Setup: ${BLUE}bash scripts/configure_litespeed_redis_object_cache.sh --enable${NC}\n" else # Simple Redis detection for summary REDIS_SOCKET="/var/run/redis/redis.sock" if [[ -S "$REDIS_SOCKET" ]] && redis-cli -s "$REDIS_SOCKET" ping >/dev/null 2>&1; then printf " Status: ${GREEN}Configured (Socket Connection)${NC}\n" printf " Method: ${BLUE}Redis${NC}\n" printf " Host: ${GREEN}%s${NC}\n" "$REDIS_SOCKET" printf " Port: ${GREEN}0${NC}\n" elif redis-cli -h 127.0.0.1 -p 6379 ping >/dev/null 2>&1; then printf " Status: ${GREEN}Configured (TCP Connection)${NC}\n" printf " Method: ${BLUE}Redis${NC}\n" printf " Host: ${GREEN}127.0.0.1${NC}\n" printf " Port: ${GREEN}6379${NC}\n" else printf " Status: ${RED}Not Configured (Redis unavailable)${NC}\n" printf " Next Steps: ${YELLOW}1. Install and start Redis${NC}\n" printf " ${YELLOW}2. Run: bash scripts/configure_litespeed_redis_object_cache.sh --enable${NC}\n" fi fi 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