16 Commits
1.0.2 ... main

20 changed files with 5648 additions and 797 deletions

686
README.md
View File

@@ -1,396 +1,290 @@
# WP Business Forum Anwender-Dokumentation
WP Business Forum bringt ein modernes, eigenständiges Community-Forum direkt in deine WordPress-Website.
Statt auf externe Plattformen auszuweichen, bleiben Diskussionen, Support-Anfragen und Mitgliederaktivität
zentral auf deiner eigenen Seite inklusive voller Kontrolle über Inhalte, Rollen und Moderation.
Diese Dokumentation richtet sich an Betreiber, Moderatoren und Community-Manager, die das Forum
schnell einrichten, sicher betreiben und im Alltag effizient verwalten möchten. Von der ersten
Installation bis zum Live-Betrieb findest du hier alle wichtigen Schritte und Funktionen kompakt erklärt.
---
## Inhalt
1. Über das Plugin
2. Funktionsübersicht
3. Voraussetzungen
4. Installation
5. Ersteinrichtung (Setup-Wizard)
6. Forum-Seite einbinden
7. Bedienung im Frontend (Mitglieder)
8. Moderation und Verwaltung
9. Einstellungen im Detail
10. Export, Import und Deinstallation
11. FAQ / Troubleshooting
---
## 1) Über das Plugin
WP Business Forum ist ein eigenständiges Foren-System für WordPress mit:
- eigenem Forum-Login (unabhängig vom WP-Login)
- Rollen- und Rechteverwaltung
- Kategorien mit Hierarchie
- Moderationswerkzeugen
- Direktnachrichten, Benachrichtigungen, Meldesystem
- Umfragen, Tags, Reaktionen, Lesezeichen
- Level-System (beitragsbasierte Rangstufen)
- Vollständigem Export / Import mit automatischer ID-Zuordnung
Das Forum wird per Shortcode in eine WordPress-Seite eingebunden.
---
## 2) Funktionsübersicht
### Für Mitglieder
- Registrieren / Einloggen / Logout
- Passwort vergessen und Reset per E-Mail
- Threads erstellen, antworten, bearbeiten
- Likes und Emoji-Reaktionen
- Tags und Thread-Präfixe
- Umfragen erstellen und abstimmen
- Lesezeichen setzen
- Nutzer erwähnen mit @mention
- Private Nachrichten (DM)
- Profil mit Avatar, Bio, Signatur und eigenen Profilfeldern
- Mitgliederliste und Suchfunktion
- Andere Nutzer ignorieren / blockieren
### Für Moderation / Admin
- Threads pinnen, schließen, archivieren, verschieben, löschen
- Beiträge löschen und wiederherstellen (Papierkorb)
- Meldungen (Reports) bearbeiten
- Kategorien und Rollen verwalten
- Mitglieder verwalten: Rolle ändern, Profil bearbeiten, Sperren, Löschen
- Einladungssystem für Registrierung
- Wartungsmodus
- Wortfilter / Zensurliste
- Statistiken und Aktivitäts-Dashboard
- Export / Import (vollständiges Backup mit Wortfilter, Ignore-Liste, Präfixen u. v. m.)
---
## 3) Voraussetzungen
- Laufende WordPress-Installation (empfohlen: aktuelle Version)
- PHP 7.4 oder höher (empfohlen: PHP 8.0+)
- MySQL 5.7 / MariaDB 10.3 oder höher
- Schreibrechte für WordPress-Uploads (für Avatar- und Bild-Uploads)
- Funktionierende E-Mail-Zustellung in WordPress (für Passwort-Reset und Benachrichtigungen)
> Das Plugin nutzt eigene Datenbanktabellen mit dem Präfix `wp_forum_*` (bzw. deinem konfigurierten Tabellenpräfix).
---
## 4) Installation
1. Plugin-Ordner `wp-business-forum` in `wp-content/plugins/` kopieren.
2. Im WordPress-Backend unter **Plugins** aktivieren.
3. Nach der Aktivierung startet einmalig der Setup-Wizard.
---
## 5) Ersteinrichtung (Setup-Wizard)
Nach der Aktivierung führt der Wizard durch drei Schritte:
1. Superadmin-Konto erstellen oder bestehendes Forum-Konto verknüpfen
2. Optional automatisch eine Forum-Seite erzeugen
3. Abschluss und Weiterleitung ins Dashboard
**Wichtig:**
- Der Superadmin ist fest mit dem WordPress-Administrator verknüpft und kann nicht über den Import überschrieben werden.
- Wenn noch kein Superadmin existiert, erscheint im Backend ein Hinweisbanner.
---
## 6) Forum-Seite einbinden
Das Forum wird mit folgendem Shortcode auf einer WordPress-Seite angezeigt:
```
[business_forum]
```
**Empfehlung:**
- Eine eigene Seite (z. B. „Forum") anlegen
- Nur diesen Shortcode als Seiteninhalt verwenden
- Die Seite in der WordPress-Navigation verlinken
---
## 7) Bedienung im Frontend (Mitglieder)
### 7.1 Registrierung und Login
- Die Registrierung kann offen, nur per Einladung oder deaktiviert sein.
- Optional müssen Nutzer die Forum-Regeln akzeptieren.
- Spam-Schutz bei der Registrierung:
- Honeypot-Feld
- Mindestzeit bis zum Formular-Absenden
- Login unterstützt „Angemeldet bleiben" (Remember-Me Cookie, 30 Tage).
### 7.2 Kategorien und Threads
- Kategorien können verschachtelt sein (Hauptkategorie + Unterkategorien).
- Die Sichtbarkeit kann rollenbasiert eingeschränkt werden.
- Threads können folgende Zustände haben: offen · geschlossen · archiviert · gepinnt
### 7.3 Thread erstellen
- Mindestlänge Titel: 5 Zeichen
- Mindestlänge Inhalt: 10 Zeichen
- Tags können vergeben werden
- Optional kann ein Thread-Präfix gesetzt werden
- Optional kann direkt eine Umfrage erstellt werden
### 7.4 Antworten und Bearbeiten
- Antworten mit BBCode-Unterstützung (`[b]`, `[i]`, `[quote]`, `[code]`, `[spoiler]`, `[url]`, `[img]` u. v. m.)
- Flood-Control: konfigurierbare Wartezeit zwischen Posts
- Eigene Posts können nur innerhalb des konfigurierten Bearbeitungsfensters geändert werden
- Moderation kann unabhängig davon jederzeit eingreifen
### 7.5 Umfragen
- Umfrage direkt beim Thread-Erstellen oder nachträglich anfügen
- 2 bis 10 Antwortoptionen
- Optional Mehrfachauswahl
- Optional Enddatum
- Nach der Abstimmung werden Ergebnisse direkt angezeigt
### 7.6 Reaktionen, Likes, Lesezeichen
- Likes auf Threads und Beiträge
- Emoji-Reaktionen (adminseitig konfigurierbar)
- Lesezeichen für Threads, im Profil jederzeit einsehbar
### 7.7 Private Nachrichten (DM)
- 1:1 Nachrichten zwischen Mitgliedern
- Inbox-Ansicht und Konversationsansicht
- Ungelesene Nachrichten werden im Header gezählt
- Optional E-Mail-Hinweis bei neuer Nachricht
### 7.8 Benachrichtigungen
Benachrichtigungen werden ausgelöst bei:
- Antworten auf abonnierte Threads
- @Erwähnungen in Beiträgen
- Neuen privaten Nachrichten
### 7.9 Profil
Mitglieder können:
- Anzeigenamen, Bio und Signatur pflegen
- Avatar hochladen (max. 2 MB, JPG/PNG/GIF/WebP)
- Passwort ändern
- Profil-Sichtbarkeit umschalten
- Benutzerdefinierte Profilfelder ausfüllen (falls vom Admin aktiviert)
- Andere Nutzer zur Ignore-Liste hinzufügen
Upload-Limits:
- Avatar: max. 2 MB (JPG / PNG / GIF / WebP)
- Bild im Beitrag: max. 5 MB (JPG / PNG / GIF / WebP)
### 7.10 Passwort vergessen
Über „Passwort vergessen" kann ein Reset-Link per E-Mail angefordert werden. Das Zurücksetzen erfolgt über einen zeitlich begrenzten Token.
---
## 8) Moderation und Verwaltung
Im WordPress-Backend gibt es den Menüpunkt **Business Forum** mit folgenden Unterseiten:
| Unterseite | Funktion |
|---|---|
| Übersicht | Dashboard mit Kennzahlen, Trends und Aktivitätsprotokoll |
| Kategorien | Struktur, Hierarchie und Sichtbarkeit verwalten |
| Rollen | Rollen, Permissions und Design anpassen |
| Level | Beitragsbasierte Rangstufen konfigurieren |
| Mitglieder | Nutzer verwalten, sperren, löschen |
| Meldungen | Gemeldete Inhalte bearbeiten |
| Profilfelder | Eigene Felder definieren |
| Einstellungen | Texte, Sicherheit, Registrierung, Regeln, Wartung |
| Reaktionen | Erlaubte Emoji-Reaktionen konfigurieren |
| Einladungen | Invite-Codes erstellen und verwalten |
| Statistiken | Forum-Auswertung und Trends |
| Papierkorb | Gelöschte Inhalte einsehen und wiederherstellen |
| Thread-Präfixe | Farbige Label für Threads verwalten |
| Wortfilter | Unerwünschte Begriffe automatisch ersetzen |
| Export / Import | Vollständiges Backup und Wiederherstellung |
| ⚠️ Deinstallieren | Komplette Löschung inkl. aller Daten |
| 🔔 Updates | Update-Status und Changelog |
### 8.1 Mitglieder verwalten
In der Mitglieder-Übersicht stehen pro Nutzer drei Aktionen zur Verfügung:
**Rolle ändern**
Rolle direkt aus dem Dropdown wählen und speichern. Bei „Gesperrt" kann zusätzlich ein Sperrgrund und ein automatisches Ablaufdatum (temporäre Sperre) gesetzt werden.
**Profil bearbeiten**
Anzeigename, E-Mail, Passwort, Bio, Signatur und benutzerdefinierte Profilfelder direkt im Admin ändern.
**Nutzer löschen**
Beim Klick auf „Löschen" öffnet sich ein Bestätigungs-Panel mit zwei Optionen:
- **DSGVO Anonymisieren** *(empfohlen)*: Der Account wird nach Art. 17 DSGVO anonymisiert — Benutzername, E-Mail und Passwort werden gelöscht, Threads und Beiträge bleiben unter „Gelöschter Nutzer" erhalten.
- **Dauerhaft löschen**: Der Datensatz wird vollständig aus der Datenbank entfernt. Alle nutzerbezogenen Daten (Nachrichten, Likes, Reaktionen, Abonnements, Lesezeichen u. a.) werden gelöscht. Threads und Beiträge bleiben anonym erhalten. **Dieser Vorgang ist nicht rückgängig zu machen.**
> Der Superadmin-Account ist in beiden Pfaden geschützt und kann nicht gelöscht werden.
### 8.2 Sperren von Nutzern
Statt eines vollständigen Löschens kann ein Nutzer auch gesperrt werden (Rolle „Gesperrt"):
- **Permanente Sperre**: Kein Forum-Zugang, Sperrgrund wird beim Login angezeigt.
- **Temporäre Sperre**: Automatische Entsperrung zum angegebenen Datum/Uhrzeit. Bei Ablauf wird die vorherige Rolle automatisch wiederhergestellt.
---
## 9) Einstellungen im Detail
Unter **Business Forum Einstellungen**:
### 9.1 Texte und UI
- Hero-Titel und Untertitel
- Topbar-Brand
- Labels für Statistiken
- Abschnittstitel und Buttontexte
- Sidebar-Titel
### 9.2 Sicherheit
- Auto-Logout nach Inaktivität (0 = deaktiviert, in Minuten)
- Post-Bearbeitungslimit (in Minuten, 0 = unbegrenzt)
- Spam-Mindestzeit bei Registrierung (in Sekunden)
- Flood-Control Intervall zwischen Posts (in Sekunden, 0 = deaktiviert)
- Standard-Profil-Sichtbarkeit für neue Mitglieder
### 9.3 Registrierung
- Modus: **offen** · **nur Einladung** · **deaktiviert**
- Freitext-Hinweis bei Einladungs-Modus
- Forum-Regeln bei Registrierung verpflichtend akzeptieren
### 9.4 Wartungsmodus
- Forum für normale Nutzer sperren
- Moderation und Admins behalten vollen Zugriff
- Eigener Wartungs-Titel und Hinweistext konfigurierbar
### 9.5 Forum-Regeln / Nutzungsbedingungen
- Regelseite aktivieren / deaktivieren
- Akzeptierung bei Registrierung optional verpflichtend
- Titel und Inhalt frei editierbar (unterstützt einfaches Markdown)
---
## 10) Export, Import und Deinstallation
### 10.1 Export
Unter **Business Forum Export / Import** kannst du einzelne oder alle Bereiche als `.json`-Datei exportieren:
| Bereich | Enthält |
|---|---|
| Einstellungen & Wortfilter | Forum-Texte, Regeln, Labels, Auto-Logout, Wortfilter, Profilfeld-Definitionen, Reaktionen-Konfiguration |
| Rollen | Alle Rollen mit Berechtigungen und Design (Superadmin wird nie überschrieben) |
| Level-System | Level-Namen, Schwellenwerte, Icons, Farben, An/Aus-Status |
| Kategorien | Kategoriestruktur inkl. Eltern-Kind-Hierarchie, Icons, Min-Rolle |
| Benutzer & Profilfelder | Accounts inkl. Passwort-Hashes, Ban-Status, Profilfeld-Werte |
| Threads, Posts & Abonnements | Alle Inhalte inkl. Tag-Zuordnungen und Thread-Abonnements |
| Umfragen | Alle Umfragen inkl. Abstimmungen |
| Lesezeichen | Alle gespeicherten Thread-Lesezeichen |
| Thread-Präfixe | Alle Präfix-Labels, Farben und Reihenfolgen |
| Likes & Reaktionen | Likes, Emoji-Reaktionen, Benachrichtigungen |
| Privatnachrichten | Alle DM-Konversationen |
| Ignore-Liste | Alle gegenseitigen Nutzer-Blockierungen |
| Meldungen | Gemeldete Beiträge inkl. Status |
| Einladungen | Alle Einladungscodes inkl. Nutzungsanzahl und Ablaufdatum |
**Tipp:** Mit „Alle wählen" / „Keine" lässt sich die Auswahl schnell anpassen. Die Datei wird sofort heruntergeladen.
### 10.2 Import
Beim Import einer zuvor exportierten `.json`-Datei gilt:
- Maximale Dateigröße: **50 MB**
- Nur Dateien im WBF-Format werden akzeptiert
- **Benutzer-IDs werden beim Import automatisch gemappt** — Threads, Posts, Likes, Reaktionen und alle anderen nutzerbezogenen Daten werden korrekt auf die neuen Datenbank-IDs übertragen, auch wenn sich diese von der Quelldatenbank unterscheiden
- Nach dem Import werden alle Zähler (Beitrags-, Thread- und Reaktionszähler) automatisch neu berechnet
- Der Superadmin kann per Import nie überschrieben werden
Über die **Überschreiben-Optionen** lässt sich pro Bereich steuern, ob bestehende Daten ersetzt oder Duplikate übersprungen werden sollen.
> ⚠️ Erstelle vor jedem Import einen aktuellen Export als Sicherung. Benutzer-Exporte enthalten Passwort-Hashes — teile diese Dateien nicht öffentlich.
### 10.3 Deinstallation
Unter **Business Forum ⚠️ Deinstallieren** oder beim Löschen des Plugins im WordPress-Backend werden vollständig entfernt:
- Alle Forum-Datenbanktabellen (`wp_forum_*`)
- Alle Plugin-Optionen in `wp_options`
- Transients
- Geplante Cron-Jobs
- Automatisch erstellte Forum-Seite
- Upload-Unterverzeichnis `wbf-avatars`
> **Das ist eine echte, unwiderrufliche Datenlöschung. Immer vorher einen vollständigen Export erstellen.**
---
## 11) FAQ / Troubleshooting
**Login funktioniert nicht**
Prüfen ob das Konto gesperrt ist. Bei temporärer Sperre das Ablaufdatum abwarten. Bei „Nur Einladung" einen gültigen Invite-Code verwenden.
**Registrierung nicht sichtbar**
In den Einstellungen den Registrierungsmodus prüfen. Bei deaktiviertem Modus ist keine Selbstregistrierung möglich.
**Keine E-Mails kommen an**
WordPress-Mailversand prüfen. Ein SMTP-Plugin wird empfohlen. Die Admin-E-Mail in WordPress kontrollieren.
**Upload von Bildern / Avatar scheitert**
Dateityp prüfen (nur JPG/PNG/GIF/WebP). Dateigröße prüfen (Avatar 2 MB, Beitrag 5 MB). Schreibrechte im Uploads-Verzeichnis prüfen.
**Import schlägt fehl oder überschreibt falsche Daten**
Sicherstellen, dass die Datei aus einer WBF-Installation stammt. Überschreiben-Optionen gezielt setzen. Bei sehr großen Backups `upload_max_filesize` und `post_max_size` in der `php.ini` erhöhen.
**Benutzer werden automatisch ausgeloggt**
Auto-Logout in den Forum-Einstellungen prüfen (Wert in Minuten, 0 = deaktiviert).
**Forum ist plötzlich „offline"**
Wartungsmodus in den Einstellungen deaktivieren.
**Suche liefert keine Ergebnisse**
Der Suchbegriff muss mindestens 2 Zeichen lang sein.
**Nach dem Import stimmen Beitragszähler nicht**
Ab Version 1.0.2 werden Zähler nach jedem Import automatisch neu berechnet. Bei älteren Imports einmalig einen neuen Import mit der aktuellen Version durchführen.
---
## Kurz-Checkliste für den Live-Betrieb
1. Setup-Wizard abschließen
2. Forum-Seite mit `[business_forum]` bereitstellen
3. Rollen und Kategorien final konfigurieren
4. Registrierungsmodus festlegen
5. Regeln / Nutzungsbedingungen hinterlegen
6. E-Mail-Versand testen
7. Vollständigen Backup-Export erstellen
Viel Erfolg mit deinem Forum!
# WP Business Forum - Anwender README
WP Business Forum bringt ein modernes, eigenständiges Community-Forum direkt in deine WordPress-Website.
Statt auf externe Plattformen auszuweichen, bleiben Diskussionen, Support-Anfragen und Mitgliederaktivität
zentral auf deiner eigenen Seite - inklusive voller Kontrolle über Inhalte, Rollen und Moderation.
Diese Dokumentation richtet sich an Betreiber, Moderatoren und Community-Manager, die das Forum
schnell einrichten, sicher betreiben und im Alltag effizient verwalten möchten. Von der ersten
Installation bis zum Live-Betrieb findest du hier alle wichtigen Schritte und Funktionen kompakt erklärt.
Wenn du eine professionelle Community mit klaren Rechten, direkter Nutzerkommunikation und
strukturierter Moderation aufbauen willst, ist WP Business Forum dafür ausgelegt.
## Inhalt
1. Über das Plugin
2. Funktionsübersicht
3. Voraussetzungen
4. Installation
5. Ersteinrichtung (Setup-Wizard)
6. Forum-Seite einbinden
7. Bedienung im Frontend (Mitglieder)
8. Moderation und Verwaltung
9. Einstellungen im Detail
10. Export, Import und Deinstallation
11. FAQ / Troubleshooting
## 1) Über das Plugin
WP Business Forum ist ein eigenständiges Foren-System für WordPress mit:
- eigenem Forum-Login (unabhängig vom WP-Login)
- Rollen- und Rechteverwaltung
- Kategorien mit Hierarchie
- Moderationswerkzeugen
- Direktnachrichten, Benachrichtigungen, Meldesystem
- Umfragen, Tags, Reaktionen, Lesezeichen
Das Forum wird per Shortcode in eine WordPress-Seite eingebunden.
## 2) Funktionsübersicht
### Für Mitglieder
- Registrieren / Einloggen / Logout
- Passwort vergessen und Reset per E-Mail
- Threads erstellen, antworten, bearbeiten
- Likes und Emoji-Reaktionen
- Tags und Thread-Präfixe
- Umfragen erstellen und abstimmen
- Lesezeichen setzen
- Nutzer erwähnen mit @mention
- Private Nachrichten (DM)
- Profil mit Avatar, Bio, Signatur und eigenen Profilfeldern
- Mitgliederliste und Suchfunktion
### Für Moderation / Admin
- Threads pinnen, schließen, archivieren, verschieben, löschen
- Beiträge löschen
- Meldungen (Reports) bearbeiten
- Kategorien und Rollen verwalten
- Einladungssystem für Registrierung
- Wartungsmodus
- Wortfilter
- Statistiken
- Papierkorb / Wiederherstellung
- Export / Import
## 3) Voraussetzungen
- Laufende WordPress-Installation
- Schreibrechte für WordPress-Uploads (für Avatar-/Bild-Uploads)
- Funktionierende E-Mail-Zustellung in WordPress (für Passwort-Reset und Benachrichtigungen)
Hinweis: Das Plugin nutzt eigene Datenbanktabellen (Präfix `wp_forum_*` bzw. mit deinem Tabellenpräfix).
## 4) Installation
1. Plugin-Ordner `wp-business-forum` in `wp-content/plugins/` kopieren.
2. Im WordPress-Backend unter Plugins aktivieren.
3. Nach der Aktivierung startet einmalig der Setup-Wizard.
## 5) Ersteinrichtung (Setup-Wizard)
Nach Aktivierung führt der Wizard durch 3 Schritte:
1. Superadmin-Konto erstellen oder bestehendes Forum-Konto hochstufen
2. Optional automatisch eine Forum-Seite erzeugen
3. Abschluss
Wichtig:
- Der Superadmin ist fest mit dem WordPress-Admin verknüpft.
- Wenn noch kein Superadmin existiert, erscheint im Backend ein Hinweisbanner.
## 6) Forum-Seite einbinden
Das Forum wird mit folgendem Shortcode auf einer WordPress-Seite angezeigt:
```text
[business_forum]
```
Empfohlen:
- Eine eigene Seite (z. B. "Forum") anlegen
- Nur diesen Shortcode als Seiteninhalt verwenden
## 7) Bedienung im Frontend (Mitglieder)
### 7.1 Registrierung und Login
- Registrierung kann offen, nur per Einladung oder deaktiviert sein.
- Optional müssen Nutzer die Forum-Regeln akzeptieren.
- Spam-Schutz bei Registrierung:
- Honeypot-Feld
- Mindestzeit bis Formular-Absenden
- Login unterstützt "Angemeldet bleiben" (Remember-Me Cookie).
### 7.2 Kategorien und Threads
- Kategorien können verschachtelt sein (Hauptkategorie + Unterkategorien).
- Sichtbarkeit kann rollenbasiert sein.
- Threads können folgende Zustände haben:
- offen
- geschlossen
- archiviert
- gepinnt
### 7.3 Thread erstellen
- Mindestlänge Titel: 5 Zeichen
- Mindestlänge Inhalt: 10 Zeichen (bei normalem Thread)
- Tags können vergeben werden
- Optional kann ein Thread-Präfix gesetzt werden
- Optional kann direkt eine Umfrage erstellt werden
### 7.4 Antworten und Bearbeiten
- Antworten mit BBCode-Unterstützung
- Flood-Control: konfigurierbare Wartezeit zwischen Posts
- Eigene Posts nur innerhalb des eingestellten Bearbeitungsfensters (z. B. 30 Minuten)
- Moderation kann unabhängig davon eingreifen
### 7.5 Umfragen
- Umfrage direkt beim Thread-Erstellen oder nachträglich im Thread
- 2 bis 10 Antwortoptionen
- Optional Mehrfachauswahl
- Optional Enddatum
- Nach Abstimmung werden Ergebnisse direkt angezeigt
### 7.6 Reaktionen, Likes, Lesezeichen
- Likes auf Thread/Beitrag
- Emoji-Reaktionen (adminseitig konfigurierbar)
- Lesezeichen für Threads (im Profil einsehbar)
### 7.7 Private Nachrichten (DM)
- 1:1 Nachrichten zwischen Mitgliedern
- Inbox-Ansicht und Konversation
- Ungelesene Nachrichten werden gezählt
- Optional E-Mail-Hinweis bei neuer Nachricht
### 7.8 Benachrichtigungen
Benachrichtigungen bei:
- Antworten auf abonnierte / relevante Threads
- @Erwähnungen
- neuen privaten Nachrichten
### 7.9 Profil
Mitglieder können:
- Anzeigenamen, Bio und Signatur pflegen
- Avatar hochladen
- Passwort ändern
- eigene Profil-Sichtbarkeit umschalten
- benutzerdefinierte Profilfelder ausfüllen (falls aktiviert)
Upload-Limits:
- Avatar: max. 2 MB (JPG/PNG/GIF/WebP)
- Bild im Beitrag: max. 5 MB (JPG/PNG/GIF/WebP)
### 7.10 Passwort vergessen
- Über "Passwort vergessen" kann ein Reset-Link per E-Mail angefordert werden.
- Das Zurücksetzen erfolgt über einen zeitlich gültigen Token.
## 8) Moderation und Verwaltung
Im WordPress-Backend gibt es den Menüpunkt "Business Forum" mit Unterseiten:
- Übersicht: Dashboard mit Kennzahlen und Aktivitäten
- Kategorien: Struktur und Sichtbarkeit verwalten
- Rollen: Rollen/Permissions anpassen
- Level: Beitragsbasierte Rangstufen
- Mitglieder: Nutzer verwalten
- Meldungen: gemeldete Inhalte bearbeiten
- Profilfelder: eigene Felder definieren
- Einstellungen: Texte, Sicherheit, Registrierung, Regeln, Wartung
- Reaktionen: erlaubte Emoji-Reaktionen
- Einladungen: Invite-Codes erstellen und verwalten
- Statistiken: Forum-Auswertung
- Papierkorb: gelöschte Inhalte wiederherstellen
- Thread-Präfixe: Label für Threads verwalten
- Wortfilter: unerwünschte Begriffe ersetzen/filtern
- Export / Import: Backup und Wiederherstellung
- Deinstallieren: komplette Löschung des Plugins inkl. Daten
## 9) Einstellungen im Detail
Unter Business Forum > Einstellungen:
### 9.1 Texte und UI
- Hero-Titel/Untertitel
- Topbar-Brand
- Label für Statistik
- Abschnittstitel
- Buttontexte
- Sidebar-Titel
### 9.2 Sicherheit
- Auto-Logout nach Inaktivität (0 = deaktiviert)
- Post-Bearbeitungslimit
- Spam-Mindestzeit bei Registrierung
- Flood-Control Intervall
- Profil-Sichtbarkeit (Standard)
### 9.3 Registrierung
- Modus:
- offen
- nur Einladung
- deaktiviert
- Freitext-Hinweis für Einladungsmode
### 9.4 Wartungsmodus
- Forum für normale Nutzer sperren
- Moderation/Admin behalten Zugriff
- Eigener Wartungs-Titel und Hinweistext
### 9.5 Forum-Regeln / Nutzungsbedingungen
- Regelseite aktivieren/deaktivieren
- Akzeptierung bei Registrierung optional verpflichtend
- Titel und Inhalt frei editierbar
## 10) Export, Import und Deinstallation
### 10.1 Export / Import
Exportierbare Bereiche (je nach Auswahl):
- Einstellungen
- Rollen und Level
- Kategorien
- Nutzer und User-Meta
- Threads und Posts
- Interaktionen (Likes/Reaktionen/Benachrichtigungen)
- Nachrichten
- Meldungen
- Einladungen
Empfehlung:
- Vor großen Änderungen immer einen Voll-Export speichern.
### 10.2 Deinstallation (wichtig)
Beim Löschen des Plugins werden komplett entfernt:
- alle Forum-Datenbanktabellen
- relevante Plugin-Optionen
- Transients
- geplanter Cron-Job
- automatisch erstellte Forum-Seite
- zugehörige Upload-Unterverzeichnisse
Das ist eine echte Datenlöschung. Vorher immer Backup erstellen.
## 11) FAQ / Troubleshooting
### Login funktioniert nicht
- Prüfen, ob das Konto gesperrt ist
- Bei zeitlicher Sperre Ablaufzeit abwarten
- Bei Registrierung "Nur Einladung" gültigen Invite-Code nutzen
### Registrierung nicht sichtbar
- In Einstellungen den Registrierungsmodus prüfen
- Bei deaktiviertem Modus ist keine Selbstregistrierung möglich
### Keine E-Mails kommen an
- WordPress-Mailversand prüfen (SMTP Plugin empfohlen)
- Admin-E-Mail in WordPress kontrollieren
### Upload von Bildern/Avatar scheitert
- Dateityp prüfen (nur JPG/PNG/GIF/WebP)
- Dateigröße prüfen (Avatar 2 MB, Beitrag 5 MB)
- Schreibrechte in Uploads prüfen
### Benutzer werden automatisch ausgeloggt
- Auto-Logout in den Forum-Einstellungen prüfen
### Forum ist plötzlich "offline"
- Wartungsmodus in den Einstellungen deaktivieren
### Suche liefert keine Ergebnisse
- Suchbegriff muss mindestens 2 Zeichen haben
---
## Kurz-Checkliste für den Live-Betrieb
1. Setup-Wizard abschließen
2. Forum-Seite mit `[business_forum]` bereitstellen
3. Rollen und Kategorien final konfigurieren
4. Registrierungsmodus festlegen
5. Regeln/Nutzungsbedingungen hinterlegen
6. E-Mail-Versand testen
7. Backup-Export erstellen
Viel Erfolg mit deinem Forum!

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
<?php
/**
* MC Bridge — Einstellungs-Sektion für den Forum-Admin
*
* Diesen Block in forum-settings.php im Admin-Formular einfügen,
* z.B. nach der Discord-Sektion.
*/
if ( ! defined( 'ABSPATH' ) ) exit;
?>
<!-- ═══ Minecraft Bridge ═══ -->
<div class="wbf-settings-section">
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="font-size:1.3em">⛏️</span> Minecraft Bridge
</h3>
<p class="description" style="margin-bottom:16px;color:#9ca3af">
Verbindet das Forum mit deinem Minecraft-Server (BungeeCord StatusAPI Plugin).
Spieler können ihren Forum-Account verknüpfen und erhalten Ingame-Benachrichtigungen
bei neuen Antworten, Erwähnungen und Privatnachrichten.
</p>
<table class="form-table">
<tr>
<th>Aktiviert</th>
<td>
<label>
<input type="checkbox" name="wbf_settings[mc_bridge_enabled]" value="1"
<?php checked( ! empty( $s['mc_bridge_enabled'] ) ); ?>>
MC Bridge aktivieren
</label>
</td>
</tr>
<tr>
<th>StatusAPI URL</th>
<td>
<input type="url" name="wbf_settings[mc_bridge_api_url]"
value="<?php echo esc_attr( $s['mc_bridge_api_url'] ?? '' ); ?>"
class="regular-text"
placeholder="http://192.168.1.100:9191">
<p class="description">
Die URL deines BungeeCord StatusAPI Servers (IP + Port).
Beispiel: <code>http://dein-server:9191</code>
</p>
</td>
</tr>
<tr>
<th>API Secret</th>
<td>
<input type="password" name="wbf_settings[mc_bridge_api_secret]"
value="<?php echo esc_attr( $s['mc_bridge_api_secret'] ?? '' ); ?>"
class="regular-text"
autocomplete="new-password">
<p class="description">
Gemeinsames Passwort für die API-Kommunikation.
Muss identisch sein mit <code>forum.api_secret</code> in der
<code>verify.properties</code> des StatusAPI Plugins.
</p>
</td>
</tr>
<tr>
<th>Verbindungstest</th>
<td>
<button type="button" id="wbf-mc-test-btn" class="button"
onclick="wbfTestMcConnection()">
🔌 Verbindung testen
</button>
<span id="wbf-mc-test-result" style="margin-left:10px"></span>
<script>
function wbfTestMcConnection() {
var btn = document.getElementById('wbf-mc-test-btn');
var result = document.getElementById('wbf-mc-test-result');
var url = document.querySelector('input[name="wbf_settings[mc_bridge_api_url]"]').value;
if (!url) { result.textContent = '❌ Bitte erst eine URL eingeben.'; return; }
btn.disabled = true;
result.textContent = '⏳ Teste...';
fetch(url.replace(/\/$/, '') + '/forum/status')
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) result.innerHTML = '✅ <strong>Verbunden!</strong> StatusAPI v' + (d.version || '?');
else result.textContent = '⚠️ Erreichbar aber Fehler: ' + JSON.stringify(d);
})
.catch(function(e) { result.textContent = '❌ Nicht erreichbar: ' + e.message; })
.finally(function() { btn.disabled = false; });
}
</script>
</td>
</tr>
</table>
</div>

