#!/bin/bash # ============================================================================== # ULTRABUNT ULTIMATE BUNTSTALLER v4.2.0 # Professional Ubuntu/Mint setup & buntage manager # - Full system scan (APT, Snap, Flatpak, binaries) # - Granular per-buntage control with visual status indicators # - Safe uninstall with multiple confirmation layers # - Stock-compatible Python (system python3 only, no custom builds) # - Comprehensive logging and error handling # - Repository management for third-party software # ============================================================================== # Check for bash version 4.0+ (required for associative arrays) if [ "${BASH_VERSION%%.*}" -lt 4 ]; then echo "Error: This script requires Bash 4.0 or higher for associative arrays." echo "Current version: $BASH_VERSION" echo "Please run with: bash $0" exit 1 fi set -euo pipefail export DEBIAN_FRONTEND=noninteractive # Configuration LOGFILE="/var/log/ultrabunt.log" BACKUP_DIR="/var/backups/ultrabunt" PHP_VER="8.3" NODE_LTS="20" # Cache for installed buntages - populated once at startup declare -A INSTALLED_CACHE # Session variables for database authentication MARIADB_SESSION_PASSWORD="" MARIADB_SESSION_ACTIVE=false # Color codes for terminal output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Initialize logging and buntage cache init_logging() { sudo mkdir -p "$(dirname "$LOGFILE")" "$BACKUP_DIR" 2>/dev/null || true sudo touch "$LOGFILE" 2>/dev/null || true sudo chown "$USER:$USER" "$LOGFILE" "$BACKUP_DIR" 2>/dev/null || true } # Build cache of all installed buntages for fast lookups build_package_cache() { log "Building buntage installation cache..." # Clear existing cache INSTALLED_CACHE=() # Cache APT buntages log "Caching APT buntages..." while IFS= read -r pkg; do INSTALLED_CACHE["apt:$pkg"]=1 done < <(dpkg-query -W -f='${Package}\n' 2>/dev/null | sort) # Cache Snap buntages if command -v snap &>/dev/null; then log "Caching Snap buntages..." while IFS= read -r pkg; do INSTALLED_CACHE["snap:$pkg"]=1 done < <(snap list 2>/dev/null | awk 'NR>1 {print $1}' | sort) fi # Cache Flatpak buntages if command -v flatpak &>/dev/null; then log "Caching Flatpak buntages..." while IFS= read -r pkg; do INSTALLED_CACHE["flatpak:$pkg"]=1 done < <(flatpak list --app --columns=application 2>/dev/null | sort) fi local total_cached=${#INSTALLED_CACHE[@]} log "Buntage cache built with $total_cached entries" } # Function to update cache for a specific buntage update_package_cache() { local name="$1" local silent="${2:-false}" # Add silent mode parameter if [[ -z "$name" ]]; then [[ "$silent" != "true" ]] && log "ERROR: No buntage name provided to update_package_cache" return 1 fi if [[ -z "${PACKAGES[$name]:-}" ]]; then [[ "$silent" != "true" ]] && log "WARNING: Buntage '$name' not found in PACKAGES array" return 1 fi local pkg="${PACKAGES[$name]}" local method="${PKG_METHOD[$name]:-}" [[ "$silent" != "true" ]] && log "Updating cache for buntage: $name (method: $method, pkg: $pkg)" # Update the specific buntage in the cache case "$method" in apt) if dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"; then INSTALLED_CACHE["apt:$pkg"]="1" [[ "$silent" != "true" ]] && log "Cache updated: $name is installed (APT)" else unset INSTALLED_CACHE["apt:$pkg"] # Removed verbose logging for not installed packages fi ;; snap) if snap list "$pkg" &>/dev/null; then INSTALLED_CACHE["snap:$pkg"]="1" [[ "$silent" != "true" ]] && log "Cache updated: $name is installed (Snap)" else unset INSTALLED_CACHE["snap:$pkg"] # Removed verbose logging for not installed packages fi ;; flatpak) if flatpak list --app 2>/dev/null | grep -q "$pkg"; then INSTALLED_CACHE["flatpak:$pkg"]="1" [[ "$silent" != "true" ]] && log "Cache updated: $name is installed (Flatpak)" else unset INSTALLED_CACHE["flatpak:$pkg"] # Removed verbose logging for not installed packages fi ;; custom) # For custom buntages, we can't easily cache them, so we'll just log [[ "$silent" != "true" ]] && log "Cache update skipped for custom buntage: $name" ;; *) [[ "$silent" != "true" ]] && log "WARNING: Unknown method '$method' for buntage '$name'" return 1 ;; esac return 0 } log() { local msg="[$(date +'%Y-%m-%d %H:%M:%S')] $*" echo "$msg" | tee -a "$LOGFILE" >&2 } log_error() { local msg="[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" echo "$msg" | tee -a "$LOGFILE" >&2 } # Ensure whiptail is available ensure_deps() { log "Checking dependencies..." if ! command -v whiptail &>/dev/null; then log "Installing whiptail..." if sudo apt-get update -qq 2>&1 | tee -a "$LOGFILE"; then log "APT cache updated for whiptail installation" else log "WARNING: APT update failed during whiptail installation" fi if sudo apt-get install -y whiptail 2>&1 | tee -a "$LOGFILE"; then log "Successfully installed whiptail" else log "ERROR: Failed to install whiptail - UI may not work properly" exit 1 fi else log "whiptail is already available" fi } # Whiptail wrapper functions ui_msg() { whiptail --title "$1" --msgbox "$2" 15 70 } ui_info() { whiptail --title "$1" --msgbox "$2" 20 80 } ui_yesno() { whiptail --title "$1" --yesno "$2" 12 70 } ui_input() { whiptail --title "$1" --inputbox "$2" 10 70 "${3:-}" 3>&1 1>&2 2>&3 } ui_password() { whiptail --title "$1" --passwordbox "$2" 10 70 3>&1 1>&2 2>&3 } # UI Functions ui_menu() { local title="$1" local text="$2" local height="$3" local width="$4" local menu_height="$5" shift 5 if ! command -v whiptail &>/dev/null; then log "ERROR: whiptail not available for ui_menu" return 1 fi # Execute whiptail and capture the result properly local choice choice=$(whiptail --title "$title" --menu "$text" "$height" "$width" "$menu_height" "$@" 3>&1 1>&2 2>&3) local exit_code=$? # Handle ESC key (exit code 255) as back navigation in nested menus if [[ $exit_code -eq 255 ]]; then log "ESC key pressed - treating as back navigation" echo "back" return 0 fi # Only log if there's an error (but not for ESC) if [[ $exit_code -ne 0 ]]; then log "ui_menu failed with exit code: $exit_code" fi if [[ $exit_code -eq 0 ]]; then echo "$choice" fi return $exit_code } ui_checklist() { local title="$1" local text="$2" local height="$3" local width="$4" local menu_height="$5" shift 5 whiptail --title "$title" --checklist "$text" "$height" "$width" "$menu_height" "$@" 3>&1 1>&2 2>&3 } # ============================================================================== # INSTALLATION METHOD SELECTION # ============================================================================== # Function to get all available installation methods for a base app name get_available_methods() { local base_name="$1" local methods=() # Check if base package exists if [[ -n "${PACKAGES[$base_name]:-}" ]]; then local method="${PKG_METHOD[$base_name]}" local desc="${PKG_DESC[$base_name]}" methods+=("$base_name" "$desc") fi # Check for alternative methods in preferred order: APT → Snap → Flatpak → Custom for variant in "${base_name}-deb" "${base_name}-snap" "${base_name}-flatpak"; do if [[ -n "${PACKAGES[$variant]:-}" ]]; then local desc="${PKG_DESC[$variant]}" methods+=("$variant" "$desc") fi done # Special cases for apps with different naming patterns case "$base_name" in "vscode") if [[ -n "${PACKAGES[vscode-snap]:-}" ]]; then methods+=("vscode-snap" "${PKG_DESC[vscode-snap]}") fi if [[ -n "${PACKAGES[vscode-flatpak]:-}" ]]; then methods+=("vscode-flatpak" "${PKG_DESC[vscode-flatpak]}") fi ;; "discord") if [[ -n "${PACKAGES[discord-deb]:-}" ]]; then methods+=("discord-deb" "${PKG_DESC[discord-deb]}") fi if [[ -n "${PACKAGES[discord-flatpak]:-}" ]]; then methods+=("discord-flatpak" "${PKG_DESC[discord-flatpak]}") fi ;; esac printf '%s\n' "${methods[@]}" } # Function to offer installation method choice choose_installation_method() { local base_name="$1" local available_methods # Get available methods as array mapfile -t available_methods < <(get_available_methods "$base_name") # If only one method available, return it if [[ ${#available_methods[@]} -eq 2 ]]; then echo "${available_methods[0]}" return 0 fi # If multiple methods available, show selection menu if [[ ${#available_methods[@]} -gt 2 ]]; then local choice choice=$(ui_menu "Installation Method" "Multiple installation methods available for $base_name.\nChoose your preferred method:" 20 80 10 "${available_methods[@]}") if [[ -n "$choice" && "$choice" != "back" ]]; then echo "$choice" return 0 else return 1 fi fi # No methods found return 1 } # Enhanced install function that checks for multiple methods install_package_with_choice() { local name="$1" # First check if this is already a specific variant (e.g., vscode-snap) if [[ -n "${PACKAGES[$name]:-}" ]]; then install_package "$name" return $? fi # Check for multiple installation methods local chosen_method chosen_method=$(choose_installation_method "$name") if [[ -n "$chosen_method" ]]; then install_package "$chosen_method" return $? else ui_msg "Installation Cancelled" "No installation method selected for $name." return 1 fi } # ============================================================================== # BUNTAGE DETECTION FUNCTIONS # ============================================================================== is_apt_installed() { local pkg="$1" if [[ -z "$pkg" ]]; then return 1 fi # Use cache for fast lookup if [[ -n "${INSTALLED_CACHE["apt:$pkg"]:-}" ]]; then return 0 else return 1 fi } is_snap_installed() { local pkg="$1" if [[ -z "$pkg" ]]; then return 1 fi if ! command -v snap &>/dev/null; then return 1 fi # Use cache for fast lookup if [[ -n "${INSTALLED_CACHE["snap:$pkg"]:-}" ]]; then return 0 else return 1 fi } is_flatpak_installed() { local pkg="$1" if [[ -z "$pkg" ]]; then return 1 fi if ! command -v flatpak &>/dev/null; then return 1 fi # Use cache for fast lookup if [[ -n "${INSTALLED_CACHE["flatpak:$pkg"]:-}" ]]; then return 0 else return 1 fi } is_binary_available() { command -v "$1" &>/dev/null } # Check if buntage exists in repos apt_package_exists() { apt-cache show "$1" &>/dev/null } # Get buntage status with icon get_status() { local pkg="$1" local method="${2:-apt}" # apt, snap, flatpak, binary case "$method" in apt) if is_apt_installed "$pkg"; then echo "✓" else echo "✗" fi ;; snap) if is_snap_installed "$pkg"; then echo "✓" else echo "✗" fi ;; flatpak) if is_flatpak_installed "$pkg"; then echo "✓" else echo "✗" fi ;; binary) if is_binary_available "$pkg"; then echo "✓" else echo "✗" fi ;; esac } # ============================================================================== # BUNTAGE DEFINITIONS # Each buntage has: name, description, install method, dependencies # ============================================================================== declare -A PACKAGES declare -A PKG_DESC declare -A PKG_METHOD declare -A PKG_CATEGORY declare -A PKG_DEPS # CORE UTILITIES PACKAGES[git]="git" PKG_DESC[git]="Version control system [APT]" PKG_METHOD[git]="apt" PKG_CATEGORY[git]="core" PACKAGES[curl]="curl" PKG_DESC[curl]="Command line tool for transferring data [APT]" PKG_METHOD[curl]="apt" PKG_CATEGORY[curl]="core" PACKAGES[wget]="wget" PKG_DESC[wget]="Network downloader [APT]" PKG_METHOD[wget]="apt" PKG_CATEGORY[wget]="core" PACKAGES[build-essential]="build-essential" PKG_DESC[build-essential]="Compilation tools (gcc, make, etc) [APT]" PKG_METHOD[build-essential]="apt" PKG_CATEGORY[build-essential]="core" PACKAGES[tree]="tree" PKG_DESC[tree]="Directory listing in tree format [APT]" PKG_METHOD[tree]="apt" PKG_CATEGORY[tree]="core" PACKAGES[ncdu]="ncdu" PKG_DESC[ncdu]="NCurses Disk Usage analyzer [APT]" PKG_METHOD[ncdu]="apt" PKG_CATEGORY[ncdu]="core" PACKAGES[jq]="jq" PKG_DESC[jq]="JSON processor [APT]" PKG_METHOD[jq]="apt" PKG_CATEGORY[jq]="core" PACKAGES[tmux]="tmux" PKG_DESC[tmux]="Terminal multiplexer [APT]" PKG_METHOD[tmux]="apt" PKG_CATEGORY[tmux]="core" PACKAGES[fzf]="fzf" PKG_DESC[fzf]="Fuzzy finder [APT]" PKG_METHOD[fzf]="apt" PKG_CATEGORY[fzf]="core" PACKAGES[ripgrep]="ripgrep" PKG_DESC[ripgrep]="Fast grep alternative (rg) [APT]" PKG_METHOD[ripgrep]="apt" PKG_CATEGORY[ripgrep]="core" PACKAGES[bat]="bat" PKG_DESC[bat]="Cat clone with syntax highlighting [APT]" PKG_METHOD[bat]="apt" PKG_CATEGORY[bat]="core" PACKAGES[eza]="eza" PKG_DESC[eza]="Modern ls replacement [APT]" PKG_METHOD[eza]="apt" PKG_CATEGORY[eza]="core" PACKAGES[tldr]="tldr" PKG_DESC[tldr]="Simplified man pages [APT]" PKG_METHOD[tldr]="apt" PKG_CATEGORY[tldr]="core" # DEVELOPMENT TOOLS PACKAGES[neovim]="neovim" PKG_DESC[neovim]="Modern Vim-based editor [APT]" PKG_METHOD[neovim]="apt" PKG_CATEGORY[neovim]="dev" PACKAGES[python3-pip]="python3-pip" PKG_DESC[python3-pip]="Python buntage installer [APT]" PKG_METHOD[python3-pip]="apt" PKG_CATEGORY[python3-pip]="dev" PACKAGES[python3-venv]="python3-venv" PKG_DESC[python3-venv]="Python virtual environments [APT]" PKG_METHOD[python3-venv]="apt" PKG_CATEGORY[python3-venv]="dev" PACKAGES[default-jdk]="default-jdk" PKG_DESC[default-jdk]="Java Development Kit [APT]" PKG_METHOD[default-jdk]="apt" PKG_CATEGORY[default-jdk]="dev" PACKAGES[golang]="golang-go" PKG_DESC[golang]="Go programming language [APT]" PKG_METHOD[golang]="apt" PKG_CATEGORY[golang]="dev" # CONTAINERS PACKAGES[docker]="docker-ce" PKG_DESC[docker]="Docker container platform [DEB]" PKG_METHOD[docker]="custom" PKG_CATEGORY[docker]="containers" PACKAGES[docker-compose]="docker-compose-plugin" PKG_DESC[docker-compose]="Docker Compose plugin [APT]" PKG_METHOD[docker-compose]="apt" PKG_CATEGORY[docker-compose]="containers" PKG_DEPS[docker-compose]="docker" # WEB STACK PACKAGES[nginx]="nginx" PKG_DESC[nginx]="High-performance web server [APT]" PKG_METHOD[nginx]="apt" PKG_CATEGORY[nginx]="web" PACKAGES[apache2]="apache2" PKG_DESC[apache2]="Apache HTTP Server [APT]" PKG_METHOD[apache2]="apt" PKG_CATEGORY[apache2]="web" PACKAGES[php-fpm]="php${PHP_VER}-fpm" PKG_DESC[php-fpm]="PHP FastCGI Process Manager" PKG_METHOD[php-fpm]="apt" PKG_CATEGORY[php-fpm]="web" PACKAGES[libapache2-mod-php]="libapache2-mod-php${PHP_VER}" PKG_DESC[libapache2-mod-php]="PHP module for Apache [APT]" PKG_METHOD[libapache2-mod-php]="apt" PKG_CATEGORY[libapache2-mod-php]="web" PACKAGES[php-mysql]="php${PHP_VER}-mysql" PKG_DESC[php-mysql]="PHP MySQL extension [APT]" PKG_METHOD[php-mysql]="apt" PKG_CATEGORY[php-mysql]="web" PACKAGES[php-curl]="php${PHP_VER}-curl" PKG_DESC[php-curl]="PHP cURL extension [APT]" PKG_METHOD[php-curl]="apt" PKG_CATEGORY[php-curl]="web" PACKAGES[php-gd]="php${PHP_VER}-gd" PKG_DESC[php-gd]="PHP GD graphics extension [APT]" PKG_METHOD[php-gd]="apt" PKG_CATEGORY[php-gd]="web" PACKAGES[php-xml]="php${PHP_VER}-xml" PKG_DESC[php-xml]="PHP XML extension [APT]" PKG_METHOD[php-xml]="apt" PKG_CATEGORY[php-xml]="web" PACKAGES[php-mbstring]="php${PHP_VER}-mbstring" PKG_DESC[php-mbstring]="PHP multibyte string extension [APT]" PKG_METHOD[php-mbstring]="apt" PKG_CATEGORY[php-mbstring]="web" PACKAGES[php-zip]="php${PHP_VER}-zip" PKG_DESC[php-zip]="PHP ZIP extension [APT]" PKG_METHOD[php-zip]="apt" PKG_CATEGORY[php-zip]="web" PACKAGES[mariadb]="mariadb-server" PKG_DESC[mariadb]="MariaDB database server [APT]" PKG_METHOD[mariadb]="apt" PKG_CATEGORY[mariadb]="web" PACKAGES[certbot]="certbot" PKG_DESC[certbot]="Let's Encrypt SSL certificate tool [APT]" PKG_METHOD[certbot]="apt" PKG_CATEGORY[certbot]="web" PACKAGES[python3-certbot-nginx]="python3-certbot-nginx" PKG_DESC[python3-certbot-nginx]="Certbot Nginx plugin [APT]" PKG_METHOD[python3-certbot-nginx]="apt" PKG_CATEGORY[python3-certbot-nginx]="web" PACKAGES[python3-certbot-apache]="python3-certbot-apache" PKG_DESC[python3-certbot-apache]="Certbot Apache plugin [APT]" PKG_METHOD[python3-certbot-apache]="apt" PKG_CATEGORY[python3-certbot-apache]="web" PACKAGES[redis]="redis-server" PKG_DESC[redis]="Redis in-memory data store [APT]" PKG_METHOD[redis]="apt" PKG_CATEGORY[redis]="web" # NODE.JS PACKAGES[nodejs]="nodejs" PKG_DESC[nodejs]="Node.js JavaScript runtime [DEB]" PKG_METHOD[nodejs]="custom" PKG_CATEGORY[nodejs]="dev" PACKAGES[npm]="npm" PKG_DESC[npm]="Node buntage manager [APT]" PKG_METHOD[npm]="apt" PKG_CATEGORY[npm]="dev" # SHELLS & UI PACKAGES[zsh]="zsh" PKG_DESC[zsh]="Z shell [APT]" PKG_METHOD[zsh]="apt" PKG_CATEGORY[zsh]="shell" PACKAGES[fonts-powerline]="fonts-powerline" PKG_DESC[fonts-powerline]="Powerline fonts [APT]" PKG_METHOD[fonts-powerline]="apt" PKG_CATEGORY[fonts-powerline]="shell" # EDITORS & IDEs PACKAGES[vscode]="code" PKG_DESC[vscode]="Visual Studio Code [DEB]" PKG_METHOD[vscode]="custom" PKG_CATEGORY[vscode]="editors" PACKAGES[sublime-text]="sublime-text" PKG_DESC[sublime-text]="Sublime Text editor [DEB]" PKG_METHOD[sublime-text]="custom" PKG_CATEGORY[sublime-text]="editors" # BROWSERS PACKAGES[brave]="brave-browser" PKG_DESC[brave]="Brave web browser [DEB]" PKG_METHOD[brave]="custom" PKG_CATEGORY[brave]="browsers" PACKAGES[firefox]="firefox" PKG_DESC[firefox]="Mozilla Firefox browser [APT]" PKG_METHOD[firefox]="apt" PKG_CATEGORY[firefox]="browsers" PACKAGES[chromium]="chromium" PKG_DESC[chromium]="Chromium web browser [APT]" PKG_METHOD[chromium]="apt" PKG_CATEGORY[chromium]="browsers" # MONITORING PACKAGES[htop]="htop" PKG_DESC[htop]="Interactive process viewer [APT]" PKG_METHOD[htop]="apt" PKG_CATEGORY[htop]="monitoring" PACKAGES[btop]="btop" PKG_DESC[btop]="Resource monitor with better graphs [APT]" PKG_METHOD[btop]="apt" PKG_CATEGORY[btop]="monitoring" PACKAGES[glances]="glances" PKG_DESC[glances]="Cross-platform system monitor [APT]" PKG_METHOD[glances]="apt" PKG_CATEGORY[glances]="monitoring" PACKAGES[nethogs]="nethogs" PKG_DESC[nethogs]="Network bandwidth monitor per process [APT]" PKG_METHOD[nethogs]="apt" PKG_CATEGORY[nethogs]="monitoring" PACKAGES[iotop]="iotop" PKG_DESC[iotop]="I/O monitor [APT]" PKG_METHOD[iotop]="apt" PKG_CATEGORY[iotop]="monitoring" # SECURITY PACKAGES[ufw]="ufw" PKG_DESC[ufw]="Uncomplicated Firewall [APT]" PKG_METHOD[ufw]="apt" PKG_CATEGORY[ufw]="security" PACKAGES[fail2ban]="fail2ban" PKG_DESC[fail2ban]="Intrusion prevention system [APT]" PKG_METHOD[fail2ban]="apt" PKG_CATEGORY[fail2ban]="security" # FLATPAK & APPS PACKAGES[flatpak]="flatpak" PKG_DESC[flatpak]="Flatpak buntage manager [APT]" PKG_METHOD[flatpak]="apt" PKG_CATEGORY[flatpak]="system" PACKAGES[localsend]="localsend" PKG_DESC[localsend]="LocalSend file sharing [SNP]" PKG_METHOD[localsend]="snap" PKG_CATEGORY[localsend]="communication" PKG_DEPS[localsend]="snapd" # SNAP APPS PACKAGES[spotify]="spotify" PKG_DESC[spotify]="Music streaming service [SNP]" PKG_METHOD[spotify]="snap" PKG_CATEGORY[spotify]="multimedia" PACKAGES[postman]="postman" PKG_DESC[postman]="API development platform [SNP]" PKG_METHOD[postman]="snap" PKG_CATEGORY[postman]="development" # DESKTOP APPS PACKAGES[obs-studio]="obs-studio" PKG_DESC[obs-studio]="OBS Studio streaming/recording [APT]" PKG_METHOD[obs-studio]="apt" PKG_CATEGORY[obs-studio]="multimedia" PACKAGES[vlc]="vlc" PKG_DESC[vlc]="VLC media player [APT]" PKG_METHOD[vlc]="apt" PKG_CATEGORY[vlc]="multimedia" # CLOUD & SYNC PACKAGES[rclone]="rclone" PKG_DESC[rclone]="Cloud storage sync tool [APT]" PKG_METHOD[rclone]="apt" PKG_CATEGORY[rclone]="cloud" PACKAGES[dropbox]="dropbox" PKG_DESC[dropbox]="File synchronization [DEB]" PKG_METHOD[dropbox]="custom" PKG_CATEGORY[dropbox]="cloud" PACKAGES[nextcloud-desktop]="nextcloud-desktop" PKG_DESC[nextcloud-desktop]="Self-hosted cloud client [APT]" PKG_METHOD[nextcloud-desktop]="apt" PKG_CATEGORY[nextcloud-desktop]="cloud" PACKAGES[syncthing]="syncthing" PKG_DESC[syncthing]="Decentralized sync [APT]" PKG_METHOD[syncthing]="apt" PKG_CATEGORY[syncthing]="cloud" PACKAGES[google-drive-ocamlfuse]="google-drive-ocamlfuse" PKG_DESC[google-drive-ocamlfuse]="Google Drive filesystem [APT]" PKG_METHOD[google-drive-ocamlfuse]="apt" PKG_CATEGORY[google-drive-ocamlfuse]="cloud" # TERMINALS PACKAGES[warp-terminal]="warp-terminal" PKG_DESC[warp-terminal]="Modern terminal with AI features [DEB]" PKG_METHOD[warp-terminal]="custom" PKG_CATEGORY[warp-terminal]="terminals" PACKAGES[alacritty]="alacritty" PKG_DESC[alacritty]="GPU-accelerated terminal emulator [APT]" PKG_METHOD[alacritty]="apt" PKG_CATEGORY[alacritty]="terminals" PACKAGES[terminator]="terminator" PKG_DESC[terminator]="Multiple terminals in one window [APT]" PKG_METHOD[terminator]="apt" PKG_CATEGORY[terminator]="terminals" PACKAGES[tilix]="tilix" PKG_DESC[tilix]="Tiling terminal emulator [APT]" PKG_METHOD[tilix]="apt" PKG_CATEGORY[tilix]="terminals" PACKAGES[ghostty]="ghostty" PKG_DESC[ghostty]="Fast, feature-rich terminal emulator [SNP]" PKG_METHOD[ghostty]="snap" PKG_CATEGORY[ghostty]="terminals" # GAMING PACKAGES[steam]="steam" PKG_DESC[steam]="Steam gaming platform [APT]" PKG_METHOD[steam]="apt" PKG_CATEGORY[steam]="gaming" PACKAGES[heroic-launcher]="heroic" PKG_DESC[heroic-launcher]="Open-source Epic Games/GOG launcher [SNP]" PKG_METHOD[heroic-launcher]="snap" PKG_CATEGORY[heroic-launcher]="gaming" PKG_DEPS[heroic-launcher]="snapd" PACKAGES[lutris]="lutris" PKG_DESC[lutris]="Gaming on Linux made easy [APT]" PKG_METHOD[lutris]="apt" PKG_CATEGORY[lutris]="gaming" PACKAGES[gamemode]="gamemode" PKG_DESC[gamemode]="Optimize gaming performance [APT]" PKG_METHOD[gamemode]="apt" PKG_CATEGORY[gamemode]="gaming" PACKAGES[gimp]="gimp" PKG_DESC[gimp]="GIMP image editor [APT]" PKG_METHOD[gimp]="apt" PKG_CATEGORY[gimp]="multimedia" # OFFICE & PRODUCTIVITY PACKAGES[libreoffice]="libreoffice" PKG_DESC[libreoffice]="LibreOffice office suite [APT]" PKG_METHOD[libreoffice]="apt" PKG_CATEGORY[libreoffice]="office" PACKAGES[thunderbird]="thunderbird" PKG_DESC[thunderbird]="Thunderbird email client [APT]" PKG_METHOD[thunderbird]="apt" PKG_CATEGORY[thunderbird]="office" PACKAGES[calibre]="calibre" PKG_DESC[calibre]="E-book management [APT]" PKG_METHOD[calibre]="apt" PKG_CATEGORY[calibre]="office" PACKAGES[obsidian]="obsidian" PKG_DESC[obsidian]="Knowledge management [SNP]" PKG_METHOD[obsidian]="snap" PKG_CATEGORY[obsidian]="office" # COMMUNICATION PACKAGES[discord]="discord" PKG_DESC[discord]="Discord voice and text chat [SNP]" PKG_METHOD[discord]="snap" PKG_CATEGORY[discord]="communication" PACKAGES[telegram-desktop]="telegram-desktop" PKG_DESC[telegram-desktop]="Telegram messaging app [APT]" PKG_METHOD[telegram-desktop]="apt" PKG_CATEGORY[telegram-desktop]="communication" PACKAGES[zoom]="zoom-client" PKG_DESC[zoom]="Zoom video conferencing [SNP]" PKG_METHOD[zoom]="snap" PKG_CATEGORY[zoom]="communication" # MULTIMEDIA PACKAGES[audacity]="audacity" PKG_DESC[audacity]="Audacity audio editor [APT]" PKG_METHOD[audacity]="apt" PKG_CATEGORY[audacity]="multimedia" PACKAGES[blender]="blender" PKG_DESC[blender]="Blender 3D creation suite [SNP]" PKG_METHOD[blender]="snap" PKG_CATEGORY[blender]="multimedia" PACKAGES[inkscape]="inkscape" PKG_DESC[inkscape]="Inkscape vector graphics editor [APT]" PKG_METHOD[inkscape]="apt" PKG_CATEGORY[inkscape]="multimedia" # AI & MODERN TOOLS PACKAGES[ollama]="ollama" PKG_DESC[ollama]="Local AI model runner (Llama, Mistral, etc.) [DEB]" PKG_METHOD[ollama]="custom" PKG_CATEGORY[ollama]="ai" PACKAGES[gollama]="gollama" PKG_DESC[gollama]="Advanced LLM model management and interaction tool [DEB]" PKG_METHOD[gollama]="custom" PKG_CATEGORY[gollama]="ai" PACKAGES[ffmpeg]="ffmpeg" PKG_DESC[ffmpeg]="Complete multimedia processing toolkit [APT]" PKG_METHOD[ffmpeg]="apt" PKG_CATEGORY[ffmpeg]="multimedia" PACKAGES[yt-dlp]="yt-dlp" PKG_DESC[yt-dlp]="Modern YouTube/media downloader (youtube-dl fork) [PIP]" PKG_METHOD[yt-dlp]="custom" PKG_CATEGORY[yt-dlp]="multimedia" PACKAGES[n8n]="n8n" PKG_DESC[n8n]="Workflow automation tool (self-hosted Zapier alternative) [NPM]" PKG_METHOD[n8n]="custom" PKG_CATEGORY[n8n]="development" # ============================================================================== # ALTERNATIVE INSTALLATION METHODS # ============================================================================== # Apps that have multiple installation options - users can choose their preferred method # VS Code alternatives PACKAGES[vscode-snap]="code" PKG_DESC[vscode-snap]="Visual Studio Code [SNP] - Alternative to DEB version" PKG_METHOD[vscode-snap]="snap" PKG_CATEGORY[vscode-snap]="editors" PACKAGES[vscode-flatpak]="com.visualstudio.code" PKG_DESC[vscode-flatpak]="Visual Studio Code [FLT] - Alternative to DEB version" PKG_METHOD[vscode-flatpak]="flatpak" PKG_CATEGORY[vscode-flatpak]="editors" PKG_DEPS[vscode-flatpak]="flatpak" # Firefox alternatives PACKAGES[firefox-snap]="firefox" PKG_DESC[firefox-snap]="Mozilla Firefox browser [SNP] - Alternative to APT version" PKG_METHOD[firefox-snap]="snap" PKG_CATEGORY[firefox-snap]="browsers" PACKAGES[firefox-flatpak]="org.mozilla.firefox" PKG_DESC[firefox-flatpak]="Mozilla Firefox browser [FLT] - Alternative to APT version" PKG_METHOD[firefox-flatpak]="flatpak" PKG_CATEGORY[firefox-flatpak]="browsers" PKG_DEPS[firefox-flatpak]="flatpak" # Chromium alternatives PACKAGES[chromium-snap]="chromium" PKG_DESC[chromium-snap]="Chromium web browser [SNP] - Alternative to APT version" PKG_METHOD[chromium-snap]="snap" PKG_CATEGORY[chromium-snap]="browsers" PACKAGES[chromium-flatpak]="org.chromium.Chromium" PKG_DESC[chromium-flatpak]="Chromium web browser [FLT] - Alternative to APT version" PKG_METHOD[chromium-flatpak]="flatpak" PKG_CATEGORY[chromium-flatpak]="browsers" PKG_DEPS[chromium-flatpak]="flatpak" # VLC alternatives PACKAGES[vlc-snap]="vlc" PKG_DESC[vlc-snap]="VLC media player [SNP] - Alternative to APT version" PKG_METHOD[vlc-snap]="snap" PKG_CATEGORY[vlc-snap]="multimedia" PACKAGES[vlc-flatpak]="org.videolan.VLC" PKG_DESC[vlc-flatpak]="VLC media player [FLT] - Alternative to APT version" PKG_METHOD[vlc-flatpak]="flatpak" PKG_CATEGORY[vlc-flatpak]="multimedia" PKG_DEPS[vlc-flatpak]="flatpak" # GIMP alternatives PACKAGES[gimp-snap]="gimp" PKG_DESC[gimp-snap]="GIMP image editor [SNP] - Alternative to APT version" PKG_METHOD[gimp-snap]="snap" PKG_CATEGORY[gimp-snap]="multimedia" PACKAGES[gimp-flatpak]="org.gimp.GIMP" PKG_DESC[gimp-flatpak]="GIMP image editor [FLT] - Alternative to APT version" PKG_METHOD[gimp-flatpak]="flatpak" PKG_CATEGORY[gimp-flatpak]="multimedia" PKG_DEPS[gimp-flatpak]="flatpak" # OBS Studio alternatives PACKAGES[obs-studio-flatpak]="com.obsproject.Studio" PKG_DESC[obs-studio-flatpak]="OBS Studio streaming/recording [FLT] - Alternative to APT version" PKG_METHOD[obs-studio-flatpak]="flatpak" PKG_CATEGORY[obs-studio-flatpak]="multimedia" PKG_DEPS[obs-studio-flatpak]="flatpak" # Discord alternatives (APT version) PACKAGES[discord-deb]="discord" PKG_DESC[discord-deb]="Discord voice and text chat [DEB] - Alternative to SNP version" PKG_METHOD[discord-deb]="custom" PKG_CATEGORY[discord-deb]="communication" PACKAGES[discord-flatpak]="com.discordapp.Discord" PKG_DESC[discord-flatpak]="Discord voice and text chat [FLT] - Alternative to SNP version" PKG_METHOD[discord-flatpak]="flatpak" PKG_CATEGORY[discord-flatpak]="communication" PKG_DEPS[discord-flatpak]="flatpak" # Audacity alternatives PACKAGES[audacity-snap]="audacity" PKG_DESC[audacity-snap]="Audacity audio editor [SNP] - Alternative to APT version" PKG_METHOD[audacity-snap]="snap" PKG_CATEGORY[audacity-snap]="multimedia" PACKAGES[audacity-flatpak]="org.audacityteam.Audacity" PKG_DESC[audacity-flatpak]="Audacity audio editor [FLT] - Alternative to APT version" PKG_METHOD[audacity-flatpak]="flatpak" PKG_CATEGORY[audacity-flatpak]="multimedia" PKG_DEPS[audacity-flatpak]="flatpak" # Inkscape alternatives PACKAGES[inkscape-snap]="inkscape" PKG_DESC[inkscape-snap]="Inkscape vector graphics [SNP] - Alternative to APT version" PKG_METHOD[inkscape-snap]="snap" PKG_CATEGORY[inkscape-snap]="multimedia" PACKAGES[inkscape-flatpak]="org.inkscape.Inkscape" PKG_DESC[inkscape-flatpak]="Inkscape vector graphics [FLT] - Alternative to APT version" PKG_METHOD[inkscape-flatpak]="flatpak" PKG_CATEGORY[inkscape-flatpak]="multimedia" PKG_DEPS[inkscape-flatpak]="flatpak" # ============================================================================== # CATEGORY DEFINITIONS # ============================================================================== CATEGORIES=( "core:Core Utilities" "dev:Development Tools" "ai:AI & LLM Tools" "containers:Containers & Orchestration" "web:Web Stack" "shell:Shells & Customization" "editors:Editors & IDEs" "browsers:Web Browsers" "monitoring:System Monitoring" "security:Security Tools" "system:System Tools" "office:Office & Productivity" "communication:Communication" "multimedia:Multimedia & Graphics" "cloud:Cloud & Sync" "terminals:Terminals" "gaming:Gaming" ) # ============================================================================== # INSTALLATION FUNCTIONS # ============================================================================== apt_update() { log "Updating APT cache..." if sudo apt-get update -qq 2>&1 | tee -a "$LOGFILE"; then log "APT cache updated successfully" else local exit_code=$? if [ $exit_code -eq 0 ]; then log "APT cache updated with warnings (non-critical)" else log "WARNING: APT cache update failed with exit code $exit_code" log "Continuing anyway - some packages may not be available" fi fi } install_apt_package() { local pkg="$1" log "Installing $pkg via APT..." if sudo apt-get install -y --no-install-recommends "$pkg" 2>&1 | tee -a "$LOGFILE"; then log "Successfully installed $pkg" return 0 else log "ERROR: Failed to install $pkg" return 1 fi } remove_apt_package() { local pkg="$1" log "Removing $pkg via APT..." if sudo apt-get remove --purge -y "$pkg" 2>&1 | tee -a "$LOGFILE"; then log "Successfully removed $pkg" return 0 else log "ERROR: Failed to remove $pkg" return 1 fi } install_snap_package() { local pkg="$1" log "Installing $pkg via Snap..." # Packages that require classic confinement case "$pkg" in ghostty) sudo snap install "$pkg" --classic 2>&1 | tee -a "$LOGFILE" ;; *) sudo snap install "$pkg" 2>&1 | tee -a "$LOGFILE" ;; esac } remove_snap_package() { local pkg="$1" log "Removing $pkg via Snap..." sudo snap remove "$pkg" 2>&1 | tee -a "$LOGFILE" } install_flatpak_package() { local pkg="$1" log "Installing $pkg via Flatpak..." flatpak install -y flathub "$pkg" 2>&1 | tee -a "$LOGFILE" } remove_flatpak_package() { local pkg="$1" log "Removing $pkg via Flatpak..." flatpak uninstall -y "$pkg" 2>&1 | tee -a "$LOGFILE" } # ============================================================================== # CUSTOM INSTALLERS (for buntages requiring special setup) # ============================================================================== install_docker() { log "Installing Docker from official repository..." # Remove old versions sudo apt-get remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true # Add Docker repository sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null apt_update install_apt_package "docker-ce" install_apt_package "docker-ce-cli" install_apt_package "containerd.io" install_apt_package "docker-buildx-plugin" install_apt_package "docker-compose-plugin" # Add user to docker group sudo usermod -aG docker "$USER" || true ui_msg "Docker Installed" "Docker installed successfully. Log out and back in for group changes to take effect." } remove_docker() { ui_yesno "Remove Docker?" "This will remove Docker and optionally its data. Continue?" || return 1 remove_apt_package "docker-ce" remove_apt_package "docker-ce-cli" remove_apt_package "containerd.io" remove_apt_package "docker-buildx-plugin" remove_apt_package "docker-compose-plugin" if ui_yesno "Remove Data?" "Remove Docker data directories (/var/lib/docker)?"; then sudo rm -rf /var/lib/docker /var/lib/containerd log "Docker data removed" fi sudo rm -f /etc/apt/sources.list.d/docker.list sudo rm -f /etc/apt/keyrings/docker.gpg } install_nodejs() { log "Installing Node.js ${NODE_LTS} from NodeSource..." curl -fsSL https://deb.nodesource.com/setup_${NODE_LTS}.x | sudo -E bash - apt_update install_apt_package "nodejs" ui_msg "Node.js Installed" "Node.js ${NODE_LTS} and npm installed successfully." } remove_nodejs() { remove_apt_package "nodejs" sudo rm -f /etc/apt/sources.list.d/nodesource.list } install_vscode() { log "Installing VS Code from Microsoft repository..." wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /usr/share/keyrings/packages.microsoft.gpg > /dev/null echo "deb [arch=amd64,arm64,armhf signed-by=/usr/share/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | sudo tee /etc/apt/sources.list.d/vscode.list apt_update install_apt_package "code" ui_msg "VS Code Installed" "Visual Studio Code installed successfully." } remove_vscode() { remove_apt_package "code" sudo rm -f /etc/apt/sources.list.d/vscode.list sudo rm -f /usr/share/keyrings/packages.microsoft.gpg } install_brave() { log "Installing Brave browser..." sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main" | sudo tee /etc/apt/sources.list.d/brave-browser-release.list apt_update install_apt_package "brave-browser" ui_msg "Brave Installed" "Brave browser installed successfully." } remove_brave() { remove_apt_package "brave-browser" sudo rm -f /etc/apt/sources.list.d/brave-browser-release.list sudo rm -f /usr/share/keyrings/brave-browser-archive-keyring.gpg } install_sublime() { log "Installing Sublime Text..." wget -qO - https://download.sublimetext.com/sublimehq-pub.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/sublimehq-archive.gpg > /dev/null echo "deb [signed-by=/usr/share/keyrings/sublimehq-archive.gpg] https://download.sublimetext.com/ apt/stable/" | sudo tee /etc/apt/sources.list.d/sublime-text.list apt_update install_apt_package "sublime-text" ui_msg "Sublime Text Installed" "Sublime Text installed successfully." } remove_sublime() { remove_apt_package "sublime-text" sudo rm -f /etc/apt/sources.list.d/sublime-text.list sudo rm -f /usr/share/keyrings/sublimehq-archive.gpg } install_warp() { log "Installing Warp terminal..." # Add Warp's official GPG key and repository sudo mkdir -p /usr/share/keyrings if ! wget -qO- https://releases.warp.dev/linux/keys/warp.asc | sudo gpg --dearmor -o /usr/share/keyrings/warp-archive-keyring.gpg; then log_error "Failed to add Warp GPG key" return 1 fi # Add Warp repository echo "deb [signed-by=/usr/share/keyrings/warp-archive-keyring.gpg] https://releases.warp.dev/linux/deb stable main" | sudo tee /etc/apt/sources.list.d/warp-terminal.list > /dev/null # Update package list and install sudo apt-get update -qq if install_apt_package "warp-terminal"; then ui_msg "Warp Installed" "Warp terminal installed successfully." else log_error "Failed to install Warp terminal from repository" return 1 fi } remove_warp() { remove_apt_package "warp-terminal" sudo rm -f /etc/apt/sources.list.d/warp-terminal.list sudo rm -f /usr/share/keyrings/warp-archive-keyring.gpg } install_ollama() { log "Installing Ollama..." # Download and install Ollama if curl -fsSL https://ollama.ai/install.sh | sh; then ui_msg "Ollama Installed" "Ollama AI model runner installed successfully." else log_error "Failed to install Ollama" return 1 fi } remove_ollama() { log "Removing Ollama..." sudo systemctl stop ollama 2>/dev/null || true sudo systemctl disable ollama 2>/dev/null || true sudo rm -f /usr/local/bin/ollama sudo rm -rf /usr/share/ollama sudo rm -f /etc/systemd/system/ollama.service sudo systemctl daemon-reload } install_yt-dlp() { log "Installing yt-dlp..." # Install via pip for latest version if command -v pip3 >/dev/null 2>&1; then if pip3 install --user yt-dlp; then ui_msg "yt-dlp Installed" "yt-dlp YouTube downloader installed successfully." else log_error "Failed to install yt-dlp via pip" return 1 fi else log_error "pip3 not found. Please install Python3 and pip first." return 1 fi } remove_yt-dlp() { log "Removing yt-dlp..." pip3 uninstall -y yt-dlp 2>/dev/null || true } install_n8n() { log "Installing n8n..." # Check if Node.js is installed if ! command -v node >/dev/null 2>&1; then log_error "Node.js is required for n8n. Please install Node.js first." return 1 fi # Install n8n globally via npm if sudo npm install -g n8n; then ui_msg "n8n Installed" "n8n workflow automation tool installed successfully." else log_error "Failed to install n8n" return 1 fi } remove_n8n() { log "Removing n8n..." sudo npm uninstall -g n8n 2>/dev/null || true } install_discord-deb() { log "Installing Discord (DEB package)..." # Download Discord DEB package local discord_url="https://discord.com/api/download?platform=linux&format=deb" local temp_file="/tmp/discord.deb" if wget -O "$temp_file" "$discord_url" 2>/dev/null; then if sudo dpkg -i "$temp_file" 2>/dev/null || sudo apt-get install -f -y; then rm -f "$temp_file" ui_msg "Discord Installed" "Discord voice and text chat installed successfully." else log_error "Failed to install Discord DEB package" rm -f "$temp_file" return 1 fi else log_error "Failed to download Discord DEB package" return 1 fi } remove_discord-deb() { log "Removing Discord (DEB)..." remove_apt_package "discord" } install_gollama() { log "Installing Gollama..." # Download and install Gollama from GitHub releases local latest_url=$(curl -s https://api.github.com/repos/sammcj/gollama/releases/latest | grep "browser_download_url.*linux.*amd64" | cut -d '"' -f 4) if [[ -z "$latest_url" ]]; then log_error "Failed to get Gollama download URL" return 1 fi local temp_file="/tmp/gollama" if wget -O "$temp_file" "$latest_url" 2>/dev/null; then chmod +x "$temp_file" sudo mv "$temp_file" /usr/local/bin/gollama ui_msg "Gollama Installed" "Gollama LLM management tool installed successfully." else log_error "Failed to download Gollama" return 1 fi } remove_gollama() { log "Removing Gollama..." sudo rm -f /usr/local/bin/gollama } # ============================================================================== # BUNTAGE MANAGEMENT DISPATCHER # ============================================================================== install_package() { local name="$1" local pkg="${PACKAGES[$name]}" local method="${PKG_METHOD[$name]}" # Check dependencies if [[ -n "${PKG_DEPS[$name]:-}" ]]; then local dep="${PKG_DEPS[$name]}" if ! is_package_installed "$dep"; then ui_msg "Dependency Required" "$name requires $dep. Install $dep first." return 1 fi fi local install_result=0 case "$method" in apt) apt_update install_apt_package "$pkg" install_result=$? ;; snap) install_snap_package "$pkg" install_result=$? ;; flatpak) if ! is_apt_installed "flatpak"; then ui_msg "Flatpak Required" "Installing Flatpak first...\n\n⏳ This process is automatic - please wait..." apt_update install_apt_package "flatpak" flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo fi install_flatpak_package "$pkg" install_result=$? ;; custom) case "$name" in docker) install_docker; install_result=$? ;; nodejs) install_nodejs; install_result=$? ;; vscode) install_vscode; install_result=$? ;; brave) install_brave; install_result=$? ;; sublime-text) install_sublime; install_result=$? ;; warp-terminal) install_warp; install_result=$? ;; ollama) install_ollama; install_result=$? ;; yt-dlp) install_yt-dlp; install_result=$? ;; n8n) install_n8n; install_result=$? ;; gollama) install_gollama; install_result=$? ;; discord-deb) install_discord-deb; install_result=$? ;; *) log_error "Unknown custom installer: $name"; install_result=1 ;; esac ;; esac # Update cache for this specific buntage if installation was successful if [[ $install_result -eq 0 ]]; then log "Installation successful, updating cache for buntage: $name" update_package_cache "$name" fi return $install_result } remove_package() { local name="$1" local pkg="${PACKAGES[$name]}" local method="${PKG_METHOD[$name]}" ui_yesno "Confirm Removal" "Are you sure you want to remove $name?\n\n${PKG_DESC[$name]}" || return 1 local remove_result=0 case "$method" in apt) remove_apt_package "$pkg" remove_result=$? ;; snap) remove_snap_package "$pkg" remove_result=$? ;; flatpak) remove_flatpak_package "$pkg" remove_result=$? ;; custom) case "$name" in docker) remove_docker; remove_result=$? ;; nodejs) remove_nodejs; remove_result=$? ;; vscode) remove_vscode; remove_result=$? ;; brave) remove_brave; remove_result=$? ;; sublime-text) remove_sublime; remove_result=$? ;; warp-terminal) remove_warp; remove_result=$? ;; ollama) remove_ollama; remove_result=$? ;; yt-dlp) remove_yt-dlp; remove_result=$? ;; n8n) remove_n8n; remove_result=$? ;; gollama) remove_gollama; remove_result=$? ;; discord-deb) remove_discord-deb; remove_result=$? ;; *) log_error "Unknown custom remover: $name"; remove_result=1 ;; esac ;; esac # Update cache for this specific buntage if removal was successful if [[ $remove_result -eq 0 ]]; then log "Removal successful, updating cache for buntage: $name" update_package_cache "$name" ui_msg "Removed" "$name has been removed." else ui_msg "Error" "Failed to remove $name. Check the logs for details." fi return $remove_result } is_package_installed() { local name="$1" local silent="${2:-false}" # Add silent mode parameter # Add error handling for empty or invalid buntage names if [[ -z "$name" ]]; then [[ "$silent" != "true" ]] && log "ERROR: Empty buntage name provided to is_package_installed" return 1 fi if [[ -z "${PACKAGES[$name]:-}" ]]; then [[ "$silent" != "true" ]] && log "WARNING: Buntage '$name' not found in PACKAGES array" return 1 fi local pkg="${PACKAGES[$name]}" local method="${PKG_METHOD[$name]:-}" if [[ -z "$method" ]]; then [[ "$silent" != "true" ]] && log "WARNING: No method defined for buntage '$name'" return 1 fi [[ "$silent" != "true" ]] && log "Buntage '$name' uses method '$method' with buntage name '$pkg'" # Add error handling for each method local result=1 case "$method" in apt) log "Checking APT installation for '$pkg'" if is_apt_installed "$pkg"; then result=0 else result=1 fi ;; snap) log "Checking Snap installation for '$pkg'" if is_snap_installed "$pkg"; then result=0 else result=1 fi ;; flatpak) log "Checking Flatpak installation for '$pkg'" if is_flatpak_installed "$pkg"; then result=0 else result=1 fi ;; custom) log "Checking custom installation for '$name'" case "$name" in docker) if is_apt_installed "docker-ce"; then result=0 else result=1 fi ;; nodejs) if is_binary_available "node"; then result=0 else result=1 fi ;; vscode) if is_apt_installed "code"; then result=0 else result=1 fi ;; brave) if is_apt_installed "brave-browser"; then result=0 else result=1 fi ;; sublime-text) if is_apt_installed "sublime-text"; then result=0 else result=1 fi ;; warp-terminal) if is_apt_installed "warp-terminal"; then result=0 else result=1 fi ;; yt-dlp) if is_binary_available "yt-dlp"; then result=0 else result=1 fi ;; *) log "WARNING: Unknown custom buntage '$name'" result=1 ;; esac ;; *) log "ERROR: Unknown installation method '$method' for buntage '$name'" result=1 ;; esac log "Buntage '$name' installation check result: $result" return $result } # ============================================================================== # MENU SYSTEM # ============================================================================== # Function to refresh package cache in background without logging refresh_cache_silent() { local refresh_pid { # Update cache for all packages silently for name in "${!PACKAGES[@]}"; do update_package_cache "$name" "true" &>/dev/null done } & refresh_pid=$! # Store the PID for potential cleanup echo "$refresh_pid" > "/tmp/ultrabunt_cache_refresh.pid" 2>/dev/null || true } # Function to check if background cache refresh is complete is_cache_refresh_complete() { local pid_file="/tmp/ultrabunt_cache_refresh.pid" if [[ -f "$pid_file" ]]; then local pid pid=$(cat "$pid_file" 2>/dev/null) if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then return 1 # Still running else rm -f "$pid_file" 2>/dev/null || true return 0 # Complete fi fi return 0 # No refresh running } show_category_menu() { log "Entering show_category_menu function" # Start background cache refresh on first load refresh_cache_silent while true; do log "Building category menu items..." local menu_items=() log "Processing categories array with ${#CATEGORIES[@]} entries" for entry in "${CATEGORIES[@]}"; do local cat_id="${entry%%:*}" local cat_name="${entry#*:}" # Count installed buntages in category local total=0 local installed=0 # Use a safer iteration method local package_names=() for name in "${!PACKAGES[@]}"; do package_names+=("$name") done for name in "${package_names[@]}"; do local pkg_category="${PKG_CATEGORY[$name]:-}" if [[ "$pkg_category" == "$cat_id" ]]; then total=$((total + 1)) # Re-enable installation check with silent mode if is_package_installed "$name" "true"; then installed=$((installed + 1)) fi fi done # Add refresh indicator if cache is still updating local refresh_indicator="" if ! is_cache_refresh_complete; then refresh_indicator=" 🔄" fi menu_items+=("$cat_id" "(.Y.) $cat_name [$installed/$total installed]$refresh_indicator") done log "Adding additional menu items..." menu_items+=("" "(_*_)") menu_items+=("system-info" "(.Y.) System Information") menu_items+=("keyboard-layout" "(.Y.) Keyboard Layout Configuration") menu_items+=("wordpress-setup" "(.Y.) WordPress Installation") menu_items+=("database-management" "(.Y.) Database Management") menu_items+=("bulk-ops" "(.Y.) Bulk Operations") menu_items+=("quit" "(Q) Exit Installer") log "Calling ui_menu with ${#menu_items[@]} menu items" log "Menu items array contents: ${menu_items[*]}" # Add error handling for ui_menu call local choice="" set +e # Temporarily disable exit on error choice=$(ui_menu "Ultrabunt Ultimate Buntstaller" \ "Select a buntegory to manage buntages:" \ 24 80 14 "${menu_items[@]}") local ui_result=$? set -e # Re-enable exit on error if [[ $ui_result -ne 0 ]]; then log "User cancelled or error occurred" break fi case "$choice" in quit|back|q) # Add confirmation for quit if [[ "$choice" == "quit" || "$choice" == "q" ]]; then if ui_yesno "Confirm Exit" "Are you sure you want to exit the Ultrabunt installer?"; then break else continue fi else break fi ;; system-info) show_system_info || { log "ERROR: show_system_info failed" ui_msg "Error" "Failed to display system information. Please check the logs." } ;; keyboard-layout) show_keyboard_layout_menu || { log "ERROR: show_keyboard_layout_menu failed" ui_msg "Error" "Failed to display keyboard layout menu. Please check the logs." } ;; wordpress-setup) show_wordpress_setup_menu || { log "ERROR: show_wordpress_setup_menu failed" ui_msg "Error" "Failed to display WordPress setup menu. Please check the logs." } ;; database-management) show_database_management_menu || { log "ERROR: show_database_management_menu failed" ui_msg "Error" "Failed to display database management menu. Please check the logs." } ;; bulk-ops) show_bulk_operations ;; "") continue ;; *) show_buntage_list "$choice" ;; esac done log "Exiting show_category_menu function" } show_buntage_list() { local category="$1" local cat_name="" for entry in "${CATEGORIES[@]}"; do if [[ "${entry%%:*}" == "$category" ]]; then cat_name="${entry#*:}" break fi done while true; do local menu_items=() for name in "${!PACKAGES[@]}"; do local pkg_category="${PKG_CATEGORY[$name]:-}" if [[ "$pkg_category" == "$category" ]]; then local status="✗ Not Installed" local display_text if is_package_installed "$name"; then status="✓ Installed" display_text="(.Y.) ${PKG_DESC[$name]:-No description} [$status]" else display_text="(_*_) ${PKG_DESC[$name]:-No description} [$status]" fi # Use buntage key as both the selection value and display text # This ensures whiptail returns the buntage key, not the description menu_items+=("$name" "$display_text") fi done if [[ ${#menu_items[@]} -eq 0 ]]; then ui_msg "No Buntages" "No buntages found in category: $cat_name" return fi # Sort menu items alphabetically by buntage name (keeping key-value pairs together) # Create temporary array for sorting local sorted_items=() local temp_array=() # Convert menu_items to sortable format (key:value pairs) for ((i=0; i<${#menu_items[@]}; i+=2)); do if [[ -n "${menu_items[i]}" ]]; then temp_array+=("${menu_items[i]}:${menu_items[i+1]}") fi done # Sort the temp array IFS=$'\n' temp_array=($(sort <<<"${temp_array[*]}")) unset IFS # Convert back to menu_items format for item in "${temp_array[@]}"; do local key="${item%%:*}" local value="${item#*:}" sorted_items+=("$key" "$value") done menu_items=("${sorted_items[@]}") menu_items+=("" "(_*_)") menu_items+=("zback" "(Z) ← Back to Categories") local choice choice=$(ui_menu "$cat_name" \ "Select a buntage to manage:" \ 24 80 14 "${menu_items[@]}") || break case "$choice" in back|zback|z|"") break ;; *) show_buntage_actions "$choice" ;; esac done } show_buntage_actions() { local name="$1" # Validate that the buntage exists in our arrays if [[ -z "${PACKAGES[$name]:-}" ]]; then log "ERROR: Buntage '$name' not found in PACKAGES array" return 1 fi local pkg="${PACKAGES[$name]}" local desc="${PKG_DESC[$name]:-No description available}" local method="${PKG_METHOD[$name]:-unknown}" local status="Not Installed" if is_package_installed "$name"; then status="✓ Installed" fi while true; do local info="Buntage: $name\nDescription: $desc\nInstall Method: $method\nBuntage Name: $pkg\nStatus: $status" local menu_items=() if is_package_installed "$name"; then menu_items+=("reinstall" "(.Y.) Reinstall Buntage") menu_items+=("remove" "(.Y.) Remove Buntage") menu_items+=("info" "(.Y.) Show Detailed Info") else menu_items+=("install" "(.Y.) Install Buntage") menu_items+=("info" "(.Y.) Show Detailed Info") fi menu_items+=("zback" "(Z) ← Back to Buntage List") local choice choice=$(ui_menu "Manage: $name" "$info" 20 80 10 "${menu_items[@]}") case "$choice" in install) install_package_with_choice "$name" ui_msg "Success" "$name installed successfully!" status="✓ Installed" ;; reinstall) remove_package "$name" install_package_with_choice "$name" ui_msg "Success" "$name reinstalled successfully!" ;; remove) remove_package "$name" status="Not Installed" ;; info) show_buntage_info "$name" ;; ""|back|zback|z) return ;; *) return ;; esac done } show_buntage_info() { local name="$1" local pkg="${PACKAGES[$name]}" local desc="${PKG_DESC[$name]}" local method="${PKG_METHOD[$name]}" local category="${PKG_CATEGORY[$name]}" local deps="${PKG_DEPS[$name]:-None}" local status="Not Installed" local details="" if is_package_installed "$name"; then status="✓ Installed" case "$method" in apt) if is_apt_installed "$pkg"; then local version=$(dpkg -l "$pkg" 2>/dev/null | awk '/^ii/ {print $3}') details="Version: $version\n" details+="Files: $(dpkg -L "$pkg" 2>/dev/null | wc -l) files installed\n" fi ;; snap) if is_snap_installed "$pkg"; then local snap_info=$(snap info "$pkg" 2>/dev/null | grep -E "^(installed|tracking)" | head -2) details="$snap_info\n" fi ;; flatpak) if is_flatpak_installed "$pkg"; then local flat_info=$(flatpak info "$pkg" 2>/dev/null | grep -E "^(Version|Branch|Ref)") details="$flat_info\n" fi ;; esac fi local info="BUNTAGE INFORMATION\n" info+="═══════════════════════════════════════\n\n" info+="Name: $name\n" info+="Description: $desc\n" info+="Category: $category\n" info+="Install Method: $method\n" info+="Buntage ID: $pkg\n" info+="Dependencies: $deps\n" info+="Status: $status\n\n" if [[ -n "$details" ]]; then info+="INSTALLED DETAILS\n" info+="═══════════════════════════════════════\n" info+="$details" fi ui_info "Buntage Info: $name" "$info" } show_system_info() { log "Starting show_system_info function" local info="SYSTEM INFORMATION\n" info+="═══════════════════════════════════════\n\n" # OS Info if [[ -f /etc/os-release ]]; then local os_name="Unknown" local os_version="N/A" # Safely parse os-release while IFS='=' read -r key value 2>/dev/null; do case "$key" in PRETTY_NAME) os_name="${value//\"/}" ;; VERSION) os_version="${value//\"/}" ;; esac done < /etc/os-release 2>/dev/null || true info+="OS: $os_name\n" info+="Version: $os_version\n" fi # Kernel local kernel_version kernel_version=$(uname -r 2>/dev/null) || kernel_version="Unknown" info+="Kernel: $kernel_version\n\n" # Hardware Info info+="HARDWARE INFORMATION\n" info+="─────────────────────────────────────\n" # CPU Info local cpu_info if [[ -f /proc/cpuinfo ]]; then cpu_info=$(grep "model name" /proc/cpuinfo 2>/dev/null | head -1 | cut -d: -f2 | sed 's/^ *//' 2>/dev/null) || cpu_info="Unknown" local cpu_cores cpu_cores=$(nproc 2>/dev/null) || cpu_cores="Unknown" info+="CPU: $cpu_info ($cpu_cores cores)\n" else info+="CPU: Information not available\n" fi # Memory Info if [[ -f /proc/meminfo ]]; then local mem_total mem_available mem_total=$(grep "MemTotal:" /proc/meminfo 2>/dev/null | awk '{print int($2/1024)}' 2>/dev/null) || mem_total="Unknown" mem_available=$(grep "MemAvailable:" /proc/meminfo 2>/dev/null | awk '{print int($2/1024)}' 2>/dev/null) || mem_available="Unknown" if [[ "$mem_total" != "Unknown" && "$mem_available" != "Unknown" ]]; then local mem_used=$((mem_total - mem_available)) info+="Memory: ${mem_used}MB used / ${mem_total}MB total\n" else info+="Memory: Information not available\n" fi else info+="Memory: Information not available\n" fi # Storage Information info+="STORAGE INFORMATION\n" info+="─────────────────────────────────────\n" # Root filesystem local disk_info disk_info=$(df -h / 2>/dev/null | tail -1 2>/dev/null | awk '{print $3 " used / " $2 " total (" $5 " full)"}' 2>/dev/null) || disk_info="Information not available" info+="Root (/): $disk_info\n" # All mounted filesystems (excluding special filesystems) local mount_info mount_info=$(df -h 2>/dev/null | grep -E '^/dev/' | grep -v -E '(tmpfs|udev|overlay)' | awk '{print $6 ": " $3 " used / " $2 " total (" $5 " full)"}' 2>/dev/null | tail -n +2 2>/dev/null) if [[ -n "$mount_info" ]]; then while IFS= read -r line; do [[ "$line" != *"/ :"* ]] && info+="$line\n" done <<< "$mount_info" fi # Swap information local swap_info swap_info=$(free -h 2>/dev/null | grep "Swap:" 2>/dev/null | awk '{print $3 " used / " $2 " total"}' 2>/dev/null) || swap_info="Not available" info+="Swap: $swap_info\n" # Disk usage summary local total_disk_count total_disk_count=$(lsblk -d 2>/dev/null | grep -c "disk" 2>/dev/null) || total_disk_count="Unknown" info+="Physical disks: $total_disk_count detected\n\n" # System Uptime and Load local uptime_info load_avg uptime_info=$(uptime -p 2>/dev/null | sed 's/up //' 2>/dev/null) || uptime_info="Unknown" load_avg=$(uptime 2>/dev/null | awk -F'load average:' '{print $2}' 2>/dev/null | sed 's/^ *//' 2>/dev/null) || load_avg="Unknown" info+="Uptime: $uptime_info\n" info+="Load average:$load_avg\n" # Process count local process_count process_count=$(ps aux 2>/dev/null | wc -l 2>/dev/null) || process_count="Unknown" [[ "$process_count" != "Unknown" ]] && process_count=$((process_count - 1)) # Subtract header line info+="Running processes: $process_count\n\n" # Buntage Managers info+="BUNTAGE MANAGERS\n" info+="─────────────────────────────────────\n" # APT detailed info local apt_count apt_upgradable apt_auto_removable apt_count=$(dpkg -l 2>/dev/null | grep -c "^ii" 2>/dev/null) || apt_count="0" apt_upgradable=$(apt list --upgradable 2>/dev/null | grep -c "upgradable" 2>/dev/null) || apt_upgradable="0" apt_auto_removable=$(apt autoremove --dry-run 2>/dev/null | grep -c "^Remv" 2>/dev/null) || apt_auto_removable="0" info+="APT: $apt_count installed" [[ "$apt_upgradable" != "0" ]] && info+=" ($apt_upgradable upgradable)" [[ "$apt_auto_removable" != "0" ]] && info+=" ($apt_auto_removable auto-removable)" info+="\n" # Snap detailed info if command -v snap &>/dev/null; then local snap_count snap_refreshable snap_count=$(snap list 2>/dev/null | tail -n +2 2>/dev/null | wc -l 2>/dev/null) || snap_count="0" snap_refreshable=$(snap refresh --list 2>/dev/null | tail -n +2 2>/dev/null | wc -l 2>/dev/null) || snap_refreshable="0" info+="Snap: $snap_count installed" [[ "$snap_refreshable" != "0" ]] && info+=" ($snap_refreshable refreshable)" info+="\n" else info+="Snap: Not available\n" fi # Flatpak detailed info if command -v flatpak &>/dev/null; then local flat_count flat_updates flat_remotes flat_count=$(flatpak list --app 2>/dev/null | wc -l 2>/dev/null) || flat_count="0" flat_updates=$(flatpak remote-ls --updates 2>/dev/null | wc -l 2>/dev/null) || flat_updates="0" flat_remotes=$(flatpak remotes 2>/dev/null | tail -n +2 2>/dev/null | wc -l 2>/dev/null) || flat_remotes="0" info+="Flatpak: $flat_count apps" [[ "$flat_updates" != "0" ]] && info+=" ($flat_updates updates)" info+=" [$flat_remotes remotes]\n" else info+="Flatpak: Not available\n" fi # Additional buntage managers if command -v pip3 &>/dev/null || command -v pip &>/dev/null; then local pip_count pip_count=$(pip3 list 2>/dev/null | tail -n +3 2>/dev/null | wc -l 2>/dev/null) || pip_count="0" [[ "$pip_count" == "0" ]] && pip_count=$(pip list 2>/dev/null | tail -n +3 2>/dev/null | wc -l 2>/dev/null) || pip_count="0" info+="Python (pip): $pip_count packages\n" fi if command -v npm &>/dev/null; then local npm_global_count npm_global_count=$(npm list -g --depth=0 2>/dev/null | grep -c "├──\|└──" 2>/dev/null) || npm_global_count="0" info+="Node.js (npm global): $npm_global_count packages\n" fi info+="\n" # Network Information info+="NETWORK INFORMATION\n" info+="─────────────────────────────────────\n" # Active network interfaces local active_interfaces active_interfaces=$(ip -o link show 2>/dev/null | awk -F': ' '$3 !~ /lo|docker|br-/ && $3 ~ /UP/ {print $2}' 2>/dev/null | tr '\n' ' ' 2>/dev/null) || active_interfaces="Unknown" [[ -n "$active_interfaces" ]] && info+="Active interfaces: $active_interfaces\n" || info+="Active interfaces: None detected\n" # IP addresses local primary_ip primary_ip=$(ip route get 8.8.8.8 2>/dev/null | awk '{print $7; exit}' 2>/dev/null) || primary_ip="Unknown" info+="Primary IP: $primary_ip\n" # Internet connectivity check local internet_status if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then internet_status="Connected" else internet_status="Disconnected" fi info+="Internet: $internet_status\n" # DNS servers local dns_servers dns_servers=$(grep "^nameserver" /etc/resolv.conf 2>/dev/null | awk '{print $2}' 2>/dev/null | tr '\n' ' ' 2>/dev/null) || dns_servers="Unknown" [[ -n "$dns_servers" ]] && info+="DNS servers: $dns_servers\n" || info+="DNS servers: Not configured\n" info+="\n" # Ultrabunt Stats info+="ULTRABUNT STATISTICS\n" info+="─────────────────────────────────────\n" local total_pkgs=${#PACKAGES[@]} local installed_count=0 # Safely count installed packages for name in "${!PACKAGES[@]}"; do if is_package_installed "$name" 2>/dev/null; then ((installed_count++)) 2>/dev/null || true fi done 2>/dev/null || true info+="Tracked packages: $total_pkgs\n" info+="Installed: $installed_count\n" info+="Available: $((total_pkgs - installed_count))\n\n" info+="Log file: $LOGFILE\n" info+="Backup dir: $BACKUP_DIR\n" log "Calling ui_info with system information" ui_info "System Information" "$info" || { log "ERROR: ui_info failed in show_system_info" return 1 } log "show_system_info completed successfully" return 0 } show_bulk_operations() { while true; do local choice choice=$(ui_menu "Bulk Operations" \ "Perform operations on multiple packages:" \ 20 80 10 \ "install-category" "(.Y.) Install All Packages in Category" \ "remove-category" "(.Y.) Remove All Packages in Category" \ "install-selected" "(.Y.) Install Selected Packages" \ "remove-selected" "(.Y.) Remove Selected Packages" \ "update-all" "(.Y.) Update All Installed Buntages" \ "cleanup" "(.Y.) Clean Buntage Cache & Remove Orphans" \ "export-list" "(.Y.) Export Installed Buntage List" \ "zback" "(Z) ← Back to Main Menu") || break case "$choice" in install-category) bulk_install_category ;; remove-category) bulk_remove_category ;; install-selected) bulk_install_selected ;; remove-selected) bulk_remove_selected ;; update-all) bulk_update_all ;; cleanup) bulk_cleanup ;; export-list) export_package_list ;; back|zback|"") break ;; esac done } bulk_install_category() { local menu_items=() for entry in "${CATEGORIES[@]}"; do local cat_id="${entry%%:*}" local cat_name="${entry#*:}" menu_items+=("$cat_id" "(.Y.) $cat_name") done local choice choice=$(ui_menu "Install Category" \ "Select category to install all packages:" \ 20 70 10 "${menu_items[@]}") || return ui_yesno "Confirm Install" "Install ALL packages in this category?\n\nThis may take several minutes." || return local installed=0 local failed=0 for name in "${!PACKAGES[@]}"; do if [[ "${PKG_CATEGORY[$name]}" == "$choice" ]]; then if ! is_package_installed "$name"; then log "Bulk installing: $name" if install_package "$name" 2>&1 | tee -a "$LOGFILE"; then ((installed++)) else ((failed++)) log_error "Failed to install: $name" fi fi fi done ui_msg "Bulk Install Complete" "Installed: $installed\nFailed: $failed\n\nCheck $LOGFILE for details." } bulk_remove_category() { local menu_items=() for entry in "${CATEGORIES[@]}"; do local cat_id="${entry%%:*}" local cat_name="${entry#*:}" menu_items+=("$cat_id" "(.Y.) $cat_name") done local choice choice=$(ui_menu "Remove Category" \ "Select category to remove all packages:" \ 20 70 10 "${menu_items[@]}") || return ui_yesno "Confirm Removal" "⚠️ WARNING ⚠️\n\nRemove ALL packages in this category?\n\nThis cannot be undone!" || return ui_yesno "Final Confirmation" "Are you ABSOLUTELY SURE?\n\nThis will remove all packages in the selected category." || return local removed=0 for name in "${!PACKAGES[@]}"; do if [[ "${PKG_CATEGORY[$name]}" == "$choice" ]]; then if is_package_installed "$name"; then log "Bulk removing: $name" local pkg="${PACKAGES[$name]}" local method="${PKG_METHOD[$name]}" case "$method" in apt) remove_apt_package "$pkg" ;; snap) remove_snap_package "$pkg" ;; flatpak) remove_flatpak_package "$pkg" ;; custom) case "$name" in docker) remove_docker ;; nodejs) remove_nodejs ;; vscode) remove_vscode ;; brave) remove_brave ;; sublime-text) remove_sublime ;; esac ;; esac ((removed++)) fi fi done ui_msg "Bulk Remove Complete" "Removed: $removed packages\n\nCheck $LOGFILE for details." } bulk_install_selected() { local checklist_items=() for name in "${!PACKAGES[@]}"; do if ! is_package_installed "$name"; then local desc="${PKG_DESC[$name]}" local cat="${PKG_CATEGORY[$name]}" checklist_items+=("$name" "[$cat] $desc" "OFF") fi done if [[ ${#checklist_items[@]} -eq 0 ]]; then ui_msg "Nothing to Install" "All tracked packages are already installed!" return fi # Sort checklist IFS=$'\n' checklist_items=($(sort -t$'\t' -k1 <<<"${checklist_items[*]}")) unset IFS local selected selected=$(ui_checklist "Select Packages to Install" \ "Choose packages to install (Space to select, Enter to confirm):" \ 24 80 15 "${checklist_items[@]}") || return if [[ -z "$selected" ]]; then return fi local pkg_list=$(echo "$selected" | tr -d '"') local count=$(echo "$pkg_list" | wc -w) ui_yesno "Confirm Installation" "Install $count selected packages?" || return local installed=0 local failed=0 for name in $pkg_list; do log "Installing selected: $name" if install_package "$name" 2>&1 | tee -a "$LOGFILE"; then ((installed++)) else ((failed++)) log_error "Failed to install: $name" fi done ui_msg "Installation Complete" "Installed: $installed\nFailed: $failed\n\nCheck $LOGFILE for details." } bulk_remove_selected() { local checklist_items=() for name in "${!PACKAGES[@]}"; do if is_package_installed "$name" "true"; then local desc="${PKG_DESC[$name]}" local cat="${PKG_CATEGORY[$name]}" checklist_items+=("$name" "[$cat] $desc" "OFF") fi done if [[ ${#checklist_items[@]} -eq 0 ]]; then ui_msg "Nothing to Remove" "No tracked packages are currently installed!" return fi # Sort checklist IFS=$'\n' checklist_items=($(sort -t$'\t' -k1 <<<"${checklist_items[*]}")) unset IFS local selected selected=$(ui_checklist "Select Packages to Remove" \ "Choose packages to remove (Space to select, Enter to confirm):" \ 24 80 15 "${checklist_items[@]}") || return if [[ -z "$selected" ]]; then return fi local pkg_list=$(echo "$selected" | tr -d '"') local count=$(echo "$pkg_list" | wc -w) ui_yesno "Confirm Removal" "⚠️ Remove $count selected packages?" || return local removed=0 for name in $pkg_list; do log "Removing selected: $name" local pkg="${PACKAGES[$name]}" local method="${PKG_METHOD[$name]}" case "$method" in apt) remove_apt_package "$pkg" ;; snap) remove_snap_package "$pkg" ;; flatpak) remove_flatpak_package "$pkg" ;; custom) case "$name" in docker) remove_docker ;; nodejs) remove_nodejs ;; vscode) remove_vscode ;; brave) remove_brave ;; sublime-text) remove_sublime ;; esac ;; esac ((removed++)) done ui_msg "Removal Complete" "Removed: $removed packages\n\nCheck $LOGFILE for details." } bulk_update_all() { ui_yesno "Update All" "Update all buntage managers and installed buntages?" || return log "Starting system update..." # APT log "Updating APT buntages..." sudo apt-get update -y 2>&1 | tee -a "$LOGFILE" sudo apt-get upgrade -y 2>&1 | tee -a "$LOGFILE" # Snap if command -v snap &>/dev/null; then log "Updating Snap buntages..." sudo snap refresh 2>&1 | tee -a "$LOGFILE" fi # Flatpak if command -v flatpak &>/dev/null; then log "Updating Flatpak apps..." flatpak update -y 2>&1 | tee -a "$LOGFILE" fi ui_msg "Update Complete" "All buntage managers updated!\n\nCheck $LOGFILE for details." } bulk_cleanup() { ui_yesno "System Cleanup" "This will:\n- Remove unused buntages\n- Clean buntage cache\n- Remove old kernels\n\nContinue?" || return log "Starting system cleanup..." # APT cleanup log "Cleaning APT cache..." sudo apt-get autoremove -y 2>&1 | tee -a "$LOGFILE" sudo apt-get autoclean -y 2>&1 | tee -a "$LOGFILE" sudo apt-get clean 2>&1 | tee -a "$LOGFILE" # Snap cleanup if command -v snap &>/dev/null; then log "Removing old snap revisions..." sudo snap list --all | awk '/disabled/{print $1, $3}' | \ while read snapname revision; do sudo snap remove "$snapname" --revision="$revision" 2>&1 | tee -a "$LOGFILE" done fi # Flatpak cleanup if command -v flatpak &>/dev/null; then log "Cleaning Flatpak cache..." flatpak uninstall --unused -y 2>&1 | tee -a "$LOGFILE" fi # Journal cleanup log "Cleaning system journal..." sudo journalctl --vacuum-time=7d 2>&1 | tee -a "$LOGFILE" ui_msg "Cleanup Complete" "System cleanup finished!\n\nCheck $LOGFILE for details." } export_package_list() { local export_file="/tmp/ultrabunt-packages-$(date +%Y%m%d-%H%M%S).txt" { echo "ULTRABUNT BUNTAGE LIST - $(date)" echo "═══════════════════════════════════════════════════════════" echo "" for entry in "${CATEGORIES[@]}"; do local cat_id="${entry%%:*}" local cat_name="${entry#*:}" echo "" echo "[$cat_name]" echo "─────────────────────────────────────────────────────────" for name in "${!PACKAGES[@]}"; do if [[ "${PKG_CATEGORY[$name]}" == "$cat_id" ]]; then local status="NOT INSTALLED" if is_package_installed "$name" "true"; then status="INSTALLED" fi printf "%-20s %-15s %s\n" "$name" "[$status]" "${PKG_DESC[$name]}" fi done done echo "" echo "═══════════════════════════════════════════════════════════" echo "Export completed: $(date)" } > "$export_file" ui_msg "Export Complete" "Buntage list exported to:\n\n$export_file" } # ============================================================================== # ADVANCED FEATURES # ============================================================================== install_oh_my_zsh() { if [[ -d "$HOME/.oh-my-zsh" ]]; then ui_msg "Already Installed" "Oh My Zsh is already installed!" return fi ui_yesno "Install Oh My Zsh?" "Install Oh My Zsh with Powerlevel10k theme?" || return if ! is_package_installed "zsh"; then install_package "zsh" fi log "Installing Oh My Zsh..." sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended 2>&1 | tee -a "$LOGFILE" log "Installing Powerlevel10k..." git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k" 2>&1 | tee -a "$LOGFILE" sed -i 's/ZSH_THEME=.*/ZSH_THEME="powerlevel10k\/powerlevel10k"/' "$HOME/.zshrc" ui_msg "Oh My Zsh Installed" "Oh My Zsh with Powerlevel10k installed!\n\nRun 'zsh' to start configuration." } # ============================================================================== # WORDPRESS INSTALLATION SYSTEM # ============================================================================== show_wordpress_setup_menu() { log "Entering show_wordpress_setup_menu function" while true; do local menu_items=( "quick-nginx" "(.Y.) 🚀 Quick Setup (Nginx + WordPress)" "quick-apache" "(.Y.) 🚀 Quick Setup (Apache + WordPress)" "" "(_*_)" "custom-nginx" "(.Y.) ⚙️ Custom Nginx Setup" "custom-apache" "(.Y.) ⚙️ Custom Apache Setup" "custom-db-nginx" "(.Y.) 🔧 Custom Database + Nginx Setup" "custom-db-apache" "(.Y.) 🔧 Custom Database + Apache Setup" "" "(_*_)" "ssl-setup" "(.Y.) 🔒 Add SSL Certificate (Let's Encrypt)" "wp-security" "(.Y.) 🛡️ WordPress Security Hardening" "" "(_*_)" "status" "(.Y.) 📊 Show WordPress Sites Status" "zback" "(Z) ← Back to Main Menu" ) local choice choice=$(ui_menu "WordPress Installation" \ "Choose your WordPress installation method:\n\n🚀 Quick Setup: Auto-generated database credentials\n⚙️ Custom Setup: Choose site directory\n🔧 Custom Database: Choose database name, user, and password" \ 22 90 14 "${menu_items[@]}") || break case "$choice" in quick-nginx) wordpress_quick_setup "nginx" ;; quick-apache) wordpress_quick_setup "apache" ;; custom-nginx) wordpress_custom_setup "nginx" ;; custom-apache) wordpress_custom_setup "apache" ;; custom-db-nginx) wordpress_custom_database_setup "nginx" ;; custom-db-apache) wordpress_custom_database_setup "apache" ;; ssl-setup) wordpress_ssl_setup ;; wp-security) wordpress_security_hardening ;; status) show_wordpress_status ;; back|zback|z|"") break ;; esac done log "Exiting show_wordpress_setup_menu function" } wordpress_quick_setup() { local web_server="$1" log "Starting WordPress quick setup with $web_server" local info="WordPress Quick Setup ($web_server)\n\n" info+="This will install and configure:\n" info+="• $web_server web server\n" info+="• PHP and required extensions\n" info+="• MariaDB database server\n" info+="• Latest WordPress\n" info+="• Basic security configuration\n\n" info+="Perfect for new Ubuntu installations!\n\n" info+="Continue with installation?" if ! ui_yesno "Quick WordPress Setup" "$info"; then return fi # Install prerequisites ui_msg "Step 1/6" "Installing WordPress prerequisites...\n\nThis includes:\n• Web server ($web_server)\n• PHP and extensions\n• MariaDB database\n• Required system packages\n\n⏳ This process is automatic - please wait..." install_wordpress_prerequisites "$web_server" || { ui_msg "Installation Failed" "Failed to install prerequisites. Check the logs for details." return 1 } log "Step 1 Complete: Prerequisites installed successfully" # Get domain name with validation ui_msg "Step 2/6" "Configuring domain settings...\n\nPlease enter your domain name or use 'localhost' for local development.\n\n👆 User input required below..." local domain while true; do domain=$(ui_input "Domain Name" "Enter your domain name (or 'localhost' for local development):" "localhost") || return domain=${domain:-localhost} # Validate domain format if [[ "$domain" == "localhost" ]] || [[ "$domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$ ]]; then break else ui_msg "Invalid Domain" "Please enter a valid domain name or 'localhost'.\n\nExamples:\n• localhost\n• mysite.local\n• example.com\n• subdomain.example.com" fi done ui_msg "Step 2 Complete" "✅ Domain configured: $domain\n\n• Site URL: http://$domain\n• Directory: /var/www/$domain\n• Admin URL: http://$domain/wp-admin/" # Confirm installation details local confirm_msg="WordPress Installation Summary\n\n" confirm_msg+="Web Server: $web_server\n" confirm_msg+="Domain: $domain\n" confirm_msg+="Site Directory: /var/www/$domain\n" confirm_msg+="Database: Auto-generated secure credentials\n\n" confirm_msg+="Proceed with installation?" if ! ui_yesno "Confirm Installation" "$confirm_msg"; then return fi # Create the WordPress database (this now handles MariaDB root access internally) ui_msg "Step 3/6" "Creating WordPress database...\n\n• Verifying MariaDB root access\n• Creating WordPress database\n• Setting up database user\n• Configuring permissions\n\n⏳ This process is automatic - please wait..." local db_info db_info=$(setup_wordpress_database "$domain") || { ui_msg "Database Creation Failed" "❌ Failed to create WordPress database.\n\nPossible causes:\n• MariaDB authentication issues\n• Insufficient privileges\n• Database service problems\n\nCheck the logs for details." return 1 } # Parse and display database info IFS=':' read -r db_name db_user db_pass <<< "$db_info" # Debug logging for database info log "Database info received: '$db_info'" log "Parsed - DB Name: '$db_name', DB User: '$db_user', DB Pass: '${db_pass:0:4}...'" # Validate that we have all required database info if [[ -z "$db_name" || -z "$db_user" || -z "$db_pass" ]]; then log "ERROR: Incomplete database information - Name: '$db_name', User: '$db_user', Pass: '${db_pass:+[SET]}'" ui_msg "Database Error" "❌ Database creation returned incomplete information.\n\nReceived: '$db_info'\n\nPlease check the logs for details." return 1 fi ui_msg "Step 3 Complete" "✅ Database created successfully!\n\n• Database Name: $db_name\n• Database User: $db_user\n• Database Password: $db_pass\n• Host: localhost\n\n🔒 Credentials saved for your reference." # Download and configure WordPress ui_msg "Step 4/6" "Downloading and configuring WordPress...\n\n• Downloading latest WordPress\n• Extracting files to /var/www/$domain\n• Configuring wp-config.php\n• Setting file permissions\n\n⏳ This process is automatic - please wait..." setup_wordpress_files "$domain" "$db_info" || { ui_msg "WordPress Setup Error" "Failed to download or configure WordPress files." return 1 } ui_msg "Step 4 Complete" "✅ WordPress files configured!\n\n• Latest WordPress: Downloaded\n• Configuration: Complete\n• File permissions: Set\n• Upload directory: Created" # Configure web server ui_msg "Step 5/6" "Configuring $web_server web server...\n\n• Creating server configuration\n• Setting up PHP processing\n• Configuring security headers\n• Enabling site\n\n⏳ This process is automatic - please wait..." if [[ "$web_server" == "nginx" ]]; then configure_nginx_wordpress "$domain" || { ui_msg "Nginx Configuration Error" "Failed to configure Nginx. Check configuration syntax." return 1 } else configure_apache_wordpress "$domain" || { ui_msg "Apache Configuration Error" "Failed to configure Apache. Check configuration syntax." return 1 } fi ui_msg "Step 5 Complete" "✅ $web_server configured successfully!\n\n• Server block: Created\n• PHP processing: Enabled\n• Security headers: Set\n• Site: Active and running" # Show completion message ui_msg "Step 6/6" "Finalizing WordPress installation...\n\nPreparing completion summary with all details.\n\n⏳ This process is automatic - please wait..." show_wordpress_completion "$domain" "$db_info" ui_msg "Installation Complete!" "🎉 WordPress installation finished successfully!\n\nYour site is ready at: http://$domain\n\nNext steps:\n1. Visit your site to complete WordPress setup\n2. Create your admin account\n3. Choose your theme and plugins\n\nAll details have been saved and displayed in the completion screen." } wordpress_custom_setup() { local web_server="$1" log "Starting WordPress custom setup with $web_server" local info="WordPress Custom Setup ($web_server)\n\n" info+="This allows you to customize:\n" info+="• Domain and directory settings\n" info+="• Database configuration\n" info+="• PHP settings\n" info+="• Security options\n\n" info+="Continue with custom setup?" if ! ui_yesno "Custom WordPress Setup" "$info"; then return fi # Check if prerequisites are installed if ! check_wordpress_prerequisites "$web_server"; then if ui_yesno "Install Prerequisites" "Required packages are missing. Install them now?"; then install_wordpress_prerequisites "$web_server" else ui_msg "Setup Cancelled" "Cannot proceed without required packages." return fi fi # Custom configuration options local domain domain=$(ui_input "Domain Name" "Enter your domain name:" "localhost") || return domain=${domain:-localhost} local site_dir site_dir=$(ui_input "Site Directory" "WordPress installation directory:" "/var/www/$domain") || return site_dir=${site_dir:-/var/www/$domain} # Database setup with custom options local db_info db_info=$(setup_wordpress_database_custom "$domain") # WordPress setup setup_wordpress_files_custom "$domain" "$site_dir" "$db_info" # Web server configuration if [[ "$web_server" == "nginx" ]]; then configure_nginx_wordpress_custom "$domain" "$site_dir" else configure_apache_wordpress_custom "$domain" "$site_dir" fi # Show completion show_wordpress_completion "$domain" "$db_info" } wordpress_custom_database_setup() { local web_server="$1" log "Starting WordPress custom database setup with $web_server" local info="WordPress Custom Database Setup ($web_server)\n\n" info+="🔧 This setup allows you to:\n" info+="• Choose your own database name\n" info+="• Set custom database username\n" info+="• Define your own password\n" info+="• Understand user management\n\n" info+="🔐 You'll learn about:\n" info+="• ROOT user (administrative access)\n" info+="• WordPress user (application access)\n" info+="• Security best practices\n\n" info+="Continue with custom database setup?" if ! ui_yesno "Custom Database Setup" "$info"; then return fi # Check if prerequisites are installed if ! check_wordpress_prerequisites "$web_server"; then if ui_yesno "Install Prerequisites" "Required packages are missing. Install them now?"; then install_wordpress_prerequisites "$web_server" else ui_msg "Setup Cancelled" "Cannot proceed without required packages." return fi fi # Domain configuration local domain domain=$(ui_input "Domain Name" "Enter your domain name:" "localhost") || return domain=${domain:-localhost} # Custom database setup with detailed explanations local db_info db_info=$(setup_wordpress_database_custom "$domain") || { ui_msg "Database Setup Failed" "Database setup was cancelled or failed. Please try again." return 1 } # Parse database info for confirmation IFS=':' read -r db_name db_user db_pass <<< "$db_info" # Confirm setup before proceeding local confirm_msg="🔐 Database Configuration Summary:\n\n" confirm_msg+="• Database Name: $db_name\n" confirm_msg+="• Database User: $db_user\n" confirm_msg+="• Database Password: $db_pass\n" confirm_msg+="• Host: localhost\n\n" confirm_msg+="📁 WordPress will be installed to: /var/www/$domain\n\n" confirm_msg+="Continue with WordPress installation?" if ! ui_yesno "Confirm Installation" "$confirm_msg"; then return fi # WordPress file setup ui_msg "Step 2/4" "Setting up WordPress files...\n\nDownloading and configuring WordPress with your custom database settings.\n\n⏳ This process is automatic - please wait..." setup_wordpress_files "$domain" "$db_info" || { ui_msg "WordPress Setup Error" "Failed to download or configure WordPress files." return 1 } # Web server configuration ui_msg "Step 3/4" "Configuring web server...\n\nSetting up $web_server virtual host for $domain.\n\n⏳ This process is automatic - please wait..." if [[ "$web_server" == "nginx" ]]; then configure_nginx_wordpress "$domain" || { ui_msg "Nginx Configuration Error" "Failed to configure Nginx. Please check the logs." return 1 } else configure_apache_wordpress "$domain" || { ui_msg "Apache Configuration Error" "Failed to configure Apache. Please check the logs." return 1 } fi # Final setup and permissions ui_msg "Step 4/4" "Finalizing installation...\n\nSetting file permissions and restarting services.\n\n⏳ This process is automatic - please wait..." finalize_wordpress_setup "$domain" "$web_server" || { ui_msg "Finalization Error" "WordPress setup completed but some final steps failed." } # Show completion with custom database details show_wordpress_completion_custom "$domain" "$db_info" "$web_server" } show_wordpress_completion_custom() { local domain="$1" local db_info="$2" local web_server="$3" # Parse database info IFS=':' read -r db_name db_user db_pass <<< "$db_info" local completion_msg="🎉 WordPress Installation Complete!\n\n" completion_msg+="🌐 Website Details:\n" completion_msg+="• URL: http://$domain\n" completion_msg+="• Directory: /var/www/$domain\n" completion_msg+="• Web Server: $web_server\n\n" completion_msg+="🔐 Custom Database Configuration:\n" completion_msg+="• Database Name: $db_name\n" completion_msg+="• Database User: $db_user (WordPress-specific)\n" completion_msg+="• Database Password: $db_pass\n" completion_msg+="• Host: localhost\n\n" completion_msg+="👤 User Management Summary:\n" completion_msg+="• ROOT USER: Administrative MariaDB access\n" completion_msg+="• WORDPRESS USER: '$db_user' (limited to '$db_name' database)\n\n" completion_msg+="🔧 Next Steps:\n" completion_msg+="1. Visit http://$domain to complete WordPress setup\n" completion_msg+="2. Use the database credentials above during WordPress installation\n" completion_msg+="3. Consider adding SSL certificate for security\n\n" completion_msg+="📝 Database credentials are saved in wp-config.php" ui_msg "Installation Complete" "$completion_msg" } install_wordpress_prerequisites() { local web_server="$1" log "Installing WordPress prerequisites for $web_server" ui_msg "Installing Prerequisites" "Installing required packages for WordPress...\n\nThis may take a few minutes.\n\n⏳ This process is automatic - please wait..." # Common packages local packages=("mariadb" "php-mysql" "php-curl" "php-gd" "php-xml" "php-mbstring" "php-zip") # Web server specific packages if [[ "$web_server" == "nginx" ]]; then packages+=("nginx" "php-fpm") else packages+=("apache2" "libapache2-mod-php") fi # Install packages for pkg in "${packages[@]}"; do if ! is_package_installed "$pkg"; then log "Installing $pkg..." install_package "$pkg" || { ui_msg "Installation Error" "Failed to install $pkg. Please check the logs." return 1 } fi done # Start and enable services if [[ "$web_server" == "nginx" ]]; then sudo systemctl enable nginx php${PHP_VER}-fpm mariadb 2>&1 | tee -a "$LOGFILE" sudo systemctl start nginx php${PHP_VER}-fpm mariadb 2>&1 | tee -a "$LOGFILE" # Verify PHP-FPM is running if ! systemctl is-active --quiet php${PHP_VER}-fpm; then log "ERROR: PHP-FPM failed to start" ui_msg "Service Error" "PHP-FPM failed to start. This will cause 500 errors.\n\nTrying to restart..." sudo systemctl restart php${PHP_VER}-fpm sleep 2 if ! systemctl is-active --quiet php${PHP_VER}-fpm; then ui_msg "Critical Error" "PHP-FPM still not running. Check: sudo systemctl status php${PHP_VER}-fpm" return 1 fi fi else sudo systemctl enable apache2 mariadb 2>&1 | tee -a "$LOGFILE" sudo systemctl start apache2 mariadb 2>&1 | tee -a "$LOGFILE" sudo a2enmod rewrite 2>&1 | tee -a "$LOGFILE" fi # Secure MariaDB installation setup_mariadb_security log "WordPress prerequisites installed successfully" } check_wordpress_prerequisites() { local web_server="$1" local required_packages=("mariadb" "php-mysql" "php-curl" "php-gd") if [[ "$web_server" == "nginx" ]]; then required_packages+=("nginx" "php-fpm") else required_packages+=("apache2" "libapache2-mod-php") fi for pkg in "${required_packages[@]}"; do if ! is_package_installed "$pkg"; then return 1 fi done return 0 } setup_mariadb_security() { log "Setting up MariaDB security" # Check if MariaDB is already configured local existing_pass="" if [[ -f "/root/.mysql_root_password" ]]; then existing_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null) log "Found existing MariaDB password file" # Test if existing password works if [[ -n "$existing_pass" ]] && mysql -u root -p"$existing_pass" -e "SELECT 1;" >/dev/null 2>&1; then log "Existing MariaDB password is valid, skipping setup" return 0 else log "Existing password doesn't work, reconfiguring MariaDB" fi fi # Generate random root password local root_pass root_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20) # First, check if MariaDB is using unix_socket authentication local auth_plugin auth_plugin=$(sudo mysql -e "SELECT plugin FROM mysql.user WHERE User='root' AND Host='localhost';" 2>/dev/null | tail -n +2 | head -n 1) # Configure root user for password authentication if [[ "$auth_plugin" == "unix_socket" ]] || [[ -z "$auth_plugin" ]]; then # Change from unix_socket to mysql_native_password sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${root_pass}';" 2>/dev/null || { # If that fails, try creating the user sudo mysql -e "CREATE USER IF NOT EXISTS 'root'@'localhost' IDENTIFIED BY '${root_pass}';" 2>/dev/null || true sudo mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;" 2>/dev/null || true } else # For existing installations, we might need to reset the password log "Attempting to reset existing MariaDB root password" # Try multiple methods to set the password if sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$root_pass';" 2>/dev/null; then log "Password set using ALTER USER" elif sudo mysql -e "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('$root_pass');" 2>/dev/null; then log "Password set using SET PASSWORD" elif sudo mysql -e "UPDATE mysql.user SET Password=PASSWORD('$root_pass') WHERE User='root' AND Host='localhost';" 2>/dev/null; then log "Password set using UPDATE" else log "All password setting methods failed, trying to reset authentication plugin" sudo mysql -e "UPDATE mysql.user SET plugin='mysql_native_password', Password=PASSWORD('$root_pass') WHERE User='root' AND Host='localhost';" 2>/dev/null fi fi # Secure installation - clean up sudo mysql -e "DELETE FROM mysql.user WHERE User='';" 2>/dev/null || true sudo mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" 2>/dev/null || true sudo mysql -e "DROP DATABASE IF EXISTS test;" 2>/dev/null || true sudo mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" 2>/dev/null || true sudo mysql -e "FLUSH PRIVILEGES;" 2>/dev/null || true # Verify the password works if mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then # Save root password and display to user echo "$root_pass" | sudo tee /root/.mysql_root_password > /dev/null sudo chmod 600 /root/.mysql_root_password # Display password to user immediately ui_msg "MariaDB Root Password Generated" "🔐 MariaDB Root Password Created!\n\n📋 ROOT PASSWORD: $root_pass\n\n📝 Important:\n• This password has been saved to /root/.mysql_root_password\n• Copy this password now for your records\n• You can view it later in Database Management > Show Credentials\n\n🔧 Access Methods:\n• Command line: mysql -u root -p'$root_pass'\n• Sudo access: sudo mysql" # Log credentials (non-blocking) log "MariaDB Root Password Set: $root_pass" log "Password saved to: /root/.mysql_root_password" log "Password displayed to user during setup" else log "MariaDB Setup Warning: Password authentication setup may have failed" fi log "MariaDB security setup completed" } setup_wordpress_database() { local domain="$1" # Prompt user for database configuration ui_msg "WordPress Database Configuration" "🔐 WordPress Database Setup\n\nYou can customize your database settings or use auto-generated values.\n\nCustom settings allow you to:\n• Choose meaningful database names\n• Set your own passwords\n• Maintain consistent naming\n\nAuto-generated settings provide:\n• Unique, secure credentials\n• No naming conflicts\n• Quick setup" >/dev/tty local use_custom=false if ui_yesno "Database Configuration" "Do you want to customize database settings?\n\n• YES: Choose database name, user, and password\n• NO: Use auto-generated secure credentials" >/dev/tty; then use_custom=true fi local db_name db_user db_pass if [[ "$use_custom" == "true" ]]; then # Custom database configuration ui_msg "Custom Database Setup" "🔧 Custom Database Configuration\n\nPlease provide your preferred database settings.\n\nDatabase names should:\n• Be descriptive (e.g., mysite_wp, blog_db)\n• Use only letters, numbers, and underscores\n• Be unique on this server" >/dev/tty # Get database name while true; do db_name=$(ui_input "Database Name" "Enter database name for WordPress:" "wp_${domain//[^a-zA-Z0-9]/_}") || return db_name=${db_name:-"wp_${domain//[^a-zA-Z0-9]/_}"} # Validate database name if [[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] && [[ ${#db_name} -le 64 ]]; then break else ui_msg "Invalid Database Name" "Database name must:\n• Contain only letters, numbers, and underscores\n• Be 64 characters or less\n• Not be empty\n\nExample: wp_mysite, blog_database" >/dev/tty fi done # Get database user while true; do db_user=$(ui_input "Database User" "Enter database username:" "wp_user_${domain//[^a-zA-Z0-9]/_}") || return db_user=${db_user:-"wp_user_${domain//[^a-zA-Z0-9]/_}"} # Validate username if [[ "$db_user" =~ ^[a-zA-Z0-9_]+$ ]] && [[ ${#db_user} -le 32 ]]; then break else ui_msg "Invalid Username" "Username must:\n• Contain only letters, numbers, and underscores\n• Be 32 characters or less\n• Not be empty\n\nExample: wp_user, mysite_user" >/dev/tty fi done # Get database password db_pass=$(ui_input "Database Password" "Enter database password (leave empty for auto-generated):" "") || return if [[ -z "$db_pass" ]]; then db_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20) ui_msg "Auto-Generated Password" "🔐 Password auto-generated for security:\n\nPassword: $db_pass\n\n📝 Please save this password - you'll need it for WordPress setup!" >/dev/tty fi else # Auto-generated configuration db_name="wp_${domain//[^a-zA-Z0-9]/_}_$(date +%s)" db_user="wpuser_$(date +%s)" db_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20) ui_msg "Auto-Generated Credentials" "🔐 Secure database credentials generated:\n\n• Database: $db_name\n• User: $db_user\n• Password: $db_pass\n\n📝 These will be automatically configured in WordPress!" >/dev/tty fi log "Creating WordPress database: $db_name for domain: $domain" log "Database user: $db_user" # Check MariaDB service first if ! systemctl is-active --quiet mariadb; then log "ERROR: MariaDB service is not running" return 1 fi # Ensure MariaDB root access for database operations local root_pass local auth_method="sudo" ui_msg "Database Access" "🔐 Checking MariaDB root access...\n\nThis is required to create the WordPress database and user." >/dev/tty # First, try to get saved root password if [[ -f "/root/.mysql_root_password" ]]; then root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null) if [[ -n "$root_pass" ]]; then # Test if password authentication works if mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then auth_method="password" log "Using saved root password for database operations" ui_msg "Root Access" "✅ MariaDB root access verified using saved credentials." >/dev/tty else log "Saved password authentication failed" root_pass="" fi fi fi # If password auth failed, try sudo if [[ "$auth_method" != "password" ]]; then if sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then auth_method="sudo" log "Using sudo authentication for database operations" ui_msg "Root Access" "✅ MariaDB root access verified using sudo." >/dev/tty else log "Sudo authentication also failed" # Need to prompt for root password ui_msg "Root Password Required" "🔑 MariaDB root access is required to create the WordPress database.\n\nPlease enter the MariaDB root password, or we can attempt to reset it." >/dev/tty local max_attempts=3 local attempt=1 while [[ $attempt -le $max_attempts ]]; do root_pass=$(ui_input "MariaDB Root Password" "Enter MariaDB root password (attempt $attempt/$max_attempts):" "" "password") || { if ui_yesno "Reset Root Password" "Would you like to attempt resetting the MariaDB root password instead?\n\n⚠️ This will stop MariaDB temporarily." >/dev/tty; then if reset_mariadb_root_password; then # Try to get the new password if [[ -f "/root/.mysql_root_password" ]]; then root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null) if mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then auth_method="password" ui_msg "Root Access" "✅ MariaDB root password reset successfully." >/dev/tty break fi fi fi ui_msg "Reset Failed" "❌ Failed to reset MariaDB root password. Please try entering the password manually." >/dev/tty continue else return 1 fi } if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then auth_method="password" # Save the working password echo "$root_pass" | sudo tee /root/.mysql_root_password >/dev/null sudo chmod 600 /root/.mysql_root_password ui_msg "Root Access" "✅ MariaDB root access verified with provided password." >/dev/tty break else ui_msg "Authentication Failed" "❌ Invalid password. Please try again." >/dev/tty ((attempt++)) fi done if [[ $attempt -gt $max_attempts ]]; then ui_msg "Access Failed" "❌ Failed to establish MariaDB root access after $max_attempts attempts.\n\nWordPress installation cannot continue without database access." >/dev/tty return 1 fi fi fi log "Authentication method: $auth_method" # Function to execute MySQL commands with proper error handling execute_mysql_cmd() { local cmd="$1" local operation="$2" local result if [[ "$auth_method" == "password" ]]; then if result=$(mysql -u root -p"$root_pass" -e "$cmd" 2>&1); then log "SUCCESS: $operation completed with password auth" return 0 else log "FAILED: $operation with password auth: $result" # Try sudo fallback if result=$(sudo mysql -e "$cmd" 2>&1); then log "SUCCESS: $operation completed with sudo fallback" return 0 else log "FAILED: $operation with sudo fallback: $result" return 1 fi fi else if result=$(sudo mysql -e "$cmd" 2>&1); then log "SUCCESS: $operation completed with sudo" return 0 else log "FAILED: $operation with sudo: $result" return 1 fi fi } # Create database log "Creating database: $db_name" execute_mysql_cmd "CREATE DATABASE IF NOT EXISTS \`${db_name}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" "Database creation" || { log "ERROR: Failed to create database $db_name" return 1 } # Create user log "Creating database user: $db_user" execute_mysql_cmd "CREATE USER IF NOT EXISTS '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';" "User creation" || { log "ERROR: Failed to create user $db_user" return 1 } # Grant privileges log "Granting privileges to user: $db_user on database: $db_name" execute_mysql_cmd "GRANT ALL PRIVILEGES ON \`${db_name}\`.* TO '${db_user}'@'localhost';" "Grant privileges" || { log "ERROR: Failed to grant privileges" return 1 } # Flush privileges log "Flushing privileges" execute_mysql_cmd "FLUSH PRIVILEGES;" "Flush privileges" || { log "ERROR: Failed to flush privileges" return 1 } # Verify database creation log "Verifying database creation" local db_exists if [[ "$auth_method" == "password" ]]; then db_exists=$(mysql -u root -p"$root_pass" -e "SHOW DATABASES LIKE '${db_name}';" 2>/dev/null | tail -n +2) else db_exists=$(sudo mysql -e "SHOW DATABASES LIKE '${db_name}';" 2>/dev/null | tail -n +2) fi if [[ -n "$db_exists" ]]; then log "SUCCESS: Database $db_name verified to exist" # Test WordPress user connection log "Testing WordPress user connection" if mysql -u "$db_user" -p"$db_pass" -e "USE \`${db_name}\`; SELECT 1;" >/dev/null 2>&1; then log "SUCCESS: WordPress user can connect to database" echo "${db_name}:${db_user}:${db_pass}" return 0 else log "ERROR: WordPress user cannot connect to database" return 1 fi else log "ERROR: Database $db_name was not created or cannot be verified" return 1 fi } setup_wordpress_database_custom() { local domain="$1" # Display user management explanation ui_msg "Database User Management" "🔐 WordPress Database Setup\n\n📋 User Management Explanation:\n\n• ROOT USER: Administrative access to MariaDB\n - Username: root\n - Used for: Creating databases, managing users\n - Authentication: Password or sudo access\n\n• WORDPRESS USER: Dedicated user for each WordPress site\n - Username: Custom (e.g., wp_user_sitename)\n - Used for: WordPress application database access\n - Authentication: Password only\n - Privileges: Limited to specific database only\n\n🎯 This setup follows security best practices:\n- Separation of administrative and application access\n- Minimal privileges for WordPress users\n- Unique credentials per site" local db_name db_name=$(ui_input "Database Name" "Enter WordPress database name:\n\n💡 Suggestions:\n• wp_${domain//[^a-zA-Z0-9]/_}\n• ${domain//[^a-zA-Z0-9]/_}_wp\n• custom_name_wp" "wp_${domain//[^a-zA-Z0-9]/_}") || return # Validate database name if [[ ! "$db_name" =~ ^[a-zA-Z0-9_]+$ ]]; then ui_msg "Invalid Name" "Database name can only contain letters, numbers, and underscores." return 1 fi local db_user db_user=$(ui_input "Database User" "Enter WordPress database username:\n\n💡 Suggestions:\n• wp_user_${domain//[^a-zA-Z0-9]/_}\n• ${domain//[^a-zA-Z0-9]/_}_user\n• wpuser_$(date +%s)" "wp_user_${domain//[^a-zA-Z0-9]/_}") || return # Validate username if [[ ! "$db_user" =~ ^[a-zA-Z0-9_]+$ ]]; then ui_msg "Invalid Username" "Username can only contain letters, numbers, and underscores." return 1 fi local db_pass db_pass=$(ui_input "Database Password" "Enter database password:\n\n🔒 Password Requirements:\n• Minimum 8 characters\n• Mix of letters, numbers, symbols\n• Avoid spaces and quotes\n\n💡 Leave empty for auto-generated secure password" "") || return if [[ -z "$db_pass" ]]; then db_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9!@#$%^&*' | head -c 16) ui_msg "Generated Password" "🔐 Auto-generated secure password:\n\n$db_pass\n\n📝 Please save this password securely!" else # Validate password strength if [[ ${#db_pass} -lt 8 ]]; then ui_msg "Weak Password" "Password must be at least 8 characters long." return 1 fi fi log "Creating custom WordPress database: $db_name with user: $db_user" # Check MariaDB service first if ! systemctl is-active --quiet mariadb; then ui_msg "Service Error" "MariaDB service is not running. Please start MariaDB first." return 1 fi # Get root authentication method local root_pass local auth_method="sudo" if [[ -f "/root/.mysql_root_password" ]]; then root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null) if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then auth_method="password" fi fi # Function to execute MySQL commands with proper error handling execute_mysql_cmd() { local cmd="$1" local operation="$2" local result if [[ "$auth_method" == "password" ]]; then if result=$(mysql -u root -p"$root_pass" -e "$cmd" 2>&1); then log "SUCCESS: $operation completed with password auth" return 0 else log "FAILED: $operation with password auth: $result" ui_msg "Database Error" "Failed: $operation\n\nError: $result\n\nTrying alternative authentication..." # Try sudo fallback if result=$(sudo mysql -e "$cmd" 2>&1); then log "SUCCESS: $operation completed with sudo fallback" return 0 else log "FAILED: $operation with sudo fallback: $result" ui_msg "Database Error" "Failed: $operation\n\nError: $result" return 1 fi fi else if result=$(sudo mysql -e "$cmd" 2>&1); then log "SUCCESS: $operation completed with sudo" return 0 else log "FAILED: $operation with sudo: $result" ui_msg "Database Error" "Failed: $operation\n\nError: $result" return 1 fi fi } # Check if database already exists local existing_db if [[ "$auth_method" == "password" ]]; then existing_db=$(mysql -u root -p"$root_pass" -e "SHOW DATABASES LIKE '${db_name}';" 2>/dev/null | tail -n +2) else existing_db=$(sudo mysql -e "SHOW DATABASES LIKE '${db_name}';" 2>/dev/null | tail -n +2) fi if [[ -n "$existing_db" ]]; then if ! ui_yesno "Database Exists" "Database '$db_name' already exists.\n\nDo you want to use the existing database?\n\n⚠️ WARNING: This will create a new user with access to the existing database."; then return 1 fi else # Create database execute_mysql_cmd "CREATE DATABASE \`${db_name}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" "Database creation" || return 1 fi # Check if user already exists local existing_user if [[ "$auth_method" == "password" ]]; then existing_user=$(mysql -u root -p"$root_pass" -e "SELECT User FROM mysql.user WHERE User='${db_user}' AND Host='localhost';" 2>/dev/null | tail -n +2) else existing_user=$(sudo mysql -e "SELECT User FROM mysql.user WHERE User='${db_user}' AND Host='localhost';" 2>/dev/null | tail -n +2) fi if [[ -n "$existing_user" ]]; then if ui_yesno "User Exists" "User '$db_user' already exists.\n\nDo you want to update the password and grant access to '$db_name'?"; then # Update existing user password execute_mysql_cmd "ALTER USER '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';" "User password update" || return 1 else return 1 fi else # Create new user execute_mysql_cmd "CREATE USER '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';" "User creation" || return 1 fi # Grant privileges execute_mysql_cmd "GRANT ALL PRIVILEGES ON \`${db_name}\`.* TO '${db_user}'@'localhost';" "Grant privileges" || return 1 # Flush privileges execute_mysql_cmd "FLUSH PRIVILEGES;" "Flush privileges" || return 1 # Test WordPress user connection if mysql -u "$db_user" -p"$db_pass" -e "USE \`${db_name}\`; SELECT 1;" >/dev/null 2>&1; then ui_msg "Database Setup Complete" "✅ WordPress Database Setup Successful!\n\n🔐 Database Credentials:\n• Database Name: $db_name\n• Username: $db_user\n• Password: $db_pass\n• Host: localhost\n\n👤 User Management Summary:\n• ROOT USER: Administrative access (existing)\n• WORDPRESS USER: '$db_user' (dedicated for this site)\n• Database: '$db_name' (exclusive access)\n\n🔒 Security: WordPress user has minimal privileges" echo "${db_name}:${db_user}:${db_pass}" else ui_msg "Connection Test Failed" "Database and user created but connection test failed.\n\nPlease check the credentials manually." return 1 fi } setup_wordpress_files() { local domain="$1" local db_info="$2" local site_dir="/var/www/$domain" log "Setting up WordPress files for $domain" # Parse database info IFS=':' read -r db_name db_user db_pass <<< "$db_info" # Create site directory with proper initial permissions sudo mkdir -p "$site_dir" # Download WordPress ui_msg "Downloading WordPress" "Downloading latest WordPress...\n\n⏳ This process is automatic - please wait..." wget -q https://wordpress.org/latest.tar.gz -O /tmp/wordpress.tar.gz || { ui_msg "Download Error" "Failed to download WordPress. Please check your internet connection." return 1 } tar -xzf /tmp/wordpress.tar.gz -C /tmp sudo rsync -a /tmp/wordpress/ "$site_dir/" rm -rf /tmp/wordpress /tmp/wordpress.tar.gz # Configure WordPress with sudo sudo cp "$site_dir/wp-config-sample.php" "$site_dir/wp-config.php" # Database configuration - escape special characters in passwords local escaped_db_name="${db_name//\'/\\\'}" local escaped_db_user="${db_user//\'/\\\'}" local escaped_db_pass="${db_pass//\'/\\\'}" # Use more robust sed replacements with different delimiters (with sudo) sudo sed -i "s|database_name_here|${escaped_db_name}|g" "$site_dir/wp-config.php" sudo sed -i "s|username_here|${escaped_db_user}|g" "$site_dir/wp-config.php" sudo sed -i "s|password_here|${escaped_db_pass}|g" "$site_dir/wp-config.php" sudo sed -i "s|localhost|localhost|g" "$site_dir/wp-config.php" # Verify database configuration was applied (using fixed-string grep to avoid regex issues) if grep -F "$escaped_db_name" "$site_dir/wp-config.php" >/dev/null 2>&1 && \ grep -F "$escaped_db_user" "$site_dir/wp-config.php" >/dev/null 2>&1 && \ grep -F "$escaped_db_pass" "$site_dir/wp-config.php" >/dev/null 2>&1; then log "Database configuration applied successfully to wp-config.php" # Test database connection if mysql -u"$db_user" -p"$db_pass" -h"localhost" -e "USE \`$db_name\`; SELECT 1;" >/dev/null 2>&1; then log "Database connection test successful" else log "WARNING: Database connection test failed - WordPress may have connection issues" ui_msg "Database Warning" "⚠️ Database configuration applied but connection test failed.\n\nThis may cause WordPress installation issues.\n\nPlease verify:\n• Database '$db_name' exists\n• User '$db_user' has access\n• Password is correct\n\nContinuing with installation..." fi else log "WARNING: Database configuration may not have been applied correctly" # Try alternative method with PHP-style replacement (with sudo) sudo tee /tmp/wp-db-config.php > /dev/null << EOF EOF sudo php /tmp/wp-db-config.php sudo rm -f /tmp/wp-db-config.php # Test database connection after PHP method if mysql -u"$db_user" -p"$db_pass" -h"localhost" -e "USE \`$db_name\`; SELECT 1;" >/dev/null 2>&1; then log "Database connection test successful after PHP method" else log "ERROR: Database connection test failed after both methods" ui_msg "Database Error" "❌ Database configuration failed!\n\nWordPress will not be able to connect to the database.\n\nPlease check:\n• Database '$db_name' exists\n• User '$db_user' has proper permissions\n• Password is correct\n• MariaDB is running\n\nYou may need to recreate the database and user." fi fi # Add security keys local salts salts=$(curl -s https://api.wordpress.org/secret-key/1.1/salt/ 2>/dev/null) || { log "Warning: Could not fetch WordPress security keys" salts="// Security keys could not be fetched automatically" } # Replace the placeholder keys using a temporary file to avoid sed escaping issues if [[ -n "$salts" ]]; then # Create temporary file with salts echo "$salts" > /tmp/wp-salts.txt # Remove existing placeholder keys sudo sed -i '/AUTH_KEY/,/NONCE_SALT/d' "$site_dir/wp-config.php" # Insert new keys before MySQL settings using a more robust method sudo awk ' /\/\*\* MySQL settings/ { while ((getline line < "/tmp/wp-salts.txt") > 0) { print line } close("/tmp/wp-salts.txt") print "" } { print } ' "$site_dir/wp-config.php" > /tmp/wp-config-new.php sudo mv /tmp/wp-config-new.php "$site_dir/wp-config.php" rm -f /tmp/wp-salts.txt else log "Skipping security keys insertion due to fetch failure" fi # Set proper permissions sudo chown -R www-data:www-data "$site_dir" sudo find "$site_dir" -type d -exec chmod 755 {} \; sudo find "$site_dir" -type f -exec chmod 644 {} \; # Ensure wp-config.php has correct permissions sudo chmod 600 "$site_dir/wp-config.php" # Create uploads directory with proper permissions sudo mkdir -p "$site_dir/wp-content/uploads" sudo chown -R www-data:www-data "$site_dir/wp-content/uploads" sudo chmod -R 755 "$site_dir/wp-content/uploads" log "WordPress files setup completed" } setup_wordpress_files_custom() { local domain="$1" local site_dir="$2" local db_info="$3" setup_wordpress_files "$domain" "$db_info" } configure_nginx_wordpress() { local domain="$1" local site_dir="/var/www/$domain" log "Configuring Nginx for WordPress: $domain" local nginx_conf="/etc/nginx/sites-available/$domain" sudo tee "$nginx_conf" > /dev/null <&1 | tee -a "$LOGFILE"; then log "Nginx configuration test passed" # Reload Nginx log "Reloading Nginx..." if sudo systemctl reload nginx 2>&1 | tee -a "$LOGFILE"; then log "Nginx reloaded successfully" # Verify Nginx is running if systemctl is-active --quiet nginx; then log "Nginx is running and site $domain is configured" # Show site status ui_msg "Site Configured" "✅ Nginx site '$domain' has been configured successfully!\n\n📁 Config file: /etc/nginx/sites-available/$domain\n🔗 Enabled at: /etc/nginx/sites-enabled/$domain\n🌐 Access at: http://$domain\n\n🔄 Nginx has been reloaded and is running." else log "WARNING: Nginx is not running after reload" ui_msg "Nginx Issue" "⚠️ Site configured but Nginx is not running.\n\nTry: sudo systemctl start nginx" return 1 fi else log "ERROR: Failed to reload Nginx" ui_msg "Reload Failed" "❌ Nginx configuration is valid but reload failed.\n\nCheck: sudo systemctl status nginx" return 1 fi else log "ERROR: Nginx configuration test failed" ui_msg "Config Error" "❌ Nginx configuration test failed!\n\nThe site configuration has been removed.\n\nCheck the logs for details." # Remove the broken configuration sudo rm -f "/etc/nginx/sites-enabled/$domain" return 1 fi } configure_nginx_wordpress_custom() { local domain="$1" local site_dir="$2" configure_nginx_wordpress "$domain" } configure_apache_wordpress() { local domain="$1" local site_dir="/var/www/$domain" log "Configuring Apache for WordPress: $domain" local apache_conf="/etc/apache2/sites-available/${domain}.conf" sudo tee "$apache_conf" > /dev/null < ServerName $domain ServerAlias www.$domain DocumentRoot $site_dir # Security headers Header always set X-Frame-Options SAMEORIGIN Header always set X-Content-Type-Options nosniff Header always set X-XSS-Protection "1; mode=block" Header always set Referrer-Policy "no-referrer-when-downgrade" # WordPress directory configuration Options -Indexes +FollowSymLinks AllowOverride All Require all granted # Security: Block access to sensitive files Require all denied Require all denied Require all denied # Block XML-RPC attacks Require all denied # Logging ErrorLog \${APACHE_LOG_DIR}/${domain}_error.log CustomLog \${APACHE_LOG_DIR}/${domain}_access.log combined EOF # Enable required modules sudo a2enmod rewrite headers 2>&1 | tee -a "$LOGFILE" # Enable site sudo a2ensite "${domain}.conf" 2>&1 | tee -a "$LOGFILE" # Test and reload Apache if sudo apache2ctl configtest 2>&1 | tee -a "$LOGFILE"; then sudo systemctl reload apache2 log "Apache configuration applied successfully" else log "ERROR: Apache configuration test failed" return 1 fi } configure_apache_wordpress_custom() { local domain="$1" local site_dir="$2" configure_apache_wordpress "$domain" } show_wordpress_completion() { local domain="$1" local db_info="$2" # Parse database info IFS=':' read -r db_name db_user db_pass <<< "$db_info" # Save credentials with comprehensive details local creds_file="$BACKUP_DIR/wordpress-${domain}-$(date +%Y%m%d-%H%M%S).txt" cat > "$creds_file" <&1 | tee -a "$LOGFILE"; then ui_msg "SSL Success" "SSL certificate installed successfully!\n\nYour site is now available at:\nhttps://$domain\n\nCertbot will automatically renew the certificate." else ui_msg "SSL Error" "Failed to obtain SSL certificate. Please check:\n\n• Domain DNS points to this server\n• Port 80/443 are open\n• Domain is accessible from internet\n\nCheck logs for details." fi } wordpress_security_hardening() { log "Starting WordPress security hardening" local domain domain=$(ui_input "Domain Name" "Enter WordPress domain to harden:" "") || return if [[ -z "$domain" ]]; then return fi local site_dir="/var/www/$domain" if [[ ! -d "$site_dir" ]]; then ui_msg "Site Not Found" "WordPress installation not found at $site_dir" return fi local security_options=( "file-permissions" "Fix File Permissions" on "disable-file-editing" "Disable File Editing in Admin" on "hide-wp-version" "Hide WordPress Version" on "limit-login-attempts" "Install Login Security Plugin" off "security-headers" "Add Security Headers" on "disable-xmlrpc" "Disable XML-RPC" on ) local selected selected=$(ui_checklist "WordPress Security" "Select security hardening options:" 20 80 10 "${security_options[@]}") || return if [[ -z "$selected" ]]; then return fi ui_msg "Applying Security" "Applying selected security hardening options..." # Apply selected security measures for option in $selected; do case "$option" in "file-permissions") apply_wp_file_permissions "$site_dir" ;; "disable-file-editing") apply_wp_disable_editing "$site_dir" ;; "hide-wp-version") apply_wp_hide_version "$site_dir" ;; "security-headers") apply_wp_security_headers "$domain" ;; "disable-xmlrpc") apply_wp_disable_xmlrpc "$site_dir" ;; esac done ui_msg "Security Applied" "WordPress security hardening completed for $domain" } apply_wp_file_permissions() { local site_dir="$1" log "Applying WordPress file permissions to $site_dir" sudo chown -R www-data:www-data "$site_dir" sudo find "$site_dir" -type d -exec chmod 755 {} \; sudo find "$site_dir" -type f -exec chmod 644 {} \; sudo chmod 600 "$site_dir/wp-config.php" } apply_wp_disable_editing() { local site_dir="$1" log "Disabling WordPress file editing" if ! grep -q "DISALLOW_FILE_EDIT" "$site_dir/wp-config.php"; then sudo sed -i "/\/\* That's all, stop editing/i define('DISALLOW_FILE_EDIT', true);" "$site_dir/wp-config.php" fi } apply_wp_hide_version() { local site_dir="$1" log "Hiding WordPress version" local functions_php="$site_dir/wp-content/themes/*/functions.php" for file in $functions_php; do if [[ -f "$file" ]]; then if ! grep -q "remove_action.*wp_head.*wp_generator" "$file"; then echo "remove_action('wp_head', 'wp_generator');" | sudo tee -a "$file" > /dev/null fi fi done } apply_wp_security_headers() { local domain="$1" log "Adding security headers for $domain" # This is already handled in the web server configuration # Just log that it's already applied log "Security headers already applied in web server configuration" } apply_wp_disable_xmlrpc() { local site_dir="$1" log "Disabling XML-RPC" if ! grep -q "xmlrpc_enabled" "$site_dir/wp-config.php"; then sudo sed -i "/\/\* That's all, stop editing/i add_filter('xmlrpc_enabled', '__return_false');" "$site_dir/wp-config.php" fi } show_wordpress_status() { log "Showing WordPress sites status" while true; do local status_info="WordPress\n\n" # Check for WordPress installations local wp_sites=() if [[ -d "/var/www" ]]; then while IFS= read -r -d '' dir; do local site_name site_name=$(basename "$dir") if [[ -f "$dir/wp-config.php" ]]; then wp_sites+=("$site_name") fi done < <(find /var/www -maxdepth 1 -type d -print0 2>/dev/null) fi if [[ ${#wp_sites[@]} -eq 0 ]]; then status_info+="No WordPress installations found in /var/www/\n\n" status_info+="Use the WordPress Installation menu to set up new sites.\n\n" else status_info+="WordPress Sites (${#wp_sites[@]} found):\n" status_info+="═══════════════════════════════════════════════════════════\n" for site in "${wp_sites[@]}"; do local site_dir="/var/www/$site" # Quick status check local status_icon="❌" local ssl_icon="🔓" local access_icon="❌" # Check if site is properly configured if [[ -f "/etc/nginx/sites-available/$site" && -L "/etc/nginx/sites-enabled/$site" ]] || \ [[ -f "/etc/apache2/sites-available/${site}.conf" ]]; then status_icon="✅" fi # Check SSL if [[ -f "/etc/letsencrypt/live/$site/fullchain.pem" ]]; then ssl_icon="🔒" fi # Quick accessibility test if command -v curl >/dev/null 2>&1; then if curl -s -o /dev/null -w "%{http_code}" "http://$site" --connect-timeout 3 --max-time 5 | grep -q "200\|301\|302"; then access_icon="🌍" fi fi status_info+="$status_icon $ssl_icon $access_icon $site\n" done status_info+="\n" fi # Web server status status_info+="Services:\n" status_info+="═══════════════════════════════════════════════════════════\n" if systemctl is-active --quiet nginx; then status_info+="🟢 Nginx: Running\n" else status_info+="🔴 Nginx: Not running\n" fi if systemctl is-active --quiet apache2; then status_info+="🟢 Apache: Running\n" else status_info+="🔴 Apache: Not running\n" fi if systemctl is-active --quiet mariadb; then status_info+="🟢 MariaDB: Running\n" else status_info+="🔴 MariaDB: Not running\n" fi status_info+="\nLegend: ✅=Configured 🔒=SSL 🌍=Accessible\n" # Create menu items for site management local menu_items=() if [[ ${#wp_sites[@]} -gt 0 ]]; then menu_items+=("manage" "🔧 Manage Sites") menu_items+=("" "(_*_)") fi menu_items+=("install" "➕ Install New WordPress Site") menu_items+=("refresh" "🔄 Refresh Status") menu_items+=("zback" "(Z) ← Back to Main Menu") local choice choice=$(ui_menu "WordPress" "$status_info" 25 90 15 "${menu_items[@]}") || break case "$choice" in manage) show_wordpress_site_management "${wp_sites[@]}" ;; install) show_wordpress_setup_menu ;; refresh) # Just loop back to refresh the display ;; back|zback|z|"") break ;; esac done } show_wordpress_site_management() { local wp_sites=("$@") while true; do local status_info="WordPress Site Management\n\n" status_info+="Select a site to manage:\n" status_info+="═══════════════════════════════════════════════════════════\n" local menu_items=() for site in "${wp_sites[@]}"; do local site_dir="/var/www/$site" local status_indicators="" # Check configuration status if [[ -f "/etc/nginx/sites-available/$site" ]]; then if [[ -L "/etc/nginx/sites-enabled/$site" ]]; then status_indicators+="✅" else status_indicators+="⚠️" fi elif [[ -f "/etc/apache2/sites-available/${site}.conf" ]]; then if sudo a2ensite "${site}.conf" --quiet 2>/dev/null && sudo apache2ctl -S 2>/dev/null | grep -q "$site"; then status_indicators+="✅" else status_indicators+="⚠️" fi else status_indicators+="❌" fi # Check SSL status if [[ -f "/etc/letsencrypt/live/$site/fullchain.pem" ]]; then status_indicators+=" 🔒" else status_indicators+=" 🔓" fi # Check accessibility if command -v curl >/dev/null 2>&1; then if curl -s -o /dev/null -w "%{http_code}" "http://$site" --connect-timeout 5 --max-time 10 | grep -q "200\|301\|302"; then status_indicators+=" 🌍" else status_indicators+=" ⚠️" fi fi menu_items+=("$site" "$status_indicators $site") done menu_items+=("" "(_*_)") menu_items+=("zback" "(Z) ← Back to Status View") local choice choice=$(ui_menu "WordPress Site Management" "$status_info" 20 80 10 "${menu_items[@]}") || break case "$choice" in back|zback|z|"") break ;; *) if [[ " ${wp_sites[*]} " =~ " $choice " ]]; then show_individual_site_management "$choice" fi ;; esac done } show_individual_site_management() { local site="$1" local site_dir="/var/www/$site" while true; do local status_info="Managing Site: $site\n\n" # Detailed site information status_info+="Site Details:\n" status_info+="═══════════════════════════════════════════════════════════\n" status_info+="📁 Directory: $site_dir\n" # WordPress version local wp_version="" if [[ -f "$site_dir/wp-includes/version.php" ]]; then wp_version=$(grep "wp_version =" "$site_dir/wp-includes/version.php" | cut -d"'" -f2 2>/dev/null || echo "Unknown") fi status_info+="🌐 WordPress Version: $wp_version\n" # Web server configuration local web_server="" local config_file="" local config_content="" if [[ -f "/etc/nginx/sites-available/$site" ]]; then web_server="Nginx" config_file="/etc/nginx/sites-available/$site" config_content=$(head -20 "$config_file" 2>/dev/null) elif [[ -f "/etc/apache2/sites-available/${site}.conf" ]]; then web_server="Apache" config_file="/etc/apache2/sites-available/${site}.conf" config_content=$(head -20 "$config_file" 2>/dev/null) fi if [[ -n "$web_server" ]]; then status_info+="🔧 Web Server: $web_server\n" status_info+="📄 Config File: $config_file\n" # Sites enabled status if [[ "$web_server" == "Nginx" ]]; then if [[ -L "/etc/nginx/sites-enabled/$site" ]]; then status_info+="🔗 Sites Enabled: ✅ Yes\n" else status_info+="🔗 Sites Enabled: ❌ No\n" fi elif [[ "$web_server" == "Apache" ]]; then if sudo a2ensite "${site}.conf" --quiet 2>/dev/null && sudo apache2ctl -S 2>/dev/null | grep -q "$site"; then status_info+="🔗 Sites Enabled: ✅ Yes\n" else status_info+="🔗 Sites Enabled: ❌ No\n" fi fi status_info+="\nConfiguration Preview:\n" status_info+="───────────────────────────────────────────────────────────\n" status_info+="$config_content\n" if [[ $(echo "$config_content" | wc -l) -ge 20 ]]; then status_info+="... (truncated, showing first 20 lines)\n" fi else status_info+="⚠️ Web Server: Not configured\n" fi status_info+="\n" # SSL status if [[ -f "/etc/letsencrypt/live/$site/fullchain.pem" ]]; then local cert_expiry="" cert_expiry=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/$site/fullchain.pem" 2>/dev/null | cut -d= -f2) status_info+="🔒 SSL Certificate: ✅ Enabled (Expires: ${cert_expiry:-Unknown})\n" else status_info+="🔒 SSL Certificate: ❌ Not configured\n" fi # Database connection test and detailed diagnostics local db_status="❌" local db_error_msg="" local db_tables_count="0" local wp_tables_exist="❌" if [[ -f "$site_dir/wp-config.php" ]]; then local db_name db_user db_pass db_host db_name=$(grep "DB_NAME" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_user=$(grep "DB_USER" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_pass=$(grep "DB_PASSWORD" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_host=$(grep "DB_HOST" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) if [[ -n "$db_name" && -n "$db_user" ]]; then # Test database connection local db_test_result if db_test_result=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "USE \`$db_name\`; SELECT 'Connection OK' as status;" 2>&1); then if echo "$db_test_result" | grep -q "Connection OK"; then db_status="✅ Connected" # Count tables in database db_tables_count=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "USE \`$db_name\`; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$db_name';" 2>/dev/null | tail -n1) # Check for WordPress core tables local wp_core_tables=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "USE \`$db_name\`; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$db_name' AND table_name LIKE '%wp_posts' OR table_name LIKE '%wp_users' OR table_name LIKE '%wp_options';" 2>/dev/null | tail -n1) if [[ "$wp_core_tables" -ge 3 ]]; then wp_tables_exist="✅ WordPress tables found" else wp_tables_exist="⚠️ WordPress tables missing ($wp_core_tables/3)" fi else db_status="❌ Connection failed" db_error_msg="Database exists but connection failed" fi else db_status="❌ Connection failed" # Parse error message for common issues if echo "$db_test_result" | grep -q "Access denied"; then db_error_msg="Access denied - check credentials" elif echo "$db_test_result" | grep -q "Unknown database"; then db_error_msg="Database '$db_name' does not exist" elif echo "$db_test_result" | grep -q "Can't connect"; then db_error_msg="Cannot connect to MySQL server" else db_error_msg="Connection error: $(echo "$db_test_result" | head -n1)" fi fi else db_status="❌ Missing credentials" db_error_msg="Database credentials not found in wp-config.php" fi else db_status="❌ No wp-config.php" db_error_msg="WordPress configuration file not found" fi status_info+="🗄️ Database Connection: $db_status\n" if [[ -n "$db_error_msg" ]]; then status_info+="⚠️ Error: $db_error_msg\n" fi # Database credentials and diagnostics (if available) if [[ -f "$site_dir/wp-config.php" ]]; then status_info+="\nDatabase Information:\n" status_info+="───────────────────────────────────────────────────────────\n" status_info+="📊 Database: ${db_name:-❌ Not found}\n" status_info+="👤 User: ${db_user:-❌ Not found}\n" status_info+="🔑 Password: ${db_pass:+✅ Set}${db_pass:-❌ Not found}\n" status_info+="🏠 Host: ${db_host:-localhost}\n" status_info+="📋 Tables Count: $db_tables_count\n" status_info+="🔧 WordPress Tables: $wp_tables_exist\n" # Additional database diagnostics if [[ "$db_status" == "✅ Connected" ]]; then # Check database charset local db_charset=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$db_name';" 2>/dev/null | tail -n1) status_info+="🔤 Charset: ${db_charset:-unknown}\n" # Check if WordPress is installed (has admin user) local wp_installed=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "USE \`$db_name\`; SELECT COUNT(*) FROM wp_users WHERE user_login != '';" 2>/dev/null | tail -n1) if [[ "$wp_installed" -gt 0 ]]; then status_info+="👥 WordPress Users: $wp_installed found\n" else status_info+="👥 WordPress Users: ❌ No users found (not installed)\n" fi fi fi # Site accessibility local site_accessible="❌" if command -v curl >/dev/null 2>&1; then local http_code http_code=$(curl -s -o /dev/null -w "%{http_code}" "http://$site" --connect-timeout 5 --max-time 10) if [[ "$http_code" =~ ^(200|301|302)$ ]]; then site_accessible="✅ (HTTP $http_code)" else site_accessible="❌ (HTTP ${http_code:-timeout})" fi fi status_info+="🌍 Site Accessible: $site_accessible\n" # WordPress admin URL and setup status status_info+="\nWordPress Setup:\n" status_info+="───────────────────────────────────────────────────────────\n" status_info+="🔗 Admin URL: http://$site/wp-admin/\n" status_info+="🏠 Site URL: http://$site/\n" # Check if WordPress is configured if [[ -f "$site_dir/wp-config.php" ]]; then local wp_configured="❌" if mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "SELECT COUNT(*) FROM ${db_name}.wp_options WHERE option_name='siteurl';" 2>/dev/null | grep -q "1"; then wp_configured="✅" fi status_info+="⚙️ WordPress Configured: $wp_configured\n" fi local menu_items=( "" "(_*_)" "view-config" "📄 View Full Web Server Config" "edit-config" "✏️ Edit Web Server Config" "wp-config" "⚙️ View/Edit wp-config.php" "test-db" "🗄️ Test Database Connection" "repair-db" "🔧 Repair Database Connection" "" "(_*_)" "enable-site" "🔗 Enable/Disable Site" "test-site" "🧪 Test Site Accessibility" "" "(_*_)" "delete-site" "🗑️ Delete This Site (DANGER)" "" "(_*_)" "zback" "(Z) ← Back to Site List" ) local choice choice=$(ui_menu "Site Management: $site" "$status_info" 35 120 15 "${menu_items[@]}") || break case "$choice" in view-config) if [[ -n "$config_file" && -f "$config_file" ]]; then local full_config full_config=$(cat "$config_file") ui_info "Configuration: $config_file" "$full_config" else ui_error "No configuration file found for $site" fi ;; edit-config) if [[ -n "$config_file" && -f "$config_file" ]]; then sudo nano "$config_file" if [[ "$web_server" == "Nginx" ]]; then sudo nginx -t && sudo systemctl reload nginx elif [[ "$web_server" == "Apache" ]]; then sudo apache2ctl configtest && sudo systemctl reload apache2 fi else ui_error "No configuration file found for $site" fi ;; wp-config) if [[ -f "$site_dir/wp-config.php" ]]; then local wp_config_content wp_config_content=$(cat "$site_dir/wp-config.php") ui_info "WordPress Configuration: $site_dir/wp-config.php" "$wp_config_content" if ui_yesno "Edit wp-config.php" "Do you want to edit the WordPress configuration file?"; then sudo nano "$site_dir/wp-config.php" fi else ui_error "wp-config.php not found at $site_dir/wp-config.php" fi ;; test-db) test_wordpress_database_connection "$site" ;; repair-db) repair_wordpress_database_connection "$site" ;; enable-site) manage_site_enablement "$site" "$web_server" ;; test-site) test_site_accessibility "$site" ;; delete-site) delete_wordpress_site "$site" break ;; back|zback|z|"") break ;; esac done } manage_site_enablement() { local site="$1" local web_server="$2" if [[ "$web_server" == "Nginx" ]]; then if [[ -L "/etc/nginx/sites-enabled/$site" ]]; then if ui_confirm "Disable site $site?"; then sudo rm "/etc/nginx/sites-enabled/$site" sudo nginx -t && sudo systemctl reload nginx ui_success "Site $site disabled" fi else if ui_confirm "Enable site $site?"; then sudo ln -sf "/etc/nginx/sites-available/$site" "/etc/nginx/sites-enabled/$site" sudo nginx -t && sudo systemctl reload nginx ui_success "Site $site enabled" fi fi elif [[ "$web_server" == "Apache" ]]; then if sudo a2ensite "${site}.conf" --quiet 2>/dev/null && sudo apache2ctl -S 2>/dev/null | grep -q "$site"; then if ui_confirm "Disable site $site?"; then sudo a2dissite "${site}.conf" sudo apache2ctl configtest && sudo systemctl reload apache2 ui_success "Site $site disabled" fi else if ui_confirm "Enable site $site?"; then sudo a2ensite "${site}.conf" sudo apache2ctl configtest && sudo systemctl reload apache2 ui_success "Site $site enabled" fi fi else ui_error "No web server configuration found for $site" fi } test_site_accessibility() { local site="$1" local test_info="Testing Site Accessibility: $site\n\n" # HTTP test if command -v curl >/dev/null 2>&1; then test_info+="HTTP Test:\n" local http_result http_result=$(curl -s -o /dev/null -w "HTTP Code: %{http_code}\nTime: %{time_total}s\nSize: %{size_download} bytes" "http://$site" --connect-timeout 10 --max-time 30 2>&1) test_info+="$http_result\n\n" # HTTPS test if SSL is configured if [[ -f "/etc/letsencrypt/live/$site/fullchain.pem" ]]; then test_info+="HTTPS Test:\n" local https_result https_result=$(curl -s -o /dev/null -w "HTTP Code: %{http_code}\nTime: %{time_total}s\nSize: %{size_download} bytes" "https://$site" --connect-timeout 10 --max-time 30 2>&1) test_info+="$https_result\n\n" fi else test_info+="curl not available for testing\n" fi # DNS resolution test if command -v nslookup >/dev/null 2>&1; then test_info+="DNS Resolution:\n" local dns_result dns_result=$(nslookup "$site" 2>&1 | head -10) test_info+="$dns_result\n" fi ui_info "Site Test Results" "$test_info" } delete_wordpress_site() { local site="$1" local site_dir="/var/www/$site" local warning_info="⚠️ WARNING: DELETE WORDPRESS SITE ⚠️\n\n" warning_info+="You are about to permanently delete:\n" warning_info+="• Site: $site\n" warning_info+="• Directory: $site_dir\n" warning_info+="• Web server configuration\n" warning_info+="• SSL certificates (if any)\n\n" warning_info+="This action CANNOT be undone!\n\n" warning_info+="Type 'DELETE $site' to confirm:" local confirmation confirmation=$(ui_input "Confirm Site Deletion" "$warning_info") if [[ "$confirmation" == "DELETE $site" ]]; then # Remove web server configuration if [[ -f "/etc/nginx/sites-available/$site" ]]; then sudo rm -f "/etc/nginx/sites-available/$site" sudo rm -f "/etc/nginx/sites-enabled/$site" sudo nginx -t && sudo systemctl reload nginx fi if [[ -f "/etc/apache2/sites-available/${site}.conf" ]]; then sudo a2dissite "${site}.conf" 2>/dev/null sudo rm -f "/etc/apache2/sites-available/${site}.conf" sudo apache2ctl configtest && sudo systemctl reload apache2 fi # Remove SSL certificates if [[ -d "/etc/letsencrypt/live/$site" ]]; then sudo certbot delete --cert-name "$site" --non-interactive 2>/dev/null fi # Remove site directory if [[ -d "$site_dir" ]]; then sudo rm -rf "$site_dir" fi ui_success "Site $site has been completely removed" else ui_info "Deletion Cancelled" "Site deletion cancelled - nothing was removed" fi } # Legacy function for backward compatibility setup_wordpress() { show_wordpress_setup_menu } # WordPress database testing and repair functions test_wordpress_database_connection() { local site="$1" local site_dir="/var/www/$site" log "Testing database connection for WordPress site: $site" if [[ ! -f "$site_dir/wp-config.php" ]]; then ui_error "wp-config.php not found" "Cannot test database connection.\n\nFile not found: $site_dir/wp-config.php" return 1 fi # Extract database credentials local db_name db_user db_pass db_host db_name=$(grep "DB_NAME" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_user=$(grep "DB_USER" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_pass=$(grep "DB_PASSWORD" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_host=$(grep "DB_HOST" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_host=${db_host:-localhost} local test_info="🗄️ Database Connection Test\n\n" test_info+="Site: $site\n" test_info+="Database: ${db_name:-❌ Not found}\n" test_info+="User: ${db_user:-❌ Not found}\n" test_info+="Host: $db_host\n\n" if [[ -z "$db_name" || -z "$db_user" || -z "$db_pass" ]]; then test_info+="❌ FAILED: Missing database credentials in wp-config.php\n\n" test_info+="Required credentials not found:\n" [[ -z "$db_name" ]] && test_info+="• DB_NAME is missing\n" [[ -z "$db_user" ]] && test_info+="• DB_USER is missing\n" [[ -z "$db_pass" ]] && test_info+="• DB_PASSWORD is missing\n" ui_info "Database Test Failed" "$test_info" return 1 fi # Test connection local db_test_result if db_test_result=$(mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`; SELECT 'Connection successful' as status, NOW() as timestamp;" 2>&1); then if echo "$db_test_result" | grep -q "Connection successful"; then test_info+="✅ SUCCESS: Database connection established\n\n" # Additional diagnostics local table_count=$(mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$db_name';" 2>/dev/null | tail -n1) local wp_tables=$(mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$db_name' AND (table_name LIKE '%wp_posts' OR table_name LIKE '%wp_users' OR table_name LIKE '%wp_options');" 2>/dev/null | tail -n1) local user_count=$(mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`; SELECT COUNT(*) FROM wp_users;" 2>/dev/null | tail -n1) test_info+="Database Statistics:\n" test_info+="• Total tables: $table_count\n" test_info+="• WordPress core tables: $wp_tables/3\n" test_info+="• WordPress users: $user_count\n\n" if [[ "$wp_tables" -ge 3 && "$user_count" -gt 0 ]]; then test_info+="✅ WordPress appears to be fully installed\n" elif [[ "$wp_tables" -ge 3 ]]; then test_info+="⚠️ WordPress tables exist but no users found\n" else test_info+="⚠️ WordPress tables missing - installation incomplete\n" fi ui_info "Database Test Successful" "$test_info" else test_info+="❌ FAILED: Connection established but database query failed\n\n" test_info+="Error details:\n$db_test_result" ui_info "Database Test Failed" "$test_info" fi else test_info+="❌ FAILED: Cannot connect to database\n\n" # Parse error for common issues if echo "$db_test_result" | grep -q "Access denied"; then test_info+="Error: Access denied\n" test_info+="• Check username and password\n" test_info+="• Verify user has database permissions\n" elif echo "$db_test_result" | grep -q "Unknown database"; then test_info+="Error: Database does not exist\n" test_info+="• Database '$db_name' was not found\n" test_info+="• Check database name spelling\n" test_info+="• Create database if needed\n" elif echo "$db_test_result" | grep -q "Can't connect"; then test_info+="Error: Cannot connect to MySQL server\n" test_info+="• Check if MariaDB/MySQL is running\n" test_info+="• Verify host address: $db_host\n" test_info+="• Check firewall settings\n" else test_info+="Error details:\n$(echo "$db_test_result" | head -n3)\n" fi ui_info "Database Test Failed" "$test_info" fi } repair_wordpress_database_connection() { local site="$1" local site_dir="/var/www/$site" log "Starting database repair for WordPress site: $site" if [[ ! -f "$site_dir/wp-config.php" ]]; then ui_error "wp-config.php not found" "Cannot repair database connection.\n\nFile not found: $site_dir/wp-config.php" return 1 fi local repair_info="🔧 Database Connection Repair\n\n" repair_info+="This will attempt to:\n" repair_info+="• Verify database credentials\n" repair_info+="• Test database connectivity\n" repair_info+="• Create missing database if needed\n" repair_info+="• Recreate database user if needed\n" repair_info+="• Fix common connection issues\n\n" repair_info+="Continue with repair?" if ! ui_yesno "Database Repair" "$repair_info"; then return fi # Extract current credentials local db_name db_user db_pass db_host db_name=$(grep "DB_NAME" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_user=$(grep "DB_USER" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_pass=$(grep "DB_PASSWORD" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_host=$(grep "DB_HOST" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null) db_host=${db_host:-localhost} local repair_steps="" local repair_success=true # Step 1: Validate credentials repair_steps+="Step 1: Validating credentials...\n" if [[ -z "$db_name" || -z "$db_user" || -z "$db_pass" ]]; then repair_steps+="❌ Missing credentials in wp-config.php\n" repair_success=false else repair_steps+="✅ Credentials found\n" fi # Step 2: Test MariaDB service repair_steps+="Step 2: Checking MariaDB service...\n" if systemctl is-active --quiet mariadb; then repair_steps+="✅ MariaDB is running\n" else repair_steps+="⚠️ MariaDB is not running - attempting to start...\n" if sudo systemctl start mariadb 2>/dev/null; then repair_steps+="✅ MariaDB started successfully\n" else repair_steps+="❌ Failed to start MariaDB\n" repair_success=false fi fi if [[ "$repair_success" == "true" && -n "$db_name" && -n "$db_user" && -n "$db_pass" ]]; then # Step 3: Test connection with current credentials repair_steps+="Step 3: Testing database connection...\n" if mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`;" 2>/dev/null; then repair_steps+="✅ Connection successful - no repair needed\n" else repair_steps+="⚠️ Connection failed - attempting repair...\n" # Step 4: Try to recreate database and user with root access repair_steps+="Step 4: Recreating database and user...\n" # Get root access local root_pass="" if [[ -f "/root/.mysql_root_password" ]]; then root_pass=$(sudo cat "/root/.mysql_root_password" 2>/dev/null | tr -d '\n\r') fi local mysql_cmd="" if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" 2>/dev/null; then mysql_cmd="mysql -u root -p\"$root_pass\"" elif sudo mysql -e "SELECT 1;" 2>/dev/null; then mysql_cmd="sudo mysql" else repair_steps+="❌ Cannot access MariaDB as root\n" repair_success=false fi if [[ -n "$mysql_cmd" ]]; then # Create database if eval "$mysql_cmd -e \"CREATE DATABASE IF NOT EXISTS \\\`$db_name\\\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\"" 2>/dev/null; then repair_steps+="✅ Database '$db_name' created/verified\n" else repair_steps+="❌ Failed to create database\n" repair_success=false fi # Create user and grant privileges if eval "$mysql_cmd -e \"CREATE USER IF NOT EXISTS '$db_user'@'localhost' IDENTIFIED BY '$db_pass';\"" 2>/dev/null; then repair_steps+="✅ User '$db_user' created/verified\n" else repair_steps+="❌ Failed to create user\n" repair_success=false fi if eval "$mysql_cmd -e \"GRANT ALL PRIVILEGES ON \\\`$db_name\\\`.* TO '$db_user'@'localhost'; FLUSH PRIVILEGES;\"" 2>/dev/null; then repair_steps+="✅ Privileges granted\n" else repair_steps+="❌ Failed to grant privileges\n" repair_success=false fi # Final test repair_steps+="Step 5: Final connection test...\n" if mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`;" 2>/dev/null; then repair_steps+="✅ Database connection repaired successfully!\n" else repair_steps+="❌ Connection still failing after repair\n" repair_success=false fi fi fi fi # Show repair results if [[ "$repair_success" == "true" ]]; then ui_info "Repair Successful" "✅ Database connection repair completed!\n\n$repair_steps\nYour WordPress site should now be able to connect to the database." else ui_info "Repair Failed" "❌ Database connection repair failed\n\n$repair_steps\nManual intervention may be required. Check the database credentials and MariaDB configuration." fi } # ============================================================================== # KEYBOARD LAYOUT CONFIGURATION # ============================================================================== # Function to detect current keyboard layout get_current_keyboard_layout() { if [[ -f "$HOME/.config/ultrabunt-keyboard-layout" ]]; then cat "$HOME/.config/ultrabunt-keyboard-layout" else echo "none" fi } show_keyboard_layout_menu() { log "Entering show_keyboard_layout_menu function" while true; do local current_layout current_layout=$(get_current_keyboard_layout) local macbook_status="(_*_)" local thinkpad_status="(_*_)" local generic_status="(_*_)" case "$current_layout" in "macbook") macbook_status="(.Y.)" ;; "thinkpad") thinkpad_status="(.Y.)" ;; "generic") generic_status="(.Y.)" ;; esac local menu_items=( "macbook" "$macbook_status MacBook Layout (Cmd as Super, Cmd+Tab switching)" "thinkpad" "$thinkpad_status ThinkPad Layout (Standard PC layout optimized)" "generic" "$generic_status Generic Laptop Layout (Standard configuration)" "" "(_*_)" "current-status" "(.Y.) Show Current Keyboard Configuration" "reset" "(.Y.) Reset to Ubuntu Defaults" "" "(_*_)" "zback" "(Z) ← Back to Main Menu" ) local choice choice=$(ui_menu "Keyboard Layout Configuration" \ "Choose a keyboard layout optimized for your laptop:" \ 20 80 12 "${menu_items[@]}") || break case "$choice" in macbook) configure_macbook_layout ;; thinkpad) configure_thinkpad_layout ;; generic) configure_generic_layout ;; current-status) show_keyboard_status ;; reset) reset_keyboard_layout ;; back|zback|z|"") break ;; esac done log "Exiting show_keyboard_layout_menu function" } configure_macbook_layout() { log "Configuring MacBook keyboard layout" local info="MacBook Layout Configuration:\n\n" info+="• Command key → Super key (for shortcuts)\n" info+="• Cmd+Tab → Switch applications\n" info+="• Cmd+\` → Switch windows of same app\n" info+="• Cmd+C/V/X/Z → Copy/Paste/Cut/Undo\n" info+="• Cmd+Space → Show applications\n" info+="• Control key remains as Control\n\n" info+="This layout makes Ubuntu feel more like macOS.\n\n" info+="Apply this configuration?" if ui_yesno "MacBook Layout" "$info"; then apply_macbook_keyboard_config ui_msg "Success" "MacBook keyboard layout applied successfully!\n\nYou may need to log out and back in for all changes to take effect." fi } configure_thinkpad_layout() { log "Configuring ThinkPad keyboard layout" local info="ThinkPad Layout Configuration:\n\n" info+="• Optimized for ThinkPad keyboards\n" info+="• Caps Lock → Additional Control key\n" info+="• Alt+Tab → Switch applications\n" info+="• Super key for launcher\n" info+="• Standard PC shortcuts (Ctrl+C/V/X/Z)\n\n" info+="This layout optimizes the ThinkPad keyboard experience.\n\n" info+="Apply this configuration?" if ui_yesno "ThinkPad Layout" "$info"; then apply_thinkpad_keyboard_config ui_msg "Success" "ThinkPad keyboard layout applied successfully!\n\nYou may need to log out and back in for all changes to take effect." fi } configure_generic_layout() { log "Configuring generic laptop keyboard layout" local info="Generic Laptop Layout Configuration:\n\n" info+="• Standard PC keyboard layout\n" info+="• Alt+Tab → Switch applications\n" info+="• Super key for launcher\n" info+="• Standard shortcuts (Ctrl+C/V/X/Z)\n" info+="• Optimized for most laptop keyboards\n\n" info+="This is a safe, standard configuration.\n\n" info+="Apply this configuration?" if ui_yesno "Generic Layout" "$info"; then apply_generic_keyboard_config ui_msg "Success" "Generic keyboard layout applied successfully!\n\nYou may need to log out and back in for all changes to take effect." fi } show_keyboard_status() { log "Showing current keyboard configuration status" local status_info="" # Check current XKB options local xkb_options xkb_options=$(setxkbmap -query | grep options || echo "options: (none)") # Check gsettings for key mappings local switch_apps switch_apps=$(gsettings get org.gnome.desktop.wm.keybindings switch-applications 2>/dev/null || echo "Not set") local switch_windows switch_windows=$(gsettings get org.gnome.desktop.wm.keybindings switch-windows 2>/dev/null || echo "Not set") status_info="Current Keyboard Configuration:\n\n" status_info+="XKB Options: ${xkb_options#options: }\n\n" status_info+="Switch Applications: $switch_apps\n" status_info+="Switch Windows: $switch_windows\n\n" # Check for autostart keyboard mapping if [[ -f "$HOME/.config/autostart/keyboard-mapping.desktop" ]]; then status_info+="Autostart Mapping: ✓ Enabled\n" else status_info+="Autostart Mapping: ✗ Not configured\n" fi ui_info "Keyboard Status" "$status_info" } reset_keyboard_layout() { log "Resetting keyboard layout to Ubuntu defaults" local info="Reset Keyboard Layout:\n\n" info+="This will restore Ubuntu's default keyboard configuration:\n\n" info+="• Remove all custom key mappings\n" info+="• Reset XKB options to defaults\n" info+="• Remove autostart keyboard configurations\n" info+="• Restore standard Ubuntu shortcuts\n\n" info+="⚠️ This will undo any custom keyboard configurations.\n\n" info+="Continue with reset?" if ui_yesno "Reset Keyboard" "$info"; then apply_keyboard_reset ui_msg "Success" "Keyboard layout reset to Ubuntu defaults!\n\nYou may need to log out and back in for all changes to take effect." fi } apply_macbook_keyboard_config() { log "Applying MacBook keyboard configuration" # Create autostart directory if it doesn't exist mkdir -p "$HOME/.config/autostart" # Create autostart file for MacBook keyboard mapping cat > "$HOME/.config/autostart/keyboard-mapping.desktop" </dev/null || log "Warning: Could not apply setxkbmap immediately" # Configure GNOME shortcuts for macOS-like behavior using Super (Command) key gsettings set org.gnome.desktop.wm.keybindings switch-applications "['Tab']" 2>/dev/null || log "Warning: Could not set switch-applications" gsettings set org.gnome.desktop.wm.keybindings switch-windows "['grave']" 2>/dev/null || log "Warning: Could not set switch-windows" # Set Super+Space for show applications (like Spotlight) gsettings set org.gnome.shell.keybindings toggle-overview "['space']" 2>/dev/null || log "Warning: Could not set toggle-overview" # Set macOS-style copy/paste shortcuts using Super (Command) key gsettings set org.gnome.desktop.wm.keybindings close "['w']" 2>/dev/null || log "Warning: Could not set close window" gsettings set org.gnome.settings-daemon.plugins.media-keys terminal "['t']" 2>/dev/null || log "Warning: Could not set terminal shortcut" # Create a flag file to indicate MacBook layout is active echo "macbook" > "$HOME/.config/ultrabunt-keyboard-layout" log "MacBook keyboard configuration applied" } apply_thinkpad_keyboard_config() { log "Applying ThinkPad keyboard configuration" # Create autostart directory if it doesn't exist mkdir -p "$HOME/.config/autostart" # Create autostart file for ThinkPad optimizations cat > "$HOME/.config/autostart/keyboard-mapping.desktop" </dev/null || log "Warning: Could not apply setxkbmap immediately" # Configure standard GNOME shortcuts gsettings set org.gnome.desktop.wm.keybindings switch-applications "['Tab']" 2>/dev/null || log "Warning: Could not set switch-applications" gsettings set org.gnome.desktop.wm.keybindings switch-windows "['grave']" 2>/dev/null || log "Warning: Could not set switch-windows" # Create a flag file to indicate ThinkPad layout is active echo "thinkpad" > "$HOME/.config/ultrabunt-keyboard-layout" log "ThinkPad keyboard configuration applied" } apply_generic_keyboard_config() { log "Applying generic laptop keyboard configuration" # Remove any existing autostart keyboard mapping rm -f "$HOME/.config/autostart/keyboard-mapping.desktop" 2>/dev/null || true # Reset XKB options to default setxkbmap -option 2>/dev/null || log "Warning: Could not reset setxkbmap options" # Configure standard GNOME shortcuts gsettings set org.gnome.desktop.wm.keybindings switch-applications "['Tab']" 2>/dev/null || log "Warning: Could not set switch-applications" gsettings set org.gnome.desktop.wm.keybindings switch-windows "['grave']" 2>/dev/null || log "Warning: Could not set switch-windows" # Create a flag file to indicate generic layout is active echo "generic" > "$HOME/.config/ultrabunt-keyboard-layout" log "Generic laptop keyboard configuration applied" } apply_keyboard_reset() { log "Resetting keyboard configuration to Ubuntu defaults" # Remove autostart keyboard mapping rm -f "$HOME/.config/autostart/keyboard-mapping.desktop" 2>/dev/null || true # Reset XKB options to default setxkbmap -option 2>/dev/null || log "Warning: Could not reset setxkbmap options" # Reset GNOME shortcuts to defaults gsettings reset org.gnome.desktop.wm.keybindings switch-applications 2>/dev/null || log "Warning: Could not reset switch-applications" gsettings reset org.gnome.desktop.wm.keybindings switch-windows 2>/dev/null || log "Warning: Could not reset switch-windows" gsettings reset org.gnome.shell.keybindings toggle-overview 2>/dev/null || log "Warning: Could not reset toggle-overview" # Remove the layout flag file rm -f "$HOME/.config/ultrabunt-keyboard-layout" 2>/dev/null || true log "Keyboard configuration reset to defaults" } # ============================================================================== # DATABASE MANAGEMENT FUNCTIONS # ============================================================================== show_database_management_menu() { log "Entering show_database_management_menu function" while true; do local menu_items=() # Check MariaDB status local mariadb_status="🔴 Not Running" if systemctl is-active --quiet mariadb; then mariadb_status="🟢 Running" fi menu_items+=("show-credentials" "(.Y.) Show MariaDB Root Credentials") menu_items+=("reset-root-password" "(.Y.) Reset MariaDB Root Password") menu_items+=("list-databases" "(.Y.) List All Databases") menu_items+=("list-users" "(.Y.) List Database Users") menu_items+=("create-database" "(.Y.) Create New Database") menu_items+=("create-user" "(.Y.) Create Database User") menu_items+=("backup-database" "(.Y.) Backup Database") menu_items+=("service-control" "(.Y.) MariaDB Service Control") # Show logout option if session is active if [[ "$MARIADB_SESSION_ACTIVE" == "true" ]]; then menu_items+=("logout-session" "(.Y.) Logout (Clear Saved Password)") fi menu_items+=("" "(_*_)") menu_items+=("zback" "(Z) Back to Main Menu") local choice choice=$(ui_menu "Database Management" \ "MariaDB Status: $mariadb_status\n\nSelect database management option:" \ 35 120 15 "${menu_items[@]}") || break case "$choice" in show-credentials) show_mariadb_credentials ;; reset-root-password) reset_mariadb_root_password ;; list-databases) list_databases ;; list-users) list_database_users ;; create-database) create_database_interactive ;; create-user) create_database_user_interactive ;; backup-database) backup_database_interactive ;; service-control) mariadb_service_control ;; logout-session) MARIADB_SESSION_ACTIVE=false MARIADB_SESSION_PASSWORD="" ui_msg "Session Cleared" "MariaDB session cleared successfully.\n\nYou will need to re-enter the password for future operations." log "MariaDB session cleared by user" ;; zback|back|"") break ;; esac done log "Exiting show_database_management_menu function" } # Function to prompt for MariaDB root password and test connection prompt_mariadb_password() { local password local attempt=1 local max_attempts=3 # Check if we have a valid session password first if [[ "$MARIADB_SESSION_ACTIVE" == "true" && -n "$MARIADB_SESSION_PASSWORD" ]]; then if mysql -u root -p"$MARIADB_SESSION_PASSWORD" -e "SELECT 1;" >/dev/null 2>&1; then echo "$MARIADB_SESSION_PASSWORD" return 0 else # Session password is invalid, reset session MARIADB_SESSION_ACTIVE=false MARIADB_SESSION_PASSWORD="" fi fi while [[ $attempt -le $max_attempts ]]; do password=$(ui_password "MariaDB Root Password" "Enter MariaDB root password (attempt $attempt/$max_attempts):") || return 1 if [[ -n "$password" ]] && mysql -u root -p"$password" -e "SELECT 1;" >/dev/null 2>&1; then # Store password in session for future use MARIADB_SESSION_PASSWORD="$password" MARIADB_SESSION_ACTIVE=true echo "$password" return 0 else if [[ $attempt -eq $max_attempts ]]; then ui_msg "Authentication Failed" "Failed to authenticate with MariaDB after $max_attempts attempts.\n\nPlease check:\n• Password is correct\n• MariaDB is running\n• Root user exists\n\nTry resetting the root password if needed." return 1 fi ui_msg "Invalid Password" "Password incorrect. Please try again.\n\nAttempt $attempt of $max_attempts" fi ((attempt++)) done return 1 } show_mariadb_credentials() { log "Displaying MariaDB credentials" # Check if password file exists and try to read it if sudo test -f "/root/.mysql_root_password"; then local root_pass root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null | tr -d '\n\r') if [[ -n "$root_pass" ]]; then local creds_msg="🔐 MariaDB Root Credentials\n\n" creds_msg+="• Username: root\n" creds_msg+="• Password: $root_pass\n" creds_msg+="• Host: localhost\n" creds_msg+="• Port: 3306\n\n" creds_msg+="🔧 Access Methods:\n" creds_msg+="• Command line: sudo mysql -u root -p\n" creds_msg+="• With password: mysql -u root -p'$root_pass'\n" creds_msg+="• Direct access: sudo mysql (as root user)\n\n" creds_msg+="📝 Password file: /root/.mysql_root_password\n" creds_msg+="📋 Copy password: $root_pass\n\n" creds_msg+="💡 Tips:\n" creds_msg+="• Use 'sudo mysql' for direct access without password\n" creds_msg+="• Create separate users for applications\n" creds_msg+="• Keep root password secure and backed up" ui_info "MariaDB Credentials" "$creds_msg" log "MariaDB root password displayed successfully" else ui_msg "No Password Found" "Root password file exists but is empty or unreadable.\n\nFile permissions: $(sudo ls -la /root/.mysql_root_password 2>/dev/null || echo 'File not accessible')\n\nYou may need to reset the root password." log "ERROR: Root password file is empty or unreadable" fi else # Check if we can access MariaDB without password (sudo method) if sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then local creds_msg="🔐 MariaDB Root Access Available\n\n" creds_msg+="• Username: root\n" creds_msg+="• Password: Not set (using sudo authentication)\n" creds_msg+="• Host: localhost\n" creds_msg+="• Port: 3306\n\n" creds_msg+="🔧 Access Method:\n" creds_msg+="• Command line: sudo mysql\n\n" creds_msg+="⚠️ Note: No password file found at /root/.mysql_root_password\n" creds_msg+="This means MariaDB is using system authentication.\n\n" creds_msg+="💡 To set a password, use the 'Reset Root Password' option." ui_info "MariaDB Access" "$creds_msg" log "MariaDB accessible via sudo, no password file found" else ui_msg "No Credentials Found" "MariaDB root password file not found and sudo access failed.\n\nThis could mean:\n• MariaDB was not installed through this script\n• Password file was deleted\n• MariaDB security setup was not completed\n• MariaDB service is not running\n\nYou can:\n• Check MariaDB status\n• Reset the root password\n• Reinstall MariaDB" log "ERROR: No MariaDB access method available" fi fi } reset_mariadb_root_password() { log "Resetting MariaDB root password" if ! systemctl is-active --quiet mariadb; then ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option." return fi if ui_yesno "Reset Root Password" "This will reset the MariaDB root password.\n\nContinue?"; then # Generate new password local new_pass new_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20) # Reset password if sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '${new_pass}';" 2>/dev/null; then # Save new password echo "$new_pass" | sudo tee /root/.mysql_root_password > /dev/null sudo chmod 600 /root/.mysql_root_password ui_info "Password Reset Complete" "🔐 New MariaDB Root Password:\n\n• Username: root\n• Password: $new_pass\n• Host: localhost\n\n📝 Password saved to: /root/.mysql_root_password\n\n✅ Password reset successful!" else ui_msg "Reset Failed" "Failed to reset MariaDB root password.\n\nThis could be due to:\n• Current password is different\n• MariaDB access issues\n• Permission problems\n\nTry using: sudo mysql_secure_installation" fi fi } list_databases() { log "Listing databases" # Check if MariaDB service is running if ! systemctl is-active --quiet mariadb; then ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option." return 1 fi # Try to connect and list databases local db_list local root_pass="" # Get root password if available if [[ -f "/root/.mysql_root_password" ]]; then root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null) fi # Try different authentication methods if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then db_list=$(mysql -u root -p"$root_pass" -e "SHOW DATABASES;" 2>/dev/null | tail -n +2) elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then db_list=$(sudo mysql -e "SHOW DATABASES;" 2>/dev/null | tail -n +2) else # Prompt for password if other methods fail local manual_pass if manual_pass=$(prompt_mariadb_password); then db_list=$(mysql -u root -p"$manual_pass" -e "SHOW DATABASES;" 2>/dev/null | tail -n +2) else ui_msg "Database Access Error" "Cannot connect to MariaDB.\n\nThis could be due to:\n• Authentication issues\n• MariaDB not properly configured\n• Permission problems\n\nTry resetting the root password or check MariaDB logs." return 1 fi fi if [[ -n "$db_list" ]]; then local db_msg="📊 MariaDB Databases\n\n" db_msg+="Available databases:\n\n" while IFS= read -r db; do if [[ "$db" == "information_schema" || "$db" == "performance_schema" || "$db" == "mysql" || "$db" == "sys" ]]; then db_msg+="• $db (system)\n" else db_msg+="• $db (user)\n" fi done <<< "$db_list" db_msg+="\n💡 System databases are used by MariaDB internally.\n" db_msg+="User databases contain your application data." ui_info "Database List" "$db_msg" else ui_msg "No Access" "Cannot access MariaDB databases.\n\nThis could be due to:\n• MariaDB not running\n• Authentication issues\n• Permission problems" fi } list_database_users() { log "Listing database users" # Check if MariaDB service is running if ! systemctl is-active --quiet mariadb; then ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option." return 1 fi # Try to connect and list users local user_list local root_pass="" # Get root password if available if [[ -f "/root/.mysql_root_password" ]]; then root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null) fi # Try different authentication methods if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then user_list=$(mysql -u root -p"$root_pass" -e "SELECT User, Host FROM mysql.user;" 2>/dev/null | tail -n +2) elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then user_list=$(sudo mysql -e "SELECT User, Host FROM mysql.user;" 2>/dev/null | tail -n +2) else # Prompt for password if other methods fail local manual_pass if manual_pass=$(prompt_mariadb_password); then user_list=$(mysql -u root -p"$manual_pass" -e "SELECT User, Host FROM mysql.user;" 2>/dev/null | tail -n +2) else ui_msg "Database Access Error" "Cannot connect to MariaDB.\n\nThis could be due to:\n• Authentication issues\n• MariaDB not properly configured\n• Permission problems\n\nTry resetting the root password or check MariaDB logs." return 1 fi fi if [[ -n "$user_list" ]]; then local user_msg="👥 MariaDB Users\n\n" user_msg+="Current database users:\n\n" while IFS=$'\t' read -r user host; do if [[ "$user" == "root" ]]; then user_msg+="• $user@$host (administrator)\n" elif [[ "$user" == "mysql.sys" || "$user" == "mysql.session" || "$user" == "mysql.infoschema" ]]; then user_msg+="• $user@$host (system)\n" else user_msg+="• $user@$host (application)\n" fi done <<< "$user_list" user_msg+="\n💡 Root users have full administrative access.\n" user_msg+="Application users should have limited privileges." ui_info "Database Users" "$user_msg" else ui_msg "No Access" "Cannot access MariaDB user list.\n\nThis could be due to:\n• MariaDB not running\n• Authentication issues\n• Permission problems" fi } create_database_interactive() { log "Creating database interactively" if ! systemctl is-active --quiet mariadb; then ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option." return fi local db_name db_name=$(ui_input "Database Name" "Enter database name:" "") || return if [[ -z "$db_name" ]]; then ui_msg "Invalid Input" "Database name cannot be empty." return fi # Validate database name if [[ ! "$db_name" =~ ^[a-zA-Z][a-zA-Z0-9_]*$ ]]; then ui_msg "Invalid Name" "Database name must:\n• Start with a letter\n• Contain only letters, numbers, and underscores\n• Not contain spaces or special characters" return fi # Try to create database with different authentication methods local root_pass="" local success=false # Get root password if available if [[ -f "/root/.mysql_root_password" ]]; then root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null) fi # Try different authentication methods if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then mysql -u root -p"$root_pass" -e "CREATE DATABASE ${db_name} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null && success=true elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then sudo mysql -e "CREATE DATABASE ${db_name} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null && success=true else # Prompt for password if other methods fail local manual_pass if manual_pass=$(prompt_mariadb_password); then mysql -u root -p"$manual_pass" -e "CREATE DATABASE ${db_name} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null && success=true fi fi if [[ "$success" == "true" ]]; then ui_info "Database Created" "✅ Database '$db_name' created successfully!\n\n• Name: $db_name\n• Character Set: utf8mb4\n• Collation: utf8mb4_unicode_ci\n\n💡 You can now create users and grant privileges to this database." else ui_msg "Creation Failed" "Failed to create database '$db_name'.\n\nThis could be due to:\n• Database already exists\n• Invalid name format\n• Permission issues\n• MariaDB access problems" fi } create_database_user_interactive() { log "Creating database user interactively" if ! systemctl is-active --quiet mariadb; then ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option." return fi local username username=$(ui_input "Username" "Enter username:" "") || return if [[ -z "$username" ]]; then ui_msg "Invalid Input" "Username cannot be empty." return fi local password password=$(ui_input "Password" "Enter password (leave empty for auto-generated):" "") || return if [[ -z "$password" ]]; then password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 16) fi local database database=$(ui_input "Database" "Enter database name to grant access to (optional):" "") || return # Create user with different authentication methods local root_pass="" local success=false # Get root password if available if [[ -f "/root/.mysql_root_password" ]]; then root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null) fi # Try different authentication methods if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then mysql -u root -p"$root_pass" -e "CREATE USER '${username}'@'localhost' IDENTIFIED BY '${password}';" 2>/dev/null && success=true elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then sudo mysql -e "CREATE USER '${username}'@'localhost' IDENTIFIED BY '${password}';" 2>/dev/null && success=true else # Prompt for password if other methods fail local manual_pass if manual_pass=$(prompt_mariadb_password); then mysql -u root -p"$manual_pass" -e "CREATE USER '${username}'@'localhost' IDENTIFIED BY '${password}';" 2>/dev/null && success=true fi fi # Create user if [[ "$success" == "true" ]]; then local success_msg="✅ User '$username' created successfully!\n\n• Username: $username\n• Password: $password\n• Host: localhost\n" # Grant privileges if database specified if [[ -n "$database" ]]; then if sudo mysql -e "GRANT ALL PRIVILEGES ON ${database}.* TO '${username}'@'localhost';" 2>/dev/null; then sudo mysql -e "FLUSH PRIVILEGES;" 2>/dev/null success_msg+="\n🔐 Privileges granted:\n• Full access to database: $database" else success_msg+="\n⚠️ Could not grant privileges to database: $database" fi else success_msg+="\n💡 No database privileges granted.\nUse GRANT commands to assign specific permissions." fi ui_info "User Created" "$success_msg" else ui_msg "Creation Failed" "Failed to create user '$username'.\n\nThis could be due to:\n• User already exists\n• Invalid username format\n• Permission issues\n• MariaDB access problems" fi } backup_database_interactive() { log "Starting interactive database backup" if ! systemctl is-active --quiet mariadb; then ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option." return fi # Get list of databases with different authentication methods local db_list local root_pass="" # Get root password if available if [[ -f "/root/.mysql_root_password" ]]; then root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null) fi # Try different authentication methods if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then db_list=$(mysql -u root -p"$root_pass" -e "SHOW DATABASES;" 2>/dev/null | tail -n +2 | grep -v -E '^(information_schema|performance_schema|mysql|sys)$') elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then db_list=$(sudo mysql -e "SHOW DATABASES;" 2>/dev/null | tail -n +2 | grep -v -E '^(information_schema|performance_schema|mysql|sys)$') else # Prompt for password if other methods fail local manual_pass if manual_pass=$(prompt_mariadb_password); then db_list=$(mysql -u root -p"$manual_pass" -e "SHOW DATABASES;" 2>/dev/null | tail -n +2 | grep -v -E '^(information_schema|performance_schema|mysql|sys)$') else ui_msg "Database Access Error" "Cannot connect to MariaDB to list databases." return fi fi if [[ -z "$db_list" ]]; then ui_msg "No Databases" "No user databases found to backup.\n\nOnly system databases are present." return fi local database database=$(ui_input "Database Name" "Enter database name to backup:\n\nAvailable databases:\n$(echo "$db_list" | sed 's/^/• /')" "") || return if [[ -z "$database" ]]; then ui_msg "Invalid Input" "Database name cannot be empty." return fi # Check if database exists if ! echo "$db_list" | grep -q "^${database}$"; then ui_msg "Database Not Found" "Database '$database' not found.\n\nAvailable databases:\n$(echo "$db_list" | sed 's/^/• /')" return fi local backup_dir="$BACKUP_DIR/database-backups" mkdir -p "$backup_dir" local backup_file="$backup_dir/${database}-$(date +%Y%m%d-%H%M%S).sql" # Perform backup with different authentication methods local backup_success=false # Try different authentication methods for mysqldump if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then mysqldump -u root -p"$root_pass" "$database" > "$backup_file" 2>/dev/null && backup_success=true elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then sudo mysqldump "$database" > "$backup_file" 2>/dev/null && backup_success=true else # Use the manual password if we got one earlier if [[ -n "$manual_pass" ]]; then mysqldump -u root -p"$manual_pass" "$database" > "$backup_file" 2>/dev/null && backup_success=true fi fi if [[ "$backup_success" == "true" ]]; then local file_size file_size=$(du -h "$backup_file" | cut -f1) ui_info "Backup Complete" "✅ Database backup successful!\n\n• Database: $database\n• Backup file: $backup_file\n• File size: $file_size\n• Timestamp: $(date)\n\n💾 Backup saved to:\n$backup_file\n\n💡 To restore:\nmysql $database < $backup_file" else ui_msg "Backup Failed" "Failed to backup database '$database'.\n\nThis could be due to:\n• Database access issues\n• Insufficient disk space\n• Permission problems\n• MariaDB connection issues" fi } mariadb_service_control() { log "MariaDB service control" local status_msg="🔧 MariaDB Service Control\n\n" if systemctl is-active --quiet mariadb; then status_msg+="Current Status: 🟢 Running\n\n" else status_msg+="Current Status: 🔴 Not Running\n\n" fi local menu_items=() menu_items+=("start" "(.Y.) Start MariaDB") menu_items+=("stop" "(.Y.) Stop MariaDB") menu_items+=("restart" "(.Y.) Restart MariaDB") menu_items+=("status" "(.Y.) Show Detailed Status") menu_items+=("enable" "(.Y.) Enable Auto-start") menu_items+=("disable" "(.Y.) Disable Auto-start") menu_items+=("logs" "(.Y.) View Recent Logs") menu_items+=("" "(_*_)") menu_items+=("back" "(B) Back") local choice choice=$(ui_menu "MariaDB Service Control" "$status_msg" \ 25 80 12 "${menu_items[@]}") || return case "$choice" in start) if sudo systemctl start mariadb 2>/dev/null; then ui_msg "Service Started" "✅ MariaDB service started successfully!" else ui_msg "Start Failed" "❌ Failed to start MariaDB service.\n\nCheck logs for details:\nsudo journalctl -u mariadb -n 20" fi ;; stop) if ui_yesno "Stop MariaDB" "This will stop the MariaDB service.\n\nAll database connections will be terminated.\n\nContinue?"; then if sudo systemctl stop mariadb 2>/dev/null; then ui_msg "Service Stopped" "✅ MariaDB service stopped successfully!" else ui_msg "Stop Failed" "❌ Failed to stop MariaDB service." fi fi ;; restart) if sudo systemctl restart mariadb 2>/dev/null; then ui_msg "Service Restarted" "✅ MariaDB service restarted successfully!" else ui_msg "Restart Failed" "❌ Failed to restart MariaDB service.\n\nCheck logs for details:\nsudo journalctl -u mariadb -n 20" fi ;; status) local detailed_status detailed_status=$(sudo systemctl status mariadb 2>/dev/null || echo "Status unavailable") ui_info "MariaDB Status" "$detailed_status" ;; enable) if sudo systemctl enable mariadb 2>/dev/null; then ui_msg "Auto-start Enabled" "✅ MariaDB will now start automatically on boot!" else ui_msg "Enable Failed" "❌ Failed to enable MariaDB auto-start." fi ;; disable) if ui_yesno "Disable Auto-start" "This will prevent MariaDB from starting automatically on boot.\n\nContinue?"; then if sudo systemctl disable mariadb 2>/dev/null; then ui_msg "Auto-start Disabled" "✅ MariaDB auto-start disabled!" else ui_msg "Disable Failed" "❌ Failed to disable MariaDB auto-start." fi fi ;; logs) local recent_logs recent_logs=$(sudo journalctl -u mariadb -n 30 --no-pager 2>/dev/null || echo "Logs unavailable") ui_info "MariaDB Logs" "$recent_logs" ;; esac } # ============================================================================== # MAIN PROGRAM # ============================================================================== main() { # Initialize init_logging log "Initializing Ultrabunt Ultimate Buntstaller..." ensure_deps log "Dependencies check completed" log "╔═══════════════════════════════════════════════════════╗" log "║ ULTRABUNT ULTIMATE BUNTSTALLER v4.2.0 STARTED ║" log "╚═══════════════════════════════════════════════════════╝" # Update buntage cache log "Starting APT cache update..." apt_update log "APT cache update completed" # Build buntage installation cache for fast lookups build_package_cache # Show main menu log "Loading main menu..." show_category_menu log "Main menu completed" # Farewell log "Ultrabunt Ultimate Buntstaller exiting." ui_msg "Goodbye!" "Thanks for bunting Ultrabunt Ultimate Buntstaller!\n\nLog: $LOGFILE\nBackups: $BACKUP_DIR\n\n💡 Tip: Reboot to apply all changes." } # Run main program main exit 0