22 Commits
1.0.1 ... main

Author SHA1 Message Date
f4d0ec73c0 Update from Git Manager GUI 2026-03-30 20:41:51 +02:00
56f8c01b52 Update from Git Manager GUI 2026-03-30 20:41:49 +02:00
0efd52d893 Update from Git Manager GUI 2026-03-30 20:41:48 +02:00
605df075cd Upload via Git Manager GUI - wp-business-forum.php 2026-03-30 18:41:41 +00:00
94f1ac46aa Upload via Git Manager GUI - uninstall.php 2026-03-30 18:41:40 +00:00
8c2955a2cf Update from Git Manager GUI 2026-03-29 22:25:43 +02:00
689fd0c77b Update from Git Manager GUI 2026-03-29 22:25:41 +02:00
e2c4e31b4b Upload via Git Manager GUI - wp-business-forum.php 2026-03-29 20:25:41 +00:00
bd87c795f9 Upload via Git Manager GUI - uninstall.php 2026-03-29 20:25:40 +00:00
989db0786a Upload via Git Manager GUI - README.md 2026-03-29 20:25:40 +00:00
4a593677dd Update from Git Manager GUI 2026-03-29 22:25:37 +02:00
43fcc6cb95 Update from Git Manager GUI 2026-03-29 13:41:29 +02:00
1c229ab72b Upload via Git Manager GUI - wp-business-forum.php 2026-03-29 11:41:28 +00:00
781dbf9f41 Upload via Git Manager GUI - uninstall.php 2026-03-29 11:41:28 +00:00
3b7fd16301 Update from Git Manager GUI 2026-03-29 13:41:26 +02:00
3ea89e9841 Update from Git Manager GUI 2026-03-29 13:41:24 +02:00
65d2371239 Update from Git Manager GUI 2026-03-22 00:40:18 +01:00
ead2f3a62a Update from Git Manager GUI 2026-03-22 00:40:17 +01:00
dfdc74bcf9 Update from Git Manager GUI 2026-03-22 00:40:15 +01:00
44672a61aa Upload file uninstall.php via GUI 2026-03-22 00:40:09 +01:00
2adba16d29 Upload file wp-business-forum.php via GUI 2026-03-22 00:40:00 +01:00
290279df1c README.md aktualisiert 2026-03-21 23:39:28 +00:00
20 changed files with 8674 additions and 815 deletions

580
README.md
View File

