#!/bin/bash
# EMR 节点 PHP 清理脚本
# 适用漏洞：PHP 远程代码执行（php2shell），CVE-2026-109618 / CVE-2026-109619
#
# 作用：
#   1. 卸载 ganglia-web 与所有 php* RPM（含 mod_php：libphp5.so、conf.d/php.conf、conf.modules.d/10-php.conf）
#   2. 清理镜像中源码编译安装的 PHP（默认 /usr/local/bin/php*）
#   3. 重建被删的 DocumentRoot 并做 httpd 配置校验，避免 reload 失败导致 httpd 无法再启动
#   4. 卸载前后做依赖预检与结果校验，失败即退出
#
# 保留：httpd、ganglia、ganglia-gmond、ganglia-gmetad、ganglia-devel 包
# 幂等：可重复执行
#
# 用法：
#   bash clean_php_ganglia.sh --dry-run        # 只打印将要执行的动作，不改动系统
#   bash clean_php_ganglia.sh                  # 执行清理
#
# 退出码：0 成功；1 前置检查或卸载失败（节点保持可用）；2 httpd 配置校验/reload 失败

set -u
export LC_ALL=C

DRY_RUN=0
for arg in "$@"; do
  case "$arg" in
    --dry-run) DRY_RUN=1 ;;
    -h|--help) sed -n '2,18p' "$0"; exit 0 ;;
    *) echo "未知参数: $arg" >&2; exit 1 ;;
  esac
done

log() {
  printf '%s %s\n' "$(date '+%F %T')" "$*"
}

die() {
  log "ERROR: $*"
  exit 1
}

# 动作类日志：dry-run 时自动加前缀，避免看起来像真的执行了
log_action() {
  if [ "$DRY_RUN" -eq 1 ]; then
    log "[dry-run] $*"
  else
    log "$*"
  fi
}

run() {
  if [ "$DRY_RUN" -eq 1 ]; then
    log "[dry-run] $*"
    return 0
  fi
  "$@"
}

in_removal_set() {
  local name="$1" item
  for item in "${REMOVAL_NAMES[@]}"; do
    [ "$item" = "$name" ] && return 0
  done
  return 1
}

package_installed() {
  rpm -q "$1" >/dev/null 2>&1
}

# ---------- 1. 生成卸载集合 ----------
REMOVAL_NAMES=(ganglia-web)
while IFS= read -r name; do
  [ -n "$name" ] || continue
  REMOVAL_NAMES+=("$name")
done < <(rpm -qa --qf '%{NAME}\n' 'php*' 2>/dev/null | sort -u)

log "计划卸载: ${REMOVAL_NAMES[*]}"

# ---------- 2. 依赖预检（不通过则不做任何改动，连备份也不写）----------
for pkg in "${REMOVAL_NAMES[@]}"; do
  package_installed "$pkg" || continue
  while IFS= read -r line; do
    case "$line" in
      ""|"no package requires"*) continue ;;
    esac
    req_name="$(rpm -q --qf '%{NAME}\n' "$line" 2>/dev/null || true)"
    [ -n "$req_name" ] || die "无法解析 $pkg 的依赖方: $line"
    if ! in_removal_set "$req_name"; then
      die "$pkg 仍被 $req_name 依赖，未做任何卸载"
    fi
  done < <(rpm -q --whatrequires "$pkg" 2>/dev/null || true)
done

TO_REMOVE=()
for pkg in "${REMOVAL_NAMES[@]}"; do
  package_installed "$pkg" && TO_REMOVE+=("$pkg")
done

# ---------- 3. 备份（按天复用；仅在确实有包要卸载时才备份）----------
# 备份内容仅限配置（/etc/httpd、/etc/ganglia、包清单），不含 RPM 包、
# libphp5.so、/usr/local 下的 PHP 二进制与 /usr/share/ganglia 页面文件
BACKUP_DIR="/root/php-clean-backup-$(date +%Y%m%d)"
BACKED_UP=0

