Merge pull request #1660 from master3395/v2.5.5-dev

CyberPanel Plugin Store

Added upgrade button UI
Added revert version button in Grid and Table views
Implemented local timezone display for cache expiry
Fixed revert plugin JavaScript function
This commit is contained in:
Master3395
2026-01-27 00:34:32 +01:00
committed by GitHub
7 changed files with 1707 additions and 85 deletions

View File

@@ -81,7 +81,12 @@ detect_os() {
fi
# Detect OS
if echo $OUTPUT | grep -q "AlmaLinux 9" ; then
if echo $OUTPUT | grep -q "AlmaLinux 10" ; then
SERVER_OS="AlmaLinux10"
OS_FAMILY="rhel"
PACKAGE_MANAGER="dnf"
print_status "Detected: AlmaLinux 10"
elif echo $OUTPUT | grep -q "AlmaLinux 9" ; then
SERVER_OS="AlmaLinux9"
OS_FAMILY="rhel"
PACKAGE_MANAGER="dnf"
@@ -133,7 +138,7 @@ detect_os() {
print_status "Detected: Debian GNU/Linux 11"
else
print_status "ERROR: Unsupported OS detected"
print_status "Supported OS: AlmaLinux 8/9, CentOS 8/9, Rocky Linux 8/9, Ubuntu 20.04/22.04, Debian 11/12"
print_status "Supported OS: AlmaLinux 8/9/10, CentOS 8/9, Rocky Linux 8/9, Ubuntu 20.04/22.04, Debian 11/12"
return 1
fi
@@ -477,8 +482,8 @@ install_dependencies() {
echo ""
echo "Step 3/4: Installing core packages..."
if [ "$SERVER_OS" = "AlmaLinux9" ] || [ "$SERVER_OS" = "CentOS9" ] || [ "$SERVER_OS" = "RockyLinux9" ]; then
# AlmaLinux 9 / CentOS 9 / Rocky Linux 9
if [ "$SERVER_OS" = "AlmaLinux9" ] || [ "$SERVER_OS" = "AlmaLinux10" ] || [ "$SERVER_OS" = "CentOS9" ] || [ "$SERVER_OS" = "RockyLinux9" ]; then
# AlmaLinux 9/10 / CentOS 9 / Rocky Linux 9
$PACKAGE_MANAGER install -y ImageMagick gd libicu oniguruma python3 python3-pip python3-devel 2>/dev/null || true
$PACKAGE_MANAGER install -y aspell 2>/dev/null || print_status "WARNING: aspell not available, skipping..."
$PACKAGE_MANAGER install -y libc-client-devel 2>/dev/null || print_status "WARNING: libc-client-devel not available, skipping..."
@@ -609,18 +614,180 @@ install_cyberpanel_direct() {
mkdir -p "$temp_dir"
cd "$temp_dir" || return 1
# CRITICAL: Disable MariaDB 12.1 repository and add dnf exclude if MariaDB 10.x is installed
# This must be done BEFORE Pre_Install_Setup_Repository runs
if command -v rpm >/dev/null 2>&1; then
# Check if MariaDB 10.x is installed
if rpm -qa | grep -qiE "^(mariadb-server|mysql-server|MariaDB-server)" 2>/dev/null; then
local mariadb_version=$(mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [ -n "$mariadb_version" ]; then
local major_ver=$(echo "$mariadb_version" | cut -d. -f1)
local minor_ver=$(echo "$mariadb_version" | cut -d. -f2)
# Check if it's MariaDB 10.x (major version < 12)
if [ "$major_ver" -lt 12 ]; then
print_status "MariaDB $mariadb_version detected, adding dnf exclude to prevent upgrade attempts"
# Add MariaDB-server to dnf excludes (multiple formats for compatibility)
local dnf_conf="/etc/dnf/dnf.conf"
local exclude_added=false
if [ -f "$dnf_conf" ]; then
# Check if [main] section exists
if grep -q "^\[main\]" "$dnf_conf" 2>/dev/null; then
# [main] section exists, add exclude there
if ! grep -q "exclude=.*MariaDB-server" "$dnf_conf" 2>/dev/null; then
if grep -q "^exclude=" "$dnf_conf" 2>/dev/null; then
# Append to existing exclude line in [main] section
sed -i '/^\[main\]/,/^\[/ { /^exclude=/ s/$/ MariaDB-server*/ }' "$dnf_conf"
else
# Add new exclude line after [main]
sed -i '/^\[main\]/a exclude=MariaDB-server*' "$dnf_conf"
fi
exclude_added=true
fi
else
# No [main] section, add it with exclude
if ! grep -q "exclude=.*MariaDB-server" "$dnf_conf" 2>/dev/null; then
echo "" >> "$dnf_conf"
echo "[main]" >> "$dnf_conf"
echo "exclude=MariaDB-server*" >> "$dnf_conf"
exclude_added=true
fi
fi
else
# Create dnf.conf with exclude
echo "[main]" > "$dnf_conf"
echo "exclude=MariaDB-server*" >> "$dnf_conf"
exclude_added=true
fi
if [ "$exclude_added" = true ]; then
print_status "Added MariaDB-server* to dnf excludes in $dnf_conf"
fi
# Also add to yum.conf for compatibility
local yum_conf="/etc/yum.conf"
if [ -f "$yum_conf" ]; then
if ! grep -q "exclude=.*MariaDB-server" "$yum_conf" 2>/dev/null; then
if grep -q "^exclude=" "$yum_conf" 2>/dev/null; then
sed -i 's/^exclude=\(.*\)/exclude=\1 MariaDB-server*/' "$yum_conf"
else
echo "exclude=MariaDB-server*" >> "$yum_conf"
fi
print_status "Added MariaDB-server* to yum excludes"
fi
fi
# Create a function to disable MariaDB repositories (will be called after repository setup)
disable_mariadb_repos() {
local repo_files=(
"/etc/yum.repos.d/mariadb-main.repo"
"/etc/yum.repos.d/mariadb.repo"
"/etc/yum.repos.d/mariadb-12.1.repo"
)
# Also check for any mariadb repo files
while IFS= read -r repo_file; do
repo_files+=("$repo_file")
done < <(find /etc/yum.repos.d -name "*mariadb*.repo" 2>/dev/null)
for repo_file in "${repo_files[@]}"; do
if [ -f "$repo_file" ] && [ -n "$repo_file" ]; then
# First, try to disable by setting enabled=0
sed -i 's/^enabled\s*=\s*1/enabled=0/g' "$repo_file" 2>/dev/null
# If file contains MariaDB 12.1 references, disable or remove it
if grep -qi "mariadb.*12\|12.*mariadb\|mariadb-main" "$repo_file" 2>/dev/null; then
# Try to add enabled=0 to each [mariadb...] section
python3 -c "
import re
import sys
try:
with open('$repo_file', 'r') as f:
content = f.read()
# Replace enabled=1 with enabled=0
content = re.sub(r'(enabled\s*=\s*)1', r'\g<1>0', content, flags=re.IGNORECASE)
# Add enabled=0 after [mariadb...] sections if not present
lines = content.split('\n')
new_lines = []
in_mariadb_section = False
has_enabled = False
for i, line in enumerate(lines):
new_lines.append(line)
if re.match(r'^\s*\[.*mariadb.*\]', line, re.IGNORECASE):
in_mariadb_section = True
has_enabled = False
elif in_mariadb_section:
if re.match(r'^\s*enabled\s*=', line, re.IGNORECASE):
has_enabled = True
elif re.match(r'^\s*\[', line) and not re.match(r'^\s*\[.*mariadb.*\]', line, re.IGNORECASE):
if not has_enabled:
new_lines.insert(-1, 'enabled=0')
in_mariadb_section = False
has_enabled = False
if in_mariadb_section and not has_enabled:
new_lines.append('enabled=0')
with open('$repo_file', 'w') as f:
f.write('\n'.join(new_lines))
except:
# Fallback: just rename the file
import os
os.rename('$repo_file', '${repo_file}.disabled')
" 2>/dev/null || \
# Fallback: rename the file to disable it
mv "$repo_file" "${repo_file}.disabled" 2>/dev/null || true
fi
fi
done
}
# Export function so it can be called from installer
export -f disable_mariadb_repos
export MARIADB_VERSION="$mariadb_version"
# Also set up a background process to monitor and disable repos
(
while [ ! -f /tmp/cyberpanel_install_complete ]; do
sleep 2
if [ -f /etc/yum.repos.d/mariadb-main.repo ] || [ -f /etc/yum.repos.d/mariadb.repo ]; then
disable_mariadb_repos
fi
done
) &
local monitor_pid=$!
echo "$monitor_pid" > /tmp/cyberpanel_repo_monitor.pid
print_status "Started background process to monitor and disable MariaDB repositories"
fi
fi
fi
fi
# Download the working CyberPanel installation files
# Use master3395 fork which has our fixes
# Try to download the actual installer script (install/install.py) from the repository
echo "Downloading from: https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh"
# Try development branch first, fallback to stable
# First, try to download the repository archive to get the correct installer
local archive_url="https://github.com/master3395/cyberpanel/archive/v2.5.5-dev.tar.gz"
local installer_url="https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh"
# Test if the development branch exists
if ! curl -s --head "$installer_url" | grep -q "200 OK"; then
echo " Development branch not available, falling back to stable"
installer_url="https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh"
else
# Test if the development branch archive exists
if curl -s --head "$archive_url" | grep -q "200 OK"; then
echo " Using development branch (v2.5.5-dev) from master3395/cyberpanel"
else
echo " Development branch archive not available, trying installer script directly..."
# Test if the installer script exists
if ! curl -s --head "$installer_url" | grep -q "200 OK"; then
echo " Development branch not available, falling back to stable"
installer_url="https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh"
archive_url="https://github.com/master3395/cyberpanel/archive/stable.tar.gz"
fi
fi
curl --silent -o cyberpanel_installer.sh "$installer_url" 2>/dev/null
@@ -629,6 +796,71 @@ install_cyberpanel_direct() {
return 1
fi
# CRITICAL: Patch the installer script to skip MariaDB installation if 10.x is already installed
if [ -n "$MARIADB_VERSION" ] && [ "$major_ver" -lt 12 ] 2>/dev/null; then
print_status "Patching installer script to skip MariaDB installation..."
# Create a backup
cp cyberpanel_installer.sh cyberpanel_installer.sh.backup
# Use Python to properly patch the installer script
python3 -c "
import re
import sys
try:
with open('cyberpanel_installer.sh', 'r') as f:
content = f.read()
original_content = content
# Pattern: Add --exclude=MariaDB-server* to dnf/yum install commands that install mariadb-server
# Match: (dnf|yum) install [flags] [packages including mariadb-server]
def add_exclude(match):
cmd = match.group(0)
# Check if --exclude is already present
if '--exclude=MariaDB-server' in cmd:
return cmd
# Add --exclude=MariaDB-server* after install and flags, before packages
return re.sub(r'((?:dnf|yum)\s+install\s+(?:-[^\s]+\s+)*)', r'\1--exclude=MariaDB-server* ', cmd, flags=re.IGNORECASE)
# Find all dnf/yum install commands that mention mariadb-server
content = re.sub(
r'(?:dnf|yum)\s+install[^;]*?mariadb-server[^;]*',
add_exclude,
content,
flags=re.IGNORECASE | re.MULTILINE
)
# Also handle MariaDB-server (capitalized)
content = re.sub(
r'(?:dnf|yum)\s+install[^;]*?MariaDB-server[^;]*',
add_exclude,
content,
flags=re.IGNORECASE | re.MULTILINE
)
# Only write if content changed
if content != original_content:
with open('cyberpanel_installer.sh', 'w') as f:
f.write(content)
print('Installer script patched successfully')
else:
print('No changes needed in installer script')
except Exception as e:
print(f'Error patching installer script: {e}')
sys.exit(1)
" 2>/dev/null && print_status "Installer script patched successfully" || {
# Fallback: Simple sed-based patching if Python fails
sed -i 's/\(dnf\|yum\) install\([^;]*\)mariadb-server/\1 install\2--exclude=MariaDB-server* mariadb-server/gi' cyberpanel_installer.sh 2>/dev/null
sed -i 's/\(dnf\|yum\) install\([^;]*\)MariaDB-server/\1 install\2--exclude=MariaDB-server* MariaDB-server/gi' cyberpanel_installer.sh 2>/dev/null
print_status "Installer script patched (fallback method)"
}
print_status "Installer script patched to exclude MariaDB-server from installation"
fi
# Make script executable and verify
chmod 755 cyberpanel_installer.sh 2>/dev/null || true
if [ ! -x "cyberpanel_installer.sh" ]; then
@@ -657,13 +889,27 @@ install_cyberpanel_direct() {
# Copy install directory to current location
if [ "$installer_url" = "https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh" ]; then
cp -r cyberpanel-stable/install . 2>/dev/null || true
cp -r cyberpanel-stable/install.sh . 2>/dev/null || true
if [ -d "cyberpanel-stable" ]; then
cp -r cyberpanel-stable/install . 2>/dev/null || true
cp -r cyberpanel-stable/install.sh . 2>/dev/null || true
fi
else
cp -r cyberpanel-v2.5.5-dev/install . 2>/dev/null || true
cp -r cyberpanel-v2.5.5-dev/install.sh . 2>/dev/null || true
if [ -d "cyberpanel-v2.5.5-dev" ]; then
cp -r cyberpanel-v2.5.5-dev/install . 2>/dev/null || true
cp -r cyberpanel-v2.5.5-dev/install.sh . 2>/dev/null || true
fi
fi
# Verify install directory was copied
if [ ! -d "install" ]; then
print_status "ERROR: install directory not found after extraction"
print_status "Archive contents:"
ls -la 2>/dev/null | head -20
return 1
fi
print_status "Verified install directory exists"
echo " ✓ CyberPanel installation files downloaded"
echo " 🔄 Starting CyberPanel installation..."
echo ""
@@ -700,27 +946,301 @@ install_cyberpanel_direct() {
fi
echo ""
# Run installer and show live output, capturing the password
# Use bash to execute to avoid permission issues
local installer_script="cyberpanel_installer.sh"
if [ ! -f "$installer_script" ]; then
print_status "ERROR: cyberpanel_installer.sh not found in current directory: $(pwd)"
return 1
# CRITICAL: Use install/install.py directly instead of cyberpanel_installer.sh
# The cyberpanel_installer.sh is the old wrapper that doesn't support auto-install
# install/install.py is the actual installer that we can control
local installer_py="install/install.py"
if [ -f "$installer_py" ]; then
print_status "Using install/install.py directly for installation (non-interactive mode)"
# CRITICAL: Patch install.py to exclude MariaDB-server from dnf/yum commands
if [ -n "$MARIADB_VERSION" ] && [ "$major_ver" -lt 12 ] 2>/dev/null; then
print_status "Patching install.py to exclude MariaDB-server from installation commands..."
# Create backup
cp "$installer_py" "${installer_py}.backup" 2>/dev/null || true
# Patch install.py to add --exclude=MariaDB-server* to dnf/yum install commands
python3 -c "
import re
import sys
try:
with open('$installer_py', 'r') as f:
content = f.read()
original_content = content
# Pattern: Add --exclude=MariaDB-server* to dnf/yum install commands that install mariadb-server
def add_exclude(match):
cmd = match.group(0)
# Check if --exclude is already present
if '--exclude=MariaDB-server' in cmd:
return cmd
# Add --exclude=MariaDB-server* after install and flags, before packages
return re.sub(r'((?:dnf|yum)\s+install\s+(?:-[^\s]+\s+)*)', r'\1--exclude=MariaDB-server* ', cmd, flags=re.IGNORECASE)
# Find all dnf/yum install commands that mention mariadb-server
content = re.sub(
r'(?:dnf|yum)\s+install[^;]*?mariadb-server[^;]*',
add_exclude,
content,
flags=re.IGNORECASE | re.MULTILINE
)
# Also handle MariaDB-server (capitalized) and in Python strings
content = re.sub(
r'(\"|\')(?:dnf|yum)\s+install[^\"]*?mariadb-server[^\"]*(\"|\')',
lambda m: m.group(1) + re.sub(r'((?:dnf|yum)\s+install\s+(?:-[^\s]+\s+)*)', r'\1--exclude=MariaDB-server* ', m.group(0)[1:-1], flags=re.IGNORECASE) + m.group(2),
content,
flags=re.IGNORECASE | re.MULTILINE
)
# Only write if content changed
if content != original_content:
with open('$installer_py', 'w') as f:
f.write(content)
print('install.py patched successfully')
else:
print('No changes needed in install.py')
except Exception as e:
print(f'Error patching install.py: {e}')
sys.exit(1)
" 2>/dev/null && print_status "install.py patched successfully" || {
# Fallback: Simple sed-based patching if Python fails
sed -i 's/\(dnf\|yum\) install\([^;]*\)mariadb-server/\1 install\2--exclude=MariaDB-server* mariadb-server/gi' "$installer_py" 2>/dev/null
sed -i 's/\(dnf\|yum\) install\([^;]*\)MariaDB-server/\1 install\2--exclude=MariaDB-server* MariaDB-server/gi' "$installer_py" 2>/dev/null
print_status "install.py patched (fallback method)"
}
fi
# If MariaDB 10.x is installed, disable repositories right before running installer
if [ -n "$MARIADB_VERSION" ] && [ -f /tmp/cyberpanel_repo_monitor.pid ]; then
# Call the disable function one more time before installer runs
if type disable_mariadb_repos >/dev/null 2>&1; then
disable_mariadb_repos
fi
fi
# Get server IP address (required by install.py)
local server_ip
if command -v curl >/dev/null 2>&1; then
server_ip=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://icanhazip.com 2>/dev/null || echo "")
fi
if [ -z "$server_ip" ]; then
# Fallback: try to get IP from network interfaces
server_ip=$(ip route get 8.8.8.8 2>/dev/null | awk '{print $7; exit}' || \
hostname -I 2>/dev/null | awk '{print $1}' || \
echo "127.0.0.1")
fi
if [ -z "$server_ip" ] || [ "$server_ip" = "127.0.0.1" ]; then
print_status "WARNING: Could not detect public IP, using 127.0.0.1"
server_ip="127.0.0.1"
fi
print_status "Detected server IP: $server_ip"
# CRITICAL: Install Python MySQL dependencies before running install.py
# installCyberPanel.py requires MySQLdb (mysqlclient) which needs development headers
echo ""
echo "==============================================================================================================="
echo "Installing Python MySQL dependencies (required for installCyberPanel.py)..."
echo "==============================================================================================================="
print_status "Installing Python MySQL dependencies..."
# Detect OS for package installation
local os_family=""
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
almalinux|rocky|centos|rhel|fedora)
os_family="rhel"
print_status "Detected RHEL-based OS: $ID"
;;
ubuntu|debian)
os_family="debian"
print_status "Detected Debian-based OS: $ID"
;;
*)
print_status "Unknown OS ID: $ID, defaulting to RHEL-based"
os_family="rhel"
;;
esac
else
print_status "WARNING: /etc/os-release not found, defaulting to RHEL-based"
os_family="rhel"
fi
# Install MariaDB/MySQL development headers and Python mysqlclient
if [ "$os_family" = "rhel" ]; then
# RHEL-based (AlmaLinux, Rocky, CentOS, RHEL)
print_status "Installing MariaDB development headers for RHEL-based system..."
# Try to install mariadb-devel (works with MariaDB 10.x and 12.x)
# NOTE: We need mariadb-devel even if we excluded MariaDB-server
# The exclude only applies to MariaDB-server, not development packages
if command -v dnf >/dev/null 2>&1; then
# For AlmaLinux 9/10 and newer - show output for debugging
print_status "Attempting to install mariadb-devel (development headers only, not server)..."
# Temporarily remove exclude for devel packages if needed
local dnf_exclude_backup=""
if [ -f /etc/dnf/dnf.conf ] && grep -q "exclude=.*MariaDB" /etc/dnf/dnf.conf; then
# Check if exclude is too broad
if grep -q "exclude=.*MariaDB-server.*MariaDB-devel" /etc/dnf/dnf.conf || \
grep -q "exclude=.*MariaDB\*" /etc/dnf/dnf.conf; then
print_status "Temporarily adjusting dnf exclude to allow mariadb-devel installation..."
# We only want to exclude MariaDB-server, not devel packages
sed -i 's/exclude=\(.*\)MariaDB-server\(.*\)MariaDB-devel\(.*\)/exclude=\1MariaDB-server\2\3/' /etc/dnf/dnf.conf 2>/dev/null || true
sed -i 's/exclude=\(.*\)MariaDB\*\(.*\)/exclude=\1MariaDB-server*\2/' /etc/dnf/dnf.conf 2>/dev/null || true
fi
fi
if dnf install -y --allowerasing --skip-broken --nobest \
mariadb-devel pkgconfig gcc python3-devel python3-pip; then
print_status "✓ Successfully installed mariadb-devel"
elif dnf install -y --allowerasing --skip-broken --nobest \
mysql-devel pkgconfig gcc python3-devel python3-pip; then
print_status "✓ Successfully installed mysql-devel"
elif dnf install -y --allowerasing --skip-broken --nobest \
mariadb-connector-c-devel pkgconfig gcc python3-devel python3-pip; then
print_status "✓ Successfully installed mariadb-connector-c-devel"
else
print_status "⚠️ WARNING: Failed to install MariaDB development headers"
print_status "This may cause MySQLdb installation to fail"
fi
else
# For older systems with yum
print_status "Using yum to install mariadb-devel..."
if yum install -y mariadb-devel pkgconfig gcc python3-devel python3-pip; then
print_status "✓ Successfully installed mariadb-devel"
elif yum install -y mysql-devel pkgconfig gcc python3-devel python3-pip; then
print_status "✓ Successfully installed mysql-devel"
else
print_status "⚠️ WARNING: Failed to install MariaDB development headers"
fi
fi
# Install mysqlclient Python package
print_status "Installing mysqlclient Python package..."
python3 -m pip install --upgrade pip setuptools wheel 2>&1 | grep -v "already satisfied" || true
if python3 -m pip install mysqlclient 2>&1; then
print_status "✓ Successfully installed mysqlclient"
else
# If pip install fails, try with build dependencies
print_status "Retrying mysqlclient installation with build dependencies..."
python3 -m pip install --no-cache-dir mysqlclient 2>&1 || {
print_status "⚠️ WARNING: Failed to install mysqlclient, trying alternative method..."
# Try installing from source
python3 -m pip install --no-binary mysqlclient mysqlclient 2>&1 || true
}
fi
elif [ "$os_family" = "debian" ]; then
# Debian-based (Ubuntu, Debian)
print_status "Installing MariaDB development headers for Debian-based system..."
apt-get update -y
if apt-get install -y libmariadb-dev libmariadb-dev-compat pkg-config build-essential python3-dev python3-pip; then
print_status "✓ Successfully installed MariaDB development headers"
elif apt-get install -y default-libmysqlclient-dev pkg-config build-essential python3-dev python3-pip; then
print_status "✓ Successfully installed MySQL development headers"
else
print_status "⚠️ WARNING: Failed to install MariaDB/MySQL development headers"
fi
# Install mysqlclient Python package
print_status "Installing mysqlclient Python package..."
python3 -m pip install --upgrade pip setuptools wheel 2>&1 | grep -v "already satisfied" || true
if python3 -m pip install mysqlclient 2>&1; then
print_status "✓ Successfully installed mysqlclient"
else
print_status "Retrying mysqlclient installation with build dependencies..."
python3 -m pip install --no-cache-dir mysqlclient 2>&1 || true
fi
fi
# Verify MySQLdb is available
print_status "Verifying MySQLdb module availability..."
if python3 -c "import MySQLdb; print('MySQLdb version:', MySQLdb.__version__)" 2>&1; then
print_status "✓ MySQLdb module is available and working"
else
print_status "⚠️ WARNING: MySQLdb module not available"
print_status "Attempting to diagnose the issue..."
python3 -c "import sys; print('Python path:', sys.path)" 2>&1 || true
python3 -m pip list | grep -i mysql || print_status "No MySQL-related packages found in pip list"
print_status "Attempting to continue anyway, but installation may fail..."
fi
echo ""
# Build installer arguments based on user preferences
# install.py requires publicip as first positional argument
local install_args=("$server_ip")
# Add optional arguments based on user preferences
# Default: OpenLiteSpeed, Full installation (postfix, powerdns, ftp), Local MySQL
# These match what the user selected in the interactive prompts
install_args+=("--postfix" "ON")
install_args+=("--powerdns" "ON")
install_args+=("--ftp" "ON")
install_args+=("--remotemysql" "OFF")
if [ "$DEBUG_MODE" = true ]; then
# Note: install.py doesn't have --debug, but we can set it via environment
export DEBUG_MODE=true
fi
# Run the Python installer directly
if [ "$DEBUG_MODE" = true ]; then
python3 "$installer_py" "${install_args[@]}" 2>&1 | tee /var/log/CyberPanel/install_output.log
else
python3 "$installer_py" "${install_args[@]}" 2>&1 | tee /var/log/CyberPanel/install_output.log
fi
else
# Fallback to cyberpanel_installer.sh if install.py not found
print_status "WARNING: install/install.py not found, using cyberpanel_installer.sh (may be interactive)"
local installer_script="cyberpanel_installer.sh"
if [ ! -f "$installer_script" ]; then
print_status "ERROR: cyberpanel_installer.sh not found in current directory: $(pwd)"
return 1
fi
# Get absolute path to installer script
local installer_path
if [[ "$installer_script" = /* ]]; then
installer_path="$installer_script"
else
installer_path="$(pwd)/$installer_script"
fi
# If MariaDB 10.x is installed, disable repositories right before running installer
if [ -n "$MARIADB_VERSION" ] && [ -f /tmp/cyberpanel_repo_monitor.pid ]; then
# Call the disable function one more time before installer runs
if type disable_mariadb_repos >/dev/null 2>&1; then
disable_mariadb_repos
fi
fi
if [ "$DEBUG_MODE" = true ]; then
bash "$installer_path" --debug 2>&1 | tee /var/log/CyberPanel/install_output.log
else
bash "$installer_path" 2>&1 | tee /var/log/CyberPanel/install_output.log
fi
fi
# Get absolute path to installer script
local installer_path
if [[ "$installer_script" = /* ]]; then
installer_path="$installer_script"
else
installer_path="$(pwd)/$installer_script"
fi
local install_exit_code=${PIPESTATUS[0]}
if [ "$DEBUG_MODE" = true ]; then
bash "$installer_path" --debug 2>&1 | tee /var/log/CyberPanel/install_output.log
else
bash "$installer_path" 2>&1 | tee /var/log/CyberPanel/install_output.log
# Stop the repository monitor
if [ -f /tmp/cyberpanel_repo_monitor.pid ]; then
local monitor_pid=$(cat /tmp/cyberpanel_repo_monitor.pid 2>/dev/null)
if [ -n "$monitor_pid" ] && kill -0 "$monitor_pid" 2>/dev/null; then
kill "$monitor_pid" 2>/dev/null
fi
rm -f /tmp/cyberpanel_repo_monitor.pid
fi
touch /tmp/cyberpanel_install_complete 2>/dev/null || true
local install_exit_code=${PIPESTATUS[0]}

View File

@@ -345,40 +345,67 @@ class preFlightsChecks:
self.stdOut(f"Successfully installed alternative: {alt_package}", 1)
break
# Install MariaDB with enhanced AlmaLinux 9.6 support
self.stdOut("Installing MariaDB for AlmaLinux 9.6...", 1)
# Disable MariaDB 12.1 repository if MariaDB 10.x is already installed
# This prevents upgrade attempts in Pre_Install_Required_Components
self.disableMariaDB12RepositoryIfNeeded()
# Try multiple installation methods for maximum compatibility
mariadb_commands = [
"dnf install -y mariadb-server mariadb-devel mariadb-client --skip-broken --nobest",
"dnf install -y mariadb-server mariadb-devel mariadb-client --allowerasing",
"dnf install -y mariadb-server mariadb-devel --skip-broken --nobest --allowerasing",
"dnf install -y mariadb-server --skip-broken --nobest --allowerasing"
]
# CRITICAL: Remove conflicting MariaDB compat packages before installation
# These packages from MariaDB 12.1 can conflict with MariaDB 10.11
self.stdOut("Removing conflicting MariaDB compat packages...", 1)
try:
subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30)
subprocess.run("dnf remove -y 'MariaDB-server-compat*' 2>/dev/null || true", shell=True, timeout=60)
r = subprocess.run("rpm -qa 2>/dev/null | grep -i MariaDB-server-compat", shell=True, capture_output=True, text=True, timeout=30)
for line in (r.stdout or "").strip().splitlines():
pkg = (line.strip().split() or [""])[0]
if pkg and "MariaDB-server-compat" in pkg:
subprocess.run(["rpm", "-e", "--nodeps", pkg], timeout=30)
self.stdOut("Removed conflicting MariaDB compat packages", 1)
except Exception as e:
self.stdOut("Warning: Could not remove compat packages: " + str(e), 0)
mariadb_installed = False
for cmd in mariadb_commands:
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
# Check if MariaDB is already installed before attempting installation
is_installed, installed_version, major_minor = self.checkExistingMariaDB()
if is_installed:
self.stdOut(f"MariaDB/MySQL is already installed (version: {installed_version}), skipping installation", 1)
mariadb_installed = True
else:
# Install MariaDB with enhanced AlmaLinux 9.6 support
self.stdOut("Installing MariaDB for AlmaLinux 9.6...", 1)
# Try multiple installation methods for maximum compatibility
mariadb_commands = [
"dnf install -y mariadb-server mariadb-devel mariadb-client --skip-broken --nobest",
"dnf install -y mariadb-server mariadb-devel mariadb-client --allowerasing",
"dnf install -y mariadb-server mariadb-devel --skip-broken --nobest --allowerasing",
"dnf install -y mariadb-server --skip-broken --nobest --allowerasing"
]
mariadb_installed = False
for cmd in mariadb_commands:
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
mariadb_installed = True
self.stdOut(f"MariaDB installed successfully with command: {cmd}", 1)
break
except subprocess.TimeoutExpired:
self.stdOut(f"Timeout installing MariaDB with command: {cmd}", 0)
continue
except Exception as e:
self.stdOut(f"Error installing MariaDB with command: {cmd} - {str(e)}", 0)
continue
if not mariadb_installed:
self.stdOut("MariaDB installation failed, trying MySQL as fallback...", 0)
try:
command = "dnf install -y mysql-server mysql-devel --skip-broken --nobest --allowerasing"
self.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
self.stdOut("MySQL installed as fallback for MariaDB", 1)
mariadb_installed = True
self.stdOut(f"MariaDB installed successfully with command: {cmd}", 1)
break
except subprocess.TimeoutExpired:
self.stdOut(f"Timeout installing MariaDB with command: {cmd}", 0)
continue
except Exception as e:
self.stdOut(f"Error installing MariaDB with command: {cmd} - {str(e)}", 0)
continue
if not mariadb_installed:
self.stdOut("MariaDB installation failed, trying MySQL as fallback...", 0)
try:
command = "dnf install -y mysql-server mysql-devel --skip-broken --nobest --allowerasing"
self.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
self.stdOut("MySQL installed as fallback for MariaDB", 1)
except:
self.stdOut("Both MariaDB and MySQL installation failed", 0)
except:
self.stdOut("Both MariaDB and MySQL installation failed", 0)
# Install additional required packages
self.stdOut("Installing additional required packages...", 1)
@@ -1238,6 +1265,13 @@ class preFlightsChecks:
self.stdOut("Installing custom binaries...", 1)
try:
# Ensure /usr/local/lsws/bin exists (dnf openlitespeed may use different layout)
ols_bin_dir = os.path.dirname(OLS_BINARY_PATH)
os.makedirs(ols_bin_dir, mode=0o755, exist_ok=True)
ols_base = os.path.dirname(ols_bin_dir)
if not os.path.isdir(ols_base):
os.makedirs(ols_base, mode=0o755, exist_ok=True)
# Make binary executable before moving
os.chmod(tmp_binary, 0o755)
@@ -1451,6 +1485,124 @@ module cyberpanel_ols {
except:
return False
def disableMariaDB12RepositoryIfNeeded(self):
"""Disable MariaDB 12.1 repository if MariaDB 10.x is already installed to prevent upgrade attempts"""
try:
is_installed, installed_version, major_minor = self.checkExistingMariaDB()
if is_installed and major_minor and major_minor != "unknown":
try:
major_ver = float(major_minor)
if major_ver < 12.0:
# MariaDB 10.x is installed, disable 12.1 repository to prevent upgrade attempts
self.stdOut(f"MariaDB {installed_version} detected, disabling MariaDB 12.1 repository to prevent upgrade conflicts", 1)
logging.InstallLog.writeToFile(f"MariaDB {installed_version} detected, disabling MariaDB 12.1 repository")
# Disable MariaDB 12.1 repository - check all possible repo file locations
repo_files = [
'/etc/yum.repos.d/mariadb-main.repo',
'/etc/yum.repos.d/mariadb.repo',
'/etc/yum.repos.d/mariadb-12.1.repo',
'/etc/yum.repos.d/mariadb-main.repo.bak'
]
# Also check for any mariadb repo files
import glob
repo_files.extend(glob.glob('/etc/yum.repos.d/*mariadb*.repo'))
disabled_any = False
for repo_file in repo_files:
if os.path.exists(repo_file):
try:
# Read the file
with open(repo_file, 'r') as f:
lines = f.readlines()
# Modify the file to disable all MariaDB repositories
modified = False
new_lines = []
in_mariadb_section = False
for line in lines:
# Check if we're entering a MariaDB repository section
if line.strip().startswith('[') and 'mariadb' in line.lower():
in_mariadb_section = True
new_lines.append(line)
# Add enabled=0 if not already present
if 'enabled' not in line.lower():
new_lines.append('enabled=0\n')
modified = True
elif in_mariadb_section:
# If we see enabled=1, change it to enabled=0
if line.strip().startswith('enabled=') and 'enabled=0' not in line.lower():
new_lines.append('enabled=0\n')
modified = True
elif line.strip().startswith('['):
# New section, exit MariaDB section
in_mariadb_section = False
new_lines.append(line)
else:
new_lines.append(line)
else:
new_lines.append(line)
# Write back if modified
if modified:
with open(repo_file, 'w') as f:
f.writelines(new_lines)
self.stdOut(f"Disabled MariaDB repository in {repo_file}", 1)
logging.InstallLog.writeToFile(f"Disabled MariaDB repository in {repo_file}")
disabled_any = True
except Exception as e:
self.stdOut(f"Warning: Could not disable repository {repo_file}: {e}", 1)
logging.InstallLog.writeToFile(f"Warning: Could not disable repository {repo_file}: {e}")
# Always exclude MariaDB-server from dnf/yum operations to prevent upgrades
try:
# Add exclude to dnf.conf
dnf_conf = '/etc/dnf/dnf.conf'
exclude_line = 'exclude=MariaDB-server'
if os.path.exists(dnf_conf):
with open(dnf_conf, 'r') as f:
dnf_content = f.read()
# Check if exclude line already exists
if exclude_line not in dnf_content:
# Check if there's already an exclude line
if 'exclude=' in dnf_content:
# Append to existing exclude line
dnf_content = re.sub(r'(exclude=.*)', r'\1 MariaDB-server', dnf_content)
else:
# Add new exclude line
dnf_content = dnf_content.rstrip() + '\n' + exclude_line + '\n'
with open(dnf_conf, 'w') as f:
f.write(dnf_content)
self.stdOut("Added MariaDB-server to dnf excludes to prevent upgrade", 1)
logging.InstallLog.writeToFile("Added MariaDB-server to dnf excludes")
else:
# Create dnf.conf with exclude
with open(dnf_conf, 'w') as f:
f.write('[main]\n')
f.write(exclude_line + '\n')
self.stdOut("Created dnf.conf with MariaDB-server exclude", 1)
logging.InstallLog.writeToFile("Created dnf.conf with MariaDB-server exclude")
except Exception as e:
self.stdOut(f"Warning: Could not add exclude to dnf.conf: {e}", 1)
logging.InstallLog.writeToFile(f"Warning: Could not add exclude to dnf.conf: {e}")
return True
except (ValueError, TypeError):
pass
return False
except Exception as e:
self.stdOut(f"Warning: Error checking MariaDB repository: {e}", 1)
logging.InstallLog.writeToFile(f"Warning: Error checking MariaDB repository: {e}")
return False
def checkExistingMariaDB(self):
"""Check if MariaDB/MySQL is already installed and return version info"""
try:
@@ -1659,7 +1811,42 @@ module cyberpanel_ols {
else:
# RHEL-based MariaDB installation
command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=12.1'
# CRITICAL: Remove conflicting MariaDB compat packages first
# These packages from MariaDB 12.1 can conflict with MariaDB 10.11
self.stdOut("Removing conflicting MariaDB compat packages...", 1)
try:
subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30)
subprocess.run("dnf remove -y 'MariaDB-server-compat*' 2>/dev/null || true", shell=True, timeout=60)
r = subprocess.run("rpm -qa 2>/dev/null | grep -i MariaDB-server-compat", shell=True, capture_output=True, text=True, timeout=30)
for line in (r.stdout or "").strip().splitlines():
pkg = (line.strip().split() or [""])[0]
if pkg and "MariaDB-server-compat" in pkg:
subprocess.run(["rpm", "-e", "--nodeps", pkg], timeout=30)
self.stdOut("Removed conflicting MariaDB compat packages", 1)
except Exception as e:
self.stdOut("Warning: Could not remove compat packages: " + str(e), 0)
# Check if MariaDB is already installed before setting up repository
is_installed, installed_version, major_minor = self.checkExistingMariaDB()
if is_installed:
self.stdOut(f"MariaDB/MySQL is already installed (version: {installed_version}), skipping installation", 1)
# Don't set up 12.1 repository if 10.x is installed to avoid upgrade issues
if major_minor and major_minor != "unknown":
try:
major_ver = float(major_minor)
if major_ver < 12.0:
self.stdOut("Skipping MariaDB 12.1 repository setup to avoid upgrade conflicts", 1)
self.stdOut("Using existing MariaDB installation", 1)
self.startMariaDB()
self.changeMYSQLRootPassword()
self.fixMariaDB()
return True
except (ValueError, TypeError):
pass
# Set up MariaDB 12.1 repository only if not already installed
command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version=12.1'
self.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True)
command = 'dnf install mariadb-server mariadb-devel mariadb-client-utils -y'
@@ -6246,6 +6433,25 @@ def main():
# Apply OS-specific fixes early in the installation process
checks.apply_os_specific_fixes()
# CRITICAL: Disable MariaDB 12.1 repository and add dnf exclude BEFORE any MariaDB installation attempts
# This must run before Pre_Install_Required_Components tries to install MariaDB
checks.disableMariaDB12RepositoryIfNeeded()
# CRITICAL: Remove MariaDB-server-compat* before ANY MariaDB installation
# This package conflicts with MariaDB 10.11 and must be removed early
preFlightsChecks.stdOut("Removing conflicting MariaDB-server-compat packages...", 1)
try:
subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30)
subprocess.run("dnf remove -y 'MariaDB-server-compat*' 2>/dev/null || true", shell=True, timeout=60)
r = subprocess.run("rpm -qa 2>/dev/null | grep -i MariaDB-server-compat", shell=True, capture_output=True, text=True, timeout=30)
for line in (r.stdout or "").strip().splitlines():
pkg = (line.strip().split() or [""])[0]
if pkg and "MariaDB-server-compat" in pkg:
subprocess.run(["rpm", "-e", "--nodeps", pkg], timeout=30)
preFlightsChecks.stdOut("MariaDB compat cleanup completed", 1)
except Exception as e:
preFlightsChecks.stdOut("Warning: compat cleanup: " + str(e), 0)
# Ensure MySQL password file is created early to prevent FileNotFoundError
checks.ensure_mysql_password_file()
@@ -6273,6 +6479,10 @@ def main():
# Apply AlmaLinux 9 comprehensive fixes first if needed
if checks.is_almalinux9():
checks.fix_almalinux9_comprehensive()
# Disable MariaDB 12.1 repository if MariaDB 10.x is already installed
# This prevents upgrade attempts in Pre_Install_Required_Components
checks.disableMariaDB12RepositoryIfNeeded()
# Install core services in the correct order
checks.installLiteSpeed(ent, serial)

View File

@@ -556,14 +556,31 @@ def call(command, distro, bracket, message, log=0, do_exit=0, code=os.EX_OK, she
os._exit(code)
return False
# CRITICAL: Use shell=True for commands with shell metacharacters
# Avoids "No matching repo to modify: 2>/dev/null, true, ||" when shlex.split splits them
if not shell and any(x in command for x in (' || ', ' 2>/dev', ' 2>', ' | ', '; true', '|| true')):
shell = True
# CRITICAL: For mysql/mariadb commands, always use shell=True and full binary path
# This fixes "No such file or directory: 'mysql'" when run via shlex.split
if not shell and ('mysql' in command or 'mariadb' in command):
import re
mysql_bin = '/usr/bin/mariadb' if os.path.exists('/usr/bin/mariadb') else '/usr/bin/mysql'
if not os.path.exists(mysql_bin):
mysql_bin = '/usr/bin/mysql'
# Replace only leading "mysql" or "mariadb" (executable), not "mysql" in SQL like "use mysql;"
if re.match(r'^\s*(sudo\s+)?(mysql|mariadb)\s', command):
command = re.sub(r'^(\s*)(?:sudo\s+)?(mysql|mariadb)(\s)', r'\g<1>' + mysql_bin + r'\g<3>', command, count=1)
shell = True
finalMessage = 'Running: %s' % (message)
stdOut(finalMessage, log)
count = 0
while True:
if shell == False:
res = subprocess.call(shlex.split(command))
else:
if shell:
res = subprocess.call(command, shell=True)
else:
res = subprocess.call(shlex.split(command))
if resFailed(distro, res):
count = count + 1

View File

@@ -4299,6 +4299,19 @@ echo $oConfig->Save() ? 'Done' : 'Error';
Upgrade.stdOut("Applying AlmaLinux 9 MariaDB fixes...", 1)
try:
# CRITICAL: Remove MariaDB-server-compat* before any MariaDB install (conflicts with 10.11)
Upgrade.stdOut("Removing conflicting MariaDB-server-compat packages...", 1)
try:
subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30)
subprocess.run("dnf remove -y 'MariaDB-server-compat*' 2>/dev/null || true", shell=True, timeout=60)
r = subprocess.run("rpm -qa 2>/dev/null | grep -i MariaDB-server-compat", shell=True, capture_output=True, text=True, timeout=30)
for line in (r.stdout or "").strip().splitlines():
pkg = (line.strip().split() or [""])[0]
if pkg and "MariaDB-server-compat" in pkg:
subprocess.run(["rpm", "-e", "--nodeps", pkg], timeout=30)
except Exception as e:
Upgrade.stdOut("Warning: compat cleanup: " + str(e), 0)
# Disable problematic MariaDB MaxScale repository
Upgrade.stdOut("Disabling problematic MariaDB MaxScale repository...", 1)
command = "dnf config-manager --disable mariadb-maxscale 2>/dev/null || true"
@@ -4320,9 +4333,9 @@ echo $oConfig->Save() ? 'Done' : 'Error';
command = "dnf clean all"
subprocess.run(command, shell=True, capture_output=True)
# Install MariaDB from official repository
# Install MariaDB 10.11 from official repository (avoid 12.1 compat conflicts)
Upgrade.stdOut("Setting up official MariaDB repository...", 1)
command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='12.1'"
command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='10.11'"
result = subprocess.run(command, shell=True, capture_output=True, text=True)
if result.returncode != 0:
Upgrade.stdOut(f"Warning: MariaDB repo setup failed: {result.stderr}", 0)

View File

@@ -709,6 +709,32 @@
cursor: not-allowed;
}
.btn-upgrade {
padding: 8px 16px;
background: #f59e0b;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-upgrade:hover:not(:disabled) {
background: #d97706;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(245,158,11,0.3);
}
.btn-upgrade:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-link {
padding: 6px 12px;
background: var(--bg-secondary, #f8f9ff);
@@ -772,6 +798,17 @@
box-shadow: 0 4px 8px rgba(88,86,214,0.3);
}
.btn-revert {
background: #6c757d;
color: white;
}
.btn-revert:hover:not(:disabled) {
background: #5a6268;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(108,117,125,0.3);
}
.btn-uninstall {
background: #dc3545;
color: white;
@@ -1042,6 +1079,9 @@
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
</button>
{% endif %}
<button class="btn-action btn-revert btn-small" onclick="showRevertDialog('{{ plugin.plugin_dir }}')" title="{% trans 'Revert to Previous Version' %}">
<i class="fas fa-undo"></i> {% trans "Revert Version" %}
</button>
<button class="btn-action btn-uninstall btn-small" onclick="uninstallPlugin('{{ plugin.plugin_dir }}')">
<i class="fas fa-trash"></i> {% trans "Uninstall" %}
</button>
@@ -1123,6 +1163,9 @@
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
</button>
{% endif %}
<button class="btn-action btn-revert" onclick="showRevertDialog('{{ plugin.plugin_dir }}')" title="{% trans 'Revert to Previous Version' %}">
<i class="fas fa-undo"></i> {% trans "Revert Version" %}
</button>
<button class="btn-action btn-uninstall" onclick="uninstallPlugin('{{ plugin.plugin_dir }}')">
<i class="fas fa-trash"></i> {% trans "Uninstall" %}
</button>
@@ -1194,6 +1237,17 @@
<!-- CyberPanel Plugin Store (always available) -->
<div id="storeView" style="display: {% if not plugins %}block{% else %}none{% endif %};">
<!-- Loading Indicator -->
<div id="storeLoading" class="store-loading" style="display: {% if not plugins %}block{% else %}none{% endif %};">
<i class="fas fa-spinner fa-spin"></i> {% trans "Loading plugins from store..." %}
</div>
<!-- Store Error -->
<div id="storeError" class="alert alert-danger" style="display: none;">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span id="storeErrorText"></span>
</div>
<!-- Notice Section (similar to CMSMS Module Manager) -->
<div class="store-notice" id="pluginStoreNotice">
<div class="notice-header">
@@ -1208,6 +1262,9 @@
<i class="fas fa-info-circle" style="color: #5856d6;"></i>
<strong>{% trans "Cache Information:" %}</strong>
{% trans "Plugin store data is cached for 1 hour to improve performance and reduce GitHub API rate limits. New plugins may take up to 1 hour to appear after being published." %}
{% if cache_expiry_timestamp %}
<br><strong>{% trans "Next cache update:" %}</strong> <span id="cacheExpiryTime" style="font-family: monospace;" data-timestamp="{{ cache_expiry_timestamp }}">{% trans "Calculating..." %}</span>
{% endif %}
</p>
<p class="warning-text">
<i class="fas fa-exclamation-triangle"></i>
@@ -1410,12 +1467,20 @@ function displayStorePlugins() {
<i class="${iconClass}"></i>
</div>`;
// Action column - Store view only shows Install/Installed (no Deactivate/Uninstall)
// Action column - Store view only shows Install/Installed/Upgrade (no Deactivate/Uninstall)
// NOTE: Store view should NOT show Deactivate/Uninstall buttons - users manage from Grid/Table views
let actionHtml = '';
if (plugin.installed) {
// Show "Installed" text
actionHtml = '<span class="status-installed">Installed</span>';
// Check if update is available
if (plugin.update_available) {
// Show Upgrade button
actionHtml = `<button class="btn-action btn-upgrade" onclick="upgradePlugin('${plugin.plugin_dir}', '${plugin.installed_version || 'Unknown'}', '${plugin.version || 'Unknown'}')">
<i class="fas fa-arrow-up"></i> Upgrade
</button>`;
} else {
// Show "Installed" text
actionHtml = '<span class="status-installed">Installed</span>';
}
} else {
// Show Install button
actionHtml = `<button class="btn-action btn-install" onclick="installFromStore('${plugin.plugin_dir}')">
@@ -1486,6 +1551,83 @@ function filterByLetter(letter) {
displayStorePlugins();
}
function upgradePlugin(pluginName, currentVersion, newVersion) {
// Show confirmation dialog with backup warning
const message = `⚠️ WARNING: Plugin Upgrade\n\n` +
`You are about to upgrade ${pluginName} from version ${currentVersion} to ${newVersion}.\n\n` +
`⚠️ IMPORTANT: You could lose data during the upgrade process.\n\n` +
`Please ensure you have backed up:\n` +
`• Plugin configuration files\n` +
`• Plugin data and databases\n` +
`• Any custom modifications\n\n` +
`Do you want to continue with the upgrade?`;
if (!confirm(message)) {
return;
}
// Double confirmation
if (!confirm(`Final confirmation: Upgrade ${pluginName} now?\n\nThis action cannot be undone.`)) {
return;
}
const btn = event.target.closest('.btn-upgrade') || event.target;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Upgrading...';
fetch(`/plugins/api/store/upgrade/${pluginName}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Upgrade Successful!',
text: data.message || `Plugin ${pluginName} upgraded successfully`,
type: 'success'
});
} else {
alert('Success: ' + (data.message || `Plugin ${pluginName} upgraded successfully`));
}
// Reload page after short delay to show success message
setTimeout(() => {
location.reload();
}, 1500);
} else {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Upgrade Failed!',
text: data.error || 'Failed to upgrade plugin',
type: 'error'
});
} else {
alert('Error: ' + (data.error || 'Failed to upgrade plugin'));
}
btn.disabled = false;
btn.innerHTML = originalText;
}
})
.catch(error => {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: 'Failed to upgrade plugin: ' + error.message,
type: 'error'
});
} else {
alert('Error: Failed to upgrade plugin - ' + error.message);
}
btn.disabled = false;
btn.innerHTML = originalText;
});
}
function installFromStore(pluginName) {
if (!confirm(`Install ${pluginName} from the CyberPanel Plugin Store?`)) {
return;
@@ -1659,6 +1801,165 @@ function installPlugin(pluginName) {
});
}
function showRevertDialog(pluginName) {
// Fetch available backups
fetch(`/plugins/api/backups/${pluginName}/`)
.then(response => response.json())
.then(data => {
if (!data.success) {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: data.error || 'Failed to load backups',
type: 'error'
});
} else {
alert('Error: ' + (data.error || 'Failed to load backups'));
}
return;
}
if (!data.backups || data.backups.length === 0) {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'No Backups Available',
text: `No backups found for ${pluginName}. Backups are automatically created before upgrades.`,
type: 'info'
});
} else {
alert(`No backups found for ${pluginName}. Backups are automatically created before upgrades.`);
}
return;
}
// Show backup selection dialog
let backupList = 'Available backups:\n\n';
data.backups.forEach((backup, index) => {
const version = backup.version || 'unknown';
const timestamp = backup.timestamp || backup.created_at || 'unknown';
backupList += `${index + 1}. Version ${version} (${timestamp})\n`;
});
backupList += '\n⚠ WARNING: Reverting will replace the current plugin version with the selected backup.\n';
backupList += 'This action cannot be undone. Continue?';
if (!confirm(backupList)) {
return;
}
// Ask which backup to restore (for now, restore the most recent)
// In a more advanced UI, you could show a dropdown
const selectedBackup = data.backups[0]; // Most recent backup
if (!confirm(`Revert ${pluginName} to version ${selectedBackup.version}?\n\nThis will replace the current installation.`)) {
return;
}
// Perform revert
revertPlugin(pluginName, selectedBackup.backup_path);
})
.catch(error => {
const errorMessage = error && error.message ? error.message : (error ? String(error) : 'Unknown error');
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: 'Failed to load backups: ' + errorMessage,
type: 'error'
});
} else {
alert('Error: Failed to load backups - ' + errorMessage);
}
});
}
function revertPlugin(pluginName, backupPath) {
// Find the revert button for this plugin (if it exists)
let btn = null;
let originalText = '';
// Try to find button by looking for onclick attribute containing the plugin name
const revertButtons = document.querySelectorAll('.btn-revert');
for (let button of revertButtons) {
const onclickAttr = button.getAttribute('onclick') || '';
if (onclickAttr.includes(pluginName)) {
btn = button;
originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Reverting...';
break;
}
}
// Show loading notification if button not found
if (!btn) {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Reverting...',
text: `Reverting ${pluginName} to previous version...`,
type: 'info'
});
}
}
fetch(`/plugins/api/revert/${pluginName}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
},
body: JSON.stringify({
backup_path: backupPath
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Revert Successful!',
text: data.message || `Plugin ${pluginName} reverted successfully`,
type: 'success'
});
} else {
alert('Success: ' + (data.message || `Plugin ${pluginName} reverted successfully`));
}
// Reload page after short delay
setTimeout(() => {
location.reload();
}, 1500);
} else {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Revert Failed!',
text: data.error || 'Failed to revert plugin',
type: 'error'
});
} else {
alert('Error: ' + (data.error || 'Failed to revert plugin'));
}
if (btn) {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
})
.catch(error => {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: 'Failed to revert plugin: ' + error.message,
type: 'error'
});
} else {
alert('Error: Failed to revert plugin - ' + error.message);
}
if (btn) {
btn.disabled = false;
btn.innerHTML = originalText;
}
});
}
function uninstallPlugin(pluginName) {
if (!confirm(`Are you sure you want to uninstall ${pluginName}? All data from this plugin will be deleted.`)) {
return;
@@ -1848,7 +2149,63 @@ if (localStorage.getItem('pluginStoreNoticeDismissed') === 'true') {
}
// Initialize view on page load
// Convert cache expiry timestamp to local time
function updateCacheExpiryTime() {
const expiryElement = document.getElementById('cacheExpiryTime');
if (!expiryElement) return;
const timestamp = expiryElement.getAttribute('data-timestamp');
if (!timestamp) return;
try {
// Convert Unix timestamp (seconds) to milliseconds for JavaScript Date
const timestampMs = parseFloat(timestamp) * 1000;
const expiryDate = new Date(timestampMs);
// Check if date is valid
if (isNaN(expiryDate.getTime())) {
expiryElement.textContent = 'Invalid timestamp';
return;
}
// Get user's locale preferences
const locale = navigator.language || navigator.userLanguage || 'en-US';
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Format date and time according to user's locale
const dateOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit'
};
const timeOptions = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
};
// Format date and time separately for better readability
const dateStr = expiryDate.toLocaleDateString(locale, dateOptions);
const timeStr = expiryDate.toLocaleTimeString(locale, timeOptions);
// Combine with timezone abbreviation
const formatted = dateStr + ' ' + timeStr;
// Display with timezone info
expiryElement.textContent = formatted;
expiryElement.title = 'Local time: ' + formatted + ' | Timezone: ' + timezone;
} catch (e) {
console.error('Error formatting cache expiry time:', e);
expiryElement.textContent = 'Error calculating time';
}
}
document.addEventListener('DOMContentLoaded', function() {
// Update cache expiry time to local timezone
updateCacheExpiryTime();
// Default to grid view if plugins exist, otherwise show store
const gridView = document.getElementById('gridView');
const storeView = document.getElementById('storeView');
@@ -1858,6 +2215,14 @@ document.addEventListener('DOMContentLoaded', function() {
// No plugins installed, show store by default
toggleView('store');
}
// Load store plugins if store view is visible (either from toggleView or already displayed)
setTimeout(function() {
const storeViewCheck = document.getElementById('storeView');
if (storeViewCheck && storeViewCheck.style.display !== 'none' && storePlugins.length === 0) {
loadPluginStore();
}
}, 100); // Small delay to ensure DOM is ready
});
</script>
{% endblock %}

View File

@@ -10,5 +10,8 @@ urlpatterns = [
path('api/disable/<str:plugin_name>/', views.disable_plugin, name='disable_plugin'),
path('api/store/plugins/', views.fetch_plugin_store, name='fetch_plugin_store'),
path('api/store/install/<str:plugin_name>/', views.install_from_store, name='install_from_store'),
path('api/store/upgrade/<str:plugin_name>/', views.upgrade_plugin, name='upgrade_plugin'),
path('api/backups/<str:plugin_name>/', views.get_plugin_backups, name='get_plugin_backups'),
path('api/revert/<str:plugin_name>/', views.revert_plugin, name='revert_plugin'),
path('<str:plugin_name>/help/', views.plugin_help, name='plugin_help'),
]

View File

@@ -26,11 +26,15 @@ PLUGIN_STATE_DIR = '/home/cyberpanel/plugin_states'
# Plugin store cache configuration
PLUGIN_STORE_CACHE_DIR = '/home/cyberpanel/plugin_store_cache'
PLUGIN_STORE_CACHE_FILE = os.path.join(PLUGIN_STORE_CACHE_DIR, 'plugins_cache.json')
PLUGIN_STORE_CACHE_DURATION = 3600 # Cache for 1 hour (3600 seconds)
PLUGIN_STORE_CACHE_DURATION = 3600 # Base cache duration: 1 hour (3600 seconds)
PLUGIN_STORE_CACHE_RANDOM_OFFSET = 600 # Random offset: ±10 minutes (600 seconds) to prevent simultaneous requests
GITHUB_REPO_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins/contents'
GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/master3395/cyberpanel-plugins/main'
GITHUB_COMMITS_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins/commits'
# Plugin backup configuration
PLUGIN_BACKUP_DIR = '/home/cyberpanel/plugin_backups'
def _get_plugin_state_file(plugin_name):
"""Get the path to the plugin state file"""
if not os.path.exists(PLUGIN_STATE_DIR):
@@ -364,9 +368,13 @@ def installed(request):
for p in pluginList:
logging.writeToFile(f" - {p.get('plugin_dir')}: installed={p.get('installed')}, enabled={p.get('enabled')}")
# Get cache expiry timestamp for display (will be converted to local time in browser)
cache_expiry_timestamp, _ = _get_cache_expiry_time()
proc = httpProc(request, 'pluginHolder/plugins.html',
{'plugins': pluginList, 'error_plugins': errorPlugins,
'installed_count': installed_count, 'active_count': active_count}, 'admin')
'installed_count': installed_count, 'active_count': active_count,
'cache_expiry_timestamp': cache_expiry_timestamp}, 'admin')
return proc.render()
@csrf_exempt
@@ -604,6 +612,37 @@ def _ensure_cache_dir():
except Exception as e:
logging.writeToFile(f"Error creating cache directory: {str(e)}")
def _get_cache_expiry_time():
"""Get the cache expiry time (when cache will be updated next)
Returns:
tuple: (expiry_timestamp, expiry_datetime_string) or (None, None) if no cache
expiry_timestamp is Unix timestamp for JavaScript conversion to local time
"""
try:
if not os.path.exists(PLUGIN_STORE_CACHE_FILE):
return None, None
# Try to read stored expiry time from cache metadata
try:
with open(PLUGIN_STORE_CACHE_FILE, 'r', encoding='utf-8') as f:
cache_data = json.load(f)
stored_expiry = cache_data.get('expiry_timestamp')
if stored_expiry:
# Return timestamp for JavaScript to convert to local time
return stored_expiry, None
except:
pass # Fall back to calculation if metadata not found
# Fallback: calculate from file modification time (for old cache files)
cache_mtime = os.path.getmtime(PLUGIN_STORE_CACHE_FILE)
expiry_timestamp = cache_mtime + PLUGIN_STORE_CACHE_DURATION
return expiry_timestamp, None
except Exception as e:
logging.writeToFile(f"Error getting cache expiry time: {str(e)}")
return None, None
def _get_cached_plugins(allow_expired=False):
"""Get plugins from cache if available and not expired
@@ -614,22 +653,32 @@ def _get_cached_plugins(allow_expired=False):
if not os.path.exists(PLUGIN_STORE_CACHE_FILE):
return None
# Check if cache is expired
cache_mtime = os.path.getmtime(PLUGIN_STORE_CACHE_FILE)
cache_age = time.time() - cache_mtime
# Read cache file to get stored expiry time
with open(PLUGIN_STORE_CACHE_FILE, 'r', encoding='utf-8') as f:
cache_data = json.load(f)
if cache_age > PLUGIN_STORE_CACHE_DURATION:
# Check expiry using stored timestamp if available, otherwise fall back to file mtime
current_time = time.time()
stored_expiry = cache_data.get('expiry_timestamp')
if stored_expiry:
# Use stored expiry time (with randomization)
cache_age = current_time - (stored_expiry - cache_data.get('cache_duration', PLUGIN_STORE_CACHE_DURATION))
is_expired = current_time >= stored_expiry
else:
# Fallback for old cache files without expiry metadata
cache_mtime = os.path.getmtime(PLUGIN_STORE_CACHE_FILE)
cache_age = current_time - cache_mtime
is_expired = cache_age > PLUGIN_STORE_CACHE_DURATION
if is_expired:
if not allow_expired:
logging.writeToFile(f"Plugin store cache expired (age: {cache_age:.0f}s)")
return None
else:
logging.writeToFile(f"Using expired cache as fallback (age: {cache_age:.0f}s)")
# Read cache file
with open(PLUGIN_STORE_CACHE_FILE, 'r', encoding='utf-8') as f:
cache_data = json.load(f)
if not allow_expired or cache_age <= PLUGIN_STORE_CACHE_DURATION:
if not allow_expired or not is_expired:
logging.writeToFile(f"Using cached plugin store data (age: {cache_age:.0f}s)")
return cache_data.get('plugins', [])
except Exception as e:
@@ -637,20 +686,213 @@ def _get_cached_plugins(allow_expired=False):
return None
def _save_plugins_cache(plugins):
"""Save plugins to cache"""
"""Save plugins to cache with randomized expiry time"""
try:
_ensure_cache_dir()
# Generate random cache duration to prevent simultaneous requests from all CyberPanel instances
# Base duration ± random offset (e.g., 1 hour ± 10 minutes)
import random
random_offset = random.randint(-PLUGIN_STORE_CACHE_RANDOM_OFFSET, PLUGIN_STORE_CACHE_RANDOM_OFFSET)
actual_cache_duration = PLUGIN_STORE_CACHE_DURATION + random_offset
# Calculate expiry timestamp
current_time = time.time()
expiry_timestamp = current_time + actual_cache_duration
cache_data = {
'plugins': plugins,
'cached_at': datetime.now().isoformat(),
'cache_duration': PLUGIN_STORE_CACHE_DURATION
'expiry_timestamp': expiry_timestamp,
'cache_duration': actual_cache_duration,
'base_duration': PLUGIN_STORE_CACHE_DURATION,
'random_offset': random_offset
}
with open(PLUGIN_STORE_CACHE_FILE, 'w', encoding='utf-8') as f:
json.dump(cache_data, f, indent=2, ensure_ascii=False)
logging.writeToFile("Plugin store cache saved successfully")
expiry_datetime = datetime.fromtimestamp(expiry_timestamp)
logging.writeToFile(f"Plugin store cache saved successfully. Expires at: {expiry_datetime.strftime('%Y-%m-%d %H:%M:%S')} (duration: {actual_cache_duration}s, offset: {random_offset:+d}s)")
except Exception as e:
logging.writeToFile(f"Error saving plugin store cache: {str(e)}")
def _compare_versions(version1, version2):
"""
Compare two version strings (semantic versioning)
Returns: 1 if version1 > version2, -1 if version1 < version2, 0 if equal
"""
try:
# Split versions into parts
v1_parts = [int(x) for x in version1.split('.')]
v2_parts = [int(x) for x in version2.split('.')]
# Pad shorter version with zeros
max_len = max(len(v1_parts), len(v2_parts))
v1_parts.extend([0] * (max_len - len(v1_parts)))
v2_parts.extend([0] * (max_len - len(v2_parts)))
# Compare each part
for v1, v2 in zip(v1_parts, v2_parts):
if v1 > v2:
return 1
elif v1 < v2:
return -1
return 0
except:
# Fallback to string comparison if parsing fails
if version1 > version2:
return 1
elif version1 < version2:
return -1
return 0
def _get_installed_version(plugin_dir, plugin_install_dir):
"""Get installed version of a plugin from meta.xml"""
installed_path = os.path.join(plugin_install_dir, plugin_dir)
meta_path = os.path.join(installed_path, 'meta.xml')
if os.path.exists(meta_path):
try:
pluginMetaData = ElementTree.parse(meta_path)
root = pluginMetaData.getroot()
version_elem = root.find('version')
if version_elem is not None and version_elem.text:
return version_elem.text.strip()
except Exception as e:
logging.writeToFile(f"Error reading version from {meta_path}: {str(e)}")
return None
def _create_plugin_backup(plugin_name, plugin_install_dir='/usr/local/CyberCP'):
"""
Create a backup of a plugin before upgrade
Returns: (backup_path, backup_info) or (None, None) on failure
"""
try:
# Ensure backup directory exists
if not os.path.exists(PLUGIN_BACKUP_DIR):
os.makedirs(PLUGIN_BACKUP_DIR, mode=0o755)
plugin_path = os.path.join(plugin_install_dir, plugin_name)
if not os.path.exists(plugin_path):
return None, None
# Get current version
installed_version = _get_installed_version(plugin_name, plugin_install_dir) or 'unknown'
# Create backup directory with timestamp
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_name = f"{plugin_name}_v{installed_version}_{timestamp}"
backup_path = os.path.join(PLUGIN_BACKUP_DIR, backup_name)
# Copy plugin directory
import shutil
shutil.copytree(plugin_path, backup_path)
# Create backup metadata
backup_info = {
'plugin_name': plugin_name,
'version': installed_version,
'timestamp': timestamp,
'backup_path': backup_path,
'created_at': datetime.now().isoformat()
}
# Save metadata as JSON
metadata_file = os.path.join(backup_path, '.backup_metadata.json')
with open(metadata_file, 'w') as f:
json.dump(backup_info, f, indent=2)
logging.writeToFile(f"Created backup for {plugin_name} version {installed_version} at {backup_path}")
return backup_path, backup_info
except Exception as e:
logging.writeToFile(f"Error creating backup for {plugin_name}: {str(e)}")
return None, None
def _get_plugin_backups(plugin_name):
"""Get list of available backups for a plugin"""
backups = []
if not os.path.exists(PLUGIN_BACKUP_DIR):
return backups
try:
for item in os.listdir(PLUGIN_BACKUP_DIR):
if item.startswith(plugin_name + '_'):
backup_path = os.path.join(PLUGIN_BACKUP_DIR, item)
if os.path.isdir(backup_path):
metadata_file = os.path.join(backup_path, '.backup_metadata.json')
if os.path.exists(metadata_file):
try:
with open(metadata_file, 'r') as f:
backup_info = json.load(f)
backups.append(backup_info)
except:
# Fallback: parse from directory name
parts = item.split('_')
if len(parts) >= 3:
version = parts[1].replace('v', '')
timestamp = '_'.join(parts[2:])
backups.append({
'plugin_name': plugin_name,
'version': version,
'timestamp': timestamp,
'backup_path': backup_path,
'created_at': timestamp
})
else:
# No metadata, try to parse from directory name
parts = item.split('_')
if len(parts) >= 3:
version = parts[1].replace('v', '')
timestamp = '_'.join(parts[2:])
backups.append({
'plugin_name': plugin_name,
'version': version,
'timestamp': timestamp,
'backup_path': backup_path,
'created_at': timestamp
})
# Sort by timestamp (newest first)
backups.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
except Exception as e:
logging.writeToFile(f"Error listing backups for {plugin_name}: {str(e)}")
return backups
def _restore_plugin_from_backup(plugin_name, backup_path):
"""Restore a plugin from a backup"""
try:
plugin_install_dir = '/usr/local/CyberCP'
plugin_path = os.path.join(plugin_install_dir, plugin_name)
# Remove current plugin installation
if os.path.exists(plugin_path):
import shutil
shutil.rmtree(plugin_path)
# Restore from backup
import shutil
shutil.copytree(backup_path, plugin_path)
# Remove backup metadata file from restored plugin
metadata_file = os.path.join(plugin_path, '.backup_metadata.json')
if os.path.exists(metadata_file):
os.remove(metadata_file)
logging.writeToFile(f"Restored {plugin_name} from backup {backup_path}")
return True
except Exception as e:
logging.writeToFile(f"Error restoring {plugin_name} from backup: {str(e)}")
return False
def _enrich_store_plugins(plugins):
"""Enrich store plugins with installed/enabled status from local system"""
enriched = []
@@ -672,8 +914,22 @@ def _enrich_store_plugins(plugins):
# Check if plugin is enabled (only if installed)
if plugin['installed']:
plugin['enabled'] = _is_plugin_enabled(plugin_dir)
# Check for updates by comparing versions
installed_version = _get_installed_version(plugin_dir, plugin_install_dir)
store_version = plugin.get('version', '0.0.0')
if installed_version and store_version:
# Update available if store version is newer
plugin['update_available'] = _compare_versions(store_version, installed_version) > 0
plugin['installed_version'] = installed_version
else:
plugin['update_available'] = False
plugin['installed_version'] = installed_version or 'Unknown'
else:
plugin['enabled'] = False
plugin['update_available'] = False
plugin['installed_version'] = None
# Ensure is_paid field exists and is properly set (default to False if not set or invalid)
# Handle all possible cases: missing, None, empty string, string values, boolean
@@ -907,6 +1163,244 @@ def fetch_plugin_store(request):
'plugins': []
}, status=500)
@csrf_exempt
@require_http_methods(["POST"])
def upgrade_plugin(request, plugin_name):
"""Upgrade an installed plugin from GitHub store"""
mailUtilities.checkHome()
try:
# Check if plugin is installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if not os.path.exists(pluginInstalled):
return JsonResponse({
'success': False,
'error': f'Plugin not installed: {plugin_name}'
}, status=400)
# Get current version before upgrade
installed_version = _get_installed_version(plugin_name, '/usr/local/CyberCP')
# Create automatic backup before upgrade
backup_path, backup_info = _create_plugin_backup(plugin_name)
if backup_path:
logging.writeToFile(f"Created automatic backup for {plugin_name} before upgrade: {backup_path}")
else:
logging.writeToFile(f"Warning: Failed to create backup for {plugin_name}, continuing with upgrade anyway")
logging.writeToFile(f"Starting upgrade of {plugin_name} from version {installed_version}")
# Download and install plugin from GitHub (same as install_from_store)
import tempfile
import shutil
import zipfile
import io
# Create temporary directory
temp_dir = tempfile.mkdtemp()
zip_path = os.path.join(temp_dir, plugin_name + '.zip')
try:
# Download from GitHub
repo_zip_url = 'https://github.com/master3395/cyberpanel-plugins/archive/refs/heads/main.zip'
logging.writeToFile(f"Downloading plugin upgrade from: {repo_zip_url}")
repo_req = urllib.request.Request(
repo_zip_url,
headers={
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
'Accept': 'application/zip'
}
)
with urllib.request.urlopen(repo_req, timeout=30) as repo_response:
repo_zip_data = repo_response.read()
# Extract plugin directory from repository ZIP
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
# Find plugin directory in ZIP
plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/'
plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)]
if not plugin_files:
raise Exception(f'Plugin {plugin_name} not found in GitHub repository')
logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub")
# Create plugin ZIP file from GitHub with correct structure
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
for file_path in plugin_files:
relative_path = file_path[len(plugin_prefix):]
if relative_path: # Skip directories
file_data = repo_zip.read(file_path)
arcname = os.path.join(plugin_name, relative_path)
plugin_zip.writestr(arcname, file_data)
plugin_zip.close()
repo_zip.close()
# Verify ZIP was created
if not os.path.exists(zip_path):
raise Exception(f'Failed to create plugin ZIP file')
logging.writeToFile(f"Created plugin ZIP: {zip_path}")
# Copy ZIP to current directory (pluginInstaller expects it in cwd)
original_cwd = os.getcwd()
os.chdir(temp_dir)
try:
zip_file = plugin_name + '.zip'
if not os.path.exists(zip_file):
raise Exception(f'Zip file {zip_file} not found in temp directory')
logging.writeToFile(f"Upgrading plugin using pluginInstaller")
# Install using pluginInstaller (this will overwrite existing files)
try:
pluginInstaller.installPlugin(plugin_name)
except Exception as install_error:
error_msg = str(install_error)
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
# Check if plugin directory exists despite the error
if not os.path.exists(pluginInstalled):
raise Exception(f'Plugin upgrade failed: {error_msg}')
# Wait for file system to sync
import time
time.sleep(3)
# Verify plugin was upgraded
if not os.path.exists(pluginInstalled):
raise Exception(f'Plugin upgrade failed: {pluginInstalled} does not exist after upgrade')
# Get new version
new_version = _get_installed_version(plugin_name, '/usr/local/CyberCP')
logging.writeToFile(f"Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}")
backup_message = ''
if backup_path:
backup_message = f' Backup created at: {backup_info.get("timestamp", "unknown")}'
return JsonResponse({
'success': True,
'message': f'Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}.{backup_message}',
'backup_created': backup_path is not None,
'backup_path': backup_path if backup_path else None
})
finally:
os.chdir(original_cwd)
finally:
# Cleanup
shutil.rmtree(temp_dir, ignore_errors=True)
except urllib.error.HTTPError as e:
error_msg = f'Failed to download plugin from GitHub: HTTP {e.code}'
if e.code == 404:
error_msg = f'Plugin {plugin_name} not found in GitHub repository'
logging.writeToFile(f"Error upgrading {plugin_name}: {error_msg}")
return JsonResponse({
'success': False,
'error': error_msg
}, status=500)
except Exception as e:
logging.writeToFile(f"Error upgrading plugin {plugin_name}: {str(e)}")
import traceback
error_details = traceback.format_exc()
logging.writeToFile(f"Traceback: {error_details}")
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
@csrf_exempt
@require_http_methods(["GET"])
def get_plugin_backups(request, plugin_name):
"""Get list of available backups for a plugin"""
mailUtilities.checkHome()
try:
backups = _get_plugin_backups(plugin_name)
return JsonResponse({
'success': True,
'backups': backups,
'count': len(backups)
})
except Exception as e:
logging.writeToFile(f"Error getting backups for {plugin_name}: {str(e)}")
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
@csrf_exempt
@require_http_methods(["POST"])
def revert_plugin(request, plugin_name):
"""Revert a plugin to a previous version from backup"""
mailUtilities.checkHome()
try:
# Get backup path from request
data = json.loads(request.body)
backup_path = data.get('backup_path')
if not backup_path:
return JsonResponse({
'success': False,
'error': 'Backup path is required'
}, status=400)
# Verify backup exists
if not os.path.exists(backup_path):
return JsonResponse({
'success': False,
'error': f'Backup not found: {backup_path}'
}, status=404)
# Get backup version info
metadata_file = os.path.join(backup_path, '.backup_metadata.json')
backup_version = 'unknown'
if os.path.exists(metadata_file):
try:
with open(metadata_file, 'r') as f:
backup_info = json.load(f)
backup_version = backup_info.get('version', 'unknown')
except:
pass
logging.writeToFile(f"Reverting {plugin_name} to version {backup_version} from backup {backup_path}")
# Restore from backup
if _restore_plugin_from_backup(plugin_name, backup_path):
return JsonResponse({
'success': True,
'message': f'Plugin {plugin_name} reverted successfully to version {backup_version}'
})
else:
return JsonResponse({
'success': False,
'error': 'Failed to restore plugin from backup'
}, status=500)
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': 'Invalid JSON data'
}, status=400)
except Exception as e:
logging.writeToFile(f"Error reverting plugin {plugin_name}: {str(e)}")
import traceback
error_details = traceback.format_exc()
logging.writeToFile(f"Traceback: {error_details}")
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
@csrf_exempt
@require_http_methods(["POST"])
def install_from_store(request, plugin_name):