#!/usr/bin/env bash set -Eeuo pipefail # proxmox-subscription-popup.sh # # Safe Proxmox VE 9 popup suppressor with: # - specific patch only # - no repair unless --repair is passed # - colored output # - backup before patch # - automatic rollback if patch verification fails # # Usage: # sudo bash proxmox-subscription-popup.sh # sudo bash proxmox-subscription-popup.sh --undo # sudo bash proxmox-subscription-popup.sh --repair # sudo bash proxmox-subscription-popup.sh --status 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" 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 patch sudo bash proxmox-subscription-popup.sh --undo Restore backup if present sudo bash proxmox-subscription-popup.sh --repair Reinstall owning package, restart proxy sudo bash proxmox-subscription-popup.sh --status Show current state EOF exit 0 ;; *) echo "Unknown argument: $arg" >&2 exit 2 ;; esac done if [[ "${EUID}" -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}"; } 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 } is_patched() { grep -Fq "orig_cmd(); return;" "$JS_FILE" } has_expected_original() { grep -Fq "if (data.status.toLowerCase() !== 'active')" "$JS_FILE" && grep -Fq "Ext.Msg.show({" "$JS_FILE" } write_patch_file() { cat > "$PATCH_FILE" <<'EOF' --- proxmoxlib.js.orig +++ proxmoxlib.js @@ -1,7 +1,8 @@ - checked_command: function(orig_cmd) { + checked_command: function(orig_cmd) { + orig_cmd(); return; Proxmox.Utils.API2Request({ url: '/nodes/localhost/subscription', method: 'GET', failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, EOF } status() { title "Proxmox subscription popup status" if ! is_proxmox; then fail "This does not look like a Proxmox host." exit 1 fi local major major="$(version_major || true)" if [[ -n "$major" ]]; then info "Detected Proxmox major version: ${C_BOLD}${major}${C_RESET}" fi if [[ ! -f "$JS_FILE" ]]; then fail "JS file not found: $JS_FILE" exit 1 fi local owner owner="$(detect_owner_pkg || true)" if [[ -n "$owner" ]]; then info "Owning package: ${C_BOLD}${owner}${C_RESET}" fi if is_patched; then ok "Popup suppression patch appears to be installed." else warn "Popup suppression 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 in ${STATE_DIR}" fi } repair() { title "Repairing Proxmox web UI file" [[ -f "$JS_FILE" ]] || { fail "Missing file: $JS_FILE"; exit 1; } 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" [[ -f "$JS_FILE" ]] || { fail "File still missing after reinstall"; exit 1; } 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; } [[ -f "$JS_FILE" ]] || { fail "Target file missing: $JS_FILE"; exit 1; } 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" [[ -f "$JS_FILE" ]] || { fail "Missing file: $JS_FILE"; exit 1; } 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 warn "This script was requested for Proxmox 9. Detected major version: ${major:-unknown}" warn "Continuing only if the target code still matches exactly." else ok "Detected Proxmox VE 9" 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, or inspect the current file manually." exit 1 fi 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 local tmp tmp="$(mktemp)" cp -a "$JS_FILE" "$tmp" write_patch_file info "Checking whether patch applies cleanly..." if ! patch --dry-run "$tmp" "$PATCH_FILE" >/dev/null 2>&1; then rm -f "$tmp" fail "Patch dry-run failed. File is not an exact match for this patch." fail "No changes were made." exit 1 fi ok "Patch dry-run succeeded" info "Applying patch..." patch "$JS_FILE" "$PATCH_FILE" >/dev/null ok "Patch applied" if ! is_patched; then warn "Verification failed after patch; restoring backup" cp -f "$BACKUP_FILE" "$JS_FILE" rm -f "$tmp" fail "Patch did not verify cleanly. Original file restored." exit 1 fi rm -f "$tmp" 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