if [ "${#TO_REMOVE[@]}" -eq 0 ]; then
  log "节点上已无 ganglia-web/php 包，跳过备份"
elif [ "$DRY_RUN" -eq 1 ]; then
  log "[dry-run] 备份 /etc/httpd、/etc/ganglia、包清单到 $BACKUP_DIR（仅配置，不含 PHP 二进制）"
elif [ -d "$BACKUP_DIR" ]; then
  log "备份目录已存在（今日已备份过），复用: $BACKUP_DIR"
  BACKED_UP=1
else
  mkdir -p "$BACKUP_DIR" || die "无法创建备份目录 $BACKUP_DIR"
  cp -a /etc/httpd "$BACKUP_DIR/httpd" 2>/dev/null || true
  cp -a /etc/ganglia "$BACKUP_DIR/ganglia" 2>/dev/null || true
  rpm -qa 'php*' ganglia-web >"$BACKUP_DIR/pkgs.txt" 2>/dev/null || true
  BACKED_UP=1
  log "已备份配置与包清单到 $BACKUP_DIR（仅配置，不含 PHP 二进制）"
fi

# ---------- 4. 卸载 RPM ----------
if [ "${#TO_REMOVE[@]}" -gt 0 ]; then
  if [ "$DRY_RUN" -eq 1 ]; then
    log "[dry-run] yum remove -y ${TO_REMOVE[*]}"
  else
    log "yum remove -y ${TO_REMOVE[*]}"
    yum remove -y "${TO_REMOVE[@]}" || die "yum remove 失败"
  fi
else
  log "未安装 ganglia-web 或 php RPM，跳过 yum"
fi

if [ "$DRY_RUN" -eq 0 ]; then
  for pkg in "${REMOVAL_NAMES[@]}"; do
    package_installed "$pkg" && die "卸载后包仍在: $pkg"
  done
fi

# ---------- 5. 清理 httpd 中加载 PHP 的配置 ----------
# 包已删但配置文件被改过时会留下 .conf，模块已不存在还去 LoadModule 会让 httpd 起不来
if ! package_installed ganglia-web; then
  run rm -f /etc/httpd/conf.d/ganglia.conf
fi
if ! package_installed php; then
  run rm -f /etc/httpd/conf.d/php.conf /etc/httpd/conf.modules.d/10-php.conf
fi

# ---------- 6. 清理 /usr/bin 下指向源码 PHP 的软链 ----------
for link in /usr/bin/php /usr/bin/php-cgi /usr/bin/phpize \
            /usr/bin/php-config /usr/bin/phpdbg /usr/bin/pear /usr/bin/pecl; do
  if [ -L "$link" ]; then
    target="$(readlink "$link" 2>/dev/null || true)"
    case "$target" in
      /usr/local/bin/php|/usr/local/bin/php-cgi|/usr/local/bin/phpize|\
      /usr/local/bin/php-config|/usr/local/bin/phpdbg|/usr/local/bin/pear|/usr/local/bin/pecl)
        run rm -f "$link"
        log_action "删除软链 $link -> $target"
        ;;
    esac
  fi
done

# ---------- 7. 清理源码编译的 PHP ----------
if [ -x /usr/local/bin/php ]; then
  php_ver="$(/usr/local/bin/php -v 2>/dev/null | awk 'NR==1 {print; exit}' || true)"
  case "$php_ver" in
    "PHP "*)
      run rm -f \
        /usr/local/bin/php \
        /usr/local/bin/php-cgi \
        /usr/local/bin/php-config \
        /usr/local/bin/phpdbg \
        /usr/local/bin/phpize \
        /usr/local/bin/pear \
        /usr/local/bin/pecl
      log_action "删除 /usr/local PHP: $php_ver"
      ;;
    *)
      die "/usr/local/bin/php 不是预期的 PHP 程序，未删除: ${php_ver:-无法执行}"
      ;;
  esac
