HEX
Server: nginx/1.29.3
System: Linux 11979.bigscoots-wpo.com 6.8.0-88-generic #89-Ubuntu SMP PREEMPT_DYNAMIC Sat Oct 11 01:02:46 UTC 2025 x86_64
User: nginx (1068)
PHP: 7.4.33
Disabled: exec,system,passthru,shell_exec,proc_open,proc_close,popen,show_source,cmd# Do not modify this line # 1684243876
Upload Files
File: //bigscoots/includes/common.sh
#!/bin/bash

#
# Common Variables
#

PATH=/usr/lib64/ccache:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:/root/bin:/root/.local/bin

# Grab the server IP
serverip=$(ip route get 1 | grep -oP 'src \K[0-9.]+')

# Set default SSH options
SSH_OPTIONS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o BatchMode=yes -q)

#
# Common Functions
#

# Function name: wpcli
# Purpose: This function wraps the 'wp' command, which is used for managing WordPress installations from the command line.
# The function adds a series of flags to the 'wp' command to modify its behavior in specific ways.

function wpcli {
    # WPCLIFLAGS is a variable that contains several flags that will be used with every 'wp' command:
    # --allow-root: This allows 'wp' commands to be run as the root user.
    # --skip-plugins: This prevents plugins from being loaded when executing 'wp' commands.
    # --skip-themes: This prevents themes from being loaded when executing 'wp' commands.
    # --require=/bigscoots/includes/err_report.php: This causes 'wp' to load the err_report.php file before running any 'wp' commands.
    # The err_report.php file can contain any PHP code, which is typically used to set up environment variables or define helper functions.
    WPCLIFLAGS=(--allow-root --skip-plugins --skip-themes --require=/bigscoots/includes/err_report.php)


    # This line runs the 'wp' command with the flags defined in WPCLIFLAGS, followed by any arguments passed to the wpcli function.
    # For example, if you run 'wpcli plugin activate my-plugin', this line will run 'wp --allow-root --skip-plugins --skip-themes --require=/bigscoots/includes/err_report.php plugin activate my-plugin'.
    wp "${WPCLIFLAGS[@]}" "$@"
}

n_wpcli() {
    [ -f /bin/wp ] && chmod 755 /bin/wp
    [ -f /usr/bin/wp ] && chmod 755 /usr/bin/wp
    su -s /bin/bash -l nginx -c "source /bigscoots/includes/common.sh && wpcli $*"
}

n_wp() {
    [ -f /bin/wp ] && chmod 755 /bin/wp
    [ -f /usr/bin/wp ] && chmod 755 /usr/bin/wp
    su -s /bin/bash -l nginx -c "source /bigscoots/includes/common.sh && wp $*"
}

# Function name: validate_domain
# Purpose: This function is used to validate the format of a domain name.

