Files
Linux-Server-Management-Too…/modules/website/wordpress/wordpress-cron-manager.sh
T
cschantz 1d8c9237ca CRITICAL FIX: Cron staggering now uses all 60 minutes
Fixed critical bug where cron staggering only used 20 time slots (0, 3, 6, 9...57)
instead of all 60 minutes, causing multiple websites to be scheduled at same time.

Previous Bug:
- minute * 3 calculation limited to 20 slots
- 200 sites → 10 sites per time slot (NOT staggered!)
- Multiple sites would run wp-cron simultaneously → server overload

Fix Applied:
- Use direct modulo: CRON_OFFSET % 60
- All 60 minutes now used for staggering
- Perfect distribution of load across the hour

Results After Fix:
- 60 sites: 1 site per minute (perfect spacing)
- 100 sites: ~1.67 per minute (evenly distributed)
- 200 sites: ~3.33 per minute (evenly distributed)
- 500 sites: ~8.33 per minute (evenly distributed)

Impact:
- Prevents server overload from simultaneous wp-cron execution
- Even large hosting accounts (500+ sites) properly staggered
- No more "thundering herd" problem

Testing:
-  Verified spacing for 10, 50, 100, 200, 250, 500 sites
-  Perfect distribution across all 60 minutes
-  No duplicate minute assignments

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 22:26:03 -05:00

2857 lines
96 KiB
Bash
Executable File

