| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/clients/client2/taschenmuschi-kaufen.de/web/ |
Upload File : |
#!/usr/bin/env bash
# ============================================================
# install.sh — Setup-Assistent für Taschenmuschi-Kaufen.de
#
# Was der Installer tut:
# 1. Voraussetzungen prüfen (php, mysql-client, whiptail, curl, git, gh)
# 2. DB-Credentials aus wp-config.php lesen oder erfragen + Verbindung testen
# 3. Optional: SQL-Dump importieren
# 4. Ordner-/Datei-Berechtigungen umstellen (sicher für ispconfig)
# 5. Nginx-Direktiven für ispconfig generieren (Pretty-URLs, Caching, Schutz)
# 6. GitHub-Repo anlegen + initial pushen ODER existierendes Repo updaten (Cache)
# 7. Optional: Installer + Dev-Hilfsdateien aus dem Web-Root löschen
#
# Ausführung im Web-Root (z. B. /var/www/clients/clientX/webY/web/):
# bash install.sh
#
# Flags:
# --non-interactive Stiller Modus — alle Schritte ausführen ohne UI-Bestätigung
# --skip-db DB-Schritt überspringen
# --skip-perms Berechtigungen nicht ändern
# --skip-nginx Keine Nginx-Direktiven generieren
# --skip-github GitHub-Phase überspringen
# --dry-run Nichts schreiben, nur anzeigen, was passieren würde
#
# Sicherheit:
# - DB-Passwörter werden NIE in die Logdatei geschrieben
# - Aborts, wenn nicht im WP-Root (kein wp-config.php gefunden)
# - Warnung bei Root-Ausführung
# ============================================================
set -euo pipefail
# -------------- Globals + Defaults --------------
PROJECT_NAME="Taschenmuschi-Kaufen.de"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
LOG_FILE="${SCRIPT_DIR}/install.log"
NGINX_OUT_FILE="${SCRIPT_DIR}/nginx-directives.conf"
CACHE_FILE="${SCRIPT_DIR}/.tmk-install-cache"
GITIGNORE_FILE="${SCRIPT_DIR}/.gitignore"
INTERACTIVE=1
DO_DB=1
DO_PERMS=1
DO_NGINX=1
DO_GITHUB=1
DRY_RUN=0
# -------------- Farbcodes (für Plain-TTY-Output) --------------
if [[ -t 1 ]]; then
C_RESET="\033[0m"; C_BOLD="\033[1m"
C_OK="\033[32m"; C_WARN="\033[33m"; C_ERR="\033[31m"; C_INFO="\033[36m"
else
C_RESET=""; C_BOLD=""; C_OK=""; C_WARN=""; C_ERR=""; C_INFO=""
fi
log() { printf "%b%s%b\n" "${C_INFO}" "[INFO] $*" "${C_RESET}"; echo "[INFO] $(date -Iseconds) — $*" >> "$LOG_FILE"; }
ok() { printf "%b%s%b\n" "${C_OK}" "[ OK ] $*" "${C_RESET}"; echo "[ OK ] $(date -Iseconds) — $*" >> "$LOG_FILE"; }
warn() { printf "%b%s%b\n" "${C_WARN}" "[WARN] $*" "${C_RESET}"; echo "[WARN] $(date -Iseconds) — $*" >> "$LOG_FILE"; }
err() { printf "%b%s%b\n" "${C_ERR}" "[FAIL] $*" "${C_RESET}"; echo "[FAIL] $(date -Iseconds) — $*" >> "$LOG_FILE"; }
die() { err "$*"; exit 1; }
# -------------- Args parsen --------------
for arg in "$@"; do
case "$arg" in
--non-interactive) INTERACTIVE=0 ;;
--skip-db) DO_DB=0 ;;
--skip-perms) DO_PERMS=0 ;;
--skip-nginx) DO_NGINX=0 ;;
--skip-github) DO_GITHUB=0 ;;
--dry-run) DRY_RUN=1 ;;
-h|--help)
# Header-Kommentar aus dem Skript-Anfang ausgeben.
awk '/^#!/{next} /^# /{print substr($0,3); next} /^#$/{print ""; next} /^[^#]/{exit}' "$0"
exit 0 ;;
*) die "Unbekannte Option: $arg (--help für Hilfe)" ;;
esac
done
# -------------- UI-Helper --------------
HAS_WHIPTAIL=0
command -v whiptail >/dev/null 2>&1 && HAS_WHIPTAIL=1
ui_msgbox() {
local title="$1"; local body="$2"
if (( INTERACTIVE && HAS_WHIPTAIL )); then
whiptail --title "$title" --backtitle "$PROJECT_NAME — Installer" --msgbox "$body" 18 78
else
printf "\n${C_BOLD}%s${C_RESET}\n%s\n\n" "$title" "$body"
fi
}
ui_yesno() {
local title="$1"; local body="$2"
if (( INTERACTIVE && HAS_WHIPTAIL )); then
whiptail --title "$title" --backtitle "$PROJECT_NAME — Installer" --yesno "$body" 14 78
return $?
elif (( INTERACTIVE )); then
read -r -p "$title — $body [y/N]: " a
[[ "$a" =~ ^[yYjJ]$ ]]
else
return 0
fi
}
ui_input() {
local title="$1"; local body="$2"; local default="${3-}"
local out=""
if (( INTERACTIVE && HAS_WHIPTAIL )); then
out=$(whiptail --title "$title" --backtitle "$PROJECT_NAME — Installer" --inputbox "$body" 14 78 "$default" 3>&1 1>&2 2>&3) || return 1
elif (( INTERACTIVE )); then
read -r -p "$title — $body [$default]: " out
[[ -z "$out" ]] && out="$default"
else
out="$default"
fi
printf '%s' "$out"
}
ui_password() {
local title="$1"; local body="$2"
local out=""
if (( INTERACTIVE && HAS_WHIPTAIL )); then
out=$(whiptail --title "$title" --backtitle "$PROJECT_NAME — Installer" --passwordbox "$body" 12 78 3>&1 1>&2 2>&3) || return 1
elif (( INTERACTIVE )); then
read -r -s -p "$title — $body: " out; echo
fi
printf '%s' "$out"
}
ui_menu() {
local title="$1"; local body="$2"; shift 2
if (( INTERACTIVE && HAS_WHIPTAIL )); then
whiptail --title "$title" --backtitle "$PROJECT_NAME — Installer" --menu "$body" 18 78 8 "$@" 3>&1 1>&2 2>&3
else
# Plain prompt fallback
local i=1; local map=(); local val
while [[ $# -gt 0 ]]; do
val="$1"; shift
local label="$1"; shift
map+=("$val")
echo " $i) $label ($val)"
i=$((i+1))
done
local sel=""
read -r -p "Auswahl [1]: " sel
sel=${sel:-1}
printf '%s' "${map[$((sel-1))]}"
fi
}
# -------------- Vorbedingungen --------------
echo "" > "$LOG_FILE" || true
log "Installer gestartet im Verzeichnis: $SCRIPT_DIR"
# In WP-Root?
[[ -f "$SCRIPT_DIR/wp-config.php" || -f "$SCRIPT_DIR/wp-config-sample.php" ]] \
|| die "Kein wp-config.php gefunden. Bitte im WordPress-Root ausführen (./install.sh)."
# Tools prüfen
NEED_TOOLS=( php mysql find chmod chown stat awk sed grep )
MISSING=()
for t in "${NEED_TOOLS[@]}"; do command -v "$t" >/dev/null 2>&1 || MISSING+=("$t"); done
if (( ${#MISSING[@]} > 0 )); then
die "Fehlende Tools: ${MISSING[*]}. Bitte via Paketverwaltung nachinstallieren (z. B. apt install php-cli mariadb-client whiptail)."
fi
(( HAS_WHIPTAIL )) || warn "whiptail nicht gefunden — fallback auf Plain-Text-Prompts. Empfohlen: 'apt install whiptail'."
# Root-Warnung
if [[ $EUID -eq 0 ]]; then
warn "Du läufst als root. ispconfig-Webdateien gehören dem Web-User (z. B. webXX) — root-Ownership würde PHP-FPM-Schreibzugriff brechen."
ui_yesno "Root erkannt" "Trotzdem fortfahren? (Berechtigungs-Schritt setzt dann auf den gewählten Ziel-User um.)" || die "Abgebrochen."
fi
# -------------- Begrüßung --------------
ui_msgbox "$PROJECT_NAME — Setup-Assistent" "Dieser Installer ist menü-getrieben.\nDu wählst gezielt, was ausgeführt werden soll — du musst NICHT alles linear durchlaufen.\n\nVerfügbare Aktionen:\n • GitHub: Repo anlegen oder Updates pushen (auch von lokal aus)\n • Voll-Setup auf Produktivserver: DB + Berechtigungen + Nginx + Cleanup\n • Einzelne Schritte gezielt ausführen\n\nLog-Datei: $(basename "$LOG_FILE")\n\nWeiter mit OK."
# ============================================================
# PHASE 1 — DB-CONFIG & IMPORT
# ============================================================
run_db_phase() {
(( DO_DB )) || { log "DB-Phase übersprungen (--skip-db)."; return 0; }
local cfg="$SCRIPT_DIR/wp-config.php"
local db_name="" db_user="" db_pass="" db_host="localhost"
if [[ -f "$cfg" ]]; then
# Werte aus wp-config.php auslesen (DB_NAME, DB_USER, DB_PASSWORD, DB_HOST).
db_name=$(awk -F"['\"]" '/define\(\s*[\x27"]DB_NAME[\x27"]/{print $4}' "$cfg" | head -1)
db_user=$(awk -F"['\"]" '/define\(\s*[\x27"]DB_USER[\x27"]/{print $4}' "$cfg" | head -1)
db_pass=$(awk -F"['\"]" '/define\(\s*[\x27"]DB_PASSWORD[\x27"]/{print $4}' "$cfg" | head -1)
db_host=$(awk -F"['\"]" '/define\(\s*[\x27"]DB_HOST[\x27"]/{print $4}' "$cfg" | head -1)
log "Werte aus wp-config.php gelesen: db=$db_name user=$db_user host=$db_host (Passwort nicht ge-logged)"
fi
# User darf Werte überschreiben.
if (( INTERACTIVE )); then
db_host=$(ui_input "Datenbank-Host" "Host (i. d. R. 'localhost' oder die ispconfig-DB-IP):" "$db_host")
db_name=$(ui_input "Datenbank-Name" "Name der Ziel-Datenbank:" "$db_name")
db_user=$(ui_input "Datenbank-User" "User mit Read/Write-Rechten auf der DB:" "$db_user")
if [[ -z "$db_pass" ]] || ui_yesno "Datenbank-Passwort" "Passwort neu erfassen? (Sonst Wert aus wp-config.php verwenden.)"; then
db_pass=$(ui_password "Datenbank-Passwort" "Passwort eingeben (wird nicht geloggt):")
fi
fi
[[ -n "$db_name" && -n "$db_user" ]] || die "DB-Name/User fehlen."
# Verbindung testen
log "Teste Verbindung zur Datenbank '$db_name'…"
if MYSQL_PWD="$db_pass" mysql --host="$db_host" --user="$db_user" --batch --skip-column-names \
-e "SELECT 'OK';" "$db_name" >/dev/null 2>>"$LOG_FILE"; then
ok "Datenbankverbindung steht."
else
warn "Verbindung fehlgeschlagen. Wenn die DB noch nicht existiert, lege sie zuerst in ispconfig an."
ui_yesno "Trotz Fehler weiter?" "Mit dem Setup fortfahren (Import wird übersprungen)?" || die "Abgebrochen."
return 0
fi
# SQL-Dump anbieten.
local default_dump=""
for cand in "$SCRIPT_DIR/install.sql" "$SCRIPT_DIR/database.sql" "$SCRIPT_DIR/_staging/install.sql" "$SCRIPT_DIR/tools/install.sql"; do
[[ -f "$cand" ]] && { default_dump="$cand"; break; }
done
if ui_yesno "SQL-Import" "SQL-Dump in die Datenbank importieren?\n(Existierende Tabellen werden ÜBERSCHRIEBEN. Bei leerer Frisch-DB einfach 'Ja'.)"; then
local dump_path
dump_path=$(ui_input "SQL-Dump-Pfad" "Pfad zur .sql-Datei (relativ zum Web-Root oder absolut):" "${default_dump:-./install.sql}")
# Pfad relativ → absolut
[[ "$dump_path" != /* ]] && dump_path="$SCRIPT_DIR/$dump_path"
[[ -f "$dump_path" ]] || { warn "Datei nicht gefunden: $dump_path — Import übersprungen."; return 0; }
if (( DRY_RUN )); then
log "[dry-run] würde importieren: $dump_path → $db_name"
else
log "Importiere $dump_path → $db_name (kann dauern)…"
if MYSQL_PWD="$db_pass" mysql --host="$db_host" --user="$db_user" "$db_name" < "$dump_path" 2>>"$LOG_FILE"; then
ok "Import abgeschlossen."
else
err "Import schlug fehl — siehe $LOG_FILE für Details."
fi
fi
else
log "Import übersprungen (User-Wahl)."
fi
# Falls Site-URL anders: Suchen/Ersetzen anbieten.
if ui_yesno "Site-URL anpassen?" "Falls die Datenbank von einer anderen Domain kopiert wurde, hier die alte URL auf die neue umschreiben (siteurl/home + serialisierte Inhalte)."; then
local old_url new_url
old_url=$(ui_input "Alte URL" "z. B. http://localhost:8090 (ohne Slash am Ende):" "")
new_url=$(ui_input "Neue URL" "z. B. https://taschenmuschi-kaufen.de:" "")
if [[ -n "$old_url" && -n "$new_url" && "$old_url" != "$new_url" ]]; then
local prefix
prefix=$(awk -F"['\"]" '/\$table_prefix\s*=/{print $2}' "$cfg" | head -1)
prefix=${prefix:-wp_}
if (( DRY_RUN )); then
log "[dry-run] würde UPDATE ${prefix}options siteurl/home + serialized replace machen"
else
MYSQL_PWD="$db_pass" mysql --host="$db_host" --user="$db_user" "$db_name" <<SQL 2>>"$LOG_FILE"
UPDATE ${prefix}options SET option_value='${new_url}' WHERE option_name IN ('siteurl','home');
SQL
ok "siteurl + home auf $new_url gesetzt. Tipp: für serialisierte Werte später 'wp search-replace' nutzen (WP-CLI)."
fi
fi
fi
}
# ============================================================
# PHASE 2 — BERECHTIGUNGEN
# ============================================================
run_perms_phase() {
(( DO_PERMS )) || { log "Berechtigungen übersprungen (--skip-perms)."; return 0; }
# Web-User detect
local detected_user detected_group
detected_user=$(stat -c %U "$SCRIPT_DIR" 2>/dev/null || echo "")
detected_group=$(stat -c %G "$SCRIPT_DIR" 2>/dev/null || echo "")
log "Verzeichnis gehört: ${detected_user:-?}:${detected_group:-?}"
local web_user="$detected_user"
local web_group="$detected_group"
if (( INTERACTIVE )); then
web_user=$(ui_input "Web-User" "User, dem die Dateien gehören sollen (ispconfig: meist 'webXX'):" "$web_user")
web_group=$(ui_input "Web-Group" "Gruppe (ispconfig: meist 'clientYY'):" "$web_group")
fi
[[ -n "$web_user" && -n "$web_group" ]] || { warn "User/Group leer — Ownership-Schritt übersprungen."; }
ui_yesno "Berechtigungen umstellen?" "Setze:\n • Verzeichnisse: 755\n • Dateien: 644\n • wp-config.php: 600\n • Ownership: ${web_user:-?}:${web_group:-?}\n\nDas dauert je nach Anzahl Dateien einige Sekunden." || { log "Berechtigungen übersprungen (User-Wahl)."; return 0; }
if (( DRY_RUN )); then
log "[dry-run] würde chown -R ${web_user}:${web_group} + find chmod laufen lassen"
return 0
fi
if [[ -n "$web_user" && -n "$web_group" ]]; then
log "Setze Ownership rekursiv auf ${web_user}:${web_group}…"
chown -R "${web_user}:${web_group}" "$SCRIPT_DIR" 2>>"$LOG_FILE" \
|| warn "Ownership-Wechsel teilweise fehlgeschlagen (root benötigt)."
fi
log "Setze Verzeichnis-Rechte auf 755…"
find "$SCRIPT_DIR" -type d -exec chmod 755 {} \; 2>>"$LOG_FILE"
log "Setze Datei-Rechte auf 644…"
find "$SCRIPT_DIR" -type f -exec chmod 644 {} \; 2>>"$LOG_FILE"
# wp-config.php strenger
[[ -f "$SCRIPT_DIR/wp-config.php" ]] && {
chmod 640 "$SCRIPT_DIR/wp-config.php"
ok "wp-config.php auf 640 gesetzt."
}
# uploads/ braucht der Web-User volle Schreibrechte (PHP-FPM läuft als web_user)
if [[ -d "$SCRIPT_DIR/wp-content/uploads" ]]; then
find "$SCRIPT_DIR/wp-content/uploads" -type d -exec chmod 755 {} \;
find "$SCRIPT_DIR/wp-content/uploads" -type f -exec chmod 644 {} \;
ok "uploads/ schreibbar gestellt (755 dirs / 644 files)."
fi
# Installer selbst ausführbar lassen
[[ -f "$SCRIPT_DIR/install.sh" ]] && chmod 750 "$SCRIPT_DIR/install.sh"
ok "Berechtigungen gesetzt."
}
# ============================================================
# PHASE 3 — NGINX-DIREKTIVEN
# ============================================================
run_nginx_phase() {
(( DO_NGINX )) || { log "Nginx-Direktiven übersprungen (--skip-nginx)."; return 0; }
local domain
domain=$(ui_input "Domain" "Domain, die diese Site bedient (für Hinweis-Kommentar, nicht in der Config):" "taschenmuschi-kaufen.de")
log "Generiere Nginx-Direktiven nach: $NGINX_OUT_FILE"
cat > "$NGINX_OUT_FILE" <<NGINX
# ============================================================
# Nginx-Direktiven für ${domain}
# generiert: $(date -Iseconds)
#
# In ispconfig: Sites → ${domain} → Reiter „Optionen" → „Nginx-Direktiven"
# (komplett einfügen, oben drüber/drunter setzt ispconfig server/location auto).
# Server-Hauptblock + Listen + SSL kommen aus den ispconfig-Site-Settings.
# ============================================================
# -- Performance / Sicherheit ---------------------------------
client_max_body_size 64M;
server_tokens off;
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;
add_header Permissions-Policy "interest-cohort=()" always;
# -- WordPress Pretty-Permalinks ------------------------------
location / {
try_files \$uri \$uri/ /index.php?\$args;
}
# -- PHP-Handler (ispconfig setzt fastcgi_pass selbst — wir geben nur die
# Datei-Maschen + Fallback an, damit Plugin-/Theme-Updates funktionieren).
location ~ \.php\$ {
try_files \$uri =404;
include /etc/nginx/fastcgi_params;
fastcgi_split_path_info ^(.+\.php)(/.+)\$;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_param PATH_INFO \$fastcgi_path_info;
fastcgi_intercept_errors on;
# Hinweis: fastcgi_pass kommt aus dem ispconfig-Auto-Block (unix:/var/lib/php7.x-fpm/web*.sock).
}
# -- Static-Asset-Caching -------------------------------------
location ~* \.(?:css|js|woff2?|ttf|eot|otf|svg|ico|png|jpe?g|gif|webp|avif|mp4|webm)\$ {
expires 30d;
access_log off;
add_header Cache-Control "public, max-age=2592000, immutable";
try_files \$uri =404;
}
# -- Schutz vor Crawlern bei sensiblen WP-Dateien --------------
location = /wp-config.php { deny all; return 404; }
location = /wp-config-sample.php { deny all; return 404; }
location = /readme.html { deny all; return 404; }
location = /license.txt { deny all; return 404; }
location = /xmlrpc.php { deny all; return 404; }
location = /wp-admin/install.php { deny all; return 404; }
location = /wp-admin/maint/repair.php { deny all; return 404; }
# -- Dev-/Build-Dateien dieses Projekts blockieren ------------
location = /install.sh { deny all; return 404; }
location = /install.log { deny all; return 404; }
location = /install-wp.php { deny all; return 404; }
location = /nginx-directives.conf { deny all; return 404; }
location = /router.php { deny all; return 404; }
location = /start-local.sh { deny all; return 404; }
location ~ /\\.local-server\\. { deny all; return 404; }
location ^~ /_staging/ { deny all; return 404; }
location ^~ /tools/ { deny all; return 404; }
# -- Schutz vor Hidden-/Dotfiles + Logs -----------------------
location ~ /\\.(git|env|htaccess|svn|hg) { deny all; return 404; }
location ~ /(?:\\.|wp-content/debug\\.log\$) { deny all; return 404; }
# -- Keine PHP-Ausführung in uploads/ -------------------------
location ~* /wp-content/uploads/.+\\.(?:php|phtml|phar|pl|py|jsp|asp|sh|cgi)\$ {
deny all;
return 404;
}
# -- Author-Enumeration verhindern ----------------------------
if (\$args ~* "author=\\d") {
return 403;
}
# -- xmlrpc.php komplett zu (siehe oben), zusätzlich path-tolerant:
location ~* ^/xmlrpc\\.php\$ { deny all; return 404; }
# -- Optionale Auth für wp-login.php (HTTP-Basic-Schutz, falls erwünscht).
# location = /wp-login.php {
# auth_basic "Login geschützt";
# auth_basic_user_file /var/www/clients/clientX/webY/.htpasswd;
# try_files \$uri =404;
# include /etc/nginx/fastcgi_params;
# fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
# # fastcgi_pass siehe ispconfig-Auto-Block
# }
# -- Health-Check für Monitoring (optional) -------------------
# location = /health.txt { return 200 "ok"; add_header Content-Type text/plain; }
NGINX
ok "Datei geschrieben: $NGINX_OUT_FILE"
ui_msgbox "Nginx-Direktiven" "Direktiven wurden gespeichert in:\n $NGINX_OUT_FILE\n\nNächste Schritte:\n 1. Datei öffnen und Inhalt kopieren\n 2. In ispconfig:\n Sites → ${domain} → Tab „Optionen" → „Nginx-Direktiven"\n 3. Speichern → ispconfig schreibt /etc/nginx/sites-available/* neu und lädt nginx\n\nNach erfolgreichem Test ist nginx-directives.conf entbehrlich und sollte gelöscht werden (wird ohnehin per Direktive blockiert)."
if (( INTERACTIVE )); then
ui_yesno "Anzeigen" "Direktiven jetzt auf dem Terminal ausgeben?" && cat "$NGINX_OUT_FILE"
fi
}
# ============================================================
# PHASE 4 — GITHUB (Repo anlegen ODER updaten)
# ============================================================
# --- Cache-Helper (Key=Value-Storage in .tmk-install-cache) ---
cache_get() {
[[ -f "$CACHE_FILE" ]] || return 1
local val
val=$(grep -E "^$1=" "$CACHE_FILE" | head -1 | cut -d= -f2-)
[[ -n "$val" ]] || return 1
printf '%s' "$val"
}
cache_save() {
# Args: KEY=VALUE …
local tmp; tmp="$(mktemp)"
[[ -f "$CACHE_FILE" ]] && cp "$CACHE_FILE" "$tmp"
for kv in "$@"; do
local key="${kv%%=*}"
# Existing key entfernen, dann neue Zeile anhängen.
sed -i.bak "/^${key}=/d" "$tmp" 2>/dev/null && rm -f "$tmp.bak"
echo "$kv" >> "$tmp"
done
mv "$tmp" "$CACHE_FILE"
chmod 600 "$CACHE_FILE"
}
# --- .gitignore generieren (nur falls fehlt) ---
ensure_gitignore() {
[[ -f "$GITIGNORE_FILE" ]] && { log ".gitignore existiert bereits — keine Änderung."; return; }
cat > "$GITIGNORE_FILE" <<'GIT'
# Secrets & Installer-Artefakte
/wp-config.php
/install.log
/nginx-directives.conf
/.tmk-install-cache
# WordPress-Runtime
/wp-content/uploads/
/wp-content/cache/
/wp-content/upgrade/
/wp-content/debug.log
/wp-content/advanced-cache.php
/wp-content/wp-cache-config.php
/wp-content/*.log
# Lokale Dev-Artefakte
/_staging/
/tools/cache/
/.local-server.log
/.local-server.pid
# Editor / OS
.vscode/
.idea/
*.swp
.DS_Store
Thumbs.db
GIT
ok ".gitignore erstellt (Secrets + Runtime + Dev-Dateien ausgeschlossen)."
}
# --- Manuelle Anleitung bei gh-Fehler ---
show_manual_create_instructions() {
local owner="$1"; local repo="$2"; local vis="$3"
cat <<MANUAL
${C_BOLD}Manuelle Alternativen zum Anlegen + Pushen:${C_RESET}
${C_INFO}Variante A — gh-cli installieren${C_RESET}
Debian/Ubuntu: type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=\$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null
sudo apt update && sudo apt install gh -y
Dann: gh auth login # einmalig pro Server
gh repo create $owner/$repo --$vis --source=. --remote=origin --push
${C_INFO}Variante B — Web-UI + git${C_RESET}
1. https://github.com/new öffnen → "$owner/$repo" anlegen
(kein README, kein .gitignore, keine License auswählen — wir haben alles lokal)
2. Im Terminal (im Web-Root):
git add -A
git commit -m "Initial commit"
git branch -M main
git remote add origin git@github.com:$owner/$repo.git
git push -u origin main
${C_INFO}Variante C — HTTPS mit Personal Access Token (PAT)${C_RESET}
1. PAT erstellen: https://github.com/settings/tokens (Scope: repo)
2. Im Terminal:
git add -A
git commit -m "Initial commit"
git branch -M main
git config credential.helper store # PAT bleibt in ~/.git-credentials
git remote add origin https://github.com/$owner/$repo.git
git push -u origin main
# → bei Passwort-Prompt den PAT eingeben
${C_INFO}Variante D — SSH-Key${C_RESET}
ssh-keygen -t ed25519 -C "deploy@${owner}" # falls noch kein Key existiert
cat ~/.ssh/id_ed25519.pub # Inhalt nach https://github.com/settings/ssh/new kopieren
ssh -T git@github.com # Verbindungstest, sollte "Hi $owner!" zeigen
git remote add origin git@github.com:$owner/$repo.git
git push -u origin main
MANUAL
}
# --- Push-Helper mit Fallback ---
try_git_push() {
local branch="$1"
if git push origin "$branch" 2>>"$LOG_FILE"; then
return 0
fi
# Fehler-Analyse
local last_err
last_err=$(tail -n 30 "$LOG_FILE" 2>/dev/null | grep -E "(rejected|fatal|permission|denied|authentication)" | tail -3)
err "Push fehlgeschlagen. Letzte Fehler-Snippets:"
[[ -n "$last_err" ]] && err " $last_err"
warn "Mögliche Auflösungen — versuche der Reihe nach:"
warn " 1) Branch divergiert → git pull --rebase origin $branch && git push origin $branch"
warn " 2) Auth-Problem (PAT) → gh auth login ODER git credential reject (PAT erneuern)"
warn " 3) SSH-Key fehlt → ssh -T git@github.com (sollte 'Hi <user>!' zurückgeben)"
warn " 4) Force-Push (nur wenn Remote leer ist!): git push -f origin $branch"
if ui_yesno "Force-Push?" "Remote ist möglicherweise leer/inkonsistent — Force-Push versuchen?\n(NUR ja, wenn du sicher bist, dass keine fremden Commits dort sind.)"; then
git push -f origin "$branch" 2>>"$LOG_FILE" && return 0
fi
return 1
}
run_github_phase() {
(( DO_GITHUB )) || { log "GitHub-Phase übersprungen (--skip-github)."; return 0; }
command -v git >/dev/null 2>&1 || {
warn "git nicht installiert — überspringe GitHub-Phase. (Installation: apt install git)"
return 0
}
local HAS_GH=0
command -v gh >/dev/null 2>&1 && HAS_GH=1
ui_yesno "GitHub-Phase?" "Projekt in ein GitHub-Repository pushen?\n\n • Erstes Mal: neues Repo anlegen + initial pushen\n • Existiert ein Cache (.tmk-install-cache): einfaches Update\n\n$( (( HAS_GH )) && echo "gh-cli verfügbar — Auto-Setup möglich." || echo "gh-cli fehlt — Fallback auf manuelle Anweisungen.")" || { log "GitHub-Phase übersprungen (User-Wahl)."; return 0; }
cd "$SCRIPT_DIR"
# Git-Repo init, falls noch keins existiert
if [[ ! -d .git ]]; then
log "Kein .git-Verzeichnis — initialisiere…"
if (( DRY_RUN )); then
log "[dry-run] würde 'git init -b main' laufen lassen"
else
git init -b main >>"$LOG_FILE" 2>&1 \
|| { git init >>"$LOG_FILE" 2>&1 && git symbolic-ref HEAD refs/heads/main; }
git config user.email "$(cache_get GIT_USER_EMAIL 2>/dev/null || echo "deploy@$(hostname -f 2>/dev/null || hostname)")" 2>/dev/null || true
git config user.name "$(cache_get GIT_USER_NAME 2>/dev/null || echo "ispconfig-deploy")" 2>/dev/null || true
ok "git-Repo initialisiert (Branch: main)."
fi
fi
ensure_gitignore
# Cache prüfen → Update oder Create?
local cached_owner cached_repo cached_remote cached_branch
cached_owner=$(cache_get GH_OWNER 2>/dev/null || echo "")
cached_repo=$(cache_get GH_REPO 2>/dev/null || echo "")
cached_remote=$(cache_get GH_REMOTE 2>/dev/null || echo "")
cached_branch=$(cache_get GH_BRANCH 2>/dev/null || echo "main")
local mode="create"
if [[ -n "$cached_remote" ]]; then
if ui_yesno "Gemerktes Repository" "Letzter Push:\n Repo: ${cached_owner}/${cached_repo}\n Remote: $cached_remote\n Branch: $cached_branch\n Datum: $(cache_get LAST_PUSH 2>/dev/null || echo "—")\n\nUpdate (Commit + Push auf dieses Repo)?\n\nNein = neues Repo erstellen (überschreibt den Cache)."; then
mode="update"
else
mode="create"
fi
fi
if [[ "$mode" == "create" ]]; then
local owner repo visibility
owner=$(ui_input "GitHub-Owner" "User- oder Org-Name auf GitHub:" "${cached_owner:-}") || return 0
repo=$(ui_input "Repository-Name" "Name des neuen Repos:" "${cached_repo:-$(basename "$SCRIPT_DIR")}") || return 0
# Standard: privat (sicherer Default — enthält ggf. Server-Konfig/Setup-Skripte).
if ui_yesno "Sichtbarkeit" "Repo PRIVAT anlegen? (Empfehlung: Ja — der Code enthält ggf. Setup-/Server-Konfiguration.)\n\nNein = Public Repo."; then
visibility="private"
else
visibility="public"
fi
if [[ -z "$owner" || -z "$repo" ]]; then
warn "Owner/Repo leer — Phase abgebrochen."
return 0
fi
if (( DRY_RUN )); then
log "[dry-run] würde 'gh repo create $owner/$repo --$visibility --source=. --remote=origin --push' versuchen"
cache_save "GH_OWNER=$owner" "GH_REPO=$repo" "GH_REMOTE=git@github.com:$owner/$repo.git" "GH_BRANCH=main" "LAST_PUSH=$(date -Iseconds)"
return 0
fi
# Erst lokal alles committen, damit wir ein vorhandenes Commit-History haben.
git add -A >>"$LOG_FILE" 2>&1
if ! git rev-parse HEAD >/dev/null 2>&1; then
git commit -m "Initial commit" >>"$LOG_FILE" 2>&1 \
|| { warn "Initial-Commit fehlgeschlagen — git config user.name/email setzen und erneut versuchen."; }
fi
local remote_url=""
if (( HAS_GH )); then
log "Versuche Repo via gh-cli anzulegen: $owner/$repo"
if gh repo create "$owner/$repo" --"$visibility" --source=. --remote=origin --push 2>>"$LOG_FILE"; then
ok "Repo angelegt + initial gepusht via gh-cli."
remote_url=$(git remote get-url origin 2>/dev/null || echo "")
else
err "gh repo create fehlgeschlagen — siehe $LOG_FILE."
show_manual_create_instructions "$owner" "$repo" "$visibility"
ui_yesno "Manuell ausgeführt?" "Wenn du die obigen Befehle erfolgreich ausgeführt hast, mit 'Ja' bestätigen, sonst 'Nein'." || return 1
remote_url=$(git remote get-url origin 2>/dev/null || echo "")
fi
else
show_manual_create_instructions "$owner" "$repo" "$visibility"
ui_yesno "Manuell ausgeführt?" "Wenn du die obigen Befehle erfolgreich ausgeführt hast, mit 'Ja' bestätigen." || return 1
remote_url=$(git remote get-url origin 2>/dev/null || echo "")
fi
cache_save \
"GH_OWNER=$owner" \
"GH_REPO=$repo" \
"GH_REMOTE=${remote_url:-git@github.com:$owner/$repo.git}" \
"GH_BRANCH=main" \
"LAST_PUSH=$(date -Iseconds)"
ok "Repository in $CACHE_FILE gemerkt — beim nächsten Lauf wird Update-Mode angeboten."
else
# ==== UPDATE-MODUS ====
local msg
msg=$(ui_input "Commit-Message" "Beschreibung des Updates:" "Update: $(date '+%Y-%m-%d %H:%M')")
if (( DRY_RUN )); then
log "[dry-run] würde 'git add -A && git commit -m \"$msg\" && git push origin $cached_branch' laufen lassen"
return 0
fi
git add -A >>"$LOG_FILE" 2>&1
if git diff --cached --quiet; then
log "Keine Änderungen im Working-Tree — nichts zu committen."
else
if ! git commit -m "$msg" >>"$LOG_FILE" 2>&1; then
warn "Commit fehlgeschlagen — User-Identity setzen:"
warn " git config user.email 'deploy@<server>'"
warn " git config user.name 'ispconfig-deploy'"
return 1
fi
ok "Commit erstellt: $msg"
fi
# Origin sicherstellen
if ! git remote get-url origin >/dev/null 2>&1; then
log "Kein origin-Remote — setze auf Cache-Wert: $cached_remote"
git remote add origin "$cached_remote" >>"$LOG_FILE" 2>&1
fi
log "Pushe auf $cached_remote ($cached_branch)…"
if try_git_push "$cached_branch"; then
ok "Push abgeschlossen."
cache_save "LAST_PUSH=$(date -Iseconds)"
else
err "Push fehlgeschlagen — manuelle Auflösung nötig (Hinweise oben)."
return 1
fi
fi
}
# ============================================================
# PHASE 5 — CLEANUP
# ============================================================
run_cleanup_phase() {
ui_yesno "Aufräumen?" "Sensible Dev-Dateien aus dem Web-Root entfernen?\n\nLöschen:\n • install-wp.php (Headless-Install-Skript)\n • router.php (Lokaler PHP-Built-in-Router)\n • start-local.sh (Lokaler Dev-Starter)\n • .local-server.* (Dev-Logs/PID)\n • _staging/ (Vorlagen-HTML)\n • tools/ (Crawler-Caches)\n\nDie nginx-Direktiven blockieren diese Pfade ohnehin — Löschung ist die strengste Schutzschicht (Datei existiert nicht mehr)." || return 0
local to_remove=( install-wp.php router.php start-local.sh .local-server.log .local-server.pid _staging tools )
for p in "${to_remove[@]}"; do
local full="$SCRIPT_DIR/$p"
[[ -e "$full" ]] || continue
if (( DRY_RUN )); then
log "[dry-run] würde löschen: $full"
else
rm -rf -- "$full" && ok "Gelöscht: $p" || warn "Konnte nicht löschen: $p"
fi
done
if ui_yesno "Installer löschen?" "install.log + nginx-directives.conf entfernen?\n\nHinweis: .tmk-install-cache bleibt erhalten — damit weiß der Installer beim nächsten Lauf, in welches Repo er pushen muss (Update-Mode).\n\ninstall.sh selbst behalten? → wird vom Nginx ohnehin geblockt." ; then
if (( DRY_RUN )); then
log "[dry-run] würde install.log und nginx-directives.conf löschen"
else
rm -f -- "$NGINX_OUT_FILE" "$LOG_FILE"
ok "install.log + nginx-directives.conf entfernt."
fi
fi
# install.sh ist getrennt löschbar — nur wenn der User explizit will.
if ui_yesno "install.sh entfernen?" "Den Installer selbst (install.sh) löschen?\n\nEmpfehlung: BEHALTEN, solange du noch in Update-Mode pushen willst — und ja, Nginx blockt den Pfad ohnehin.\n\nNur 'Ja' wählen, wenn du sicher bist, dass du den Installer nie wieder ausführst."; then
if (( DRY_RUN )); then
log "[dry-run] würde install.sh löschen"
else
ok "Cleanup gleich abgeschlossen — entferne mich selbst."
rm -f -- "$SCRIPT_DIR/install.sh" || warn "install.sh konnte nicht entfernt werden — manuell löschen."
fi
fi
}
# ============================================================
# HAUPTMENÜ
# ============================================================
main_menu() {
while true; do
local choice
choice=$(ui_menu "$PROJECT_NAME — Hauptmenü" "Was möchtest du tun?\n(Lokal ist 'GitHub' der relevante Eintrag — Server-Schritte erst auf der Produktiv-Maschine ausführen.)" \
"github" "GitHub: Repo anlegen oder Updates pushen" \
"deploy" "Voll-Setup Produktivserver (DB + Perms + Nginx + Cleanup)" \
"db" "Nur Datenbank-Schritt (Credentials testen, SQL importieren)" \
"perms" "Nur Berechtigungen umstellen" \
"nginx" "Nur Nginx-Direktiven generieren" \
"cleanup" "Nur Dev-Dateien aufräumen" \
"exit" "Beenden") || break
case "$choice" in
github)
run_github_phase
;;
deploy)
run_db_phase
run_perms_phase
run_nginx_phase
run_github_phase
run_cleanup_phase
ui_msgbox "Voll-Setup abgeschlossen" "Alle Phasen sind durchlaufen.\n\nLog: ${LOG_FILE}\n\nLetzte Server-Schritte:\n • DNS auf den Server prüfen\n • SSL-Zertifikat in ispconfig ausstellen\n • Nginx-Reload via ispconfig (Optionen → Speichern)"
;;
db) run_db_phase ;;
perms) run_perms_phase ;;
nginx) run_nginx_phase ;;
cleanup) run_cleanup_phase ;;
exit|"") break ;;
esac
# Nach jeder Aktion: weitermachen oder beenden?
ui_yesno "Weiter?" "Möchtest du eine weitere Aktion ausführen?\n\nNein = Installer beenden." || break
done
}
# ============================================================
# MAIN
# ============================================================
if (( INTERACTIVE )); then
main_menu
else
# Headless-Modus: alle Phasen linear, mit Skip-Flags steuerbar.
log "Headless-Modus (--non-interactive) — alle aktivierten Phasen werden linear ausgeführt."
run_db_phase
run_perms_phase
run_nginx_phase
run_github_phase
run_cleanup_phase
fi
ok "Installer beendet."