validate_domain() {
	# We store the first argument passed to the function in a local variable 'domain'.
	local domain="$1"
    local script="$2"

	# Here, we check whether the 'domain' variable is empty or does not match the regular expression.
	# The regular expression checks if the domain name contains only alphanumeric characters, hyphens, periods, and at least one dot (.) character. 
	# The domain name should also end with a sequence of alphabetic characters (a-z or A-Z), signifying a valid top-level domain (e.g., .com, .net, .org).
	if [[ -z "$domain" || ! "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]+$ ]]
    then
	  
	  # If the domain name is either empty or doesn't match the regular expression, we echo a JSON response with a "status" of "fail" and an appropriate error message.
	  echo "{\"status\":\"fail\",\"msg\":\"Invalid domain name format. A valid domain name should contain a dot (.) and consist of alphanumeric characters, hyphens, and periods.\"}"
	  
	  # After displaying the error message, we exit the function with a non-zero status code (1), indicating that an error occurred.
	  exit 1
	fi

    # Check if the domain starts with "www."
    if [[ "$domain" == www.* ]]
    then
        send_slack_alert "#wpo-fail" ":x:" "Domain Validation" "$domain" "Domain starts with www. which means this should not exist in /home/nginx/domains/ \n *Script:* $script"
        exit 1
    fi
}

remove_ocsp_directives() {
    local conf_dir="/usr/local/nginx/conf/conf.d"
    local dry_run="${1:-false}"
    local files_modified=0
    local files_skipped=0

    echo "=== OCSP/Resolver Directive Removal ==="
    echo "Directory: $conf_dir"
    echo ""

    for conf_file in "$conf_dir"/*.conf; do
        # Skip if no files match
        [[ -e "$conf_file" ]] || { echo "No .conf files found in $conf_dir"; return 1; }

        # Check if file actually contains any of the directives
        if ! grep -qE '^\s*(resolver|resolver_timeout|ssl_stapling|ssl_stapling_verify)\s' "$conf_file"; then
            echo "[SKIP]     $conf_file  (no matching directives found)"
            ((files_skipped++))
            continue
        fi

        echo "[FOUND]    $conf_file"

        # Show what will be removed
        grep -nE '^\s*(resolver|resolver_timeout|ssl_stapling|ssl_stapling_verify)\s' "$conf_file" | \
            while IFS= read -r line; do
                echo "           Line $line"
            done

        if [[ "$dry_run" == "true" ]]; then
            echo "           [DRY-RUN] No changes made"
        else
            # Backup original
            cp "$conf_file" "${conf_file}.bak"

            # Remove the directives
            sed -i '/^\s*resolver\s/d;
                    /^\s*resolver_timeout\s/d;
                    /^\s*ssl_stapling\s/d;
                    /^\s*ssl_stapling_verify\s/d' "$conf_file"

            echo "           [DONE] Backup saved to ${conf_file}.bak"
            ((files_modified++))
        fi
        echo ""
    done

    echo "=== Summary ==="
    if [[ "$dry_run" == "true" ]]; then
        echo "Mode:     DRY-RUN (no files were modified)"
    else
        echo "Modified: $files_modified file(s)"
    fi
    echo "Skipped:  $files_skipped file(s) (no matching directives)"
    echo ""

    # Validate nginx config if changes were made
    if [[ "$dry_run" != "true" && $files_modified -gt 0 ]]; then
        echo "=== Nginx Config Test ==="
        if nginx -t 2>&1; then
            echo ""
            echo "Config OK β€” run 'systemctl reload nginx' to apply changes."
        else
            echo ""
            echo "WARNING: nginx -t failed! Review the errors above."
            echo "Backups are available at each modified file path with .bak extension."
        fi
    fi
}

validate_domain_in_path() {
    local domain="$1"
    local script="$2"
    local error_msg=""

    # 1. Check if the domain is empty or does not match regex
    if [[ -z "$domain" || ! "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]+$ ]]; then
        error_msg="Domain is empty or invalid format"

    # 2. Check if the domain starts with "www."
    elif [[ "$domain" == www.* ]]; then
        error_msg="Domain starts with www (should be root domain)"

    # 3. Check if the directory exists
    elif [ ! -d "/home/nginx/domains/$domain/public" ]; then
        error_msg="Public directory not found on server"
    fi

    # --- FAILURE HANDLER ---
    if [[ -n "$error_msg" ]]; then
        # 1. Send Slack Alert (Preserved your existing logic)
        send_slack_alert "#wpo-fail" ":x:" "Domain Validation" "$domain" "${error_msg}. \n *Script:* $script"

        # 2. Output JSON Failure
        # We escape quotes in $domain and $error_msg to ensure valid JSON
        printf '{"status": "failure", "domain": "%s", "error": "%s"}\n' "$domain" "$error_msg"
        
        return 1
    fi

    # Optional: If you need a success JSON, uncomment the line below
    # printf '{"status": "success", "domain": "%s", "error": null}\n' "$domain"
    
    return 0
}

# ==============================================================================
# SLACK NOTIFICATIONS (via n8n/Saumya)
# ==============================================================================
# Usage:
#   1. Initial Message: THREAD_ID=$(send_slack_initial "Main Alert Text" "#channel")
#   2. Thread Reply:    send_slack_thread "$THREAD_ID" "Reply Text" "#channel"
# 
# Note: #channel is optional (defaults to #alerts). 
# send_slack_initial returns the parent_msg_id needed for threading.
# ==============================================================================
source /bigscoots/includes/slack_functions.sh

# Function name: send_slack_alert
# Purpose: This function is used to send a Slack alert with customizable parameters

send_slack_alert() {
    # Declare local variables for the arguments
    # channel: The Slack channel to send the alert to
    local channel="$1"
    # emoji: The emoji to include in the alert
    local emoji="$2"
    # tag: A tag to include in the alert, typically to signify the source or type of alert
    local tag="$3"
    # Domain: Domain related to the error, may not always be included.
    local domain="$4"
    # message: The main content of the alert
    local message=$(echo "$5" | sed ':a;N;$!ba;s/\n/\\n/g')

    # Call the slack.sh script with the constructed message.
    # This script is assumed to handle the sending of the message to Slack.
    # It is passed the channel and a constructed message that includes
    # the emoji, tag, hostname of the local machine, server IP, and the main message content.
    bash /bigscoots/general/slack.sh "${channel}" "${emoji} *${tag}*\n*Hostname:* $(hostname)\n *Server IP:* ${serverip}\n *Domain:* ${domain}\n *Message:* ${message}"
    
    # Example usage:
    # send_slack_alert "#team-chat" ":smile:" "Error(Appears next to Emoji)" "bigscoots.com" "server on fire"
}

check_virt() {
    # 1. Is it LXD? 
    # (Check for the LXD device folder or the agent mount)
    if [ -d /dev/lxd ] || [ -d /dev/.lxd-mounts ] || [ -d /run/lxd_agent ]; then
        # Now distinguish between VM and Container
        if [ -d /run/lxd_agent ] || grep -q "/dev/sd" /proc/mounts; then
            echo "lxd-virtual-machine"
        else
            echo "lxd-container"
        fi
        return
    fi

    # 2. Is it OpenVZ?
    if [ -f /proc/user_beancounters ] || [ -d /proc/vz ] || grep -q "ploop" /proc/mounts; then
        echo "openvz"
        return
    fi

    # 3. Fallback to Systemd (Bare Metal will return 'none')
    if command -v systemd-detect-virt >/dev/null 2>&1; then
        local res=$(systemd-detect-virt)
        if [ "$res" = "none" ]; then
            echo "baremetal"
        else
            echo "$res"
        fi
    else
        echo "unknown"
    fi
}

# Function name: correct_permissions_ownership
# Purpose: This function is used to correct the ownership and permissions of files and directories

correct_permissions_ownership() {
    (
        set +m
        local domain="${1:-*}"
        # Remove the quotes from the search_path definition so the shell expands the wildcard
        local search_path=/home/nginx/domains/$domain/public

        # Check if the path actually exists before running
        if ls $search_path >/dev/null 2>&1; then
             # Use the expanded path directly
             nohup setsid ionice -c 3 find $search_path \
                \( \! -user nginx -exec chown nginx: {} + \) , \
                \( -type f \! -perm 644 -exec chmod 644 {} + \) , \
                \( -type d \! -perm 755 -exec chmod 755 {} + \) > /dev/null 2>&1 &
        fi
    )
}

skip_all_plugins_except() {
    local noskip_plugin="$1"
    local skipped_plugins=""
    local formatted_skipped_plugins=""

    # Build base command
    local cmd=(wpcli plugin list --field=name)

    # Only add --path if $domain is set
    if [ -n "$domain" ]; then
        cmd+=(--path="/home/nginx/domains/${domain}/public")
    fi

    # Run the command, filter out the plugin to keep, strip "/plugin-file.php" if present
    skipped_plugins=$("${cmd[@]}" 2>/dev/null | grep -v "${noskip_plugin}" | sed 's:/.*::')

    # Format as comma-separated
    formatted_skipped_plugins=$(echo "$skipped_plugins" | paste -sd,)

    echo "${formatted_skipped_plugins}"
}

# Function to start the timer
start_timer() {
  START_TIME=$(date +%s)
  START_TIME_READABLE=$(date)
}

# Function to end the timer and display the elapsed time
end_timer() {
  END_TIME=$(date +%s)
  END_TIME_READABLE=$(date)
  echo "Task started at: $START_TIME_READABLE"
  echo "Task ended at: $END_TIME_READABLE"
  
  ELAPSED_TIME=$((END_TIME - START_TIME))
  ELAPSED_TIME_READABLE=$(date -u -d @"$ELAPSED_TIME" +'%T')
  
  echo "Total elapsed time: $ELAPSED_TIME seconds ($ELAPSED_TIME_READABLE)"
}

ngxreload_t() {
    local message="" noexit=false

    # parse args
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --noexit) noexit=true ;;
            *) message="$1" ;;
        esac
        shift
    done

    local lineno="${BASH_LINENO[0]}"
    local caller_file="${BASH_SOURCE[1]}"

    if nginx -t > /dev/null 2>&1; then
        ngxreload > /dev/null 2>&1
    else
        send_slack_alert "#team-chat" ":warning:" "nginx config fail" "N/A" \
          "$message\n *Script throwing error:* \`${caller_file}\` *Line:* \`${lineno}\`"

        if $noexit; then
            return 1   # fail, but don’t exit the whole script
        else
            exit 1
        fi
    fi
}

centmin_upgrade_124() {
    if ! grep -q ^132. /etc/centminmod-release > /dev/null 2>&1
    then
        (
            cmupdate > /dev/null 2>&1 && 
            cmupdate update-stable > /dev/null 2>&1 && 
            pushd /usr/local/src/centminmod > /dev/null 2>&1 && 
            expect /bigscoots/wpo/manage/expect/centmin > /dev/null 2>&1
        ) || send_slack_alert "#wpo-fail" ":x:" "Centmin Upgrade" "NA" "Failed at step $?"
    fi
}

fix_mariadb103_repo() {
  # Handle MariaDB 10.1 – alert and exit early
  if grep -q "10\.1\." /etc/yum.repos.d/mariadb.repo; then
    yum-config-manager --save --setopt=mariadb.skip_if_unavailable=true > /dev/null 2>&1
    send_slack_alert "#wpo-fail" ":warning:" "mariadb-version" "NA" "I am running MariaDB10.1 please upgrade."
    return
  fi

  # Convert CentminMod-style MariaDB 10.3 CentOS 7 repo to archive
  if grep -q "http://yum.mariadb.org/10.3/centos7-amd64" /etc/yum.repos.d/mariadb.repo; then
    sed -i 's|http://yum.mariadb.org/10.3/centos7-amd64|https://archive.mariadb.org/mariadb-10.3/yum/centos7-amd64|' /etc/yum.repos.d/mariadb.repo
    sed -i 's|https://yum.mariadb.org/RPM-GPG-KEY-MariaDB|https://archive.mariadb.org/PublicKey|' /etc/yum.repos.d/mariadb.repo
    yum -q clean all
  fi

  # Convert CentminMod-style MariaDB 10.3 CentOS 8 repo to archive
  if grep -q "http://yum.mariadb.org/10.3/centos8-amd64" /etc/yum.repos.d/mariadb.repo; then
    sed -i 's|http://yum.mariadb.org/10.3/centos8-amd64|https://archive.mariadb.org/mariadb-10.3/yum/centos8-amd64|' /etc/yum.repos.d/mariadb.repo
    sed -i 's|https://yum.mariadb.org/RPM-GPG-KEY-MariaDB|https://archive.mariadb.org/PublicKey|' /etc/yum.repos.d/mariadb.repo
    yum -q clean all
  fi

  # Fix legacy Rackspace mirror usage
  if grep -q "mirror.rackspace.com/mariadb/yum/10.3" /etc/yum.repos.d/mariadb.repo; then
    cat > /etc/yum.repos.d/mariadb.repo <<EOF
[mariadb]
name = MariaDB
baseurl = https://archive.mariadb.org/mariadb-10.3/yum/centos7-amd64
gpgkey = https://supplychain.mariadb.com/MariaDB-Server-GPG-KEY
gpgcheck = 1
exclude = MariaDB-Galera-server
EOF
    yum -q clean all
  fi

  # Source OS info
  . /etc/os-release

  # Replace deprecated dlm.mariadb.com repo for CentOS 7
  if [[ "$ID" == "centos" && "$VERSION_ID" == "7" ]] && \
     grep -q "dlm.mariadb.com/repo/mariadb-server/10.6" /etc/yum.repos.d/mariadb.repo; then
    cat > /etc/yum.repos.d/mariadb.repo <<EOF
[mariadb]
name = MariaDB
baseurl = https://archive.mariadb.org/mariadb-10.6/yum/centos7-amd64
gpgkey=https://supplychain.mariadb.com/MariaDB-Server-GPG-KEY
gpgcheck=1
exclude=MariaDB-Galera-server
skip_if_unavailable = 1
keepcache = 0
EOF
  fi
}

wpcli_update() {
    # --- Pre-check: Nuke and reinstall if wp binary is missing or empty ---
    local wp_bin
    wp_bin=$(type -P wp 2>/dev/null)
    if [ -z "$wp_bin" ] || [ ! -s "$wp_bin" ] || [ ! -s /usr/bin/wp ]; then
        rm -f /usr/local/bin/wp /usr/bin/wp /bin/wp
        if bash /usr/local/src/centminmod/addons/wpcli.sh install > /dev/null 2>&1; then
            ln -fs /bin/wp /usr/local/bin/wp
            chmod 775 /bin/wp /usr/local/bin/wp
        else
            send_slack_alert "#wpo-fail" ":warning:" "wp cli reinstall failed (empty binary)" "NA" "Binary was empty or missing. Reinstall via \`wpcli.sh install\` also failed."
            return 1
        fi
    fi

    current_wpcli_version=$(wp cli version --allow-root 2>/dev/null | awk '{print $2}')
    mariadb_version=$(mysql -V | grep -oP 'Distrib\s+\K10\.3')

    # --- Part 1: Handle Main WP-CLI Binary (Update/Downgrade logic) ---
    if [ "$mariadb_version" == "10.3" ]; then
        if [ "$current_wpcli_version" != "2.11.0" ]; then
            rm -f /usr/local/bin/wp /bin/wp
            curl -sSL -o /usr/bin/wp https://github.com/wp-cli/wp-cli/releases/download/v2.11.0/wp-cli-2.11.0.phar
            chmod +x /usr/bin/wp
            ln -fs /usr/bin/wp /usr/local/bin/wp
            wp cli cache clear --allow-root --quiet
        fi
    else
        if ! err_output=$(wp cli update --allow-root --yes --quiet 2>&1); then
            [ -d /root/.wp-cli ] && rm -rf /root/.wp-cli /usr/bin/wp /usr/local/bin/wp
            if bash /usr/local/src/centminmod/addons/wpcli.sh install > /dev/null 2>&1; then
                ln -fs /bin/wp /usr/local/bin/wp
                chmod 775 /bin/wp /usr/local/bin/wp
            else
                send_slack_alert "#wpo-fail" ":warning:" "wp cli update failed" "NA" "Attempt via \`wp cli update\` \`\`\` $err_output \`\`\` \n attempted \`bash /usr/local/src/centminmod/addons/wpcli.sh install\` as well."
            fi
        else
            if [ ! -L /usr/local/bin/wp ]; then
                rm -f /usr/local/bin/wp /bin/wp
                bash /usr/local/src/centminmod/addons/wpcli.sh install > /dev/null 2>&1
                ln -fs /bin/wp /usr/local/bin/wp
                chmod 775 /bin/wp /usr/local/bin/wp
            fi
        fi
    fi

    # --- Part 2: Generate PHP Version Wrappers (wp74, wp81, etc.) ---
    for php_bin in /opt/remi/php*/root/usr/bin/php; do
        [ -e "$php_bin" ] || continue
        if [[ $php_bin =~ /opt/remi/php([0-9]+)/root/usr/bin/php ]]; then
            version="${BASH_REMATCH[1]}"
            wrapper="/usr/local/bin/wp${version}"
            cat <<EOF > "$wrapper"
#!/bin/bash
# Auto-generated wrapper: Run WP-CLI using PHP ${version}
exec "$php_bin" /usr/local/bin/wp --allow-root "\$@"
EOF
            chmod +x "$wrapper"
        fi
    done
}

get_wpo_api_hash() {
    local wpo_api_hash

    if ! wpo_api_hash=$(curl -s https://www.bigscoots.com/downloads/uniquevisithash 2>/dev/null)
    then
        send_slack_alert "#wpo-errors" ":warning:" "wpo api hash fail" "Failed to pull the hash using CURL."
        exit 1
    fi

    echo "$wpo_api_hash"
}

yum_check() {
    local lock_file="/var/lock/yum_check.lock"
    local expiration_time=86400  # 24 hours in seconds

    # Check if lock file exists and exit if it does
    if [[ -f "$lock_file" ]]; then
        local current_time=$(date +%s)
        local file_modification_time=$(stat -c %Y "$lock_file")

        # Check if lock file is older than 24 hours
        if (( current_time - file_modification_time < expiration_time )); then
            return 0
        fi
    fi
    
    # Clean yum cache
    yum clean all > /dev/null 2>&1

    # Check for available updates
    local output=$(yum check-update 2>&1)

    # Check the exit code
    local exit_code=$?
    if [[ $exit_code -ne 0 && $exit_code -ne 100 ]]; then
        local logfile="/root/.bigscoots/logs/yum_checkupdate-fail-$(date +%s).log"
        echo "$output" > "$logfile"
        send_slack_alert "#wpo-fail" ":x:" "YUM" "N/A" "A yum check failed, please check: $logfile"
        exit 1
    fi
    
    # Create lock file
    touch "$lock_file"
}

check_process_runtime() {
    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case $1 in
            --kill)
                KILL_PROCESS=true
                shift
                ;;
            *)
                PROCESS_NAMES+=("$1")
                shift
                ;;
        esac
    done

    # Loop through each process name provided as arguments
    for process_name in "${PROCESS_NAMES[@]}"
    do
        # Get the PID and CPU time of the specified process, excluding awk itself
        ps_output=$(ps -eo pid,etime,args | awk -v process_name="$process_name" '$0 ~ process_name && !/awk/ {print}')

        # Loop through each line of the output
        while read -r line
        do
            # Split the line into PID, elapsed time, and command
            pid=$(echo "$line" | awk '{print $1}')
            etime=$(echo "$line" | awk '{print $2}')
            command=$(echo "$line" | awk '{$1=""; $2=""; print substr($0,3)}')

            # Remove leading zeros from the elapsed time
            etime=$(echo "$etime" | sed 's/^0*//')

            # Extract days, hours, minutes, and seconds from elapsed time
            IFS='-:' read -r days hours minutes seconds <<< "$etime"

            # Calculate total minutes
            total_minutes=$(( (10#$days * 24 * 60) + (10#$hours * 60) + (10#$minutes) ))

            # Check if the process has been running for more than 30 minutes
            if [ "$total_minutes" -gt 30 ]
            then
                send_slack_alert "#wpo-alerts" ":hourglass_flowing_sand:" "Long Running Process" "NA" "Process with PID $pid ($process_name) has been running for more than 30 minutes."

                # Kill the process if the --kill option is provided
                if [ "$KILL_PROCESS" = true ]; then
                    kill -9 "$pid"
                    send_slack_alert "#wpo-alerts" ":skull_and_crossbones:" "Process Killed" "NA" "Process with PID $pid ($process_name) has been terminated."
                fi
            fi
        done <<< "$ps_output"
    done
}

find_wp_installs() {
    local root_dir="/home/nginx/domains"
    local wp_includes="wp-includes/version.php"
    # Function to check if a directory contains a WordPress installation
    check_wp_install() {
        local dir=$1
        if [[ -f "$dir/$wp_includes" ]]; then
            echo "$dir"
        fi
    }
    # Use find to locate wp-includes directories, excluding wp-content and wp-admin
    find "$root_dir"/*/public -mindepth 1 -maxdepth 2 \( -path "*/wp-content" -o -path "*/wp-admin" \) -prune -o -type d -name "wp-includes" -print | while read -r wp_includes_dir; do
        check_wp_install "$(dirname "$wp_includes_dir")"
    done
}

install_package() {
    if ! rpm -q $1 >/dev/null 2>&1
    then
        yum install -y -q $1
    fi
}

generate_json_response() {
    local success="$1"
    local message="$2"
    local result="$3"
    local ip="${4:-""}"
    local domain="${5:-""}"

    jq -n \
        --argjson success "$success" \
        --arg message "$message" \
        --argjson result "$result" \
        --arg ip "$ip" \
        --arg domain "$domain" \
        '{
            "success": $success,
            "messages": [$message],
            "ip": $ip,
            "domain": $domain,
            "result": $result
        }'
}

send_webhook() {
    local webhook_url="$1"
    local auth_header="$2"
    local json_payload="$3"

    #echo "curl -s -w \"\\n%{http_code}\" -X POST -H \"Authorization: ${auth_header}\" -H \"Content-Type: application/json\" -d '${json_payload}' \"${webhook_url}\""

    # Send the webhook and capture the response
    response=$(curl -s -w "\n%{http_code}" -X POST -H "Authorization: ${auth_header}" -H "Content-Type: application/json" -d "${json_payload}" "${webhook_url}")
    http_status=$(echo "$response" | tail -n1)
    body=$(echo "$response" | head -n1)

    # Determine success based on HTTP status code
    if [ "$http_status" -eq 200 ]; then
        generate_json_response true "Webhook has been sent successfully" "$body"
    else
        generate_json_response false "Webhook failed with status code $http_status" "$body"
    fi
}

# Initialize a default JSON response
function init_json_response() {
  json='{"errors": [], "messages": [], "success": false, "result": {}}'
}

# Function to add an error to the JSON
function add_json_error() {
  local error_message="$1"
  json=$(jq --arg error "$error_message" '.errors += [$error]' <<< "$json")
}

# Function to add a message to the JSON
function add_json_message() {
  local message="$1"
  json=$(jq --arg message "$message" '.messages += [$message]' <<< "$json")
}

# Function to set success flag to true/false
function set_json_success() {
  local value="${1:-true}"
  if [ "$value" = "false" ]; then
    json=$(jq '.success = false' <<< "$json")
  else
    json=$(jq '.success = true' <<< "$json")
  fi
}

# Function to add a raw JSON object as a message
function add_actual_json_message() {
  local raw_json="$1"
  # Use --argjson so that the passed parameter is treated as actual JSON
  json=$(jq --argjson message "$raw_json" '.messages += [$message]' <<< "$json")
}

# Function to add a raw JSON object as a error
function add_actual_json_error() {
  local raw_json="$1"
  json=$(jq --argjson error "$raw_json" '.errors += [$error]' <<< "$json")
}

# Function to set result data using a temporary file to avoid argument list too long
function set_json_result() {
  local result_data="$1"
  local tmp_json
  tmp_json=$(mktemp)
  local tmp_result
  tmp_result=$(mktemp)

  # Write current json and result_data to temporary files
  echo "$json" > "$tmp_json"
  echo "$result_data" > "$tmp_result"

  # Use --argfile to pass the large JSON data from file
  json=$(jq --argfile result "$tmp_result" '.result = $result' "$tmp_json")

  rm -f "$tmp_json" "$tmp_result"
}

# Function to print the JSON response
function print_json_response() {
  echo "$json" | jq .
}

# Function to compare nginx version
compare_nginx_version() {
    # Split the version into major, minor, and patch
    local current_version="$1"
    local minimum_version="1.25.1"
    if [[ "$(echo -e "$current_version\n$minimum_version" | sort -V | head -n1)" == "$minimum_version" ]]
    then
        return 0
    else
        return 1
    fi
}

# Function to check the Nginx version and process config files
check_nginx_and_update_http2() {
    # Get Nginx version
    local nginx_version=$(nginx -v 2>&1 | grep -oP "(?<=nginx/)[0-9.]+")
    if [ -z "$nginx_version" ]; then
        return 1
    fi

    # Compare versions
    if compare_nginx_version "$nginx_version"; then
        # Initialize variable to track if changes were made
        local nginxt=false

        # Files to check
        local files=(
            "/usr/local/nginx/conf/conf.d/*.conf"
            "/usr/local/nginx/conf/conf.d/phpmyadmin_ssl.conf"
        )
        for file in ${files[@]}; do
            if [ -f "$file" ]; then
                # Step 2: Check for 'listen' directive with http2 and modify
                if grep -q 'listen .*http2' "$file"; then
                    # Remove all instances of 'http2 on;' first
                    sed -i '/http2[[:space:]]\+on;/d' "$file"
                    
                    # Remove 'http2' from 'listen' directive and add 'http2 on;' after
                    sed -i '/listen .*http2/s/http2//g' "$file"
                    sed -i '/listen .*;/s/ ;/;/g' "$file"
                    sed -i '/listen .*;/a \  http2 on;' "$file"

                    # Set flag to true indicating a change was made
                    nginxt=true
                fi
            fi
        done
        # After the loop, check if any changes were made and reload Nginx if necessary
    fi
}

convert_wpsecure() {
    find /usr/local/nginx/conf/wpincludes/ -type f -name "wpsecure_*.conf" ! -name "*_blacklist.conf" ! -name "*_whitelist.conf" | while read -r file; do
      # Extract the domain name
      domain=$(basename "$file" | sed 's/wpsecure_//; s/\.conf//')

      # Define paths for blacklist and whitelist files
      blacklist_file="/usr/local/nginx/conf/wpincludes/${domain}/wpsecure_${domain}_blacklist.conf"
      whitelist_file="/usr/local/nginx/conf/wpincludes/${domain}/wpsecure_${domain}_whitelist.conf"

      # Find the SSL conf file
      ssl_conf_file="/usr/local/nginx/conf/conf.d/${domain}.ssl.conf"
      
      # Check if SSL conf file exists
      if [[ -f "$ssl_conf_file" ]]; then
        # Extract PHP includes, ignoring commented lines and capturing only the path after "include"
        php_includes=$(grep -h "^\s*include /usr/local/nginx/conf/php" "$ssl_conf_file" | awk '{print $2}' | sed 's/;$//' | sort | uniq -c | sort -rn | head -n 1 | awk '{print $2}')
        
        # If we didn't find any PHP includes, skip this domain
        if [[ -z "$php_includes" ]]; then
          echo "No PHP include found for $domain, skipping..."
          continue
        fi
      else
        echo "SSL conf file for $domain not found, skipping..."
        continue
      fi

      # Replace ${vhostname} and ${phpconf} in the template and write to the original file
      sed -e "s/\${vhostname}/$domain/g" -e "s|\${phpconf}|$php_includes|g" /bigscoots/wpo/nginx/includes/bs_wp_whitelist_v2.conf > "$file"

      # Create empty blacklist and whitelist files with the replaced domain name
      touch "$blacklist_file"
      touch "$whitelist_file"
    done
}

gimme_the_goods_les () 
{ 
    HISTCONTROL=ignorespace;

    # Load check: exit early if load > 2x CPU cores
    CPU_COUNT=$(nproc)
    LOAD_AVG=$(awk '{print $1}' /proc/loadavg)
    LOAD_LIMIT=$(echo "$CPU_COUNT * 2" | bc)

    COMPARE=$(echo "$LOAD_AVG > $LOAD_LIMIT" | bc)
    if [[ "$COMPARE" -gt 1 ]]; then
        echo -e "\033[1;31mSystem load ($LOAD_AVG) is too high to proceed (limit: $LOAD_LIMIT).\033[0m"
        return 1
    fi

    sleep 1;
    history -d $((HISTCMD-1));

    # Display Recent Commands
    echo -e "\n\033[1;36m=== Recent Commands ===\033[0m";
    history | tail -n 10 | awk '{$1=""; print " "$0}';

    # Display Current Users
    echo -e "\n\033[1;36m=== Current Users ===\033[0m";
    if grep --color=auto -q "AlmaLinux release 9" /etc/redhat-release 2> /dev/null; then
        W_CMD="w -i";
    else
        W_CMD="w";
    fi;

    UNKNOWN_USER_DETECTED=0

    $W_CMD | awk 'BEGIN {
        split("38.58.227.48:Zubair 67.202.70.147:Nexus 208.117.38.38:Justin 38.65.255.4:Justin 50.31.114.76:Jay 208.117.38.157:Shibin 208.117.38.23:Dean 198.175.25.80:Zach 208.117.38.24:Prasul 50.31.30.56:Zack 50.31.119.9:Julian 208.100.53.125:Sadiq 208.117.4.65:Michael 208.100.53.146:Khan 208.100.53.138:Gibu 74.121.204.16:JK 50.31.116.25:Ahmad 50.31.99.57:Les 38.65.224.44:Lijith 38.65.227.5:Chris 38.65.224.198:Abdal 38.65.227.37:Asher 38.65.227.35:David 38.65.227.38:Muhammad 38.58.224.253:Pell 38.58.225.37:Aleks 38.58.225.38:Roscoe 38.58.226.2:Pavle", pairs, " ")
        for (i in pairs) {
            split(pairs[i], p, ":")
            ips[p[1]] = p[2]
        }
    }
    $1 == "USER" {
        printf "\033[1m%-10s %-10s %-16s %-20s %s\033[0m\n", "USER", "TTY", "FROM", "NAME", "LOGIN@ IDLE JCPU PCPU WHAT"
        next
    }
    NF > 3 {
        if ($0 ~ /^[[:alnum:]]/) {
            cmd = substr($0, index($0, $4))
            # Extract a valid IPv4 address only (four octets, 0-255)
            match($3, /([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, ip_arr)
            ip = (ip_arr[1] != "") ? ip_arr[1] : "Unknown"
            name = (ip in ips) ? ips[ip] : "Unknown"

            # Check if an unknown user is detected
            if (name == "Unknown") {
                system("echo -e \"\033[1;31m\n🚨 ALERT: Unauthorized Access Detected! 🚨\n\nAn unknown user has connected from " $3 ".\nIf this is unexpected, please investigate immediately.\nBigScoots staff has been notified.\nEnsure you are connecting via a proper tunnel IP.\n\033[0m\"");
                system("UNKNOWN_USER_DETECTED=1")
            }

            printf "%-10s %-10s %-16s %-20s %s\n", $1, $2, $3, name, cmd
        } else {
            print
        }
    }' 2> /dev/null

    # Display Actively Running Tasks
    echo -e "\n\033[1;36m=== Actively Running Tasks ===\033[0m";
    RUNNING_TASKS=$(ps -eo pid,cmd --sort=start_time | grep '[b]ash\|[s]h' | grep '/bigscoots/')

    if [[ -z "$RUNNING_TASKS" ]]; then
        echo "None"
    else
        echo -e "PID   CMD\n$RUNNING_TASKS"
    fi

    # Display final alert if an unknown user was detected
    if [[ "$UNKNOWN_USER_DETECTED" -eq 1 ]]; then
        echo -e "\033[1;31m\n🚨 ALERT: Unauthorized Access Detected! 🚨\nPlease check the logs and take action immediately.\033[0m"
    fi

    # Check if remote database info file exists
    if [[ -f /root/.bigscoots/db/info ]]; then
        echo -e "\n\033[1;36m=== Remote Database Detected ===\033[0m"
        
        # Extract database information
        DBHOST=$(grep '^dbhost:' /root/.bigscoots/db/info | awk '{print $2}')
        DBHOSTPUBLIC=$(grep '^dbhostpublic:' /root/.bigscoots/db/info | awk '{print $2}')

        echo -e "Private Network IP: \033[1;32m$DBHOST\033[0m"
        echo -e "Public Network IP: \033[1;32m$DBHOSTPUBLIC\033[0m"
    fi
}

# nsgrep: A custom grep function that excludes common static file types (images, fonts, videos, audio, compressed files, and documents).
# Usage: nsgrep <pattern> <file>
# This function helps filter out static assets like .jpg, .png, .css, .js, fonts, videos, etc., from your search results.
nsgrep() {
    local search="$1"
    shift
    grep -vE "/[^ ]+\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff|tif|css|scss|js|json|woff|woff2|ttf|eot|otf|svgz|webm|mp4|mp3|wav|ogg|flac|aac|zip|tar|gz|rar|7z|pdf|txt|md|xml)([? ].*)?$" "$@" | grep "$search"
}


clean_nginx_config() {
    local config_file="$1"

    if [[ ! -f "$config_file" ]]; then
        echo "Error: Config file $config_file not found!"
        return 1
    fi

    # Create a timestamped backup
    epoch=$(date +%s)
    config_file_bk="${config_file}.bk.${epoch}"
    cp -rf "$config_file" "$config_file_bk"

    # Remove unwanted cache includes (wpcacheenabler, wpsupercache, rediscache)
    sed -i '/^\s*#\?include \/usr\/local\/nginx\/conf\/wpincludes\/.*\/\(wpcacheenabler\|wpsupercache\|rediscache\)_.*\.conf;/d' "$config_file"

    # Remove ngx_pagespeed includes
    sed -i '/^\s*#\?include \/usr\/local\/nginx\/conf\/pagespeed\(handler\|statslog\)\?\.conf;/d ; /ngx_pagespeed/d' "$config_file"

    # Remove all try_files and comments inside location / block
    awk '
    BEGIN { inside_location = 0 }
    /^  location \/ \{/ { inside_location = 1 }
    inside_location && /^\s*(#|try_files|\s*$)/ { next }
    inside_location && /^\s*}/ { inside_location = 0 }
    { print }
    ' "$config_file" > "${config_file}.tmp"

    mv -f "${config_file}.tmp" "$config_file"

    # Ensure the correct try_files line is added
    sed -i '/^  location \/ {$/a\ \ \ \ try_files $uri $uri/ /index.php?$args;' "$config_file"

    # Remove consecutive blank lines **only inside the location block**
    awk '
    BEGIN { inside_location = 0 }
    /^  location \/ \{/ { inside_location = 1 }
    inside_location && /^\s*$/ { blank++ }
    inside_location && /^\s*}/ { inside_location = 0; blank=0 }
    blank > 1 { next }
    { print }
    ' "$config_file" > "${config_file}.tmp"

    mv -f "${config_file}.tmp" "$config_file"

    # Reload Nginx with validation
    ngxreload_t "\`clean_nginx_config\` nginx conf failed. Backup available: $config_file_bk" --noexit
}

# Function: Count unique visitors per user agent
ua_unique_visitors() {
    local log_files=("$@")
    [[ ${#log_files[@]} -eq 0 ]] && log_files=("access.log")

    {
        for f in "${log_files[@]}"; do
            [[ "$f" == *.gz ]] && zcat --force "$f" || cat "$f"
        done
    } 2>/dev/null | awk '
    {
        ip = $1
        n = split($0, q, /\"/)
        ua = q[n - 1]

        if (ua != "") {
            key = ua "|" ip
            if (!seen[key]++) {
                count[ua]++
            }
        }
    }
    END {
        for (ua in count) {
            printf "%7d  %s\n", count[ua], ua
        }
    }' | sort -rn
}

# Function: Fetch verified bot names from Cloudflare Radar API
fetch_verified_bots() {
    local BOT_LIST_URL="https://www.bigscoots.com/verified-bots.txt"
    local BOT_LIST_FILE="/tmp/verified.bots.raw"
    local BOT_REGEX_LIST_FILE="/tmp/verified.bots"

    curl -s "$BOT_LIST_URL" -o "$BOT_LIST_FILE"
    sed 's/.*/".*&.*"/' "$BOT_LIST_FILE" | tr '[:upper:]' '[:lower:]' > "$BOT_REGEX_LIST_FILE"

    echo "$BOT_REGEX_LIST_FILE"
}

match_verified_bots_to_logs() {
    local token="$1"
    shift
    local log_files=("$@")
    [[ ${#log_files[@]} -eq 0 ]] && log_files=("access.log")

    # Fetch and load bot patterns
    bot_file=$(fetch_verified_bots "$token")
    mapfile -t bots < "$bot_file"

    tmp_ua_visitors="/tmp/ua_visitors.tmp"

    {
        for f in "${log_files[@]}"; do
            [[ "$f" == *.gz ]] && zcat --force "$f" || cat "$f"
        done
    } 2>/dev/null | awk '
    {
        ip = $1
        n = split($0, q, "\"")

        ua = tolower(q[n - 1])
        if (ua != "") {
            key = ua "|" ip
            if (!seen[key]++) {
                count[ua]++
            }
            total[ua]++
        }
    }
    END {
        for (ua in count) {
            gsub(/"/, "", ua)
            print count[ua] "|" total[ua] "|" ua
        }
    }' > "$tmp_ua_visitors"

    # Output headers
    printf "%-10s %-15s %s\n" "UNIQUE" "TOTAL_REQS" "BOT_NAME"
    printf "%-10s %-15s %s\n" "------" "-----------" "---------"

    total_unique=0
    total_requests=0
    output_lines=()

    for bot in "${bots[@]}"; do
        matches=$(grep -i "|.*${bot//\"/}" "$tmp_ua_visitors")
        unique=0
        total=0
        while IFS="|" read -r u t ua; do
            unique=$((unique + u))
            total=$((total + t))
        done <<< "$matches"

        if [ "$total" -gt 0 ]; then
            output_lines+=("$(printf "%-10d %-15d %s" "$unique" "$total" "$bot")")
            total_unique=$((total_unique + unique))
            total_requests=$((total_requests + total))
        fi
    done

    # Print sorted output
    printf "%s\n" "${output_lines[@]}" | sort -k1 -rn

    # Print grand totals
    echo
    echo "========== GRAND TOTAL =========="
    printf "%-10s %-15s\n" "UNIQUE" "TOTAL_REQS"
    printf "%-10s %-15s\n" "------" "-----------"
    printf "%-10d %-15d\n" "$total_unique" "$total_requests"
}

# Function: Detect IPs with a high number of unique UAs (likely spoofers)
detect_ip_useragent_spammers() {
    local log_files=("$@")
    [[ ${#log_files[@]} -eq 0 ]] && log_files=("access.log")

    zcat -f "${log_files[@]}" 2>/dev/null | awk -F\" '
    {
        split($1, parts, " ")
        ip = parts[1]
        ua = $6
        ip_ua[ip][ua] = 1
    }
    END {
        total_spoofed_ua_count = 0
        total_bot_ips = 0

        for (ip in ip_ua) {
            n = asorti(ip_ua[ip], tmp)
            if (n > 10) {
                print "BOT_IP", ip, "had", n, "unique user agents – treat as 1 visit or exclude"
                total_spoofed_ua_count += n
                total_bot_ips++
            }
        }

        print "\nTotal spoofed UA count: ", total_spoofed_ua_count
        print "Total BOT_IPs detected: ", total_bot_ips
    }'
}

function deploy_opcache_gui {
  # Generate a UUID for filename
  UUID=$(uuidgen)
  FILE_NAME="${UUID}.php"

  # Download the file
  wget -q -O "${FILE_NAME}" https://raw.githubusercontent.com/amnuts/opcache-gui/refs/heads/master/index.php
  if [[ ! -f "${FILE_NAME}" ]]; then
    echo "Failed to download opcache-gui."
    return 1
  fi

  # Get site URL
  SITE_URL=$(wp option get siteurl 2>/dev/null)
  if [[ -z "${SITE_URL}" ]]; then
    echo "Could not determine site URL via wp-cli."
    return 1
  fi

  # Print access URL
  echo "Opcache GUI available at: ${SITE_URL}/${FILE_NAME}"

  # Background removal task using screen
  screen -dmS "remove_opcache_${UUID}" bash -c "sleep 86400 && rm -f \"$(pwd)/${FILE_NAME}\""
}

function reinstall_all_plugins {
  if [[ "$1" == "--all-domains" ]]; then
    echo "Reinstalling all plugins across all domains (10 at a time)..."
    find_wp_installs | xargs -P10 -I{} bash -c '
      WP="$0"
      source /bigscoots/includes/common.sh
      echo "wpcli core is-installed --path=$WP"
      if wpcli core is-installed --path="$WP" >/dev/null 2>&1; then
        echo "[INFO] Processing $WP"
        wpcli plugin list --field=name --path="$WP" | while read -r P; do
          echo "[INFO] Reinstalling $P in $WP"
          wpcli plugin install "$P" --force --path="$WP"
        done
      else
        echo "[WARN] Skipping $WP (not a valid WP install)"
      fi
    ' {}
  else
    source /bigscoots/includes/common.sh
    if ! wpcli core is-installed >/dev/null 2>&1; then
      echo "[ERROR] This is not a valid WordPress install (wp core is-installed failed)."
      return 1
    fi

    echo "Reinstalling plugins in current WordPress install..."
    wpcli plugin list --field=name | while read -r P; do
      echo "[INFO] Reinstalling $P"
      wpcli plugin install "$P" --force
    done
  fi
}

check_domains_lxd() {
  local DOMAINS_ROOT="/home/nginx/domains"
  local OUTPUT_JSON=0

  if [[ "${1:-}" == "--json" ]]; then
    OUTPUT_JSON=1
    shift
  fi
  export OUTPUT_JSON

  # Get current server's last octet once
  local CURRENT
  CURRENT=$(curl -s --connect-timeout 5 --max-time 10 ipinfo.io/ip 2>/dev/null | awk -F'.' '{print $4}')
  [[ -z "$CURRENT" ]] && CURRENT="unknown"
  export CURRENT

  _check_one_domain() {
    local d="$1"
    local DOMAIN PAGE EXPECTED FINAL_URL REDIRECT_DOMAIN
    local REDIRECT_CHECK ROOT_STATUS ROOT_CHECK WPADMIN_STATUS WPADMIN_CHECK
    local CACHE_BUSTER

    local -a CURL_OPTS=(-sL --connect-timeout 5 --max-time 10)
    local -a CURL_OPTS_HEAD=(-sLI --connect-timeout 5 --max-time 10)

    DOMAIN=$(basename "$d")

    # Generate random string locally
    CACHE_BUSTER=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c4)

    # Use it in the URL
    PAGE=$(curl "${CURL_OPTS[@]}" "https://$DOMAIN/bigscoots.php?nocache=$CACHE_BUSTER" 2>/dev/null)
    
    EXPECTED=$(echo "$PAGE" | grep -oP '(?<=hostname-footer">)[0-9]+-\K[0-9]+(?=</div>)' 2>/dev/null)

    FINAL_URL=$(curl "${CURL_OPTS_HEAD[@]}" -o /dev/null -w "%{url_effective}" "https://$DOMAIN" 2>/dev/null)
    REDIRECT_DOMAIN=$(echo "$FINAL_URL" | awk -F/ '{print $3}')

    local is_redirect=0
    if [[ "$REDIRECT_DOMAIN" != "$DOMAIN" && "$REDIRECT_DOMAIN" != "www.$DOMAIN" && "$REDIRECT_DOMAIN" != "" ]]; then
      is_redirect=1
      REDIRECT_CHECK="Redirect: $REDIRECT_DOMAIN"
    else
      ROOT_STATUS=$(curl "${CURL_OPTS[@]}" -o /dev/null -w "%{http_code}" "https://$DOMAIN" 2>/dev/null)
      [[ "$ROOT_STATUS" =~ ^(200|301)$ ]] && ROOT_CHECK="βœ“" || ROOT_CHECK="βœ—($ROOT_STATUS)"

      WPADMIN_STATUS=$(curl "${CURL_OPTS[@]}" -o /dev/null -w "%{http_code}" "https://$DOMAIN/wp-admin" 2>/dev/null)
      [[ "$WPADMIN_STATUS" =~ ^(200|301|302)$ ]] && WPADMIN_CHECK="βœ“" || WPADMIN_CHECK="βœ—($WPADMIN_STATUS)"

      REDIRECT_CHECK="Frontend: $ROOT_CHECK WP-Admin: $WPADMIN_CHECK"
    fi

    if [[ "$OUTPUT_JSON" -eq 1 ]]; then
      # Booleans / status
      local status matched frontend_ok wpadmin_ok

      matched=false
      frontend_ok=false
      wpadmin_ok=false

      if [[ -z "$EXPECTED" ]]; then
        status="no_bigscoots"
      elif [[ "$CURRENT" == "unknown" ]]; then
        status="current_unknown"
      elif [[ "$EXPECTED" == "$CURRENT" ]]; then
        status="ok"
        matched=true
      else
        status="mismatch"
      fi

      if [[ "$is_redirect" -eq 0 ]]; then
        [[ "${ROOT_STATUS:-}" =~ ^(200|301)$ ]] && frontend_ok=true
        [[ "${WPADMIN_STATUS:-}" =~ ^(200|301|302)$ ]] && wpadmin_ok=true
      fi

      # JSON-safe values (domains are hostnames, so safe to embed)
      local expected_json current_json redirect_json root_json wpadmin_json

      [[ -n "$EXPECTED" ]] && expected_json="\"$EXPECTED\"" || expected_json="null"
      [[ "$CURRENT" != "unknown" ]] && current_json="\"$CURRENT\"" || current_json="null"
      [[ "$is_redirect" -eq 1 ]] && redirect_json="\"$REDIRECT_DOMAIN\"" || redirect_json="null"
      [[ -n "${ROOT_STATUS:-}" ]] && root_json="\"$ROOT_STATUS\"" || root_json="null"
      [[ -n "${WPADMIN_STATUS:-}" ]] && wpadmin_json="\"$WPADMIN_STATUS\"" || wpadmin_json="null"

      printf '{"domain":"%s","status":"%s","current_octet":%s,"expected_octet":%s,"redirect_domain":%s,"root_status":%s,"wpadmin_status":%s,"matched":%s,"frontend_ok":%s,"wpadmin_ok":%s}\n' \
        "$DOMAIN" "$status" \
        "$current_json" "$expected_json" "$redirect_json" \
        "$root_json" "$wpadmin_json" \
        "$matched" "$frontend_ok" "$wpadmin_ok"
      return 0
    fi

    # Human output (original style)
    if [[ -n "$EXPECTED" && "$CURRENT" != "unknown" ]]; then
      if [[ "$EXPECTED" = "$CURRENT" ]]; then
        echo "βœ“ $DOMAIN (.$CURRENT) | $REDIRECT_CHECK"
      else
        echo "βœ— $DOMAIN (on .$EXPECTED, this is .$CURRENT) | $REDIRECT_CHECK"
      fi
    elif [[ -n "$EXPECTED" && "$CURRENT" == "unknown" ]]; then
      echo "? $DOMAIN (expected .$EXPECTED, current IP unknown) | $REDIRECT_CHECK"
    else
      echo "? $DOMAIN (no bigscoots.php) | $REDIRECT_CHECK"
    fi
  }

  export -f _check_one_domain

  if [[ "$OUTPUT_JSON" -eq 1 ]]; then
    # Collect one-JSON-object-per-line from workers, then wrap into an array.
    local objs
    objs=$(
      printf '%s\n' "$DOMAINS_ROOT"/*/ 2>/dev/null \
        | xargs -P3 -n1 bash -c '_check_one_domain "$1"' _
    )

    # Join lines with commas into a valid JSON array
    echo "$objs" | awk '
      BEGIN { print "[" }
      NF {
        if (n++ > 0) printf(",")
        printf("%s", $0)
      }
      END { print "]" }
    '
  else
    printf '%s\n' "$DOMAINS_ROOT"/*/ 2>/dev/null \
      | xargs -P3 -n1 bash -c '_check_one_domain "$1"' _
  fi
}

update_nginx_drop_conf_robotstxt() {
    local target="/usr/local/nginx/conf/drop.conf"

    # Search for the simple one-line version
    if grep -q "location = /robots.txt.*access_log off" "$target"; then
        
        # Replace matches with the multi-line block
        # We use single quotes around the sed command to prevent $uri and $args from breaking
        sed -i 's|.*location = /robots.txt.*{.*access_log off.*}|location = /robots.txt {\n    allow all;\n    try_files $uri /index.php?$args;\n    log_not_found off;\n    access_log off;\n}|' "$target"
        
        echo "Success: Replaced robots.txt rule."
    else
        echo "Skipping: Target robots.txt line not found (or already updated)."
    fi
}

# In common.sh
assert_lxd_plan() {
    local api_test
    api_test=$(curl -sf --unix-socket /dev/lxd/sock http://lxd/1.0 2>/dev/null || echo "")
    if [[ -z "$api_test" ]]; then
        echo "[${serverip:-UNKNOWN}] ERROR: LXD guest API not accessible - lxcfs may be broken. Aborting." >&2
        exit 1
    fi

    LXD_PLAN=$(curl -sf --unix-socket /dev/lxd/sock http://lxd/1.0/config/user.plan 2>/dev/null | tr -d '"' || echo "")
    if [[ -z "$LXD_PLAN" ]]; then
        echo "[${serverip:-UNKNOWN}] ERROR: user.plan not set on this container. Run --backfill-plans first." >&2
        exit 1
    fi

    export LXD_PLAN
}