#!/bin/bash
# alpha-phpver — install / set-default / remove PHP runtimes from the panel.
# Root-owned; called by www-data via NOPASSWD sudoers. Version strictly
# validated; the panel's own runtime, the CLI default and nginx-referenced
# versions are protected from removal. apt work runs detached (logged) so the
# panel request never hangs on a package download.
#   install <ver>              apt install php<ver>-fpm + sane extension set (bg)
#   default <ver>              update-alternatives: /usr/bin/php -> php<ver>
#   remove  <ver> [panelver]   apt purge php<ver>-* (bg; refuses in-use versions)
#     panelver = the PHP version currently serving the panel (passed by the panel
#     itself, from its live PHP_VERSION — the authoritative "do not remove me").
# NB: no `pipefail` — several checks pipe a producer into `grep -q`/`head`, which
# close the pipe early and SIGPIPE the producer; with pipefail that made the pipe
# report failure even on a match (a real bug: apt-cache|grep -q wrongly returned
# NO_CANDIDATE). We care about the filter's result, not the producer's SIGPIPE.
set -eu
ACTION="${1:-}"; VER="${2:-}"; PANEL_HINT="${3:-}"
LOG=/var/log/alpha-phpver.log

[[ "$VER" =~ ^[0-9]+\.[0-9]+$ ]] || { echo "BAD_VER: $VER"; exit 2; }
[[ "$PANEL_HINT" =~ ^[0-9]+\.[0-9]+$ ]] || PANEL_HINT=""

