#!/bin/bash
# Emergency mitigation for Remi SCL PHP-FPM and system PHP-FPM on ISPConfig hosts.
# Run as root. This is NOT a substitute for installing patched PHP packages.
set -euo pipefail

if [ "$(id -u)" -ne 0 ]; then echo 'ERROR: Run as root.' >&2; exit 1; fi
for c in awk sed cp mkdir mktemp systemctl grep nginx sort; do
  command -v "$c" >/dev/null 2>&1 || { echo "ERROR: Missing $c" >&2; exit 1; }
done

MODE="${1:---apply}"
if [[ "$MODE" != '--apply' && "$MODE" != '--check' ]]; then
  echo "Usage: $0 [--check|--apply]"; exit 2
fi

STAMP=$(date +%Y%m%d-%H%M%S)-$$
BACKUP="/root/php2shell-backup-$STAMP"
LOG="/root/php2shell-audit-$STAMP.log"
declare -a SERVICES=() INIS=() DIRS=() FPMS=() PHP85=() CHANGED=() NGINX_FILES=()
NGINX_TMP=""

# Avoid touching unrelated paths: each active FPM systemd service has its known layout.
for unit in php-fpm.service /etc/opt/remi/php[0-9][0-9]; do
  if [[ "$unit" == 'php-fpm.service' ]]; then
    service='php-fpm.service'; ini='/etc/php.ini'; dir='/etc/php-fpm.d'; binary='/usr/sbin/php-fpm'; ver=system
  else
    [[ -d "$unit" ]] || continue
    ver=${unit##*/}
    service="${ver}-php-fpm.service"
    ini="/etc/opt/remi/$ver/php.ini"
    dir="/etc/opt/remi/$ver/php-fpm.d"
    binary="/opt/remi/$ver/root/usr/sbin/php-fpm"
  fi
  if ! systemctl is-active --quiet "$service"; then continue; fi
  if [[ ! -f "$ini" || ! -d "$dir" || ! -x "$binary" ]]; then
    echo "ERROR: $service active but config or FPM binary missing" >&2; exit 1
  fi
  SERVICES+=("$service"); INIS+=("$ini"); DIRS+=("$dir"); FPMS+=("$binary")
  if [[ "$ver" == php85 ]]; then PHP85+=(1); else PHP85+=(0); fi
done

if (( ${#SERVICES[@]} == 0 )); then echo 'ERROR: No active PHP-FPM services found.' >&2; exit 1; fi

# Back up only files being modified; exact paths preserved below backup root.
backup() {
  local path="$1"
  mkdir -p "$BACKUP$(dirname "$path")"
  cp -p "$path" "$BACKUP$path"
  CHANGED+=("$path")
}
restore() {
  local path
  echo 'Restoring backed-up files...'
  for path in "${CHANGED[@]}"; do cp -p "$BACKUP$path" "$path"; done
}
# Cleanup temporary Nginx dump even if the script fails.
cleanup_nginx_dump() { [[ -z "$NGINX_TMP" ]] || rm -f "$NGINX_TMP"; }
trap cleanup_nginx_dump EXIT

# Restore changed files if an unexpected error occurs before completion.
FAILED=0
on_error() {
  local rc=$?
  trap - ERR
  if (( ${#CHANGED[@]} > 0 )) && (( FAILED == 0 )); then
    FAILED=1
    restore || true
  fi
  echo "ERROR: Aborted with status $rc; backup at $BACKUP" >&2
  exit "$rc"
}
trap on_error ERR

# Files are written only after awk completes successfully, preserving original permissions.
write_ini() {
  local path="$1" is85="$2" tmp
  tmp=$(mktemp)
  if ! awk '
    /^; BEGIN PHP2SHELL-MITIGATION$/ {managed=1; next}
    /^; END PHP2SHELL-MITIGATION$/ {managed=0; next}
    managed {next}
    /^[[:space:]]*(memory_limit|session\.upload_progress\.enabled|fatal_error_backtraces)[[:space:]]*=/ {next}
    {print}
    END {if (managed) exit 2}
  ' "$path" > "$tmp"; then
    rm -f "$tmp"; echo "Malformed managed block in $path" >&2; return 1
  fi
  {
    cat "$tmp"
    echo '; BEGIN PHP2SHELL-MITIGATION'
    echo 'memory_limit = -1'
    echo 'session.upload_progress.enabled = 0'
    if [[ "$is85" == 1 ]]; then echo 'fatal_error_backtraces = 0'; fi
    echo '; END PHP2SHELL-MITIGATION'
  } > "${tmp}.new"
  backup "$path"
  cat "${tmp}.new" > "$path"
  rm -f "$tmp" "${tmp}.new"
}

# In each pool section, remove old managed blocks and conflicting active PHP directives.
# Inject php_admin_value directives immediately after the section header.
# Works with multiple pools in a single file; ignores files with no pool sections.
write_pool() {
  local path="$1" is85="$2" tmp
  tmp=$(mktemp)
  if ! awk -v is85="$is85" '
    /^; BEGIN PHP2SHELL-MITIGATION$/ {managed=1; next}
    /^; END PHP2SHELL-MITIGATION$/ {managed=0; next}
    managed {next}
    /^[[:space:]]*php_(admin_)?(value|flag)\[(memory_limit|session\.upload_progress\.enabled|fatal_error_backtraces)\][[:space:]]*=/ {next}
    {
      print
      if ($0 ~ /^[[:space:]]*\[[^]]+\][[:space:]]*([;#].*)?$/ && $0 !~ /^[[:space:]]*\[global\]/) {
        print "; BEGIN PHP2SHELL-MITIGATION"
        print "php_admin_value[memory_limit] = -1"
        print "php_admin_value[session.upload_progress.enabled] = 0"
        if (is85==1) print "php_admin_value[fatal_error_backtraces] = 0"
        print "; END PHP2SHELL-MITIGATION"
      }
    }
    END {if (managed) exit 2}
  ' "$path" > "$tmp"; then
    rm -f "$tmp"; echo "Malformed managed block in $path" >&2; return 1
  fi
  backup "$path"
  cat "$tmp" > "$path"
  rm -f "$tmp"
}

# Enumerate active Nginx configuration files rather than editing unused backups.
# nginx -T is checked first, and its dump stays in a private temporary file.
discover_nginx_files() {
  NGINX_TMP=$(mktemp /tmp/php2shell-nginx.XXXXXXXX)
  chmod 600 "$NGINX_TMP"
  if ! nginx -T > "$NGINX_TMP" 2>/dev/null; then
    echo 'ERROR: nginx -T failed before modifications. Fix Nginx configuration first.' >&2
    return 1
  fi
  while IFS= read -r file; do
    [[ -f "$file" ]] || continue
    NGINX_FILES+=("$file")
  done < <(awk '/^# configuration file \/.*:$/ {p=$0; sub(/^# configuration file /,"",p); sub(/:$/,"",p); print p}' "$NGINX_TMP" | sort -u)
}

# Transform only FastCGI PHP_VALUE/PHP_ADMIN_VALUE/PHP_FLAG directives.
# Preserve max_execution_time, timeout directives, and every other setting.
# One directive may span multiple physical lines; handle until its closing ';'.
# Indirect values ($php_value etc.) are deliberately not rewritten.
write_nginx() {
  local path="$1" tmp
  tmp=$(mktemp)
  if ! awk '
    function patch(s,    k,old,newval) {
      if (s !~ /fastcgi_param[[:space:]]+PHP_(ADMIN_VALUE|VALUE|FLAG)[[:space:]]+/) return s
      # Replace finite (or other) memory settings wherever present.
      gsub(/memory_limit[[:space:]]*=[[:space:]]*[^[:space:]\\";]+/, "memory_limit=-1", s)
      gsub(/session\.upload_progress\.enabled[[:space:]]*=[[:space:]]*[^[:space:]\\";]+/, "session.upload_progress.enabled=0", s)
      gsub(/fatal_error_backtraces[[:space:]]*=[[:space:]]*[^[:space:]\\";]+/, "fatal_error_backtraces=0", s)
      return s
    }
    function finish() {
      if (pending) { printf "%s", patch(buf); buf=""; pending=0 }
    }
    {
      if (!pending && $0 ~ /^[[:space:]]*fastcgi_param[[:space:]]+PHP_(ADMIN_VALUE|VALUE|FLAG)[[:space:]]+/) {
        pending=1; buf=$0 "\n"
        if ($0 ~ /;[[:space:]]*(#.*)?$/) finish()
      } else if (pending) {
        buf=buf $0 "\n"
        if ($0 ~ /;[[:space:]]*(#.*)?$/) finish()
      } else print
    }
    END {if(pending) {printf "%s",buf; exit 2}}
  ' "$path" > "$tmp"; then
    rm -f "$tmp"; echo "Could not safely parse FastCGI entry in $path" >&2; return 1
  fi
  if ! cmp -s "$tmp" "$path"; then
    echo "Nginx: $path"
    backup "$path"
    cat "$tmp" > "$path"
  fi
  rm -f "$tmp"
}

show_checks() {
  local i f count=0
  echo '=== Active PHP-FPM services and configured values ==='
  for ((i=0; i<${#SERVICES[@]}; i++)); do
    echo "--- ${SERVICES[i]} ---"
    grep -nE '^[[:space:]]*(memory_limit|session\.upload_progress\.enabled|fatal_error_backtraces)[[:space:]]*=' "${INIS[i]}" || true
    for f in "${DIRS[i]}"/*.conf; do
      [[ -f "$f" ]] || continue
      grep -nEH '^[[:space:]]*php_(admin_)?(value|flag)\[(memory_limit|session\.upload_progress\.enabled|fatal_error_backtraces)\][[:space:]]*=' "$f" || true
    done
  done
  echo '=== Active Nginx FastCGI overrides (original source files) ==='
  local nf
  for nf in "${NGINX_FILES[@]}"; do
    grep -nE 'fastcgi_param[[:space:]]+PHP_(ADMIN_VALUE|VALUE|FLAG)' "$nf" | sed "s|^|$nf:|" || true
  done
  echo 'NOTE: These are file checks, not a web-request runtime verification.'
}

discover_nginx_files
if [[ "$MODE" == '--check' ]]; then show_checks; exit 0; fi

mkdir -m 700 "$BACKUP"
echo "Backup: $BACKUP"
for ((i=0; i<${#SERVICES[@]}; i++)); do
  echo "Applying ${SERVICES[i]}"
  write_ini "${INIS[i]}" "${PHP85[i]}"
  for f in "${DIRS[i]}"/*.conf; do
    [[ -f "$f" ]] || continue
    # Only true pool files. Match section [name] but not [global].
    if awk '/^[[:space:]]*\[[^]]+\][[:space:]]*$/ && $0 !~ /^[[:space:]]*\[global\]/ {found=1} END {exit !found}' "$f"; then
      write_pool "$f" "${PHP85[i]}"
    fi
  done
done

echo 'Patching active Nginx FastCGI overrides...'
for f in "${NGINX_FILES[@]}"; do
  if grep -qE 'fastcgi_param[[:space:]]+PHP_(ADMIN_VALUE|VALUE|FLAG)' "$f"; then
    write_nginx "$f"
  fi
done

# Detect indirect FastCGI PHP configuration. These must be audited manually.
if grep -Eq 'fastcgi_param[[:space:]]+PHP_(ADMIN_VALUE|VALUE|FLAG)[[:space:]]+\$' "$NGINX_TMP"; then
  echo 'WARNING: Variable-based FastCGI PHP parameters detected (for example $php_value).' >&2
  echo 'Review variable definitions and nginx -T output manually; indirect values were not modified.' >&2
fi

echo 'Validating Nginx and every FPM configuration before restarting...' 
if ! nginx -t; then
  echo 'FAILED: nginx -t' >&2
  restore; FAILED=1; exit 1
fi
for ((i=0; i<${#SERVICES[@]}; i++)); do
  if ! "${FPMS[i]}" -t; then
    echo "FAILED: ${SERVICES[i]}" >&2
    restore
    FAILED=1
    echo 'No services restarted.' >&2
    exit 1
  fi
done

echo 'Reloading Nginx...'
if ! systemctl reload nginx; then
  echo 'Nginx reload failed; restoring files and attempting recovery.' >&2
  restore; FAILED=1
  systemctl reload nginx || true
  exit 1
fi

echo 'Restarting active services...'
for ((i=0; i<${#SERVICES[@]}; i++)); do
  if ! systemctl restart "${SERVICES[i]}"; then
    echo "Restart failed: ${SERVICES[i]}; restoring config and attempting recovery" >&2
    restore
    FAILED=1
    systemctl reload nginx || true
    for s in "${SERVICES[@]}"; do systemctl restart "$s" || true; done
    echo "ERROR: Manual service verification is required" >&2
    exit 1
  fi
done

echo '=== Service status ==='
for s in "${SERVICES[@]}"; do
  if systemctl is-active --quiet "$s"; then echo "OK $s"; else echo "FAILED $s"; fi
done
show_checks > "$LOG"
echo "Completed. Backups: $BACKUP"
echo "Audit: $LOG"
echo 'WARNING: ISPConfig can regenerate web pool files; patched PHP updates are still required.'