@@ -1,290 +1,290 @@
# 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!
# WP Business Forum - Anwender README
WP Business Forum bringt ein modernes, eigenständiges Community-Forum direkt in deine WordPress-Website.
Statt auf externe Plattformen auszuweichen, bleiben Diskussionen, Support-Anfragen und Mitgliederaktivität
zentral auf deiner eigenen Seite - inklusive voller Kontrolle über Inhalte, Rollen und Moderation.
Diese Dokumentation richtet sich an Betreiber, Moderatoren und Community-Manager, die das Forum
schnell einrichten, sicher betreiben und im Alltag effizient verwalten möchten. Von der ersten
Installation bis zum Live-Betrieb findest du hier alle wichtigen Schritte und Funktionen kompakt erklärt.
Wenn du eine professionelle Community mit klaren Rechten, direkter Nutzerkommunikation und
strukturierter Moderation aufbauen willst, ist WP Business Forum dafür ausgelegt.
## Inhalt
1. Über das Plugin
2. Funktionsübersicht
3. Voraussetzungen
4. Installation
5. Ersteinrichtung (Setup-Wizard)
6. Forum-Seite einbinden
7. Bedienung im Frontend (Mitglieder)
8. Moderation und Verwaltung
9. Einstellungen im Detail
10. Export, Import und Deinstallation
11. FAQ / Troubleshooting
## 1) Über das Plugin
WP Business Forum ist ein eigenständiges Foren-System für WordPress mit:
- eigenem Forum-Login (unabhängig vom WP-Login)
- Rollen- und Rechteverwaltung
- Kategorien mit Hierarchie
- Moderationswerkzeugen
- Direktnachrichten, Benachrichtigungen, Meldesystem
- Umfragen, Tags, Reaktionen, Lesezeichen
Das Forum wird per Shortcode in eine WordPress-Seite eingebunden.
## 2) Funktionsübersicht
### Für Mitglieder
- Registrieren / Einloggen / Logout
- Passwort vergessen und Reset per E-Mail
- Threads erstellen, antworten, bearbeiten
- Likes und Emoji-Reaktionen
- Tags und Thread-Präfixe
- Umfragen erstellen und abstimmen
- Lesezeichen setzen
- Nutzer erwähnen mit @mention
- Private Nachrichten (DM)
- Profil mit Avatar, Bio, Signatur und eigenen Profilfeldern
- Mitgliederliste und Suchfunktion
### Für Moderation / Admin
- Threads pinnen, schließen, archivieren, verschieben, löschen
- Beiträge löschen
- Meldungen (Reports) bearbeiten
- Kategorien und Rollen verwalten
- Einladungssystem für Registrierung
- Wartungsmodus
- Wortfilter
- Statistiken
- Papierkorb / Wiederherstellung
- Export / Import
## 3) Voraussetzungen
- Laufende WordPress-Installation
- Schreibrechte für WordPress-Uploads (für Avatar-/Bild-Uploads)
- Funktionierende E-Mail-Zustellung in WordPress (für Passwort-Reset und Benachrichtigungen)
Hinweis: Das Plugin nutzt eigene Datenbanktabellen (Präfix `wp_forum_*` bzw. mit deinem Tabellenpräfix).
## 4) Installation
1. Plugin-Ordner `wp-business-forum` in `wp-content/plugins/` kopieren.
2. Im WordPress-Backend unter Plugins aktivieren.
3. Nach der Aktivierung startet einmalig der Setup-Wizard.
## 5) Ersteinrichtung (Setup-Wizard)
Nach Aktivierung führt der Wizard durch 3 Schritte:
1. Superadmin-Konto erstellen oder bestehendes Forum-Konto hochstufen
2. Optional automatisch eine Forum-Seite erzeugen
3. Abschluss
Wichtig:
- Der Superadmin ist fest mit dem WordPress-Admin verknüpft.
- Wenn noch kein Superadmin existiert, erscheint im Backend ein Hinweisbanner.
## 6) Forum-Seite einbinden
Das Forum wird mit folgendem Shortcode auf einer WordPress-Seite angezeigt:
```text
[business_forum]
```
Empfohlen:
- Eine eigene Seite (z. B. "Forum") anlegen
- Nur diesen Shortcode als Seiteninhalt verwenden
## 7) Bedienung im Frontend (Mitglieder)
### 7.1 Registrierung und Login
- Registrierung kann offen, nur per Einladung oder deaktiviert sein.
- Optional müssen Nutzer die Forum-Regeln akzeptieren.
- Spam-Schutz bei Registrierung:
- Honeypot-Feld
- Mindestzeit bis Formular-Absenden
- Login unterstützt "Angemeldet bleiben" (Remember-Me Cookie).
### 7.2 Kategorien und Threads
- Kategorien können verschachtelt sein (Hauptkategorie + Unterkategorien).
- Sichtbarkeit kann rollenbasiert sein.
- Threads können folgende Zustände haben:
- offen
- geschlossen
- archiviert
- gepinnt
### 7.3 Thread erstellen
- Mindestlänge Titel: 5 Zeichen
- Mindestlänge Inhalt: 10 Zeichen (bei normalem Thread)
- Tags können vergeben werden
- Optional kann ein Thread-Präfix gesetzt werden
- Optional kann direkt eine Umfrage erstellt werden
### 7.4 Antworten und Bearbeiten
- Antworten mit BBCode-Unterstützung
- Flood-Control: konfigurierbare Wartezeit zwischen Posts
- Eigene Posts nur innerhalb des eingestellten Bearbeitungsfensters (z. B. 30 Minuten)
- Moderation kann unabhängig davon eingreifen
### 7.5 Umfragen
- Umfrage direkt beim Thread-Erstellen oder nachträglich im Thread
- 2 bis 10 Antwortoptionen
- Optional Mehrfachauswahl
- Optional Enddatum
- Nach Abstimmung werden Ergebnisse direkt angezeigt
### 7.6 Reaktionen, Likes, Lesezeichen
- Likes auf Thread/Beitrag
- Emoji-Reaktionen (adminseitig konfigurierbar)
- Lesezeichen für Threads (im Profil einsehbar)
### 7.7 Private Nachrichten (DM)
- 1:1 Nachrichten zwischen Mitgliedern
- Inbox-Ansicht und Konversation
- Ungelesene Nachrichten werden gezählt
- Optional E-Mail-Hinweis bei neuer Nachricht
### 7.8 Benachrichtigungen
Benachrichtigungen bei:
- Antworten auf abonnierte / relevante Threads
- @Erwähnungen
- neuen privaten Nachrichten
### 7.9 Profil
Mitglieder können:
- Anzeigenamen, Bio und Signatur pflegen
- Avatar hochladen
- Passwort ändern
- eigene Profil-Sichtbarkeit umschalten
- benutzerdefinierte Profilfelder ausfüllen (falls aktiviert)
Upload-Limits:
- Avatar: max. 2 MB (JPG/PNG/GIF/WebP)
- Bild im Beitrag: max. 5 MB (JPG/PNG/GIF/WebP)
### 7.10 Passwort vergessen
- Über "Passwort vergessen" kann ein Reset-Link per E-Mail angefordert werden.
- Das Zurücksetzen erfolgt über einen zeitlich gültigen Token.
## 8) Moderation und Verwaltung
Im WordPress-Backend gibt es den Menüpunkt "Business Forum" mit Unterseiten:
- Übersicht: Dashboard mit Kennzahlen und Aktivitäten
- Kategorien: Struktur und Sichtbarkeit verwalten
- Rollen: Rollen/Permissions anpassen
- Level: Beitragsbasierte Rangstufen
- Mitglieder: Nutzer verwalten
- Meldungen: gemeldete Inhalte bearbeiten
- Profilfelder: eigene Felder definieren
- Einstellungen: Texte, Sicherheit, Registrierung, Regeln, Wartung
- Reaktionen: erlaubte Emoji-Reaktionen
- Einladungen: Invite-Codes erstellen und verwalten
- Statistiken: Forum-Auswertung
- Papierkorb: gelöschte Inhalte wiederherstellen
- Thread-Präfixe: Label für Threads verwalten
- Wortfilter: unerwünschte Begriffe ersetzen/filtern
- Export / Import: Backup und Wiederherstellung
- Deinstallieren: komplette Löschung des Plugins inkl. Daten
## 9) Einstellungen im Detail
Unter Business Forum > Einstellungen:
### 9.1 Texte und UI
- Hero-Titel/Untertitel
- Topbar-Brand
- Label für Statistik
- Abschnittstitel
- Buttontexte
- Sidebar-Titel
### 9.2 Sicherheit
- Auto-Logout nach Inaktivität (0 = deaktiviert)
- Post-Bearbeitungslimit
- Spam-Mindestzeit bei Registrierung
- Flood-Control Intervall
- Profil-Sichtbarkeit (Standard)
### 9.3 Registrierung
- Modus:
- offen
- nur Einladung
- deaktiviert
- Freitext-Hinweis für Einladungsmode
### 9.4 Wartungsmodus
- Forum für normale Nutzer sperren
- Moderation/Admin behalten Zugriff
- Eigener Wartungs-Titel und Hinweistext
### 9.5 Forum-Regeln / Nutzungsbedingungen
- Regelseite aktivieren/deaktivieren
- Akzeptierung bei Registrierung optional verpflichtend
- Titel und Inhalt frei editierbar
## 10) Export, Import und Deinstallation
### 10.1 Export / Import
Exportierbare Bereiche (je nach Auswahl):
- Einstellungen
- Rollen und Level
- Kategorien
- Nutzer und User-Meta
- Threads und Posts
- Interaktionen (Likes/Reaktionen/Benachrichtigungen)
- Nachrichten
- Meldungen
- Einladungen
Empfehlung:
- Vor großen Änderungen immer einen Voll-Export speichern.
### 10.2 Deinstallation (wichtig)
Beim Löschen des Plugins werden komplett entfernt:
- alle Forum-Datenbanktabellen
- relevante Plugin-Optionen
- Transients
- geplanter Cron-Job
- automatisch erstellte Forum-Seite
- zugehörige Upload-Unterverzeichnisse
Das ist eine echte Datenlöschung. Vorher immer Backup erstellen.
## 11) FAQ / Troubleshooting
### Login funktioniert nicht
- Prüfen, ob das Konto gesperrt ist
- Bei zeitlicher Sperre Ablaufzeit abwarten
- Bei Registrierung "Nur Einladung" gültigen Invite-Code nutzen
### Registrierung nicht sichtbar
- In Einstellungen den Registrierungsmodus prüfen
- Bei deaktiviertem Modus ist keine Selbstregistrierung möglich
### Keine E-Mails kommen an
- WordPress-Mailversand prüfen (SMTP Plugin empfohlen)
- Admin-E-Mail in WordPress kontrollieren
### Upload von Bildern/Avatar scheitert
- Dateityp prüfen (nur JPG/PNG/GIF/WebP)
- Dateigröße prüfen (Avatar 2 MB, Beitrag 5 MB)
- Schreibrechte in Uploads prüfen
### Benutzer werden automatisch ausgeloggt
- Auto-Logout in den Forum-Einstellungen prüfen
### Forum ist plötzlich "offline"
- Wartungsmodus in den Einstellungen deaktivieren
### Suche liefert keine Ergebnisse
- Suchbegriff muss mindestens 2 Zeichen haben
---
## Kurz-Checkliste für den Live-Betrieb
1. Setup-Wizard abschließen
2. Forum-Seite mit `[business_forum]` bereitstellen
3. Rollen und Kategorien final konfigurieren
4. Registrierungsmodus festlegen
5. Regeln/Nutzungsbedingungen hinterlegen
6. E-Mail-Versand testen
7. Backup-Export erstellen
Viel Erfolg mit deinem Forum!

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -52,17 +52,63 @@ if ( ! function_exists('wbf_get_settings') ) {
'rules_accept_required' => '1',
'rules_title' => 'Forum-Regeln & Nutzungsbedingungen',
'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 );
}
}
// ── Admin-Seite ───────────────────────────────────────────────────────────────
/**
* Gibt ein Array der Rollen-Keys zurück die nicht geblockt/ignoriert werden können.
* Superadmin ist immer enthalten — unabhängig von der Einstellung.
*
* @return string[] z.B. ['superadmin', 'admin', 'moderator']
*/
if ( ! function_exists('wbf_get_ignore_blocked_roles') ) {
function wbf_get_ignore_blocked_roles() {
$raw = wbf_get_settings()['ignore_blocked_roles'] ?? 'superadmin,admin,moderator';
$keys = array_filter( array_map( 'trim', explode( ',', $raw ) ) );
// superadmin immer schützen
if ( ! in_array('superadmin', $keys, true) ) {
$keys[] = 'superadmin';
}
return array_values( $keys );
}
}
/**
* Prüft ob ein User ignoriert/geblockt werden darf.
*
* @param object $target Forum-User-Objekt
* @return bool true = darf ignoriert werden, false = nicht erlaubt
*/
if ( ! function_exists('wbf_can_be_ignored') ) {
function wbf_can_be_ignored( $target ) {
if ( ! $target ) return false;
$blocked_roles = wbf_get_ignore_blocked_roles();
return ! in_array( $target->role, $blocked_roles, true );
}
}
if ( ! function_exists('wbf_admin_settings') ) {
function wbf_admin_settings() {
@@ -96,7 +142,57 @@ 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(
array_map( 'sanitize_key', (array)( $_POST['ignore_blocked_roles'] ?? [] ) ),
$all_role_keys
);
// superadmin ist immer blockiert — kann nicht entfernt werden
if ( ! in_array('superadmin', $checked_roles, true) ) {
$checked_roles[] = 'superadmin';
}
$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>';
}
@@ -198,6 +294,40 @@ function wbf_admin_settings() {
🔒 Sicherheit
</h2>
<table class="form-table" role="presentation">
<!-- ── Superadmin WP-User-ID ─────────────────── -->
<tr>
<th scope="row">
<label for="wbf_superadmin_wp_id">Superadmin WordPress-User-ID</label>
</th>
<td>
<?php
$sa_id = (int) get_option( 'wbf_superadmin_wp_id', 1 );
$sa_wpuser = get_userdata( $sa_id );
?>
<input type="number" id="wbf_superadmin_wp_id" name="superadmin_wp_id"
value="<?php echo $sa_id; ?>"
min="1" step="1"
style="width:80px">
<?php if ( $sa_wpuser ) : ?>
<span style="margin-left:10px;color:#16a34a;font-weight:600">
✅ <?php echo esc_html( $sa_wpuser->display_name ); ?>
&lt;<?php echo esc_html( $sa_wpuser->user_email ); ?>&gt;
</span>
<?php else : ?>
<span style="margin-left:10px;color:#dc2626;font-weight:600">
⚠️ Kein WordPress-User mit dieser ID gefunden!
</span>
<?php endif; ?>
<p class="description">
Nur dieser WordPress-User erhält automatisch die Forum-Rolle <strong>Superadmin</strong>
und kann sie nicht verlieren. Alle anderen WordPress-Admins können normale Forum-Rollen
haben und im Mitglieder-Bereich frei zugewiesen werden.<br>
<em>Standard: 1 (erster bei der WP-Installation angelegter User)</em>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="wbf_auto_logout_minutes">Auto-Logout nach Inaktivität</label>
@@ -401,6 +531,325 @@ function wbf_admin_settings() {
</tr>
</table>
<!-- ── Ignore / Block-System ────────────────────────── -->
<h2 style="border-bottom:1px solid #ddd;padding-bottom:.4rem;margin-top:1.5rem">
🚫 Ignore / Block-System
</h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label>Nicht blockierbare Rollen</label></th>
<td>
<?php
$blocked_roles = array_filter( array_map('trim', explode(',', $s['ignore_blocked_roles'] ?? 'superadmin,admin,moderator')) );
$all_roles = WBF_Roles::get_sorted();
foreach ( $all_roles as $key => $role ):
$is_superadmin = ($key === 'superadmin');
$is_checked = in_array($key, $blocked_roles, true);
$rc = esc_attr($role['color']);
$rb = esc_attr($role['bg_color']);
?>
<label style="display:flex;align-items:center;gap:8px;margin-bottom:7px;cursor:<?php echo $is_superadmin?'not-allowed':'pointer'; ?>">
<input type="checkbox"
name="ignore_blocked_roles[]"
value="<?php echo esc_attr($key); ?>"
<?php checked($is_checked, true); ?>
<?php echo $is_superadmin ? 'disabled' : ''; ?>
style="width:16px;height:16px;accent-color:<?php echo $rc; ?>">
<?php if ($is_superadmin): ?>
<!-- superadmin immer als hidden mitschicken da disabled nicht übermittelt wird -->
<input type="hidden" name="ignore_blocked_roles[]" value="superadmin">
<?php endif; ?>
<span style="display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:20px;font-size:.78rem;font-weight:700;color:<?php echo $rc; ?>;background:<?php echo $rb; ?>;border:1px solid <?php echo $rc; ?>">
<i class="<?php echo esc_attr($role['icon'] ?? 'fas fa-user'); ?>"></i>
<?php echo esc_html($role['label']); ?>
</span>
<?php if ($is_superadmin): ?>
<span style="font-size:.72rem;color:#999">(immer geschützt)</span>
<?php endif; ?>
</label>
<?php endforeach; ?>
<p class="description" style="margin-top:8px">
Nutzer mit diesen Rollen können von anderen Mitgliedern <strong>nicht</strong> geblockt oder ignoriert werden.
Superadmin ist permanent geschützt und kann nicht abgewählt werden.
</p>
</td>
</tr>
</table>
<!-- ══════════════════════════════════════════════════════════
DISCORD-INTEGRATION
══════════════════════════════════════════════════════════ -->
<h2 style="border-bottom:1px solid #ddd;padding-bottom:.4rem;margin-top:2rem">
<span style="color:#5865f2">🎮</span> Discord-Integration
</h2>
<p class="description" style="margin-bottom:1rem">
Bot-Token und Guild-ID findest du im <a href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</a>.
Der Bot muss Mitglied deines Servers sein und die Berechtigung <strong>Direct Messages lesen/senden</strong> sowie
<strong>Server-Mitglieder verwalten</strong> besitzen.
</p>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="wbf_discord_bot_token">Bot-Token</label></th>
<td>
<input type="password" id="wbf_discord_bot_token" name="discord_bot_token"
value="<?php echo esc_attr($s['discord_bot_token']); ?>"
class="regular-text" autocomplete="off" placeholder="Bot-Token aus dem Developer Portal">
<p class="description">Niemals öffentlich teilen! Wird verschlüsselt in der Datenbank gespeichert.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_discord_guild_id">Server-ID (Guild ID)</label></th>
<td>
<input type="text" id="wbf_discord_guild_id" name="discord_guild_id"
value="<?php echo esc_attr($s['discord_guild_id']); ?>"
class="regular-text" placeholder="z. B. 123456789012345678">
<p class="description">Rechtsklick auf deinen Server → ID kopieren (Entwicklermodus muss aktiv sein).</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_discord_client_id">Client ID (optional)</label></th>
<td>
<input type="text" id="wbf_discord_client_id" name="discord_client_id"
value="<?php echo esc_attr($s['discord_client_id']); ?>"
class="regular-text" placeholder="Application ID">
<p class="description">Für zukünftige OAuth2-Unterstützung. Aktuell optional.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_discord_client_secret">Client Secret (optional)</label></th>
<td>
<input type="password" id="wbf_discord_client_secret" name="discord_client_secret"
value="<?php echo esc_attr($s['discord_client_secret']); ?>"
class="regular-text" autocomplete="off" placeholder="Client Secret">
</td>
</tr>
<tr>
<th scope="row">Rollen-Sync aktivieren</th>
<td>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" name="discord_role_sync" value="1"
<?php checked('1', $s['discord_role_sync'] ?? '0'); ?>>
Discord-Serverrollen automatisch auf Forum-Rollen mappen
</label>
<p class="description">
Wenn aktiviert, wird bei jedem Login und stündlich per Cron die Discord-Rolle des Nutzers
geprüft und die Forum-Rolle entsprechend der unten definierten Zuordnung aktualisiert.
</p>
</td>
</tr>
</table>
<!-- Discord Rollen-Map -->
<h3 style="margin-top:1.5rem">🔗 Discord-Rollen → Forum-Rollen Zuordnung</h3>
<p class="description" style="margin-bottom:.75rem">
Trage die Discord-Rollen-ID und die gewünschte Forum-Rolle ein.
Mehrere Einträge werden der Reihe nach geprüft — der erste Treffer gewinnt.
</p>
<?php
$role_map_raw = $s['discord_role_map'] ?? '{}';
$role_map = json_decode($role_map_raw, true) ?: [];
$forum_roles = WBF_Roles::get_sorted();
// Sicherstellen dass mindestens eine leere Zeile zum Hinzufügen da ist
if ( empty($role_map) ) $role_map[''] = '';
?>
<table class="widefat" id="wbf-discord-role-map" style="max-width:680px;margin-bottom:.75rem">
<thead><tr>
<th style="width:50%">Discord Rollen-ID</th>
<th style="width:40%">Forum-Rolle</th>
<th style="width:10%"></th>
</tr></thead>
<tbody>
<?php foreach ( $role_map as $dc_id => $fr_key ) : ?>
<tr class="wbf-role-map-row">
<td><input type="text" name="discord_role_id[]"
value="<?php echo esc_attr($dc_id); ?>"
placeholder="Discord Rollen-ID"
class="widefat" style="font-family:monospace"></td>
<td>
<select name="discord_forum_role[]" class="widefat">
<option value="">— wählen —</option>
<?php foreach ( $forum_roles as $rk => $role ) :
if ( $rk === 'superadmin' ) continue; ?>
<option value="<?php echo esc_attr($rk); ?>"
<?php selected($rk, $fr_key); ?>>
<?php echo esc_html($role['label']); ?>
</option>
<?php endforeach; ?>
</select>
</td>
<td><button type="button" class="button button-small wbf-rm-role-row"
style="color:#c00">✕</button></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<button type="button" class="button" id="wbf-add-role-row">+ Zeile hinzufügen</button>
<script>
(function(){
document.getElementById('wbf-add-role-row').addEventListener('click', function(){
var tbody = document.querySelector('#wbf-discord-role-map tbody');
var row = document.querySelector('.wbf-role-map-row').cloneNode(true);
row.querySelectorAll('input').forEach(function(i){i.value='';});
row.querySelectorAll('select').forEach(function(s){s.selectedIndex=0;});
tbody.appendChild(row);
});
document.addEventListener('click', function(e){
if (e.target.classList.contains('wbf-rm-role-row')) {
var rows = document.querySelectorAll('.wbf-role-map-row');
if (rows.length > 1) e.target.closest('tr').remove();
}
});
})();
</script>
<!-- Test-Verbindung -->
<div style="margin-top:1.25rem;padding:1rem;background:#f0f7ff;border:1px solid #c3dafe;border-radius:6px;max-width:680px">
<strong>🔌 Verbindungstest</strong><br>
<p style="margin:.4rem 0 .75rem;color:#374151;font-size:.9rem">
Speichere zuerst die Einstellungen, dann klicke „Testen" um zu prüfen ob der Bot erreichbar ist.
</p>
<button type="button" class="button button-secondary" id="wbf-discord-test-btn">
🔌 Discord-Verbindung testen
</button>
<span id="wbf-discord-test-result" style="margin-left:10px;font-weight:600"></span>
</div>
<script>
document.getElementById('wbf-discord-test-btn').addEventListener('click', function(){
var btn = this;
var res = document.getElementById('wbf-discord-test-result');
btn.disabled = true;
res.textContent = '⏳ Teste…';
fetch(ajaxurl, {
method: 'POST',
headers: {'Content-Type':'application/x-www-form-urlencoded'},
body: 'action=wbf_discord_test&nonce=<?php echo wp_create_nonce("wbf_discord_test"); ?>'
})
.then(r => r.json())
.then(function(d){
if (d.success) {
res.style.color = '#16a34a';
res.textContent = '✅ ' + (d.data.message || 'Verbunden!');
} else {
res.style.color = '#dc2626';
res.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
}
btn.disabled = false;
})
.catch(function(){ res.style.color='#dc2626'; res.textContent='❌ Netzwerkfehler'; btn.disabled=false; });
});
</script>
<!-- ══════════════════════════════════════════════════════════
Minecraft Bridge
════════════════════════════════════════════════════════════ -->
<div class="wbf-settings-box" style="margin-top:2rem;padding:1.5rem;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb">
<h2 style="margin-top:0;display:flex;align-items:center;gap:8px">
<span style="font-size:1.3em">⛏️</span> Minecraft Bridge
</h2>
<p class="description" style="margin-bottom:1.2rem;color:#6b7280">
Verbindet das Forum mit deinem BungeeCord-Server (StatusAPI Plugin).
Spieler können ihren Forum-Account mit <code>/forumlink &lt;token&gt;</code> verknüpfen
und erhalten dann Ingame-Benachrichtigungen bei neuen Antworten, Erwähnungen und PNs.
</p>
<table class="form-table" role="presentation">
<tr>
<th scope="row">Aktiviert</th>
<td>
<label>
<input type="checkbox" name="mc_bridge_enabled" value="1"
<?php checked( '1', $s['mc_bridge_enabled'] ?? '0' ); ?>>
Minecraft Bridge aktivieren
</label>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_mc_api_url">StatusAPI URL</label></th>
<td>
<input type="url" id="wbf_mc_api_url" name="mc_bridge_api_url"
value="<?php echo esc_attr( $s['mc_bridge_api_url'] ?? '' ); ?>"
class="regular-text"
placeholder="http://dein-server:9191">
<p class="description">
IP + Port deines BungeeCord StatusAPI Servers.
Beispiel: <code>http://192.168.1.100:9191</code>
</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_mc_api_secret">API Secret</label></th>
<td>
<input type="password" id="wbf_mc_api_secret" name="mc_bridge_api_secret"
value="<?php echo esc_attr( $s['mc_bridge_api_secret'] ?? '' ); ?>"
class="regular-text"
autocomplete="new-password"
placeholder="Gemeinsames Passwort">
<p class="description">
Muss identisch sein mit <code>forum.api_secret</code> in der
<code>verify.properties</code> des StatusAPI Plugins.
</p>
</td>
</tr>
<tr>
<th scope="row">Verbindungstest</th>
<td>
<button type="button" id="wbf-mc-test-btn" class="button"
onclick="wbfTestMcConnection()">
🔌 Verbindung testen
</button>
<span id="wbf-mc-test-result" style="margin-left:10px;font-weight:600"></span>
<script>
function wbfTestMcConnection() {
var btn = document.getElementById('wbf-mc-test-btn');
var result = document.getElementById('wbf-mc-test-result');
var url = document.getElementById('wbf_mc_api_url').value.replace(/\/$/, '');
if (!url) { result.style.color='#dc2626'; result.textContent = '❌ Bitte erst eine URL eingeben.'; return; }
btn.disabled = true;
result.style.color = '#6b7280';
result.textContent = '⏳ Teste Verbindung...';
// Test gegen WordPress REST-Endpoint (sicherer als direkter BungeeCord-Aufruf vom Browser)
fetch('<?php echo esc_url( rest_url("mc-bridge/v1/status") ); ?>')
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
result.style.color = '#16a34a';
result.innerHTML = '✅ <strong>WordPress-Endpoint aktiv!</strong> Plugin v' + (d.version || '?');
} else {
result.style.color = '#dc2626';
result.textContent = '⚠️ Endpoint antwortet, aber Fehler: ' + JSON.stringify(d);
}
})
.catch(function(e) {
result.style.color = '#dc2626';
result.textContent = '❌ Nicht erreichbar: ' + e.message;
})
.finally(function() { btn.disabled = false; });
}
</script>
<p class="description" style="margin-top:.5rem">
Testet ob der WordPress REST-Endpoint <code>/wp-json/mc-bridge/v1/status</code> erreichbar ist.
Danach in <code>verify.properties</code>: <code>forum.wp_url</code> und <code>forum.api_secret</code> eintragen.
</p>
</td>
</tr>
</table>
<div style="background:#fffbeb;border:1px solid #fcd34d;border-radius:6px;padding:1rem;margin-top:1rem;font-size:.875rem">
<strong>⚙️ Einrichtung in 3 Schritten:</strong>
<ol style="margin:.5rem 0 0 1.2rem;padding:0;line-height:1.8">
<li>API Secret hier festlegen und Einstellungen speichern.</li>
<li>In <code>verify.properties</code> des BungeeCord-Plugins setzen:
<br><code>forum.enabled=true</code>
<br><code>forum.wp_url=<?php echo esc_html( get_site_url() ); ?></code>
<br><code>forum.api_secret=DEIN_SECRET</code>
</li>
<li>Spieler können sich nun mit <strong><code>/forumlink &lt;token&gt;</code></strong> ingame verknüpfen.
Den Token generieren sie in ihrem Forum-Profil unter dem Tab <em>Verbindungen</em>.</li>
</ol>
</div>
</div>
<?php submit_button(
'💾 Einstellungen speichern',
'primary',
@@ -408,7 +857,6 @@ function wbf_admin_settings() {
true,
[ 'style' => 'margin-top:1rem' ]
); ?>
</form>
<!-- ── Vorschau-Tabelle ──────────────────────────────── -->
<hr style="margin-top:2.5rem">

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,10 @@
/* ── Utilities ──────────────────────────────────────────────── */
function wbfPost(action, data, cb, errCb) {
if (typeof WBF === 'undefined' || !WBF.ajax_url || !WBF.nonce) {
if (errCb) errCb({message: 'Forum-Fehler: AJAX-Setup fehlt. Bitte Seite neu laden.'});
return;
}
data.action = action;
data.nonce = WBF.nonce;
$.post(WBF.ajax_url, data, function (res) {
@@ -52,12 +56,18 @@
/* ── Registrieren ───────────────────────────────────────────── */
$(document).on('click', '.wbf-reg-submit-btn', function () {
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
var $invite = $(this).closest('.wbf-auth-box').find('.wbf-field-invite-code');
var inviteVal = '';
if ($invite.length > 0) {
var raw = $invite.val();
if (typeof raw === 'string') inviteVal = raw.toUpperCase().trim();
}
wbfPost('wbf_register', {
username: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-user').val(),
display_name: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-name').val(),
email: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-email').val(),
password: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-pass').val(),
invite_code: $(this).closest('.wbf-auth-box').find('.wbf-field-invite-code').val().toUpperCase().trim(),
invite_code: inviteVal,
rules_accepted: $(this).closest('.wbf-auth-box').find('.wbf-field-rules-accept').is(':checked') ? '1' : ''
}, function () {
location.reload();
@@ -510,23 +520,63 @@
});
});
/* ── Profil speichern ───────────────────────────────────────── */
$(document).on('click', '#wbfSaveProfile, #wbfSaveProfileCf', function () {
/* ── Profil speichern (alles auf einmal) ───────────────────── */
$(document).on('click', '#wbfSaveProfile', function () {
var $btn = $(this).prop('disabled', true);
var $msg = $(this).siblings('.wbf-msg').length ? $(this).siblings('.wbf-msg') : $('#wbfProfileMsg');
var $msg = $('#wbfProfileMsg');
var data = {
display_name: $('#wbfEditName').val(),
bio: $('#wbfEditBio').val(),
signature: $('#wbfEditSignature').val(),
new_password: $('#wbfNewPassword').val()
signature: $('#wbfEditSignature').val()
};
// Benutzerdefinierte Profilfelder einsammeln
// Alle benutzerdefinierten Felder (alle Kategorien) einsammeln
$('.wbf-cf-input').each(function () {
data[$(this).data('field')] = $(this).val();
});
wbfPost('wbf_update_profile', data, function (d) {
showMsg($msg, d.message, true);
$btn.prop('disabled', false);
// Bio und Signatur sofort aktualisieren (ohne Reload)
if (typeof data.bio !== 'undefined') {
$('.wbf-profile-sidebar__bio-text').text(data.bio);
}
if (typeof data.signature !== 'undefined') {
$('.wbf-profile-sidebar__sig').text(data.signature);
}
}, function (d) {
showMsg($msg, d.message || 'Fehler', false);
$btn.prop('disabled', false);
});
});
/* ── Passwort ändern ────────────────────────────────────────── */
$(document).on('click', '#wbfSavePassword', function () {
var $btn = $(this).prop('disabled', true);
var $msg = $('#wbfPasswordMsg');
var cur = $('#wbfCurrentPassword').val();
var pw1 = $('#wbfNewPassword').val();
var pw2 = $('#wbfNewPassword2').val();
if (!cur) {
showMsg($msg, 'Bitte aktuelles Passwort eingeben.', false);
return $btn.prop('disabled', false);
}
if (pw1.length < 6) {
showMsg($msg, 'Neues Passwort mindestens 6 Zeichen.', false);
return $btn.prop('disabled', false);
}
if (pw1 !== pw2) {
showMsg($msg, 'Die Passwörter stimmen nicht überein.', false);
return $btn.prop('disabled', false);
}
wbfPost('wbf_update_profile', {
current_password: cur,
new_password: pw1
}, function (d) {
showMsg($msg, d.message, true);
$('#wbfCurrentPassword, #wbfNewPassword, #wbfNewPassword2').val('');
$btn.prop('disabled', false);
}, function (d) {
showMsg($msg, d.message || 'Fehler', false);
$btn.prop('disabled', false);
@@ -542,10 +592,16 @@
$(document).on('change', '#wbfAvatarFile', function () {
var file = this.files[0];
if (!file) return;
// Sofort-Vorschau — synchron, kein Callback, kein Warten
var objectUrl = URL.createObjectURL(file);
$('#wbfProfileAvatar').attr('src', objectUrl).css('opacity', '.6');
var fd = new FormData();
fd.append('action', 'wbf_upload_avatar');
fd.append('nonce', WBF.nonce);
fd.append('avatar', file);
$.ajax({
url: WBF.ajax_url,
type: 'POST',
@@ -553,9 +609,61 @@
processData: false,
contentType: false,
success: function (res) {
$('#wbfProfileAvatar').css('opacity', '1');
if (res.success) {
$('.wbf-profile-page__avatar').attr('src', res.data.avatar_url);
// Object-URL freigeben, endgültige Server-URL setzen
URL.revokeObjectURL(objectUrl);
var finalUrl = res.data.avatar_url + '?v=' + Date.now();
$('#wbfProfileAvatar').attr('src', finalUrl);
// Topbar-Avatar ebenfalls aktualisieren
$('.wbf-topbar__user img').attr('src', finalUrl);
$('.wbf-profile-widget__avatar img').attr('src', finalUrl);
}
},
error: function () {
$('#wbfProfileAvatar').css('opacity', '1');
}
});
});
// ── Banner-Upload ─────────────────────────────────────────────────────────
$(document).on('change', '#wbfBannerFile', function () {
var file = this.files[0];
if (!file) return;
// Sofort-Vorschau
var objectUrl = URL.createObjectURL(file);
var $wrap = $('#wbfProfileBannerWrap');
var $existing = $wrap.find('.wbf-profile-banner__img');
// Falls noch kein Banner-Bild existiert, eins einfügen
if ($existing.length === 0) {
$wrap.prepend('<img src="' + objectUrl + '" alt="" id="wbfProfileBanner" class="wbf-profile-banner__img" style="opacity:.4">');
} else {
$existing.attr('src', objectUrl).css('opacity', '.4');
}
var fd = new FormData();
fd.append('action', 'wbf_upload_banner');
fd.append('nonce', WBF.nonce);
fd.append('banner', file);
$.ajax({
url: WBF.ajax_url,
type: 'POST',
data: fd,
processData: false,
contentType: false,
success: function (res) {
var $img = $wrap.find('.wbf-profile-banner__img');
if (res.success) {
URL.revokeObjectURL(objectUrl);
$img.attr('src', res.data.banner_url + '?v=' + Date.now());
}
$img.css('opacity', '');
},
error: function () {
$wrap.find('.wbf-profile-banner__img').css('opacity', '');
}
});
});
@@ -1091,7 +1199,7 @@
var html = '';
d.notifications.forEach(function (n) {
var isUnread = n.is_read == 0;
var avatar = n.actor_avatar || '';
var avatar = $('<img>').attr('src', n.actor_avatar || '').attr('alt', '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
var base = WBF.forum_url || window.location.href.split('?')[0];
var sep = base.indexOf('?') !== -1 ? '&' : '?';
var actor = '<strong>' + $('<span>').text(n.actor_name).html() + '</strong>';
@@ -1118,7 +1226,7 @@
}
html += '<a class="wbf-notif-item' + (isUnread ? ' wbf-notif-item--unread' : '') + '" href="' + url + '">' +
'<div class="wbf-notif-item__avatar"><img src="' + avatar + '" alt=""></div>' +
'<div class="wbf-notif-item__avatar">' + avatar + '</div>' +
'<div class="wbf-notif-item__body">' +
'<div class="wbf-notif-item__text">' + text +
(sub ? '<br><span style="color:var(--c-muted);font-size:.78rem">' + $('<span>').text(sub).html() + '</span>' : '') +
@@ -1218,7 +1326,7 @@
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Loeschen"><i class="fas fa-trash-can"></i></button>';
var html = '<div class="' + cls + '" data-msg-id="' + m.id + '">';
if (!isMine) {
html += '<img src="' + (m.sender_avatar||'') + '" class="wbf-dm-inbox-item__avatar">';
html += $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
}
html += '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
@@ -1293,12 +1401,16 @@
'</div>'
].join('')).appendTo('body');
var wbfLogoutFired = false; // Guard gegen doppelten Logout-Call
function wbfDoLogout() {
if (wbfLogoutFired) return; // doppelten Aufruf verhindern
wbfLogoutFired = true;
clearTimeout(wbfIdleTimer);
clearTimeout(wbfWarnTimer);
clearInterval(wbfCountTimer);
$wbfToast.hide();
wbfPost('wbf_logout', {}, function () {
wbfPost('wbf_logout', { nonce: WBF.nonce }, function () {
location.reload();
});
}
@@ -1308,23 +1420,23 @@
var secs = 30;
$('#wbfIdleCountdown').text(secs);
$wbfToast.fadeIn(200);
// Countdown-Interval läuft bis 0 und ruft dann wbfDoLogout() auf —
// kein zusätzlicher setTimeout(wbfDoLogout) nötig (war die Ursache des Doppel-Logouts)
wbfCountTimer = setInterval(function () {
secs--;
$('#wbfIdleCountdown').text(secs);
$('#wbfIdleCountdown').text(Math.max(0, secs));
if (secs <= 0) {
clearInterval(wbfCountTimer);
wbfDoLogout();
}
}, 1000);
// Auto-logout after warning period
wbfIdleTimer = setTimeout(wbfDoLogout, wbfWarnMs);
}
function wbfResetIdleTimer() {
if (wbfWarning) return; // Nutzer hat aktiv Warnung bestätigt — nicht resetten
clearTimeout(wbfIdleTimer);
clearTimeout(wbfWarnTimer);
// Warn 30 sec before timeout
// Warnung 30 Sek. vor Ablauf zeigen
wbfWarnTimer = setTimeout(wbfShowWarning, wbfIdleMs - wbfWarnMs);
}
@@ -1334,6 +1446,7 @@
clearInterval(wbfCountTimer);
$wbfToast.fadeOut(200);
wbfWarning = false;
wbfLogoutFired = false; // Guard zurücksetzen
wbfResetIdleTimer();
});
@@ -1452,7 +1565,7 @@
var html = '';
d.users.forEach(function(u) {
html += '<div class="wbf-mention-item" data-username="' + $('<span>').text(u.username).html() + '">'
+ '<img src="' + (u.avatar_url || '') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
+ '<span>' + $('<span>').text(u.display_name).html() + '</span>'
+ '<small>@' + $('<span>').text(u.username).html() + '</small>'
+ '</div>';
@@ -1500,7 +1613,7 @@
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Löschen"><i class="fas fa-trash-can"></i></button>';
if (!isMine) {
html += '<div class="' + cls + '" data-msg-id="' + m.id + '">'
+ '<img src="' + (m.sender_avatar || '') + '" class="wbf-dm-msg__avatar">'
+ $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-msg__avatar')[0].outerHTML
+ '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
+ '</div><div class="wbf-dm-msg__time">' + time + delBtn + '</div></div></div>';
@@ -1524,7 +1637,7 @@
var href = window.location.pathname + '?forum_dm=inbox&with=' + conv.partner_id;
var unread = parseInt(conv.unread_cnt) > 0;
html += '<a class="wbf-dm-inbox-item' + (unread ? ' wbf-dm-inbox-item--unread' : '') + '" href="' + href + '">'
+ '<img src="' + (conv.partner_avatar || '') + '" class="wbf-dm-inbox-item__avatar">'
+ $('<img>').attr('src', conv.partner_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
+ '<div class="wbf-dm-inbox-item__body">'
+ '<span class="wbf-dm-inbox-item__name">' + $('<span>').text(conv.partner_name).html() + '</span>'
+ (unread ? '<span class="wbf-dm-inbox-item__badge">' + conv.unread_cnt + '</span>' : '')
@@ -1550,7 +1663,7 @@
var backUrl = window.location.pathname + '?forum_dm=inbox';
$('#wbfDmHeader').html(
'<a href="' + backUrl + '" class="wbf-dm-back-btn" title="Zurück zur Inbox"><i class="fas fa-arrow-left"></i></a>'
+ '<img src="' + (p.avatar_url||'') + '" class="wbf-dm-inbox-item__avatar">'
+ $('<img>').attr('src', p.avatar_url || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
+ '<strong>' + $('<span>').text(p.display_name).html() + '</strong>'
+ '<a href="?forum_profile=' + p.id + '" style="font-size:.78rem;color:var(--c-muted);text-decoration:none">@' + $('<span>').text(p.username).html() + '</a>'
);
@@ -1634,7 +1747,7 @@
var html = '';
d.users.forEach(function(u) {
html += '<div class="wbf-tag-suggest-item" data-id="' + u.id + '" data-name="' + $('<span>').text(u.display_name).html() + '">'
+ '<img src="' + (u.avatar_url||'') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
+ $('<span>').text(u.display_name).html()
+ '<span style="color:var(--c-muted);font-size:.75rem">@' + $('<span>').text(u.username).html() + '</span>'
+ '</div>';
@@ -1944,6 +2057,55 @@
});
});
/* ── E-Mail-Adresse ändern ──────────────────────────────────────────── */
$(document).on('click', '#wbfSaveEmail', function () {
var $btn = $(this);
var email = $('#wbfNewEmail').val().trim();
var password = $('#wbfEmailPassword').val();
var $msg = $('#wbfEmailMsg');
if (!email) { $msg.removeClass('wbf-ok').addClass('wbf-err').text('Bitte E-Mail eingeben.'); return; }
if (!password) { $msg.removeClass('wbf-ok').addClass('wbf-err').text('Bitte Passwort eingeben.'); return; }
$btn.prop('disabled', true);
wbfPost('wbf_change_email', { new_email: email, password: password }, function (d) {
$msg.removeClass('wbf-err').addClass('wbf-ok').text(d.message || 'E-Mail geaendert.');
$('#wbfNewEmail').val('');
$('#wbfEmailPassword').val('');
$btn.prop('disabled', false);
}, function (d) {
$msg.removeClass('wbf-ok').addClass('wbf-err').text(d.message || 'Fehler.');
$btn.prop('disabled', false);
});
});
/* ── Toggle-Switch (Notification Prefs) ─────────────────────────────── */
$(document).on('click', '.wbf-toggle', function () {
var $t = $(this);
var on = String($t.data('state')) === '1';
var val = on ? '0' : '1';
$t.data('state', val).attr('data-state', val);
if (val === '1') { $t.addClass('wbf-toggle--on'); }
else { $t.removeClass('wbf-toggle--on'); }
});
/* ── Benachrichtigungs-Einstellungen speichern ───────────────────────── */
$(document).on('click', '#wbfSaveNotifPrefs', function () {
var $btn = $(this);
var $msg = $('#wbfNotifPrefsMsg');
$btn.prop('disabled', true);
wbfPost('wbf_save_notification_prefs', {
notify_reply: String($('#wbfNotifReply').data('state')) === '1' ? '1' : '0',
notify_mention: String($('#wbfNotifMention').data('state')) === '1' ? '1' : '0',
notify_message: String($('#wbfNotifMessage').data('state')) === '1' ? '1' : '0'
}, function (d) {
$msg.removeClass('wbf-err').addClass('wbf-ok').text(d.message || 'Gespeichert!');
$btn.prop('disabled', false);
}, function (d) {
$msg.removeClass('wbf-ok').addClass('wbf-err').text(d.message || 'Fehler.');
$btn.prop('disabled', false);
});
});
/* ── Lesezeichen ────────────────────────────────────────────────────── */
$(document).on('click', '.wbf-bookmark-btn', function () {
var $btn = $(this);
@@ -1959,4 +2121,238 @@
});
});
}(jQuery));
/* ── Nutzer ignorieren / Ignorierung aufheben ────────────────────────── */
$(document).on('click', '.wbf-ignore-btn', function () {
var $btn = $(this);
var ignoredId = parseInt($btn.data('id'), 10);
var name = $btn.data('name') || 'diesen Nutzer';
var isIgnored = String($btn.data('ignored')) === '1';
// Bestätigung nur beim Ignorieren, nicht beim Entblocken
if (!isIgnored) {
if (!confirm(name + ' ignorieren?\n\nDessen Beiträge werden in Threads ausgeblendet und DMs werden blockiert.')) {
return;
}
}
$btn.prop('disabled', true);
wbfPost('wbf_toggle_ignore', { ignored_id: ignoredId }, function (d) {
var nowIgnored = d.ignored;
// Alle Buttons mit dieser User-ID auf der Seite aktualisieren
$('.wbf-ignore-btn[data-id="' + ignoredId + '"]').each(function () {
var $b = $(this);
$b.data('ignored', nowIgnored ? '1' : '0');
$b.attr('data-ignored', nowIgnored ? '1' : '0');
// Icon + Label aktualisieren
$b.find('i').attr('class', 'fas fa-' + (nowIgnored ? 'eye' : 'eye-slash'));
// Button-Variante (Post-Footer, klein ohne wbf-btn)
if (!$b.hasClass('wbf-btn')) {
$b.text('');
$b.append('<i class="fas fa-' + (nowIgnored ? 'eye' : 'eye-slash') + '"></i> ' + (nowIgnored ? 'Entblocken' : 'Ignorieren'));
} else {
// Profil-Variante mit wbf-btn
$b.html('<i class="fas fa-' + (nowIgnored ? 'eye' : 'eye-slash') + '"></i> ' + (nowIgnored ? 'Ignorierung aufheben' : 'Nutzer ignorieren'));
}
$b.prop('disabled', false);
});
// Posts des Users auf der aktuellen Seite ein-/ausblenden
$('.wbf-post, .wbf-post--op').each(function () {
var $post = $(this);
// Buttons innerhalb dieses Posts mit der User-ID suchen
var $ib = $post.find('.wbf-ignore-btn[data-id="' + ignoredId + '"]');
if (!$ib.length) return;
if (nowIgnored) {
// Ignoriert → Overlay zeigen wenn noch nicht vorhanden
if (!$post.find('.wbf-ignored-bar').length) {
var barHtml = '<div class="wbf-ignored-bar">' +
'<span><i class="fas fa-eye-slash"></i> Beitrag von ignoriertem Nutzer: <strong>' +
$('<span>').text(name).html() + '</strong></span>' +
'<button class="wbf-show-ignored-btn" type="button">Trotzdem anzeigen</button>' +
'</div>' +
'<div class="wbf-ignored-content" style="display:none">';
$post.addClass('wbf-post--ignored');
$post.prepend(barHtml);
// Restlichen Inhalt in ignored-content verschieben
$post.children(':not(.wbf-ignored-bar):not(.wbf-ignored-content)').wrapAll('<div class="wbf-ignored-content-inner">');
$post.find('.wbf-ignored-content').append($post.find('.wbf-ignored-content-inner').children());
$post.find('.wbf-ignored-content-inner').remove();
}
} else {
// Entblockt → Overlay entfernen
var $bar = $post.find('.wbf-ignored-bar');
var $content = $post.find('.wbf-ignored-content');
if ($bar.length) {
// Inhalt wieder nach oben holen
$content.children().unwrap();
$bar.remove();
$post.removeClass('wbf-post--ignored');
}
}
});
// Ignore-Liste im Profil aktualisieren (falls Nutzer auf eigener Profil-Seite)
if (!nowIgnored) {
// Eintrag aus der Liste entfernen
$('#wbf-ignore-item-' + ignoredId).fadeOut(300, function () {
$(this).remove();
var remaining = $('#wbfIgnoreList .wbf-ignore-item').length;
$('#wbfIgnoreCount').text(remaining);
if (remaining === 0) {
$('#wbfIgnoreList').replaceWith('<p class="wbf-profile-empty" id="wbfIgnoreEmpty">Du ignorierst niemanden.</p>');
}
});
}
// Toast-Meldung
var $t = $('<div class="wbf-toast">' + (d.message || (nowIgnored ? name + ' ignoriert.' : 'Ignorierung aufgehoben.')) + '</div>').appendTo('body');
setTimeout(function () { $t.remove(); }, 3000);
}, function () {
// Fehler-Callback
$('.wbf-ignore-btn[data-id="' + ignoredId + '"]').prop('disabled', false);
});
});
/* "Trotzdem anzeigen" — eingeklappten ignorierten Post aufdecken */
$(document).on('click', '.wbf-show-ignored-btn', function () {
var $bar = $(this).closest('.wbf-ignored-bar');
var $content = $bar.next('.wbf-ignored-content');
$content.slideDown(200);
$bar.hide();
});
// ── Discord-Integration (3-Schritt Verifikation) ─────────────────────────
var wbfDcStep = 1; // aktueller Schritt
function wbfDcMsg(text, color) {
var $m = $('#wbf-discord-msg');
$m.css('color', color || 'var(--c-muted)').html(text);
}
function wbfDcSetBadge(connected) {
var $badge = $('.wbf-connection-card--discord .wbf-connection-badge');
if (connected) {
$badge.removeClass('wbf-connection-badge--disconnected')
.addClass('wbf-connection-badge--connected')
.html('<i class="fas fa-check-circle"></i> Verbunden');
} else {
$badge.removeClass('wbf-connection-badge--connected')
.addClass('wbf-connection-badge--disconnected')
.html('<i class="fas fa-circle-xmark"></i> Nicht verbunden');
}
}
// Schritt 1 → Code senden
$(document).on('click', '#wbf-discord-send-code', function () {
var username = $('#wbf-discord-input').val().trim();
if (!username) { wbfDcMsg('<i class="fas fa-triangle-exclamation"></i> Bitte Benutzername eingeben.', '#f97316'); return; }
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Sende…');
wbfDcMsg('');
$.post(WBF.ajax_url, {
action: 'wbf_discord_send_code',
nonce: WBF.nonce,
discord_username: username,
}, function (res) {
$btn.prop('disabled', false).html('<i class="fab fa-discord"></i> Code senden');
if (res.success) {
wbfDcMsg('<i class="fas fa-check" style="color:#16a34a"></i> ' + (res.data.message || 'Code gesendet!'), '#16a34a');
$('#wbf-dc-step1').slideUp(200, function () { $('#wbf-dc-step2').slideDown(200); });
$('#wbf-discord-code-input').val('').focus();
wbfDcStep = 2;
} else {
wbfDcMsg('<i class="fas fa-circle-xmark"></i> ' + ((res.data && res.data.message) || 'Fehler.'), '#dc2626');
}
}).fail(function () {
$btn.prop('disabled', false).html('<i class="fab fa-discord"></i> Code senden');
wbfDcMsg('<i class="fas fa-circle-xmark"></i> Netzwerkfehler.', '#dc2626');
});
});
// Schritt 2 → Code bestätigen
$(document).on('click', '#wbf-discord-verify', function () {
var code = $('#wbf-discord-code-input').val().trim().toUpperCase();
if (code.length < 4) { wbfDcMsg('<i class="fas fa-triangle-exclamation"></i> Bitte Code eingeben.', '#f97316'); return; }
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Prüfe…');
$.post(WBF.ajax_url, {
action: 'wbf_discord_verify_code',
nonce: WBF.nonce,
verify_code: code,
}, function (res) {
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen');
if (res.success) {
wbfDcMsg('<i class="fas fa-check-circle"></i> ' + (res.data.message || 'Verbunden!'), '#16a34a');
wbfDcSetBadge(true);
// UI auf "Verbunden"-Ansicht umschalten
var name = res.data.display_name || '';
$('#wbf-discord-form').slideUp(200);
// Verbunden-Info einfügen/aktualisieren
var $info = $('.wbf-discord-connected-info');
if ($info.length) {
$info.find('.wbf-discord-linked-name').html('<i class="fab fa-discord" style="color:#5865f2"></i> ' + $('<span>').text(name).html());
} else {
// Frisch laden damit die PHP-Struktur stimmt
setTimeout(function(){ location.reload(); }, 1200);
}
} else {
wbfDcMsg('<i class="fas fa-circle-xmark"></i> ' + ((res.data && res.data.message) || 'Fehler.'), '#dc2626');
}
}).fail(function () {
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen');
wbfDcMsg('<i class="fas fa-circle-xmark"></i> Netzwerkfehler.', '#dc2626');
});
});
// Enter-Taste auf Code-Feld
$(document).on('keydown', '#wbf-discord-code-input', function (e) {
if (e.key === 'Enter') { e.preventDefault(); $('#wbf-discord-verify').trigger('click'); }
});
// Enter-Taste auf Username-Feld
$(document).on('keydown', '#wbf-discord-input', function (e) {
if (e.key === 'Enter') { e.preventDefault(); $('#wbf-discord-send-code').trigger('click'); }
});
// „Zurück" in Schritt 2
$(document).on('click', '#wbf-discord-code-back', function () {
$('#wbf-dc-step2').slideUp(200, function () { $('#wbf-dc-step1').slideDown(200); });
wbfDcMsg('');
wbfDcStep = 1;
});
// „Neu verknüpfen" bei bereits verbundenem Account
$(document).on('click', '#wbf-discord-relink', function () {
$('#wbf-discord-form').slideDown(200);
$('#wbf-discord-input').val('').focus();
});
// Verbindung trennen
$(document).on('click', '#wbf-discord-disconnect', function () {
if (!confirm('Discord-Verbindung wirklich trennen?')) return;
var $btn = $(this).prop('disabled', true);
$.post(WBF.ajax_url, {
action: 'wbf_save_discord',
nonce: WBF.nonce,
sub_action: 'disconnect',
}, function (res) {
$btn.prop('disabled', false);
if (res.success) {
wbfDcMsg('<i class="fas fa-check"></i> ' + (res.data.message || 'Getrennt.'), '#16a34a');
wbfDcSetBadge(false);
setTimeout(function () { location.reload(); }, 900);
} else {
wbfDcMsg('<i class="fas fa-circle-xmark"></i> ' + ((res.data && res.data.message) || 'Fehler.'), '#dc2626');
}
});
});
}(jQuery));
// Overwrite last line — Discord handlers appended via patch:

View File

@@ -7,7 +7,7 @@ class WBF_Ajax {
$actions = [
'wbf_login', 'wbf_register', 'wbf_logout',
'wbf_new_thread', 'wbf_new_post', 'wbf_toggle_like',
'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image',
'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image', 'wbf_upload_banner',
'wbf_forgot_password', 'wbf_reset_password', 'wbf_load_more_messages',
'wbf_create_invite', 'wbf_delete_invite',
'wbf_toggle_subscribe', 'wbf_restore_content', 'wbf_toggle_profile_visibility',
@@ -18,6 +18,16 @@ class WBF_Ajax {
'wbf_create_poll',
'wbf_toggle_bookmark',
'wbf_set_thread_prefix',
'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)]);
@@ -40,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.']);
@@ -95,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';
@@ -109,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']);
}
@@ -157,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'] ?? '' );
@@ -223,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,
@@ -369,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);
}
@@ -394,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 );
}
@@ -411,9 +481,37 @@ class WBF_Ajax {
if (empty($_FILES['avatar'])) wp_send_json_error(['message'=>'Keine Datei.']);
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
$mime = $_FILES['avatar']['type'] ?? '';
if (!in_array($mime, $allowed_types)) wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']);
if ($_FILES['avatar']['size'] > 2 * 1024 * 1024) wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
// Dateigröße vor dem MIME-Check prüfen
if ( $_FILES['avatar']['size'] > 2 * 1024 * 1024 ) {
wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
}
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] kommt vom Client
// und ist beliebig fälschbar (z.B. PHP-Datei als image/jpeg getarnt).
// finfo_file() liest den echten Magic-Byte der temporären Datei.
$tmp = $_FILES['avatar']['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 {
// Fallback: exif_imagetype() wenn finfo nicht verfügbar
$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';
@@ -427,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() {
@@ -468,16 +612,36 @@ class WBF_Ajax {
// Nur Bilder erlauben
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
$mime = $_FILES['image']['type'] ?? '';
if ( ! in_array($mime, $allowed_types) ) {
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
}
// Max 5 MB
// Max 5 MB — Größe zuerst prüfen bevor teure MIME-Erkennung läuft
if ( $_FILES['image']['size'] > 5 * 1024 * 1024 ) {
wp_send_json_error(['message' => 'Maximale Dateigröße: 5 MB.']);
}
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] ist client-kontrolliert
// und kann beliebig auf 'image/jpeg' gesetzt werden, auch für .php-Dateien.
$tmp = $_FILES['image']['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';
@@ -543,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]);
}
@@ -588,6 +753,18 @@ class WBF_Ajax {
$is_mod = WBF_DB::can($user, 'delete_post');
if ( ! $is_own && ! $is_mod ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
// Post-Bearbeitungslimit prüfen — gilt auch für Thread-Erstbeiträge
// (spiegelt identisches Verhalten zu handle_edit_post() wider)
if ( $is_own && ! $is_mod ) {
$limit_min = (int)( wbf_get_settings()['post_edit_limit'] ?? 30 );
if ( $limit_min > 0 ) {
$age_min = ( time() - strtotime( $thread->created_at ) ) / 60;
if ( $age_min > $limit_min ) {
wp_send_json_error(['message' => "Bearbeitung nur innerhalb von {$limit_min} Minuten nach dem Erstellen möglich."]);
}
}
}
global $wpdb;
$wpdb->update(
"{$wpdb->prefix}forum_threads",
@@ -682,6 +859,11 @@ class WBF_Ajax {
if ($to_id === (int)$user->id) wp_send_json_error(['message'=>'Du kannst dir nicht selbst schreiben.']);
if (!WBF_DB::get_user($to_id)) wp_send_json_error(['message'=>'Empfänger nicht gefunden.']);
// DM-Blockierung: Empfänger hat Sender ignoriert
if ( WBF_DB::is_ignored( $to_id, $user->id ) ) {
wp_send_json_error(['message' => 'Diese Person akzeptiert keine Nachrichten von dir.']);
}
$id = WBF_DB::send_message($user->id, $to_id, $content);
// Notify recipient
WBF_DB::create_notification($to_id, 'message', $id, $user->id);
@@ -739,14 +921,22 @@ class WBF_Ajax {
// ── User-Autocomplete (für @Erwähnungen + DM) ─────────────────────────────
public static function handle_user_suggest() {
// Nur eingeloggte Nutzer dürfen die User-Suche nutzen
// (verhindert Enumeration aller Usernamen + Rollendaten durch Gäste)
if ( ! WBF_Auth::is_forum_logged_in() ) {
wp_send_json_success(['users'=>[]]);
}
$q = sanitize_text_field($_POST['q'] ?? $_GET['q'] ?? '');
if (mb_strlen($q) < 1) wp_send_json_success(['users'=>[]]);
global $wpdb;
$like = $wpdb->esc_like($q) . '%';
// Rolle wird bewusst NICHT zurückgegeben — nicht für Autocomplete nötig
// und verhindert Informationsleck über Rollen-Verteilung im Forum.
$users = $wpdb->get_results($wpdb->prepare(
"SELECT id, username, display_name, avatar_url, role
"SELECT id, username, display_name, avatar_url
FROM {$wpdb->prefix}forum_users
WHERE username LIKE %s OR display_name LIKE %s
WHERE (username LIKE %s OR display_name LIKE %s)
AND role != 'banned'
ORDER BY display_name ASC LIMIT 8",
$like, $like
));
@@ -817,6 +1007,13 @@ class WBF_Ajax {
private static function send_notification_email( $to_user, $type, $actor_name, $extra = [] ) {
if ( ! $to_user || empty($to_user->email) ) return;
// Prüfen ob der User diesen Benachrichtigungstyp aktiviert hat
// Standard: alle aktiviert (1). User kann im Profil deaktivieren (0).
$pref_key = 'notify_' . $type; // notify_reply, notify_mention, notify_message
$meta = WBF_DB::get_user_meta( $to_user->id );
// Nur deaktivieren wenn explizit auf '0' gesetzt — Standard ist aktiviert
if ( isset($meta[$pref_key]) && $meta[$pref_key] === '0' ) return;
$blog_name = get_bloginfo('name');
$forum_url = wbf_get_forum_url();
$from_email = get_option('admin_email');
@@ -897,6 +1094,17 @@ class WBF_Ajax {
$email = sanitize_email( $_POST['email'] ?? '' );
if ( ! is_email($email) ) wp_send_json_error(['message'=>'Ungültige E-Mail-Adresse.']);
// ── Rate-Limiting: max. 1 Reset-Mail pro E-Mail-Adresse alle 15 Minuten ──
// Verhindert, dass ein Angreifer tausende Reset-Mails pro Sekunde
// für beliebige Adressen triggert und den Mail-Server überlastet.
$rate_key = 'wbf_pwreset_' . md5( strtolower( $email ) );
if ( get_transient( $rate_key ) !== false ) {
// Immer Erfolg melden — kein Leak ob Rate-Limit oder kein Account
wp_send_json_success(['message'=>'Falls diese E-Mail registriert ist, wurde eine E-Mail gesendet.']);
}
// Cooldown setzen — 15 Minuten
set_transient( $rate_key, 1, 15 * MINUTE_IN_SECONDS );
$user = WBF_DB::get_user_by('email', $email);
// Immer Erfolg melden (kein User-Enumeration)
if ( ! $user ) {
@@ -922,7 +1130,13 @@ class WBF_Ajax {
}
public static function handle_reset_password() {
self::verify();
// Kein self::verify() hier — Gäste haben keine Forum-Session.
// Das Reset-Token selbst authentifiziert die Anfrage.
// Wir prüfen trotzdem den WP-Nonce als CSRF-Schutz; dieser wird
// von wp_localize_script für alle Besucher (auch Gäste) generiert.
if ( ! check_ajax_referer( 'wbf_nonce', 'nonce', false ) ) {
wp_send_json_error(['message' => 'Sicherheitsfehler.']);
}
$token = sanitize_text_field( $_POST['token'] ?? '' );
$password = $_POST['password'] ?? '';
$password2= $_POST['password2'] ?? '';
@@ -1041,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]);
@@ -1207,6 +1427,403 @@ class WBF_Ajax {
wp_send_json_success(['prefix' => $prefix]);
}
// ── E-Mail-Adresse ändern ─────────────────────────────────────────────────
public static function handle_change_email() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$new_email = sanitize_email( $_POST['new_email'] ?? '' );
$password = $_POST['password'] ?? '';
if ( ! is_email($new_email) ) {
wp_send_json_error(['message' => 'Ungültige E-Mail-Adresse.']);
}
if ( empty($password) ) {
wp_send_json_error(['message' => 'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
}
if ( ! password_verify($password, $user->password) ) {
wp_send_json_error(['message' => 'Falsches Passwort.']);
}
if ( strtolower($new_email) === strtolower($user->email) ) {
wp_send_json_error(['message' => 'Das ist bereits deine aktuelle E-Mail-Adresse.']);
}
// Prüfen ob E-Mail bereits vergeben
$existing = WBF_DB::get_user_by('email', $new_email);
if ( $existing && (int)$existing->id !== (int)$user->id ) {
wp_send_json_error(['message' => 'Diese E-Mail-Adresse ist bereits registriert.']);
}
WBF_DB::update_user($user->id, ['email' => $new_email]);
wp_send_json_success(['message' => 'E-Mail-Adresse erfolgreich geändert.']);
}
// ── Benachrichtigungs-Einstellungen speichern ─────────────────────────────
public static function handle_save_notification_prefs() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$allowed = ['notify_reply', 'notify_mention', 'notify_message'];
foreach ( $allowed as $key ) {
// 1 wenn Checkbox aktiviert, 0 wenn deaktiviert
$val = isset($_POST[$key]) && $_POST[$key] === '1' ? '1' : '0';
WBF_DB::set_user_meta($user->id, $key, $val);
}
wp_send_json_success(['message' => 'Benachrichtigungs-Einstellungen gespeichert.']);
}
// ── User ignorieren / Ignorierung aufheben ────────────────────────────────
public static function handle_toggle_ignore() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error( ['message' => 'Nicht eingeloggt.'] );
$ignored_id = (int)( $_POST['ignored_id'] ?? 0 );
if ( ! $ignored_id ) wp_send_json_error( ['message' => 'Ungültiger Nutzer.'] );
if ( $ignored_id === (int)$user->id ) {
wp_send_json_error( ['message' => 'Du kannst dich nicht selbst ignorieren.'] );
}
$target = WBF_DB::get_user( $ignored_id );
if ( ! $target ) wp_send_json_error( ['message' => 'Nutzer nicht gefunden.'] );
// Prüfen ob diese Rolle geblockt werden darf (konfigurierbar in den Einstellungen)
if ( ! wbf_can_be_ignored( $target ) ) {
$role_label = WBF_Roles::get($target->role)['label'] ?? $target->role;
wp_send_json_error( ['message' => 'Nutzer mit der Rolle "' . $role_label . '" können nicht ignoriert werden.'] );
}
$now_ignored = WBF_DB::toggle_ignore( $user->id, $ignored_id );
wp_send_json_success( [
'ignored' => $now_ignored,
'ignored_id' => $ignored_id,
'display_name' => $target->display_name,
'message' => $now_ignored
? esc_html( $target->display_name ) . ' wird jetzt ignoriert.'
: 'Ignorierung von ' . esc_html( $target->display_name ) . ' aufgehoben.',
] );
}
// ── Discord: Verifikations-Code per Bot-DM senden ─────────────────────────
public static function handle_discord_send_code() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$s = wbf_get_settings();
$token = trim($s['discord_bot_token'] ?? '');
$guild = trim($s['discord_guild_id'] ?? '');
if ( ! $token ) {
wp_send_json_error(['message' => 'Discord-Bot ist noch nicht konfiguriert. Bitte wende dich an einen Admin.']);
}
$username_input = sanitize_text_field($_POST['discord_username'] ?? '');
if ( ! $username_input ) {
wp_send_json_error(['message' => 'Bitte Discord-Benutzername eingeben.']);
}
// Nutzer auf dem Guild suchen (nach Username oder per Search)
$discord_user_id = self::discord_find_user_id($username_input, $token, $guild);
if ( ! $discord_user_id ) {
wp_send_json_error(['message' => 'Discord-Nutzer nicht auf dem Server gefunden. Stelle sicher, dass du Mitglied des Servers bist.']);
}
// Verifikations-Code generieren (6-stellig)
$code = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 6));
$expires = time() + 600; // 10 Minuten
// Code + Discord-User-ID temporär speichern
WBF_DB::set_user_meta($user->id, 'discord_verify_code', $code);
WBF_DB::set_user_meta($user->id, 'discord_verify_expires', (string)$expires);
WBF_DB::set_user_meta($user->id, 'discord_verify_pending_id', $discord_user_id);
// DM senden
$sent = self::discord_send_dm($discord_user_id, $token,
"🔐 **Dein Verifikationscode für " . get_bloginfo('name') . ":**\n\n" .
"```" . $code . "```\n" .
"Gib diesen Code im Forum ein. Er ist **10 Minuten** gültig.\n" .
"_Falls du diese Nachricht nicht erwartet hast, ignoriere sie einfach._"
);
if ( ! $sent ) {
wp_send_json_error(['message' => 'DM konnte nicht gesendet werden. Stelle sicher, dass du DMs von Server-Mitgliedern zulässt.']);
}
wp_send_json_success(['message' => '✅ Code gesendet! Prüfe deine Discord-DMs und gib den 6-stelligen Code ein.', 'step' => 'enter_code']);
}
// ── Discord: Code überprüfen + Verbindung herstellen ─────────────────────
public static function handle_discord_verify_code() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$code_input = strtoupper(sanitize_text_field($_POST['verify_code'] ?? ''));
$meta = WBF_DB::get_user_meta($user->id);
$stored_code = strtoupper($meta['discord_verify_code'] ?? '');
$expires = (int)($meta['discord_verify_expires'] ?? 0);
$discord_uid = $meta['discord_verify_pending_id'] ?? '';
if ( ! $stored_code || ! $discord_uid ) {
wp_send_json_error(['message' => 'Kein offener Verifizierungs-Vorgang. Bitte erneut starten.']);
}
if ( time() > $expires ) {
wp_send_json_error(['message' => 'Code abgelaufen. Bitte erneut einen Code anfordern.']);
}
if ( ! hash_equals($stored_code, $code_input) ) {
wp_send_json_error(['message' => 'Falscher Code. Bitte erneut versuchen.']);
}
// Discord-Username abrufen (für Anzeige)
$s = wbf_get_settings();
$token = trim($s['discord_bot_token'] ?? '');
$display_name = $discord_uid;
if ( $token ) {
$res = wp_remote_get("https://discord.com/api/v10/users/{$discord_uid}", [
'timeout' => 5,
'headers' => ['Authorization' => 'Bot ' . $token],
]);
if ( ! is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200 ) {
$d = json_decode(wp_remote_retrieve_body($res), true);
$display_name = $d['global_name'] ?? $d['username'] ?? $discord_uid;
}
}
// Speichern
WBF_DB::set_user_meta($user->id, 'discord_user_id', $discord_uid);
WBF_DB::set_user_meta($user->id, 'discord_username', $display_name);
// Temp-Daten löschen
WBF_DB::set_user_meta($user->id, 'discord_verify_code', '');
WBF_DB::set_user_meta($user->id, 'discord_verify_expires', '');
WBF_DB::set_user_meta($user->id, 'discord_verify_pending_id', '');
// Rollen-Sync direkt nach Verifikation
$guild = trim($s['discord_guild_id'] ?? '');
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
if ( ($s['discord_role_sync'] ?? '0') === '1' && $token && $guild && $role_map ) {
wbf_sync_discord_role_for_user($user->id, $discord_uid, $token, $guild, $role_map);
}
wp_send_json_success([
'message' => '🎉 Discord erfolgreich verknüpft!',
'connected' => true,
'display_name' => esc_html($display_name),
]);
}
// ── Discord: Verbindung trennen ───────────────────────────────────────────
public static function handle_save_discord() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$action = sanitize_key( $_POST['sub_action'] ?? 'save' );
if ( $action === 'disconnect' ) {
WBF_DB::set_user_meta($user->id, 'discord_username', '');
WBF_DB::set_user_meta($user->id, 'discord_user_id', '');
wp_send_json_success(['message' => 'Discord-Verbindung getrennt.', 'connected' => false]);
}
wp_send_json_error(['message' => 'Unbekannte Aktion.']);
}
// ── Discord Hilfsmethoden ─────────────────────────────────────────────────
private static function discord_find_user_id($username_input, $token, $guild) {
if ( ! $guild ) return null;
// Guild-Member-Search (max. 1 Treffer)
$search = rawurlencode($username_input);
$res = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}/members/search?query={$search}&limit=5", [
'timeout' => 6,
'headers' => ['Authorization' => 'Bot ' . $token],
]);
if ( is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200 ) return null;
$members = json_decode(wp_remote_retrieve_body($res), true);
if ( empty($members) ) return null;
// Exakten Treffer bevorzugen
$input_lower = strtolower($username_input);
foreach ( $members as $m ) {
$uname = strtolower($m['user']['username'] ?? '');
$global = strtolower($m['user']['global_name'] ?? '');
if ( $uname === $input_lower || $global === $input_lower ) {
return $m['user']['id'];
}
}
// Erster Treffer als Fallback
return $members[0]['user']['id'] ?? null;
}
private static function discord_send_dm($user_id, $token, $message) {
// DM-Channel erstellen
$ch_res = wp_remote_post('https://discord.com/api/v10/users/@me/channels', [
'timeout' => 6,
'headers' => ['Authorization' => 'Bot ' . $token, 'Content-Type' => 'application/json'],
'body' => json_encode(['recipient_id' => $user_id]),
]);
if ( is_wp_error($ch_res) || wp_remote_retrieve_response_code($ch_res) !== 200 ) return false;
$channel = json_decode(wp_remote_retrieve_body($ch_res), true);
$ch_id = $channel['id'] ?? '';
if ( ! $ch_id ) return false;
// Nachricht senden
$msg_res = wp_remote_post("https://discord.com/api/v10/channels/{$ch_id}/messages", [
'timeout' => 6,
'headers' => ['Authorization' => 'Bot ' . $token, 'Content-Type' => 'application/json'],
'body' => json_encode(['content' => $message]),
]);
return ( ! is_wp_error($msg_res) && wp_remote_retrieve_response_code($msg_res) === 200 );
}
// ══════════════════════════════════════════════════════════════════════════
// ── 2FA / TOTP ────────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════
/**
* Setup-Schritt 1: Neues Secret generieren und als "pending" speichern.
* Gibt Secret (zur manuellen Eingabe) und otpauth:// URI (für QR-Code) zurück.
*/
public static function handle_2fa_setup_begin() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
$secret = WBF_TOTP::generate_secret();
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_PENDING, $secret );
wp_send_json_success( [
"secret" => $secret,
"uri" => WBF_TOTP::get_otpauth_uri( $user->username, $secret ),
] );
}
/**
* Setup-Schritt 2: Code verifizieren und 2FA aktivieren.
*/
public static function handle_2fa_setup_verify() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_PENDING );
if ( empty($secret) ) {
wp_send_json_error( ["message" => "Kein ausstehender 2FA-Setup. Bitte neu starten."] );
}
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
wp_send_json_error( ["message" => "Ungültiger Code. Bitte Uhrzeit prüfen und erneut versuchen."] );
}
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_SECRET, $secret );
global $wpdb;
$wpdb->delete( "{$wpdb->prefix}forum_user_meta",
["user_id" => $user->id, "meta_key" => WBF_TOTP::META_PENDING], ["%d", "%s"] );
wp_send_json_success( ["message" => "2FA erfolgreich aktiviert!"] );
}
/**
* 2FA deaktivieren (User-seitig).
* Erfordert aktuelles Passwort + gültigen TOTP-Code.
*/
public static function handle_2fa_disable() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
$password = $_POST["password"] ?? "";
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
$fresh = WBF_DB::get_user( $user->id );
if ( ! $fresh || ! password_verify( $password, $fresh->password ) ) {
wp_send_json_error( ["message" => "Falsches Passwort."] );
}
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
if ( empty($secret) ) {
wp_send_json_error( ["message" => "2FA ist nicht aktiv."] );
}
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
wp_send_json_error( ["message" => "Ungültiger Authenticator-Code."] );
}
WBF_TOTP::disable_for( $user->id );
wp_send_json_success( ["message" => "2FA wurde deaktiviert."] );
}
/**
* Login-Schritt 2: TOTP-Code nach erfolgreichem Passwort prüfen.
* Kein Nonce — ausstehende Session-ID ist der Auth-Beweis.
* Brute-Force-Schutz: max. 5 Versuche / IP / 10 Min.
*/
public static function handle_2fa_verify_login() {
WBF_Auth::init();
$ip_key = "wbf_2fa_fail_" . md5( $_SERVER["REMOTE_ADDR"] ?? "unknown" );
$fails = (int) get_transient( $ip_key );
if ( $fails >= 5 ) {
wp_send_json_error( ["message" => "Zu viele Fehlversuche. Bitte warte 10 Minuten.", "locked" => true] );
}
$pending = (int) ( $_SESSION[ WBF_TOTP::SESSION_PENDING ] ?? 0 );
if ( ! $pending ) {
wp_send_json_error( ["message" => "Keine ausstehende Anmeldung. Bitte erneut einloggen."] );
}
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
$user = WBF_DB::get_user( $pending );
if ( ! $user ) {
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
wp_send_json_error( ["message" => "Ungültige Sitzung."] );
}
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
if ( empty($secret) || ! WBF_TOTP::verify( $secret, $code ) ) {
set_transient( $ip_key, $fails + 1, 10 * MINUTE_IN_SECONDS );
wp_send_json_error( ["message" => "Ungültiger Code. Bitte erneut versuchen."] );
}
delete_transient( $ip_key );
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
if ( WBF_Roles::level($user->role) < 0 ) {
wp_send_json_error( ["message" => "Dein Konto ist gesperrt."] );
}
if ( session_id() ) session_regenerate_id( true );
$_SESSION[ WBF_Auth::SESSION_KEY ] = $user->id;
WBF_DB::touch_last_active( $user->id );
if ( ! empty( $_SESSION["wbf_2fa_remember"] ) ) {
WBF_Auth::set_remember_cookie( $user->id );
unset( $_SESSION["wbf_2fa_remember"] );
}
wp_send_json_success( [
"display_name" => $user->display_name,
"avatar_url" => $user->avatar_url,
"user_id" => $user->id,
] );
}
}
add_action( 'init', [ 'WBF_Ajax', 'init' ] );

