403Webshell
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 :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/clients/client2/taschenmuschi-kaufen.de/web/install.sh
#!/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."

Youez - 2016 - github.com/yon3zu
LinuXploit