View File

@@ -54,12 +54,24 @@ if ( ! function_exists('wbf_get_settings') ) {
'rules_content' => "**1. Respektvoller Umgang**\nBehandle alle Mitglieder freundlich und respektvoll. Beleidigungen, Mobbing und Diskriminierung sind nicht toleriert.\n\n**2. Keine Spam-Inhalte**\nWerbung, Spam und irrelevante Links sind verboten.\n\n**3. Keine illegalen Inhalte**\nJegliche Inhalte, die gegen geltendes Recht verstoßen, sind streng verboten.\n\n**4. Themenrelevanz**\nBeiträge sollten zur jeweiligen Kategorie passen.\n\n**5. Urheberrecht**\nVeröffentliche keine Inhalte, an denen du keine Rechte besitzt.\n\n**6. Datenschutz**\nTeile keine persönlichen Daten anderer Personen ohne deren Zustimmung.\n\n**7. Moderations-Entscheidungen**\nEntscheidungen der Moderatoren sind zu respektieren. Bei Fragen wende dich direkt ans Team.\n\nVerstöße können zur Verwarnung oder dauerhaften Sperrung führen.",
// Ignore/Block-System: Rollen die nicht geblockt werden können (kommagetrennte Schlüssel)
'ignore_blocked_roles' => 'superadmin,admin,moderator',
// Discord-Integration
'discord_bot_token' => '',
'discord_guild_id' => '',
'discord_client_id' => '',
'discord_client_secret' => '',
'discord_role_sync' => '0', // Rollen-Sync aktiviert?
'discord_role_map' => '', // JSON: {"discord_role_id":"forum_role_key"}
// Minecraft Bridge
'mc_bridge_enabled' => '0',
'mc_bridge_api_url' => '',
'mc_bridge_api_secret' => '',
];
$saved = get_option( 'wbf_settings', [] );
// Fehlende Keys mit Defaults auffüllen, leere Strings ignorieren
return array_merge( $defaults, array_filter( (array) $saved, 'strlen' ) );
// Keine Filterung mehr, damit auch bewusst geleerte Felder gespeichert werden
return array_merge( $defaults, (array) $saved );
}
}
@@ -130,6 +142,38 @@ function wbf_admin_settings() {
// rules_content separat (nicht in $fields, da textarea mit eigener Behandlung)
$settings['rules_content'] = sanitize_textarea_field( $_POST['rules_content'] ?? '' );
// Discord-Einstellungen gesondert speichern (sensitiv — niemals in wbf_settings öffentlich)
$discord_fields = ['discord_bot_token', 'discord_guild_id', 'discord_client_id', 'discord_client_secret'];
foreach ( $discord_fields as $df ) {
$settings[$df] = sanitize_text_field( $_POST[$df] ?? '' );
}
$settings['discord_role_sync'] = isset($_POST['discord_role_sync']) && $_POST['discord_role_sync'] === '1' ? '1' : '0';
// Discord-Rollen-Map: Array von discord_role_id => forum_role_key
$role_map = [];
$dc_ids = array_map('sanitize_text_field', (array)($_POST['discord_role_id'] ?? []));
$fr_keys = array_map('sanitize_key', (array)($_POST['discord_forum_role'] ?? []));
$valid_roles = array_keys(WBF_Roles::get_all());
foreach ( $dc_ids as $i => $dc_id ) {
$dc_id = trim($dc_id);
$fr_key = $fr_keys[$i] ?? '';
if ( $dc_id !== '' && in_array($fr_key, $valid_roles, true) ) {
$role_map[$dc_id] = $fr_key;
}
}
$settings['discord_role_map'] = json_encode($role_map);
// ── Minecraft Bridge ──────────────────────────────────────────────────
$settings['mc_bridge_api_url'] = esc_url_raw( trim( $_POST['mc_bridge_api_url'] ?? '' ) );
$settings['mc_bridge_api_secret'] = sanitize_text_field( $_POST['mc_bridge_api_secret'] ?? '' );
// Checkbox-Felder explizit als '0' speichern wenn nicht angehakt,
// damit array_filter(...,'strlen') sie nicht wegwirft und der Default '1' greift.
$checkbox_fields = ['maintenance_mode', 'rules_enabled', 'rules_accept_required', 'mc_bridge_enabled'];
foreach ( $checkbox_fields as $cb ) {
$settings[$cb] = isset($_POST[$cb]) && $_POST[$cb] === '1' ? '1' : '0';
}
// ignore_blocked_roles: kommagetrennte Liste der gewählten Rollen-Keys
$all_role_keys = array_keys( WBF_Roles::get_all() );
$checked_roles = array_intersect(
@@ -143,6 +187,12 @@ function wbf_admin_settings() {
$settings['ignore_blocked_roles'] = implode( ',', $checked_roles );
update_option( 'wbf_settings', $settings );
// Superadmin WP-User-ID separat speichern (außerhalb von wbf_settings)
$sa_wp_id = (int) ( $_POST['superadmin_wp_id'] ?? 1 );
if ( $sa_wp_id < 1 ) $sa_wp_id = 1;
update_option( 'wbf_superadmin_wp_id', $sa_wp_id );
echo '<div class="notice notice-success is-dismissible"><p>✅ Einstellungen gespeichert!</p></div>';
}
@@ -244,6 +294,40 @@ function wbf_admin_settings() {
🔒 Sicherheit
</h2>
<table class="form-table" role="presentation">
<!-- ── Superadmin WP-User-ID ─────────────────── -->
<tr>
<th scope="row">
<label for="wbf_superadmin_wp_id">Superadmin WordPress-User-ID</label>
</th>
<td>
<?php
$sa_id = (int) get_option( 'wbf_superadmin_wp_id', 1 );
$sa_wpuser = get_userdata( $sa_id );
?>
<input type="number" id="wbf_superadmin_wp_id" name="superadmin_wp_id"
value="<?php echo $sa_id; ?>"
min="1" step="1"
style="width:80px">
<?php if ( $sa_wpuser ) : ?>
<span style="margin-left:10px;color:#16a34a;font-weight:600">
✅ <?php echo esc_html( $sa_wpuser->display_name ); ?>
&lt;<?php echo esc_html( $sa_wpuser->user_email ); ?>&gt;
</span>
<?php else : ?>
<span style="margin-left:10px;color:#dc2626;font-weight:600">
⚠️ Kein WordPress-User mit dieser ID gefunden!
</span>
<?php endif; ?>
<p class="description">
Nur dieser WordPress-User erhält automatisch die Forum-Rolle <strong>Superadmin</strong>
und kann sie nicht verlieren. Alle anderen WordPress-Admins können normale Forum-Rollen
haben und im Mitglieder-Bereich frei zugewiesen werden.<br>
<em>Standard: 1 (erster bei der WP-Installation angelegter User)</em>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="wbf_auto_logout_minutes">Auto-Logout nach Inaktivität</label>
@@ -492,6 +576,280 @@ function wbf_admin_settings() {
</tr>
</table>
<!-- ══════════════════════════════════════════════════════════
DISCORD-INTEGRATION
══════════════════════════════════════════════════════════ -->
<h2 style="border-bottom:1px solid #ddd;padding-bottom:.4rem;margin-top:2rem">
<span style="color:#5865f2">🎮</span> Discord-Integration
</h2>
<p class="description" style="margin-bottom:1rem">
Bot-Token und Guild-ID findest du im <a href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</a>.
Der Bot muss Mitglied deines Servers sein und die Berechtigung <strong>Direct Messages lesen/senden</strong> sowie
<strong>Server-Mitglieder verwalten</strong> besitzen.
</p>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="wbf_discord_bot_token">Bot-Token</label></th>
<td>
<input type="password" id="wbf_discord_bot_token" name="discord_bot_token"
value="<?php echo esc_attr($s['discord_bot_token']); ?>"
class="regular-text" autocomplete="off" placeholder="Bot-Token aus dem Developer Portal">
<p class="description">Niemals öffentlich teilen! Wird verschlüsselt in der Datenbank gespeichert.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_discord_guild_id">Server-ID (Guild ID)</label></th>
<td>
<input type="text" id="wbf_discord_guild_id" name="discord_guild_id"
value="<?php echo esc_attr($s['discord_guild_id']); ?>"
class="regular-text" placeholder="z. B. 123456789012345678">
<p class="description">Rechtsklick auf deinen Server → ID kopieren (Entwicklermodus muss aktiv sein).</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_discord_client_id">Client ID (optional)</label></th>
<td>
<input type="text" id="wbf_discord_client_id" name="discord_client_id"
value="<?php echo esc_attr($s['discord_client_id']); ?>"
class="regular-text" placeholder="Application ID">
<p class="description">Für zukünftige OAuth2-Unterstützung. Aktuell optional.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_discord_client_secret">Client Secret (optional)</label></th>
<td>
<input type="password" id="wbf_discord_client_secret" name="discord_client_secret"
value="<?php echo esc_attr($s['discord_client_secret']); ?>"
class="regular-text" autocomplete="off" placeholder="Client Secret">
</td>
</tr>
<tr>
<th scope="row">Rollen-Sync aktivieren</th>
<td>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" name="discord_role_sync" value="1"
<?php checked('1', $s['discord_role_sync'] ?? '0'); ?>>
Discord-Serverrollen automatisch auf Forum-Rollen mappen
</label>
<p class="description">
Wenn aktiviert, wird bei jedem Login und stündlich per Cron die Discord-Rolle des Nutzers
geprüft und die Forum-Rolle entsprechend der unten definierten Zuordnung aktualisiert.
</p>
</td>
</tr>
</table>
<!-- Discord Rollen-Map -->
<h3 style="margin-top:1.5rem">🔗 Discord-Rollen → Forum-Rollen Zuordnung</h3>
<p class="description" style="margin-bottom:.75rem">
Trage die Discord-Rollen-ID und die gewünschte Forum-Rolle ein.
Mehrere Einträge werden der Reihe nach geprüft — der erste Treffer gewinnt.
</p>
<?php
$role_map_raw = $s['discord_role_map'] ?? '{}';
$role_map = json_decode($role_map_raw, true) ?: [];
$forum_roles = WBF_Roles::get_sorted();
// Sicherstellen dass mindestens eine leere Zeile zum Hinzufügen da ist
if ( empty($role_map) ) $role_map[''] = '';
?>
<table class="widefat" id="wbf-discord-role-map" style="max-width:680px;margin-bottom:.75rem">
<thead><tr>
<th style="width:50%">Discord Rollen-ID</th>
<th style="width:40%">Forum-Rolle</th>
<th style="width:10%"></th>
</tr></thead>
<tbody>
<?php foreach ( $role_map as $dc_id => $fr_key ) : ?>
<tr class="wbf-role-map-row">
<td><input type="text" name="discord_role_id[]"
value="<?php echo esc_attr($dc_id); ?>"
placeholder="Discord Rollen-ID"
class="widefat" style="font-family:monospace"></td>
<td>
<select name="discord_forum_role[]" class="widefat">
<option value="">— wählen —</option>
<?php foreach ( $forum_roles as $rk => $role ) :
if ( $rk === 'superadmin' ) continue; ?>
<option value="<?php echo esc_attr($rk); ?>"
<?php selected($rk, $fr_key); ?>>
<?php echo esc_html($role['label']); ?>
</option>
<?php endforeach; ?>
</select>
</td>
<td><button type="button" class="button button-small wbf-rm-role-row"
style="color:#c00">✕</button></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<button type="button" class="button" id="wbf-add-role-row">+ Zeile hinzufügen</button>
<script>
(function(){
document.getElementById('wbf-add-role-row').addEventListener('click', function(){
var tbody = document.querySelector('#wbf-discord-role-map tbody');
var row = document.querySelector('.wbf-role-map-row').cloneNode(true);
row.querySelectorAll('input').forEach(function(i){i.value='';});
row.querySelectorAll('select').forEach(function(s){s.selectedIndex=0;});
tbody.appendChild(row);
});
document.addEventListener('click', function(e){
if (e.target.classList.contains('wbf-rm-role-row')) {
var rows = document.querySelectorAll('.wbf-role-map-row');
if (rows.length > 1) e.target.closest('tr').remove();
}
});
})();
</script>
<!-- Test-Verbindung -->
<div style="margin-top:1.25rem;padding:1rem;background:#f0f7ff;border:1px solid #c3dafe;border-radius:6px;max-width:680px">
<strong>🔌 Verbindungstest</strong><br>
<p style="margin:.4rem 0 .75rem;color:#374151;font-size:.9rem">
Speichere zuerst die Einstellungen, dann klicke „Testen" um zu prüfen ob der Bot erreichbar ist.
</p>
<button type="button" class="button button-secondary" id="wbf-discord-test-btn">
🔌 Discord-Verbindung testen
</button>
<span id="wbf-discord-test-result" style="margin-left:10px;font-weight:600"></span>
</div>
<script>
document.getElementById('wbf-discord-test-btn').addEventListener('click', function(){
var btn = this;
var res = document.getElementById('wbf-discord-test-result');
btn.disabled = true;
res.textContent = '⏳ Teste…';
fetch(ajaxurl, {
method: 'POST',
headers: {'Content-Type':'application/x-www-form-urlencoded'},
body: 'action=wbf_discord_test&nonce=<?php echo wp_create_nonce("wbf_discord_test"); ?>'
})
.then(r => r.json())
.then(function(d){
if (d.success) {
res.style.color = '#16a34a';
res.textContent = '✅ ' + (d.data.message || 'Verbunden!');
} else {
res.style.color = '#dc2626';
res.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
}
btn.disabled = false;
})
.catch(function(){ res.style.color='#dc2626'; res.textContent='❌ Netzwerkfehler'; btn.disabled=false; });
});
</script>
<!-- ══════════════════════════════════════════════════════════
Minecraft Bridge
════════════════════════════════════════════════════════════ -->
<div class="wbf-settings-box" style="margin-top:2rem;padding:1.5rem;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb">
<h2 style="margin-top:0;display:flex;align-items:center;gap:8px">
<span style="font-size:1.3em">⛏️</span> Minecraft Bridge
</h2>
<p class="description" style="margin-bottom:1.2rem;color:#6b7280">
Verbindet das Forum mit deinem BungeeCord-Server (StatusAPI Plugin).
Spieler können ihren Forum-Account mit <code>/forumlink &lt;token&gt;</code> verknüpfen
und erhalten dann Ingame-Benachrichtigungen bei neuen Antworten, Erwähnungen und PNs.
</p>
<table class="form-table" role="presentation">
<tr>
<th scope="row">Aktiviert</th>
<td>
<label>
<input type="checkbox" name="mc_bridge_enabled" value="1"
<?php checked( '1', $s['mc_bridge_enabled'] ?? '0' ); ?>>
Minecraft Bridge aktivieren
</label>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_mc_api_url">StatusAPI URL</label></th>
<td>
<input type="url" id="wbf_mc_api_url" name="mc_bridge_api_url"
value="<?php echo esc_attr( $s['mc_bridge_api_url'] ?? '' ); ?>"
class="regular-text"
placeholder="http://dein-server:9191">
<p class="description">
IP + Port deines BungeeCord StatusAPI Servers.
Beispiel: <code>http://192.168.1.100:9191</code>
</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_mc_api_secret">API Secret</label></th>
<td>
<input type="password" id="wbf_mc_api_secret" name="mc_bridge_api_secret"
value="<?php echo esc_attr( $s['mc_bridge_api_secret'] ?? '' ); ?>"
class="regular-text"
autocomplete="new-password"
placeholder="Gemeinsames Passwort">
<p class="description">
Muss identisch sein mit <code>forum.api_secret</code> in der
<code>verify.properties</code> des StatusAPI Plugins.
</p>
</td>
</tr>
<tr>
<th scope="row">Verbindungstest</th>
<td>
<button type="button" id="wbf-mc-test-btn" class="button"
onclick="wbfTestMcConnection()">
🔌 Verbindung testen
</button>
<span id="wbf-mc-test-result" style="margin-left:10px;font-weight:600"></span>
<script>
function wbfTestMcConnection() {
var btn = document.getElementById('wbf-mc-test-btn');
var result = document.getElementById('wbf-mc-test-result');
var url = document.getElementById('wbf_mc_api_url').value.replace(/\/$/, '');
if (!url) { result.style.color='#dc2626'; result.textContent = '❌ Bitte erst eine URL eingeben.'; return; }
btn.disabled = true;
result.style.color = '#6b7280';
result.textContent = '⏳ Teste Verbindung...';
// Test gegen WordPress REST-Endpoint (sicherer als direkter BungeeCord-Aufruf vom Browser)
fetch('<?php echo esc_url( rest_url("mc-bridge/v1/status") ); ?>')
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
result.style.color = '#16a34a';
result.innerHTML = '✅ <strong>WordPress-Endpoint aktiv!</strong> Plugin v' + (d.version || '?');
} else {
result.style.color = '#dc2626';
result.textContent = '⚠️ Endpoint antwortet, aber Fehler: ' + JSON.stringify(d);
}
})
.catch(function(e) {
result.style.color = '#dc2626';
result.textContent = '❌ Nicht erreichbar: ' + e.message;
})
.finally(function() { btn.disabled = false; });
}
</script>
<p class="description" style="margin-top:.5rem">
Testet ob der WordPress REST-Endpoint <code>/wp-json/mc-bridge/v1/status</code> erreichbar ist.
Danach in <code>verify.properties</code>: <code>forum.wp_url</code> und <code>forum.api_secret</code> eintragen.
</p>
</td>
</tr>
</table>
<div style="background:#fffbeb;border:1px solid #fcd34d;border-radius:6px;padding:1rem;margin-top:1rem;font-size:.875rem">
<strong>⚙️ Einrichtung in 3 Schritten:</strong>
<ol style="margin:.5rem 0 0 1.2rem;padding:0;line-height:1.8">
<li>API Secret hier festlegen und Einstellungen speichern.</li>
<li>In <code>verify.properties</code> des BungeeCord-Plugins setzen:
<br><code>forum.enabled=true</code>
<br><code>forum.wp_url=<?php echo esc_html( get_site_url() ); ?></code>
<br><code>forum.api_secret=DEIN_SECRET</code>
</li>
<li>Spieler können sich nun mit <strong><code>/forumlink &lt;token&gt;</code></strong> ingame verknüpfen.
Den Token generieren sie in ihrem Forum-Profil unter dem Tab <em>Verbindungen</em>.</li>
</ol>
</div>
</div>
<?php submit_button(
'💾 Einstellungen speichern',
'primary',
@@ -499,7 +857,6 @@ function wbf_admin_settings() {
true,
[ 'style' => 'margin-top:1rem' ]
); ?>
</form>
<!-- ── Vorschau-Tabelle ──────────────────────────────── -->
<hr style="margin-top:2.5rem">

View File

@@ -125,18 +125,22 @@ class WBF_Setup {
$page_title = sanitize_text_field($_POST['page_title'] ?? 'Forum');
if ($create_page) {
$existing = get_posts(['post_type'=>'page','s'=>$page_title,'posts_per_page'=>1]);
if (empty($existing)) {
$page_id = wp_insert_post([
'post_title' => $page_title,
'post_content' => '[business_forum]',
'post_status' => 'publish',
'post_type' => 'page',
]);
if ($page_id) {
update_option('wbf_forum_page_id', $page_id);
$success = get_permalink($page_id);
}
global $wpdb;
$existing_id = $wpdb->get_var( $wpdb->prepare(
"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status = 'publish' AND post_content LIKE %s LIMIT 1",
'%[business_forum]%'
) );
if (empty($existing_id)) {
$page_id = wp_insert_post([
'post_title' => $page_title,
'post_content' => '[business_forum]',
'post_status' => 'publish',
'post_type' => 'page',
]);
if ($page_id) {
update_option('wbf_forum_page_id', $page_id);
$success = get_permalink($page_id);
}
} else {
$success = get_permalink($existing[0]->ID);
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,10 @@
/* ── Utilities ──────────────────────────────────────────────── */
function wbfPost(action, data, cb, errCb) {
if (typeof WBF === 'undefined' || !WBF.ajax_url || !WBF.nonce) {
if (errCb) errCb({message: 'Forum-Fehler: AJAX-Setup fehlt. Bitte Seite neu laden.'});
return;
}
data.action = action;
data.nonce = WBF.nonce;
$.post(WBF.ajax_url, data, function (res) {
@@ -52,12 +56,18 @@
/* ── Registrieren ───────────────────────────────────────────── */
$(document).on('click', '.wbf-reg-submit-btn', function () {
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
var $invite = $(this).closest('.wbf-auth-box').find('.wbf-field-invite-code');
var inviteVal = '';
if ($invite.length > 0) {
var raw = $invite.val();
if (typeof raw === 'string') inviteVal = raw.toUpperCase().trim();
}
wbfPost('wbf_register', {
username: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-user').val(),
display_name: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-name').val(),
email: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-email').val(),
password: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-pass').val(),
invite_code: $(this).closest('.wbf-auth-box').find('.wbf-field-invite-code').val().toUpperCase().trim(),
invite_code: inviteVal,
rules_accepted: $(this).closest('.wbf-auth-box').find('.wbf-field-rules-accept').is(':checked') ? '1' : ''
}, function () {
location.reload();
@@ -510,23 +520,63 @@
});
});
/* ── Profil speichern ───────────────────────────────────────── */
$(document).on('click', '#wbfSaveProfile, #wbfSaveProfileCf', function () {
/* ── Profil speichern (alles auf einmal) ───────────────────── */
$(document).on('click', '#wbfSaveProfile', function () {
var $btn = $(this).prop('disabled', true);
var $msg = $(this).siblings('.wbf-msg').length ? $(this).siblings('.wbf-msg') : $('#wbfProfileMsg');
var $msg = $('#wbfProfileMsg');
var data = {
display_name: $('#wbfEditName').val(),
bio: $('#wbfEditBio').val(),
signature: $('#wbfEditSignature').val(),
new_password: $('#wbfNewPassword').val()
signature: $('#wbfEditSignature').val()
};
// Benutzerdefinierte Profilfelder einsammeln
// Alle benutzerdefinierten Felder (alle Kategorien) einsammeln
$('.wbf-cf-input').each(function () {
data[$(this).data('field')] = $(this).val();
});
wbfPost('wbf_update_profile', data, function (d) {
showMsg($msg, d.message, true);
$btn.prop('disabled', false);
// Bio und Signatur sofort aktualisieren (ohne Reload)
if (typeof data.bio !== 'undefined') {
$('.wbf-profile-sidebar__bio-text').text(data.bio);
}
if (typeof data.signature !== 'undefined') {
$('.wbf-profile-sidebar__sig').text(data.signature);
}
}, function (d) {
showMsg($msg, d.message || 'Fehler', false);
$btn.prop('disabled', false);
});
});
/* ── Passwort ändern ────────────────────────────────────────── */
$(document).on('click', '#wbfSavePassword', function () {
var $btn = $(this).prop('disabled', true);
var $msg = $('#wbfPasswordMsg');
var cur = $('#wbfCurrentPassword').val();
var pw1 = $('#wbfNewPassword').val();
var pw2 = $('#wbfNewPassword2').val();
if (!cur) {
showMsg($msg, 'Bitte aktuelles Passwort eingeben.', false);
return $btn.prop('disabled', false);
}
if (pw1.length < 6) {
showMsg($msg, 'Neues Passwort mindestens 6 Zeichen.', false);
return $btn.prop('disabled', false);
}
if (pw1 !== pw2) {
showMsg($msg, 'Die Passwörter stimmen nicht überein.', false);
return $btn.prop('disabled', false);
}
wbfPost('wbf_update_profile', {
current_password: cur,
new_password: pw1
}, function (d) {
showMsg($msg, d.message, true);
$('#wbfCurrentPassword, #wbfNewPassword, #wbfNewPassword2').val('');
$btn.prop('disabled', false);
}, function (d) {
showMsg($msg, d.message || 'Fehler', false);
$btn.prop('disabled', false);
@@ -542,10 +592,16 @@
$(document).on('change', '#wbfAvatarFile', function () {
var file = this.files[0];
if (!file) return;
// Sofort-Vorschau — synchron, kein Callback, kein Warten
var objectUrl = URL.createObjectURL(file);
$('#wbfProfileAvatar').attr('src', objectUrl).css('opacity', '.6');
var fd = new FormData();
fd.append('action', 'wbf_upload_avatar');
fd.append('nonce', WBF.nonce);
fd.append('avatar', file);
$.ajax({
url: WBF.ajax_url,
type: 'POST',
@@ -553,9 +609,61 @@
processData: false,
contentType: false,
success: function (res) {
$('#wbfProfileAvatar').css('opacity', '1');
if (res.success) {
$('.wbf-profile-page__avatar').attr('src', res.data.avatar_url);
// Object-URL freigeben, endgültige Server-URL setzen
URL.revokeObjectURL(objectUrl);
var finalUrl = res.data.avatar_url + '?v=' + Date.now();
$('#wbfProfileAvatar').attr('src', finalUrl);
// Topbar-Avatar ebenfalls aktualisieren
$('.wbf-topbar__user img').attr('src', finalUrl);
$('.wbf-profile-widget__avatar img').attr('src', finalUrl);
}
},
error: function () {
$('#wbfProfileAvatar').css('opacity', '1');
}
});
});
// ── Banner-Upload ─────────────────────────────────────────────────────────
$(document).on('change', '#wbfBannerFile', function () {
var file = this.files[0];
if (!file) return;
// Sofort-Vorschau
var objectUrl = URL.createObjectURL(file);
var $wrap = $('#wbfProfileBannerWrap');
var $existing = $wrap.find('.wbf-profile-banner__img');
// Falls noch kein Banner-Bild existiert, eins einfügen
if ($existing.length === 0) {
$wrap.prepend('<img src="' + objectUrl + '" alt="" id="wbfProfileBanner" class="wbf-profile-banner__img" style="opacity:.4">');
} else {
$existing.attr('src', objectUrl).css('opacity', '.4');
}
var fd = new FormData();
fd.append('action', 'wbf_upload_banner');
fd.append('nonce', WBF.nonce);
fd.append('banner', file);
$.ajax({
url: WBF.ajax_url,
type: 'POST',
data: fd,
processData: false,
contentType: false,
success: function (res) {
var $img = $wrap.find('.wbf-profile-banner__img');
if (res.success) {
URL.revokeObjectURL(objectUrl);
$img.attr('src', res.data.banner_url + '?v=' + Date.now());
}
$img.css('opacity', '');
},
error: function () {
$wrap.find('.wbf-profile-banner__img').css('opacity', '');
}
});
});
@@ -1091,7 +1199,7 @@
var html = '';
d.notifications.forEach(function (n) {
var isUnread = n.is_read == 0;
var avatar = n.actor_avatar || '';
var avatar = $('<img>').attr('src', n.actor_avatar || '').attr('alt', '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
var base = WBF.forum_url || window.location.href.split('?')[0];
var sep = base.indexOf('?') !== -1 ? '&' : '?';
var actor = '<strong>' + $('<span>').text(n.actor_name).html() + '</strong>';
@@ -1118,7 +1226,7 @@
}
html += '<a class="wbf-notif-item' + (isUnread ? ' wbf-notif-item--unread' : '') + '" href="' + url + '">' +
'<div class="wbf-notif-item__avatar"><img src="' + avatar + '" alt=""></div>' +
'<div class="wbf-notif-item__avatar">' + avatar + '</div>' +
'<div class="wbf-notif-item__body">' +
'<div class="wbf-notif-item__text">' + text +
(sub ? '<br><span style="color:var(--c-muted);font-size:.78rem">' + $('<span>').text(sub).html() + '</span>' : '') +
@@ -1218,7 +1326,7 @@
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Loeschen"><i class="fas fa-trash-can"></i></button>';
var html = '<div class="' + cls + '" data-msg-id="' + m.id + '">';
if (!isMine) {
html += '<img src="' + (m.sender_avatar||'') + '" class="wbf-dm-inbox-item__avatar">';
html += $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
}
html += '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
@@ -1293,12 +1401,16 @@
'</div>'
].join('')).appendTo('body');
var wbfLogoutFired = false; // Guard gegen doppelten Logout-Call
function wbfDoLogout() {
if (wbfLogoutFired) return; // doppelten Aufruf verhindern
wbfLogoutFired = true;
clearTimeout(wbfIdleTimer);
clearTimeout(wbfWarnTimer);
clearInterval(wbfCountTimer);
$wbfToast.hide();
wbfPost('wbf_logout', {}, function () {
wbfPost('wbf_logout', { nonce: WBF.nonce }, function () {
location.reload();
});
}
@@ -1308,23 +1420,23 @@
var secs = 30;
$('#wbfIdleCountdown').text(secs);
$wbfToast.fadeIn(200);
// Countdown-Interval läuft bis 0 und ruft dann wbfDoLogout() auf —
// kein zusätzlicher setTimeout(wbfDoLogout) nötig (war die Ursache des Doppel-Logouts)
wbfCountTimer = setInterval(function () {
secs--;
$('#wbfIdleCountdown').text(secs);
$('#wbfIdleCountdown').text(Math.max(0, secs));
if (secs <= 0) {
clearInterval(wbfCountTimer);
wbfDoLogout();
}
}, 1000);
// Auto-logout after warning period
wbfIdleTimer = setTimeout(wbfDoLogout, wbfWarnMs);
}
function wbfResetIdleTimer() {
if (wbfWarning) return; // Nutzer hat aktiv Warnung bestätigt — nicht resetten
clearTimeout(wbfIdleTimer);
clearTimeout(wbfWarnTimer);
// Warn 30 sec before timeout
// Warnung 30 Sek. vor Ablauf zeigen
wbfWarnTimer = setTimeout(wbfShowWarning, wbfIdleMs - wbfWarnMs);
}
@@ -1334,6 +1446,7 @@
clearInterval(wbfCountTimer);
$wbfToast.fadeOut(200);
wbfWarning = false;
wbfLogoutFired = false; // Guard zurücksetzen
wbfResetIdleTimer();
});
@@ -1452,7 +1565,7 @@
var html = '';
d.users.forEach(function(u) {
html += '<div class="wbf-mention-item" data-username="' + $('<span>').text(u.username).html() + '">'
+ '<img src="' + (u.avatar_url || '') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
+ '<span>' + $('<span>').text(u.display_name).html() + '</span>'
+ '<small>@' + $('<span>').text(u.username).html() + '</small>'
+ '</div>';
@@ -1500,7 +1613,7 @@
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Löschen"><i class="fas fa-trash-can"></i></button>';
if (!isMine) {
html += '<div class="' + cls + '" data-msg-id="' + m.id + '">'
+ '<img src="' + (m.sender_avatar || '') + '" class="wbf-dm-msg__avatar">'
+ $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-msg__avatar')[0].outerHTML
+ '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
+ '</div><div class="wbf-dm-msg__time">' + time + delBtn + '</div></div></div>';
@@ -1524,7 +1637,7 @@
var href = window.location.pathname + '?forum_dm=inbox&with=' + conv.partner_id;
var unread = parseInt(conv.unread_cnt) > 0;
html += '<a class="wbf-dm-inbox-item' + (unread ? ' wbf-dm-inbox-item--unread' : '') + '" href="' + href + '">'
+ '<img src="' + (conv.partner_avatar || '') + '" class="wbf-dm-inbox-item__avatar">'
+ $('<img>').attr('src', conv.partner_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
+ '<div class="wbf-dm-inbox-item__body">'
+ '<span class="wbf-dm-inbox-item__name">' + $('<span>').text(conv.partner_name).html() + '</span>'
+ (unread ? '<span class="wbf-dm-inbox-item__badge">' + conv.unread_cnt + '</span>' : '')
@@ -1550,7 +1663,7 @@
var backUrl = window.location.pathname + '?forum_dm=inbox';
$('#wbfDmHeader').html(
'<a href="' + backUrl + '" class="wbf-dm-back-btn" title="Zurück zur Inbox"><i class="fas fa-arrow-left"></i></a>'
+ '<img src="' + (p.avatar_url||'') + '" class="wbf-dm-inbox-item__avatar">'
+ $('<img>').attr('src', p.avatar_url || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
+ '<strong>' + $('<span>').text(p.display_name).html() + '</strong>'
+ '<a href="?forum_profile=' + p.id + '" style="font-size:.78rem;color:var(--c-muted);text-decoration:none">@' + $('<span>').text(p.username).html() + '</a>'
);
@@ -1634,7 +1747,7 @@
var html = '';
d.users.forEach(function(u) {
html += '<div class="wbf-tag-suggest-item" data-id="' + u.id + '" data-name="' + $('<span>').text(u.display_name).html() + '">'
+ '<img src="' + (u.avatar_url||'') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
+ $('<span>').text(u.display_name).html()
+ '<span style="color:var(--c-muted);font-size:.75rem">@' + $('<span>').text(u.username).html() + '</span>'
+ '</div>';
@@ -2114,4 +2227,132 @@
$bar.hide();
});
}(jQuery));
// ── Discord-Integration (3-Schritt Verifikation) ─────────────────────────
var wbfDcStep = 1; // aktueller Schritt
function wbfDcMsg(text, color) {
var $m = $('#wbf-discord-msg');
$m.css('color', color || 'var(--c-muted)').html(text);
}
function wbfDcSetBadge(connected) {
var $badge = $('.wbf-connection-card--discord .wbf-connection-badge');
if (connected) {
$badge.removeClass('wbf-connection-badge--disconnected')
.addClass('wbf-connection-badge--connected')
.html('<i class="fas fa-check-circle"></i> Verbunden');
} else {
$badge.removeClass('wbf-connection-badge--connected')
.addClass('wbf-connection-badge--disconnected')
.html('<i class="fas fa-circle-xmark"></i> Nicht verbunden');
}
}
// Schritt 1 → Code senden
$(document).on('click', '#wbf-discord-send-code', function () {
var username = $('#wbf-discord-input').val().trim();
if (!username) { wbfDcMsg('<i class="fas fa-triangle-exclamation"></i> Bitte Benutzername eingeben.', '#f97316'); return; }
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Sende…');
wbfDcMsg('');
$.post(WBF.ajax_url, {
action: 'wbf_discord_send_code',
nonce: WBF.nonce,
discord_username: username,
}, function (res) {
$btn.prop('disabled', false).html('<i class="fab fa-discord"></i> Code senden');
if (res.success) {
wbfDcMsg('<i class="fas fa-check" style="color:#16a34a"></i> ' + (res.data.message || 'Code gesendet!'), '#16a34a');
$('#wbf-dc-step1').slideUp(200, function () { $('#wbf-dc-step2').slideDown(200); });
$('#wbf-discord-code-input').val('').focus();
wbfDcStep = 2;
} else {
wbfDcMsg('<i class="fas fa-circle-xmark"></i> ' + ((res.data && res.data.message) || 'Fehler.'), '#dc2626');
}
}).fail(function () {
$btn.prop('disabled', false).html('<i class="fab fa-discord"></i> Code senden');
wbfDcMsg('<i class="fas fa-circle-xmark"></i> Netzwerkfehler.', '#dc2626');
});
});
// Schritt 2 → Code bestätigen
$(document).on('click', '#wbf-discord-verify', function () {
var code = $('#wbf-discord-code-input').val().trim().toUpperCase();
if (code.length < 4) { wbfDcMsg('<i class="fas fa-triangle-exclamation"></i> Bitte Code eingeben.', '#f97316'); return; }
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Prüfe…');
$.post(WBF.ajax_url, {
action: 'wbf_discord_verify_code',
nonce: WBF.nonce,
verify_code: code,
}, function (res) {
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen');
if (res.success) {
wbfDcMsg('<i class="fas fa-check-circle"></i> ' + (res.data.message || 'Verbunden!'), '#16a34a');
wbfDcSetBadge(true);
// UI auf "Verbunden"-Ansicht umschalten
var name = res.data.display_name || '';
$('#wbf-discord-form').slideUp(200);
// Verbunden-Info einfügen/aktualisieren
var $info = $('.wbf-discord-connected-info');
if ($info.length) {
$info.find('.wbf-discord-linked-name').html('<i class="fab fa-discord" style="color:#5865f2"></i> ' + $('<span>').text(name).html());
} else {
// Frisch laden damit die PHP-Struktur stimmt
setTimeout(function(){ location.reload(); }, 1200);
}
} else {
wbfDcMsg('<i class="fas fa-circle-xmark"></i> ' + ((res.data && res.data.message) || 'Fehler.'), '#dc2626');
}
}).fail(function () {
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen');
wbfDcMsg('<i class="fas fa-circle-xmark"></i> Netzwerkfehler.', '#dc2626');
});
});
// Enter-Taste auf Code-Feld
$(document).on('keydown', '#wbf-discord-code-input', function (e) {
if (e.key === 'Enter') { e.preventDefault(); $('#wbf-discord-verify').trigger('click'); }
});
// Enter-Taste auf Username-Feld
$(document).on('keydown', '#wbf-discord-input', function (e) {
if (e.key === 'Enter') { e.preventDefault(); $('#wbf-discord-send-code').trigger('click'); }
});
// „Zurück" in Schritt 2
$(document).on('click', '#wbf-discord-code-back', function () {
$('#wbf-dc-step2').slideUp(200, function () { $('#wbf-dc-step1').slideDown(200); });
wbfDcMsg('');
wbfDcStep = 1;
});
// „Neu verknüpfen" bei bereits verbundenem Account
$(document).on('click', '#wbf-discord-relink', function () {
$('#wbf-discord-form').slideDown(200);
$('#wbf-discord-input').val('').focus();
});
// Verbindung trennen
$(document).on('click', '#wbf-discord-disconnect', function () {
if (!confirm('Discord-Verbindung wirklich trennen?')) return;
var $btn = $(this).prop('disabled', true);
$.post(WBF.ajax_url, {
action: 'wbf_save_discord',
nonce: WBF.nonce,
sub_action: 'disconnect',
}, function (res) {
$btn.prop('disabled', false);
if (res.success) {
wbfDcMsg('<i class="fas fa-check"></i> ' + (res.data.message || 'Getrennt.'), '#16a34a');
wbfDcSetBadge(false);
setTimeout(function () { location.reload(); }, 900);
} else {
wbfDcMsg('<i class="fas fa-circle-xmark"></i> ' + ((res.data && res.data.message) || 'Fehler.'), '#dc2626');
}
});
});
}(jQuery));
// Overwrite last line — Discord handlers appended via patch:

View File

@@ -7,7 +7,7 @@ class WBF_Ajax {
$actions = [
'wbf_login', 'wbf_register', 'wbf_logout',
'wbf_new_thread', 'wbf_new_post', 'wbf_toggle_like',
'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image',
'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image', 'wbf_upload_banner',
'wbf_forgot_password', 'wbf_reset_password', 'wbf_load_more_messages',
'wbf_create_invite', 'wbf_delete_invite',
'wbf_toggle_subscribe', 'wbf_restore_content', 'wbf_toggle_profile_visibility',
@@ -21,6 +21,13 @@ class WBF_Ajax {
'wbf_toggle_ignore',
'wbf_change_email',
'wbf_save_notification_prefs',
'wbf_save_discord',
'wbf_discord_send_code',
'wbf_discord_verify_code',
'wbf_2fa_setup_begin',
'wbf_2fa_setup_verify',
'wbf_2fa_disable',
'wbf_2fa_verify_login',
];
foreach ($actions as $action) {
add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]);
@@ -43,23 +50,50 @@ class WBF_Ajax {
// ── Auth ──────────────────────────────────────────────────────────────────
public static function handle_login() {
// Brute-Force-Schutz: max. 10 Versuche pro IP in 15 Minuten
$ip_key = 'wbf_login_fail_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
$fails = (int) get_transient( $ip_key );
if ( $fails >= 10 ) {
wp_send_json_error([
'message' => 'Zu viele fehlgeschlagene Loginversuche. Bitte warte 15 Minuten.',
'locked' => true,
]);
}
// Login braucht keinen Nonce — Credentials sind die Authentifizierung
$result = WBF_Auth::login(
sanitize_text_field($_POST['username'] ?? ''),
$_POST['password'] ?? ''
$_POST['password'] ?? '',
! empty($_POST['remember_me'])
);
if ($result['success']) {
// Erfolgreicher Login: Fehlzähler löschen
delete_transient( $ip_key );
$u = $result['user'];
if ( ! empty($_POST['remember_me']) ) {
WBF_Auth::set_remember_cookie($u->id);
}
wp_send_json_success(['display_name'=>$u->display_name,'avatar_url'=>$u->avatar_url,'user_id'=>$u->id]);
} elseif ( ! empty($result['2fa_required']) ) {
// 2FA erforderlich — kein Fehlerzähler erhöhen, kein Fehlermeldung
wp_send_json_error(['2fa_required' => true]);
} else {
// Fehlversuch zählen — außer bei gesperrten Konten (kein Passwortfehler)
if ( empty($result['banned']) ) {
set_transient( $ip_key, $fails + 1, 15 * MINUTE_IN_SECONDS );
}
wp_send_json_error($result);
}
}
public static function handle_register() {
// Brute-Force/Spam-Schutz: max. 5 Registrierungen pro IP pro Stunde
$reg_ip_key = 'wbf_reg_ip_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
$reg_fails = (int) get_transient( $reg_ip_key );
if ( $reg_fails >= 5 ) {
wp_send_json_error(['message' => 'Zu viele Registrierungsversuche von dieser IP. Bitte warte eine Stunde.']);
}
// Spam-Schutz: Honeypot + Zeitlimit
if ( ! empty($_POST['wbf_website']) ) {
wp_send_json_error(['message' => 'Spam erkannt.']);
@@ -98,6 +132,8 @@ class WBF_Ajax {
sanitize_text_field($_POST['display_name'] ?? '')
);
if ($result['success']) {
// Registrierungs-Zähler für IP erhöhen
set_transient( $reg_ip_key, $reg_fails + 1, HOUR_IN_SECONDS );
$u = $result['user'];
// Einladungscode einlösen
$reg_mode2 = wbf_get_settings()['registration_mode'] ?? 'open';
@@ -112,7 +148,10 @@ class WBF_Ajax {
}
public static function handle_logout() {
// Kein Nonce-Check für Logout nötig — Session-Clearing ist sicher
// Nonce-Check für Logout
if ( ! isset($_POST['nonce']) || ! check_ajax_referer('wbf_nonce', 'nonce', false) ) {
wp_send_json_error(['message' => 'invalid_nonce'], 403);
}
WBF_Auth::logout();
wp_send_json_success(['message' => 'logged_out']);
}
@@ -160,6 +199,10 @@ class WBF_Ajax {
'content' => WBF_DB::apply_word_filter($content),
'prefix_id' => $prefix_id,
]);
// Ingame-Benachrichtigung
if (function_exists('wbf_notify_ingame')) {
wbf_notify_ingame($user->username, 'Neuer Thread: ' . mb_substr($title, 0, 80));
}
// Tags speichern
$raw_tags = sanitize_text_field( $_POST['tags'] ?? '' );
@@ -226,9 +269,11 @@ class WBF_Ajax {
}
// Thread-Abonnenten benachrichtigen
$subscribers = WBF_DB::get_thread_subscribers($thread_id);
// $notif_users is a flat array of IDs (from get_col) — cast to int for comparison
$notif_ids = array_map('intval', $notif_users);
foreach ($subscribers as $sub) {
if ((int)$sub->id === (int)$user->id) continue; // nicht sich selbst
if (in_array($sub->id, array_column($notif_users, 'id') ?: [])) continue; // schon benachrichtigt
if (in_array((int)$sub->id, $notif_ids, true)) continue; // schon benachrichtigt
self::send_notification_email($sub, 'reply', $user->display_name, [
'thread_id' => $thread_id,
'thread_title' => $thread->title,
@@ -372,6 +417,19 @@ class WBF_Ajax {
if (!empty($_POST['new_password'])) {
if (strlen($_POST['new_password']) < 6) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']);
// Sicherheit: aktuelles Passwort muss zur Bestätigung angegeben werden
$current_pw = $_POST['current_password'] ?? '';
if ( empty($current_pw) ) {
wp_send_json_error(['message'=>'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
}
if ( ! password_verify($current_pw, $user->password) ) {
wp_send_json_error(['message'=>'Aktuelles Passwort ist falsch.']);
}
// Bestätigungsfeld server-seitig prüfen
$new_pw2 = $_POST['new_password2'] ?? '';
if ( ! empty($new_pw2) && $new_pw2 !== $_POST['new_password'] ) {
wp_send_json_error(['message'=>'Die Passwörter stimmen nicht überein.']);
}
$update['password'] = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
}
@@ -397,6 +455,15 @@ class WBF_Ajax {
$value = sanitize_textarea_field( $raw );
} elseif ( $def['type'] === 'number' ) {
$value = is_numeric($raw) ? (string)(float)$raw : '';
} elseif ( $def['type'] === 'date' ) {
// Datum validieren — nur YYYY-MM-DD, nicht in der Zukunft
$raw_date = sanitize_text_field( trim($raw) );
if ( preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw_date) ) {
$ts = strtotime($raw_date);
$value = ($ts && $ts <= time()) ? $raw_date : '';
} else {
$value = '';
}
} else {
$value = sanitize_text_field( $raw );
}
@@ -458,6 +525,52 @@ class WBF_Ajax {
wp_send_json_success(['avatar_url'=>$url]);
}
public static function handle_upload_banner() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
if ( empty($_FILES['banner']) ) wp_send_json_error(['message' => 'Keine Datei.']);
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
if ( $_FILES['banner']['size'] > 3 * 1024 * 1024 ) {
wp_send_json_error(['message' => 'Maximale Dateigröße: 3 MB.']);
}
$tmp = $_FILES['banner']['tmp_name'] ?? '';
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
wp_send_json_error(['message' => 'Ungültiger Datei-Upload.']);
}
if ( function_exists('finfo_open') ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$real_mime = finfo_file( $finfo, $tmp );
finfo_close( $finfo );
} else {
$et_map = [
IMAGETYPE_JPEG => 'image/jpeg',
IMAGETYPE_PNG => 'image/png',
IMAGETYPE_GIF => 'image/gif',
IMAGETYPE_WEBP => 'image/webp',
];
$et = @exif_imagetype( $tmp );
$real_mime = $et_map[$et] ?? '';
}
if ( ! in_array( $real_mime, $allowed_types, true ) ) {
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
}
require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
$id = media_handle_upload('banner', 0);
if ( is_wp_error($id) ) wp_send_json_error(['message' => $id->get_error_message()]);
$url = wp_get_attachment_url($id);
WBF_DB::update_user($user->id, ['banner_url' => $url]);
wp_send_json_success(['banner_url' => $url]);
}
// ── Report ────────────────────────────────────────────────────────────────
public static function handle_report_post() {
@@ -594,7 +707,8 @@ class WBF_Ajax {
self::verify();
$query = sanitize_text_field( $_POST['query'] ?? '' );
if ( mb_strlen( $query ) < 2 ) wp_send_json_error(['message' => 'Suchbegriff zu kurz.']);
$results = WBF_DB::search( $query, 40 );
$current_search = WBF_Auth::get_current_user();
$results = WBF_DB::search( $query, 40, $current_search );
wp_send_json_success(['results' => $results, 'query' => $query]);
}
@@ -1141,6 +1255,12 @@ class WBF_Ajax {
self::verify();
$user = WBF_Auth::get_current_user();
if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
// Sicherstellen dass Spalte existiert (Schutz für bestehende Installs)
global $wpdb;
$cols = $wpdb->get_col( "DESCRIBE {$wpdb->prefix}forum_users" );
if ( ! in_array( 'profile_public', $cols ) ) {
$wpdb->query( "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) NOT NULL DEFAULT 1" );
}
$current = (int)($user->profile_public ?? 1);
$new = $current ? 0 : 1;
WBF_DB::update_user($user->id, ['profile_public'=>$new]);
@@ -1391,6 +1511,319 @@ class WBF_Ajax {
] );
}
// ── Discord: Verifikations-Code per Bot-DM senden ─────────────────────────
public static function handle_discord_send_code() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$s = wbf_get_settings();
$token = trim($s['discord_bot_token'] ?? '');
$guild = trim($s['discord_guild_id'] ?? '');
if ( ! $token ) {
wp_send_json_error(['message' => 'Discord-Bot ist noch nicht konfiguriert. Bitte wende dich an einen Admin.']);
}
$username_input = sanitize_text_field($_POST['discord_username'] ?? '');
if ( ! $username_input ) {
wp_send_json_error(['message' => 'Bitte Discord-Benutzername eingeben.']);
}
// Nutzer auf dem Guild suchen (nach Username oder per Search)
$discord_user_id = self::discord_find_user_id($username_input, $token, $guild);
if ( ! $discord_user_id ) {
wp_send_json_error(['message' => 'Discord-Nutzer nicht auf dem Server gefunden. Stelle sicher, dass du Mitglied des Servers bist.']);
}
// Verifikations-Code generieren (6-stellig)
$code = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 6));
$expires = time() + 600; // 10 Minuten
// Code + Discord-User-ID temporär speichern
WBF_DB::set_user_meta($user->id, 'discord_verify_code', $code);
WBF_DB::set_user_meta($user->id, 'discord_verify_expires', (string)$expires);
WBF_DB::set_user_meta($user->id, 'discord_verify_pending_id', $discord_user_id);
// DM senden
$sent = self::discord_send_dm($discord_user_id, $token,
"🔐 **Dein Verifikationscode für " . get_bloginfo('name') . ":**\n\n" .
"```" . $code . "```\n" .
"Gib diesen Code im Forum ein. Er ist **10 Minuten** gültig.\n" .
"_Falls du diese Nachricht nicht erwartet hast, ignoriere sie einfach._"
);
if ( ! $sent ) {
wp_send_json_error(['message' => 'DM konnte nicht gesendet werden. Stelle sicher, dass du DMs von Server-Mitgliedern zulässt.']);
}
wp_send_json_success(['message' => '✅ Code gesendet! Prüfe deine Discord-DMs und gib den 6-stelligen Code ein.', 'step' => 'enter_code']);
}
// ── Discord: Code überprüfen + Verbindung herstellen ─────────────────────
public static function handle_discord_verify_code() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$code_input = strtoupper(sanitize_text_field($_POST['verify_code'] ?? ''));
$meta = WBF_DB::get_user_meta($user->id);
$stored_code = strtoupper($meta['discord_verify_code'] ?? '');
$expires = (int)($meta['discord_verify_expires'] ?? 0);
$discord_uid = $meta['discord_verify_pending_id'] ?? '';
if ( ! $stored_code || ! $discord_uid ) {
wp_send_json_error(['message' => 'Kein offener Verifizierungs-Vorgang. Bitte erneut starten.']);
}
if ( time() > $expires ) {
wp_send_json_error(['message' => 'Code abgelaufen. Bitte erneut einen Code anfordern.']);
}
if ( ! hash_equals($stored_code, $code_input) ) {
wp_send_json_error(['message' => 'Falscher Code. Bitte erneut versuchen.']);
}
// Discord-Username abrufen (für Anzeige)
$s = wbf_get_settings();
$token = trim($s['discord_bot_token'] ?? '');
$display_name = $discord_uid;
if ( $token ) {
$res = wp_remote_get("https://discord.com/api/v10/users/{$discord_uid}", [
'timeout' => 5,
'headers' => ['Authorization' => 'Bot ' . $token],
]);
if ( ! is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200 ) {
$d = json_decode(wp_remote_retrieve_body($res), true);
$display_name = $d['global_name'] ?? $d['username'] ?? $discord_uid;
}
}
// Speichern
WBF_DB::set_user_meta($user->id, 'discord_user_id', $discord_uid);
WBF_DB::set_user_meta($user->id, 'discord_username', $display_name);
// Temp-Daten löschen
WBF_DB::set_user_meta($user->id, 'discord_verify_code', '');
WBF_DB::set_user_meta($user->id, 'discord_verify_expires', '');
WBF_DB::set_user_meta($user->id, 'discord_verify_pending_id', '');
// Rollen-Sync direkt nach Verifikation
$guild = trim($s['discord_guild_id'] ?? '');
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
if ( ($s['discord_role_sync'] ?? '0') === '1' && $token && $guild && $role_map ) {
wbf_sync_discord_role_for_user($user->id, $discord_uid, $token, $guild, $role_map);
}
wp_send_json_success([
'message' => '🎉 Discord erfolgreich verknüpft!',
'connected' => true,
'display_name' => esc_html($display_name),
]);
}
// ── Discord: Verbindung trennen ───────────────────────────────────────────
public static function handle_save_discord() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$action = sanitize_key( $_POST['sub_action'] ?? 'save' );
if ( $action === 'disconnect' ) {
WBF_DB::set_user_meta($user->id, 'discord_username', '');
WBF_DB::set_user_meta($user->id, 'discord_user_id', '');
wp_send_json_success(['message' => 'Discord-Verbindung getrennt.', 'connected' => false]);
}
wp_send_json_error(['message' => 'Unbekannte Aktion.']);
}
// ── Discord Hilfsmethoden ─────────────────────────────────────────────────
private static function discord_find_user_id($username_input, $token, $guild) {
if ( ! $guild ) return null;
// Guild-Member-Search (max. 1 Treffer)
$search = rawurlencode($username_input);
$res = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}/members/search?query={$search}&limit=5", [
'timeout' => 6,
'headers' => ['Authorization' => 'Bot ' . $token],
]);
if ( is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200 ) return null;
$members = json_decode(wp_remote_retrieve_body($res), true);
if ( empty($members) ) return null;
// Exakten Treffer bevorzugen
$input_lower = strtolower($username_input);
foreach ( $members as $m ) {
$uname = strtolower($m['user']['username'] ?? '');
$global = strtolower($m['user']['global_name'] ?? '');
if ( $uname === $input_lower || $global === $input_lower ) {
return $m['user']['id'];
}
}
// Erster Treffer als Fallback
return $members[0]['user']['id'] ?? null;
}
private static function discord_send_dm($user_id, $token, $message) {
// DM-Channel erstellen
$ch_res = wp_remote_post('https://discord.com/api/v10/users/@me/channels', [
'timeout' => 6,
'headers' => ['Authorization' => 'Bot ' . $token, 'Content-Type' => 'application/json'],
'body' => json_encode(['recipient_id' => $user_id]),
]);
if ( is_wp_error($ch_res) || wp_remote_retrieve_response_code($ch_res) !== 200 ) return false;
$channel = json_decode(wp_remote_retrieve_body($ch_res), true);
$ch_id = $channel['id'] ?? '';
if ( ! $ch_id ) return false;
// Nachricht senden
$msg_res = wp_remote_post("https://discord.com/api/v10/channels/{$ch_id}/messages", [
'timeout' => 6,
'headers' => ['Authorization' => 'Bot ' . $token, 'Content-Type' => 'application/json'],
'body' => json_encode(['content' => $message]),
]);
return ( ! is_wp_error($msg_res) && wp_remote_retrieve_response_code($msg_res) === 200 );
}
// ══════════════════════════════════════════════════════════════════════════
// ── 2FA / TOTP ────────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Setup-Schritt 1: Neues Secret generieren und als "pending" speichern.
* Gibt Secret (zur manuellen Eingabe) und otpauth:// URI (für QR-Code) zurück.
*/
public static function handle_2fa_setup_begin() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
$secret = WBF_TOTP::generate_secret();
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_PENDING, $secret );
wp_send_json_success( [
"secret" => $secret,
"uri" => WBF_TOTP::get_otpauth_uri( $user->username, $secret ),
] );
}
/**
* Setup-Schritt 2: Code verifizieren und 2FA aktivieren.
*/
public static function handle_2fa_setup_verify() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_PENDING );
if ( empty($secret) ) {
wp_send_json_error( ["message" => "Kein ausstehender 2FA-Setup. Bitte neu starten."] );
}
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
wp_send_json_error( ["message" => "Ungültiger Code. Bitte Uhrzeit prüfen und erneut versuchen."] );
}
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_SECRET, $secret );
global $wpdb;
$wpdb->delete( "{$wpdb->prefix}forum_user_meta",
["user_id" => $user->id, "meta_key" => WBF_TOTP::META_PENDING], ["%d", "%s"] );
wp_send_json_success( ["message" => "2FA erfolgreich aktiviert!"] );
}
/**
* 2FA deaktivieren (User-seitig).
* Erfordert aktuelles Passwort + gültigen TOTP-Code.
*/
public static function handle_2fa_disable() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
$password = $_POST["password"] ?? "";
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
$fresh = WBF_DB::get_user( $user->id );
if ( ! $fresh || ! password_verify( $password, $fresh->password ) ) {
wp_send_json_error( ["message" => "Falsches Passwort."] );
}
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
if ( empty($secret) ) {
wp_send_json_error( ["message" => "2FA ist nicht aktiv."] );
}
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
wp_send_json_error( ["message" => "Ungültiger Authenticator-Code."] );
}
WBF_TOTP::disable_for( $user->id );
wp_send_json_success( ["message" => "2FA wurde deaktiviert."] );
}
/**
* Login-Schritt 2: TOTP-Code nach erfolgreichem Passwort prüfen.
* Kein Nonce — ausstehende Session-ID ist der Auth-Beweis.
* Brute-Force-Schutz: max. 5 Versuche / IP / 10 Min.
*/
public static function handle_2fa_verify_login() {
WBF_Auth::init();
$ip_key = "wbf_2fa_fail_" . md5( $_SERVER["REMOTE_ADDR"] ?? "unknown" );
$fails = (int) get_transient( $ip_key );
if ( $fails >= 5 ) {
wp_send_json_error( ["message" => "Zu viele Fehlversuche. Bitte warte 10 Minuten.", "locked" => true] );
}
$pending = (int) ( $_SESSION[ WBF_TOTP::SESSION_PENDING ] ?? 0 );
if ( ! $pending ) {
wp_send_json_error( ["message" => "Keine ausstehende Anmeldung. Bitte erneut einloggen."] );
}
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
$user = WBF_DB::get_user( $pending );
if ( ! $user ) {
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
wp_send_json_error( ["message" => "Ungültige Sitzung."] );
}
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
if ( empty($secret) || ! WBF_TOTP::verify( $secret, $code ) ) {
set_transient( $ip_key, $fails + 1, 10 * MINUTE_IN_SECONDS );
wp_send_json_error( ["message" => "Ungültiger Code. Bitte erneut versuchen."] );
}
delete_transient( $ip_key );
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
if ( WBF_Roles::level($user->role) < 0 ) {
wp_send_json_error( ["message" => "Dein Konto ist gesperrt."] );
}
if ( session_id() ) session_regenerate_id( true );
$_SESSION[ WBF_Auth::SESSION_KEY ] = $user->id;
WBF_DB::touch_last_active( $user->id );
if ( ! empty( $_SESSION["wbf_2fa_remember"] ) ) {
WBF_Auth::set_remember_cookie( $user->id );
unset( $_SESSION["wbf_2fa_remember"] );
}
wp_send_json_success( [
"display_name" => $user->display_name,
"avatar_url" => $user->avatar_url,
"user_id" => $user->id,
] );
}
}
add_action( 'init', [ 'WBF_Ajax', 'init' ] );

View File

@@ -3,11 +3,25 @@ if ( ! defined( 'ABSPATH' ) ) exit;
class WBF_Auth {
const SESSION_KEY = 'wbf_forum_user';
const SESSION_KEY = 'wbf_forum_user';
public static function init() {
// PHP 8.3: session_start() nach gesendeten Headers erzeugt E_WARNING,
// der direkt in den HTML-Output fließt und das Layout zerstört.
// Lösung: headers_sent() prüfen + session_start() mit Cookie-Optionen aufrufen.
if ( ! session_id() ) {
session_start();
if ( headers_sent() ) {
return;
}
$session_opts = [
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
'use_strict_mode' => true,
];
if ( is_ssl() || ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ) {
$session_opts['cookie_secure'] = true;
}
session_start( $session_opts );
}
// Auto-login via Remember-Me cookie if not already logged in
if ( empty( $_SESSION[ self::SESSION_KEY ] ) && isset( $_COOKIE['wbf_remember'] ) ) {
@@ -33,7 +47,7 @@ class WBF_Auth {
return WBF_DB::get_user( (int) $_SESSION[ self::SESSION_KEY ] );
}
public static function login( $username_or_email, $password ) {
public static function login( $username_or_email, $password, $remember = false ) {
self::init();
$user = WBF_DB::get_user_by( 'username', $username_or_email );
if ( ! $user ) {
@@ -43,6 +57,19 @@ class WBF_Auth {
if ( ! password_verify( $password, $user->password ) ) {
return array( 'success' => false, 'message' => 'Falsches Passwort.' );
}
// ── 2FA-Check ─────────────────────────────────────────────────────────
// Wenn 2FA aktiv: Login pausieren und TOTP-Code anfordern.
// remember-Flag in Session merken, damit es nach 2FA-Verifikation gesetzt wird.
if ( class_exists('WBF_TOTP') && WBF_TOTP::is_enabled_for( $user->id ) ) {
$_SESSION[ WBF_TOTP::SESSION_PENDING ] = $user->id;
if ( $remember ) {
$_SESSION['wbf_2fa_remember'] = true;
}
return array( 'success' => false, '2fa_required' => true );
}
// ── Ende 2FA-Check ────────────────────────────────────────────────────
if ( WBF_Roles::level($user->role) < 0 ) {
// Zeitlich begrenzte Sperre prüfen — automatisch aufheben wenn abgelaufen
if ( ! empty($user->ban_until) && strtotime($user->ban_until) <= time() ) {
@@ -53,20 +80,20 @@ class WBF_Auth {
'ban_until' => null,
'pre_ban_role' => '',
]);
// Frisch laden und einloggen
$user = WBF_DB::get_user( $user->id );
if ( session_id() ) session_regenerate_id( true );
$_SESSION[ self::SESSION_KEY ] = $user->id;
WBF_DB::touch_last_active( $user->id );
return array( 'success' => true, 'user' => $user );
}
$reason = !empty($user->ban_reason) ? $user->ban_reason : 'Dein Konto wurde gesperrt.';
// Zeitstempel anhängen wenn temporäre Sperre
if ( ! empty($user->ban_until) ) {
$until_fmt = date_i18n( 'd.m.Y \u\m H:i \U\h\r', strtotime($user->ban_until) );
$reason .= ' (Gesperrt bis: ' . $until_fmt . ')';
}
return array( 'success' => false, 'banned' => true, 'message' => $reason );
}
if ( session_id() ) session_regenerate_id( true );
$_SESSION[ self::SESSION_KEY ] = $user->id;
WBF_DB::touch_last_active( $user->id );
return array( 'success' => true, 'user' => $user );
@@ -96,6 +123,7 @@ class WBF_Auth {
'avatar_url' => $avatar,
));
if ( session_id() ) session_regenerate_id( true );
$_SESSION[ self::SESSION_KEY ] = $id;
return array('success'=>true,'user'=>WBF_DB::get_user($id));
}
@@ -104,10 +132,14 @@ class WBF_Auth {
self::init();
$user_id = $_SESSION[ self::SESSION_KEY ] ?? 0;
unset( $_SESSION[ self::SESSION_KEY ] );
// 2FA-Pending-State ebenfalls löschen
if ( class_exists('WBF_TOTP') ) {
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
unset( $_SESSION['wbf_2fa_remember'] );
}
if ( $user_id ) {
WBF_DB::delete_remember_token( (int)$user_id );
}
// Remove cookie
if ( isset($_COOKIE['wbf_remember']) ) {
setcookie( 'wbf_remember', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
}

View File

@@ -123,12 +123,23 @@ class WBF_BBCode {
$s
);
// [size=small|large|xlarge]
// [size=small|large|xlarge] oder [size=17] (klassisches BBCode)
$s = preg_replace_callback(
'/\[size=(small|large|xlarge)\](.*?)\[\/size\]/is',
'/\[size=([a-zA-Z0-9]+)\](.*?)\[\/size\]/is',
function ( $m ) {
$map = [ 'small' => '.8em', 'large' => '1.2em', 'xlarge' => '1.5em' ];
return '<span style="font-size:' . $map[$m[1]] . '">' . $m[2] . '</span>';
$val = strtolower( $m[1] );
// Benannte Größen
$named = [ 'small' => '.8em', 'large' => '1.2em', 'xlarge' => '1.5em' ];
if ( isset( $named[ $val ] ) ) {
$size = $named[ $val ];
// Numerische Größen 17 (klassisches BBCode-Schema)
} elseif ( ctype_digit( $val ) && (int)$val >= 1 && (int)$val <= 7 ) {
$num_map = [ 1 => '.7em', 2 => '.85em', 3 => '1em', 4 => '1.2em', 5 => '1.4em', 6 => '1.6em', 7 => '2em' ];
$size = $num_map[ (int)$val ];
} else {
return $m[2]; // Unbekannter Wert → nur Text
}
return '<span style="font-size:' . $size . '">' . $m[2] . '</span>';
},
$s
);

View File

@@ -151,7 +151,6 @@ class WBF_DB {
dbDelta( $sql_threads );
dbDelta( $sql_posts );
dbDelta( $sql_likes );
dbDelta( $sql_reports );
dbDelta( $sql_tags );
dbDelta( $sql_thread_tags );
dbDelta( $sql_messages );
@@ -174,6 +173,8 @@ class WBF_DB {
// Zeitlich begrenzte Sperren
self::maybe_add_column("{$wpdb->prefix}forum_users", 'ban_until', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN ban_until DATETIME DEFAULT NULL");
self::maybe_add_column("{$wpdb->prefix}forum_users", 'pre_ban_role', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN pre_ban_role VARCHAR(20) DEFAULT 'member'");
// Profilbanner
self::maybe_add_column("{$wpdb->prefix}forum_users", 'banner_url', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN banner_url VARCHAR(255) DEFAULT ''");
// Thread-Abonnements
$sql_subscriptions = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_subscriptions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -199,7 +200,6 @@ class WBF_DB {
) $charset;";
// Ensure reports + notifications tables exist on existing installs
dbDelta( $sql_reports );
dbDelta( $sql_notifications );
// Einladungs-Tabelle
@@ -354,6 +354,67 @@ class WBF_DB {
public static function update_user( $id, $data ) {
global $wpdb;
$wpdb->update("{$wpdb->prefix}forum_users", $data, ['id' => $id]);
// --- Discord-Rollen-Sync nach Rollenänderung ---
if (isset($data['role'])) {
// Discord-User-ID holen
$discord_user_id = $wpdb->get_var($wpdb->prepare(
"SELECT meta_value FROM {$wpdb->prefix}forum_user_meta WHERE user_id = %d AND meta_key = 'discord_user_id'",
$id
));
if ($discord_user_id) {
// Einstellungen laden
$s = function_exists('wbf_get_settings') ? wbf_get_settings() : [];
$token = trim($s['discord_bot_token'] ?? '');
$guild = trim($s['discord_guild_id'] ?? '');
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
if ($token && $guild && !empty($role_map)) {
// Ziel-Discord-Rolle anhand Mapping finden
$target_discord_role = null;
foreach ($role_map as $dc_role_id => $forum_role) {
if ($forum_role === $data['role']) {
$target_discord_role = (string)$dc_role_id;
break;
}
}
if ($target_discord_role) {
// Aktuelle Rollen des Users abrufen
$res = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}/members/{$discord_user_id}", [
'timeout' => 6,
'headers' => ['Authorization' => 'Bot ' . $token],
]);
if (!is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200) {
$member = json_decode(wp_remote_retrieve_body($res), true);
$user_roles = $member['roles'] ?? [];
// Alle gemappten Discord-Rollen entfernen, außer Zielrolle
$remove_roles = [];
foreach ($role_map as $dc_role_id => $forum_role) {
if ((string)$dc_role_id !== $target_discord_role && in_array((string)$dc_role_id, $user_roles, true)) {
$remove_roles[] = (string)$dc_role_id;
}
}
// Zielrolle hinzufügen, falls nicht vorhanden
if (!in_array($target_discord_role, $user_roles, true)) {
$user_roles[] = $target_discord_role;
}
// Entfernte Rollen rausnehmen
$user_roles = array_values(array_diff($user_roles, $remove_roles));
// PATCH an Discord senden
$body = json_encode(['roles' => array_values($user_roles)]);
wp_remote_request("https://discord.com/api/v10/guilds/{$guild}/members/{$discord_user_id}", [
'method' => 'PATCH',
'timeout' => 6,
'headers' => [
'Authorization' => 'Bot ' . $token,
'Content-Type' => 'application/json',
],
'body' => $body,
]);
}
}
}
}
}
}
public static function get_all_users( $limit = 100, $offset = 0 ) {
@@ -490,7 +551,7 @@ class WBF_DB {
}
// Move post_count contribution too
$post_count = (int)$wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id
));
if ( $post_count > 0 ) {
$wpdb->query($wpdb->prepare(
@@ -512,7 +573,7 @@ class WBF_DB {
FROM {$wpdb->prefix}forum_threads t
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id
WHERE t.id = %d", $id
WHERE t.id = %d AND t.deleted_at IS NULL", $id
));
}
@@ -541,10 +602,24 @@ class WBF_DB {
) );
}
}
// Posts zählen, User-IDs sammeln
$posts = $wpdb->get_results($wpdb->prepare("SELECT user_id FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $id));
$post_count = count($posts);
$user_post_counts = [];
foreach ($posts as $p) {
$uid = (int)$p->user_id;
if (!isset($user_post_counts[$uid])) $user_post_counts[$uid] = 0;
$user_post_counts[$uid]++;
}
// Posts löschen
$wpdb->delete("{$wpdb->prefix}forum_posts", ['thread_id' => $id]);
$wpdb->delete("{$wpdb->prefix}forum_threads", ['id' => $id]);
// Zähler anpassen
if ( $thread->status !== 'archived' ) {
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d", $thread->category_id));
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0), post_count=GREATEST(post_count-%d,0) WHERE id=%d", $post_count, $thread->category_id));
}
foreach ($user_post_counts as $uid => $cnt) {
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=GREATEST(post_count-%d,0) WHERE id=%d", $cnt, $uid));
}
}
@@ -572,7 +647,7 @@ class WBF_DB {
public static function count_posts( $thread_id ) {
global $wpdb;
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id));
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id));
}
public static function create_post( $data ) {
@@ -643,8 +718,8 @@ class WBF_DB {
public static function get_stats() {
global $wpdb;
return [
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived'"),
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts"),
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived' AND deleted_at IS NULL"),
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL"),
'members' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users"),
'newest' => $wpdb->get_var("SELECT display_name FROM {$wpdb->prefix}forum_users ORDER BY registered DESC LIMIT 1"),
];
@@ -731,9 +806,23 @@ class WBF_DB {
// ── Suche ─────────────────────────────────────────────────────────────────
public static function search( $query, $limit = 30 ) {
public static function search( $query, $limit = 30, $user = null ) {
global $wpdb;
$like = '%' . $wpdb->esc_like( $query ) . '%';
// Kategorie-Sichtbarkeit: Gäste und Member dürfen keine privaten Kategorien sehen
$user_level = $user ? WBF_Roles::level( $user->role ) : -99;
if ( $user_level >= 50 ) {
// Moderatoren+ sehen alles (inkl. soft-deleted ist extra)
$cat_filter = '';
} elseif ( $user ) {
// Eingeloggte Member/VIP: nur guest_visible oder eigene Rolle reicht
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role IN ('member','vip'))";
} else {
// Gäste: nur komplett öffentliche Kategorien
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role = 'member')";
}
return $wpdb->get_results( $wpdb->prepare(
"SELECT 'thread' AS result_type,
t.id, t.title, t.content, t.created_at, t.reply_count,
@@ -742,7 +831,9 @@ class WBF_DB {
FROM {$wpdb->prefix}forum_threads t
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
WHERE (t.title LIKE %s OR t.content LIKE %s) AND t.status != 'archived'
WHERE (t.title LIKE %s OR t.content LIKE %s)
AND t.status != 'archived' AND t.deleted_at IS NULL
$cat_filter
UNION ALL
SELECT 'post' AS result_type,
p.id, t.title, p.content, p.created_at, 0 AS reply_count,
@@ -752,7 +843,9 @@ class WBF_DB {
JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id
JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
WHERE p.content LIKE %s AND t.status != 'archived'
WHERE p.content LIKE %s
AND p.deleted_at IS NULL AND t.status != 'archived' AND t.deleted_at IS NULL
$cat_filter
ORDER BY created_at DESC
LIMIT %d",
$like, $like, $like, $limit
@@ -778,6 +871,8 @@ class WBF_DB {
'object_id' => $object_id,
'actor_id' => $actor_id,
] );
// MC Bridge: Ingame-Benachrichtigung auslösen wenn Spieler verknüpft ist
do_action( 'wbf_notification_created', $user_id, $type, $object_id, $actor_id );
}
public static function get_notifications( $user_id, $limit = 20 ) {
@@ -1172,12 +1267,13 @@ class WBF_DB {
public static function create_remember_token( $user_id ) {
global $wpdb;
$token = bin2hex( random_bytes(32) );
$token_hash = hash('sha256', $token);
$expires = date('Y-m-d H:i:s', strtotime('+30 days'));
// Delete existing tokens for this user first
$wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", ['user_id' => $user_id] );
$wpdb->insert( "{$wpdb->prefix}forum_remember_tokens", [
'user_id' => $user_id,
'token' => $token,
'token' => $token_hash,
'expires_at' => $expires,
] );
return $token;
@@ -1187,10 +1283,11 @@ class WBF_DB {
global $wpdb;
$table = "{$wpdb->prefix}forum_remember_tokens";
if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return null;
$token_hash = hash('sha256', sanitize_text_field($token));
return $wpdb->get_row( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->prefix}forum_remember_tokens
WHERE token=%s AND expires_at > NOW()",
sanitize_text_field($token)
$token_hash
) );
}
@@ -1379,11 +1476,25 @@ class WBF_DB {
public static function soft_delete_post( $post_id ) {
global $wpdb;
// Soft-Delete setzen
$wpdb->update(
"{$wpdb->prefix}forum_posts",
['deleted_at' => current_time('mysql')],
['id' => (int)$post_id]
);
// Zähler anpassen
$post = $wpdb->get_row($wpdb->prepare("SELECT thread_id, user_id FROM {$wpdb->prefix}forum_posts WHERE id=%d", $post_id));
if ($post) {
// Thread reply_count -1
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET reply_count=GREATEST(reply_count-1,0) WHERE id=%d", $post->thread_id));
// User post_count -1
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=GREATEST(post_count-1,0) WHERE id=%d", $post->user_id));
// Kategorie post_count -1
$cat_id = $wpdb->get_var($wpdb->prepare("SELECT category_id FROM {$wpdb->prefix}forum_threads WHERE id=%d", $post->thread_id));
if ($cat_id) {
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET post_count=GREATEST(post_count-1,0) WHERE id=%d", $cat_id));
}
}
}
public static function restore_thread( $thread_id ) {
@@ -1476,6 +1587,25 @@ class WBF_DB {
update_option( 'wbf_profile_fields', $fields );
}
public static function get_profile_field_categories() {
$cats = get_option( 'wbf_profile_field_cats', null );
if ( $cats === null ) {
// Default-Kategorien beim ersten Aufruf
$defaults = [
[ 'id' => 'cat_allgemein', 'name' => 'Allgemein', 'icon' => '👤' ],
[ 'id' => 'cat_kontakt', 'name' => 'Kontakt', 'icon' => '✉️' ],
[ 'id' => 'cat_social', 'name' => 'Social Media', 'icon' => '🌐' ],
];
update_option( 'wbf_profile_field_cats', $defaults );
return $defaults;
}
return is_array( $cats ) ? $cats : [];
}
public static function save_profile_field_categories( $cats ) {
update_option( 'wbf_profile_field_cats', $cats );
}
public static function get_user_meta( $user_id ) {
global $wpdb;
$rows = $wpdb->get_results( $wpdb->prepare(
@@ -1487,6 +1617,18 @@ class WBF_DB {
return $out;
}
/**
* Gibt einen einzelnen Meta-Wert zurück (oder leeren String wenn nicht vorhanden).
*/
public static function get_user_meta_single( $user_id, $key ) {
global $wpdb;
$value = $wpdb->get_var( $wpdb->prepare(
"SELECT meta_value FROM {$wpdb->prefix}forum_user_meta WHERE user_id = %d AND meta_key = %s LIMIT 1",
(int) $user_id, $key
) );
return $value !== null ? $value : '';
}
public static function set_user_meta( $user_id, $key, $value ) {
global $wpdb;
$wpdb->replace(

View File

@@ -103,6 +103,7 @@ class WBF_Export {
case 'settings':
$data['settings'] = get_option( 'wbf_settings', [] );
$data['profile_fields'] = get_option( 'wbf_profile_fields', [] );
$data['profile_field_cats'] = get_option( 'wbf_profile_field_cats', [] );
$data['reactions_cfg'] = get_option( 'wbf_reactions', [] );
$data['word_filter'] = get_option( 'wbf_word_filter', '' );
break;
@@ -129,7 +130,7 @@ class WBF_Export {
case 'users':
$data['users'] = $wpdb->get_results(
"SELECT id, username, email, password, display_name, avatar_url,
"SELECT id, username, email, password, display_name, avatar_url, banner_url,
bio, signature, role, pre_ban_role, ban_reason, ban_until,
post_count, registered, last_active, profile_public,
reset_token, reset_token_expires
@@ -275,6 +276,7 @@ class WBF_Export {
}
if ( isset( $data['profile_fields'] ) ) {
update_option( 'wbf_profile_fields', $data['profile_fields'] );
if ( isset($data['profile_field_cats']) ) update_option( 'wbf_profile_field_cats', $data['profile_field_cats'] );
$log[] = '✅ Profilfeld-Definitionen (' . count( $data['profile_fields'] ) . ') importiert.';
}
if ( isset( $data['reactions_cfg'] ) && is_array( $data['reactions_cfg'] ) ) {
@@ -1172,7 +1174,7 @@ class WBF_Export {
/** Prüft ob eine Tabelle existiert */
private static function table_exists( string $table ): bool {
global $wpdb;
return $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) === $table;
return $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table ) ) === $table;
}
/** Erstellt ein standardisiertes Ergebnis-Array */

View File

@@ -43,12 +43,12 @@ class WBF_Levels {
return $defaults;
}
$levels = (array) $saved;
usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
usort( $levels, function($a, $b) { return (int)$a['min'] <=> (int)$b['min']; } );
return $levels;
}
public static function save( $levels ) {
usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
usort( $levels, function($a, $b) { return (int)$a['min'] <=> (int)$b['min']; } );
update_option( self::OPTION_KEY, $levels );
}

View File

@@ -0,0 +1,480 @@
<?php
/**
* WBF_MC_Bridge — Minecraft ↔ Forum Verknüpfung & Ingame-Benachrichtigungen
*
* Dieses Modul verbindet das WP Business Forum mit dem BungeeCord StatusAPI Plugin.
*
* Features:
* - Account-Verknüpfung: Forum-User ↔ MC-UUID (über Token-System)
* - Push-Benachrichtigungen: Neue Antwort/Erwähnung/PN → Ingame-Nachricht
* - REST API Endpoints für die BungeeCord-Seite
*
* Einbindung in wp-business-forum.php:
* require_once WBF_PATH . 'includes/class-forum-mc-bridge.php';
*
* Konfiguration in WBF-Einstellungen (Admin → Forum → Einstellungen):
* mc_bridge_enabled = true/false
* mc_bridge_api_url = http://server-ip:9191 (StatusAPI URL)
* mc_bridge_api_secret = Shared Secret für API-Authentifizierung
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class WBF_MC_Bridge {
/** Meta-Keys in forum_user_meta */
const META_MC_UUID = 'mc_uuid';
const META_MC_NAME = 'mc_name';
const META_LINK_TOKEN = 'mc_link_token';
const META_LINK_EXPIRY = 'mc_link_token_expires';
/**
* Hooks registrieren — wird beim Plugin-Laden aufgerufen.
*/
public static function init() {
// Hook: Wird in der modifizierten WBF_DB::create_notification() gefeuert
add_action( 'wbf_notification_created', [ __CLASS__, 'on_notification' ], 10, 4 );
// REST API Endpoints für BungeeCord
add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] );
// AJAX: Token generieren (für eingeloggte Forum-User)
add_action( 'wp_ajax_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
add_action( 'wp_ajax_nopriv_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
// AJAX: Verknüpfung lösen
add_action( 'wp_ajax_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
add_action( 'wp_ajax_nopriv_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
// AJAX: Link-Status prüfen (Polling im Profil nach Token-Generierung)
add_action( 'wp_ajax_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
add_action( 'wp_ajax_nopriv_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
}
// ══════════════════════════════════════════════════════════════════════════
// ── Einstellungen ─────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Prüft ob die MC-Bridge aktiviert ist.
*/
public static function is_enabled() {
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
return ! empty( $s['mc_bridge_enabled'] );
}
/**
* Gibt die StatusAPI-URL zurück (z.B. http://192.168.1.100:9191).
*/
private static function get_api_url() {
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
return rtrim( $s['mc_bridge_api_url'] ?? '', '/' );
}
/**
* Gibt das Shared Secret zurück.
*/
private static function get_api_secret() {
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
return $s['mc_bridge_api_secret'] ?? '';
}
// ══════════════════════════════════════════════════════════════════════════
// ── Notification Hook ─────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Wird aufgerufen wenn eine Forum-Notification erstellt wird.
* Prüft ob der Empfänger eine MC-UUID hat und pusht die Nachricht an BungeeCord.
*
* @param int $user_id Forum-User ID des Empfängers
* @param string $type Typ: 'reply', 'mention', 'message'
* @param int $object_id Thread-ID (bei reply/mention) oder Message-ID (bei message)
* @param int $actor_id Forum-User ID des Auslösers
*/
public static function on_notification( $user_id, $type, $object_id, $actor_id ) {
if ( ! self::is_enabled() ) return;
$api_url = self::get_api_url();
if ( empty( $api_url ) ) return;
// MC-UUID des Empfängers prüfen
$mc_uuid = WBF_DB::get_user_meta_single( $user_id, self::META_MC_UUID );
if ( empty( $mc_uuid ) ) return;
// Actor-Info laden
$actor = WBF_DB::get_user( (int) $actor_id );
$actor_name = $actor ? $actor->display_name : 'Unbekannt';
// Kontext-Daten sammeln
$title = '';
$url = '';
$forum_url = wbf_get_forum_url();
switch ( $type ) {
case 'reply':
case 'mention':
$thread = WBF_DB::get_thread( (int) $object_id );
if ( $thread ) {
$title = $thread->title;
$url = $forum_url . '?forum_thread=' . (int) $thread->id;
}
break;
case 'message':
$title = 'Neue Privatnachricht';
$url = $forum_url . '?forum_dm=1';
break;
}
// Push an BungeeCord senden
self::push_to_bungee( $mc_uuid, $type, $title, $actor_name, $url, $user_id );
}
/**
* Sendet die Benachrichtigung per HTTP POST an den BungeeCord StatusAPI Server.
*/
private static function push_to_bungee( $mc_uuid, $type, $title, $author, $url, $wp_user_id ) {
$api_url = self::get_api_url();
$secret = self::get_api_secret();
$payload = wp_json_encode( [
'player_uuid' => $mc_uuid,
'type' => $type,
'title' => $title,
'author' => $author,
'url' => $url,
'wp_user_id' => (int) $wp_user_id,
] );
$args = [
'method' => 'POST',
'timeout' => 5,
'blocking' => false, // Non-blocking — Seite wartet nicht auf Antwort
'headers' => [
'Content-Type' => 'application/json; charset=UTF-8',
'X-Api-Key' => $secret,
],
'body' => $payload,
'sslverify' => false, // Lokales Netzwerk braucht kein SSL
];
wp_remote_post( $api_url . '/forum/notify', $args );
}
// ══════════════════════════════════════════════════════════════════════════
// ── REST API (für BungeeCord → WordPress) ─────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
public static function register_rest_routes() {
// POST /wp-json/mc-bridge/v1/verify-link
// BungeeCord schickt Token + MC-UUID → WP verifiziert und speichert
register_rest_route( 'mc-bridge/v1', '/verify-link', [
'methods' => 'POST',
'callback' => [ __CLASS__, 'rest_verify_link' ],
'permission_callback' => '__return_true',
] );
// POST /wp-json/mc-bridge/v1/unlink
// BungeeCord kann Verknüpfung auch von der MC-Seite lösen
register_rest_route( 'mc-bridge/v1', '/unlink', [
'methods' => 'POST',
'callback' => [ __CLASS__, 'rest_unlink' ],
'permission_callback' => '__return_true',
] );
// GET /wp-json/mc-bridge/v1/status
// Verbindungstest
register_rest_route( 'mc-bridge/v1', '/status', [
'methods' => 'GET',
'callback' => [ __CLASS__, 'rest_status' ],
'permission_callback' => '__return_true',
] );
}
/**
* REST: Verknüpfung bestätigen.
* BungeeCord sendet: { "token": "...", "mc_uuid": "...", "mc_name": "..." }
*/
public static function rest_verify_link( $request ) {
// Rate Limiting: max 10 Versuche pro IP pro Minute
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$limit_key = 'wbf_mc_link_' . md5($ip);
$attempts = (int) get_transient($limit_key);
if ($attempts >= 10) {
return new WP_REST_Response([
'success' => false,
'error' => 'rate_limited',
'message' => 'Zu viele Versuche. Bitte warte eine Minute.'
], 429);
}
set_transient($limit_key, $attempts + 1, 60);
// API-Secret prüfen
$secret = self::get_api_secret();
if ( ! empty( $secret ) ) {
$provided = $request->get_header( 'X-Api-Key' );
if ( $provided !== $secret ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 );
}
}
$token = sanitize_text_field( $request->get_param( 'token' ) );
$mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) );
$mc_name = sanitize_text_field( $request->get_param( 'mc_name' ) );
if ( empty( $token ) || empty( $mc_uuid ) ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_fields' ], 400 );
}
// Token in forum_user_meta suchen
global $wpdb;
$meta_row = $wpdb->get_row( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
self::META_LINK_TOKEN, $token
) );
if ( ! $meta_row ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'invalid_token' ], 404 );
}
$forum_user_id = (int) $meta_row->user_id;
// Ablauf prüfen
$expiry_meta = WBF_DB::get_user_meta( $forum_user_id );
$expiry = $expiry_meta[ self::META_LINK_EXPIRY ] ?? '0';
if ( (int) $expiry < time() ) {
// Token abgelaufen — aufräumen
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' );
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' );
return new WP_REST_Response( [ 'success' => false, 'error' => 'token_expired' ], 410 );
}
// Prüfen ob diese MC-UUID bereits mit einem anderen Account verknüpft ist
$existing = $wpdb->get_row( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
self::META_MC_UUID, $mc_uuid
) );
if ( $existing && (int) $existing->user_id !== $forum_user_id ) {
return new WP_REST_Response( [
'success' => false,
'error' => 'uuid_already_linked',
'message' => 'Diese Minecraft-UUID ist bereits mit einem anderen Forum-Account verknüpft.',
], 409 );
}
// Verknüpfung speichern
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, $mc_uuid );
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, $mc_name );
// Token aufräumen
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' );
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' );
// Forum-User-Info für BungeeCord zurückgeben
$forum_user = WBF_DB::get_user( $forum_user_id );
return new WP_REST_Response( [
'success' => true,
'forum_user_id' => $forum_user_id,
'display_name' => $forum_user ? $forum_user->display_name : '',
'username' => $forum_user ? $forum_user->username : '',
], 200 );
}
/**
* REST: Verknüpfung lösen (von BungeeCord-Seite).
*/
public static function rest_unlink( $request ) {
$secret = self::get_api_secret();
if ( ! empty( $secret ) ) {
$provided = $request->get_header( 'X-Api-Key' );
if ( $provided !== $secret ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 );
}
}
$mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) );
if ( empty( $mc_uuid ) ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_mc_uuid' ], 400 );
}
global $wpdb;
$meta_row = $wpdb->get_row( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
self::META_MC_UUID, $mc_uuid
) );
if ( ! $meta_row ) {
return new WP_REST_Response( [ 'success' => false, 'error' => 'not_linked' ], 404 );
}
$forum_user_id = (int) $meta_row->user_id;
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, '' );
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, '' );
return new WP_REST_Response( [ 'success' => true ], 200 );
}
/**
* REST: Status-Endpoint für Verbindungstest.
*/
public static function rest_status( $request ) {
return new WP_REST_Response( [
'success' => true,
'enabled' => self::is_enabled(),
'version' => defined( 'WBF_VERSION' ) ? WBF_VERSION : '?',
'plugin' => 'WP Business Forum',
], 200 );
}
// ══════════════════════════════════════════════════════════════════════════
// ── AJAX: Token generieren ────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Generiert einen 8-stelligen Verknüpfungs-Token (15 Minuten gültig).
* Der User gibt diesen Token dann ingame mit /forumlink <token> ein.
*/
public static function ajax_generate_token() {
check_ajax_referer( 'wbf_nonce', 'nonce' );
$user = WBF_Auth::get_current_user();
if ( ! $user ) {
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] );
}
// Prüfen ob bereits verknüpft
$existing_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
if ( ! empty( $existing_uuid ) ) {
$mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME );
wp_send_json_error( [
'message' => 'Dein Account ist bereits mit ' . esc_html( $mc_name ?: $existing_uuid ) . ' verknüpft.',
'linked' => true,
'mc_name' => $mc_name,
'mc_uuid' => $existing_uuid,
] );
}
// Token generieren: 8 Zeichen, alphanumerisch, uppercase
$token = strtoupper( substr( bin2hex( random_bytes( 5 ) ), 0, 8 ) );
$expiry = time() + ( 15 * 60 ); // 15 Minuten
WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, $token );
WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, (string) $expiry );
wp_send_json_success( [
'token' => $token,
'expires_in' => 15, // Minuten
'command' => '/forumlink ' . $token,
] );
}
/**
* AJAX: Verknüpfung lösen (von der Forum-Seite).
*/
public static function ajax_unlink() {
check_ajax_referer( 'wbf_nonce', 'nonce' );
$user = WBF_Auth::get_current_user();
if ( ! $user ) {
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] );
}
$mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
// Meta löschen
WBF_DB::set_user_meta( $user->id, self::META_MC_UUID, '' );
WBF_DB::set_user_meta( $user->id, self::META_MC_NAME, '' );
WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, '' );
WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, '' );
// Optional: BungeeCord informieren
if ( ! empty( $mc_uuid ) && self::is_enabled() ) {
$api_url = self::get_api_url();
$secret = self::get_api_secret();
if ( ! empty( $api_url ) ) {
wp_remote_post( $api_url . '/forum/unlink', [
'method' => 'POST',
'timeout' => 3,
'blocking' => false,
'headers' => [
'Content-Type' => 'application/json',
'X-Api-Key' => $secret,
],
'body' => wp_json_encode( [ 'mc_uuid' => $mc_uuid ] ),
'sslverify' => false,
] );
}
}
wp_send_json_success( [ 'message' => 'Minecraft-Verknüpfung wurde aufgehoben.' ] );
}
/**
* AJAX: Verknüpfungs-Status prüfen.
* Wird vom Frontend-Polling alle 5 Sekunden nach Token-Generierung aufgerufen.
* Gibt zurück ob der User bereits verknüpft ist (BungeeCord hat verify-link gesendet).
*/
public static function ajax_link_status() {
check_ajax_referer( 'wbf_nonce', 'nonce' );
$user = WBF_Auth::get_current_user();
if ( ! $user ) {
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.', 'linked' => false ] );
return;
}
$mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
$mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME );
if ( ! empty( $mc_uuid ) ) {
wp_send_json_success( [
'linked' => true,
'mc_uuid' => $mc_uuid,
'mc_name' => $mc_name,
] );
} else {
wp_send_json_success( [ 'linked' => false ] );
}
}
// ══════════════════════════════════════════════════════════════════════════
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Gibt die MC-UUID eines Forum-Users zurück (oder leer).
*/
public static function get_mc_uuid( $forum_user_id ) {
return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_UUID );
}
/**
* Gibt den MC-Namen eines Forum-Users zurück (oder leer).
*/
public static function get_mc_name( $forum_user_id ) {
return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_NAME );
}
/**
* Prüft ob ein Forum-User mit MC verknüpft ist.
*/
public static function is_linked( $forum_user_id ) {
$uuid = self::get_mc_uuid( $forum_user_id );
return ! empty( $uuid );
}
/**
* HTML-Badge für Profilansicht: Zeigt MC-Verknüpfung an.
*/
public static function profile_badge( $forum_user_id ) {
if ( ! self::is_enabled() ) return '';
$mc_name = self::get_mc_name( $forum_user_id );
if ( empty( $mc_name ) ) return '';
$name_esc = esc_html( $mc_name );
$head_url = 'https://mc-heads.net/avatar/' . urlencode( $mc_name ) . '/24';
return "<span class=\"wbf-mc-badge\" title=\"Minecraft: {$name_esc}\">"
. "<img src=\"{$head_url}\" alt=\"\" width=\"16\" height=\"16\" style=\"border-radius:2px;vertical-align:middle;margin-right:4px\">"
. "<span style=\"color:#55ff55\">{$name_esc}</span>"
. "</span>";
}
}
// Initialisierung
add_action( 'init', [ 'WBF_MC_Bridge', 'init' ], 20 );

