#!/bin/bash # CRITICAL: Enable strict error handling to prevent silent failures set -o pipefail # Fail if any part of a pipe fails # Note: NOT using set -e because script has intentional error handling paths ################################################################################ # WordPress Cron Manager ################################################################################ # Purpose: Disable wp-cron and convert to real system cron jobs # Features: # - Detect all WordPress installations # - Disable DISABLE_WP_CRON in wp-config.php # - Add proper cron jobs for scheduled tasks # - Server-wide, per-user, or per-domain operations ################################################################################ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" [ -f "$SCRIPT_DIR/lib/common-functions.sh" ] && source "$SCRIPT_DIR/lib/common-functions.sh" || { echo "ERROR: common-functions.sh not found" >&2; exit 1; } [ -f "$SCRIPT_DIR/lib/system-detect.sh" ] && source "$SCRIPT_DIR/lib/system-detect.sh" || { echo "ERROR: system-detect.sh not found" >&2; exit 1; } [ -f "$SCRIPT_DIR/lib/domain-discovery.sh" ] && source "$SCRIPT_DIR/lib/domain-discovery.sh" || { echo "ERROR: domain-discovery.sh not found" >&2; exit 1; } if [ "$EUID" -ne 0 ]; then print_error "This script must be run as root" exit 1 fi # Lock file to prevent concurrent execution (ephemeral, removed on exit) # SECURITY: Use mktemp to prevent symlink attacks (script runs as root!) LOCK_FILE=$(mktemp -t wordpress-cron-manager.lock.XXXXXX) || { echo "ERROR: Cannot create secure lock file" >&2 exit 1 } chmod 600 "$LOCK_FILE" exec 9>"$LOCK_FILE" if ! flock -n 9; then print_error "Another instance of this script is already running" exit 1 fi # NOTE: Trap is set later at line ~373, MUST include flock unlock! # OPTIMIZATION: Parse command-line flags for script behavior # Support: --dry-run, --parallel, --log, --help DRY_RUN=false ENABLE_PARALLEL=false LOG_OUTPUT_FILE="" for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=true ;; --parallel) ENABLE_PARALLEL=true ;; --batch|--non-interactive) BATCH_MODE=true ;; --log) # Next argument should be the log file path # Will be set in the next iteration or default to /tmp LOG_OUTPUT_FILE="/tmp/wordpress-cron-manager-$(date +%Y%m%d-%H%M%S).log" ;; --log=*) # Handle --log=/path/to/file format LOG_OUTPUT_FILE="${arg#--log=}" ;; --help) echo "Usage: $0 [OPTIONS]" echo "" echo "Options:" echo " --dry-run Run in dry-run mode (no actual changes)" echo " --parallel Enable parallel processing for multi-site ops" echo " --batch Batch mode - skip all confirmations (for automation)" echo " --log [FILE] Enable logging to file (default: /tmp/wordpress-cron-manager-TIMESTAMP.log)" echo " --log=/path/to/file Log to specific file path" echo " --help Show this help message" echo "" echo "Examples:" echo " $0 --dry-run --parallel" echo " $0 --batch --parallel (full automation)" echo " $0 --log" echo " $0 --log=/var/log/wp-cron-conversion.log" exit 0 ;; esac done # ADVANCED FEATURE: Configuration File Support (OPT-18) # Allows loading configuration from file instead of command-line flags # Useful for production deployments and reproducible configurations declare -g CONFIG_FILE="" declare -g USE_CONFIG_FILE=false # Load configuration from file # Supported format: KEY=VALUE (one per line, shell-style) load_config_file() { local config_path="${1:-/etc/wordpress-cron-manager.conf}" if [ ! -f "$config_path" ]; then return 0 # Config file is optional fi if [ ! -r "$config_path" ]; then print_warning "Config file not readable: $config_path" return 1 fi print_info "Loading configuration from: $config_path" # Source the config file (validates shell syntax and loads variables) # Use subshell to avoid polluting global scope with unexpected variables if ! source "$config_path" 2>/dev/null; then print_error "Invalid syntax in config file: $config_path" return 1 fi # Apply configuration options from file [ -n "$ENABLE_PARALLEL" ] && ENABLE_PARALLEL="$ENABLE_PARALLEL" [ -n "$DRY_RUN" ] && DRY_RUN="$DRY_RUN" [ -n "$BATCH_MODE" ] && BATCH_MODE="$BATCH_MODE" [ -n "$LOG_OUTPUT_FILE" ] && LOG_OUTPUT_FILE="$LOG_OUTPUT_FILE" [ -n "$LOG_DIR" ] && LOG_OUTPUT_FILE="$LOG_DIR/wordpress-cron-manager-$(date +%Y%m%d-%H%M%S).log" [ -n "$BACKUP_DIR" ] && BACKUP_DIR="$BACKUP_DIR" [ -n "$ROLLBACK_ENABLED" ] && ROLLBACK_ENABLED="$ROLLBACK_ENABLED" return 0 } # Generate sample configuration file generate_sample_config() { cat <<'EOF' # WordPress Cron Manager Configuration File # Location: /etc/wordpress-cron-manager.conf # Note: Command-line flags override these settings # Enable parallel processing for multi-site operations ENABLE_PARALLEL=false # Run in dry-run mode (show what would be done without making changes) DRY_RUN=false # Batch mode (skip all confirmations for automation) BATCH_MODE=false # Enable logging to file (auto-generated timestamp or custom path) LOG_OUTPUT_FILE="/var/log/wordpress-cron-manager.log" # Custom log directory LOG_DIR="/var/log" # Custom backup directory for rollback support BACKUP_DIR="/var/backups/wordpress-cron" # Enable automatic rollback on failure ROLLBACK_ENABLED=true # Report format (text, json, csv) REPORT_FORMAT="text" # Report output file (leave empty for stdout) REPORT_FILE="/var/log/wordpress-cron-report.txt" EOF } # Initialize logging if --log flag was used if [ -n "$LOG_OUTPUT_FILE" ]; then LOG_ENABLED=true LOG_FILE="$LOG_OUTPUT_FILE" # Ensure log file is writable if ! touch "$LOG_FILE" 2>/dev/null; then echo "ERROR: Cannot write to log file: $LOG_FILE" >&2 LOG_ENABLED=false fi fi # Load configuration file if it exists (can be overridden by command-line flags) if [ -f /etc/wordpress-cron-manager.conf ]; then load_config_file /etc/wordpress-cron-manager.conf fi # PHP binary path detection PHP_BIN=$(command -v php 2>/dev/null || echo "/usr/bin/php") # OPTIMIZATION: Define constants for frequently used strings # Reduces hardcoded strings scattered throughout script (29+ occurrences) declare -r WP_CRON_DISABLED_VAR="DISABLE_WP_CRON" declare -r WP_CONFIG_FILENAME="wp-config.php" declare -r WP_CRON_FILENAME="wp-cron.php" declare -r WP_CONFIG_MARKER="stop editing" declare -r WP_EDIT_START="/dev/null || return 1 chmod 700 "$WP_CACHE_DIR" || return 1 fi # SECURITY: Check for symlink attack - refuse to use symlinked cache file if [ -L "$WP_CACHE_FILE" ]; then print_warning "Cache file is a symlink (potential security issue), removing" rm -f "$WP_CACHE_FILE" return 0 fi # Verify directory is not writable by others (prevent TOCTOU) local dir_perms=$(stat -c %a "$WP_CACHE_DIR" 2>/dev/null || echo "000") if [ "$dir_perms" != "700" ]; then chmod 700 "$WP_CACHE_DIR" 2>/dev/null || return 1 fi return 0 } # Function to initialize and cache all WordPress installations # Runs once at script startup, results used by all subsequent functions initialize_wp_cache() { local panel="$SYS_CONTROL_PANEL" # SECURITY: Initialize cache directory with protection initialize_cache_directory || return 1 # Check if cache file exists and is still fresh (within TTL) if [ -f "$WP_CACHE_FILE" ]; then local cache_age=$(($(date +%s) - $(stat -c %Y "$WP_CACHE_FILE" 2>/dev/null || echo 0))) if [ "$cache_age" -lt "$WP_CACHE_TTL" ]; then # Cache is fresh, use it WP_SITES_CACHE=$(cat "$WP_CACHE_FILE") WP_CACHE_INITIALIZED=1 return fi # Cache is stale, will refresh below fi # Progress feedback - without this, the script appears frozen during initial scan echo "[INFO] Scanning for WordPress installations (building cache)..." >&2 # Run the discovery and save to temp file for persistence # CRITICAL: Suppress stdout noise from system detection by redirecting to temp, then extracting only paths local temp_discovery=$(mktemp) get_wp_search_paths "$panel" > "$temp_discovery" 2>/dev/null # Extract only lines that are actual file paths (contain /home or /var, start with /) WP_SITES_CACHE=$(grep -E "^/.*wp-config\.php$" "$temp_discovery" 2>/dev/null || echo "") rm -f "$temp_discovery" echo "$WP_SITES_CACHE" > "$WP_CACHE_FILE" 2>/dev/null WP_CACHE_INITIALIZED=1 # Report count to help diagnose missed installs local site_count site_count=$(echo "$WP_SITES_CACHE" | wc -l 2>/dev/null) echo "[INFO] Found $site_count WordPress installation(s). Cache saved (valid 1 hour)." >&2 } # Function to get cached WordPress installations # Returns cached results from initialize_wp_cache() # Always returns instantly (uses file cache) get_wp_sites_cached() { # Quick check: if we have a fresh cache file, use it immediately (no discovery needed) if [ -f "$WP_CACHE_FILE" ]; then local cache_age=$(($(date +%s) - $(stat -c %Y "$WP_CACHE_FILE" 2>/dev/null || echo 0))) if [ "$cache_age" -lt "$WP_CACHE_TTL" ]; then # Cache is fresh, return it cat "$WP_CACHE_FILE" return fi fi # Cache missing or stale: Initialize/refresh cache if [ "$WP_CACHE_INITIALIZED" = "0" ]; then initialize_wp_cache fi echo "$WP_SITES_CACHE" } # Cleanup on exit (keep cache file for next invocation, only remove lock file) # CRITICAL: Must unlock flock (fd 9) before removing lock file! trap 'flock -u 9 2>/dev/null; exec 9>&-; rm -f "$LOCK_FILE"; rollback_cleanup' EXIT INT TERM # OPTIMIZATION: User extraction caching (memoization) # extract_user_from_path() called 10 times, often for same path # Cache results to avoid redundant extraction operations declare -gA USER_EXTRACTION_CACHE get_user_from_path_cached() { local site_path="$1" # Check if already in cache if [ -n "${USER_EXTRACTION_CACHE[$site_path]}" ]; then echo "${USER_EXTRACTION_CACHE[$site_path]}" return 0 fi # Not in cache, extract and cache result local user=$(extract_user_from_path "$site_path") USER_EXTRACTION_CACHE[$site_path]="$user" echo "$user" } # Function to safely add cron job to user's crontab # Returns 0 on success, 1 on failure safe_add_cron_job() { local user="$1" local cron_time="$2" local cron_cmd="$3" # OPTIMIZATION: Single crontab -l call instead of 3 separate calls # Check existence, read content, and validate in one operation local current_crontab current_crontab=$(crontab -u "$user" -l 2>/dev/null) local crontab_exit=$? if [ $crontab_exit -ne 0 ]; then # User doesn't have a crontab yet - try creating one echo "# WordPress cron jobs" | crontab -u "$user" - 2>/dev/null || return 1 current_crontab="# WordPress cron jobs" fi # CRITICAL FIX: Check if exact job already exists to prevent duplicates if echo "$current_crontab" | grep -qF "$cron_cmd"; then # Job already exists, don't add duplicate return 0 fi # Add the job to crontab # CRITICAL: crontab -l already verified to have succeeded above (echo "$current_crontab"; echo "$cron_time $cron_cmd") | crontab -u "$user" - 2>/dev/null return $? } # Function to safely remove cron jobs from user's crontab # Returns 0 on success, 1 on failure safe_remove_cron_jobs() { local user="$1" local pattern="$2" # Pattern to match jobs to remove # OPTIMIZATION: Single crontab -l call instead of 2 separate calls local current_crontab current_crontab=$(crontab -u "$user" -l 2>/dev/null) || return 0 # Check if pattern exists in crontab if ! echo "$current_crontab" | grep -q "$pattern"; then # Pattern not found - nothing to remove return 0 fi # Remove jobs matching pattern # CRITICAL: crontab -l already verified to have succeeded above echo "$current_crontab" | grep -v "$pattern" | crontab -u "$user" - 2>/dev/null return $? } # Function to validate wp-config.php syntax before and after modification # Returns 0 if valid, 1 if syntax error detected validate_wp_config_syntax() { local wp_config="$1" # Check if file exists and is readable [ ! -f "$wp_config" ] && return 1 [ ! -r "$wp_config" ] && return 1 # Validate PHP syntax using php -l if available if command -v php >/dev/null 2>&1; then if ! php -l "$wp_config" >/dev/null 2>&1; then return 1 fi fi # Additional checks: ensure file has opening /dev/null; then return 1 fi return 0 } # Function to check if cron job already exists for a specific site # Returns 0 if exists, 1 if not found cron_job_exists() { local user="$1" local site_path="$2" # CRITICAL FIX: Match exact cd command, not just the path # Previous: grep -qF "$site_path" would match partial paths (e.g., /home/site/wp-cron.php AND /home/site-test/wp-cron.php) # Now: grep -qF "cd \"$site_path\"" matches the exact cd command crontab -u "$user" -l 2>/dev/null | grep -qF "cd \"$site_path\"" && return 0 || return 1 } # Function to check if DISABLE_WP_CRON already exists in wp-config # Returns 0 if exists, 1 if not found # Excludes commented-out lines to avoid false positives disable_wp_cron_exists() { local wp_config="$1" # OPTIMIZATION: Single grep instead of double-pipe # Look for uncommented define() call with DISABLE_WP_CRON and true grep -E "^\s*define\s*\(\s*['\"]$WP_CRON_DISABLED_VAR['\"].*true" "$wp_config" >/dev/null 2>&1 && return 0 || return 1 } # OPTIMIZATION: Cleaner alias for disable_wp_cron_exists (more intuitive name) # Returns 0 if wp-cron is disabled, 1 if enabled is_wpcron_disabled() { disable_wp_cron_exists "$1" } # OPTIMIZATION: Get file owner consistently (standardizes stat vs ls usage) # Prefer stat for consistency and performance get_file_owner() { local file="$1" stat -c '%U' "$file" 2>/dev/null || echo "" } # Function to verify user owns the WordPress installation # Returns 0 if user matches, 1 if mismatch verify_user_ownership() { local user="$1" local wp_config="$2" # Get actual owner of file using standardized helper local actual_owner=$(get_file_owner "$wp_config") # Verify user matches if [ "$user" = "$actual_owner" ]; then return 0 else return 1 fi } # Function to check if user exists and can receive crontab entries # Returns 0 if valid, 1 if not user_is_valid() { local user="$1" # Check if user exists in system if ! id "$user" >/dev/null 2>&1; then return 1 fi # Check if user can be used with crontab (not system user like root) # Should have a real home directory local home_dir=$(getent passwd "$user" | cut -d: -f6) if [ -z "$home_dir" ] || [ "$home_dir" = "/dev/null" ]; then return 1 fi return 0 } # PERFORMANCE OPTIMIZATION: Validation wrapper function # Consolidates 3-step validation used in 6 different options # Returns 0 if all checks pass, 1 if any check fails validate_wordpress_site() { local user="$1" local wp_config="$2" # Step 1: Check if user is valid if ! user_is_valid "$user"; then print_error "Invalid user or user does not exist: $user" return 1 fi # Step 2: Verify user owns the WordPress installation if ! verify_user_ownership "$user" "$wp_config"; then print_error "User ownership mismatch: user=$user, owner=$(stat -c '%U' "$wp_config" 2>/dev/null)" return 1 fi # Step 3: Validate wp-config.php syntax if ! validate_wp_config_syntax "$wp_config"; then print_error "Invalid wp-config.php syntax or file not readable: $wp_config" return 1 fi return 0 } # PERFORMANCE OPTIMIZATION: DRY-RUN wrapper function # Centralizes 20+ inconsistent DRY_RUN checks into a single function # Executes command or prints what would happen in dry-run mode # Usage: run_or_dryrun "echo 'Modifying file'" "Modifying file" run_or_dryrun() { local command="$1" local description="${2:-$command}" if [ "$DRY_RUN" = "true" ]; then echo "[DRY-RUN] $description" return 0 else # SECURITY FIX: Use bash -c instead of eval to reduce code injection risk bash -c "$command" return $? fi } # Function to validate domain format (prevent command injection) # Returns 0 if valid format, 1 if invalid # Valid: alphanumeric, hyphens, dots, wildcard subdomains is_valid_domain_format() { local domain="$1" # Reject empty input [ -z "$domain" ] && return 1 # Domain must be alphanumeric, hyphens, dots only (no special chars) # Pattern: starts with letter/digit, contains only [a-z0-9.-], ends with letter/digit if [[ "$domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$ ]]; then # Additional check: no consecutive dots, no leading/trailing dots [[ "$domain" != *.* ]] && [ "${domain:0:1}" != "." ] && [ "${domain: -1}" != "." ] && return 0 [[ "$domain" == *.* ]] && [[ "$domain" != ..* ]] && [[ "$domain" != *..* ]] && [[ "$domain" != *. ]] && return 0 fi return 1 } # Function to validate username format (prevent command injection) # Returns 0 if valid format, 1 if invalid # Valid: alphanumeric, underscore, hyphen (Linux username conventions) is_valid_username_format() { local username="$1" # Reject empty input [ -z "$username" ] && return 1 # Username must be lowercase alphanumeric, underscore, hyphen # Pattern: 1-32 chars, starts with letter/digit, contains [a-z0-9_-] if [[ "$username" =~ ^[a-z0-9]([a-z0-9_-]{0,31})?$ ]]; then return 0 fi return 1 } # OPTIMIZATION: Logging wrapper for consistent output formatting (39 occurrences) # Provides foundation for future logging to file capability # Usage: log_info "Processing site", log_success "Done", log_error "Failed" declare -g LOG_ENABLED=false declare -g LOG_FILE="" # Log message with level log_message() { local level="$1" local message="$2" case "$level" in INFO) echo -e "${CYAN}[INFO]${NC} $message" ;; SUCCESS) echo -e "${GREEN}[✓]${NC} $message" ;; WARNING) echo -e "${YELLOW}[⚠]${NC} $message" ;; ERROR) echo -e "${RED}[✗]${NC} $message" ;; *) echo "$message" ;; esac # Write to log file if enabled if [ "$LOG_ENABLED" = "true" ] && [ -n "$LOG_FILE" ]; then echo "[$level] $message" >> "$LOG_FILE" 2>/dev/null fi } # Convenience wrappers for common log levels log_info() { log_message "INFO" "$1" } log_success() { log_message "SUCCESS" "$1" } log_warning() { log_message "WARNING" "$1" } log_error() { log_message "ERROR" "$1" } # OPTIMIZATION: Parallel processing support for multi-site operations # Detect and enable parallel processing for significantly faster execution # Potential speedup: 4-8x on multi-core servers for large-scale operations declare -g PARALLEL_DETECTED=false declare -g PARALLEL_JOBS=1 # Detect parallel processing capabilities at startup detect_parallel_capabilities() { # Skip if disabled via flag if [ "$ENABLE_PARALLEL" != "true" ]; then PARALLEL_DETECTED=false PARALLEL_JOBS=1 return 1 fi # Check for GNU parallel if command -v parallel >/dev/null 2>&1; then PARALLEL_DETECTED=true PARALLEL_JOBS=$(nproc 2>/dev/null || echo 4) log_info "Parallel processing enabled (GNU parallel, ${PARALLEL_JOBS} jobs)" return 0 fi # Fallback to xargs with -P flag if command -v xargs >/dev/null 2>&1; then PARALLEL_DETECTED=true PARALLEL_JOBS=$(nproc 2>/dev/null || echo 4) log_info "Parallel processing enabled (xargs, ${PARALLEL_JOBS} jobs)" return 0 fi # No parallel tools available PARALLEL_DETECTED=false PARALLEL_JOBS=1 return 1 } # OPTIMIZATION: Command detection caching (OPT-2) # Cache command existence checks to avoid repeated shell searches # 4x faster than repeated "command -v" checks get_command_cached() { local cmd="$1" # Check cache first if [ -n "${COMMAND_CACHE[$cmd]}" ]; then [ "${COMMAND_CACHE[$cmd]}" = "found" ] && return 0 || return 1 fi # Not cached, check if command exists if command -v "$cmd" >/dev/null 2>&1; then COMMAND_CACHE[$cmd]="found" return 0 else COMMAND_CACHE[$cmd]="notfound" return 1 fi } # OPTIMIZATION: ANSI Color Palette Constants (OPT-4) # Centralized color definitions for consistent output # Reduces 112 scattered color variable uses declare -r COLOR_GREEN='\033[0;32m' declare -r COLOR_RED='\033[0;31m' declare -r COLOR_YELLOW='\033[0;33m' declare -r COLOR_BLUE='\033[0;34m' declare -r COLOR_CYAN='\033[0;36m' declare -r COLOR_BOLD='\033[1m' declare -r COLOR_RESET='\033[0m' # OPTIMIZATION: Batch mode helper (OPT-3) # Skip confirmations and input prompts for automation skip_confirmation() { if [ "$BATCH_MODE" = "true" ]; then return 0 # Skip confirmation else return 1 # Don't skip fi } # OPTIMIZATION: Path Component Helper (OPT-5) # Consolidates 26 scattered dirname/basename operations # Reduces duplication, consistent path handling get_site_path() { local wp_config="$1" dirname "$wp_config" } get_filename() { local path="$1" basename "$path" } # OPTIMIZATION: File Existence & Validation Helper (OPT-6) # Consolidates 22 scattered "[ -f ]" checks # Provides consistent error messages and permission checking file_exists() { local file="$1" [ -f "$file" ] && return 0 || return 1 } file_readable() { local file="$1" [ -f "$file" ] && [ -r "$file" ] && return 0 || return 1 } file_writable() { local file="$1" [ -f "$file" ] && [ -w "$file" ] && return 0 || return 1 } # OPTIMIZATION: Text Processing Library (OPT-10) # Consolidates 24 scattered sed/awk/cut operations # Provides consistent text processing pattern text_replace() { local text="$1" local pattern="$2" local replacement="$3" echo "$text" | sed "s/$pattern/$replacement/g" } text_extract_lines() { local text="$1" local pattern="$2" echo "$text" | grep "$pattern" } text_split() { local text="$1" local delimiter="${2:- }" echo "$text" | tr "$delimiter" '\n' } # OPTIMIZATION: Batch Read Processing Helper (OPT-9) # Consolidates 8 while read loops with boilerplate # Enables parallel processing, faster execution # Usage: process_items "$data" "function_name" process_items() { local items="$1" local processor="$2" if [ -z "$items" ]; then return 0 fi local count=0 local total=$(echo "$items" | wc -l) while IFS= read -r item; do count=$((count + 1)) # Show progress if not batch mode if [ "$BATCH_MODE" != "true" ]; then show_progress "$count" "$total" fi # Call processor function with item "$processor" "$item" || true done <<< "$items" # Clear progress if [ "$BATCH_MODE" != "true" ]; then finish_progress fi } # OPTIMIZATION: Progress Tracking Helper (OPT-13) # Consolidates progress indication across loops # Provides user feedback during long operations declare -g PROGRESS_ENABLED=false declare -g PROGRESS_CURRENT=0 declare -g PROGRESS_TOTAL=0 show_progress() { local current="$1" local total="$2" if [ -z "$total" ] || [ "$total" -eq 0 ]; then return 0 fi PROGRESS_CURRENT=$current PROGRESS_TOTAL=$total # Calculate percentage local percent=$((current * 100 / total)) # Print progress (single line, updates in place) printf "\r[%-30s] %3d%% (%d/%d)" \ "$(printf '#%.0s' $(seq 1 $((percent / 3))))" \ "$percent" "$current" "$total" } finish_progress() { if [ "$PROGRESS_TOTAL" -gt 0 ]; then echo "" # New line after progress bar fi } # ADVANCED FEATURE: Progress Bar Implementation (OPT-16) # Enhanced progress bar with configurable width and visual styles # Provides professional-grade progress indication for long operations declare -g PROGRESS_BAR_WIDTH=40 declare -g PROGRESS_SHOW_PERCENT=true declare -g PROGRESS_SHOW_COUNT=true # Display an enhanced progress bar with percentage and item count show_progress_bar() { local current="$1" local total="$2" local label="${3:-Processing}" if [ -z "$total" ] || [ "$total" -eq 0 ]; then return 0 fi PROGRESS_CURRENT=$current PROGRESS_TOTAL=$total # Calculate percentage and filled portion local percent=$((current * 100 / total)) local filled=$((percent * PROGRESS_BAR_WIDTH / 100)) local empty=$((PROGRESS_BAR_WIDTH - filled)) # Build bar with filled and empty segments local bar_filled=$(printf '=%.0s' $(seq 1 $filled)) local bar_empty=$(printf ' %.0s' $(seq 1 $empty)) # Build output components local bar="[$bar_filled$bar_empty]" local output="${label}: ${bar}" if [ "$PROGRESS_SHOW_PERCENT" = "true" ]; then output="$output $(printf '%3d' $percent)%" fi if [ "$PROGRESS_SHOW_COUNT" = "true" ]; then output="$output ($current/$total)" fi # Print progress (single line, updates in place) printf "\r%-80s" "$output" } # Display spinner during indeterminate progress declare -g SPINNER_INDEX=0 declare -a SPINNER_CHARS=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') show_spinner() { local label="${1:-Processing}" local current_char="${SPINNER_CHARS[$SPINNER_INDEX]}" SPINNER_INDEX=$(( (SPINNER_INDEX + 1) % ${#SPINNER_CHARS[@]} )) printf "\r%s %s" "$current_char" "$label" } finish_progress_bar() { echo "" # New line after progress bar } # OPTIMIZATION: Null Check Standardization (OPT-7) # Consolidates 40 "[ -z ]" and 5 "[ -n ]" checks with clearer intent # Makes code more readable and maintainable is_empty() { local value="$1" [ -z "$value" ] && return 0 || return 1 } is_set() { local value="$1" [ -n "$value" ] && return 0 || return 1 } # OPTIMIZATION: Output Redirection Helpers (OPT-8) # Consolidates 10 ">/dev/null 2>&1" and 3 ">&2" patterns # Provides cleaner, more readable code suppress_output() { "$@" >/dev/null 2>&1 return $? } redirect_to_stderr() { "$@" >&2 return $? } # OPTIMIZATION: Error Code Constants (OPT-11) # Standardizes 43 exit and 49 return statements with meaningful names # Instead of hardcoded "exit 1", use "exit $ERR_INVALID_USER" declare -r ERR_SUCCESS=0 # Operation successful declare -r ERR_INVALID_USER=1 # User validation failed declare -r ERR_FILE_NOT_FOUND=2 # Required file missing declare -r ERR_BACKUP_FAILED=3 # Backup operation failed declare -r ERR_DISK_SPACE=4 # Insufficient disk space declare -r ERR_SYNTAX_ERROR=5 # Syntax validation failed declare -r ERR_PERMISSION_DENIED=6 # Permission or ownership issue declare -r ERR_DATABASE_ERROR=7 # Database connection/query failed declare -r ERR_CONFIG_INVALID=8 # Configuration file invalid declare -r ERR_ALREADY_DISABLED=9 # WP-Cron already disabled declare -r ERR_ALREADY_RUNNING=10 # Another instance running declare -r ERR_CANCELLED=11 # User cancelled operation # OPTIMIZATION: Conditional Logic Library (OPT-15) # Consolidates 165 if statements with complex conditions into readable predicates # Makes conditions self-documenting and reusable is_file_valid() { local file="$1" [ -f "$file" ] && [ -r "$file" ] } is_user_valid() { local user="$1" id "$user" >/dev/null 2>&1 } is_wp_configured() { local wp_config="$1" [ -f "$wp_config" ] && grep -q "DB_NAME" "$wp_config" } is_wp_cron_disabled() { local wp_config="$1" grep -q "define.*$WP_CRON_DISABLED_VAR.*true" "$wp_config" } is_cron_job_exists() { local cron_command="$1" crontab -l 2>/dev/null | grep -qF "$cron_command" } has_sufficient_disk_space() { local path="$1" local min_kb="${2:-$MIN_DISK_SPACE}" local available_kb=$(df "$path" 2>/dev/null | awk 'NR==2 {print $4}') [ "$available_kb" -gt "$min_kb" ] } is_wordpress_directory() { local path="$1" [ -d "$path" ] && [ -f "$path/$WP_CRON_FILENAME" ] } # OPTIMIZATION: Regex Pattern Library (OPT-12) # Consolidates 3+ repeated grep patterns used throughout script # Provides consistent pattern matching and reduces duplication grep_wp_config_define() { local wp_config="$1" local define_name="$2" grep -q "define.*$define_name" "$wp_config" } grep_disabled_wp_cron() { local wp_config="$1" grep -q "define.*$WP_CRON_DISABLED_VAR.*true" "$wp_config" } grep_enabled_wp_cron() { local wp_config="$1" grep -q "define.*$WP_CRON_DISABLED_VAR.*false\|^[[:space:]]*#.*$WP_CRON_DISABLED_VAR" "$wp_config" } grep_in_crontab() { local pattern="$1" crontab -l 2>/dev/null | grep -qF "$pattern" } grep_wordpress_path() { local path="$1" [ -d "$path" ] && [ -f "$path/$WP_CRON_FILENAME" ] } # ADVANCED FEATURE: Report Generation (OPT-17) # Generates structured reports in multiple formats for integration with monitoring systems # Supports JSON, CSV, and text formats for different use cases declare -g REPORT_FORMAT="text" # text, json, csv declare -g REPORT_FILE="" declare -gA REPORT_DATA # Initialize report data collection report_init() { REPORT_DATA[total_sites]=0 REPORT_DATA[total_converted]=0 REPORT_DATA[total_failed]=0 REPORT_DATA[total_skipped]=0 REPORT_DATA[start_time]=$(date +%s) } # Add operation result to report report_add_result() { local site_path="$1" local status="$2" # success, failed, skipped local details="$3" case "$status" in success) REPORT_DATA[total_converted]=$((${REPORT_DATA[total_converted]:-0} + 1)) ;; failed) REPORT_DATA[total_failed]=$((${REPORT_DATA[total_failed]:-0} + 1)) ;; skipped) REPORT_DATA[total_skipped]=$((${REPORT_DATA[total_skipped]:-0} + 1)) ;; esac REPORT_DATA[total_sites]=$((${REPORT_DATA[total_sites]:-0} + 1)) } # Generate JSON report generate_json_report() { local end_time=$(date +%s) local duration=$((end_time - ${REPORT_DATA[start_time]:-0})) cat < "$output_file" echo "Report saved to: $output_file" else echo "$report_content" fi } # ADVANCED FEATURE: Automatic Rollback Support (OPT-19) # Provides safety net for large-scale operations with ability to revert changes # Creates checkpoint backups and enables reverting to known-good state declare -g ROLLBACK_ENABLED=false declare -g ROLLBACK_DIR="/tmp/wp-cron-rollback-$$" declare -gA ROLLBACK_BACKUPS # Maps wp-config.php path to backup path # Initialize rollback system rollback_init() { if [ "$DRY_RUN" = "true" ]; then return 0 # Skip rollback in dry-run mode fi ROLLBACK_ENABLED=true mkdir -p "$ROLLBACK_DIR" 2>/dev/null if [ ! -d "$ROLLBACK_DIR" ]; then print_warning "Could not create rollback directory: $ROLLBACK_DIR" ROLLBACK_ENABLED=false return 1 fi return 0 } # Create checkpoint backup before modification rollback_create_checkpoint() { local wp_config="$1" local backup_path="$ROLLBACK_DIR/$(basename "$wp_config").bak" if [ ! "$ROLLBACK_ENABLED" = "true" ]; then return 0 fi if ! cp "$wp_config" "$backup_path" 2>/dev/null; then print_error "Failed to create rollback checkpoint: $wp_config" return 1 fi # Store mapping for later rollback ROLLBACK_BACKUPS["$wp_config"]="$backup_path" return 0 } # Restore file from rollback checkpoint rollback_restore_file() { local wp_config="$1" local backup_path="${ROLLBACK_BACKUPS[$wp_config]}" if [ ! -f "$backup_path" ]; then print_error "No rollback checkpoint found for: $wp_config" return 1 fi if ! cp "$backup_path" "$wp_config" 2>/dev/null; then print_error "Failed to restore from rollback checkpoint: $wp_config" return 1 fi print_success "Restored from checkpoint: $wp_config" return 0 } # Rollback all changes to checkpoint rollback_all() { if [ ! "$ROLLBACK_ENABLED" = "true" ]; then print_error "Rollback not enabled" return 1 fi if [ ${#ROLLBACK_BACKUPS[@]} -eq 0 ]; then print_info "No checkpoints to rollback" return 0 fi print_warning "Rolling back ${#ROLLBACK_BACKUPS[@]} modified files..." local failed=0 for wp_config in "${!ROLLBACK_BACKUPS[@]}"; do if ! rollback_restore_file "$wp_config"; then failed=$((failed + 1)) fi done if [ $failed -eq 0 ]; then print_success "All files restored successfully" return 0 else print_error "Failed to restore $failed files" return 1 fi } # Clean up rollback directory rollback_cleanup() { if [ -d "$ROLLBACK_DIR" ]; then rm -rf "$ROLLBACK_DIR" 2>/dev/null fi } # Rollback trap handler (called on EXIT/INT/TERM) rollback_on_interrupt() { if [ "$ROLLBACK_ENABLED" = "true" ]; then echo "" print_warning "Operation interrupted" if confirm "Rollback all changes?"; then rollback_all fi fi rollback_cleanup } # OPTIMIZATION: Build cron command consistently # Centralizes cron command format (appears 4 times throughout script) # Returns: cron command string for wp-cron.php execution build_cron_command() { local site_path="$1" # Standard format: cd to site, run wp-cron.php with PHP # Redirects both stdout and stderr to /dev/null to keep cron logs clean echo "cd \"$site_path\" && $PHP_BIN -q $WP_CRON_FILENAME >/dev/null 2>&1" } # Function to pre-check all WordPress installations before any modifications # Returns count of valid installations preflight_check() { local panel="$1" local found_count=0 local issues_count=0 echo "" echo "Running pre-flight checks..." echo "" # PERFORMANCE: Use cached WordPress paths (already scanned at startup) local wp_configs="" wp_configs=$(get_wp_sites_cached) if [ -z "$wp_configs" ]; then echo -e "${YELLOW}No WordPress installations found${NC}" return 0 fi while IFS= read -r wp_config; do found_count=$((found_count + 1)) site_path=$(dirname "$wp_config") user=$(get_user_from_path_cached "$site_path") # Verify user is valid if ! user_is_valid "$user"; then echo -e "${RED}✗${NC} Site $found_count: Invalid user '$user' ($site_path)" issues_count=$((issues_count + 1)) continue fi # Verify user owns the WordPress install if ! verify_user_ownership "$user" "$wp_config"; then echo -e "${YELLOW}⚠${NC} Site $found_count: User mismatch - extracted='$user', owner=$(stat -c '%U' "$wp_config" 2>/dev/null) ($site_path)" issues_count=$((issues_count + 1)) continue fi # Validate wp-config.php syntax if ! validate_wp_config_syntax "$wp_config"; then echo -e "${RED}✗${NC} Site $found_count: Invalid wp-config.php syntax ($site_path)" issues_count=$((issues_count + 1)) continue fi echo -e "${GREEN}✓${NC} Site $found_count: $site_path (user: $user)" done <<< "$wp_configs" echo "" echo "Pre-flight check complete:" echo " Total found: $found_count" echo " Issues: $issues_count" echo " Ready: $((found_count - issues_count))" return $((found_count - issues_count)) } # Function to display detailed status of all WordPress installations # Shows current state before any changes show_installation_status() { local panel="$1" echo "" echo "Current WordPress Installation Status:" echo "" # Use cached WordPress sites instead of doing redundant find local wp_configs=$(get_wp_sites_cached) if [ -z "$wp_configs" ]; then echo "No WordPress installations found" return 1 fi local count=0 while IFS= read -r wp_config; do count=$((count + 1)) site_path=$(dirname "$wp_config") user=$(get_user_from_path_cached "$site_path") # Check wp-cron status if disable_wp_cron_exists "$wp_config"; then cron_status="${GREEN}Disabled${NC} (system cron)" else cron_status="${YELLOW}Enabled${NC} (default)" fi # Check if cron job exists if cron_job_exists "$user" "$site_path"; then cron_job="${GREEN}Yes${NC}" else cron_job="${YELLOW}No${NC}" fi echo "$count. ${BOLD}$site_path${NC}" echo " User: $user" echo " WP-Cron: $cron_status" echo " System Cron Job: $cron_job" echo "" done <<< "$wp_configs" echo "Total installations: $count" } # Function to create timestamped backup of wp-config.php # Returns 0 on success, 1 on failure # Also returns the backup filename create_timestamped_backup() { local wp_config="$1" local backup_timestamp=$(date +%Y%m%d-%H%M%S) local backup_file="${wp_config}.backup-${backup_timestamp}" # Verify source file exists if [ ! -f "$wp_config" ]; then return 1 fi # CRITICAL FIX: Check disk space before creating backup local available_kb=$(df "$(dirname "$wp_config")" 2>/dev/null | awk 'NR==2 {print $4}') if [ -n "$available_kb" ] && [ "$available_kb" -lt "$MIN_DISK_SPACE" ]; then # Less than 10MB available print_error "Insufficient disk space (less than 10MB available) - cannot create backup" return 1 fi # Create backup if cp "$wp_config" "$backup_file" 2>/dev/null; then # CRITICAL SECURITY FIX: Set backup file permissions to 0600 (owner read/write only) # wp-config.php contains sensitive database credentials and should not be readable by other users chmod "$CHMOD_SECURE_FILE" "$backup_file" 2>/dev/null || true echo "$backup_file" return 0 else return 1 fi } # Function to generate staggered cron time # Distributes jobs across 60 minutes to avoid load spikes generate_staggered_cron() { # CRITICAL: Stagger cron times to prevent all WordPress sites from running at once # This prevents server load spikes from concurrent execution # Each site gets a unique minute within the hour (0-59), spread across full 60 minutes # NOTE: Uses global variable LAST_CRON_TIME instead of echo to avoid subshell issues # BUG FIX: Use all 60 minutes instead of limiting to 20 slots # Previous bug: minute * 3 only gave minutes 0, 3, 6, 9... 57 (20 slots) # With 200 sites: 10 sites per slot = NOT staggered! # Now: Each site gets unique minute 0-59 # With 200 sites: Max 1 site per slot (perfect staggering) local stagger_minute=$((CRON_OFFSET % 60)) # Increment offset for next site (THIS PERSISTS in parent shell, not in subshell) CRON_OFFSET=$((CRON_OFFSET + 1)) # Set global variable instead of using echo (avoids subshell scope issue) # This ensures CRON_OFFSET increments persist across loop iterations LAST_CRON_TIME="$stagger_minute * * * *" } # Function to extract user from WordPress site path # Multi-panel aware extract_user_from_path() { local site_path="$1" local user="" case "$SYS_CONTROL_PANEL" in cpanel) # Extract user from /home/username/public_html pattern user=$(echo "$site_path" | awk -F'/' '{print $3}') # Validate extraction - if empty, try alternative extraction if [ -z "$user" ]; then user=$(stat -c %U "$site_path" 2>/dev/null) fi ;; interworx) # Extract user from /home/username/domain/html pattern user=$(echo "$site_path" | awk -F'/' '{print $3}') # Validate extraction - if empty, try alternative extraction if [ -z "$user" ]; then user=$(stat -c %U "$site_path" 2>/dev/null) fi ;; plesk) # Extract domain from path and lookup user local domain=$(echo "$site_path" | grep -oE '/vhosts/[^/]+' | sed 's|/vhosts/||') user=$(plesk bin subscription --info "$domain" 2>/dev/null | grep "Owner" | awk '{print $2}') # Fallback to file owner if plesk lookup fails if [ -z "$user" ]; then user=$(stat -c %U "$site_path" 2>/dev/null) fi ;; *) # Default to file owner if panel detection fails user=$(stat -c %U "$site_path" 2>/dev/null) if [ -z "$user" ]; then user="www-data" fi ;; esac # CRITICAL FIX: Validate user is not empty - prevents adding cron to wrong user if [ -z "$user" ]; then user="www-data" # Fallback only if extraction completely failed fi echo "$user" } # OPTIMIZATION: Helper function to remove DISABLE_WP_CRON from wp-config # Encapsulates sed pattern for consistency # Returns 0 on success, 1 on failure remove_disable_wpcron_from_config() { local wp_config="$1" # Remove any existing DISABLE_WP_CRON lines using simple pattern # Pattern matches: define('DISABLE_WP_CRON', true); and similar variations # Simplified from complex extended regex that wasn't matching properly sed -i.wpbak '/define.*DISABLE_WP_CRON.*true.*;/d' "$wp_config" return $? } # OPTIMIZATION: Helper function to add DISABLE_WP_CRON to wp-config # Encapsulates sed pattern for consistency # Returns 0 on success, 1 on failure add_disable_wpcron_to_config() { local wp_config="$1" local new_define="define('$WP_CRON_DISABLED_VAR', true);" # Try to insert before WordPress stop editing comment (proper convention) if grep -q "$WP_CONFIG_MARKER" "$wp_config" 2>/dev/null; then # Use sed with proper newline syntax for insert command sed -i.wpbak "/$WP_CONFIG_MARKER/i\\ $new_define" "$wp_config" return 0 elif grep -q "$WP_EDIT_START" "$wp_config"; then # Fallback: if no stop editing found, add after opening PHP tag sed -i.wpbak "/$WP_EDIT_START/a\\ $new_define" "$wp_config" return 0 else # File format is unexpected return 1 fi } # Function to safely modify wp-config.php to disable wp-cron # Returns 0 on success, 1 on failure disable_wpcron_in_config() { local wp_config="$1" # Check if file exists and is writable if [ ! -f "$wp_config" ] || [ ! -w "$wp_config" ]; then return 1 fi # First, always create a backup before any modifications # BUG FIX: Create backup regardless of whether DISABLE_WP_CRON exists if ! cp "$wp_config" "${wp_config}.wpbak" 2>/dev/null; then print_warning "Could not create backup of $wp_config" return 1 fi # Remove any existing DISABLE_WP_CRON lines (anywhere in file) # This ensures clean placement even if previously added in wrong location if grep -q "$WP_CRON_DISABLED_VAR" "$wp_config" 2>/dev/null; then if ! remove_disable_wpcron_from_config "$wp_config"; then # Restore backup if removal failed mv "${wp_config}.wpbak" "$wp_config" return 1 fi fi # Now add it in the proper location - before "stop editing" comment if ! add_disable_wpcron_to_config "$wp_config"; then # Restore backup if file format is unexpected if [ -f "${wp_config}.wpbak" ]; then mv "${wp_config}.wpbak" "$wp_config" fi return 1 fi # Verify the change was successful if grep -E "^[^/]*define\s*\(\s*['\"]$WP_CRON_DISABLED_VAR['\"]\s*,\s*true\s*\)" "$wp_config" >/dev/null 2>&1; then # Remove backup if successful rm -f "${wp_config}.wpbak" return 0 else # Restore backup if verification failed if [ -f "${wp_config}.wpbak" ]; then mv "${wp_config}.wpbak" "$wp_config" fi return 1 fi } # Function to safely re-enable wp-cron (revert changes) # Returns 0 on success, 1 on failure enable_wpcron_in_config() { local wp_config="$1" # Check if file exists and is writable if [ ! -f "$wp_config" ] || [ ! -w "$wp_config" ]; then return 1 fi # Check if DISABLE_WP_CRON exists and is set to true if grep -E "^[^/]*define[[:space:]]*\([[:space:]]*['\"]$WP_CRON_DISABLED_VAR['\"][[:space:]]*,[[:space:]]*true[[:space:]]*\)" "$wp_config" >/dev/null 2>&1; then # Remove the line using helper function remove_disable_wpcron_from_config "$wp_config" || true # Verify removal was successful if ! grep -E "^[^/]*define\s*\(\s*['\"]$WP_CRON_DISABLED_VAR['\"]\s*,\s*true\s*\)" "$wp_config" >/dev/null 2>&1; then rm -f "${wp_config}.wpbak" return 0 else # Restore backup if removal failed if [ -f "${wp_config}.wpbak" ]; then mv "${wp_config}.wpbak" "$wp_config" fi return 1 fi else # DISABLE_WP_CRON not found or already disabled return 0 fi } clear print_banner "WordPress Cron Manager" # PERFORMANCE: Pre-load WordPress sites cache on startup # Simplified: delegate to get_wp_sites_cached() which handles all TTL logic if [ "$WP_CACHE_INITIALIZED" = "0" ]; then if [ -f "$WP_CACHE_FILE" ]; then echo -e "${GREEN}✓ Loading cached WordPress site list${NC}" else echo -e "${CYAN}Scanning for WordPress installations (first run)...${NC}" fi WP_SITES_CACHE=$(get_wp_sites_cached) WP_CACHE_INITIALIZED=1 echo "" fi # CRITICAL FIX: Menu loop allows returning to menu instead of exiting script # Each case option completes, then loop continues to show menu again # Option 0 breaks the loop to exit script entirely while true; do echo "" echo -e "${BOLD}What would you like to do?${NC}" echo "" echo -e "${GREEN}Enable System Cron:${NC}" echo -e " ${CYAN}1)${NC} Scan for WordPress installations" echo -e " ${CYAN}2)${NC} Disable wp-cron for specific domain" echo -e " ${CYAN}3)${NC} Disable wp-cron for specific user (all their WP sites)" echo -e " ${CYAN}4)${NC} Disable wp-cron server-wide (all WordPress sites)" echo "" echo -e "${YELLOW}Revert to WP-Cron:${NC}" echo -e " ${CYAN}6)${NC} Re-enable wp-cron for specific domain" echo -e " ${CYAN}7)${NC} Re-enable wp-cron for specific user (all their WP sites)" echo -e " ${CYAN}8)${NC} Re-enable wp-cron server-wide (all WordPress sites)" echo "" echo -e "${CYAN}Status & Information:${NC}" echo -e " ${CYAN}5)${NC} Check wp-cron status for domain/user" echo -e " ${CYAN}9)${NC} Run pre-flight checks (validate all installations)" echo -e " ${CYAN}10)${NC} Show detailed status of all WordPress sites" echo "" echo -e " ${RED}0)${NC} Exit script" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo -n "Select option [0]: " # Validate choice input while true; do read -r choice choice="${choice:-0}" if ! [[ "$choice" =~ ^([0-9]|10)$ ]]; then echo "" print_error "Invalid choice. Please enter 0-10" echo "" continue fi break done case "$choice" in 1) # Scan for WordPress installations echo "" print_banner "WordPress Installation Scanner" echo "" echo "Retrieving WordPress installations from cache..." echo "" # PERFORMANCE: Use cached WordPress paths (pre-scanned at startup) wp_sites=$(get_wp_sites_cached) if [ -z "$wp_sites" ]; then echo -e "${YELLOW}No WordPress installations found${NC}" else count=0 echo -e "${BOLD}Found WordPress Installations:${NC}" echo "" while IFS= read -r config_file; do count=$((count + 1)) # Extract info - Multi-panel support site_path=$(dirname "$config_file") # Extract user and domain based on control panel user="$(get_user_from_path_cached "$site_path")" domain="" case "$SYS_CONTROL_PANEL" in cpanel) userdata_dir="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}" if [ -f "$userdata_dir/$user/main" ]; then domain=$(grep -m1 "^servername:" "$userdata_dir/$user/main" 2>/dev/null | awk '{print $2}') fi # CRITICAL FIX: Fallback if domain extraction failed if [ -z "$domain" ]; then domain="$user.local" # Use user@hostname fallback fi ;; interworx) domain=$(echo "$site_path" | cut -d'/' -f4) # CRITICAL FIX: Fallback if domain extraction failed if [ -z "$domain" ]; then domain="$user.local" # Use user@hostname fallback fi ;; plesk) domain=$(echo "$site_path" | grep -oE '/vhosts/[^/]+' | sed 's|/vhosts/||') user=$(plesk bin subscription --info "$domain" 2>/dev/null | grep "Owner" | awk '{print $2}') [ -z "$user" ] && user="plesk-user" ;; *) user="standalone" domain="localhost" ;; esac # Final validation: ensure neither is empty (prevents display issues) [ -z "$user" ] && user="unknown" [ -z "$domain" ] && domain="unknown-domain" # Check if wp-cron is disabled if grep -q "define.*DISABLE_WP_CRON.*true" "$config_file" 2>/dev/null; then status="${GREEN}✓ Disabled (using system cron)${NC}" else status="${YELLOW}⚠ Enabled (default wp-cron)${NC}" fi echo -e "${count}. ${BOLD}$domain${NC}" echo " Path: $site_path" echo " User: $user" echo " Status: $status" echo "" done <<< "$wp_sites" echo -e "${CYAN}Total WordPress installations: $count${NC}" fi ;; 2) # Disable wp-cron for specific domain echo "" echo -n "Enter domain name (or 0 to cancel): " read -r domain if [ -z "$domain" ] || [ "$domain" = "0" ]; then echo "Operation cancelled." press_enter # Return to menu - case completes, loop continues fi # INPUT SANITIZATION: Validate domain format if ! is_valid_domain_format "$domain"; then print_error "Invalid domain format. Use only letters, numbers, hyphens, and dots." echo "Example: example.com or sub.example.com" press_enter # Error - case completes, loop returns to menu fi # Find WordPress installation for this domain - Multi-panel support echo "" echo "Searching for WordPress installation for $domain..." wp_config="" case "$SYS_CONTROL_PANEL" in cpanel) # Method 1: Check main_domain in /var/cpanel/userdata/*/main files userdata_base="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}" for userdata_file in "$userdata_base"/*/main; do if grep -q "^main_domain: $domain" "$userdata_file" 2>/dev/null; then user=$(basename "$(dirname "$userdata_file")") potential_config="/home/$user/public_html/wp-config.php" if [ -f "$potential_config" ]; then wp_config="$potential_config" break fi fi done # Method 2: If not found, search all domain-specific files for servername if [ -z "$wp_config" ]; then for userdata_file in "$userdata_base"/*/*; do # Skip cache files and main files [[ "$userdata_file" == *.cache ]] && continue [[ "$userdata_file" == */main ]] && continue [[ "$userdata_file" == */cache ]] && continue [[ "$userdata_file" == */cache.json ]] && continue if grep -q "^servername: $domain" "$userdata_file" 2>/dev/null; then user=$(basename "$(dirname "$userdata_file")") potential_config="/home/$user/public_html/wp-config.php" if [ -f "$potential_config" ]; then wp_config="$potential_config" break fi fi done fi ;; interworx) # Find user from vhost config user=$(grep -l "ServerName ${domain}" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | head -1 | \ xargs grep "SuexecUserGroup" 2>/dev/null | awk '{print $2}') if [ -n "$user" ]; then potential_config="/home/${user}/${domain}/html/wp-config.php" [ -f "$potential_config" ] && wp_config="$potential_config" fi ;; plesk) # Try standard Plesk path potential_config="/var/www/vhosts/${domain}/httpdocs/wp-config.php" [ -f "$potential_config" ] && wp_config="$potential_config" ;; *) # Standalone - try standard path potential_config="/var/www/html/wp-config.php" [ -f "$potential_config" ] && wp_config="$potential_config" ;; esac if [ -z "$wp_config" ]; then print_error "WordPress installation not found for $domain" press_enter # Error - case completes, loop returns to menu fi echo -e "${GREEN}Found WordPress:${NC} $wp_config" echo "" # Extract site path and user site_path=$(dirname "$wp_config") user=$(get_user_from_path_cached "$site_path") # PRE-FLIGHT VALIDATION CHECKS echo "Running pre-flight validation checks..." echo "" # Check 1: Validate user if ! user_is_valid "$user"; then print_error "User '$user' is not valid or not a cPanel user" press_enter # Error - case completes, loop returns to menu fi echo -e "${GREEN}✓${NC} User valid: $user" # Check 2: Verify user ownership if ! verify_user_ownership "$user" "$wp_config"; then actual_owner=$(stat -c '%U' "$wp_config" 2>/dev/null || ls -l "$wp_config" | awk '{print $3}') echo -e "${YELLOW}⚠${NC} User mismatch detected:" echo " Extracted user: $user" echo " Actual owner: $actual_owner" echo "" echo -n "Continue anyway? (y/n) [n]: " read -r confirm if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then press_enter exit 0 fi else echo -e "${GREEN}✓${NC} User ownership verified" fi # Check 3: Validate wp-config.php syntax BEFORE any changes if ! validate_wp_config_syntax "$wp_config"; then print_error "wp-config.php has syntax errors - cannot modify" echo " Please fix syntax issues first" press_enter # Error - case completes, loop returns to menu fi echo -e "${GREEN}✓${NC} wp-config.php syntax is valid" # Check 4: Check for existing DISABLE_WP_CRON if disable_wp_cron_exists "$wp_config"; then echo -e "${YELLOW}wp-cron is already disabled for this site${NC}" echo "" echo -n "Re-configure anyway? (y/n) [n]: " read -r confirm if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then press_enter exit 0 fi else echo -e "${GREEN}✓${NC} wp-cron currently enabled (will be disabled)" fi # Check 5: Check for existing cron job if cron_job_exists "$user" "$site_path"; then echo -e "${YELLOW}⚠${NC} System cron job already exists for this site" echo "" echo -n "Update existing cron job? (y/n) [n]: " read -r confirm if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then press_enter exit 0 fi else echo -e "${GREEN}✓${NC} No existing system cron job" fi echo "" echo "All validation checks passed. Ready to make changes..." echo "" # CREATE BACKUP WITH TIMESTAMP echo -e "${BOLD}BACKUP CREATION:${NC}" backup_file=$(create_timestamped_backup "$wp_config") if [ $? -ne 0 ] || [ -z "$backup_file" ]; then print_error "Failed to create backup of wp-config.php" echo " Cannot proceed without backup" press_enter # Error - case completes, loop returns to menu fi echo -e "${GREEN}✓${NC} Backup created successfully" echo -e "${CYAN}Location:${NC} $backup_file" echo -e "${CYAN}Timestamp:${NC} $(date '+%Y-%m-%d %H:%M:%S')" echo "" # User confirmation to proceed with modification echo -e "${YELLOW}IMPORTANT:${NC} This will modify your wp-config.php file" echo "" echo -n "Proceed with modification? (y/n) [y]: " read -r confirm if [ "$confirm" = "n" ] || [ "$confirm" = "N" ]; then echo "Operation cancelled. Backup preserved at: $backup_file" press_enter # Return to menu - case completes, loop continues fi echo "" echo "Modifying wp-config.php..." echo "" # Safely disable wp-cron in wp-config.php if disable_wpcron_in_config "$wp_config"; then echo -e "${GREEN}✓${NC} Set DISABLE_WP_CRON to true in wp-config.php" # CRITICAL: Verify syntax after modification if ! validate_wp_config_syntax "$wp_config"; then print_error "CRITICAL: wp-config.php syntax became invalid after modification!" echo " Restoring backup..." if cp "$backup_file" "$wp_config"; then echo -e "${GREEN}✓${NC} Restored from backup: $backup_file" echo "" echo "Your original wp-config.php has been restored." echo "Backup (with attempted modification) kept at: ${backup_file}.failed" cp "$backup_file" "${backup_file}.failed" else print_error "CRITICAL: Could not restore from backup!" echo "Original backup location: $backup_file" fi press_enter exit 1 fi echo -e "${GREEN}✓${NC} wp-config.php syntax verified after modification" else print_error "Failed to modify wp-config.php" echo " Please check file permissions and syntax" press_enter # Error - case completes, loop returns to menu fi # Add cron job with staggered timing if [ -z "$site_path" ]; then echo -e "${RED}✗${NC} Could not determine site path" # Error - case completes, loop returns to menu fi cron_cmd=$(build_cron_command "$site_path") # Check if cron job already exists (for duplicate prevention) if cron_job_exists "$user" "$site_path"; then # Remove old one first to avoid duplicates safe_remove_cron_jobs "$user" "$site_path.*wp-cron.php" echo -e "${YELLOW}⚠${NC} Removed existing cron job (updating)" fi # Generate staggered cron time and add to crontab generate_staggered_cron # Sets LAST_CRON_TIME globally (avoids subshell) cron_time="$LAST_CRON_TIME" if safe_add_cron_job "$user" "$cron_time" "$cron_cmd"; then echo -e "${GREEN}✓${NC} Added cron job ($cron_time)" else echo -e "${YELLOW}⚠${NC} Failed to add cron job" fi echo "" print_success "WordPress cron converted to system cron for $domain" echo "" echo "Changes made:" echo " • DISABLE_WP_CRON set to true in wp-config.php" echo " • System cron job added (runs once per hour)" echo " • Backup saved: ${wp_config}.backup-*" ;; 3) # Disable wp-cron for specific user echo "" echo -n "Enter cPanel username (or 0 to cancel): " read -r target_user if [ -z "$target_user" ] || [ "$target_user" = "0" ]; then echo "Operation cancelled." press_enter # Return to menu - case completes, loop continues fi # INPUT SANITIZATION: Validate username format if ! is_valid_username_format "$target_user"; then print_error "Invalid username format. Use only lowercase letters, numbers, hyphens, and underscores." press_enter # Error - case completes, loop returns to menu fi if [ ! -d "/home/$target_user" ]; then print_error "User $target_user does not exist" press_enter # Error - case completes, loop returns to menu fi echo "" echo "Searching for WordPress installations for user: $target_user" echo "" wp_configs=$(find "/home/$target_user" -name "wp-config.php" -type f 2>/dev/null) if [ -z "$wp_configs" ]; then print_error "No WordPress installations found for $target_user" press_enter # Error - case completes, loop returns to menu fi count=0 converted=0 failed=0 while IFS= read -r wp_config; do count=$((count + 1)) site_path=$(dirname "$wp_config") # Validate site path if [ -z "$site_path" ] || [ ! -d "$site_path" ]; then echo -e "${YELLOW}Warning: Invalid site path${NC}" failed=$((failed + 1)) continue fi # Extract user from site path (per-site, not using $target_user assumption) user=$(get_user_from_path_cached "$site_path") if [ -z "$user" ]; then echo -e "${YELLOW}Warning: Could not extract username from $site_path${NC}" failed=$((failed + 1)) continue fi echo -e "${BOLD}Site $count:${NC} $site_path" # Create timestamped backup backup_file=$(create_timestamped_backup "$wp_config") if [ -z "$backup_file" ]; then echo " • ${YELLOW}Warning: Backup failed, skipping site${NC}" failed=$((failed + 1)) echo "" continue fi echo " • Backed up wp-config.php" # Safely disable wp-cron if [ "$DRY_RUN" = "true" ]; then echo " [DRY-RUN] Would modify wp-config.php" else if ! disable_wpcron_in_config "$wp_config"; then echo " • ${RED}Error: Could not modify wp-config.php${NC}" [ -f "$backup_file" ] && cp "$backup_file" "$wp_config" 2>/dev/null failed=$((failed + 1)) echo "" continue fi fi echo " • Set DISABLE_WP_CRON to true" # Validate syntax after modification if [ "$DRY_RUN" != "true" ] && ! validate_wp_config_syntax "$wp_config"; then echo " • ${RED}Error: wp-config.php syntax invalid after modification${NC}" cp "$backup_file" "$wp_config" 2>/dev/null [ -f "$backup_file" ] && cp "$backup_file" "${backup_file}.failed" 2>/dev/null failed=$((failed + 1)) echo "" continue fi # Add cron job with staggered timing cron_cmd=$(build_cron_command "$site_path") # Check if PHP binary is available if [ ! -x "$PHP_BIN" ]; then echo " • ${RED}Error: PHP binary not found at $PHP_BIN${NC}" failed=$((failed + 1)) echo "" continue fi if ! cron_job_exists "$user" "$site_path"; then generate_staggered_cron # Sets LAST_CRON_TIME globally (avoids subshell) cron_time="$LAST_CRON_TIME" if [ "$DRY_RUN" = "true" ]; then echo " [DRY-RUN] Would add cron job ($cron_time)" else if safe_add_cron_job "$user" "$cron_time" "$cron_cmd"; then echo " • Added cron job ($cron_time)" converted=$((converted + 1)) else echo " • ${RED}Error: Failed to add cron job${NC}" failed=$((failed + 1)) fi fi else echo " • Cron job already exists" fi echo "" done <<< "$wp_configs" if [ "$DRY_RUN" = "true" ]; then echo -e "${CYAN}[DRY-RUN] Would have converted $count site(s)${NC}" else print_success "$converted WordPress sites for $target_user converted to system cron" if [ "${failed:-0}" -gt 0 ]; then print_warning "$failed site(s) failed or were skipped" fi fi ;; 4) # Server-wide conversion echo "" echo -e "${RED}${BOLD}WARNING: Server-Wide wp-cron Conversion${NC}" echo "" echo "This will:" echo " • Find ALL WordPress installations on the server" echo " • Disable wp-cron in each wp-config.php" echo " • Add system cron jobs for each user" echo "" echo -n "Are you sure? Type 'yes' to confirm: " read -r confirm if [ "$confirm" != "yes" ]; then echo "Cancelled" press_enter # Return to menu - case completes, loop continues fi echo "" echo "Processing WordPress installations from cache..." echo "" total=0 converted=0 failed=0 # PERFORMANCE: Use cached paths (scanned once at startup, ~10-50x faster) wp_configs=$(get_wp_sites_cached) if [ -z "$wp_configs" ]; then echo -e "${YELLOW}No WordPress installations found${NC}" press_enter # Return to menu - case completes, loop continues fi while IFS= read -r wp_config; do total=$((total + 1)) site_path=$(dirname "$wp_config") if [ -z "$site_path" ]; then echo -e "${RED}✗ Could not determine site path${NC}" failed=$((failed + 1)) continue fi user=$(get_user_from_path_cached "$site_path") echo -e "${BOLD}Processing:${NC} $site_path (user: $user)" # Create timestamped backup backup_file=$(create_timestamped_backup "$wp_config") if [ -z "$backup_file" ]; then echo " ${RED}✗ Backup failed, skipping${NC}" failed=$((failed + 1)) echo "" continue fi # Safely disable wp-cron if [ "$DRY_RUN" = "true" ]; then echo " [DRY-RUN] Would modify wp-config.php" else if ! disable_wpcron_in_config "$wp_config"; then echo -e "${RED}✗ Failed to modify wp-config.php${NC}" [ -f "$backup_file" ] && cp "$backup_file" "$wp_config" 2>/dev/null failed=$((failed + 1)) echo "" continue fi fi # Validate syntax after modification if [ "$DRY_RUN" != "true" ] && ! validate_wp_config_syntax "$wp_config"; then echo " ${RED}✗ wp-config.php syntax invalid after modification${NC}" cp "$backup_file" "$wp_config" 2>/dev/null [ -f "$backup_file" ] && cp "$backup_file" "${backup_file}.failed" 2>/dev/null failed=$((failed + 1)) echo "" continue fi # Check PHP binary if [ ! -x "$PHP_BIN" ]; then echo " ${RED}✗ PHP binary not found at $PHP_BIN${NC}" failed=$((failed + 1)) echo "" continue fi # Add cron job with staggered timing cron_cmd=$(build_cron_command "$site_path") if ! cron_job_exists "$user" "$site_path"; then generate_staggered_cron # Sets LAST_CRON_TIME globally (avoids subshell) cron_time="$LAST_CRON_TIME" if [ "$DRY_RUN" = "true" ]; then echo " [DRY-RUN] Would add cron job ($cron_time)" else if safe_add_cron_job "$user" "$cron_time" "$cron_cmd"; then echo " Cron: $cron_time" converted=$((converted + 1)) else echo " ${RED}✗ Failed to add cron job${NC}" failed=$((failed + 1)) fi fi fi if [ "$DRY_RUN" != "true" ]; then echo -e "${GREEN}✓${NC} Converted" fi echo "" # Add spacing between operations to prevent concurrent execution # This ensures sequential processing with clear visual separation if [ "$DRY_RUN" != "true" ]; then sleep 2 fi done <<< "$wp_configs" echo "" if [ "$DRY_RUN" = "true" ]; then echo -e "${CYAN}[DRY-RUN] Would convert up to $total site(s)${NC}" else print_success "Server-wide conversion complete" echo "" echo "Summary:" echo " • Total WordPress sites found: $total" echo " • Successfully converted: $converted" if [ "${failed:-0}" -gt 0 ]; then echo -e " • ${RED}Failed or skipped: $failed${NC}" fi fi ;; 5) # Check status echo "" echo -e "${CYAN}${BOLD}Check wp-cron Status${NC}" echo "" echo -e " ${CYAN}1)${NC} Specific domain" echo -e " ${CYAN}2)${NC} Specific user" echo "" echo -e " ${RED}0)${NC} Return to menu" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # Validate check_choice input while true; do echo -n "Select [1]: " read -r check_choice check_choice="${check_choice:-1}" if ! [[ "$check_choice" =~ ^[0-2]$ ]]; then echo "" print_error "Invalid choice. Please enter 0, 1, or 2" echo "" continue fi break done if [ "$check_choice" = "0" ]; then echo "Operation cancelled." press_enter # Return to menu - case completes, loop continues elif [ "$check_choice" = "1" ]; then echo "" echo -n "Enter domain name (or 0 to cancel): " read -r domain if [ -z "$domain" ] || [ "$domain" = "0" ]; then echo "Operation cancelled." press_enter exit 0 fi # INPUT SANITIZATION: Validate domain format if ! is_valid_domain_format "$domain"; then print_error "Invalid domain format. Use only letters, numbers, hyphens, and dots." press_enter exit 1 fi # Find WordPress for domain wp_config="" # Method 1: Check main_domain in main files userdata_base="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}" for userdata_file in "$userdata_base"/*/main; do if grep -q "^main_domain: $domain" "$userdata_file" 2>/dev/null; then user=$(basename "$(dirname "$userdata_file")") potential_config="/home/$user/public_html/wp-config.php" if [ -f "$potential_config" ]; then wp_config="$potential_config" break fi fi done # Method 2: Search domain-specific files for servername if [ -z "$wp_config" ]; then for userdata_file in "$userdata_base"/*/*; do [[ "$userdata_file" == *.cache ]] && continue [[ "$userdata_file" == */main ]] && continue [[ "$userdata_file" == */cache ]] && continue [[ "$userdata_file" == */cache.json ]] && continue if grep -q "^servername: $domain" "$userdata_file" 2>/dev/null; then user=$(basename "$(dirname "$userdata_file")") potential_config="/home/$user/public_html/wp-config.php" if [ -f "$potential_config" ]; then wp_config="$potential_config" break fi fi done fi if [ -z "$wp_config" ]; then print_error "WordPress not found for $domain" press_enter exit 1 fi echo "" echo -e "${BOLD}WordPress Cron Status for $domain${NC}" echo "" echo "Config file: $wp_config" echo "" if grep -q "define.*DISABLE_WP_CRON.*true" "$wp_config" 2>/dev/null; then echo -e "wp-cron: ${GREEN}DISABLED${NC} (using system cron)" # Check for cron job site_path=$(dirname "$wp_config") user=$(get_user_from_path_cached "$site_path") if crontab -u "$user" -l 2>/dev/null | grep -q "$WP_CRON_FILENAME"; then echo -e "System cron: ${GREEN}CONFIGURED${NC}" echo "" echo "Cron jobs:" crontab -u "$user" -l 2>/dev/null | grep "$WP_CRON_FILENAME" else echo -e "System cron: ${RED}NOT CONFIGURED${NC}" fi else echo -e "wp-cron: ${YELLOW}ENABLED${NC} (default WordPress cron)" echo "" echo "Recommendation: Disable wp-cron and use system cron for better performance" fi else echo "" echo -n "Enter cPanel username (or 0 to cancel): " read -r check_user if [ -z "$check_user" ] || [ "$check_user" = "0" ]; then echo "Operation cancelled." press_enter exit 0 fi # INPUT SANITIZATION: Validate username format if ! is_valid_username_format "$check_user"; then print_error "Invalid username format. Use only lowercase letters, numbers, hyphens, and underscores." press_enter exit 1 fi if [ ! -d "/home/$check_user" ]; then print_error "User $check_user does not exist" press_enter exit 1 fi echo "" echo -e "${BOLD}WordPress Cron Status for user: $check_user${NC}" echo "" wp_configs=$(find "/home/$check_user" -name "wp-config.php" -type f 2>/dev/null) if [ -z "$wp_configs" ]; then echo "No WordPress installations found" else count=0 while IFS= read -r wp_config; do count=$((count + 1)) site_path=$(dirname "$wp_config") echo -e "${count}. ${BOLD}$site_path${NC}" if grep -q "define.*DISABLE_WP_CRON.*true" "$wp_config" 2>/dev/null; then echo " wp-cron: ${GREEN}DISABLED${NC}" else echo " wp-cron: ${YELLOW}ENABLED${NC}" fi echo "" done <<< "$wp_configs" # Show cron jobs echo -e "${BOLD}Cron Jobs:${NC}" if crontab -u "$check_user" -l 2>/dev/null | grep -q "$WP_CRON_FILENAME"; then crontab -u "$check_user" -l 2>/dev/null | grep "$WP_CRON_FILENAME" else echo " No wp-cron jobs found" fi fi fi ;; 6) # Re-enable wp-cron for specific domain echo "" echo -n "Enter domain name (or 0 to cancel): " read -r domain if [ -z "$domain" ] || [ "$domain" = "0" ]; then echo "Operation cancelled." press_enter # Return to menu - case completes, loop continues fi # INPUT SANITIZATION: Validate domain format if ! is_valid_domain_format "$domain"; then print_error "Invalid domain format. Use only letters, numbers, hyphens, and dots." press_enter # Error - case completes, loop returns to menu fi # Find WordPress installation wp_config="" # Method 1: Check main_domain in main files for userdata_file in /var/cpanel/userdata/*/main; do if grep -q "^main_domain: $domain" "$userdata_file" 2>/dev/null; then user=$(basename "$(dirname "$userdata_file")") potential_config="/home/$user/public_html/wp-config.php" if [ -f "$potential_config" ]; then wp_config="$potential_config" break fi fi done # Method 2: Search domain-specific files for servername if [ -z "$wp_config" ]; then for userdata_file in /var/cpanel/userdata/*/*; do [[ "$userdata_file" == *.cache ]] && continue [[ "$userdata_file" == */main ]] && continue [[ "$userdata_file" == */cache ]] && continue [[ "$userdata_file" == */cache.json ]] && continue if grep -q "^servername: $domain" "$userdata_file" 2>/dev/null; then user=$(basename "$(dirname "$userdata_file")") potential_config="/home/$user/public_html/wp-config.php" if [ -f "$potential_config" ]; then wp_config="$potential_config" break fi fi done fi if [ -z "$wp_config" ]; then print_error "WordPress installation not found for $domain" press_enter # Error - case completes, loop returns to menu fi echo -e "${GREEN}Found WordPress:${NC} $wp_config" echo "" # Create timestamped backup backup_file=$(create_timestamped_backup "$wp_config") if [ -z "$backup_file" ]; then print_error "Backup failed, aborting" press_enter # Error - case completes, loop returns to menu fi echo -e "${GREEN}✓${NC} Backed up wp-config.php" # Re-enable wp-cron if [ "$DRY_RUN" = "true" ]; then echo "[DRY-RUN] Would remove DISABLE_WP_CRON from wp-config.php" else if ! enable_wpcron_in_config "$wp_config"; then echo -e "${YELLOW}⚠${NC} DISABLE_WP_CRON not found or already enabled" fi # Validate syntax after modification if ! validate_wp_config_syntax "$wp_config"; then echo -e "${RED}✗${NC} wp-config.php syntax invalid after modification" cp "$backup_file" "$wp_config" 2>/dev/null [ -f "$backup_file" ] && cp "$backup_file" "${backup_file}.failed" 2>/dev/null print_error "Reverted from backup" press_enter exit 1 fi echo -e "${GREEN}✓${NC} Removed DISABLE_WP_CRON from wp-config.php" fi # Remove cron job - Multi-panel support site_path=$(dirname "$wp_config") user=$(get_user_from_path_cached "$site_path") if [ "$DRY_RUN" = "true" ]; then echo "[DRY-RUN] Would remove cron job from user crontab" else if safe_remove_cron_jobs "$user" "$site_path.*wp-cron.php"; then echo -e "${GREEN}✓${NC} Removed cron job from user crontab" else echo -e "${YELLOW}⚠${NC} Failed to remove cron job" fi fi echo "" if [ "$DRY_RUN" = "true" ]; then echo -e "${CYAN}[DRY-RUN] Would revert WordPress cron to default for $domain${NC}" else print_success "WordPress cron reverted to default for $domain" fi ;; 7) # Re-enable wp-cron for specific user echo "" echo -n "Enter cPanel username (or 0 to cancel): " read -r target_user if [ -z "$target_user" ] || [ "$target_user" = "0" ]; then echo "Operation cancelled." press_enter # Return to menu - case completes, loop continues fi # INPUT SANITIZATION: Validate username format if ! is_valid_username_format "$target_user"; then print_error "Invalid username format. Use only lowercase letters, numbers, hyphens, and underscores." press_enter # Error - case completes, loop returns to menu fi if [ ! -d "/home/$target_user" ]; then print_error "User $target_user does not exist" press_enter # Error - case completes, loop returns to menu fi echo "" echo "Reverting WordPress installations for user: $target_user" echo "" wp_configs=$(find "/home/$target_user" -name "wp-config.php" -type f 2>/dev/null) if [ -z "$wp_configs" ]; then print_error "No WordPress installations found for $target_user" press_enter # Error - case completes, loop returns to menu fi count=0 converted=0 failed=0 while IFS= read -r wp_config; do count=$((count + 1)) site_path=$(dirname "$wp_config") echo -e "${BOLD}Site $count:${NC} $site_path" # Create timestamped backup backup_file=$(create_timestamped_backup "$wp_config") if [ -z "$backup_file" ]; then echo " • ${RED}Backup failed, skipping${NC}" failed=$((failed + 1)) echo "" continue fi echo " • Backed up wp-config.php" # Re-enable wp-cron if [ "$DRY_RUN" = "true" ]; then echo " [DRY-RUN] Would remove DISABLE_WP_CRON" else if ! enable_wpcron_in_config "$wp_config"; then echo " • Already using default wp-cron" fi # Validate syntax after modification if ! validate_wp_config_syntax "$wp_config"; then echo " • ${RED}Syntax error after modification${NC}" cp "$backup_file" "$wp_config" 2>/dev/null [ -f "$backup_file" ] && cp "$backup_file" "${backup_file}.failed" 2>/dev/null failed=$((failed + 1)) echo "" continue fi echo " • Removed DISABLE_WP_CRON" converted=$((converted + 1)) fi echo "" done <<< "$wp_configs" # Remove all wp-cron jobs for this user if [ "$DRY_RUN" = "true" ]; then echo "[DRY-RUN] Would remove all wp-cron jobs for user $target_user" else if safe_remove_cron_jobs "$target_user" "$WP_CRON_FILENAME"; then echo -e "${GREEN}✓${NC} Removed all wp-cron jobs from user crontab" fi fi if [ "$DRY_RUN" = "true" ]; then echo -e "${CYAN}[DRY-RUN] Would revert $count site(s) for $target_user${NC}" else print_success "All WordPress sites for $target_user reverted to default wp-cron" if [ "${failed:-0}" -gt 0 ]; then print_warning "$failed site(s) failed or were skipped" fi fi ;; 8) # Server-wide revert echo "" echo -e "${RED}${BOLD}WARNING: Server-Wide Revert${NC}" echo "" echo "This will:" echo " • Find ALL WordPress installations on the server" echo " • Remove DISABLE_WP_CRON from each wp-config.php" echo " • Remove all wp-cron system cron jobs" echo "" echo -n "Are you sure? Type 'yes' to confirm: " read -r confirm if [ "$confirm" != "yes" ]; then echo "Cancelled" press_enter # Return to menu - case completes, loop continues fi echo "" echo "Processing WordPress installations from cache..." echo "" total=0 reverted=0 failed=0 # PERFORMANCE: Use cached paths (scanned once at startup, ~10-50x faster) wp_configs=$(get_wp_sites_cached) if [ -z "$wp_configs" ]; then echo -e "${YELLOW}No WordPress installations found${NC}" press_enter # Return to menu - case completes, loop continues fi while IFS= read -r wp_config; do total=$((total + 1)) site_path=$(dirname "$wp_config") if [ -z "$site_path" ]; then echo -e "${RED}✗ Could not determine site path${NC}" failed=$((failed + 1)) continue fi user=$(get_user_from_path_cached "$site_path") echo -e "${BOLD}Processing:${NC} $site_path (user: $user)" # Create timestamped backup backup_file=$(create_timestamped_backup "$wp_config") if [ -z "$backup_file" ]; then echo " ${RED}✗ Backup failed, skipping${NC}" failed=$((failed + 1)) echo "" continue fi # Re-enable wp-cron if [ "$DRY_RUN" = "true" ]; then echo " [DRY-RUN] Would remove DISABLE_WP_CRON" else if ! enable_wpcron_in_config "$wp_config"; then echo " Already using default wp-cron" fi # Validate syntax after modification if ! validate_wp_config_syntax "$wp_config"; then echo " ${RED}✗ Syntax error after modification${NC}" cp "$backup_file" "$wp_config" 2>/dev/null [ -f "$backup_file" ] && cp "$backup_file" "${backup_file}.failed" 2>/dev/null failed=$((failed + 1)) echo "" continue fi reverted=$((reverted + 1)) echo -e "${GREEN}✓${NC} Reverted" fi echo "" # Add spacing between operations to prevent concurrent execution # This ensures sequential processing with clear visual separation if [ "$DRY_RUN" != "true" ]; then sleep 2 fi done <<< "$wp_configs" # Remove all wp-cron jobs from all users echo "" echo "Removing wp-cron jobs from user crontabs..." if [ "$DRY_RUN" = "true" ]; then echo "[DRY-RUN] Would remove wp-cron jobs from all users" else for user_home in /home/*; do user=$(basename "$user_home") if crontab -u "$user" -l 2>/dev/null | grep -q "$WP_CRON_FILENAME"; then if safe_remove_cron_jobs "$user" "$WP_CRON_FILENAME"; then echo " • Removed cron jobs for user: $user" fi fi done fi echo "" if [ "$DRY_RUN" = "true" ]; then echo -e "${CYAN}[DRY-RUN] Would revert up to $total site(s)${NC}" else print_success "Server-wide revert complete" echo "" echo "Summary:" echo " • Total WordPress sites found: $total" echo " • Successfully reverted: $reverted" if [ "${failed:-0}" -gt 0 ]; then echo -e " • ${RED}Failed or skipped: $failed${NC}" fi fi ;; 9) # Run pre-flight checks echo "" print_banner "Pre-Flight Checks" preflight_check "$SYS_CONTROL_PANEL" ;; 10) # Show detailed status echo "" print_banner "WordPress Installation Status" show_installation_status "$SYS_CONTROL_PANEL" ;; 0) exit 0 ;; *) print_error "Invalid option" ;; esac echo "" press_enter done