ultimate-wp-installer/ultimate-wp-installer-lite.sh

824 lines
27 KiB
Bash
Raw Normal View History

2025-08-08 20:15:53 +03:00
#!/usr/bin/env bash
# ============================================================================
# WOO v12 — WordPress Operations Orchestrator (Interactive, One-Command)
# Target: Ubuntu 22.04 / 24.04 LTS
2025-08-05 23:15:48 +03:00
#
2025-08-08 20:33:36 +03:00
# - Run once to set up the server. After that, just type `woo` for the menu.
# - Always installs the latest stable PHP from Ondřejs PPA (tries 8.3, falls back to 8.2).
# - Per-site isolation: separate PHP-FPM pool, DB+user, Nginx vhost, filesystem.
# - No Multisite, No Staging, No Cloudflare, No remote backups (local only).
# - XML-RPC kept ON but protected by a global whitelist (for Odoo).
# - Smart, safe, zero-fail mindset with rollback on errors and clear reports.
2025-08-08 20:15:53 +03:00
# ============================================================================
set -euo pipefail
IFS=$'\n\t'
2025-08-08 20:33:36 +03:00
# --------------------------- Auto-sudo (OVH-friendly) -----------------------
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
exec sudo -E bash "$0" "$@"
fi
2025-08-08 20:15:53 +03:00
# --------------------------- Colors & Log -----------------------------------
c_blue='\033[0;34m'; c_green='\033[0;32m'; c_yellow='\033[1;33m'; c_red='\033[0;31m'; c_nc='\033[0m'
2025-08-06 19:21:42 +03:00
readonly LOG_DIR="/var/log/woo-toolkit"
2025-08-08 20:15:53 +03:00
readonly LOG_FILE="${LOG_DIR}/woo-$(date +%Y%m%d_%H%M%S).log"
mkdir -p "$LOG_DIR"; touch "$LOG_FILE"
2025-08-06 19:16:08 +03:00
2025-08-08 20:15:53 +03:00
log() { echo -e "${c_blue}[$(date '+%F %T')]${c_nc} $*" | tee -a "$LOG_FILE"; }
ok() { echo -e "${c_green}${c_nc} $*" | tee -a "$LOG_FILE"; }
warn() { echo -e "${c_yellow}${c_nc} $*" | tee -a "$LOG_FILE"; }
fail(){ echo -e "${c_red}${c_nc} $*" | tee -a "$LOG_FILE"; exit 1; }
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
# --------------------------- Global Defaults --------------------------------
2025-08-06 17:45:54 +03:00
readonly WEBROOT="/var/www"
2025-08-08 20:15:53 +03:00
readonly CONFIG_DIR="/etc/woo"
readonly PHP_CANDIDATES=("8.3" "8.2")
PHP_VERSION=""
readonly MIN_RAM_MB=2048
readonly MIN_DISK_MB=10240
2025-08-06 19:01:20 +03:00
readonly XMLRPC_WHITELIST_FILE="/etc/nginx/conf.d/xmlrpc_whitelist.conf"
2025-08-08 20:15:53 +03:00
readonly LIMITS_FILE="/etc/nginx/conf.d/limits.conf"
readonly CACHE_FILE="/etc/nginx/conf.d/fastcgi_cache.conf"
2025-08-06 19:01:20 +03:00
2025-08-08 20:33:36 +03:00
declare -A CTX=() # rollback context keys: DOMAIN, SITE_DIR, DB_NAME, DB_USER
2025-08-06 19:01:20 +03:00
2025-08-08 20:33:36 +03:00
# --------------------------- Helpers ----------------------------------------
2025-08-08 20:15:53 +03:00
have(){ command -v "$1" &>/dev/null; }
gen_pass(){ openssl rand -base64 "${1:-24}"; }
rand_hex(){ openssl rand -hex "${1:-6}"; }
press_any(){ read -n 1 -s -r -p "Press any key to continue..."; echo; }
2025-08-05 23:42:21 +03:00
2025-08-08 20:33:36 +03:00
error_handler(){
local l="$1" c="$2"
log "Error on line $l: $c"
rollback_safe
fail "Aborted. See log: $LOG_FILE"
}
rollback_safe(){
if [[ -n "${CTX[DOMAIN]:-}" ]]; then
rm -f "/etc/nginx/sites-available/${CTX[DOMAIN]}" "/etc/nginx/sites-enabled/${CTX[DOMAIN]}" || true
rm -f "/etc/php/${PHP_VERSION}/fpm/pool.d/${CTX[DOMAIN]}.conf" || true
systemctl reload "php${PHP_VERSION}-fpm" nginx || true
fi
[[ -n "${CTX[DB_NAME]:-}" ]] && mysql --defaults-file=/root/.my.cnf -e "DROP DATABASE IF EXISTS \`${CTX[DB_NAME]}\`;" || true
[[ -n "${CTX[DB_USER]:-}" ]] && mysql --defaults-file=/root/.my.cnf -e "DROP USER IF EXISTS '${CTX[DB_USER]}'@'localhost';" || true
[[ -n "${CTX[SITE_DIR]:-}" ]] && rm -rf "${CTX[SITE_DIR]}" || true
}
2025-08-08 20:15:53 +03:00
# --------------------------- Preflight & Detection --------------------------
preflight(){
2025-08-08 20:33:36 +03:00
log "Preflight checks…"
[[ -f /etc/os-release ]] || fail "Unsupported OS."
2025-08-08 20:15:53 +03:00
. /etc/os-release
2025-08-08 20:33:36 +03:00
[[ "${NAME}" =~ Ubuntu ]] || fail "Ubuntu required."
if [[ "${VERSION_ID}" != "22.04" && "${VERSION_ID}" != "24.04" ]]; then
warn "Tested on 22.04/24.04. You are ${VERSION_ID}."
fi
2025-08-08 20:15:53 +03:00
local free_mb; free_mb=$(df -Pm / | awk 'NR==2 {print $4}')
2025-08-08 20:33:36 +03:00
(( free_mb >= MIN_DISK_MB )) || fail "Low disk: ${free_mb}MB (< ${MIN_DISK_MB})."
2025-08-08 20:15:53 +03:00
ok "Preflight OK."
}
2025-08-08 19:30:16 +03:00
2025-08-08 20:15:53 +03:00
ensure_swap(){
local ram_mb; ram_mb=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)
if (( ram_mb < MIN_RAM_MB )); then
2025-08-08 20:33:36 +03:00
warn "RAM ${ram_mb}MB < ${MIN_RAM_MB}MB. Creating 2G swap for stability…"
2025-08-08 20:15:53 +03:00
if ! swapon --show | grep -q /swapfile; then
fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048
chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
ok "Swap enabled."
2025-08-08 19:30:16 +03:00
fi
2025-08-08 20:15:53 +03:00
fi
2025-08-05 21:25:52 +03:00
}
2025-08-08 20:15:53 +03:00
select_php_version(){
for v in "${PHP_CANDIDATES[@]}"; do
2025-08-08 20:33:36 +03:00
if apt-cache policy "php${v}-fpm" | grep -q Candidate; then PHP_VERSION="$v"; break; fi
2025-08-08 20:15:53 +03:00
done
2025-08-08 20:33:36 +03:00
[[ -n "$PHP_VERSION" ]] || PHP_VERSION="8.2"
2025-08-08 20:15:53 +03:00
ok "Using PHP ${PHP_VERSION}."
2025-08-06 19:01:20 +03:00
}
2025-08-06 17:45:54 +03:00
2025-08-08 20:15:53 +03:00
# --------------------------- Stack Install ----------------------------------
install_stack(){
2025-08-08 20:33:36 +03:00
log "Installing & updating packages…"
2025-08-08 20:15:53 +03:00
apt-get update -y
2025-08-08 20:33:36 +03:00
apt-get install -y software-properties-common curl wget unzip git rsync psmisc jq dnsutils ca-certificates gnupg lsb-release
2025-08-08 20:15:53 +03:00
add-apt-repository -y ppa:ondrej/php
add-apt-repository -y ppa:ondrej/nginx
apt-get update -y
2025-08-05 21:25:52 +03:00
2025-08-08 20:15:53 +03:00
select_php_version
DEBIAN_FRONTEND=noninteractive apt-get install -y \
nginx mariadb-server redis-server fail2ban ufw \
certbot python3-certbot-nginx unattended-upgrades haveged \
php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-curl php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml \
php${PHP_VERSION}-zip php${PHP_VERSION}-gd php${PHP_VERSION}-opcache php${PHP_VERSION}-redis php${PHP_VERSION}-imagick php${PHP_VERSION}-bcmath
if ! have wp; then
curl -fsSL -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x /usr/local/bin/wp
fi
ok "Stack installed."
}
2025-08-06 19:27:38 +03:00
2025-08-08 20:15:53 +03:00
secure_mariadb(){
2025-08-08 20:33:36 +03:00
log "Securing MariaDB…"
if [[ -f /root/.my.cnf ]]; then warn "MariaDB already secured. Skipping."; return; fi
2025-08-08 20:15:53 +03:00
local pass; pass="$(gen_pass 32)"
mysql -u root <<SQL
ALTER USER 'root'@'localhost' IDENTIFIED BY '${pass}';
2025-08-06 20:49:10 +03:00
DELETE FROM mysql.user WHERE User='';
2025-08-08 20:15:53 +03:00
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost','127.0.0.1','::1');
2025-08-06 20:49:10 +03:00
DROP DATABASE IF EXISTS test;
2025-08-06 19:32:21 +03:00
FLUSH PRIVILEGES;
2025-08-08 20:15:53 +03:00
SQL
cat >/root/.my.cnf <<EOF
[client]
user=root
password=${pass}
EOF
chmod 600 /root/.my.cnf
ok "MariaDB secured."
}
tune_mariadb(){
2025-08-08 20:33:36 +03:00
log "Tuning MariaDB…"
2025-08-08 20:15:53 +03:00
local ram_kb ib_k ib
ram_kb=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
2025-08-08 20:33:36 +03:00
ib_k=$(( ram_kb / 4 )); if (( ib_k > 4194304 )); then ib="4096M"; else ib="${ib_k}K"; fi
2025-08-08 20:15:53 +03:00
cat >/etc/mysql/mariadb.conf.d/99-woo-tuned.cnf <<EOF
[mysqld]
innodb_buffer_pool_size=${ib}
innodb_log_file_size=256M
innodb_file_per_table=1
max_allowed_packet=256M
tmp_table_size=128M
max_heap_table_size=128M
innodb_flush_log_at_trx_commit=1
EOF
systemctl restart mariadb
ok "MariaDB tuned."
}
harden_php(){
2025-08-08 20:33:36 +03:00
log "Hardening PHP ${PHP_VERSION}"
2025-08-08 20:15:53 +03:00
mkdir -p "/etc/php/${PHP_VERSION}/fpm/conf.d"
cat >"/etc/php/${PHP_VERSION}/fpm/conf.d/99-woo-opcache.ini" <<EOF
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=100000
opcache.validate_timestamps=0
opcache.jit=1255
opcache.jit_buffer_size=64M
realpath_cache_size=4096k
realpath_cache_ttl=600
2025-08-06 19:32:21 +03:00
EOF
2025-08-08 20:33:36 +03:00
sed -i 's/^expose_php = On/expose_php = Off/' "/etc/php/${PHP_VERSION}/fpm/php.ini" || true
sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' "/etc/php/${PHP_VERSION}/fpm/php.ini" || true
2025-08-08 20:15:53 +03:00
systemctl restart "php${PHP_VERSION}-fpm"
ok "PHP hardened."
}
harden_imagick(){
2025-08-08 20:33:36 +03:00
log "Applying safe ImageMagick policy…"
2025-08-08 20:15:53 +03:00
local pol="/etc/ImageMagick-6/policy.xml"; [[ -f "$pol" ]] || pol="/etc/ImageMagick/policy.xml"
if [[ -f "$pol" ]]; then
2025-08-08 20:33:36 +03:00
cp "$pol" "${pol}.bak.$(date +%s)" || true
2025-08-08 20:15:53 +03:00
if ! grep -q 'policy domain="resource" name="memory"' "$pol"; then
cat >>"$pol" <<'EOF'
<policymap>
<policy domain="resource" name="memory" value="512MiB"/>
<policy domain="resource" name="map" value="1GiB"/>
<policy domain="resource" name="width" value="8000"/>
<policy domain="resource" name="height" value="8000"/>
<policy domain="coder" rights="none" pattern="PDF"/>
<policy domain="coder" rights="none" pattern="PS"/>
</policymap>
EOF
fi
fi
2025-08-05 21:25:52 +03:00
}
2025-08-08 20:15:53 +03:00
harden_firewall_fail2ban(){
2025-08-08 20:33:36 +03:00
log "UFW + Fail2Ban…"
2025-08-08 20:15:53 +03:00
ufw default deny incoming || true
ufw default allow outgoing || true
ufw allow OpenSSH || true
ufw allow 'Nginx Full' || true
echo "y" | ufw enable || true
cat >/etc/fail2ban/jail.d/wordpress.conf <<EOF
2025-08-06 19:01:20 +03:00
[sshd]
2025-08-06 17:45:54 +03:00
enabled = true
2025-08-06 19:01:20 +03:00
maxretry = 3
bantime = 1d
2025-08-08 20:15:53 +03:00
[nginx-wp-login]
2025-08-06 19:01:20 +03:00
enabled = true
2025-08-08 20:15:53 +03:00
filter = nginx-wp-login
2025-08-06 17:45:54 +03:00
logpath = /var/log/nginx/*access.log
2025-08-08 20:15:53 +03:00
maxretry = 10
findtime = 600
bantime = 1d
[nginx-xmlrpc]
enabled = true
filter = nginx-xmlrpc
logpath = /var/log/nginx/*access.log
maxretry = 20
findtime = 600
bantime = 1d
2025-08-06 19:01:20 +03:00
EOF
2025-08-08 20:33:36 +03:00
2025-08-08 20:15:53 +03:00
cat >/etc/fail2ban/filter.d/nginx-wp-login.conf <<'EOF'
2025-08-06 19:16:08 +03:00
[Definition]
2025-08-08 20:15:53 +03:00
failregex = <HOST> - .* "(POST|GET) /wp-login.php
2025-08-06 19:01:20 +03:00
ignoreregex =
2025-08-05 23:15:48 +03:00
EOF
2025-08-08 20:33:36 +03:00
2025-08-08 20:15:53 +03:00
cat >/etc/fail2ban/filter.d/nginx-xmlrpc.conf <<'EOF'
[Definition]
failregex = <HOST> - .* "POST /xmlrpc.php
ignoreregex =
EOF
2025-08-08 20:33:36 +03:00
2025-08-08 20:15:53 +03:00
systemctl restart fail2ban || true
2025-08-06 19:01:20 +03:00
}
2025-08-08 20:15:53 +03:00
nginx_global(){
2025-08-08 20:33:36 +03:00
log "Writing global Nginx configs (limits, cache, xmlrpc whitelist, default)…"
2025-08-08 20:15:53 +03:00
cat >"$LIMITS_FILE" <<'EOF'
limit_req_zone $binary_remote_addr zone=logins:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=10r/m;
limit_conn_zone $binary_remote_addr zone=perip:10m;
2025-08-06 19:01:20 +03:00
EOF
2025-08-06 21:07:08 +03:00
2025-08-08 20:15:53 +03:00
cat >"$CACHE_FILE" <<'EOF'
2025-08-08 19:30:16 +03:00
fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
EOF
2025-08-08 20:15:53 +03:00
cat >"$XMLRPC_WHITELIST_FILE" <<'EOF'
# Global XML-RPC whitelist for all sites (0 = blocked by default)
geo $xmlrpc_allowed {
default 0;
2025-08-08 20:33:36 +03:00
# Add IPs via the WOO menu -> XML-RPC whitelist
2025-08-08 20:15:53 +03:00
# Example: 203.0.113.10 1;
}
EOF
cat >/etc/nginx/sites-available/default <<'EOF'
2025-08-06 21:07:08 +03:00
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
2025-08-08 20:15:53 +03:00
return 444;
2025-08-06 21:07:08 +03:00
}
EOF
2025-08-08 20:15:53 +03:00
ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
2025-08-05 21:25:52 +03:00
}
2025-08-08 20:15:53 +03:00
systemd_logrotate(){
log "Systemd & logrotate…"
mkdir -p /etc/systemd/system/nginx.service.d
cat >/etc/systemd/system/nginx.service.d/override.conf <<'EOF'
[Service]
Restart=always
RestartSec=2
LimitNOFILE=100000
2025-08-05 23:15:48 +03:00
EOF
2025-08-08 20:15:53 +03:00
systemctl daemon-reload
systemctl restart nginx
cat >/etc/logrotate.d/woo-nginx <<'EOF'
/var/log/nginx/*.log {
daily
rotate 14
compress
missingok
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -s /run/nginx.pid ] && kill -USR1 `cat /run/nginx.pid`
endscript
2025-08-06 17:45:54 +03:00
}
2025-08-08 20:15:53 +03:00
EOF
2025-08-06 17:45:54 +03:00
2025-08-08 20:15:53 +03:00
cat >/etc/logrotate.d/woo-phpfpm <<'EOF'
/var/log/php-fpm/*.log {
daily
rotate 14
compress
missingok
notifempty
create 0640 www-data adm
2025-08-06 19:01:20 +03:00
}
2025-08-08 20:15:53 +03:00
EOF
2025-08-06 17:45:54 +03:00
}
2025-08-08 20:15:53 +03:00
# --------------------------- Per-Site Functions -----------------------------
php_pool_create(){
local domain="$1"
cat >"/etc/php/${PHP_VERSION}/fpm/pool.d/${domain}.conf" <<EOF
2025-08-06 19:01:20 +03:00
[${domain}]
user = www-data
group = www-data
listen = /run/php/php${PHP_VERSION}-${domain}.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 50
pm.process_idle_timeout = 10s
pm.max_requests = 500
slowlog = /var/log/php-fpm/${domain}-slow.log
php_admin_value[error_log] = /var/log/php-fpm/${domain}-error.log
php_admin_flag[log_errors] = on
php_value[session.save_handler] = redis
php_value[session.save_path] = "tcp://127.0.0.1:6379"
EOF
2025-08-08 20:15:53 +03:00
mkdir -p /var/log/php-fpm
touch "/var/log/php-fpm/${domain}-error.log" "/var/log/php-fpm/${domain}-slow.log"
chown -R www-data:www-data /var/log/php-fpm
systemctl restart "php${PHP_VERSION}-fpm"
2025-08-06 17:45:54 +03:00
}
2025-08-08 20:15:53 +03:00
nginx_site_write(){
2025-08-08 20:33:36 +03:00
local domain="$1" docroot="$2" cache_on="$3"
2025-08-08 20:15:53 +03:00
local cache_block=""
if [[ "$cache_on" == "on" ]]; then
cache_block=$(cat <<'EOS'
2025-08-06 19:01:20 +03:00
set $skip_cache 0;
2025-08-06 19:21:42 +03:00
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") { set $skip_cache 1; }
if ($http_cookie ~* "comment_author|wordpress_logged_in|wp-postpass") { set $skip_cache 1; }
2025-08-06 19:01:20 +03:00
fastcgi_cache WORDPRESS;
2025-08-06 17:45:54 +03:00
fastcgi_cache_valid 200 60m;
2025-08-06 19:01:20 +03:00
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
2025-08-08 20:15:53 +03:00
EOS
2025-08-06 17:45:54 +03:00
)
2025-08-08 20:15:53 +03:00
fi
2025-08-06 17:45:54 +03:00
2025-08-08 20:15:53 +03:00
cat >"/etc/nginx/sites-available/${domain}" <<EOF
2025-08-06 16:48:57 +03:00
server {
2025-08-08 19:30:16 +03:00
listen 443 ssl http2;
2025-08-06 19:01:20 +03:00
server_name ${domain} www.${domain};
2025-08-08 20:15:53 +03:00
root ${docroot};
2025-08-06 19:01:20 +03:00
index index.php;
2025-08-08 20:15:53 +03:00
ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
2025-08-06 19:01:20 +03:00
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
2025-08-08 20:15:53 +03:00
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
2025-08-06 19:01:20 +03:00
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
2025-08-08 20:15:53 +03:00
include ${LIMITS_FILE};
2025-08-06 19:01:20 +03:00
2025-08-08 20:15:53 +03:00
${cache_block}
2025-08-06 16:48:57 +03:00
2025-08-08 20:15:53 +03:00
location / { try_files \$uri \$uri/ /index.php\$is_args\$args; }
location = /wp-login.php {
limit_req zone=logins burst=10 nodelay;
2025-08-06 16:48:57 +03:00
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php${PHP_VERSION}-${domain}.sock;
}
2025-08-06 19:01:20 +03:00
location = /xmlrpc.php {
2025-08-06 19:21:42 +03:00
if (\$xmlrpc_allowed = 0) { return 403; }
2025-08-08 20:15:53 +03:00
limit_req zone=xmlrpc burst=20 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php${PHP_VERSION}-${domain}.sock;
}
location ~ \.php\$ {
2025-08-06 19:01:20 +03:00
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php${PHP_VERSION}-${domain}.sock;
2025-08-08 20:15:53 +03:00
limit_conn perip 20;
2025-08-06 16:48:57 +03:00
}
2025-08-06 19:01:20 +03:00
location ~* /(?:uploads|files)/.*\.php\$ { deny all; }
location ~* \.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)\$ { deny all; }
location ~ /\. { deny all; }
location = /readme.html { deny all; }
location = /license.txt { deny all; }
}
server {
listen 80;
server_name ${domain} www.${domain};
return 301 https://\$host\$request_uri;
2025-08-06 16:48:57 +03:00
}
EOF
2025-08-08 20:15:53 +03:00
ln -sf "/etc/nginx/sites-available/${domain}" "/etc/nginx/sites-enabled/${domain}"
nginx -t && systemctl reload nginx
}
create_wp_site(){
clear; echo -e "${c_green}--- Add New WordPress Site ---${c_nc}\n"
read -rp "Domain (example.com): " domain
2025-08-08 20:33:36 +03:00
[[ -n "$domain" ]] || { warn "Domain is required."; press_any; return; }
2025-08-08 20:15:53 +03:00
read -rp "Admin email: " admin_email
2025-08-08 20:33:36 +03:00
[[ -n "$admin_email" ]] || { warn "Admin email is required."; press_any; return; }
2025-08-08 20:15:53 +03:00
local admin_user=""
while true; do
read -rp "Admin username (avoid 'admin'): " admin_user
[[ -z "$admin_user" ]] && { warn "Username required."; continue; }
[[ "$admin_user" == "admin" ]] && { warn "Please choose something other than 'admin'."; continue; }
break
done
local admin_pass; admin_pass="$(gen_pass 16)"
local site_dir="${WEBROOT}/${domain}"
2025-08-08 20:33:36 +03:00
[[ -d "$site_dir" ]] && { warn "Directory already exists: ${site_dir}"; press_any; return; }
2025-08-08 20:15:53 +03:00
CTX[DOMAIN]="$domain"; CTX[SITE_DIR]="$site_dir"
2025-08-08 20:33:36 +03:00
mkdir -p "${site_dir}"; chown -R www-data:www-data "${site_dir}"
2025-08-08 20:15:53 +03:00
local db_name="wp_$(echo "$domain" | tr '.' '_' | cut -c1-20)_$(rand_hex 4)"
local db_user="usr_$(rand_hex 6)"
local db_pass; db_pass="$(gen_pass 24)"
CTX[DB_NAME]="$db_name"; CTX[DB_USER]="$db_user"
2025-08-08 20:33:36 +03:00
log "Creating database & user…"
2025-08-08 20:15:53 +03:00
mysql --defaults-file=/root/.my.cnf <<SQL
CREATE DATABASE \`${db_name}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';
GRANT ALL PRIVILEGES ON \`${db_name}\`.* TO '${db_user}'@'localhost';
FLUSH PRIVILEGES;
SQL
2025-08-06 19:47:34 +03:00
2025-08-08 20:33:36 +03:00
log "Downloading WordPress core…"
2025-08-08 20:15:53 +03:00
sudo -u www-data WP_CLI_CACHE_DIR='/tmp/wp-cli-cache' wp core download --path="${site_dir}" --locale=en_US
2025-08-05 21:25:52 +03:00
2025-08-08 20:33:36 +03:00
log "Creating wp-config.php…"
2025-08-08 20:15:53 +03:00
local table_prefix="wp_$(rand_hex 3)_"
sudo -u www-data wp config create --path="${site_dir}" --dbname="${db_name}" --dbuser="${db_user}" --dbpass="${db_pass}" --dbprefix="${table_prefix}" --extra-php <<PHP
define('WP_CACHE', true);
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('FS_METHOD', 'direct');
define('FORCE_SSL_ADMIN', true);
define('DISALLOW_FILE_EDIT', true);
define('WP_AUTO_UPDATE_CORE', 'minor');
define('WP_DEBUG', false);
define('WP_MEMORY_LIMIT', '128M');
define('WP_MAX_MEMORY_LIMIT', '256M');
define('AUTOSAVE_INTERVAL', 120);
define('WP_POST_REVISIONS', 10);
PHP
sudo -u www-data wp config set WP_CACHE_KEY_SALT "'${domain}'" --path="${site_dir}" --raw
sudo -u www-data wp config set DISABLE_WP_CRON true --path="${site_dir}" --raw
2025-08-08 20:33:36 +03:00
log "Installing WordPress (single site)…"
2025-08-08 20:15:53 +03:00
sudo -u www-data wp core install --path="${site_dir}" --url="https://${domain}" --title="${domain}" --admin_user="${admin_user}" --admin_password="${admin_pass}" --admin_email="${admin_email}"
2025-08-08 20:33:36 +03:00
log "Enabling Redis object cache…"
2025-08-08 20:15:53 +03:00
sudo -u www-data wp plugin install redis-cache --activate --path="${site_dir}"
sudo -u www-data wp redis enable --path="${site_dir}"
mkdir -p "${site_dir}/wp-content/mu-plugins"
cat >"${site_dir}/wp-content/mu-plugins/woo-hardening.php" <<'PHP'
<?php
/**
* MU-hardening for WOO
*/
add_filter('admin_init', function(){ remove_action('wp_head', 'wp_generator'); });
add_filter('rest_endpoints', function($endpoints){ unset($endpoints['/wp/v2/users']); return $endpoints; });
if (!defined('DISALLOW_FILE_EDIT')) define('DISALLOW_FILE_EDIT', true);
if (!defined('WP_POST_REVISIONS')) define('WP_POST_REVISIONS', 10);
if (!defined('AUTOSAVE_INTERVAL')) define('AUTOSAVE_INTERVAL', 120);
PHP
chown -R www-data:www-data "${site_dir}"
php_pool_create "$domain"
2025-08-08 20:33:36 +03:00
nginx_site_write "$domain" "$site_dir" "on"
2025-08-08 20:15:53 +03:00
2025-08-08 20:33:36 +03:00
log "Requesting Let's Encrypt SSL…"
2025-08-08 20:15:53 +03:00
if certbot --nginx --hsts --redirect --staple-ocsp -d "$domain" -d "www.${domain}" -m "$admin_email" --agree-tos --non-interactive; then
ok "SSL issued."
else
2025-08-08 20:33:36 +03:00
warn "SSL issuance failed (DNS/ports?). Retry later with: certbot --nginx -d $domain -d www.$domain"
2025-08-08 20:15:53 +03:00
fi
(crontab -l 2>/dev/null | grep -v "wp cron event run --due-now --path=${site_dir}" ; \
echo "* * * * * sudo -u www-data WP_CLI_CACHE_DIR=/tmp/wp-cli-cache wp cron event run --due-now --path=${site_dir} >/dev/null 2>&1") | crontab -
find "${site_dir}" -type d -exec chmod 755 {} \;
find "${site_dir}" -type f -exec chmod 644 {} \;
chmod 600 "${site_dir}/wp-config.php"
mkdir -p /root/woo_credentials
local cred="/root/woo_credentials/${domain}.txt"
cat >"$cred" <<EOF
========================================
WordPress Credentials for: ${domain}
========================================
2025-08-05 23:42:21 +03:00
Admin URL: https://${domain}/wp-admin
2025-08-08 20:15:53 +03:00
Admin User: ${admin_user}
Admin Pass: ${admin_pass}
2025-08-06 19:01:20 +03:00
2025-08-08 20:15:53 +03:00
Database: ${db_name}
DB User: ${db_user}
DB Pass: ${db_pass}
========================================
2025-08-05 23:42:21 +03:00
EOF
2025-08-08 20:15:53 +03:00
chmod 600 "$cred"
clear
echo -e "${c_green}✔ Site installed successfully!${c_nc}"
echo
echo "Installation Report:"
echo " - Domain: https://${domain}"
echo " - Admin URL: https://${domain}/wp-admin"
echo " - Admin User: ${admin_user}"
echo " - Admin Pass: ${admin_pass}"
echo " - DB Name: ${db_name}"
echo " - DB User: ${db_user}"
echo " - SSL: $( [[ -f /etc/letsencrypt/live/${domain}/fullchain.pem ]] && echo Issued || echo Pending/Failed )"
echo " - Redis: Enabled"
echo " - Cache: FastCGI (ON)"
echo " - Cron: System cron enabled"
echo
echo "Credentials saved at: ${cred}"
press_any
2025-08-08 20:33:36 +03:00
CTX=()
2025-08-08 20:15:53 +03:00
}
remove_wp_site(){
clear; echo -e "${c_red}--- Remove WordPress Site ---${c_nc}\n"
local domain
read -rp "Domain to remove (example.com): " domain
[[ -n "$domain" ]] || { warn "No domain provided."; press_any; return; }
local site_dir="${WEBROOT}/${domain}"
if [[ ! -d "$site_dir" ]]; then
warn "No site found at ${site_dir}."
press_any; return
fi
echo -e "${c_yellow}This will permanently delete files, DB, user, and Nginx/PHP configs for ${domain}.${c_nc}"
read -rp "Type the domain again to confirm: " confirm_domain
[[ "$confirm_domain" == "$domain" ]] || { warn "Confirmation mismatch. Aborting."; press_any; return; }
local db_name=""; local db_user=""
if [[ -f "${site_dir}/wp-config.php" ]]; then
db_name=$(grep "DB_NAME" "${site_dir}/wp-config.php" | cut -d \' -f 4 || true)
db_user=$(grep "DB_USER" "${site_dir}/wp-config.php" | cut -d \' -f 4 || true)
fi
rm -f "/etc/nginx/sites-available/${domain}" "/etc/nginx/sites-enabled/${domain}"
rm -f "/etc/php/${PHP_VERSION}/fpm/pool.d/${domain}.conf"
systemctl reload nginx "php${PHP_VERSION}-fpm" || true
if [[ -n "$db_name" ]]; then
mysql --defaults-file=/root/.my.cnf -e "DROP DATABASE IF EXISTS \`${db_name}\`;"
mysql --defaults-file=/root/.my.cnf -e "DROP USER IF EXISTS '${db_user}'@'localhost';"
fi
rm -rf "${site_dir}"
ok "Site ${domain} removed."
press_any
}
list_sites(){
clear; echo -e "${c_blue}--- Managed Sites ---${c_nc}\n"
2025-08-08 20:33:36 +03:00
if ls -1 "${WEBROOT}" | grep -v '^html$' 2>/dev/null; then :; else echo "No sites found."; fi
2025-08-08 20:15:53 +03:00
echo; press_any
}
manage_cache(){
clear; echo -e "${c_blue}--- Manage Cache ---${c_nc}\n"
read -rp "Domain: " domain
local conf="/etc/nginx/sites-available/${domain}"
[[ -f "$conf" ]] || { warn "Vhost not found."; press_any; return; }
if grep -q "fastcgi_cache WORDPRESS" "$conf"; then
echo "Cache is currently: ON"
read -rp "Turn OFF cache? (y/N): " a
if [[ "$a" =~ ^[Yy]$ ]]; then
2025-08-08 20:33:36 +03:00
nginx_site_write "$domain" "${WEBROOT}/${domain}" "off"
ok "Cache disabled."
2025-08-08 20:15:53 +03:00
fi
else
echo "Cache is currently: OFF"
read -rp "Turn ON cache? (y/N): " a
if [[ "$a" =~ ^[Yy]$ ]]; then
2025-08-08 20:33:36 +03:00
nginx_site_write "$domain" "${WEBROOT}/${domain}" "on"
ok "Cache enabled."
2025-08-08 20:15:53 +03:00
fi
fi
press_any
}
xmlrpc_whitelist_menu(){
clear; echo -e "${c_blue}--- XML-RPC Whitelist (Global) ---${c_nc}\n"
echo "Current entries:"
grep -E "^\s*([0-9]{1,3}\.){3}[0-9]{1,3}" "$XMLRPC_WHITELIST_FILE" | awk '{print " - " $1}' || echo " - None"
echo
echo "1) Add IP to whitelist (e.g., your Odoo server)"
echo "2) Remove IP from whitelist"
echo "3) Back"
read -rp "Choice: " c
case "$c" in
1)
read -rp "Enter IP to ALLOW: " ip
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
if grep -q "$ip" "$XMLRPC_WHITELIST_FILE"; then
warn "IP already present."
else
2025-08-08 20:33:36 +03:00
sed -i "/^geo /a\ \ \ \ ${ip} 1;" "$XMLRPC_WHITELIST_FILE"
2025-08-08 20:15:53 +03:00
nginx -t && systemctl reload nginx
ok "Added ${ip}."
fi
else
warn "Invalid IP format."
fi
;;
2)
read -rp "Enter IP to REMOVE: " ip
if grep -q "$ip" "$XMLRPC_WHITELIST_FILE"; then
2025-08-08 20:33:36 +03:00
sed -i "/${ip}/d" "$XMLRPC_WHITELIST_FILE"
2025-08-08 20:15:53 +03:00
nginx -t && systemctl reload nginx
ok "Removed ${ip}."
else
warn "IP not found."
fi
;;
*);;
esac
press_any
}
backups_menu(){
clear; echo -e "${c_blue}--- Backups (Local Only) ---${c_nc}\n"
echo "1) Backup a single site now"
echo "2) Backup ALL sites now"
echo "3) Schedule daily backups at 02:00"
echo "4) Disable scheduled backups"
echo "5) Back"
read -rp "Choice: " c
case "$c" in
2025-08-08 20:33:36 +03:00
1) read -rp "Domain: " domain; backup_site "$domain" ;;
2) backup_all_sites ;;
3) (crontab -l 2>/dev/null | grep -v "woo.sh backup-all"; echo "0 2 * * * /usr/bin/env bash /root/woo.sh backup-all >> /var/log/woo-toolkit/backup.log 2>&1") | crontab - ; ok "Daily backups scheduled."; press_any ;;
4) (crontab -l 2>/dev/null | grep -v "woo.sh backup-all") | crontab - ; ok "Scheduled backups disabled."; press_any ;;
*) ;;
2025-08-08 20:15:53 +03:00
esac
}
backup_site(){
local domain="$1"
local dir="${WEBROOT}/${domain}"
[[ -d "$dir" ]] || { warn "No site found at ${dir}."; press_any; return; }
2025-08-08 20:33:36 +03:00
log "Backing up ${domain}"
2025-08-08 20:15:53 +03:00
local out="/root/woo_backups/${domain}"; mkdir -p "$out"
local ts="$(date +%Y%m%d_%H%M%S)"
local archive="${out}/full_${ts}.tar.gz"
local db="$(sudo -u www-data wp config get DB_NAME --path="$dir" --quiet || echo)"
local tmpdb="/tmp/db_${domain}_${ts}.sql"
if [[ -n "$db" ]]; then
sudo -u www-data wp db export "$tmpdb" --path="$dir" || true
tar -czf "$archive" -C "$dir" . -C /tmp "$(basename "$tmpdb")"
rm -f "$tmpdb"
else
tar -czf "$archive" -C "$dir" .
fi
ls -tp "${out}"/full_*.tar.gz 2>/dev/null | tail -n +8 | xargs -r -d $'\n' rm -f --
ok "Backup stored at: ${archive}"
press_any
}
backup_all_sites(){
log "--- Backing up all sites ---"
for s in $(ls -1 "${WEBROOT}" 2>/dev/null | grep -v '^html$' || true); do
backup_site "$s"
done
ok "All backups complete."
}
doctor_report(){
clear; echo -e "${c_blue}--- System Doctor Report ---${c_nc}\n"
echo "- OS: $(. /etc/os-release; echo "$PRETTY_NAME")"
echo "- Nginx: $(nginx -v 2>&1)"
echo "- PHP-FPM: $(php-fpm${PHP_VERSION} -v 2>/dev/null | head -n1 || echo php${PHP_VERSION})"
echo "- MariaDB: $(mysql --version)"
echo "- Redis: $(redis-server --version 2>/dev/null | head -n1 || echo 'installed')"
echo "- RAM total: $(awk '/MemTotal/ {print int($2/1024) "MB"}' /proc/meminfo)"
echo "- Free disk (/): $(df -h / | awk 'NR==2{print $4 " free"}')"
echo "- Nginx config test:"; nginx -t || true
echo "- SSL certs expiring in <14 days:"
for d in /etc/letsencrypt/live/*; do
[[ -d "$d" ]] || continue
local crt="$d/fullchain.pem"; [[ -f "$crt" ]] || continue
local exp_ts=$(date -d "$(openssl x509 -enddate -noout -in "$crt" | cut -d= -f2)" +%s)
local now=$(date +%s); local diff=$(( (exp_ts-now)/86400 ))
(( diff < 14 )) && echo " - $(basename "$d"): ${diff}d"
done
echo; press_any
}
2025-08-08 20:33:36 +03:00
# --------------------------- First-run Setup --------------------------------
2025-08-08 20:15:53 +03:00
first_run_setup(){
preflight
ensure_swap
install_stack
secure_mariadb
tune_mariadb
harden_php
harden_imagick
harden_firewall_fail2ban
nginx_global
systemd_logrotate
2025-08-08 20:33:36 +03:00
# Create the 'woo' convenience command
cat >/usr/local/bin/woo <<'EOF'
#!/usr/bin/env bash
exec sudo -E bash /root/woo.sh "$@"
EOF
chmod +x /usr/local/bin/woo
ok "Base server setup complete. Next time, just type: woo"
2025-08-08 20:15:53 +03:00
press_any
}
# --------------------------- Menu -------------------------------------------
main_menu(){
while true; do
clear
echo -e "${c_blue}=====================================================${c_nc}"
echo -e "${c_blue} WOO — WordPress Operations Orchestrator (v12) ${c_nc}"
echo -e "${c_blue}=====================================================${c_nc}\n"
echo " 1) Add New Site"
echo " 2) Remove Site"
echo " 3) List Sites"
echo " 4) Manage Cache"
echo " 5) XML-RPC Whitelist"
echo " 6) Backups"
echo " 7) Doctor (Report)"
echo " 8) Exit"
echo
read -rp "Choose an option [1-8]: " ans
case "$ans" in
1) create_wp_site ;;
2) remove_wp_site ;;
3) list_sites ;;
4) manage_cache ;;
5) xmlrpc_whitelist_menu ;;
6) backups_menu ;;
7) doctor_report ;;
8) clear; exit 0 ;;
*) warn "Invalid option." ; press_any ;;
2025-08-08 20:33:36 +03:00
case_esac
2025-08-08 20:15:53 +03:00
done
2025-08-06 20:00:38 +03:00
}
2025-08-06 19:01:20 +03:00
2025-08-08 20:15:53 +03:00
# --------------------------- Entry Point ------------------------------------
2025-08-08 20:33:36 +03:00
main(){
# Normalize line endings if needed (harmless if already fine)
command -v dos2unix >/dev/null 2>&1 || { apt-get update -y && apt-get install -y dos2unix; }
dos2unix "$0" >/dev/null 2>&1 || true
perl -i -pe 's/\r$//' "$0" || true
# Detect PHP version after packages (or default for paths)
if [[ -z "$PHP_VERSION" ]]; then
for v in "${PHP_CANDIDATES[@]}"; do
[[ -d "/etc/php/${v}" ]] && PHP_VERSION="$v" && break
done
[[ -z "$PHP_VERSION" ]] && PHP_VERSION="8.2"
fi
# One-time base setup
if [[ ! -f /root/.woo-first-run-complete ]]; then
first_run_setup
touch /root/.woo-first-run-complete
fi
# Always show the menu
main_menu
}
main "$@"