View File

@@ -108,7 +108,7 @@ class WBF_Roles {
/** Nach Level sortiert (höchstes zuerst) */
public static function get_sorted() {
$all = self::get_all();
uasort($all, fn($a,$b) => $b['level'] <=> $a['level']);
uasort($all, function($a, $b) { return $b['level'] <=> $a['level']; });
return $all;
}
@@ -192,20 +192,37 @@ class WBF_Roles {
];
}
/** Ist der aktuelle WP-User der Seiteninhaber (Superadmin)? */
public static function is_wp_superadmin() {
return current_user_can('administrator') || (is_multisite() && is_super_admin());
/**
* Gibt die WP-User-ID des echten Superadmins zurück.
* Das ist immer der bei der WordPress-Installation angelegte erste Nutzer (ID 1).
* Alle anderen WP-Administratoren sind KEINE Forum-Superadmins.
*/
public static function get_wp_superadmin_id() {
// Primär: gespeicherte ID aus den Plugin-Einstellungen (falls manuell überschrieben)
$saved_id = (int) get_option( 'wbf_superadmin_wp_id', 0 );
if ( $saved_id > 0 ) return $saved_id;
// Fallback: WP-Installations-User (ID 1)
return 1;
}
/** Superadmin-Status erzwingen: Forum-User des WP-Admins immer auf superadmin setzen */
/** Ist der aktuelle eingeloggte WP-User der echte Superadmin (nur ID 1 bzw. gespeicherte ID)? */
public static function is_wp_superadmin() {
if ( ! is_user_logged_in() ) return false;
return get_current_user_id() === self::get_wp_superadmin_id();
}
/**
* Superadmin-Status erzwingen — aber NUR für den einen echten WP-Superadmin (ID 1).
* Alle anderen WP-Admins können normale Forum-Rollen haben und behalten diese auch.
*/
public static function sync_superadmin() {
if ( ! is_user_logged_in() ) return;
if ( ! self::is_wp_superadmin() ) return;
if ( ! self::is_wp_superadmin() ) return; // nur ID 1 kommt durch
$wp_user = wp_get_current_user();
$forum_user = WBF_DB::get_user_by('email', $wp_user->user_email);
$wp_user = wp_get_current_user();
$forum_user = WBF_DB::get_user_by( 'email', $wp_user->user_email );
if ( $forum_user && $forum_user->role !== self::SUPERADMIN ) {
WBF_DB::update_user($forum_user->id, ['role' => self::SUPERADMIN]);
WBF_DB::update_user( $forum_user->id, [ 'role' => self::SUPERADMIN ] );
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
/**
* WBF_TOTP — RFC 6238 Time-based One-Time Password (TOTP)
*
* Keine externe Bibliothek nötig — reines PHP 7.0+.
* Kompatibel mit: Google Authenticator, Aegis, Authy, Bitwarden, 2FAS, etc.
*
* Secrets werden in forum_user_meta gespeichert (meta_key = 'totp_secret').
* Kein Schema-Change an der Haupt-Usertabelle nötig.
*/
class WBF_TOTP {
const DIGITS = 6;
const PERIOD = 30; // Sekunden pro Schritt
const WINDOW = 1; // ±1 Step Toleranz (= ±30 s Uhrabweichung OK)
const SECRET_LEN = 20; // Bytes → 32 Base32-Zeichen
// Meta-Keys
const META_SECRET = 'totp_secret';
const META_PENDING = 'totp_secret_pending';
// Session-Key für ausstehenden Login
const SESSION_PENDING = 'wbf_2fa_pending';
// ── Secret ──────────────────────────────────────────────────────────────
/**
* Erzeugt einen neuen, kryptografisch sicheren Base32-Secret.
* @return string z.B. "JBSWY3DPEBLW64TMMQ======"
*/
public static function generate_secret() {
return self::base32_encode( random_bytes( self::SECRET_LEN ) );
}
// ── Verifikation ──────────────────────────────────────────────────────────
/**
* Prüft ob $code für $secret zum aktuellen Zeitfenster passt.
*
* @param string $secret Base32-Secret des Users
* @param string $code 6-stelliger Code aus der Authenticator-App
* @param int $window Anzahl Steps Toleranz (default = 1 = ±30 s)
* @return bool
*/
public static function verify( $secret, $code, $window = self::WINDOW ) {
// Leerzeichen tolerieren (z.B. "123 456")
$code = preg_replace( '/\s+/', '', (string) $code );
if ( strlen($code) !== self::DIGITS ) return false;
if ( ! ctype_digit($code) ) return false;
$key = self::base32_decode( $secret );
if ( empty($key) ) return false;
$ts = (int) floor( time() / self::PERIOD );
for ( $i = -$window; $i <= $window; $i++ ) {
$expected = self::hotp( $key, $ts + $i );
// Timing-safe Vergleich
if ( hash_equals( $expected, $code ) ) return true;
}
return false;
}
// ── HOTP-Kern (RFC 4226) ─────────────────────────────────────────────────
private static function hotp( $key, $counter ) {
// 64-bit Big-Endian Counter
$msg = pack( 'N', 0 ) . pack( 'N', $counter );
$hash = hash_hmac( 'sha1', $msg, $key, true );
$offset = ord( $hash[19] ) & 0x0f;
$code = (
( ord($hash[$offset ]) & 0x7f ) << 24 |
( ord($hash[$offset + 1]) & 0xff ) << 16 |
( ord($hash[$offset + 2]) & 0xff ) << 8 |
( ord($hash[$offset + 3]) & 0xff )
) % ( 10 ** self::DIGITS );
return str_pad( (string) $code, self::DIGITS, '0', STR_PAD_LEFT );
}
// ── otpauth:// URI ────────────────────────────────────────────────────────
/**
* Gibt die otpauth:// URI zurück — wird vom QR-Code-Generator verwendet.
*
* @param string $username Forum-Benutzername
* @param string $secret Base32-Secret
* @param string|null $issuer Anzeigename in der App (default: Blogname)
* @return string
*/
public static function get_otpauth_uri( $username, $secret, $issuer = null ) {
if ( ! $issuer ) {
$issuer = html_entity_decode( get_bloginfo('name'), ENT_QUOTES ) ?: 'WP Business Forum';
}
$label = rawurlencode( $issuer . ':' . $username );
return 'otpauth://totp/' . $label . '?'
. 'secret=' . rawurlencode( $secret )
. '&issuer=' . rawurlencode( $issuer )
. '&algorithm=SHA1'
. '&digits=' . self::DIGITS
. '&period=' . self::PERIOD;
}
// ── User-Helfer ───────────────────────────────────────────────────────────
/** Ist 2FA für diesen User aktiv? */
public static function is_enabled_for( $user_id ) {
$s = WBF_DB::get_user_meta_single( (int) $user_id, self::META_SECRET );
return ! empty( $s );
}
/**
* 2FA für einen User deaktivieren (löscht Secret + ggf. pending Secret).
* Kann von Admin und User selbst (nach Verifikation) aufgerufen werden.
*/
public static function disable_for( $user_id ) {
global $wpdb;
$uid = (int) $user_id;
$wpdb->delete(
"{$wpdb->prefix}forum_user_meta",
[ 'user_id' => $uid, 'meta_key' => self::META_SECRET ],
[ '%d', '%s' ]
);
$wpdb->delete(
"{$wpdb->prefix}forum_user_meta",
[ 'user_id' => $uid, 'meta_key' => self::META_PENDING ],
[ '%d', '%s' ]
);
}
// ── Base32 ────────────────────────────────────────────────────────────────
private static $b32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
public static function base32_encode( $input ) {
$output = '';
$buf = 0;
$buf_bits = 0;
for ( $i = 0, $len = strlen($input); $i < $len; $i++ ) {
$buf = ( $buf << 8 ) | ord( $input[$i] );
$buf_bits += 8;
while ( $buf_bits >= 5 ) {
$buf_bits -= 5;
$output .= self::$b32[ ( $buf >> $buf_bits ) & 0x1f ];
}
}
if ( $buf_bits > 0 ) {
$output .= self::$b32[ ( $buf << ( 5 - $buf_bits ) ) & 0x1f ];
}
// Padding to multiple of 8
while ( strlen($output) % 8 !== 0 ) $output .= '=';
return $output;
}
public static function base32_decode( $input ) {
// Leerzeichen & Padding entfernen, Uppercase
$input = strtoupper( preg_replace( '/\s+/', '', $input ) );
$input = rtrim( $input, '=' );
$map = array_flip( str_split( self::$b32 ) );
$output = '';
$buf = 0;
$bits = 0;
for ( $i = 0, $len = strlen($input); $i < $len; $i++ ) {
if ( ! isset( $map[ $input[$i] ] ) ) continue; // ungültiges Zeichen ignorieren
$buf = ( $buf << 5 ) | $map[ $input[$i] ];
$bits += 5;
if ( $bits >= 8 ) {
$bits -= 8;
$output .= chr( ( $buf >> $bits ) & 0xff );
}
}
return $output;
}
}

View File

@@ -0,0 +1,32 @@
<?php
// Ingame-Benachrichtigung via StatusAPI
if ( ! defined( 'ABSPATH' ) ) exit;
function wbf_notify_ingame($player, $message) {
// Einstellungen laden
$settings = function_exists('wbf_get_settings') ? wbf_get_settings() : [];
$enabled = !empty($settings['mc_bridge_enabled']);
$api_url = trim($settings['mc_bridge_api_url'] ?? '');
$api_secret = trim($settings['mc_bridge_api_secret'] ?? '');
if (!$enabled || !$api_url || !$api_secret) {
return;
}
$url = rtrim($api_url, '/') . '/notify-pn';
$data = [
'player' => $player,
'message' => $message
];
$args = [
'body' => json_encode($data),
'headers' => [
'Content-Type' => 'application/json',
'X-API-Key' => $api_secret,
],
'timeout' => 2,
'data_format' => 'body',
];
wp_remote_post($url, $args);
}

View File

@@ -1,114 +1,115 @@
<?php
/**
* WP Business Forum — Uninstaller
* Wird automatisch aufgerufen wenn das Plugin über WP-Admin gelöscht wird.
* Entfernt: alle DB-Tabellen, wp_options, Transients, Cron-Jobs, Upload-Verzeichnis.
*/
// Sicherheits-Check — nur via WordPress-Uninstall erlaubt
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
global $wpdb;
// ── 1. Alle Datenbank-Tabellen löschen ───────────────────────────────────────
// Reihenfolge beachten: abhängige Tabellen zuerst (Foreign Keys)
$tables = [
'forum_poll_votes',
'forum_polls',
'forum_reactions',
'forum_notifications',
'forum_subscriptions',
'forum_bookmarks', // ← fehlte: Lesezeichen
'forum_ignore_list', // ← Ignore/Block-Liste
'forum_invites',
'forum_thread_tags',
'forum_tags',
'forum_prefixes', // ← fehlte: Thread-Präfixe
'forum_reports',
'forum_likes',
'forum_messages',
'forum_remember_tokens',
'forum_user_meta',
'forum_posts',
'forum_threads',
'forum_categories',
'forum_users',
];
foreach ( $tables as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}{$table}`" );
}
// ── 2. Alle wp_options löschen ───────────────────────────────────────────────
$options = [
'wbf_settings',
'wbf_custom_roles',
'wbf_level_config',
'wbf_levels_enabled',
'wbf_profile_fields',
'wbf_reactions',
'wbf_forum_page_id',
'wbf_superadmin_email',
'wbf_db_version',
'wbf_word_filter',
];
foreach ( $options as $option ) {
delete_option( $option );
}
// Multisite: Netzwerk-Optionen ebenfalls entfernen
if ( is_multisite() ) {
foreach ( $options as $option ) {
delete_site_option( $option );
}
}
// ── 3. Transients löschen ────────────────────────────────────────────────────
delete_transient( 'wbf_activation_redirect' );
delete_transient( 'wbf_stats_cache' );
delete_transient( 'wbf_update_check' );
// Alle wbf_* Transients per LIKE-Query entfernen (inkl. Update-Dismissed-Transients)
$wpdb->query(
"DELETE FROM `{$wpdb->options}`
WHERE `option_name` LIKE '_transient_wbf_%'
OR `option_name` LIKE '_transient_timeout_wbf_%'"
);
// ── 4. Geplante Cron-Jobs entfernen ──────────────────────────────────────────
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
wp_clear_scheduled_hook( 'wbf_check_for_updates' );
// ── 5. Forum-Seite löschen (vom Setup-Wizard erstellt) ───────────────────────
$forum_page_id = get_option( 'wbf_forum_page_id' );
if ( $forum_page_id ) {
wp_delete_post( (int) $forum_page_id, true ); // true = dauerhaft löschen
}
// ── 6. Upload-Unterverzeichnis entfernen ─────────────────────────────────────
$upload_dir = wp_upload_dir();
$wbf_dir = trailingslashit( $upload_dir['basedir'] ) . 'wbf-avatars';
if ( is_dir( $wbf_dir ) ) {
wbf_uninstall_rmdir( $wbf_dir );
}
/**
* Hilfsfunktion: Verzeichnis rekursiv löschen.
* Nur innerhalb des WP-Upload-Verzeichnisses erlaubt.
*/
function wbf_uninstall_rmdir( $dir ) {
$upload_base = wp_upload_dir()['basedir'];
// Sicherheitscheck: nur Unterverzeichnisse von uploads/ löschen
if ( strpos( realpath( $dir ), realpath( $upload_base ) ) !== 0 ) {
return;
}
$files = array_diff( scandir( $dir ), [ '.', '..' ] );
foreach ( $files as $file ) {
$path = $dir . DIRECTORY_SEPARATOR . $file;
is_dir( $path ) ? wbf_uninstall_rmdir( $path ) : unlink( $path );
}
rmdir( $dir );
<?php
/**
* WP Business Forum — Uninstaller
* Wird automatisch aufgerufen wenn das Plugin über WP-Admin gelöscht wird.
* Entfernt: alle DB-Tabellen, wp_options, Transients, Cron-Jobs, Upload-Verzeichnis.
*/
// Sicherheits-Check — nur via WordPress-Uninstall erlaubt
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
global $wpdb;
// ── 1. Alle Datenbank-Tabellen löschen ───────────────────────────────────────
// Reihenfolge beachten: abhängige Tabellen zuerst (Foreign Keys)
$tables = [
'forum_poll_votes',
'forum_polls',
'forum_reactions',
'forum_notifications',
'forum_subscriptions',
'forum_bookmarks', // ← fehlte: Lesezeichen
'forum_ignore_list', // ← Ignore/Block-Liste
'forum_invites',
'forum_thread_tags',
'forum_tags',
'forum_prefixes', // ← fehlte: Thread-Präfixe
'forum_reports',
'forum_likes',
'forum_messages',
'forum_remember_tokens',
'forum_user_meta',
'forum_posts',
'forum_threads',
'forum_categories',
'forum_users',
];
foreach ( $tables as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}{$table}`" );
}
// ── 2. Alle wp_options löschen ───────────────────────────────────────────────
$options = [
'wbf_settings',
'wbf_custom_roles',
'wbf_level_config',
'wbf_levels_enabled',
'wbf_profile_fields',
'wbf_profile_field_cats',
'wbf_reactions',
'wbf_forum_page_id',
'wbf_superadmin_email',
'wbf_db_version',
'wbf_word_filter',
];
foreach ( $options as $option ) {
delete_option( $option );
}
// Multisite: Netzwerk-Optionen ebenfalls entfernen
if ( is_multisite() ) {
foreach ( $options as $option ) {
delete_site_option( $option );
}
}
// ── 3. Transients löschen ────────────────────────────────────────────────────
delete_transient( 'wbf_activation_redirect' );
delete_transient( 'wbf_stats_cache' );
delete_transient( 'wbf_update_check' );
// Alle wbf_* Transients per LIKE-Query entfernen (inkl. Update-Dismissed-Transients)
$wpdb->query(
"DELETE FROM `{$wpdb->options}`
WHERE `option_name` LIKE '_transient_wbf_%'
OR `option_name` LIKE '_transient_timeout_wbf_%'"
);
// ── 4. Geplante Cron-Jobs entfernen ──────────────────────────────────────────
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
wp_clear_scheduled_hook( 'wbf_check_for_updates' );
// ── 5. Forum-Seite löschen (vom Setup-Wizard erstellt) ───────────────────────
$forum_page_id = get_option( 'wbf_forum_page_id' );
if ( $forum_page_id ) {
wp_delete_post( (int) $forum_page_id, true ); // true = dauerhaft löschen
}
// ── 6. Upload-Unterverzeichnis entfernen ─────────────────────────────────────
$upload_dir = wp_upload_dir();
$wbf_dir = trailingslashit( $upload_dir['basedir'] ) . 'wbf-avatars';
if ( is_dir( $wbf_dir ) ) {
wbf_uninstall_rmdir( $wbf_dir );
}
/**
* Hilfsfunktion: Verzeichnis rekursiv löschen.
* Nur innerhalb des WP-Upload-Verzeichnisses erlaubt.
*/
function wbf_uninstall_rmdir( $dir ) {
$upload_base = wp_upload_dir()['basedir'];
// Sicherheitscheck: nur Unterverzeichnisse von uploads/ löschen
if ( strpos( realpath( $dir ), realpath( $upload_base ) ) !== 0 ) {
return;
}
$files = array_diff( scandir( $dir ), [ '.', '..' ] );
foreach ( $files as $file ) {
$path = $dir . DIRECTORY_SEPARATOR . $file;
is_dir( $path ) ? wbf_uninstall_rmdir( $path ) : unlink( $path );
}
rmdir( $dir );
}

View File

@@ -3,17 +3,18 @@
* Plugin Name: WP Business Forum
* Plugin URI: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum
* Description: Professionelles Forum mit eigenem Login, Rollen, Signaturen, Hierarchie und Moderations-Tools.
* Version: 1.0.2
* Version: 1.0.5
* Author: M_Viper
* Author URI: https://m-viper.de
* Text Domain: wp-business-forum
* Requires PHP: 7.0
*/
if ( ! defined( 'ABSPATH' ) ) exit;
define( 'WBF_PATH', plugin_dir_path( __FILE__ ) );
define( 'WBF_URL', plugin_dir_url( __FILE__ ) );
define( 'WBF_VERSION', '1.0.2' );
define( 'WBF_VERSION', '1.0.5' );
require_once WBF_PATH . 'includes/class-forum-db.php';
require_once WBF_PATH . 'includes/class-forum-roles.php';
@@ -23,6 +24,9 @@ require_once WBF_PATH . 'includes/class-forum-auth.php';
require_once WBF_PATH . 'includes/class-forum-shortcodes.php';
require_once WBF_PATH . 'includes/class-forum-ajax.php';
require_once WBF_PATH . 'includes/class-forum-export.php';
require_once WBF_PATH . 'includes/class-forum-mc-bridge.php';
require_once WBF_PATH . 'includes/class-forum-totp.php';
require_once WBF_PATH . 'includes/forum-statusapi.php';
require_once WBF_PATH . 'admin/forum-admin.php';
require_once WBF_PATH . 'admin/forum-settings.php';
require_once WBF_PATH . 'admin/forum-setup.php';
@@ -39,6 +43,33 @@ add_action( 'plugins_loaded', function() {
WBF_Export::hooks();
}, 5 );
// ── DB-Schema sicherstellen (läuft bei jedem Seitenaufruf, sehr günstig) ─────
// Stellt sicher dass neue Spalten auch auf bestehenden Installs vorhanden sind,
// ohne dass das Plugin erneut deaktiviert/aktiviert werden muss.
add_action( 'plugins_loaded', function() {
$db_ver = (int) get_option( 'wbf_db_version', 0 );
if ( $db_ver < 2 ) {
global $wpdb;
// profile_public: Sicherheits-kritisch — muss immer existieren
$cols = $wpdb->get_col( "DESCRIBE {$wpdb->prefix}forum_users" );
if ( ! in_array( 'profile_public', $cols ) ) {
$wpdb->query( "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) NOT NULL DEFAULT 1" );
// Alle bestehenden User explizit auf öffentlich setzen
$wpdb->query( "UPDATE {$wpdb->prefix}forum_users SET profile_public = 1 WHERE profile_public IS NULL" );
}
update_option( 'wbf_db_version', 2 );
}
}, 10 );
// ── Session frühzeitig starten (PHP 8.3 Fix) ────────────────────────────────
// session_start() MUSS vor jedem HTML-Output laufen.
// plugins_loaded (Prio 1) ist der früheste sichere Zeitpunkt in WordPress.
// Der 'init'-Hook (in class-forum-auth.php) läuft als Fallback weiterhin,
// aber dieser frühe Aufruf verhindert den PHP 8.3 E_WARNING "headers already sent".
add_action( 'plugins_loaded', function() {
WBF_Auth::init();
}, 1 );
// ── Superadmin-Sync ───────────────────────────────────────────────────────────
add_action( 'wp_login', function() { WBF_Roles::sync_superadmin(); } );
add_action( 'init', function() { WBF_Roles::sync_superadmin(); } );
@@ -75,14 +106,13 @@ function wbf_get_forum_url() {
$url = get_permalink( $page_id );
if ( $url ) return $url;
}
// 2. Fallback: Seite mit [business_forum] Shortcode suchen
$pages = get_posts([
'post_type' => 'page',
'post_status' => 'publish',
'posts_per_page' => 1,
's' => 'business_forum',
]);
if ( $pages ) return get_permalink( $pages[0]->ID );
// 2. Fallback: Seite mit [business_forum] Shortcode suchen (direkt im post_content)
global $wpdb;
$page_id = $wpdb->get_var( $wpdb->prepare(
"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status = 'publish' AND post_content LIKE %s LIMIT 1",
'%[business_forum]%'
) );
if ( $page_id ) return get_permalink( $page_id );
// 3. Letzter Fallback: aktuelle Seite
return home_url('/');
}
@@ -91,6 +121,10 @@ function wbf_get_forum_url() {
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_style( 'wbf-style', WBF_URL . 'assets/css/forum-style.css', [], WBF_VERSION );
wp_enqueue_script( 'wbf-script', WBF_URL . 'assets/js/forum-script.js', ['jquery'], WBF_VERSION, true );
// 2FA: QR-Code-Bibliothek (nur laden wenn User eingeloggt oder auf Loginseite)
wp_enqueue_script( 'qrcodejs', 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js', [], '1.0.0', true );
wp_add_inline_script( 'wbf-script', wbf_get_2fa_inline_js(), 'after' );
$wbf_user = WBF_Auth::get_current_user();
if ( $wbf_user ) {
WBF_DB::touch_last_active( $wbf_user->id );
@@ -257,4 +291,302 @@ add_action( 'admin_init', function() {
delete_transient( WBF_UPDATE_TRANSIENT );
wp_safe_redirect( remove_query_arg(['wbf_refresh_update','_wpnonce']) );
exit;
} );
} );
// ══════════════════════════════════════════════════════════════════════════════
// ── 2FA Inline-JavaScript ─────────────────────────────────────────────────────
// Liefert das JS für: Login-2FA-Step, Profil-Setup-Wizard, Deaktivierung
// ══════════════════════════════════════════════════════════════════════════════
function wbf_get_2fa_inline_js() {
return <<<'JS'
(function ($) {
'use strict';
/* ══════════════════════════════════════════════════════════════
2FA — Login-Flow
Wenn der Server 2fa_required:true zurückgibt, zeigt das
Login-Formular eine Code-Eingabe anstatt die Fehlermeldung.
══════════════════════════════════════════════════════════════ */
// Original-Login-Handler überschreiben um 2FA abzufangen
$(document).off('click', '.wbf-login-submit-btn');
$(document).on('click', '.wbf-login-submit-btn', function () {
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
var $box = $(this).closest('.wbf-auth-box');
// 2FA-Panel verstecken falls sichtbar
$box.find('.wbf-2fa-login-step').remove();
$.post(WBF.ajax_url, {
action: 'wbf_login',
nonce: WBF.nonce,
username: $box.find('.wbf-field-username').val(),
password: $box.find('.wbf-field-password').val(),
remember_me: $box.find('.wbf-field-remember').is(':checked') ? '1' : ''
}, function (res) {
if (res && res.success) {
location.reload();
} else if (res && res.data && res.data['2fa_required']) {
// 2FA erforderlich — Code-Eingabe einblenden
$btn.prop('disabled', false).html('<i class="fas fa-sign-in-alt"></i> Einloggen');
wbfShow2faLoginStep($box);
} else {
var msg = (res && res.data && res.data.message) ? res.data.message : 'Fehler';
$box.find('.wbf-login-msg').text(msg).css('color', '#f05252').show();
setTimeout(function () { $box.find('.wbf-login-msg').fadeOut(); }, 4000);
$btn.prop('disabled', false).html('<i class="fas fa-sign-in-alt"></i> Einloggen');
}
}, 'json').fail(function (xhr) {
$box.find('.wbf-login-msg').text('Verbindungsfehler (' + xhr.status + ')').css('color', '#f05252').show();
$btn.prop('disabled', false).html('<i class="fas fa-sign-in-alt"></i> Einloggen');
});
});
function wbfShow2faLoginStep($box) {
// Altes Modal entfernen falls vorhanden
$('#wbf2faLoginModal').remove();
var modal =
'<div id="wbf2faLoginModal" class="wbf-2fa-modal-overlay">' +
'<div class="wbf-2fa-modal-box">' +
'<div class="wbf-2fa-modal-header">' +
'<span class="wbf-2fa-modal-icon">🛡️</span>' +
'<div>' +
'<strong class="wbf-2fa-modal-title">Zwei-Faktor-Authentifizierung</strong>' +
'<p class="wbf-2fa-modal-sub">Gib den Code aus deiner Authenticator-App ein.</p>' +
'</div>' +
'</div>' +
'<input type="text" class="wbf-2fa-code-input" placeholder="1 2 3 4 5 6"' +
' maxlength="7" inputmode="numeric" autocomplete="one-time-code">' +
'<div class="wbf-2fa-modal-actions">' +
'<button class="wbf-btn wbf-btn--primary wbf-2fa-submit-btn">' +
'<i class="fas fa-check"></i> Bestätigen' +
'</button>' +
'<button class="wbf-btn wbf-2fa-cancel-btn">' +
'<i class="fas fa-xmark"></i> Abbrechen' +
'</button>' +
'</div>' +
'<span class="wbf-2fa-msg"></span>' +
'</div>' +
'</div>';
$('body').append(modal);
// Kurze Verzögerung für CSS-Transition
setTimeout(function () {
$('#wbf2faLoginModal').addClass('wbf-2fa-modal--visible');
$('#wbf2faLoginModal .wbf-2fa-code-input').focus();
}, 20);
}
// 2FA-Code absenden
$(document).on('click', '.wbf-2fa-submit-btn', function () {
var $step = $(this).closest('.wbf-2fa-modal-box');
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
var code = $step.find('.wbf-2fa-code-input').val().replace(/\s+/g, '');
if (code.length !== 6) {
$step.find('.wbf-2fa-msg').text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show();
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Code bestätigen');
return;
}
$.post(WBF.ajax_url, {
action: 'wbf_2fa_verify_login',
code: code
}, function (res) {
if (res && res.success) {
location.reload();
} else {
var msg = (res && res.data && res.data.message) ? res.data.message : 'Ungültiger Code.';
$step.find('.wbf-2fa-msg').text(msg).css('color', '#f05252').show();
$step.find('.wbf-2fa-code-input').val('').focus();
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Code bestätigen');
}
}, 'json').fail(function (xhr) {
$step.find('.wbf-2fa-msg').text('Verbindungsfehler.').css('color', '#f05252').show();
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Code bestätigen');
});
});
// Enter-Taste im Code-Feld
$(document).on('keydown', '.wbf-2fa-code-input', function (e) {
if (e.key === 'Enter') $(this).closest('.wbf-2fa-login-step').find('.wbf-2fa-submit-btn').click();
});
// Abbrechen: 2FA-Modal schließen
$(document).on('click', '.wbf-2fa-cancel-btn', function () {
var $modal = $('#wbf2faLoginModal');
$modal.removeClass('wbf-2fa-modal--visible');
setTimeout(function () { $modal.remove(); }, 250);
});
// Klick außerhalb des Modals schließt es
$(document).on('click', '#wbf2faLoginModal', function (e) {
if ($(e.target).is('#wbf2faLoginModal')) {
var $modal = $(this);
$modal.removeClass('wbf-2fa-modal--visible');
setTimeout(function () { $modal.remove(); }, 250);
}
});
/* ══════════════════════════════════════════════════════════════
2FA — Profil-Setup-Wizard
══════════════════════════════════════════════════════════════ */
// Schritt 1 starten: Secret + QR generieren
$(document).on('click', '#wbf2faStartBtn', function () {
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Lädt…');
$.post(WBF.ajax_url, {
action: 'wbf_2fa_setup_begin',
nonce: WBF.nonce
}, function (res) {
if (!res || !res.success) {
$btn.prop('disabled', false).html('<i class="fas fa-shield-halved"></i> 2FA einrichten');
alert((res && res.data && res.data.message) ? res.data.message : 'Fehler');
return;
}
var secret = res.data.secret;
var uri = res.data.uri;
// QR-Code rendern (qrcodejs)
$('#wbf2faQr').empty();
if (typeof QRCode !== 'undefined') {
// QR-Code in isolierten Wrapper einbetten (kein Flex-Kontext)
var qrEl = document.getElementById('wbf2faQr');
qrEl.innerHTML = '';
new QRCode(qrEl, {
text: uri,
width: 200,
height: 200,
correctLevel: QRCode.CorrectLevel.M
});
// Kein JS-Eingriff nötig — CSS übernimmt Größe + img-Verstecken
} else {
// Fallback: Link anzeigen
$('#wbf2faQr').html(
'<a href="' + uri + '" style="font-size:.75rem;word-break:break-all">otpauth Link</a>'
);
}
// Secret für manuelle Eingabe formatiert anzeigen (Leerzeichen alle 4 Zeichen)
var fmt = secret.replace(/=/g, '').replace(/(.{4})/g, '$1 ').trim();
$('#wbf2faSecret').text(fmt);
// Panels tauschen
$('#wbf2faInactive').hide();
$('#wbf2faStep1').fadeIn(200);
}, 'json').fail(function () {
$btn.prop('disabled', false).html('<i class="fas fa-shield-halved"></i> 2FA einrichten');
alert('Verbindungsfehler. Bitte Seite neu laden.');
});
});
// Weiter zu Schritt 2
$(document).on('click', '#wbf2faToStep2', function () {
$('#wbf2faStep1').hide();
$('#wbf2faStep2').fadeIn(200);
$('#wbf2faVerifyCode').focus();
});
// Zurück zu Schritt 1
$(document).on('click', '#wbf2faBackBtn', function () {
$('#wbf2faStep2').hide();
$('#wbf2faStep1').fadeIn(200);
});
// Schritt 2: Code bestätigen und 2FA aktivieren
$(document).on('click', '#wbf2faVerifyBtn', function () {
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
var $msg = $('#wbf2faVerifyMsg');
var code = $('#wbf2faVerifyCode').val().replace(/\s+/g, '');
if (code.length !== 6) {
$msg.text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show();
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen & aktivieren');
return;
}
$.post(WBF.ajax_url, {
action: 'wbf_2fa_setup_verify',
nonce: WBF.nonce,
code: code
}, function (res) {
if (res && res.success) {
$('#wbf2faStep2').hide();
$('#wbf2faStep3').fadeIn(300);
// Badge im Header aktualisieren
$('#wbf2faCard .wbf-2fa-badge')
.removeClass('wbf-2fa-badge--off')
.addClass('wbf-2fa-badge--on')
.html('<i class="fas fa-check-circle"></i> Aktiv');
// Nach 2 Sek. Seite neu laden damit der Header-Status stimmt
setTimeout(function () { location.reload(); }, 2500);
} else {
var msg = (res && res.data && res.data.message) ? res.data.message : 'Ungültiger Code.';
$msg.text(msg).css('color', '#f05252').show();
$('#wbf2faVerifyCode').val('').focus();
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen & aktivieren');
}
}, 'json').fail(function () {
$msg.text('Verbindungsfehler.').css('color', '#f05252').show();
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen & aktivieren');
});
});
// Enter-Taste im Verifikationsfeld
$(document).on('keydown', '#wbf2faVerifyCode', function (e) {
if (e.key === 'Enter') $('#wbf2faVerifyBtn').click();
});
/* ══════════════════════════════════════════════════════════════
2FA — Deaktivierung (Profil)
══════════════════════════════════════════════════════════════ */
$(document).on('click', '#wbf2faDisableBtn', function () {
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
var $msg = $('#wbf2faDisableMsg');
var pw = $('#wbf2faDisablePw').val();
var code = $('#wbf2faDisableCode').val().replace(/\s+/g, '');
if (!pw) {
$msg.text('Bitte Passwort eingeben.').css('color', '#f05252').show();
$btn.prop('disabled', false)
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
return;
}
if (code.length !== 6) {
$msg.text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show();
$btn.prop('disabled', false)
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
return;
}
$.post(WBF.ajax_url, {
action: 'wbf_2fa_disable',
nonce: WBF.nonce,
password: pw,
code: code
}, function (res) {
if (res && res.success) {
$msg.text('✔ ' + (res.data.message || '2FA deaktiviert.')).css('color', '#56cf7e').show();
setTimeout(function () { location.reload(); }, 1500);
} else {
var msg = (res && res.data && res.data.message) ? res.data.message : 'Fehler.';
$msg.text(msg).css('color', '#f05252').show();
$('#wbf2faDisableCode').val('').focus();
$btn.prop('disabled', false)
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
}
}, 'json').fail(function () {
$msg.text('Verbindungsfehler.').css('color', '#f05252').show();
$btn.prop('disabled', false)
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
});
});
}(jQuery));
JS;
}