#!/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="<?php"
# OPTIMIZATION: Magic numbers as named constants (OPT-1)
# Single source of truth for configuration values
declare -r MIN_DISK_SPACE=10240 # 10MB in kilobytes
declare -r CRON_MINUTES_PER_HOUR=60 # Minutes to wrap cron offset
declare -r CHMOD_SECURE_FILE=600 # Secure file permissions (owner only)
declare -r MAX_LOCK_WAIT=5 # Max seconds to wait for lock
declare -r DEFAULT_PARALLEL_JOBS=4 # Default parallel job count
# OPTIMIZATION: Command detection cache (OPT-2)
# Cache command existence to avoid repeated shell searches
declare -gA COMMAND_CACHE
# OPTIMIZATION: Batch/non-interactive mode flag (OPT-3)
# Skip all confirmations and press_enter for automation
BATCH_MODE=false
# Global counter for staggering cron times
CRON_OFFSET=0
# Global variable to hold generated cron time (avoids subshell scope issues)
declare -g LAST_CRON_TIME=""
# PERFORMANCE OPTIMIZATION: Lazy system detection initialization
# Don't auto-detect system at library load - only when first needed
declare -g SYSTEM_DETECTION_LAZY=0
# PERFORMANCE OPTIMIZATION: Global cache for find results
# Instead of running find 23 times, run once and reuse results
declare -g WP_SITES_CACHE=""
declare -g WP_CACHE_INITIALIZED=0
# SECURITY: Use /var/cache instead of /tmp to prevent symlink attacks (script runs as root!)
# Falls back to /tmp with symlink detection if /var/cache unavailable
declare -g WP_CACHE_DIR="${WP_CACHE_DIR:-/var/cache/wordpress-toolkit}"
declare -g WP_CACHE_FILE="${WP_CACHE_DIR}/wp-sites-cache"
declare -g WP_CACHE_TTL=3600 # Cache valid for 1 hour (3600 seconds)
# Lazy-initialize system detection only when first needed
ensure_system_detection() {
if [ "$SYSTEM_DETECTION_LAZY" = "0" ]; then
initialize_system_detection
SYSTEM_DETECTION_LAZY=1
fi
}
# OPTIMIZATION: Function Registry (OPT-14)
# Maintains a registry of all available functions for discoverability and validation
# Enables runtime function validation and automatic documentation generation
declare -gA FUNCTION_REGISTRY=(
[get_wp_search_paths]="Get WordPress installation paths based on control panel"
[get_home_path]="Build home path for control panel and username"
[initialize_wp_cache]="Initialize global WordPress site cache"
[get_wp_sites_cached]="Get cached WordPress sites, or query if not cached"
[validate_wordpress_site]="Validate WordPress site configuration and ownership"
[run_or_dryrun]="Execute command or show dry-run output"
[is_valid_domain_format]="Validate domain format to prevent command injection"
[is_valid_username_format]="Validate username format to prevent command injection"
[log_message]="Log message with level and optional file output"
[is_empty]="Check if variable is empty/unset"
[is_set]="Check if variable is non-empty"
[suppress_output]="Run command with output suppressed"
[redirect_to_stderr]="Run command and send output to stderr"
[is_file_valid]="Check if file exists and is readable"
[is_user_valid]="Check if user exists on system"
[is_wp_configured]="Check if wp-config.php has required database defines"
[is_wp_cron_disabled]="Check if DISABLE_WP_CRON is set to true"
[is_cron_job_exists]="Check if cron command exists in crontab"
[has_sufficient_disk_space]="Check if directory has minimum required disk space"
[is_wordpress_directory]="Check if directory is valid WordPress installation"
[grep_wp_config_define]="Search wp-config for a specific define statement"
[grep_disabled_wp_cron]="Find disabled WP-Cron setting"
[grep_enabled_wp_cron]="Find enabled WP-Cron setting"
[grep_in_crontab]="Search crontab for a pattern safely"
)
# Function to validate that a function is registered
function_exists_registered() {
local func="$1"
[ -n "${FUNCTION_REGISTRY[$func]}" ] && return 0 || return 1
}
# Function to get description of a registered function
function_get_description() {
local func="$1"
echo "${FUNCTION_REGISTRY[$func]}"
}
# PERFORMANCE OPTIMIZATION: Use system domain discovery instead of find commands
# References already-discovered domains from main management system (much faster!)
# Returns wp-config.php paths for all WordPress installations
# CRITICAL FIX: Use find directly (much faster than list_all_domains + get_domain_docroot)
# Direct find approach: O(1) scan vs. domain discovery approach: O(N) system calls
get_wp_search_paths() {
# Lazy-initialize system detection only when needed (not at startup)
ensure_system_detection
local panel="${1:-$SYS_CONTROL_PANEL}"
# Use direct find search - fastest method for locating all wp-config.php files
# This avoids expensive list_all_domains + per-domain docroot lookups
case "$panel" in
cpanel)
find /home/*/public_html -name "wp-config.php" -type f 2>/dev/null | head -1000
;;
interworx)
find /home/*/*/html -name "wp-config.php" -type f 2>/dev/null | head -1000
;;
plesk)
find /var/www/vhosts/*/httpdocs -name "wp-config.php" -type f 2>/dev/null | head -1000
;;
*)
# Standalone: check common paths
{
find /var/www/html -name "wp-config.php" -type f 2>/dev/null
find /home/*/public_html -name "wp-config.php" -type f 2>/dev/null
} | head -1000
;;
esac
}
# OPTIMIZATION: Build home path based on control panel and username
# Consolidates path construction logic (appears 6+ times throughout script)
# Returns: /home/user path for given control panel type
get_home_path() {
local user="$1"
local panel="${2:-$SYS_CONTROL_PANEL}"
case "$panel" in
cpanel)
echo "/home/$user/public_html"
;;
interworx)
# InterWorx structure: /home/user/domain/html
# For default domain, typically same as username
echo "/home/$user/$user/html"
;;
plesk)
# Plesk uses /var/www/vhosts
echo "/var/www/vhosts/$user/httpdocs"
;;
*)
# Standalone/default: /var/www/html
echo "/var/www/html"
;;
esac
}
# SECURITY: Safely initialize cache directory, prevent symlink attacks
# Must be called before first cache access
initialize_cache_directory() {
# Create cache directory if needed (with secure permissions)
if [ ! -d "$WP_CACHE_DIR" ]; then
mkdir -p "$WP_CACHE_DIR" 2>/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
# Run the discovery and save to temp file for persistence
WP_SITES_CACHE=$(get_wp_search_paths "$panel")
echo "$WP_SITES_CACHE" > "$WP_CACHE_FILE" 2>/dev/null
WP_CACHE_INITIALIZED=1
}
# 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 <?php and is not corrupted
if ! grep -q "^<?php" "$wp_config" 2>/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"
# Look for uncommented define() call with DISABLE_WP_CRON and true
grep -E "^\s*define\s*\(\s*['\"]$WP_CRON_DISABLED_VAR['\"]" "$wp_config" 2>/dev/null | grep -q "true" && 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
eval "$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 <<EOF
{
"report_type": "WordPress Cron Manager",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"duration_seconds": $duration,
"summary": {
"total_sites": ${REPORT_DATA[total_sites]:-0},
"converted": ${REPORT_DATA[total_converted]:-0},
"failed": ${REPORT_DATA[total_failed]:-0},
"skipped": ${REPORT_DATA[total_skipped]:-0}
},
"dry_run": "$DRY_RUN",
"parallel_enabled": "$ENABLE_PARALLEL"
}
EOF
}
# Generate CSV report
generate_csv_report() {
cat <<EOF
Report Type,WordPress Cron Manager
Timestamp,$(date -u +%Y-%m-%dT%H:%M:%SZ)
Total Sites,${REPORT_DATA[total_sites]:-0}
Converted,${REPORT_DATA[total_converted]:-0}
Failed,${REPORT_DATA[total_failed]:-0}
Skipped,${REPORT_DATA[total_skipped]:-0}
Dry Run,$DRY_RUN
Parallel Enabled,$ENABLE_PARALLEL
EOF
}
# Generate text report
generate_text_report() {
local end_time=$(date +%s)
local duration=$((end_time - ${REPORT_DATA[start_time]:-0}))
cat <<EOF
================================================================================
WordPress Cron Manager - Operation Report
================================================================================
Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)
Duration: ${duration}s
Mode: $([ "$DRY_RUN" = "true" ] && echo "DRY-RUN" || echo "LIVE")
Parallel: $([ "$ENABLE_PARALLEL" = "true" ] && echo "ENABLED" || echo "DISABLED")
Summary:
Total Sites: ${REPORT_DATA[total_sites]:-0}
✓ Converted: ${REPORT_DATA[total_converted]:-0}
✗ Failed: ${REPORT_DATA[total_failed]:-0}
⊘ Skipped: ${REPORT_DATA[total_skipped]:-0}
Success Rate: $([ ${REPORT_DATA[total_sites]:-0} -gt 0 ] && echo "scale=1; ${REPORT_DATA[total_converted]:-0} * 100 / ${REPORT_DATA[total_sites]:-0}" | bc || echo "N/A")%
================================================================================
EOF
}
# Write report to file or stdout
report_save() {
local format="${1:-$REPORT_FORMAT}"
local output_file="${2:-$REPORT_FILE}"
local report_content
case "$format" in
json)
report_content=$(generate_json_report)
;;
csv)
report_content=$(generate_csv_report)
;;
*)
report_content=$(generate_text_report)
;;
esac
if [ -n "$output_file" ]; then
echo "$report_content" > "$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"
# Try to insert before WordPress stop editing comment (proper convention)
if grep -q "$WP_CONFIG_MARKER" "$wp_config" 2>/dev/null; then
sed -i '#'"$WP_CONFIG_MARKER"'#i\
define('"'"''"$WP_CRON_DISABLED_VAR"''"'"', true);' "$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 '#'"$WP_EDIT_START"'#a\
define('"'"''"$WP_CRON_DISABLED_VAR"''"'"', true);' "$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
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} Return to menu"
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
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."
echo "Example: example.com or sub.example.com"
press_enter
exit 1
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
exit 1
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
exit 1
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
exit 1
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
exit 1
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
exit 0
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
exit 1
fi
# Add cron job with staggered timing
if [ -z "$site_path" ]; then
echo -e "${RED}${NC} Could not determine site path"
exit 1
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
exit 0
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
exit 1
fi
if [ ! -d "/home/$target_user" ]; then
print_error "User $target_user does not exist"
press_enter
exit 1
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
exit 1
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 -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
exit 0
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
exit 0
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 -gt 0 ]; then
echo -e "${RED}Failed or skipped: $failed${NC}"
fi
fi
;;
5)
# Check status
echo ""
echo "Check wp-cron status for:"
echo -e " ${CYAN}1)${NC} Specific domain"
echo -e " ${CYAN}2)${NC} Specific user"
echo -e " ${RED}0)${NC} Cancel"
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
exit 0
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
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 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
exit 1
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
exit 1
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
exit 0
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
exit 1
fi
if [ ! -d "/home/$target_user" ]; then
print_error "User $target_user does not exist"
press_enter
exit 1
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
exit 1
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 -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
exit 0
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
exit 0
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 -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