#!/bin/bash
# Hesabix Installation Script
# Version: 2.6
# Modernized and optimized for Ubuntu 24.04 LTS
# Last Updated: April 28, 2025
# Exit on any error
set -e
# Colors for better UI
declare -r RED='\e[31m'
declare -r GREEN='\e[32m'
declare -r YELLOW='\e[33m'
declare -r BLUE='\e[34m'
declare -r NC='\e[0m'
declare -r BOLD='\e[1m'
declare -r UNDERLINE='\e[4m'
# ASCII Art
print_logo() {
echo -e "${BOLD}${BLUE}"
echo " _ _ ___ ___ _ ___ ___ __ __"
echo " | || | | __| / __| /_\ | _ ) |_ _| \ \/ /"
echo " | __ | | _| \__ \ / _ \ | _ \ | | > < "
echo " |_||_| |___| |___/ /_/ \_\ |___/ |___| /_/\_\\"
echo -e "${NC}"
}
# Print header
print_header() {
clear
print_logo
echo -e "${BOLD}${BLUE}=================================================${NC}"
echo -e "${BOLD}${BLUE} Hesabix Installation Script ${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}"
echo -e "${YELLOW}Hesabix is a powerful open-source accounting software${NC}"
echo -e "${YELLOW}developed with ❤ by Babak Alizadeh (alizadeh.babak)${NC}"
echo -e "${YELLOW}License: GNU GPL v3${NC}"
echo -e "${YELLOW}Website: ${UNDERLINE}https://hesabix.ir${NC}"
echo -e "${YELLOW}Support us: ${UNDERLINE}https://hesabix.ir/page/sponsors${NC} ❤"
echo -e "${BOLD}${BLUE}=================================================${NC}\n"
# Show prerequisites
echo -e "${BOLD}${YELLOW}Prerequisites:${NC}"
echo -e "1. A domain name pointing to this server"
echo -e "2. DNS records properly configured:"
echo -e " • A record pointing to server IP"
echo -e " • www subdomain pointing to server IP"
echo -e "3. Port 80 and 443 open and accessible"
echo -e "4. At least 2GB of free disk space"
echo -e "5. At least 1GB of RAM"
echo -e "\n${BOLD}${YELLOW}Important Notes:${NC}"
echo -e "• SSL certificate installation requires proper DNS configuration"
echo -e "• Domain must be accessible from the internet"
echo -e "• Installation may take 10-15 minutes"
echo -e "• System will be automatically rolled back if installation fails"
echo -e "\n${BOLD}${YELLOW}Do you want to continue?${NC}"
read -p "Press Enter to continue or Ctrl+C to abort..."
echo -e "${BOLD}${BLUE}=================================================${NC}\n"
}
# Show the header immediately
print_header
# Configuration
declare -r LOG_FILE="/var/log/hesabix_install.log"
declare -r LOG_LEVEL="DEBUG" # Options: DEBUG, INFO, WARNING, ERROR
declare -r REQUIRED_DISK_SPACE_MB=2000
declare -r REQUIRED_MEMORY_MB=1024
declare -r NODE_VERSION="20"
declare -r COMPOSER_TIMEOUT=600
declare -r NPM_TIMEOUT=600
# Global variables
declare SEND_TELEMETRY=false
declare domain=""
declare apache_user="www-data"
# Function to log messages in JSON format
log_message() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Create JSON log entry
local log_entry
log_entry=$(printf '{"timestamp":"%s","level":"%s","message":"%s"}\n' "$timestamp" "$level" "$message")
# Write to log file
echo "$log_entry" >> "$LOG_FILE"
# Display to console based on level
case "$level" in
"INFO")
echo -e "${GREEN}[INFO] $message${NC}"
;;
"WARNING")
echo -e "${YELLOW}[WARNING] $message${NC}"
;;
"ERROR")
echo -e "${RED}[ERROR] $message${NC}"
;;
"DEBUG")
[[ "$LOG_LEVEL" == "DEBUG" ]] && echo -e "${BLUE}[DEBUG] $message${NC}"
;;
esac
}
# Function to initialize logging
init_logging() {
mkdir -p "$(dirname "$LOG_FILE")" || {
echo -e "${RED}Error: Cannot create log directory${NC}"
exit 1
}
touch "$LOG_FILE" || {
echo -e "${RED}Error: Cannot create log file${NC}"
exit 1
}
chmod 644 "$LOG_FILE"
log_message "INFO" "Hesabix installation script started"
log_message "INFO" "Log file: $LOG_FILE"
log_message "INFO" "To view logs: tail -f $LOG_FILE"
}
# Function to handle errors
handle_error() {
local message="$1"
local exit_code="${2:-1}"
log_message "ERROR" "$message"
log_message "ERROR" "Installation failed. Check logs: $LOG_FILE"
exit "$exit_code"
}
# Function to check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function to check internet connectivity
check_internet() {
log_message "INFO" "Checking internet connectivity..."
if ! ping -c 1 8.8.8.8 >/dev/null 2>&1; then
handle_error "No internet connection detected. Please ensure the server has internet access."
fi
log_message "INFO" "Internet connectivity verified"
}
# Function to validate system requirements
check_system_requirements() {
log_message "INFO" "Checking system requirements..."
# Check root privileges
[[ "$EUID" -ne 0 ]] && handle_error "This script must be run as root"
# Check OS compatibility
if [[ -f /etc/os-release ]]; then
. /etc/os-release
log_message "INFO" "Detected OS: $NAME $VERSION"
[[ ! "$ID" =~ ^(ubuntu|debian)$ ]] && handle_error "Only Ubuntu and Debian are supported"
else
handle_error "Cannot determine OS type"
fi
# Check disk space
local available_space
available_space=$(df -m / | awk 'NR==2 {print $4}')
log_message "DEBUG" "Required disk space: ${REQUIRED_DISK_SPACE_MB}MB"
log_message "DEBUG" "Available disk space: ${available_space}MB"
[[ "$available_space" -lt "$REQUIRED_DISK_SPACE_MB" ]] && \
handle_error "Insufficient disk space. Required: ${REQUIRED_DISK_SPACE_MB}MB, Available: ${available_space}MB"
# Check memory
local total_memory
total_memory=$(free -m | awk '/^Mem:/{print $2}')
log_message "DEBUG" "Required memory: ${REQUIRED_MEMORY_MB}MB"
log_message "DEBUG" "Total memory: ${total_memory}MB"
[[ "$total_memory" -lt "$REQUIRED_MEMORY_MB" ]] && \
log_message "WARNING" "Low memory detected. Performance may be affected"
log_message "INFO" "System requirements check completed"
}
# Function to update package lists
update_packages() {
log_message "INFO" "Updating package lists..."
apt-get update -y || handle_error "Failed to update package lists"
}
# Function to install required tools
install_tools() {
local tools=("curl" "coreutils" "git" "unzip")
# Update package lists first
update_packages
for tool in "${tools[@]}"; do
if ! command_exists "$tool"; then
log_message "INFO" "Installing $tool..."
apt-get install -y "$tool" || handle_error "Failed to install $tool"
log_message "INFO" "$tool installed successfully"
else
log_message "INFO" "$tool is already installed"
fi
done
}
# Function to get installed PHP version
get_php_version() {
local version
version=$(php -v | head -n 1 | awk '{print $2}' | cut -d'.' -f1,2)
echo "$version"
}
# Function to get all installed PHP versions
get_installed_php_versions() {
local versions=()
for version_dir in /etc/php/*; do
if [[ -d "$version_dir" && "$version_dir" =~ /etc/php/[0-9]+\.[0-9]+$ ]]; then
versions+=("$(basename "$version_dir")")
fi
done
echo "${versions[@]}"
}
# Function to check if all required extensions are installed for all PHP versions
check_required_extensions() {
local missing_packages=()
local installed_versions
installed_versions=($(get_installed_php_versions))
for version in "${installed_versions[@]}"; do
log_message "INFO" "Checking required packages for PHP $version..."
for pkg in php${version}-raphf php${version}-http php${version}-dom php${version}-xml php${version}-gd php${version}-curl php${version}-simplexml php${version}-xmlwriter php${version}-zip; do
if ! dpkg -l | grep -q "^ii $pkg "; then
missing_packages+=("$pkg")
log_message "WARNING" "Package $pkg is missing"
fi
done
done
if [[ ${#missing_packages[@]} -gt 0 ]]; then
echo -e "\nWarning: The following packages are missing:"
printf '%s\n' "${missing_packages[@]}"
echo -e "\nThese packages will need to be installed manually after the installation is complete."
return 0
fi
return 0
}
# Function to verify CLI extensions
verify_cli_extensions() {
log_message "INFO" "Verifying CLI extensions for PHP..."
# Get the list of enabled extensions from CLI
local enabled_extensions
enabled_extensions=$(php -m)
# Check each required extension
for ext in iconv raphf http dom xml gd curl simplexml xmlwriter zip intl mbstring mysql bcmath; do
if ! echo "$enabled_extensions" | grep -q "^$ext$"; then
log_message "ERROR" "Extension $ext is not enabled in CLI"
log_message "INFO" "Attempting to enable $ext for CLI..."
# Try to enable the extension
phpenmod "$ext" || {
log_message "ERROR" "Failed to enable $ext for CLI"
return 1
}
# Verify again after enabling
enabled_extensions=$(php -m)
if ! echo "$enabled_extensions" | grep -q "^$ext$"; then
log_message "ERROR" "Extension $ext is still not enabled in CLI"
return 1
fi
fi
done
return 0
}
# Function to install PHP and extensions
install_php() {
log_message "INFO" "Checking PHP installation..."
# Update package lists first
update_packages
# Remove any existing PHP packages first
apt-get remove --purge php* -y
apt-get autoremove -y
apt-get clean
# Install PHP and required extensions
log_message "INFO" "Installing PHP and required extensions..."
local php_packages=(
"php"
"php-cli"
"libapache2-mod-php"
"php-intl"
"php-mbstring"
"php-zip"
"php-gd"
"php-mysql"
"php-curl"
"php-xml"
"php-bcmath"
"php-raphf"
"php-http"
"php-dom"
"php-simplexml"
"php-xmlwriter"
"php-iconv"
)
# Install each package individually to handle dependencies
for pkg in "${php_packages[@]}"; do
log_message "INFO" "Installing $pkg..."
if ! apt-get install -y "$pkg"; then
log_message "WARNING" "Failed to install $pkg, continuing with other packages..."
fi
done
# Install specific PHP 8.3 packages
for pkg in php8.3-dom php8.3-simplexml php8.3-xmlwriter; do
if ! apt-get install -y "$pkg"; then
log_message "WARNING" "Failed to install $pkg, continuing with other packages..."
fi
done
# Enable PHP module for Apache
a2enmod php8.3 || a2enmod php8.2 || a2enmod php8.1 || a2enmod php8.0 || a2enmod php7.4 || log_message "WARNING" "Failed to enable PHP module"
# Restart Apache
systemctl restart apache2 || log_message "WARNING" "Failed to restart Apache"
log_message "INFO" "PHP installation and configuration completed"
}
# Function to install MySQL
install_mysql() {
log_message "INFO" "Checking MySQL installation..."
# Update package lists first
update_packages
if command_exists mysql; then
local mysql_version
mysql_version=$(mysql -V | grep -oP '\d+\.\d+\.\d+' | head -1)
log_message "INFO" "MySQL/MariaDB version $mysql_version is installed"
else
log_message "INFO" "Installing MySQL..."
apt-get install -y mysql-server || handle_error "Failed to install MySQL"
systemctl enable mysql || handle_error "Failed to enable MySQL"
systemctl start mysql || handle_error "Failed to start MySQL"
log_message "INFO" "MySQL installed successfully"
fi
# Verify MySQL service
if ! systemctl is-active --quiet mysql; then
handle_error "MySQL service is not running"
fi
}
# Function to install Node.js
install_nodejs() {
log_message "INFO" "Checking Node.js installation..."
# Update package lists first
update_packages
if command_exists node; then
local node_version
node_version=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [[ "$node_version" -ge "$NODE_VERSION" ]]; then
log_message "INFO" "Node.js version $(node -v) is installed"
return
fi
fi
log_message "INFO" "Installing Node.js $NODE_VERSION..."
check_internet
curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - || handle_error "Failed to setup Node.js repository"
apt-get install -y nodejs || handle_error "Failed to install Node.js"
# Verify npm installation
if ! command_exists npm; then
log_message "INFO" "Installing npm..."
apt-get install -y npm || handle_error "Failed to install npm"
fi
log_message "INFO" "Node.js and npm installed successfully"
}
# Function to install Apache
install_apache() {
log_message "INFO" "Checking Apache installation..."
# Update package lists first
update_packages
if command_exists apache2; then
log_message "INFO" "Apache is installed"
else
log_message "INFO" "Installing Apache..."
apt-get install -y apache2 || handle_error "Failed to install Apache"
systemctl enable apache2 || handle_error "Failed to enable Apache"
systemctl start apache2 || handle_error "Failed to start Apache"
log_message "INFO" "Apache installed successfully"
fi
# Verify Apache service
if ! systemctl is-active --quiet apache2; then
handle_error "Apache service is not running"
fi
}
# Function to install Composer
install_composer() {
log_message "INFO" "Checking Composer installation..."
if command_exists composer; then
log_message "INFO" "Composer is already installed"
return
fi
log_message "INFO" "Installing Composer..."
check_internet
local composer_setup="/tmp/composer-setup.php"
# Set environment variables for Composer
export COMPOSER_ALLOW_SUPERUSER=1
export COMPOSER_HOME=/root/.config/composer
export COMPOSER_NO_INTERACTION=1
php -r "copy('https://getcomposer.org/installer', '$composer_setup');" || handle_error "Failed to download Composer installer"
php "$composer_setup" --install-dir=/usr/local/bin --filename=composer || handle_error "Failed to install Composer"
rm -f "$composer_setup"
# Configure Composer to work with root user
mkdir -p /root/.config/composer
cat > /root/.config/composer/config.json << EOF
{
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"sort-packages": true,
"optimize-autoloader": true,
"preferred-install": "dist",
"no-interaction": true
}
}
EOF
command_exists composer || handle_error "Composer installation verification failed"
log_message "INFO" "Composer installed successfully"
}
# Function to setup SSL with Let's Encrypt
setup_ssl() {
local domain="$1"
local max_attempts=3
local attempt=1
local success=false
log_message "INFO" "Setting up SSL for $domain..."
# Check if domain is accessible
log_message "INFO" "Checking if domain $domain is accessible..."
if ! host "$domain" >/dev/null 2>&1; then
log_message "WARNING" "Domain $domain is not accessible. Please make sure DNS is properly configured."
echo -e "\n${YELLOW}SSL setup skipped. Please ensure:${NC}"
echo -e "1. DNS records for $domain are properly configured"
echo -e "2. Domain is pointing to this server's IP address"
echo -e "3. Port 80 is accessible from the internet"
echo -e "\n${YELLOW}You can run SSL setup later using:${NC}"
echo -e "${GREEN}sudo certbot --apache -d $domain${NC}"
return 1
fi
# Check if certbot is installed
if ! command_exists certbot; then
log_message "INFO" "Installing Certbot..."
apt-get install -y certbot python3-certbot-apache || {
log_message "ERROR" "Failed to install Certbot"
return 1
}
fi
# Check if Apache is running
if ! systemctl is-active --quiet apache2; then
log_message "ERROR" "Apache is not running. Please start Apache first."
return 1
fi
# Try to setup SSL with multiple attempts
while [[ $attempt -le $max_attempts ]] && [[ $success == false ]]; do
log_message "INFO" "Attempt $attempt of $max_attempts to setup SSL..."
# Stop Apache temporarily to free port 80
systemctl stop apache2 || {
log_message "WARNING" "Failed to stop Apache, continuing anyway..."
}
# Run certbot only for main domain
if certbot --apache -d "$domain" --non-interactive --agree-tos --email "admin@$domain" --force-renewal; then
success=true
log_message "INFO" "SSL setup completed successfully"
else
log_message "WARNING" "Attempt $attempt failed to setup SSL"
attempt=$((attempt + 1))
sleep 5
fi
# Start Apache again
systemctl start apache2 || {
log_message "WARNING" "Failed to start Apache"
}
done
if [[ $success == true ]]; then
# Verify SSL installation
if curl -s "https://$domain" >/dev/null 2>&1; then
log_message "INFO" "SSL verification successful"
echo -e "\n${GREEN}SSL setup completed successfully!${NC}"
echo -e "You can access your site securely at: ${UNDERLINE}https://$domain${NC}"
else
log_message "WARNING" "SSL verification failed"
echo -e "\n${YELLOW}SSL setup completed but verification failed.${NC}"
echo -e "Please check your SSL configuration manually."
fi
else
log_message "ERROR" "Failed to setup SSL after $max_attempts attempts"
echo -e "\n${RED}SSL setup failed.${NC}"
echo -e "${YELLOW}Possible reasons:${NC}"
echo -e "1. Domain DNS is not properly configured"
echo -e "2. Port 80 is blocked by firewall"
echo -e "3. Let's Encrypt rate limit exceeded"
echo -e "\n${YELLOW}You can try setting up SSL manually using:${NC}"
echo -e "${GREEN}sudo certbot --apache -d $domain${NC}"
echo -e "\n${YELLOW}Or check the logs:${NC}"
echo -e "${GREEN}sudo certbot certificates${NC}"
echo -e "${GREEN}sudo certbot --apache -d $domain --dry-run${NC}"
return 1
fi
}
# Function to validate domain
validate_domain() {
local domain="$1"
if [[ "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
return 0
fi
handle_error "Invalid domain format: $domain. Must be like example.com"
}
# Function to get domain input
get_domain() {
local domain
while true; do
read -p "Please enter the domain for Hesabix installation (e.g., example.com): " domain
if validate_domain "$domain"; then
echo "$domain"
break
fi
done
}
# Function to setup domain
setup_domain() {
local domain="$1"
local domain_path="/var/www/html/$domain/public_html"
local config_file="/etc/apache2/sites-available/$domain.conf"
local sessions_path="/var/www/html/$domain/hesabixCore/var/sessions"
log_message "INFO" "Setting up domain: $domain"
# Remove existing virtual host if it exists
if [[ -f "$config_file" ]]; then
log_message "INFO" "Removing existing virtual host configuration..."
a2dissite "$domain.conf" >/dev/null 2>&1
rm -f "$config_file"
fi
# Check for existing domain directory
if [[ -d "/var/www/html/$domain" ]]; then
echo -e "\n${YELLOW}Warning: The directory /var/www/html/$domain already exists.${NC}"
echo -e "${YELLOW}All its contents will be deleted.${NC}"
read -p "Do you want to continue? (y/n) [n]: " response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
log_message "ERROR" "User chose not to delete existing directory"
handle_error "Installation aborted by user"
fi
log_message "INFO" "Removing existing domain directory..."
rm -rf "/var/www/html/$domain"
fi
# Create domain directory
mkdir -p "$domain_path" || handle_error "Failed to create domain directory"
chown -R "$apache_user:$apache_user" "$domain_path"
chmod -R 755 "$domain_path"
# Create sessions directory
log_message "INFO" "Creating sessions directory..."
mkdir -p "$sessions_path" || handle_error "Failed to create sessions directory"
chown -R "$apache_user:$apache_user" "$sessions_path"
chmod -R 777 "$sessions_path"
# Enable Apache rewrite module
a2enmod rewrite || handle_error "Failed to enable Apache rewrite module"
# Create Apache virtual host
cat > "$config_file" << EOF
ServerName $domain
ServerAlias www.$domain
DocumentRoot $domain_path
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
ErrorLog \${APACHE_LOG_DIR}/$domain-error.log
CustomLog \${APACHE_LOG_DIR}/$domain-access.log combined
EOF
apache2ctl configtest || handle_error "Apache configuration test failed"
a2ensite "$domain.conf" || handle_error "Failed to enable Apache site"
systemctl restart apache2 || handle_error "Failed to restart Apache"
log_message "INFO" "Domain setup completed"
}
# Function to setup database
setup_database() {
local domain="$1"
local base_db_name="hesabix_$(echo "$domain" | tr '.-' '_')"
local db_name="$base_db_name"
local db_user="hesabix_user"
local db_password
# Generate password with only alphanumeric characters
db_password=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1)
local domain_path="/var/www/html/$domain"
local counter=1
log_message "INFO" "Setting up database..."
# Verify MySQL is running
if ! systemctl is-active --quiet mysql; then
handle_error "MySQL service is not running"
fi
# Check if database exists and create new name if needed
while mysql -e "SHOW DATABASES LIKE '$db_name'" | grep -q "$db_name"; do
db_name="${base_db_name}_${counter}"
counter=$((counter + 1))
done
if [[ "$db_name" != "$base_db_name" ]]; then
log_message "WARNING" "Database $base_db_name already exists, using $db_name instead"
fi
# Drop existing user if exists
mysql -e "DROP USER IF EXISTS '$db_user'@'localhost';" || \
log_message "WARNING" "Failed to drop existing user"
# Create database
mysql -e "CREATE DATABASE IF NOT EXISTS \`$db_name\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" || \
handle_error "Failed to create database"
# Create user with proper permissions
mysql -e "CREATE USER '$db_user'@'localhost' IDENTIFIED BY '$db_password';" || \
handle_error "Failed to create database user"
# Grant all privileges and make sure they are applied
mysql -e "GRANT ALL PRIVILEGES ON \`$db_name\`.* TO '$db_user'@'localhost';" || \
handle_error "Failed to grant database privileges"
mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush privileges"
# Verify user can connect
if ! mysql -u"$db_user" -p"$db_password" -e "SELECT 1;" >/dev/null 2>&1; then
handle_error "Failed to verify database user access"
fi
# Import default database structure
log_message "INFO" "Importing default database structure..."
if [[ -f "$domain_path/hesabixBackup/databasefiles/hesabix-db-default.sql" ]]; then
mysql -u"$db_user" -p"$db_password" "$db_name" < "$domain_path/hesabixBackup/databasefiles/hesabix-db-default.sql" || \
log_message "WARNING" "Failed to import default database structure"
else
log_message "WARNING" "Default database structure file not found"
fi
# Update environment configuration
local env_file="$domain_path/hesabixCore/.env.local.php"
cat > "$env_file" << EOF
'prod',
'SYMFONY_DOTENV_PATH' => './.env',
'APP_SECRET' => '$(openssl rand -hex 16)',
'DATABASE_URL' => 'mysql://$db_user:$db_password@127.0.0.1:3306/$db_name?serverVersion=8.0.32&charset=utf8mb4',
'MESSENGER_TRANSPORT_DSN' => 'doctrine://default?auto_setup=0',
'MAILER_DSN' => 'null://null',
'CORS_ALLOW_ORIGIN' => '*',
'LOCK_DSN' => 'flock',
);
EOF
# Set proper permissions
chown "$apache_user:$apache_user" "$env_file"
chmod 644 "$env_file"
# Update database schema
log_message "INFO" "Updating database schema..."
cd "$domain_path/hesabixCore" || handle_error "Failed to change to hesabixCore directory"
# Set environment variables for doctrine command
export DATABASE_URL="mysql://$db_user:$db_password@127.0.0.1:3306/$db_name?serverVersion=8.0.32&charset=utf8mb4"
php bin/console doctrine:schema:update --force || \
log_message "WARNING" "Failed to update database schema"
log_message "INFO" "Database setup completed"
}
# Function to install software
install_software() {
local domain="$1"
local domain_path="/var/www/html/$domain"
local current_version
current_version=$(get_php_version)
log_message "INFO" "Installing software for domain: $domain"
# Ensure domain directory exists
if [[ ! -d "$domain_path" ]]; then
handle_error "Domain directory $domain_path does not exist"
fi
cd "$domain_path" || handle_error "Failed to change to domain directory: $domain_path"
# Check internet connectivity
check_internet
# Check required extensions before proceeding
if ! check_required_extensions; then
echo -e "\nWarning: Some required extensions are missing or could not be installed."
echo -e "Do you want to continue with the installation anyway?"
echo -e "Note: This might cause issues with some features."
read -p "Continue? (y/n) [n]: " response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
log_message "ERROR" "User chose not to continue with missing extensions"
handle_error "Installation aborted by user"
fi
# If user continues, add --ignore-platform-req flags
local composer_flags="--ignore-platform-req=ext-simplexml --ignore-platform-req=ext-xmlwriter --ignore-platform-req=ext-zip"
else
local composer_flags=""
fi
# Initialize git repository if not already initialized
if [[ ! -d ".git" ]]; then
log_message "INFO" "Initializing git repository in $domain_path..."
git init || handle_error "Failed to initialize git repository"
fi
# Check if remote origin exists
if ! git remote get-url origin >/dev/null 2>&1; then
# Add remote repository if it doesn't exist
git remote add origin https://github.com/morrning/hesabixCore.git || \
handle_error "Failed to add remote repository"
else
# Update remote URL if it exists
git remote set-url origin https://github.com/morrning/hesabixCore.git || \
handle_error "Failed to update remote repository"
fi
# Fetch and checkout the repository
log_message "INFO" "Fetching repository contents..."
git fetch origin || handle_error "Failed to fetch repository"
# Check if master branch exists
if git show-ref --verify --quiet refs/heads/master; then
# Switch to master branch
git checkout master || handle_error "Failed to checkout master branch"
# Pull latest changes
git pull origin master || handle_error "Failed to pull latest changes"
else
# Create and checkout master branch
git checkout -b master origin/master || handle_error "Failed to checkout repository"
fi
# Verify repository path
if [[ ! -d "$domain_path" ]]; then
handle_error "Repository directory $domain_path was not created"
fi
cd "$domain_path" || handle_error "Failed to change to domain directory: $domain_path"
# Verify composer.json exists in hesabixCore directory
if [[ ! -f "hesabixCore/composer.json" ]]; then
handle_error "composer.json file not found in $domain_path/hesabixCore"
fi
# Configure Composer
export COMPOSER_ALLOW_SUPERUSER=1
export COMPOSER_HOME=/root/.config/composer
export COMPOSER_NO_INTERACTION=1
# Install dependencies
log_message "INFO" "Installing Composer dependencies..."
cd "hesabixCore" || handle_error "Failed to change to hesabixCore directory"
# Try to install dependencies
if ! timeout "$COMPOSER_TIMEOUT" composer install --no-interaction --optimize-autoloader $composer_flags; then
log_message "ERROR" "Failed to install Composer dependencies"
handle_error "Failed to install Composer dependencies"
fi
# Generate environment
log_message "INFO" "Generating environment file..."
timeout 300 composer dump-env prod --no-interaction || \
handle_error "Failed to generate environment file"
# Verify environment file
if [[ ! -f ".env.local.php" ]]; then
handle_error "Environment file (.env.local.php) was not generated"
fi
log_message "INFO" "Software installation completed"
}
# Function to setup web UI
setup_web_ui() {
local domain="$1"
local domain_path="/var/www/html/$domain"
local webui_path="$domain_path/webUI"
log_message "INFO" "Setting up web UI..."
# Check if webUI directory exists
if [[ ! -d "$webui_path" ]]; then
handle_error "Web UI directory ($webui_path) does not exist"
fi
cd "$webui_path" || handle_error "Failed to change to webUI directory"
# Set initial permissions for npm operations
chown -R "$SUDO_USER:$SUDO_USER" "$webui_path"
chmod -R 777 "$webui_path"
# Install dependencies
log_message "INFO" "Installing web UI dependencies..."
timeout "$NPM_TIMEOUT" npm install || handle_error "Failed to install web UI dependencies"
# Build web UI
log_message "INFO" "Building web UI..."
timeout "$NPM_TIMEOUT" npm run build-only || handle_error "Failed to build web UI"
# After build, set final ownership to apache
chown -R "$apache_user:$apache_user" "$webui_path"
# Set final permissions
find "$webui_path" -type d -exec chmod 755 {} \;
find "$webui_path" -type f -exec chmod 644 {} \;
# Set special permissions for node_modules
if [[ -d "$webui_path/node_modules" ]]; then
chmod -R 755 "$webui_path/node_modules"
find "$webui_path/node_modules" -type f -exec chmod 644 {} \;
find "$webui_path/node_modules" -type d -exec chmod 755 {} \;
find "$webui_path/node_modules/.bin" -type f -exec chmod 755 {} \;
fi
log_message "INFO" "Web UI setup completed"
}
# Function to set Apache ownership
set_apache_ownership() {
local domain="$1"
local domain_path="/var/www/html/$domain"
local webui_path="$domain_path/webUI"
log_message "INFO" "Setting Apache ownership..."
chown -R "$apache_user:$apache_user" "$domain_path" || \
handle_error "Failed to set Apache ownership"
# Set permissions for all directories and files
find "$domain_path" -type d -exec chmod 755 {} \;
find "$domain_path" -type f -exec chmod 644 {} \;
# Set special permissions for webUI node_modules
if [[ -d "$webui_path/node_modules" ]]; then
log_message "INFO" "Setting special permissions for webUI node_modules..."
chmod -R 755 "$webui_path/node_modules"
find "$webui_path/node_modules" -type f -exec chmod 644 {} \;
find "$webui_path/node_modules" -type d -exec chmod 755 {} \;
find "$webui_path/node_modules/.bin" -type f -exec chmod 755 {} \;
fi
log_message "INFO" "Apache ownership set"
}
# Function to display GPL license
display_gpl_license() {
echo -e "\n${BOLD}${BLUE}=================================================${NC}"
echo -e "${BOLD}${BLUE} GNU General Public License v3 ${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}"
echo -e "${YELLOW}This program is free software: you can redistribute it and/or modify${NC}"
echo -e "${YELLOW}it under the terms of the GNU General Public License as published by${NC}"
echo -e "${YELLOW}the Free Software Foundation, either version 3 of the License, or${NC}"
echo -e "${YELLOW}(at your option) any later version.${NC}"
echo -e "\n${YELLOW}This program is distributed in the hope that it will be useful,${NC}"
echo -e "${YELLOW}but WITHOUT ANY WARRANTY; without even the implied warranty of${NC}"
echo -e "${YELLOW}MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.${NC}"
echo -e "${YELLOW}See the GNU General Public License for more details.${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}\n"
read -p "Do you accept the terms of the GNU General Public License v3? (y/n) [y]: " response
if [[ ! "$response" =~ ^[Yy]$ ]] && [[ -n "$response" ]]; then
handle_error "License terms not accepted"
fi
}
# Function to confirm installation
confirm_installation() {
echo -e "\n${BOLD}${BLUE}=================================================${NC}"
echo -e "${BOLD}${BLUE} Installation Confirmation ${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}"
echo -e "${YELLOW}This script will install:${NC}"
echo -e "• PHP and required extensions"
echo -e "• MySQL/MariaDB"
echo -e "• Apache"
echo -e "• Node.js"
echo -e "• Composer"
echo -e "• phpMyAdmin"
echo -e "• Hesabix Core"
echo -e "• Hesabix Web UI"
echo -e "\n${YELLOW}The installation will require approximately 2GB of disk space.${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}\n"
read -p "Do you want to continue with the installation? (y/n) [y]: " response
if [[ ! "$response" =~ ^[Yy]$ ]] && [[ -n "$response" ]]; then
handle_error "Installation cancelled by user"
fi
}
# Function to install phpMyAdmin
install_phpmyadmin() {
log_message "INFO" "Installing phpMyAdmin..."
# Update package lists first
update_packages
# Install phpMyAdmin
apt-get install -y phpmyadmin || handle_error "Failed to install phpMyAdmin"
# Configure phpMyAdmin
log_message "INFO" "Configuring phpMyAdmin..."
# Create Apache configuration
cat > /etc/apache2/conf-available/phpmyadmin.conf << EOF
Alias /phpmyadmin /usr/share/phpmyadmin
Options FollowSymLinks
DirectoryIndex index.php
AllowOverride All
Require all granted
EOF
# Enable configuration
a2enconf phpmyadmin || handle_error "Failed to enable phpMyAdmin configuration"
# Restart Apache
systemctl restart apache2 || handle_error "Failed to restart Apache"
log_message "INFO" "phpMyAdmin installation completed"
}
# Function to show installation summary
show_installation_summary() {
local domain="$1"
local domain_path="/var/www/html/$domain"
local missing_packages=()
local db_name="hesabix_$(echo "$domain" | tr '.-' '_')"
local db_user="hesabix_user"
local db_password
local env_file="$domain_path/hesabixCore/.env.local.php"
# Get database password from env file
if [[ -f "$env_file" ]]; then
db_password=$(php -r "include '$env_file'; echo \$env['DATABASE_URL']; echo PHP_EOL;" | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p')
fi
log_message "INFO" "Showing installation summary..."
# Check for missing packages
for version in $(get_installed_php_versions); do
for pkg in php${version}-raphf php${version}-http php${version}-dom php${version}-xml php${version}-gd php${version}-curl php${version}-simplexml php${version}-xmlwriter php${version}-zip; do
if ! dpkg -l | grep -q "^ii $pkg "; then
missing_packages+=("$pkg")
fi
done
done
echo -e "\n${BOLD}${BLUE}=================================================${NC}"
echo -e "${BOLD}${BLUE} Hesabix Installation Summary ${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}"
echo -e "\n${YELLOW}Installation Details:${NC}"
echo -e "Domain: $domain"
echo -e "Installation Path: $domain_path"
echo -e "Web Server: Apache"
echo -e "PHP Version: $(php -v | head -n 1 | awk '{print $2}')"
echo -e "Node.js Version: $(node -v)"
echo -e "phpMyAdmin URL: https://$domain/phpmyadmin"
echo -e "\n${YELLOW}Database Information:${NC}"
echo -e "Database Name: $db_name"
echo -e "Database User: $db_user"
if [[ -n "$db_password" ]]; then
echo -e "Database Password: $db_password"
else
echo -e "${RED}Database Password: Not found in .env.local.php${NC}"
echo -e "\n${YELLOW}To get database information, you can:${NC}"
echo -e "1. Check the file: $env_file"
echo -e "2. Run this command to extract password:"
echo -e " ${GREEN}php -r \"include '$env_file'; echo \$env['DATABASE_URL']; echo PHP_EOL;\" | sed -n 's/.*:\/\/[^:]*:\\([^@]*\\)@.*/\\1/p'${NC}"
echo -e "3. Or check MySQL directly:"
echo -e " ${GREEN}mysql -u root -e \"SELECT User, Host FROM mysql.user WHERE User='$db_user';\"${NC}"
fi
echo -e "Database Host: localhost"
echo -e "Database Port: 3306"
if [[ ${#missing_packages[@]} -gt 0 ]]; then
echo -e "\n${RED}Warning: The following packages were not installed:${NC}"
printf '%s\n' "${missing_packages[@]}"
echo -e "\n${YELLOW}Please install these packages manually using:${NC}"
echo -e "sudo apt-get install ${missing_packages[*]}"
fi
echo -e "\n${YELLOW}Next Steps:${NC}"
echo -e "1. Configure domain DNS to point to this server"
echo -e "2. Access the web interface: https://$domain"
echo -e "3. Access phpMyAdmin: https://$domain/phpmyadmin"
echo -e "4. Register the first user (system administrator)"
echo -e "\n${YELLOW}Support:${NC}"
echo -e "• Developer: Babak Alizadeh (alizadeh.babak)"
echo -e "• License: GNU GPL v3"
echo -e "• Website: ${UNDERLINE}https://hesabix.ir${NC}"
echo -e "• Support us: ${UNDERLINE}https://hesabix.ir/page/sponsors${NC} ❤"
echo -e "\n${GREEN}Installation completed successfully!${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}"
# Restart Apache at the end of installation
log_message "INFO" "Restarting Apache..."
systemctl restart apache2 || log_message "WARNING" "Failed to restart Apache"
}
# Function to display telemetry consent
display_telemetry_consent() {
echo -e "\n${RED}================================================="
echo -e "${RED} Anonymous Data Collection "
echo -e "${RED}================================================="
echo -e "${BLUE}To improve Hesabix, we would like to collect anonymous data:"
echo -e "${BLUE}• System information (OS, PHP, MySQL versions)"
echo -e "${BLUE}• Installation path and domain"
echo -e "${BLUE}• Installation date"
read -p "Do you agree? (y/n) [n]: " response
[[ "$response" =~ ^[Yy]$ ]] && SEND_TELEMETRY=true
}
# Function to handle rollback
rollback() {
local domain="$1"
local domain_path="/var/www/html/$domain"
log_message "ERROR" "Starting rollback process..."
echo -e "\n${RED}Starting rollback process...${NC}"
# Log rollback steps in main log file
{
echo "----------------------------------------"
echo "ROLLBACK STARTED AT: $(date)"
echo "Domain: $domain"
echo "Domain path: $domain_path"
echo "----------------------------------------"
echo "Note: System packages installed via apt are not removed"
echo "----------------------------------------"
} >> "$LOG_FILE"
# Remove domain directory if it exists
if [[ -d "$domain_path" ]]; then
log_message "INFO" "Removing domain directory..."
rm -rf "$domain_path"
echo "Removed domain directory: $domain_path" >> "$LOG_FILE"
fi
# Remove Apache virtual host
local config_file="/etc/apache2/sites-available/$domain.conf"
if [[ -f "$config_file" ]]; then
log_message "INFO" "Removing Apache virtual host..."
a2dissite "$domain.conf" >/dev/null 2>&1
rm -f "$config_file"
echo "Removed Apache virtual host: $config_file" >> "$LOG_FILE"
fi
# Remove SSL certificates if they exist
if certbot certificates | grep -q "$domain"; then
log_message "INFO" "Removing SSL certificates..."
certbot delete --cert-name "$domain" --non-interactive
echo "Removed SSL certificates for: $domain" >> "$LOG_FILE"
fi
# Remove database if it exists
local db_name="hesabix_$(echo "$domain" | tr '.-' '_')"
if mysql -e "SHOW DATABASES LIKE '$db_name'" | grep -q "$db_name"; then
log_message "INFO" "Removing database..."
mysql -e "DROP DATABASE IF EXISTS \`$db_name\`;"
echo "Removed database: $db_name" >> "$LOG_FILE"
fi
# Remove database user if it exists
local db_user="hesabix_user"
if mysql -e "SELECT User FROM mysql.user WHERE User='$db_user'" | grep -q "$db_user"; then
log_message "INFO" "Removing database user..."
mysql -e "DROP USER IF EXISTS '$db_user'@'localhost';"
echo "Removed database user: $db_user" >> "$LOG_FILE"
fi
# Restart Apache
systemctl restart apache2
# Log completion in main log file
{
echo "----------------------------------------"
echo "ROLLBACK COMPLETED AT: $(date)"
echo "----------------------------------------"
} >> "$LOG_FILE"
log_message "INFO" "Rollback completed"
echo -e "\n${YELLOW}Rollback completed.${NC}"
echo -e "${YELLOW}Check the installation log file for details: ${UNDERLINE}$LOG_FILE${NC}"
echo -e "\n${YELLOW}Note: System packages installed via apt were not removed.${NC}"
echo -e "${YELLOW}You can safely run the installation script again.${NC}"
}
# Main execution
main() {
# Show header first
print_header
# Wait for user to see the header
sleep 2
# Then start logging
init_logging
# Get domain first for rollback purposes
domain=$(get_domain)
# Create a trap for rollback
trap 'rollback "$domain"; exit 1' ERR
check_system_requirements
install_tools
display_gpl_license
confirm_installation
display_telemetry_consent
update_packages
install_php
install_mysql
install_nodejs
install_apache
install_composer
install_phpmyadmin
setup_domain "$domain"
install_software "$domain"
setup_database "$domain"
setup_web_ui "$domain"
set_apache_ownership "$domain"
setup_ssl "$domain"
show_installation_summary "$domain"
# Remove the trap at the end of successful installation
trap - ERR
}
# Execute main with error handling
main || handle_error "Installation failed"