fi

if [ -f /usr/local/lib/php/PEAR.php ] || [ -d /usr/local/lib/php/PEAR ]; then
  run rm -rf /usr/local/lib/php
  log_action "删除 /usr/local/lib/php"
fi

if [ -d /usr/local/php/man ]; then
  run rm -rf /usr/local/php/man
  run rmdir /usr/local/php
  log_action "删除 /usr/local/php/man"
fi

# ---------- 8. 重建 DocumentRoot（ganglia-web 卸载会把目标目录一起删掉）----------
HTTPD_BIN=""
for b in /usr/sbin/httpd /usr/sbin/apachectl; do
  if [ -x "$b" ]; then HTTPD_BIN="$b"; break; fi
done

if [ -n "$HTTPD_BIN" ] && [ -f /etc/httpd/conf/httpd.conf ]; then
  DOC_ROOT="$(awk '/^[[:space:]]*DocumentRoot/ {gsub(/"/, "", $2); print $2; exit}' /etc/httpd/conf/httpd.conf)"
  if [ -n "$DOC_ROOT" ]; then
    if [ -L "$DOC_ROOT" ]; then
      TARGET_DIR="$(readlink "$DOC_ROOT" 2>/dev/null || true)"
      if [ -n "$TARGET_DIR" ] && [ ! -d "$TARGET_DIR" ]; then
        run mkdir -p "$TARGET_DIR"
        run chmod 755 "$TARGET_DIR"
        log_action "重建 DocumentRoot 目标目录: $TARGET_DIR"
      fi
    elif [ ! -d "$DOC_ROOT" ]; then
      run mkdir -p "$DOC_ROOT"
      run chmod 755 "$DOC_ROOT"
      log_action "重建 DocumentRoot 目录: $DOC_ROOT"
    fi
  fi
fi

# ---------- 9. 配置校验（失败则不 reload，避免把 httpd 置于无法重启的状态）----------
if [ -n "$HTTPD_BIN" ] && [ -f /etc/httpd/conf/httpd.conf ] && [ "$DRY_RUN" -eq 0 ]; then
  if ! "$HTTPD_BIN" -t >/tmp/php_clean_httpd_test.$$ 2>&1; then
    cat /tmp/php_clean_httpd_test.$$ >&2
    rm -f /tmp/php_clean_httpd_test.$$
    log "ERROR: httpd 配置校验失败，未执行 reload。请检查 /etc/httpd/conf/httpd.conf"
    exit 2
  fi
  rm -f /tmp/php_clean_httpd_test.$$
fi

if systemctl is-active --quiet httpd 2>/dev/null; then
  if [ "$DRY_RUN" -eq 0 ]; then
    log "httpd 正在运行，执行 reload"
    if ! systemctl reload httpd; then
      log "ERROR: systemctl reload httpd 失败"
      exit 2
    fi
  else
    log "[dry-run] httpd 正在运行，将执行 systemctl reload httpd"
  fi
fi

# ---------- 10. 结果校验 ----------
if [ "$DRY_RUN" -eq 0 ]; then
  if command -v php >/dev/null 2>&1; then
    die "卸载校验失败: 仍能找到 php 命令 ($(command -v php))"
  fi
  if [ -x /usr/local/bin/php ]; then
    die "卸载校验失败: /usr/local/bin/php 仍存在"
  fi
  if [ -e /usr/lib64/httpd/modules/libphp5.so ]; then
    die "卸载校验失败: libphp5.so 仍存在"
  fi
fi

log "完成。保留的包: $(rpm -q ganglia ganglia-gmond ganglia-gmetad ganglia-devel httpd 2>&1 | tr '\n' ' ')"
if [ "$BACKED_UP" -eq 1 ]; then
  log "备份目录: ${BACKUP_DIR}（仅配置，不含 PHP 二进制与 RPM）"
fi
exit 0