# PHP version serving the panel (from the vhost that roots /var/www/alpha).
# NB sites-enabled holds SYMLINKS — must dereference (readlink -f) before grep,
# or `grep -r` skips them and the guard fails open. This is why an earlier
# version nearly purged the panel's own runtime.
panel_ver() {
  local f real
  for f in /etc/nginx/sites-enabled/* /etc/nginx/sites-available/*; do
    [ -e "$f" ] || continue
    real=$(readlink -f "$f" 2>/dev/null) || continue
    grep -qs 'root[[:space:]]\+/var/www/alpha' "$real" || continue
    grep -hoE 'php[0-9]+\.[0-9]+-fpm' "$real" 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+' && return 0
  done
  return 0
}
cli_ver() { readlink -f /usr/bin/php 2>/dev/null | grep -oE '[0-9]+\.[0-9]+$' || true; }

# core packages the runtime always needs (install must succeed with these)
CORE="fpm cli common"
# optional extensions — added only if each has an apt candidate (some are missing
# for bleeding-edge versions). NEVER php<ver>-psr (native PSR breaks Monolog/Laravel).
OPT_EXTS="mysql pgsql sqlite3 xml curl mbstring zip gd intl opcache readline bcmath soap imap"

case "$ACTION" in
  install)
    [ -d "/etc/php/$VER" ] && { echo "ALREADY: PHP $VER is already installed"; exit 0; }
    # SIGPIPE-proof availability check: capture candidate, then test (no grep -q pipe)
    CAND=$(apt-cache policy "php${VER}-fpm" 2>/dev/null | awk '/Candidate:/{print $2; exit}')
    { [ -n "$CAND" ] && [ "$CAND" != "(none)" ]; } \
      || { echo "NO_CANDIDATE: php${VER}-fpm is not available from apt on this system"; exit 3; }
    CORE_PKGS=""; for e in $CORE; do CORE_PKGS="$CORE_PKGS php${VER}-${e}"; done
    setsid bash -c "{
      echo \"== \$(date -Is) install PHP ${VER}\"
      # refresh indexes first — a stale cache lists EOL versions whose .deb files
      # are already gone from the pool (404), which would fail the install
      DEBIAN_FRONTEND=noninteractive apt-get update -q || true
      # core runtime (must succeed)
      DEBIAN_FRONTEND=noninteractive apt-get install -y -q $CORE_PKGS
      RC=\$?
      if [ \"\$RC\" = 0 ]; then
        # add each optional extension that actually has a candidate (skip missing)
        ADD=\"\"
        for e in $OPT_EXTS; do
          c=\$(apt-cache policy \"php${VER}-\$e\" 2>/dev/null | awk '/Candidate:/{print \$2; exit}')
          [ -n \"\$c\" ] && [ \"\$c\" != \"(none)\" ] && ADD=\"\$ADD php${VER}-\$e\"
        done
        [ -n \"\$ADD\" ] && DEBIAN_FRONTEND=noninteractive apt-get install -y -q \$ADD || true
        systemctl enable --now php${VER}-fpm
      fi
      echo \"== install PHP ${VER} done rc=\$RC\"
    } >> $LOG 2>&1" </dev/null &
    echo "STARTED: installing PHP ${VER} in the background (~1 min); refresh this page to see it" ;;
  default)
    [ -x "/usr/bin/php${VER}" ] || { echo "NOT_INSTALLED: php${VER} has no CLI binary"; exit 4; }
    update-alternatives --set php "/usr/bin/php${VER}" >/dev/null
    update-alternatives --set phar "/usr/bin/phar${VER}" >/dev/null 2>&1 || true
    update-alternatives --set phar.phar "/usr/bin/phar.phar${VER}" >/dev/null 2>&1 || true
    echo "OK: system default (CLI) is now PHP ${VER}" ;;
  remove)
    [ -d "/etc/php/$VER" ] || { echo "NOT_INSTALLED: PHP $VER is not installed"; exit 4; }
    # 1) the panel's own live version (authoritative — passed by the panel)
    [ -n "$PANEL_HINT" ] && [ "$VER" = "$PANEL_HINT" ] && { echo "IN_USE: PHP $VER serves this panel — cannot remove"; exit 5; }
    # 2) independent nginx detection of the panel version
    PV=$(panel_ver)
    [ "$VER" = "${PV:-__none__}" ] && { echo "IN_USE: PHP $VER serves this panel — cannot remove"; exit 5; }
    # 3) CLI default
    [ "$VER" = "$(cli_ver)" ] && { echo "IS_DEFAULT: PHP $VER is the CLI default — set another default first"; exit 5; }
    # 4) any nginx vhost referencing this FPM (dereference symlinks — grep -r skips them)
    for f in /etc/nginx/sites-enabled/* /etc/nginx/sites-available/*; do
      [ -e "$f" ] || continue
      real=$(readlink -f "$f" 2>/dev/null) || continue
      if grep -qsE "php${VER//./\\.}-fpm" "$real"; then
        echo "IN_USE: nginx vhost $(basename "$f") still references php${VER}-fpm — switch those sites first"; exit 5
      fi
    done
    # orphaned config dir (no dpkg packages, no FPM binary) → just clean it up,
    # synchronously and instantly. This is what a leftover /etc/php/<ver> from an
    # old PPA is; there is nothing to purge.
    if [ ! -x "/usr/sbin/php-fpm${VER}" ] && [ "$(dpkg -l 2>/dev/null | grep -c "^ii  php${VER}-")" = "0" ]; then
      rm -rf "/etc/php/${VER}"
      echo "OK: removed leftover PHP ${VER} config (no packages were installed)"
      exit 0
    fi
    setsid bash -c "{
      echo \"== \$(date -Is) remove PHP ${VER}\"
      systemctl stop php${VER}-fpm 2>/dev/null || true
      systemctl disable php${VER}-fpm 2>/dev/null || true
      DEBIAN_FRONTEND=noninteractive apt-get purge -y -q \"php${VER}-*\"
      RC=\$?
      [ \"\$RC\" = 0 ] && rm -rf \"/etc/php/${VER}\"   # only drop the config dir if the purge succeeded
      echo \"== remove PHP ${VER} done rc=\$RC\"
    } >> $LOG 2>&1" </dev/null &
    echo "STARTED: removing PHP ${VER} in the background; refresh this page shortly" ;;
  *) echo "BAD_ACTION: $ACTION"; exit 1 ;;
esac
