#!/usr/bin/env bash set -Eeuo pipefail readonly SCRIPT_NAME="${0##*/}" readonly MARKER="OCMOD_PVE_NAG_DISABLED" readonly DEFAULT_WIDGET_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js" readonly DEFAULT_BACKUP_DIR="/var/backups/ocmod-pve-optimizer" readonly INSTALL_BIN="/usr/bin/install" WIDGET_FILE="${PVE_OPTIMIZER_WIDGET_FILE:-$DEFAULT_WIDGET_FILE}" BACKUP_DIR="${PVE_OPTIMIZER_BACKUP_DIR:-$DEFAULT_BACKUP_DIR}" SKIP_RESTART="${PVE_OPTIMIZER_SKIP_RESTART:-0}" ALLOW_NON_ROOT="${PVE_OPTIMIZER_ALLOW_NON_ROOT:-0}" if [[ "$ALLOW_NON_ROOT" != "1" ]]; then PATH="/usr/sbin:/usr/bin:/sbin:/bin" export PATH fi log() { printf '[%s] %s\n' "$SCRIPT_NAME" "$*" } die() { printf '[%s] ERROR: %s\n' "$SCRIPT_NAME" "$*" >&2 exit 1 } usage() { cat <<'EOF' Usage: disable-subscription-nag.sh apply disable-subscription-nag.sh status disable-subscription-nag.sh restore [backup-file] Commands: apply Back up proxmoxlib.js and hide only the subscription popup. status Report whether this script's patch is currently present. restore Restore a verified backup made by this script. EOF } require_root() { if [[ "$ALLOW_NON_ROOT" != "1" && ${EUID:-$(id -u)} -ne 0 ]]; then die "请使用 root 权限运行。" fi } require_system_tools() { [[ -x "$INSTALL_BIN" ]] || die "未找到系统命令 $INSTALL_BIN。" command -v perl >/dev/null 2>&1 || die "未找到 perl。" command -v sha256sum >/dev/null 2>&1 || die "未找到 sha256sum。" } require_widget_file() { [[ -f "$WIDGET_FILE" ]] || die "未找到 $WIDGET_FILE;请确认脚本运行在 PVE 宿主机,而不是虚拟机或容器内。" } sha256_file() { sha256sum "$1" | awk '{print $1}' } package_version() { dpkg-query -W -f='${Version}' proxmox-widget-toolkit 2>/dev/null || printf 'unknown' } restart_pveproxy() { if [[ "$SKIP_RESTART" == "1" ]]; then log "测试模式:跳过 pveproxy.service 重启。" return fi command -v systemctl >/dev/null 2>&1 || die "未找到 systemctl,文件已修改但无法重启 pveproxy.service。" systemctl restart pveproxy.service systemctl is-active --quiet pveproxy.service || die "pveproxy.service 重启后未处于 active 状态。" log "pveproxy.service 已重启并处于 active 状态。" } status_patch() { require_widget_file if grep -Fq "$MARKER" "$WIDGET_FILE"; then log "状态:订阅弹窗已关闭。" return 0 fi if grep -Fq "No valid subscription" "$WIDGET_FILE"; then log "状态:订阅弹窗仍启用。" return 1 fi die "未找到预期的订阅提示代码;当前 PVE 版本可能已改变文件结构。" } apply_patch() { local timestamp package backup metadata original_sha patched_sha matches require_root require_system_tools require_widget_file if grep -Fq "$MARKER" "$WIDGET_FILE"; then log "订阅弹窗已经关闭,无需重复修改。" return 0 fi matches=$(perl -0ne "while (/Ext\\.Msg\\.show\\(\\{\\s*title:\\s*gettext\\(['\"]No valid subscription['\"]\\),/g) { \$count++ } END { print \$count // 0 }" "$WIDGET_FILE") [[ "$matches" == "1" ]] || die "预期精确匹配 1 处订阅弹窗,实际为 $matches 处;为保护 Web UI,未做修改。" "$INSTALL_BIN" -d -m 0700 "$BACKUP_DIR" timestamp=$(date -u +%Y%m%dT%H%M%SZ) package=$(package_version | tr '/: ' '___') original_sha=$(sha256_file "$WIDGET_FILE") backup="$BACKUP_DIR/proxmoxlib.js.${timestamp}.${package}.${original_sha}.bak" metadata="$backup.meta" "$INSTALL_BIN" -m 0600 "$WIDGET_FILE" "$backup" perl -0pi -e "s/Ext\\.Msg\\.show\\(\\{(\\s*title:\\s*gettext\\(['\"]No valid subscription['\"]\\),)/void({ \/\/ $MARKER\$1/" "$WIDGET_FILE" grep -Fq "$MARKER" "$WIDGET_FILE" || die "修改后的标记验证失败;备份保留在 $backup。" [[ $(perl -0ne "while (/void\\(\\{\\s*\\/\\/ $MARKER\\s*title:\\s*gettext\\(['\"]No valid subscription['\"]\\),/g) { \$count++ } END { print \$count // 0 }" "$WIDGET_FILE") == "1" ]] || die "修改后的结构验证失败;备份保留在 $backup。" patched_sha=$(sha256_file "$WIDGET_FILE") { printf 'target=%s\n' "$WIDGET_FILE" printf 'package_version=%s\n' "$(package_version)" printf 'original_sha256=%s\n' "$original_sha" printf 'patched_sha256=%s\n' "$patched_sha" } >"$metadata" chmod 0600 "$metadata" restart_pveproxy log "完成。原文件备份:$backup" } restore_patch() { local backup="${1:-}" metadata expected_target expected_package original_sha patched_sha current_sha require_root require_system_tools require_widget_file if [[ -z "$backup" ]]; then backup=$(find "$BACKUP_DIR" -maxdepth 1 -type f -name 'proxmoxlib.js.*.bak' -print 2>/dev/null | sort | tail -n 1) fi [[ -n "$backup" && -f "$backup" ]] || die "没有找到可恢复的备份。" metadata="$backup.meta" [[ -f "$metadata" ]] || die "备份缺少校验元数据:$metadata" expected_target=$(sed -n 's/^target=//p' "$metadata") expected_package=$(sed -n 's/^package_version=//p' "$metadata") original_sha=$(sed -n 's/^original_sha256=//p' "$metadata") patched_sha=$(sed -n 's/^patched_sha256=//p' "$metadata") [[ "$expected_target" == "$WIDGET_FILE" ]] || die "备份目标与当前目标不一致,拒绝恢复。" [[ "$expected_package" == "$(package_version)" ]] || die "proxmox-widget-toolkit 版本已变化,拒绝恢复旧版本文件;请用 apt 重新安装该软件包。" [[ "$(sha256_file "$backup")" == "$original_sha" ]] || die "备份校验失败,拒绝恢复。" current_sha=$(sha256_file "$WIDGET_FILE") [[ "$current_sha" == "$patched_sha" ]] || die "当前文件在打补丁后又发生了变化,拒绝覆盖;请先人工检查。" "$INSTALL_BIN" -m 0644 "$backup" "$WIDGET_FILE" [[ "$(sha256_file "$WIDGET_FILE")" == "$original_sha" ]] || die "恢复后的文件校验失败。" restart_pveproxy log "已恢复备份:$backup" } main() { case "${1:-}" in apply) [[ $# -eq 1 ]] || { usage >&2; exit 2; } apply_patch ;; status) [[ $# -eq 1 ]] || { usage >&2; exit 2; } status_patch ;; restore) [[ $# -le 2 ]] || { usage >&2; exit 2; } restore_patch "${2:-}" ;; -h|--help|help) usage ;; *) usage >&2 exit 2 ;; esac } main "$@"