Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3583a6231 | |||
| f4d0ec73c0 | |||
| 56f8c01b52 | |||
| 0efd52d893 | |||
| 605df075cd | |||
| 94f1ac46aa | |||
| 8c2955a2cf | |||
| 689fd0c77b | |||
| e2c4e31b4b | |||
| bd87c795f9 | |||
| 989db0786a | |||
| 4a593677dd | |||
| 43fcc6cb95 | |||
| 1c229ab72b | |||
| 781dbf9f41 | |||
| 3b7fd16301 | |||
| 3ea89e9841 |
686
README.md
686
README.md
@@ -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
89
admin/forum-settings-mc-section.php
Normal file
89
admin/forum-settings-mc-section.php
Normal 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>
|
||||
@@ -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 ); ?>
|
||||
<<?php echo esc_html( $sa_wpuser->user_email ); ?>>
|
||||
</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 <token></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 <token></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">
|
||||
|
||||
@@ -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
@@ -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:
|
||||
@@ -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' ] );
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -123,12 +123,23 @@ class WBF_BBCode {
|
||||
$s
|
||||
);
|
||||
|
||||
// [size=small|large|xlarge]
|
||||
// [size=small|large|xlarge] oder [size=1–7] (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 1–7 (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
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
|
||||
480
includes/class-forum-mc-bridge.php
Normal file
480
includes/class-forum-mc-bridge.php
Normal 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 );
|
||||
@@ -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
179
includes/class-forum-totp.php
Normal file
179
includes/class-forum-totp.php
Normal 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;
|
||||
}
|
||||
}
|
||||
32
includes/forum-statusapi.php
Normal file
32
includes/forum-statusapi.php
Normal 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);
|
||||
}
|
||||
227
uninstall.php
227
uninstall.php
@@ -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 );
|
||||
}
|
||||
@@ -1,19 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* 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
|
||||
* Author: M_Viper
|
||||
* Author URI: https://m-viper.de
|
||||
* Text Domain: wp-business-forum
|
||||
*/
|
||||
/*
|
||||
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.5
|
||||
Author: M_Viper
|
||||
Author URI: https://m-viper.de
|
||||
Requires at least: 6.8
|
||||
Tested up to: 6.8
|
||||
PHP Version: 7.4
|
||||
License: GPL2
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
Text Domain: wp-business-forum
|
||||
Tags: forum, community, roles, moderation, shortcode, bbcode, admin, frontend, security, export-import
|
||||
Support: [Discord Support](https://discord.com/invite/FdRs4BRd8D)
|
||||
Support: [Telegram Support](https://t.me/M_Viper04)
|
||||
*/
|
||||
|
||||
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 +31,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 +50,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 +113,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 +128,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 +298,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;
|
||||
}
|
||||
Reference in New Issue
Block a user