#!/usr/bin/env bash set -Eeuo pipefail # proxmox-subscription-popup.sh # # Safe Proxmox VE 9 subscription popup suppressor. # # Defaults: # - patch only # - no repair unless --repair is passed # - refuses to patch unless file matches expected structure # # Usage: # sudo bash proxmox-subscription-popup.sh # sudo bash proxmox-subscription-popup.sh --status # sudo bash proxmox-subscription-popup.sh --undo # sudo bash proxmox-subscription-popup.sh --repair # # Notes: # - Uses `patch` with a very specific diff # - Creates a backup before patching # - Restarts pveproxy after changes # - Any package update may restore the original file SCRIPT_NAME="$(basename "$0")" JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js" STATE_DIR="/root/.proxmox-subscription-popup" BACKUP_FILE="${STATE_DIR}/proxmoxlib.js.bak" PATCH_FILE="${STATE_DIR}/subscription-popup.patch" WORK_ORIG="${STATE_DIR}/proxmoxlib.js.orig" DO_REPAIR=0 DO_UNDO=0 DO_STATUS=0 for arg in "$@"; do case "$arg" in --repair) DO_REPAIR=1 ;; --undo) DO_UNDO=1 ;; --status) DO_STATUS=1 ;; -h|--help) cat <<'EOF' Usage: sudo bash proxmox-subscription-popup.sh Apply the popup suppression patch sudo bash proxmox-subscription-popup.sh --status Show current state sudo bash proxmox-subscription-popup.sh --undo Restore the original file from backup sudo bash proxmox-subscription-popup.sh --repair Reinstall the package that owns proxmoxlib.js and restart pveproxy EOF exit 0 ;; *) echo "Unknown argument: $arg" >&2 exit 2 ;; esac done if [ "$(id -u)" -ne 0 ]; then echo "Please run as root." >&2 exit 1 fi mkdir -p "$STATE_DIR" if [[ -t 1 ]]; then C_RESET=$'\033[0m' C_BOLD=$'\033[1m' C_DIM=$'\033[2m' C_RED=$'\033[1;31m' C_GREEN=$'\033[1;32m' C_YELLOW=$'\033[1;33m' C_BLUE=$'\033[1;34m' C_MAGENTA=$'\033[1;35m' C_CYAN=$'\033[1;36m' else C_RESET="" C_BOLD="" C_DIM="" C_RED="" C_GREEN="" C_YELLOW="" C_BLUE="" C_MAGENTA="" C_CYAN="" fi say() { printf "%b\n" "$*"; } info() { say "${C_CYAN}▶${C_RESET} $*"; } ok() { say "${C_GREEN}✔${C_RESET} $*"; } warn() { say "${C_YELLOW}⚠${C_RESET} $*"; } fail() { say "${C_RED}✖${C_RESET} $*" >&2; } title() { say "${C_BOLD}${C_MAGENTA}$*${C_RESET}"; } trap 'fail "Script failed on line ${LINENO}. No further changes will be made."' ERR restart_proxy() { info "Restarting pveproxy..." systemctl restart pveproxy ok "pveproxy restarted" } show_browser_hint() { say say "${C_BOLD}Browser refresh:${C_RESET} hard refresh the UI after this." say "Mac: ${C_DIM}Cmd+Shift+R${C_RESET}" say "Other: ${C_DIM}Ctrl+Shift+R${C_RESET}" say } detect_owner_pkg() { dpkg -S "$JS_FILE" 2>/dev/null | head -n1 | cut -d: -f1 } is_proxmox() { command -v pveversion >/dev/null 2>&1 } version_major() { if ! command -v pveversion >/dev/null 2>&1; then return 1 fi pveversion | sed -n 's/^pve-manager\/\([0-9]\+\)\..*/\1/p' | head -n1 } require_file() { [[ -f "$JS_FILE" ]] || { fail "Missing file: $JS_FILE"; exit 1; } } has_patch_tool() { command -v patch >/dev/null 2>&1 } ensure_patch_tool() { if has_patch_tool; then return 0 fi fail "'patch' command not found." fail "Install it with: apt-get update && apt-get install -y patch" exit 1 } # Very specific to the function structure from the uploaded file. has_expected_original() { grep -Fq "checked_command: function (orig_cmd) {" "$JS_FILE" && grep -Fq "url: '/nodes/localhost/subscription'" "$JS_FILE" && grep -Fq "res.data.status.toLowerCase() !== 'active'" "$JS_FILE" && grep -Fq "title: gettext('No valid subscription')" "$JS_FILE" } # Detects our injected short-circuit immediately after the function opens. is_patched() { python3 - <<'PY' "$JS_FILE" import re, sys path = sys.argv[1] text = open(path, 'r', encoding='utf-8').read() pat = re.compile( r"checked_command:\s*function\s*\(orig_cmd\)\s*\{\s*orig_cmd\(\);\s*return;\s*Proxmox\.Utils\.API2Request\(", re.S, ) sys.exit(0 if pat.search(text) else 1) PY } write_patch_file() { cat > "$PATCH_FILE" <<'EOF' --- proxmoxlib.js.orig +++ proxmoxlib.js @@ -1,5 +1,7 @@ checked_command: function (orig_cmd) { + orig_cmd(); + return; Proxmox.Utils.API2Request({ url: '/nodes/localhost/subscription', method: 'GET', EOF } status() { title "Proxmox subscription popup status" if ! is_proxmox; then fail "This does not look like a Proxmox host." exit 1 fi require_file local major owner major="$(version_major || true)" owner="$(detect_owner_pkg || true)" if [[ -n "$major" ]]; then info "Detected Proxmox major version: ${C_BOLD}${major}${C_RESET}" else warn "Could not determine Proxmox major version" fi if [[ -n "$owner" ]]; then info "Owning package: ${C_BOLD}${owner}${C_RESET}" else warn "Could not determine owning package" fi if has_expected_original; then ok "Expected original popup code pattern is present" else warn "Expected original popup code pattern is NOT present" fi if is_patched; then ok "Patch appears to be installed" else warn "Patch does not appear to be installed" fi if [[ -f "$BACKUP_FILE" ]]; then info "Backup present: ${C_BOLD}${BACKUP_FILE}${C_RESET}" else warn "No backup found at ${STATE_DIR}" fi } repair() { title "Repairing Proxmox web UI file" require_file local owner owner="$(detect_owner_pkg || true)" [[ -n "$owner" ]] || { fail "Could not determine owning package for $JS_FILE"; exit 1; } info "Reinstalling package: ${C_BOLD}${owner}${C_RESET}" apt-get update apt-get install --reinstall -y "$owner" require_file restart_proxy ok "Repair complete" show_browser_hint } undo() { title "Restoring original file" [[ -f "$BACKUP_FILE" ]] || { fail "No backup found at $BACKUP_FILE"; exit 1; } require_file cp -f "$BACKUP_FILE" "$JS_FILE" ok "Original file restored from backup" restart_proxy show_browser_hint } apply_patch() { title "Applying safe Proxmox popup patch" require_file ensure_patch_tool if ! is_proxmox; then fail "This does not look like a Proxmox host." exit 1 fi local major major="$(version_major || true)" if [[ "$major" == "9" ]]; then ok "Detected Proxmox VE 9" else warn "Expected Proxmox VE 9, detected: ${major:-unknown}" warn "Proceeding only if the file matches the exact expected structure" fi if is_patched; then ok "Patch is already installed. Nothing to do." show_browser_hint return 0 fi if ! has_expected_original; then fail "The expected original code pattern was not found." fail "Refusing to patch, because the file layout is not what this script expects." fail "Use --repair first if the file is damaged, then try again." exit 1 fi ok "Expected original code pattern found" if [[ ! -f "$BACKUP_FILE" ]]; then info "Creating backup at ${C_BOLD}${BACKUP_FILE}${C_RESET}" cp -a "$JS_FILE" "$BACKUP_FILE" ok "Backup created" else warn "Backup already exists, leaving it untouched" fi cp -a "$JS_FILE" "$WORK_ORIG" write_patch_file info "Testing patch with dry-run..." if ! (cd "$STATE_DIR" && patch --dry-run proxmoxlib.js.orig subscription-popup.patch >/dev/null 2>&1); then fail "Patch dry-run failed." fail "No changes were made." exit 1 fi ok "Patch dry-run succeeded" info "Applying patch..." cp -a "$JS_FILE" "$WORK_ORIG" if ! (cd "$STATE_DIR" && patch "$WORK_ORIG" subscription-popup.patch >/dev/null 2>&1); then fail "Patch command failed unexpectedly." exit 1 fi cp -f "$WORK_ORIG" "$JS_FILE" if ! is_patched; then warn "Verification failed after patch; restoring backup" cp -f "$BACKUP_FILE" "$JS_FILE" fail "Patch did not verify cleanly. Original file restored." exit 1 fi ok "Patch verified successfully" restart_proxy ok "Subscription popup suppression is in place" show_browser_hint } case 1 in $DO_STATUS) status ;; $DO_UNDO) undo ;; $DO_REPAIR) repair ;; *) apply_patch ;; esac