#!/bin/bash ################################################################################ # Fast 500 Error Tracker ################################################################################ # Purpose: ONLY track 500 errors - find them fast and show WHY # No menus, no options - just find 500s and diagnose the root cause ################################################################################ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$SCRIPT_DIR/lib/common-functions.sh" source "$SCRIPT_DIR/lib/system-detect.sh" source "$SCRIPT_DIR/lib/user-manager.sh" source "$SCRIPT_DIR/lib/ip-reputation.sh" # Ensure color variables are set DIM='\033[2m' BOLD='\033[1m' print_banner "Fast 500 Error Tracker" echo "" echo "Scanning for 500 errors and diagnosing root causes..." echo "" # Ask for time range echo -e "${CYAN}How far back to scan?${NC}" echo -e " ${CYAN}1)${NC} Last 24 hours (default)" echo -e " ${CYAN}2)${NC} Last 7 days" echo -e " ${CYAN}3)${NC} Last 30 days" echo -e " ${CYAN}0)${NC} Cancel and return to menu" echo "" # Validate time_choice input while true; do read -p "Select option [1]: " time_choice time_choice=${time_choice:-1} if ! [[ "$time_choice" =~ ^[0-3]$ ]]; then print_error "Invalid choice. Please enter 0, 1, 2, or 3" continue fi case $time_choice in 0) echo "" echo "Scan cancelled." echo "" exit 0 ;; 1) HOURS_TO_SCAN=24; break ;; 2) HOURS_TO_SCAN=168; break ;; 3) HOURS_TO_SCAN=720; break ;; esac done echo "" echo "→ Scanning last $HOURS_TO_SCAN hours of access logs..." echo "" # Temporary files TEMP_DIR="/tmp/500-tracker-$$" mkdir -p "$TEMP_DIR" trap '[ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"' EXIT ERRORS_500="$TEMP_DIR/errors_500.txt" ERROR_DETAILS="$TEMP_DIR/error_details.txt" # Configuration - Use system-detected paths DOMLOGS_DIR="${SYS_LOG_DIR}" CONTROL_PANEL="${SYS_CONTROL_PANEL}" # Get cutoff time cutoff_time=$(date -d "$HOURS_TO_SCAN hours ago" +%s 2>/dev/null || echo "0") declare -A domain_count declare -A domain_user total_500s=0 filtered_bots=0 # Scan all domain access logs for 500 errors (including subdirectories) while IFS= read -r log; do [ -f "$log" ] || continue [[ "$log" =~ (bytes_log|offset|error_log|ftpxferlog|-ssl_log)$ ]] && continue # Extract domain from log filename domain="${log##*/}" domain="${domain%%-*}" # Skip non-domain system logs (proxy, localhost, etc.) [[ "$domain" =~ ^(proxy|localhost|default|cpanel|webmail|whm|cpcalendars|cpcontacts|webdisk)$ ]] && continue # Find user for this domain - Multi-panel support user="" case "$CONTROL_PANEL" in cpanel) user=$(grep "^${domain}:" /etc/userdatadomains 2>/dev/null | cut -d: -f2 | awk -F'==' '{print $1}' | head -1) ;; interworx) 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}') ;; plesk) user=$(plesk bin subscription --info "$domain" 2>/dev/null | grep "Owner" | awk '{print $2}') ;; esac [ -z "$user" ] && user="unknown" domain_user["$domain"]="$user" # Scan for 500 errors in this log while IFS= read -r line; do # Check for 500 status code if [[ "$line" =~ '"'[[:space:]](5[0-9]{2})[[:space:]] ]]; then status="${BASH_REMATCH[1]}" # Time filtering if [ "$cutoff_time" != "0" ]; then if [[ "$line" =~ \[([0-9]{2}/[A-Z][a-z]{2}/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2}) ]]; then log_date="${BASH_REMATCH[1]}" log_time=$(date -d "${log_date/:/ }" +%s 2>/dev/null || echo "0") [ "$log_time" != "0" ] && [ "$log_time" -lt "$cutoff_time" ] && continue fi fi # Parse log line to get IP first read -r ip _ _ timestamp _ request _ _ <<< "$line" # IP-based filtering - skip non-relevant IPs # Skip localhost/internal IPs if [[ "$ip" =~ ^(127\.|::1|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.) ]]; then ((filtered_bots++)) continue fi # Skip common cloud scanner IPs and known bot networks # AWS, GCP, Azure scanner ranges, security scanners if [[ "$ip" =~ ^(3\.|13\.|18\.|34\.|35\.|52\.|54\.) ]] || \ [[ "$ip" =~ ^(104\.196\.|104\.154\.|130\.211\.|35\.184\.|35\.185\.|35\.186\.|35\.187\.|35\.188\.|35\.189\.|35\.19) ]] || \ [[ "$ip" =~ ^(20\.|40\.|51\.|52\.|13\.64\.|13\.65\.|13\.66\.|13\.67\.|13\.68\.) ]]; then # Check if it's actually a bot by examining user agent line_lower="${line,,}" if [[ "$line_lower" =~ (bot|crawler|spider|scanner|monitor|cloud|amazon|google|microsoft|azure) ]]; then ((filtered_bots++)) # Track bot IP with BOT flag flag_ip_attack "$ip" "BOT" 0 "500 error - filtered as bot/scanner" >/dev/null 2>&1 & continue fi fi # Bot/Scanner filtering - skip noise line_lower="${line,,}" if [[ "$line_lower" =~ (bot|crawler|spider|scraper|scanner|check|monitor|uptime|pingdom|newrelic|datadog|nagios|zabbix|prtg|gomez|keynote|catchpoint|dotcom-monitor|site24x7|uptimerobot|statuscake|nodequery|hetrixtools|freshping|uptrendscom|siteuptime|montastic|updown\.io|apex|alertsite|webmon|wormly) ]]; then ((filtered_bots++)) # Track monitoring/uptime bot flag_ip_attack "$ip" "BOT" 0 "Monitoring/uptime bot" >/dev/null 2>&1 & continue fi if [[ "$line_lower" =~ (semrush|ahrefs|moz|majestic|serpstat|screaming|screamingfrog|sitebulb|linkchecker|validator|scanner|security|acunetix|nessus|openvas|burp|nikto|skipfish|w3af|sqlmap|metasploit|nmap|masscan|zmap|shodan|censys|binaryedge) ]]; then ((filtered_bots++)) # Track scanner with higher score flag_ip_attack "$ip" "SCANNER" 0 "Security scanner detected" >/dev/null 2>&1 & continue fi if [[ "$line_lower" =~ (curl|wget|python|perl|ruby|java|go-http|libwww|axios|node-fetch|http\.client|httpie|postman|insomnia|apachehttp|okhttp|httpclient) ]]; then ((filtered_bots++)) # Track programmatic access flag_ip_attack "$ip" "BOT" 0 "HTTP library/tool" >/dev/null 2>&1 & continue fi # Extract URL if [[ "$request" =~ '"'[A-Z]+[[:space:]]([^[:space:]]+) ]]; then url="${BASH_REMATCH[1]:0:80}" else url="/" fi # Extract timestamp if [[ "$line" =~ \[([^]]+)\] ]]; then timestamp="${BASH_REMATCH[1]}" else timestamp="unknown" fi ((domain_count["$domain"]++)) ((total_500s++)) # Track IP in reputation database (500 error = slight increase in score) # Most 500s are due to server issues, not attacks, so low score increase increment_ip_hits "$ip" 1 >/dev/null 2>&1 & # Save for analysis echo "$domain|$user|$status|$url|$timestamp|$ip" >> "$ERRORS_500" fi done < <(tail -n 100000 "$log" 2>/dev/null) done < <( # Multi-panel log discovery case "$CONTROL_PANEL" in cpanel) # cPanel: Centralized domlogs find "$DOMLOGS_DIR" -type f ! -name "*bytes_log" ! -name "*offset*" ! -name "*error_log" ! -name "*ftpxferlog*" ! -name "*-ssl_log" 2>/dev/null ;; interworx) # InterWorx: Per-domain logs in user homes (uses 'transfer.log') find /home/*/var/*/logs -type f -name "transfer.log" 2>/dev/null ;; plesk) # Plesk: System vhosts logs find /var/www/vhosts/system/*/logs -type f \( -name "access_log" -o -name "access_ssl_log" \) 2>/dev/null ;; *) # Standalone: Try common locations if [ -f "/var/log/httpd/access_log" ]; then echo "/var/log/httpd/access_log"; fi if [ -f "/var/log/apache2/access.log" ]; then echo "/var/log/apache2/access.log"; fi ;; esac ) if [ "$total_500s" -eq 0 ]; then echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${GREEN} ✓ NO 500 ERRORS FOUND IN LAST $HOURS_TO_SCAN HOURS!${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" exit 0 fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " 500 ERRORS DETECTED: $total_500s errors (filtered out $filtered_bots bot/scanner requests)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo -e "${RED}${BOLD}TOP AFFECTED DOMAINS:${NC}" echo "" # Sort and display top domains for domain in "${!domain_count[@]}"; do echo "${domain_count[$domain]} $domain ${domain_user[$domain]}" done | sort -rn | head -10 | while read count domain user; do printf " ${RED}●${NC} %-40s ${BOLD}%4d errors${NC} (user: %s)\n" "$domain" "$count" "$user" done echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " DIAGNOSING ROOT CAUSES" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Detailed diagnostic tracking - per URL/file DETAILED_DIAGNOSIS="$TEMP_DIR/detailed_diagnosis.txt" > "$DETAILED_DIAGNOSIS" declare -A diagnosed_causes declare -A cause_examples total_to_diagnose=$(wc -l < "$ERRORS_500") current_line=0 echo "Analyzing $total_to_diagnose errors for root causes..." echo "" while IFS='|' read -r domain user status url timestamp ip; do [ -z "$domain" ] && continue ((current_line++)) # Show progress every 500 errors if [ $((current_line % 500)) -eq 0 ]; then echo -ne "\rAnalyzed $current_line / $total_to_diagnose errors..." fi diagnosis="" cause="UNKNOWN" specific_file="" # Get document root docroot="/home/$user/public_html" # Try to determine the actual file being requested if [[ "$url" =~ ^/([^?]+) ]]; then url_path="${BASH_REMATCH[1]}" # Handle common patterns if [ -z "$url_path" ] || [ "$url_path" = "/" ]; then possible_files=("$docroot/index.php" "$docroot/index.html") elif [[ "$url_path" =~ \.php$ ]]; then possible_files=("$docroot/$url_path") else possible_files=("$docroot/$url_path" "$docroot/${url_path}.php" "$docroot/$url_path/index.php") fi # Find the actual file for pf in "${possible_files[@]}"; do if [ -f "$pf" ]; then specific_file="$pf" break fi done fi # Find PHP error log - check multiple locations error_log="" if [ "$user" != "unknown" ]; then for potential_log in \ "/home/$user/public_html/error_log" \ "/home/$user/logs/error_log" \ "/home/$user/error_log" \ "/var/log/apache2/domlogs/$domain-error_log" \ "/usr/local/apache/domlogs/$domain"; do if [ -f "$potential_log" ]; then error_log="$potential_log" break fi done fi # Check if error log exists and has recent errors if [ -n "$error_log" ] && [ -f "$error_log" ]; then # Look for errors matching this URL/timestamp recent_error=$(tail -1000 "$error_log" | grep "$url" | tail -1) # If no URL match, get most recent error [ -z "$recent_error" ] && recent_error=$(tail -500 "$error_log" | grep -E "Fatal error|Parse error|syntax error|memory.*exhausted|database|MySQL|Permission denied|failed to open stream" | tail -1) if [ -n "$recent_error" ]; then # Extract file path from error if present if [[ "$recent_error" =~ in[[:space:]](/[^:]+\.php) ]]; then error_file="${BASH_REMATCH[1]}" specific_file="$error_file" fi # Determine cause and diagnose if [[ "$recent_error" =~ (memory.*exhausted|Allowed memory size) ]]; then cause="PHP_MEMORY_EXHAUSTED" # Check current memory limit mem_limit=$(grep -h "memory_limit" "$docroot/.user.ini" "$docroot/../.php.ini" 2>/dev/null | tail -1 | cut -d'=' -f2 | tr -d ' ') [ -z "$mem_limit" ] && mem_limit=$(php -r "echo ini_get('memory_limit');" 2>/dev/null) diagnosis="$domain$url - Memory exhausted (current limit: ${mem_limit:-unknown})" [ -n "$specific_file" ] && diagnosis="$diagnosis - File: $specific_file" elif [[ "$recent_error" =~ (failed to open stream|Permission denied) ]]; then cause="PERMISSION_ERROR" # Extract the file with permission issue if [[ "$recent_error" =~ failed\ to\ open\ stream.*\'([^\']+)\' ]]; then perm_file="${BASH_REMATCH[1]}" elif [[ "$recent_error" =~ Permission\ denied.*\'([^\']+)\' ]]; then perm_file="${BASH_REMATCH[1]}" else perm_file="$specific_file" fi # Check permissions on that specific file if [ -n "$perm_file" ] && [ -e "$perm_file" ]; then file_perms=$(stat -c "%a" "$perm_file" 2>/dev/null) file_owner=$(stat -c "%U:%G" "$perm_file" 2>/dev/null) diagnosis="$domain$url - Permission denied on: $perm_file (perms: $file_perms, owner: $file_owner)" # Check if file is readable by Apache if [ -f "$perm_file" ]; then if ! sudo -u nobody test -r "$perm_file" 2>/dev/null; then diagnosis="$diagnosis - File NOT readable by Apache (nobody user)" fi fi else diagnosis="$domain$url - Permission denied (file in error: $perm_file - does not exist)" fi elif [[ "$recent_error" =~ (Fatal error|PHP Fatal error) ]]; then if [[ "$recent_error" =~ (undefined function|Call to undefined function) ]]; then cause="MISSING_PHP_FUNCTION" # Extract function name if [[ "$recent_error" =~ Call\ to\ undefined\ function\ ([a-zA-Z0-9_]+) ]]; then missing_func="${BASH_REMATCH[1]}" # Check which extension provides this function ext_info=$(php -r "if(function_exists('$missing_func')) echo 'EXISTS'; else echo 'MISSING';" 2>&1) diagnosis="$domain$url - Missing function: $missing_func" [ -n "$specific_file" ] && diagnosis="$diagnosis - in file: $specific_file" else diagnosis="$domain$url - Missing PHP function - $recent_error" fi else cause="PHP_FATAL_ERROR" diagnosis="$domain$url - PHP Fatal Error" [ -n "$specific_file" ] && diagnosis="$diagnosis - File: $specific_file" diagnosis="$diagnosis - ${recent_error:0:200}" fi elif [[ "$recent_error" =~ (Parse error|syntax error) ]]; then cause="PHP_SYNTAX_ERROR" # Extract line number if present if [[ "$recent_error" =~ line\ ([0-9]+) ]]; then error_line="${BASH_REMATCH[1]}" diagnosis="$domain$url - PHP Syntax Error at line $error_line" else diagnosis="$domain$url - PHP Syntax Error" fi [ -n "$specific_file" ] && diagnosis="$diagnosis - File: $specific_file" elif [[ "$recent_error" =~ (database|MySQL|Can\'t connect|Access denied for user) ]]; then cause="DATABASE_CONNECTION" if [[ "$recent_error" =~ Access\ denied\ for\ user\ \'([^\']+)\' ]]; then db_user="${BASH_REMATCH[1]}" diagnosis="$domain$url - Database access denied for user: $db_user" elif [[ "$recent_error" =~ Unknown\ database\ \'([^\']+)\' ]]; then db_name="${BASH_REMATCH[1]}" diagnosis="$domain$url - Unknown database: $db_name" else diagnosis="$domain$url - Database connection error" fi [ -n "$specific_file" ] && diagnosis="$diagnosis - from file: $specific_file" fi # Save detailed diagnosis only if we identified a specific cause if [ "$cause" != "UNKNOWN" ] && [ -n "$diagnosis" ]; then echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS" ((diagnosed_causes["$cause"]++)) [ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis" else # No matching error in error_log - run comprehensive checks if [ "$user" != "unknown" ]; then issue_found="" # Check 1: .htaccess issues if [ -f "$docroot/.htaccess" ]; then htaccess_file="$docroot/.htaccess" htaccess_issues="" # Invalid PHP directives if grep -qE "^[[:space:]]*(php_value|php_flag|php_admin_value|php_admin_flag)" "$htaccess_file" 2>/dev/null; then if ! apache2ctl -M 2>/dev/null | grep -q "php.*module"; then htaccess_issues="PHP directives incompatible with FPM" fi fi # Malformed RewriteRule bad_rewrite=$(grep -nE "RewriteRule.*\[.*[^]]*$" "$htaccess_file" 2>/dev/null | head -1) [ -n "$bad_rewrite" ] && htaccess_issues="${htaccess_issues:+$htaccess_issues; }Malformed RewriteRule: ${bad_rewrite:0:50}" # Missing RewriteBase with RewriteRule if grep -q "RewriteRule" "$htaccess_file" 2>/dev/null; then if ! grep -q "RewriteBase" "$htaccess_file" 2>/dev/null; then # Check if rules use relative paths if grep -qE "RewriteRule.*\^[^/]" "$htaccess_file" 2>/dev/null; then htaccess_issues="${htaccess_issues:+$htaccess_issues; }Missing RewriteBase with relative paths" fi fi fi if [ -n "$htaccess_issues" ]; then cause="HTACCESS_ERROR" diagnosis="$domain$url - $htaccess_issues" issue_found="yes" fi fi # Check 2: PHP file exists and is readable if [ -z "$issue_found" ] && [ -n "$specific_file" ]; then if [ ! -f "$specific_file" ]; then cause="FILE_NOT_FOUND" diagnosis="$domain$url - File does not exist: $specific_file" issue_found="yes" elif [ ! -r "$specific_file" ]; then file_perms=$(stat -c "%a" "$specific_file" 2>/dev/null) cause="PERMISSION_ERROR" diagnosis="$domain$url - File not readable: $specific_file (perms: $file_perms)" issue_found="yes" fi fi # Check 3: Document root permissions if [ -z "$issue_found" ]; then if [ ! -d "$docroot" ]; then cause="DOCROOT_MISSING" diagnosis="$domain$url - Document root does not exist: $docroot" issue_found="yes" else docroot_perms=$(stat -c "%a" "$docroot" 2>/dev/null) if [ "$docroot_perms" != "755" ] && [ "$docroot_perms" != "750" ] && [ "$docroot_perms" != "711" ]; then cause="PERMISSION_ERROR" diagnosis="$domain$url - Docroot bad perms: $docroot ($docroot_perms, should be 755)" issue_found="yes" fi fi fi # Check 4: PHP handler/version issues if [ -z "$issue_found" ] && [ -n "$specific_file" ] && [ -f "$specific_file" ]; then # Check if PHP is configured for this domain php_handler=$(grep -i "phpversion\|php_admin_value" /var/cpanel/userdata/$user/$domain 2>/dev/null | head -1) if [ -z "$php_handler" ]; then # Check if .htaccess tries to set PHP version but fails if grep -qE "AddHandler.*php|SetHandler.*php" "$docroot/.htaccess" 2>/dev/null; then cause="PHP_HANDLER_ERROR" diagnosis="$domain$url - PHP handler misconfigured (check .htaccess AddHandler/SetHandler)" issue_found="yes" fi fi fi # Check 5: WordPress-specific issues (if WP detected) if [ -z "$issue_found" ] && [ -f "$docroot/wp-config.php" ]; then # Check if wp-config.php is readable if [ ! -r "$docroot/wp-config.php" ]; then cause="PERMISSION_ERROR" diagnosis="$domain$url - wp-config.php not readable" issue_found="yes" else # Check for WP_DEBUG causing issues if grep -q "define.*WP_DEBUG.*true" "$docroot/wp-config.php" 2>/dev/null; then # Check if WP_DEBUG_DISPLAY is also true (can cause 500s with some errors) if grep -q "define.*WP_DEBUG_DISPLAY.*true" "$docroot/wp-config.php" 2>/dev/null; then cause="WP_DEBUG_ERROR" diagnosis="$domain$url - WP_DEBUG_DISPLAY enabled (may cause 500 on warnings)" issue_found="yes" fi fi fi fi # Save diagnosis if we found an issue if [ -n "$issue_found" ] && [ -n "$diagnosis" ]; then echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS" ((diagnosed_causes["$cause"]++)) [ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis" else ((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++)) fi else ((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++)) fi fi else # No error in error_log at all - check .htaccess if [ "$user" != "unknown" ] && [ -f "$docroot/.htaccess" ]; then htaccess_file="$docroot/.htaccess" htaccess_issues="" # Check for invalid PHP directives if grep -qE "^[[:space:]]*(php_value|php_flag|php_admin_value|php_admin_flag)" "$htaccess_file" 2>/dev/null; then if ! apache2ctl -M 2>/dev/null | grep -q "php.*module"; then htaccess_issues="PHP directives (php_value/php_flag) incompatible with current PHP handler (not mod_php)" fi fi # Check for malformed RewriteRule bad_rewrite=$(grep -nE "RewriteRule.*\[.*[^]]*$" "$htaccess_file" 2>/dev/null | head -1) if [ -n "$bad_rewrite" ]; then htaccess_issues="${htaccess_issues:+$htaccess_issues; }Malformed RewriteRule: $bad_rewrite" fi if [ -n "$htaccess_issues" ]; then cause="HTACCESS_ERROR" diagnosis="$domain$url - .htaccess error: $htaccess_issues" echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS" ((diagnosed_causes["$cause"]++)) [ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis" else ((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++)) fi else ((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++)) fi fi else # No error log found - check .htaccess and permissions thoroughly if [ "$user" != "unknown" ]; then htaccess_file="$docroot/.htaccess" if [ -f "$htaccess_file" ]; then # Check .htaccess file permissions first htaccess_perms=$(stat -c "%a" "$htaccess_file" 2>/dev/null) htaccess_owner=$(stat -c "%U:%G" "$htaccess_file" 2>/dev/null) # Check if readable by Apache htaccess_readable="yes" if ! sudo -u nobody test -r "$htaccess_file" 2>/dev/null; then htaccess_readable="no" fi # Actually validate .htaccess syntax htaccess_test=$(apache2ctl -t 2>&1) htaccess_issues="" # Check for invalid directives if grep -qE "^[[:space:]]*(php_value|php_flag|php_admin_value|php_admin_flag)" "$htaccess_file" 2>/dev/null; then if ! apache2ctl -M 2>/dev/null | grep -q "php.*module"; then htaccess_issues="PHP directives (php_value/php_flag) incompatible with current PHP handler (not mod_php)" fi fi # Check for malformed RewriteRule bad_rewrite=$(grep -nE "RewriteRule.*\[.*[^]]*$" "$htaccess_file" 2>/dev/null | head -1) if [ -n "$bad_rewrite" ]; then htaccess_issues="${htaccess_issues:+$htaccess_issues; }Malformed RewriteRule: $bad_rewrite" fi # Check for RewriteCond without following RewriteRule orphan_cond=$(grep -n "RewriteCond" "$htaccess_file" | while read line; do linenum=$(echo "$line" | cut -d: -f1) nextline=$((linenum + 1)) next=$(sed -n "${nextline}p" "$htaccess_file") if [[ ! "$next" =~ RewriteRule|RewriteCond ]]; then echo "Line $linenum: RewriteCond without RewriteRule" fi done | head -1) if [ -n "$orphan_cond" ]; then htaccess_issues="${htaccess_issues:+$htaccess_issues; }$orphan_cond" fi # Check for syntax errors in .htaccess if echo "$htaccess_test" | grep -qi "syntax error"; then syntax_err=$(echo "$htaccess_test" | grep -i "syntax error" | head -1) htaccess_issues="${htaccess_issues:+$htaccess_issues; }Apache syntax error: $syntax_err" fi if [ "$htaccess_readable" = "no" ]; then cause="PERMISSION_ERROR" diagnosis="$domain$url - .htaccess not readable by Apache (perms: $htaccess_perms, owner: $htaccess_owner)" echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS" ((diagnosed_causes["$cause"]++)) [ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis" elif [ -n "$htaccess_issues" ]; then cause="HTACCESS_ERROR" diagnosis="$domain$url - .htaccess error: $htaccess_issues" echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS" ((diagnosed_causes["$cause"]++)) [ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis" else # .htaccess appears valid - check document root and file permissions docroot_perms=$(stat -c "%a" "$docroot" 2>/dev/null) docroot_owner=$(stat -c "%U:%G" "$docroot" 2>/dev/null) if [ "$docroot_perms" != "755" ] && [ "$docroot_perms" != "750" ]; then cause="PERMISSION_ERROR" diagnosis="$domain$url - Document root incorrect permissions (perms: $docroot_perms, owner: $docroot_owner, should be 755)" echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS" ((diagnosed_causes["$cause"]++)) [ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis" elif [ -n "$specific_file" ] && [ -f "$specific_file" ]; then # Check the specific PHP file permissions file_perms=$(stat -c "%a" "$specific_file" 2>/dev/null) file_owner=$(stat -c "%U:%G" "$specific_file" 2>/dev/null) file_readable="yes" if ! sudo -u nobody test -r "$specific_file" 2>/dev/null; then file_readable="no" fi if [ "$file_readable" = "no" ]; then cause="PERMISSION_ERROR" diagnosis="$domain$url - File not readable by Apache: $specific_file (perms: $file_perms, owner: $file_owner)" echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS" ((diagnosed_causes["$cause"]++)) [ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis" else ((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++)) fi else ((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++)) fi fi else # No .htaccess - check document root and file permissions docroot_perms=$(stat -c "%a" "$docroot" 2>/dev/null) docroot_owner=$(stat -c "%U:%G" "$docroot" 2>/dev/null) if [ "$docroot_perms" != "755" ] && [ "$docroot_perms" != "750" ]; then cause="PERMISSION_ERROR" diagnosis="$domain$url - Document root incorrect permissions (perms: $docroot_perms, owner: $docroot_owner, should be 755)" echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS" ((diagnosed_causes["$cause"]++)) [ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis" elif [ -n "$specific_file" ] && [ -f "$specific_file" ]; then # Check the specific PHP file file_perms=$(stat -c "%a" "$specific_file" 2>/dev/null) file_owner=$(stat -c "%U:%G" "$specific_file" 2>/dev/null) if ! sudo -u nobody test -r "$specific_file" 2>/dev/null; then cause="PERMISSION_ERROR" diagnosis="$domain$url - File not readable: $specific_file (perms: $file_perms, owner: $file_owner)" echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS" ((diagnosed_causes["$cause"]++)) [ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis" else ((diagnosed_causes["NO_ERROR_LOG_FILE"]++)) fi else ((diagnosed_causes["NO_ERROR_LOG_FILE"]++)) fi fi else ((diagnosed_causes["NO_ERROR_LOG_FILE"]++)) fi fi done < "$ERRORS_500" echo -e "\rAnalyzed $total_to_diagnose / $total_to_diagnose errors - Complete! " echo "" # Display diagnosed causes echo -e "${CYAN}${BOLD}ROOT CAUSES IDENTIFIED:${NC}" echo "" for cause in "${!diagnosed_causes[@]}"; do count="${diagnosed_causes[$cause]}" echo "$count|$cause" done | sort -rn | while IFS='|' read count cause; do clean_cause=$(echo "$cause" | tr '_' ' ') case "$cause" in PHP_MEMORY_EXHAUSTED) echo -e "${RED}$clean_cause${NC} - $count occurrences" echo -e " ${YELLOW}Fix:${NC} Increase memory_limit in /home/USER/public_html/.user.ini" echo -e " ${YELLOW}Set:${NC} memory_limit = 512M" ;; PHP_FATAL_ERROR) echo -e "${RED}$clean_cause${NC} - $count occurrences" echo -e " ${YELLOW}Fix:${NC} Review recent code/plugin changes" ;; PHP_SYNTAX_ERROR) echo -e "${RED}$clean_cause${NC} - $count occurrences" echo -e " ${YELLOW}Fix:${NC} Check for PHP syntax errors in recent file edits" ;; MISSING_PHP_FUNCTION) echo -e "${RED}$clean_cause${NC} - $count occurrences" echo -e " ${YELLOW}Fix:${NC} Install missing PHP extension (check error for which one)" ;; DATABASE_CONNECTION) echo -e "${RED}$clean_cause${NC} - $count occurrences" echo -e " ${YELLOW}Fix:${NC} Check database credentials or MySQL service" ;; HTACCESS_ERROR) echo -e "${RED}.HTACCESS ERROR DETECTED${NC} - $count occurrences" ;; PERMISSION_ERROR) echo -e "${RED}PERMISSION ERROR DETECTED${NC} - $count occurrences" ;; NO_ERROR_LOG_FILE) echo -e "${YELLOW}$clean_cause${NC} - $count occurrences" echo -e " ${YELLOW}Note:${NC} Checked multiple error_log locations - none found" echo -e " ${YELLOW}Check:${NC} May need to enable PHP error logging" ;; NO_PHP_ERROR_LOGGED) echo -e "${YELLOW}$clean_cause${NC} - $count occurrences" echo -e " ${YELLOW}Note:${NC} 500 error but no PHP error in log - likely .htaccess or Apache config" ;; FILE_NOT_FOUND) echo -e "${RED}$clean_cause${NC} - $count occurrences" echo -e " ${YELLOW}Fix:${NC} Requested file does not exist - check URL or restore missing files" ;; PHP_HANDLER_ERROR) echo -e "${RED}PHP HANDLER ERROR${NC} - $count occurrences" echo -e " ${YELLOW}Fix:${NC} PHP handler misconfigured - check cPanel PHP version or .htaccess AddHandler" ;; WP_DEBUG_ERROR) echo -e "${YELLOW}WORDPRESS DEBUG ERROR${NC} - $count occurrences" echo -e " ${YELLOW}Fix:${NC} Disable WP_DEBUG_DISPLAY in wp-config.php or fix underlying warnings" ;; DOCROOT_MISSING) echo -e "${RED}$clean_cause${NC} - $count occurrences" echo -e " ${YELLOW}Fix:${NC} Document root directory missing - restore from backup or check domain configuration" ;; UNKNOWN) # Skip - these are errors we couldn't diagnose ;; *) echo -e "${INFO_COLOR}$clean_cause${NC} - $count occurrences" ;; esac # Show example if we have one if [ -n "${cause_examples[$cause]}" ]; then example="${cause_examples[$cause]}" echo -e " ${DIM}Example: ${example:0:120}...${NC}" fi echo "" done echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " DETAILED 500 ERROR LIST" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Show most frequent URLs getting 500s echo "Most Frequent 500 Error URLs:" echo "" cut -d'|' -f1,4 "$ERRORS_500" | sort | uniq -c | sort -rn | head -15 | while read count domain_url; do domain=$(echo "$domain_url" | cut -d'|' -f1) url=$(echo "$domain_url" | cut -d'|' -f2) user="${domain_user[$domain]}" printf " ${RED}%4d×${NC} %s%s ${DIM}(user: %s)${NC}\n" "$count" "$domain" "$url" "$user" done echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " SPECIFIC DIAGNOSTICS (per URL/file)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Show detailed diagnostics grouped by cause and issue pattern if [ -f "$DETAILED_DIAGNOSIS" ] && [ -s "$DETAILED_DIAGNOSIS" ]; then for cause_type in PHP_MEMORY_EXHAUSTED PERMISSION_ERROR HTACCESS_ERROR PHP_FATAL_ERROR PHP_SYNTAX_ERROR MISSING_PHP_FUNCTION DATABASE_CONNECTION FILE_NOT_FOUND PHP_HANDLER_ERROR WP_DEBUG_ERROR DOCROOT_MISSING; do cause_count=$(grep "^${cause_type}|" "$DETAILED_DIAGNOSIS" 2>/dev/null | wc -l) cause_count=${cause_count//[^0-9]/} # Remove any non-numeric characters cause_count=${cause_count:-0} # Default to 0 if empty if [ "$cause_count" -gt 0 ]; then cause_display=$(echo "$cause_type" | tr '_' ' ') echo -e "${RED}${BOLD}$cause_display ($cause_count occurrences)${NC}" echo "" # Group by unique error pattern (not domain) declare -A issue_domains # First pass: collect all domains per issue pattern declare -A pattern_domains_temp while IFS='|' read -r ctype full_diag; do # Extract just the error part (after domain/) issue_pattern=$(echo "$full_diag" | sed 's/^[^ ]* - //') domain_part=$(echo "$full_diag" | grep -oP '^[^/]+' 2>/dev/null) # Append to temporary storage pattern_domains_temp[$issue_pattern]+="$domain_part"$'\n' done < <(grep "^${cause_type}|" "$DETAILED_DIAGNOSIS" 2>/dev/null) # Second pass: deduplicate and limit to 5 unique domains per pattern for pattern in "${!pattern_domains_temp[@]}"; do # Get unique domains, limit to 5 unique_list=$(echo "${pattern_domains_temp[$pattern]}" | sort -u | head -5 | tr '\n' ',') # Remove trailing comma unique_list=${unique_list%,} # Count total unique domains total_unique=$(echo "${pattern_domains_temp[$pattern]}" | sort -u | wc -l) # Add "..." if there are more than 5 if [ "$total_unique" -gt 5 ]; then unique_list="$unique_list,..." fi issue_domains[$pattern]="$unique_list" done unset pattern_domains_temp # Display grouped issues shown=0 for pattern in "${!issue_domains[@]}"; do [ "${shown:-0}" -ge 10 ] && break ((shown++)) domains="${issue_domains[$pattern]}" domain_count=$(echo "$domains" | tr ',' '\n' | grep -v '^\.\.\.$' | wc -l) echo -e " ${YELLOW}Issue:${NC} $pattern" echo -e " ${DIM}Affected ($domain_count):${NC} ${domains//,/, }" echo "" done if [ "${#issue_domains[@]}" -gt 10 ]; then remaining=$((${#issue_domains[@]} - 10)) echo -e " ${DIM}... and $remaining more issue patterns${NC}" echo "" fi unset issue_domains fi done else echo "No detailed diagnostics available." echo "" fi echo "Full error list saved to: $ERRORS_500" echo "Detailed diagnostics saved to: $DETAILED_DIAGNOSIS" echo "" press_enter