View File

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

View File

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

View File

@@ -151,7 +151,6 @@ class WBF_DB {
dbDelta( $sql_threads );
dbDelta( $sql_posts );
dbDelta( $sql_likes );
dbDelta( $sql_reports );
dbDelta( $sql_tags );
dbDelta( $sql_thread_tags );
dbDelta( $sql_messages );
@@ -174,6 +173,8 @@ class WBF_DB {
// Zeitlich begrenzte Sperren
self::maybe_add_column("{$wpdb->prefix}forum_users", 'ban_until', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN ban_until DATETIME DEFAULT NULL");
self::maybe_add_column("{$wpdb->prefix}forum_users", 'pre_ban_role', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN pre_ban_role VARCHAR(20) DEFAULT 'member'");
// Profilbanner
self::maybe_add_column("{$wpdb->prefix}forum_users", 'banner_url', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN banner_url VARCHAR(255) DEFAULT ''");
// Thread-Abonnements
$sql_subscriptions = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_subscriptions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -199,7 +200,6 @@ class WBF_DB {
) $charset;";
// Ensure reports + notifications tables exist on existing installs
dbDelta( $sql_reports );
dbDelta( $sql_notifications );
// Einladungs-Tabelle
@@ -279,6 +279,18 @@ class WBF_DB {
) $charset;";
dbDelta( $sql_bookmarks );
// ── Ignore-Liste ──────────────────────────────────────────────────────
$sql_ignore = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_ignore_list (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
ignored_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY user_ignored (user_id, ignored_id),
KEY ignored_id (ignored_id)
) $charset;";
dbDelta( $sql_ignore );
// ── prefix_id zu threads ──────────────────────────────────────────────
self::maybe_add_column( "{$wpdb->prefix}forum_threads", 'prefix_id',
"ALTER TABLE {$wpdb->prefix}forum_threads ADD COLUMN prefix_id BIGINT UNSIGNED DEFAULT NULL" );
@@ -342,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 ) {
@@ -478,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(
@@ -500,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
));
}
@@ -529,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));
}
}
@@ -560,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 ) {
@@ -623,15 +710,16 @@ 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
AND t.status != 'archived' ORDER BY t.created_at DESC LIMIT %d", $limit
WHERE t.status != 'archived' AND t.deleted_at IS NULL
ORDER BY t.created_at DESC LIMIT %d", $limit
));
}
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"),
];
@@ -718,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,
@@ -729,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,
@@ -739,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
@@ -765,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 ) {
@@ -1159,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;
@@ -1174,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
) );
}
@@ -1207,13 +1317,18 @@ class WBF_DB {
global $wpdb;
$token = bin2hex( random_bytes(32) );
$hash = hash( 'sha256', $token );
// Alte Tokens löschen
$wpdb->delete( "{$wpdb->prefix}forum_users", [] ); // nur placeholder
// Altes Token dieses Users zurücksetzen bevor ein neues gesetzt wird
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->prefix}forum_users
SET reset_token=NULL, reset_token_expires=NULL
WHERE id=%d",
(int) $user_id
) );
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->prefix}forum_users
SET reset_token=%s, reset_token_expires=DATE_ADD(NOW(), INTERVAL 1 HOUR)
WHERE id=%d",
$hash, $user_id
$hash, (int) $user_id
) );
return $token; // Klartext-Token → per E-Mail senden
}
@@ -1361,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 ) {
@@ -1458,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(
@@ -1469,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(
@@ -1689,6 +1849,128 @@ class WBF_DB {
));
}
// ── Ignore-Liste ──────────────────────────────────────────────────────────
public static function toggle_ignore( $user_id, $ignored_id ) {
global $wpdb;
$user_id = (int) $user_id;
$ignored_id = (int) $ignored_id;
if ( self::is_ignored( $user_id, $ignored_id ) ) {
$wpdb->delete( "{$wpdb->prefix}forum_ignore_list", [
'user_id' => $user_id,
'ignored_id' => $ignored_id,
] );
return false;
}
$wpdb->replace( "{$wpdb->prefix}forum_ignore_list", [
'user_id' => $user_id,
'ignored_id' => $ignored_id,
] );
return true;
}
public static function is_ignored( $user_id, $ignored_id ) {
global $wpdb;
$table = "{$wpdb->prefix}forum_ignore_list";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return false;
return (bool) $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d AND ignored_id=%d",
(int) $user_id, (int) $ignored_id
) );
}
/** Gibt alle ignorierten User-IDs als int-Array zurück */
public static function get_ignored_ids( $user_id ) {
global $wpdb;
$table = "{$wpdb->prefix}forum_ignore_list";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return [];
$ids = $wpdb->get_col( $wpdb->prepare(
"SELECT ignored_id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d",
(int) $user_id
) );
return array_map( 'intval', $ids );
}
/** Vollständige Ignore-Liste mit User-Daten */
public static function get_ignore_list( $user_id ) {
global $wpdb;
$table = "{$wpdb->prefix}forum_ignore_list";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return [];
return $wpdb->get_results( $wpdb->prepare(
"SELECT u.id, u.username, u.display_name, u.avatar_url, u.role,
il.created_at AS ignored_since
FROM {$wpdb->prefix}forum_ignore_list il
JOIN {$wpdb->prefix}forum_users u ON u.id = il.ignored_id
WHERE il.user_id = %d
ORDER BY il.created_at DESC",
(int) $user_id
) );
}
// ── DSGVO Art. 17: Konto vollständig löschen ──────────────────────────────
public static function delete_user_gdpr( $user_id ) {
global $wpdb;
$user_id = (int) $user_id;
$user = self::get_user( $user_id );
if ( ! $user ) return false;
if ( $user->role === 'superadmin' ) return false;
$wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'from_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'to_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", [ 'user_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'user_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'actor_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_subscriptions", [ 'user_id' => $user_id ] );
$table_bm = "{$wpdb->prefix}forum_bookmarks";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_bm'" ) === $table_bm ) {
$wpdb->delete( $table_bm, [ 'user_id' => $user_id ] );
}
$wpdb->delete( "{$wpdb->prefix}forum_likes", [ 'user_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_reactions", [ 'user_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_reports", [ 'reporter_id' => $user_id ] );
$table_pv = "{$wpdb->prefix}forum_poll_votes";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_pv'" ) === $table_pv ) {
$wpdb->delete( $table_pv, [ 'user_id' => $user_id ] );
}
// Ignore-Liste beidseitig bereinigen
$table_il = "{$wpdb->prefix}forum_ignore_list";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_il'" ) === $table_il ) {
$wpdb->delete( $table_il, [ 'user_id' => $user_id ] );
$wpdb->delete( $table_il, [ 'ignored_id' => $user_id ] );
}
delete_transient( 'wbf_flood_' . $user_id );
delete_transient( 'wbf_flood_ts_' . $user_id );
self::delete_user_meta_all( $user_id );
$anon_hash = substr( hash( 'sha256', $user_id . wp_salt() . microtime( true ) ), 0, 12 );
$wpdb->update(
"{$wpdb->prefix}forum_users",
[
'username' => 'deleted_' . $anon_hash,
'email' => 'deleted_' . $anon_hash . '@deleted.invalid',
'password' => '',
'display_name' => 'Gelöschter Nutzer',
'avatar_url' => '',
'bio' => '',
'signature' => '',
'ban_reason' => '',
'reset_token' => null,
'reset_token_expires' => null,
'pre_ban_role' => '',
'ban_until' => null,
'role' => 'banned',
],
[ 'id' => $user_id ]
);
return true;
}
// ── Wortfilter ────────────────────────────────────────────────────────────
public static function get_word_filter() {
@@ -1711,25 +1993,29 @@ class WBF_DB {
// ── Flood Control ─────────────────────────────────────────────────────────
public static function check_flood( $user_id ) {
$user_id = (int) $user_id;
if ( $user_id <= 0 ) return true; // kein eingeloggter User — kein Flood-Check
$interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 );
if ( $interval <= 0 ) return true; // deaktiviert
$key = 'wbf_flood_' . (int)$user_id;
$key = 'wbf_flood_' . (int)$user_id;
$ts_key = 'wbf_flood_ts_' . (int)$user_id;
$last = get_transient( $key );
if ( $last !== false ) {
return false; // noch gesperrt
}
set_transient( $key, time(), $interval );
set_transient( $key, 1, $interval );
set_transient( $ts_key, time(), $interval + 5 );
return true;
}
public static function flood_remaining( $user_id ) {
$interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 );
if ( $interval <= 0 ) return 0;
$key = 'wbf_flood_' . (int)$user_id;
$last = get_transient( $key );
if ( $last === false ) return 0;
// Transients speichern keine genaue Restzeit — wir schätzen über $interval
return $interval;
$ts_key = 'wbf_flood_ts_' . (int)$user_id;
$sent = get_transient( $ts_key );
if ( $sent === false ) return 0;
$remaining = $interval - ( time() - (int)$sent );
return max( 0, $remaining );
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -3,17 +3,18 @@
* Plugin Name: WP Business Forum
* Plugin URI: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum
* Description: Professionelles Forum mit eigenem Login, Rollen, Signaturen, Hierarchie und Moderations-Tools.
* Version: 1.0.1
* Version: 1.0.5
* Author: M_Viper
* Author URI: https://m-viper.de
* Text Domain: wp-business-forum
* Requires PHP: 7.0
*/
if ( ! defined( 'ABSPATH' ) ) exit;
define( 'WBF_PATH', plugin_dir_path( __FILE__ ) );
define( 'WBF_URL', plugin_dir_url( __FILE__ ) );
define( 'WBF_VERSION', '1.0.1' );
define( 'WBF_VERSION', '1.0.5' );
require_once WBF_PATH . 'includes/class-forum-db.php';
require_once WBF_PATH . 'includes/class-forum-roles.php';
@@ -22,6 +23,10 @@ require_once WBF_PATH . 'includes/class-forum-bbcode.php';
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';
@@ -33,6 +38,38 @@ register_activation_hook( __FILE__, function() {
set_transient( 'wbf_activation_redirect', true, 30 );
});
// ── Export / Import Hooks ─────────────────────────────────────────────────────
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(); } );
@@ -57,6 +94,7 @@ if ( ! wp_next_scheduled( 'wbf_check_expired_bans' ) ) {
register_deactivation_hook( __FILE__, function() {
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
wp_clear_scheduled_hook( 'wbf_check_for_updates' );
} );
@@ -68,14 +106,13 @@ function wbf_get_forum_url() {
$url = get_permalink( $page_id );
if ( $url ) return $url;
}
// 2. Fallback: Seite mit [business_forum] Shortcode suchen
$pages = get_posts([
'post_type' => 'page',
'post_status' => 'publish',
'posts_per_page' => 1,
's' => 'business_forum',
]);
if ( $pages ) return get_permalink( $pages[0]->ID );
// 2. Fallback: Seite mit [business_forum] Shortcode suchen (direkt im post_content)
global $wpdb;
$page_id = $wpdb->get_var( $wpdb->prepare(
"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status = 'publish' AND post_content LIKE %s LIMIT 1",
'%[business_forum]%'
) );
if ( $page_id ) return get_permalink( $page_id );
// 3. Letzter Fallback: aktuelle Seite
return home_url('/');
}
@@ -84,6 +121,10 @@ function wbf_get_forum_url() {
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_style( 'wbf-style', WBF_URL . 'assets/css/forum-style.css', [], WBF_VERSION );
wp_enqueue_script( 'wbf-script', WBF_URL . 'assets/js/forum-script.js', ['jquery'], WBF_VERSION, true );
// 2FA: QR-Code-Bibliothek (nur laden wenn User eingeloggt oder auf Loginseite)
wp_enqueue_script( 'qrcodejs', 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js', [], '1.0.0', true );
wp_add_inline_script( 'wbf-script', wbf_get_2fa_inline_js(), 'after' );
$wbf_user = WBF_Auth::get_current_user();
if ( $wbf_user ) {
WBF_DB::touch_last_active( $wbf_user->id );
@@ -98,4 +139,454 @@ add_action( 'wp_enqueue_scripts', function() {
'forum_url' => wbf_get_forum_url(),
'reactions' => WBF_DB::get_allowed_reactions(),
]);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// ── Update-Checker ────────────────────────────────────────────────────────────
// Prüft täglich gegen die Gitea-Releases-API ob eine neue Version verfügbar ist.
// Releases-URL: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum/releases
// ══════════════════════════════════════════════════════════════════════════════
define( 'WBF_UPDATE_API', 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/WP-Business-Forum/releases?limit=1&page=1' );
define( 'WBF_RELEASES_PAGE', 'https://git.viper.ipv64.net/M_Viper/WP-Business-Forum/releases' );
define( 'WBF_UPDATE_TRANSIENT','wbf_update_check' );
/**
* Holt die neueste Release-Info von Gitea (gecacht per Transient, 12h).
* Gibt null zurück wenn kein Update verfügbar oder API nicht erreichbar.
*
* @return array|null ['version'=>string, 'url'=>string, 'name'=>string, 'published'=>string, 'body'=>string]
*/
function wbf_get_latest_release() {
$cached = get_transient( WBF_UPDATE_TRANSIENT );
if ( $cached !== false ) {
return $cached ?: null; // false = noch nie gecacht, '' = kein Update
}
$response = wp_remote_get( WBF_UPDATE_API, [
'timeout' => 8,
'user-agent' => 'WP-Business-Forum/' . WBF_VERSION . '; ' . get_bloginfo('url'),
'sslverify' => true,
] );
if ( is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200 ) {
// Bei Fehler 1h warten bevor erneut versucht
set_transient( WBF_UPDATE_TRANSIENT, '', HOUR_IN_SECONDS );
return null;
}
$body = wp_remote_retrieve_body( $response );
$releases = json_decode( $body, true );
if ( empty($releases) || ! is_array($releases) || empty($releases[0]) ) {
set_transient( WBF_UPDATE_TRANSIENT, '', 12 * HOUR_IN_SECONDS );
return null;
}
$latest = $releases[0];
$version = ltrim( $latest['tag_name'] ?? '', 'v' ); // "v1.2.0" → "1.2.0"
$info = [
'version' => $version,
'url' => $latest['html_url'] ?? WBF_RELEASES_PAGE,
'name' => $latest['name'] ?? $latest['tag_name'] ?? $version,
'published' => $latest['published_at'] ?? '',
'body' => wp_strip_all_tags( $latest['body'] ?? '' ),
];
// 12 Stunden cachen
set_transient( WBF_UPDATE_TRANSIENT, $info, 12 * HOUR_IN_SECONDS );
return $info;
}
/**
* Prüft ob ein Update verfügbar ist.
* Gibt die Release-Info zurück wenn Gitea-Version > installierte Version.
*/
function wbf_update_available() {
$latest = wbf_get_latest_release();
if ( ! $latest || empty($latest['version']) ) return null;
if ( version_compare( $latest['version'], WBF_VERSION, '>' ) ) {
return $latest;
}
return null;
}
// ── Cron: täglich Update prüfen (Cache warm halten) ──────────────────────────
add_action( 'wbf_check_for_updates', function() {
delete_transient( WBF_UPDATE_TRANSIENT );
wbf_get_latest_release();
} );
if ( ! wp_next_scheduled( 'wbf_check_for_updates' ) ) {
wp_schedule_event( time(), 'twicedaily', 'wbf_check_for_updates' );
}
// ── Admin-Notice wenn Update verfügbar ───────────────────────────────────────
add_action( 'admin_notices', function() {
if ( ! current_user_can('manage_options') ) return;
$update = wbf_update_available();
if ( ! $update ) return;
// Notice ausblenden wenn der User sie weggeklickt hat (per GET-Parameter)
if ( isset($_GET['wbf_dismiss_update']) && check_admin_referer('wbf_dismiss_update') ) {
set_transient( 'wbf_update_dismissed_' . WBF_VERSION, $update['version'], 7 * DAY_IN_SECONDS );
wp_safe_redirect( remove_query_arg(['wbf_dismiss_update','_wpnonce']) );
exit;
}
$dismissed = get_transient( 'wbf_update_dismissed_' . WBF_VERSION );
if ( $dismissed === $update['version'] ) return;
$dismiss_url = wp_nonce_url(
add_query_arg('wbf_dismiss_update', '1'),
'wbf_dismiss_update'
);
$changelog_url = esc_url( $update['url'] );
$new_ver = esc_html( $update['version'] );
$cur_ver = esc_html( WBF_VERSION );
echo "
<div class=\"notice notice-warning is-dismissible\" style=\"border-left-color:#f59e0b;padding:12px 15px\">
<div style=\"display:flex;align-items:center;gap:14px;flex-wrap:wrap\">
<span style=\"font-size:1.6rem\">🔔</span>
<div>
<strong style=\"font-size:.95rem\">WP Business Forum — Update verfügbar!</strong>
<p style=\"margin:.3rem 0 0;color:#374151\">
Version <strong>{$new_ver}</strong> ist verfügbar. Du verwendest <strong>{$cur_ver}</strong>.
</p>
</div>
<div style=\"display:flex;gap:8px;margin-left:auto\">
<a href=\"{$changelog_url}\" target=\"_blank\" rel=\"noopener\"
class=\"button button-primary\" style=\"background:#f59e0b;border-color:#d97706\">
📋 Changelog & Download
</a>
<a href=\"" . esc_url($dismiss_url) . "\" class=\"button\">Später erinnern</a>
</div>
</div>
</div>";
} );
// ── Update-Badge im WP-Admin-Menü ─────────────────────────────────────────────
add_action( 'admin_menu', function() {
$update = wbf_update_available();
if ( ! $update ) return;
global $menu;
if ( ! is_array($menu) ) return;
foreach ( $menu as &$item ) {
if ( isset($item[2]) && $item[2] === 'wbf-admin' ) {
$item[0] .= ' <span class="update-plugins"><span class="plugin-count">1</span></span>';
break;
}
}
}, 999 );
// ── Manuellen Cache-Reset erlauben (für die Admin-UI) ─────────────────────────
add_action( 'admin_init', function() {
if ( ! isset($_GET['wbf_refresh_update']) ) return;
if ( ! current_user_can('manage_options') ) return;
if ( ! check_admin_referer('wbf_refresh_update') ) return;
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;
}