Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4d0ec73c0 | |||
| 56f8c01b52 | |||
| 0efd52d893 | |||
| 605df075cd | |||
| 94f1ac46aa | |||
| 8c2955a2cf | |||
| 689fd0c77b | |||
| e2c4e31b4b | |||
| bd87c795f9 | |||
| 989db0786a | |||
| 4a593677dd |
368
README.md
368
README.md
@@ -1,17 +1,17 @@
|
||||
# WP Business Forum — Anwender-Dokumentation
|
||||
# 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.
|
||||
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
|
||||
@@ -24,29 +24,19 @@ Installation bis zum Live-Betrieb findest du hier alle wichtigen Schritte und Fu
|
||||
10. Export, Import und Deinstallation
|
||||
11. FAQ / Troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## 1) Über das Plugin
|
||||
|
||||
WP Business Forum ist ein eigenständiges Foren-System für WordPress mit:
|
||||
|
||||
- eigenem Forum-Login (unabhängig vom WP-Login)
|
||||
- Rollen- und Rechteverwaltung
|
||||
- Kategorien mit Hierarchie
|
||||
- Moderationswerkzeugen
|
||||
- Direktnachrichten, Benachrichtigungen, Meldesystem
|
||||
- Umfragen, Tags, Reaktionen, Lesezeichen
|
||||
- Level-System (beitragsbasierte Rangstufen)
|
||||
- Vollständigem Export / Import mit automatischer ID-Zuordnung
|
||||
|
||||
Das Forum wird per Shortcode in eine WordPress-Seite eingebunden.
|
||||
|
||||
---
|
||||
|
||||
## 2) Funktionsübersicht
|
||||
|
||||
### Für Mitglieder
|
||||
|
||||
- Registrieren / Einloggen / Logout
|
||||
- Passwort vergessen und Reset per E-Mail
|
||||
- Threads erstellen, antworten, bearbeiten
|
||||
@@ -58,339 +48,243 @@ Das Forum wird per Shortcode in eine WordPress-Seite eingebunden.
|
||||
- Private Nachrichten (DM)
|
||||
- Profil mit Avatar, Bio, Signatur und eigenen Profilfeldern
|
||||
- Mitgliederliste und Suchfunktion
|
||||
- Andere Nutzer ignorieren / blockieren
|
||||
|
||||
### Für Moderation / Admin
|
||||
|
||||
- Threads pinnen, schließen, archivieren, verschieben, löschen
|
||||
- Beiträge löschen und wiederherstellen (Papierkorb)
|
||||
- Beiträge löschen
|
||||
- Meldungen (Reports) bearbeiten
|
||||
- Kategorien und Rollen verwalten
|
||||
- Mitglieder verwalten: Rolle ändern, Profil bearbeiten, Sperren, Löschen
|
||||
- Einladungssystem für Registrierung
|
||||
- Wartungsmodus
|
||||
- Wortfilter / Zensurliste
|
||||
- Statistiken und Aktivitäts-Dashboard
|
||||
- Export / Import (vollständiges Backup mit Wortfilter, Ignore-Liste, Präfixen u. v. m.)
|
||||
|
||||
---
|
||||
- Wortfilter
|
||||
- Statistiken
|
||||
- Papierkorb / Wiederherstellung
|
||||
- Export / Import
|
||||
|
||||
## 3) Voraussetzungen
|
||||
|
||||
- Laufende WordPress-Installation (empfohlen: aktuelle Version)
|
||||
- PHP 7.4 oder höher (empfohlen: PHP 8.0+)
|
||||
- MySQL 5.7 / MariaDB 10.3 oder höher
|
||||
- Schreibrechte für WordPress-Uploads (für Avatar- und Bild-Uploads)
|
||||
- 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)
|
||||
|
||||
> Das Plugin nutzt eigene Datenbanktabellen mit dem Präfix `wp_forum_*` (bzw. deinem konfigurierten Tabellenpräfix).
|
||||
|
||||
---
|
||||
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.
|
||||
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:
|
||||
|
||||
Nach der Aktivierung führt der Wizard durch drei Schritte:
|
||||
|
||||
1. Superadmin-Konto erstellen oder bestehendes Forum-Konto verknüpfen
|
||||
1. Superadmin-Konto erstellen oder bestehendes Forum-Konto hochstufen
|
||||
2. Optional automatisch eine Forum-Seite erzeugen
|
||||
3. Abschluss und Weiterleitung ins Dashboard
|
||||
3. Abschluss
|
||||
|
||||
**Wichtig:**
|
||||
- Der Superadmin ist fest mit dem WordPress-Administrator verknüpft und kann nicht über den Import überschrieben werden.
|
||||
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]
|
||||
```
|
||||
|
||||
**Empfehlung:**
|
||||
- Eine eigene Seite (z. B. „Forum") anlegen
|
||||
Empfohlen:
|
||||
- Eine eigene Seite (z. B. "Forum") anlegen
|
||||
- Nur diesen Shortcode als Seiteninhalt verwenden
|
||||
- Die Seite in der WordPress-Navigation verlinken
|
||||
|
||||
---
|
||||
|
||||
## 7) Bedienung im Frontend (Mitglieder)
|
||||
|
||||
### 7.1 Registrierung und Login
|
||||
|
||||
- Die Registrierung kann offen, nur per Einladung oder deaktiviert sein.
|
||||
- Registrierung kann offen, nur per Einladung oder deaktiviert sein.
|
||||
- Optional müssen Nutzer die Forum-Regeln akzeptieren.
|
||||
- Spam-Schutz bei der Registrierung:
|
||||
- Spam-Schutz bei Registrierung:
|
||||
- Honeypot-Feld
|
||||
- Mindestzeit bis zum Formular-Absenden
|
||||
- Login unterstützt „Angemeldet bleiben" (Remember-Me Cookie, 30 Tage).
|
||||
- Mindestzeit bis Formular-Absenden
|
||||
- Login unterstützt "Angemeldet bleiben" (Remember-Me Cookie).
|
||||
|
||||
### 7.2 Kategorien und Threads
|
||||
|
||||
- Kategorien können verschachtelt sein (Hauptkategorie + Unterkategorien).
|
||||
- Die Sichtbarkeit kann rollenbasiert eingeschränkt werden.
|
||||
- Threads können folgende Zustände haben: offen · geschlossen · archiviert · gepinnt
|
||||
- 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
|
||||
- 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 (`[b]`, `[i]`, `[quote]`, `[code]`, `[spoiler]`, `[url]`, `[img]` u. v. m.)
|
||||
- Antworten mit BBCode-Unterstützung
|
||||
- Flood-Control: konfigurierbare Wartezeit zwischen Posts
|
||||
- Eigene Posts können nur innerhalb des konfigurierten Bearbeitungsfensters geändert werden
|
||||
- Moderation kann unabhängig davon jederzeit eingreifen
|
||||
- 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 anfügen
|
||||
- Umfrage direkt beim Thread-Erstellen oder nachträglich im Thread
|
||||
- 2 bis 10 Antwortoptionen
|
||||
- Optional Mehrfachauswahl
|
||||
- Optional Enddatum
|
||||
- Nach der Abstimmung werden Ergebnisse direkt angezeigt
|
||||
- Nach Abstimmung werden Ergebnisse direkt angezeigt
|
||||
|
||||
### 7.6 Reaktionen, Likes, Lesezeichen
|
||||
|
||||
- Likes auf Threads und Beiträge
|
||||
- Likes auf Thread/Beitrag
|
||||
- Emoji-Reaktionen (adminseitig konfigurierbar)
|
||||
- Lesezeichen für Threads, im Profil jederzeit einsehbar
|
||||
- Lesezeichen für Threads (im Profil einsehbar)
|
||||
|
||||
### 7.7 Private Nachrichten (DM)
|
||||
|
||||
- 1:1 Nachrichten zwischen Mitgliedern
|
||||
- Inbox-Ansicht und Konversationsansicht
|
||||
- Ungelesene Nachrichten werden im Header gezählt
|
||||
- Inbox-Ansicht und Konversation
|
||||
- Ungelesene Nachrichten werden gezählt
|
||||
- Optional E-Mail-Hinweis bei neuer Nachricht
|
||||
|
||||
### 7.8 Benachrichtigungen
|
||||
|
||||
Benachrichtigungen werden ausgelöst bei:
|
||||
|
||||
- Antworten auf abonnierte Threads
|
||||
- @Erwähnungen in Beiträgen
|
||||
- Neuen privaten Nachrichten
|
||||
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 (max. 2 MB, JPG/PNG/GIF/WebP)
|
||||
- Avatar hochladen
|
||||
- Passwort ändern
|
||||
- Profil-Sichtbarkeit umschalten
|
||||
- Benutzerdefinierte Profilfelder ausfüllen (falls vom Admin aktiviert)
|
||||
- Andere Nutzer zur Ignore-Liste hinzufügen
|
||||
- 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)
|
||||
- Avatar: max. 2 MB (JPG/PNG/GIF/WebP)
|
||||
- Bild im Beitrag: max. 5 MB (JPG/PNG/GIF/WebP)
|
||||
|
||||
### 7.10 Passwort vergessen
|
||||
|
||||
Über „Passwort vergessen" kann ein Reset-Link per E-Mail angefordert werden. Das Zurücksetzen erfolgt über einen zeitlich begrenzten Token.
|
||||
|
||||
---
|
||||
- Ü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:
|
||||
|
||||
Im WordPress-Backend gibt es den Menüpunkt **Business Forum** mit folgenden Unterseiten:
|
||||
|
||||
| Unterseite | Funktion |
|
||||
|---|---|
|
||||
| Übersicht | Dashboard mit Kennzahlen, Trends und Aktivitätsprotokoll |
|
||||
| Kategorien | Struktur, Hierarchie und Sichtbarkeit verwalten |
|
||||
| Rollen | Rollen, Permissions und Design anpassen |
|
||||
| Level | Beitragsbasierte Rangstufen konfigurieren |
|
||||
| Mitglieder | Nutzer verwalten, sperren, löschen |
|
||||
| Meldungen | Gemeldete Inhalte bearbeiten |
|
||||
| Profilfelder | Eigene Felder definieren |
|
||||
| Einstellungen | Texte, Sicherheit, Registrierung, Regeln, Wartung |
|
||||
| Reaktionen | Erlaubte Emoji-Reaktionen konfigurieren |
|
||||
| Einladungen | Invite-Codes erstellen und verwalten |
|
||||
| Statistiken | Forum-Auswertung und Trends |
|
||||
| Papierkorb | Gelöschte Inhalte einsehen und wiederherstellen |
|
||||
| Thread-Präfixe | Farbige Label für Threads verwalten |
|
||||
| Wortfilter | Unerwünschte Begriffe automatisch ersetzen |
|
||||
| Export / Import | Vollständiges Backup und Wiederherstellung |
|
||||
| ⚠️ Deinstallieren | Komplette Löschung inkl. aller Daten |
|
||||
| 🔔 Updates | Update-Status und Changelog |
|
||||
|
||||
### 8.1 Mitglieder verwalten
|
||||
|
||||
In der Mitglieder-Übersicht stehen pro Nutzer drei Aktionen zur Verfügung:
|
||||
|
||||
**Rolle ändern**
|
||||
Rolle direkt aus dem Dropdown wählen und speichern. Bei „Gesperrt" kann zusätzlich ein Sperrgrund und ein automatisches Ablaufdatum (temporäre Sperre) gesetzt werden.
|
||||
|
||||
**Profil bearbeiten**
|
||||
Anzeigename, E-Mail, Passwort, Bio, Signatur und benutzerdefinierte Profilfelder direkt im Admin ändern.
|
||||
|
||||
**Nutzer löschen**
|
||||
Beim Klick auf „Löschen" öffnet sich ein Bestätigungs-Panel mit zwei Optionen:
|
||||
|
||||
- **DSGVO Anonymisieren** *(empfohlen)*: Der Account wird nach Art. 17 DSGVO anonymisiert — Benutzername, E-Mail und Passwort werden gelöscht, Threads und Beiträge bleiben unter „Gelöschter Nutzer" erhalten.
|
||||
- **Dauerhaft löschen**: Der Datensatz wird vollständig aus der Datenbank entfernt. Alle nutzerbezogenen Daten (Nachrichten, Likes, Reaktionen, Abonnements, Lesezeichen u. a.) werden gelöscht. Threads und Beiträge bleiben anonym erhalten. **Dieser Vorgang ist nicht rückgängig zu machen.**
|
||||
|
||||
> Der Superadmin-Account ist in beiden Pfaden geschützt und kann nicht gelöscht werden.
|
||||
|
||||
### 8.2 Sperren von Nutzern
|
||||
|
||||
Statt eines vollständigen Löschens kann ein Nutzer auch gesperrt werden (Rolle „Gesperrt"):
|
||||
|
||||
- **Permanente Sperre**: Kein Forum-Zugang, Sperrgrund wird beim Login angezeigt.
|
||||
- **Temporäre Sperre**: Automatische Entsperrung zum angegebenen Datum/Uhrzeit. Bei Ablauf wird die vorherige Rolle automatisch wiederhergestellt.
|
||||
|
||||
---
|
||||
- Ü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**:
|
||||
Unter Business Forum > Einstellungen:
|
||||
|
||||
### 9.1 Texte und UI
|
||||
|
||||
- Hero-Titel und Untertitel
|
||||
- Hero-Titel/Untertitel
|
||||
- Topbar-Brand
|
||||
- Labels für Statistiken
|
||||
- Abschnittstitel und Buttontexte
|
||||
- Label für Statistik
|
||||
- Abschnittstitel
|
||||
- Buttontexte
|
||||
- Sidebar-Titel
|
||||
|
||||
### 9.2 Sicherheit
|
||||
|
||||
- Auto-Logout nach Inaktivität (0 = deaktiviert, in Minuten)
|
||||
- Post-Bearbeitungslimit (in Minuten, 0 = unbegrenzt)
|
||||
- Spam-Mindestzeit bei Registrierung (in Sekunden)
|
||||
- Flood-Control Intervall zwischen Posts (in Sekunden, 0 = deaktiviert)
|
||||
- Standard-Profil-Sichtbarkeit für neue Mitglieder
|
||||
- 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 bei Einladungs-Modus
|
||||
- Forum-Regeln bei Registrierung verpflichtend akzeptieren
|
||||
- Modus:
|
||||
- offen
|
||||
- nur Einladung
|
||||
- deaktiviert
|
||||
- Freitext-Hinweis für Einladungsmode
|
||||
|
||||
### 9.4 Wartungsmodus
|
||||
|
||||
- Forum für normale Nutzer sperren
|
||||
- Moderation und Admins behalten vollen Zugriff
|
||||
- Eigener Wartungs-Titel und Hinweistext konfigurierbar
|
||||
- Moderation/Admin behalten Zugriff
|
||||
- Eigener Wartungs-Titel und Hinweistext
|
||||
|
||||
### 9.5 Forum-Regeln / Nutzungsbedingungen
|
||||
|
||||
- Regelseite aktivieren / deaktivieren
|
||||
- Regelseite aktivieren/deaktivieren
|
||||
- Akzeptierung bei Registrierung optional verpflichtend
|
||||
- Titel und Inhalt frei editierbar (unterstützt einfaches Markdown)
|
||||
|
||||
---
|
||||
- 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
|
||||
|
||||
### 10.1 Export
|
||||
Empfehlung:
|
||||
- Vor großen Änderungen immer einen Voll-Export speichern.
|
||||
|
||||
Unter **Business Forum › Export / Import** kannst du einzelne oder alle Bereiche als `.json`-Datei exportieren:
|
||||
|
||||
| Bereich | Enthält |
|
||||
|---|---|
|
||||
| Einstellungen & Wortfilter | Forum-Texte, Regeln, Labels, Auto-Logout, Wortfilter, Profilfeld-Definitionen, Reaktionen-Konfiguration |
|
||||
| Rollen | Alle Rollen mit Berechtigungen und Design (Superadmin wird nie überschrieben) |
|
||||
| Level-System | Level-Namen, Schwellenwerte, Icons, Farben, An/Aus-Status |
|
||||
| Kategorien | Kategoriestruktur inkl. Eltern-Kind-Hierarchie, Icons, Min-Rolle |
|
||||
| Benutzer & Profilfelder | Accounts inkl. Passwort-Hashes, Ban-Status, Profilfeld-Werte |
|
||||
| Threads, Posts & Abonnements | Alle Inhalte inkl. Tag-Zuordnungen und Thread-Abonnements |
|
||||
| Umfragen | Alle Umfragen inkl. Abstimmungen |
|
||||
| Lesezeichen | Alle gespeicherten Thread-Lesezeichen |
|
||||
| Thread-Präfixe | Alle Präfix-Labels, Farben und Reihenfolgen |
|
||||
| Likes & Reaktionen | Likes, Emoji-Reaktionen, Benachrichtigungen |
|
||||
| Privatnachrichten | Alle DM-Konversationen |
|
||||
| Ignore-Liste | Alle gegenseitigen Nutzer-Blockierungen |
|
||||
| Meldungen | Gemeldete Beiträge inkl. Status |
|
||||
| Einladungen | Alle Einladungscodes inkl. Nutzungsanzahl und Ablaufdatum |
|
||||
|
||||
**Tipp:** Mit „Alle wählen" / „Keine" lässt sich die Auswahl schnell anpassen. Die Datei wird sofort heruntergeladen.
|
||||
|
||||
### 10.2 Import
|
||||
|
||||
Beim Import einer zuvor exportierten `.json`-Datei gilt:
|
||||
|
||||
- Maximale Dateigröße: **50 MB**
|
||||
- Nur Dateien im WBF-Format werden akzeptiert
|
||||
- **Benutzer-IDs werden beim Import automatisch gemappt** — Threads, Posts, Likes, Reaktionen und alle anderen nutzerbezogenen Daten werden korrekt auf die neuen Datenbank-IDs übertragen, auch wenn sich diese von der Quelldatenbank unterscheiden
|
||||
- Nach dem Import werden alle Zähler (Beitrags-, Thread- und Reaktionszähler) automatisch neu berechnet
|
||||
- Der Superadmin kann per Import nie überschrieben werden
|
||||
|
||||
Über die **Überschreiben-Optionen** lässt sich pro Bereich steuern, ob bestehende Daten ersetzt oder Duplikate übersprungen werden sollen.
|
||||
|
||||
> ⚠️ Erstelle vor jedem Import einen aktuellen Export als Sicherung. Benutzer-Exporte enthalten Passwort-Hashes — teile diese Dateien nicht öffentlich.
|
||||
|
||||
### 10.3 Deinstallation
|
||||
|
||||
Unter **Business Forum › ⚠️ Deinstallieren** oder beim Löschen des Plugins im WordPress-Backend werden vollständig entfernt:
|
||||
|
||||
- Alle Forum-Datenbanktabellen (`wp_forum_*`)
|
||||
- Alle Plugin-Optionen in `wp_options`
|
||||
### 10.2 Deinstallation (wichtig)
|
||||
Beim Löschen des Plugins werden komplett entfernt:
|
||||
- alle Forum-Datenbanktabellen
|
||||
- relevante Plugin-Optionen
|
||||
- Transients
|
||||
- Geplante Cron-Jobs
|
||||
- Automatisch erstellte Forum-Seite
|
||||
- Upload-Unterverzeichnis `wbf-avatars`
|
||||
- geplanter Cron-Job
|
||||
- automatisch erstellte Forum-Seite
|
||||
- zugehörige Upload-Unterverzeichnisse
|
||||
|
||||
> **Das ist eine echte, unwiderrufliche Datenlöschung. Immer vorher einen vollständigen Export erstellen.**
|
||||
|
||||
---
|
||||
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
|
||||
|
||||
**Login funktioniert nicht**
|
||||
Prüfen ob das Konto gesperrt ist. Bei temporärer Sperre das Ablaufdatum abwarten. Bei „Nur Einladung" einen gültigen Invite-Code verwenden.
|
||||
### Registrierung nicht sichtbar
|
||||
- In Einstellungen den Registrierungsmodus prüfen
|
||||
- Bei deaktiviertem Modus ist keine Selbstregistrierung möglich
|
||||
|
||||
**Registrierung nicht sichtbar**
|
||||
In den Einstellungen den Registrierungsmodus prüfen. Bei deaktiviertem Modus ist keine Selbstregistrierung möglich.
|
||||
### Keine E-Mails kommen an
|
||||
- WordPress-Mailversand prüfen (SMTP Plugin empfohlen)
|
||||
- Admin-E-Mail in WordPress kontrollieren
|
||||
|
||||
**Keine E-Mails kommen an**
|
||||
WordPress-Mailversand prüfen. Ein SMTP-Plugin wird empfohlen. Die Admin-E-Mail in WordPress kontrollieren.
|
||||
### Upload von Bildern/Avatar scheitert
|
||||
- Dateityp prüfen (nur JPG/PNG/GIF/WebP)
|
||||
- Dateigröße prüfen (Avatar 2 MB, Beitrag 5 MB)
|
||||
- Schreibrechte in Uploads prüfen
|
||||
|
||||
**Upload von Bildern / Avatar scheitert**
|
||||
Dateityp prüfen (nur JPG/PNG/GIF/WebP). Dateigröße prüfen (Avatar 2 MB, Beitrag 5 MB). Schreibrechte im Uploads-Verzeichnis prüfen.
|
||||
### Benutzer werden automatisch ausgeloggt
|
||||
- Auto-Logout in den Forum-Einstellungen prüfen
|
||||
|
||||
**Import schlägt fehl oder überschreibt falsche Daten**
|
||||
Sicherstellen, dass die Datei aus einer WBF-Installation stammt. Überschreiben-Optionen gezielt setzen. Bei sehr großen Backups `upload_max_filesize` und `post_max_size` in der `php.ini` erhöhen.
|
||||
### Forum ist plötzlich "offline"
|
||||
- Wartungsmodus in den Einstellungen deaktivieren
|
||||
|
||||
**Benutzer werden automatisch ausgeloggt**
|
||||
Auto-Logout in den Forum-Einstellungen prüfen (Wert in Minuten, 0 = deaktiviert).
|
||||
|
||||
**Forum ist plötzlich „offline"**
|
||||
Wartungsmodus in den Einstellungen deaktivieren.
|
||||
|
||||
**Suche liefert keine Ergebnisse**
|
||||
Der Suchbegriff muss mindestens 2 Zeichen lang sein.
|
||||
|
||||
**Nach dem Import stimmen Beitragszähler nicht**
|
||||
Ab Version 1.0.2 werden Zähler nach jedem Import automatisch neu berechnet. Bei älteren Imports einmalig einen neuen Import mit der aktuellen Version durchführen.
|
||||
### 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
|
||||
5. Regeln/Nutzungsbedingungen hinterlegen
|
||||
6. E-Mail-Versand testen
|
||||
7. Vollständigen Backup-Export erstellen
|
||||
7. Backup-Export erstellen
|
||||
|
||||
Viel Erfolg mit deinem Forum!
|
||||
@@ -30,6 +30,7 @@ add_action( 'admin_menu', function() {
|
||||
add_submenu_page( 'wbf-admin', 'Thread-Präfixe','Thread-Präfixe','manage_options', 'wbf-prefixes', 'wbf_admin_prefixes' );
|
||||
add_submenu_page( 'wbf-admin', 'Wortfilter', 'Wortfilter', 'manage_options', 'wbf-wordfilter', 'wbf_admin_wordfilter' );
|
||||
add_submenu_page( 'wbf-admin', 'Export / Import','Export / Import','manage_options', 'wbf-export', 'wbf_admin_export' );
|
||||
add_submenu_page( 'wbf-admin', '🎮 Discord', '🎮 Discord', 'manage_options', 'wbf-discord', 'wbf_admin_discord' );
|
||||
add_submenu_page( 'wbf-admin', '⚠️ Deinstallieren', '⚠️ Deinstallieren', 'manage_options', 'wbf-uninstall', 'wbf_admin_uninstall' );
|
||||
add_submenu_page( 'wbf-admin', '🔔 Updates', '🔔 Updates', 'manage_options', 'wbf-updates', 'wbf_admin_updates' );
|
||||
}, 10 );
|
||||
@@ -366,6 +367,11 @@ function wbf_admin_page() {
|
||||
$existing = $wpdb->get_col("SHOW TABLES LIKE '{$wpdb->prefix}forum_%'");
|
||||
$missing = array_filter($exp_tables, fn($t) => !in_array($wpdb->prefix.$t, $existing));
|
||||
|
||||
// ── MC Bridge StatusAPI ───────────────────────────────────────────────────
|
||||
$mc_s = wbf_get_settings();
|
||||
$mc_enabled = ! empty( $mc_s['mc_bridge_enabled'] );
|
||||
$mc_api_url = trim( $mc_s['mc_bridge_api_url'] ?? '' );
|
||||
|
||||
// ── Trends ────────────────────────────────────────────────────────────────
|
||||
$pt = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)");
|
||||
$pl = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL AND created_at BETWEEN DATE_SUB(NOW(), INTERVAL 14 DAY) AND DATE_SUB(NOW(), INTERVAL 7 DAY)");
|
||||
@@ -481,6 +487,11 @@ function wbf_admin_page() {
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ SYSTEM BAR ════════════════════════════════════════════════ -->
|
||||
<?php
|
||||
// Plugin-Status prüfen
|
||||
$gallery_active = class_exists('MC_Gallery_Core');
|
||||
$shop_active = class_exists('WIS_DB');
|
||||
?>
|
||||
<div class="wbf-sysbar">
|
||||
<span class="wbf-sysbar__label">System</span>
|
||||
<span class="wbf-sbadge <?php echo $php_rec?'wbf-sbadge--ok':($php_ok?'wbf-sbadge--warn':'wbf-sbadge--err'); ?>">
|
||||
@@ -493,11 +504,57 @@ function wbf_admin_page() {
|
||||
<i class="fas fa-<?php echo $mail_ok?'envelope-circle-check':'xmark'; ?>"></i> wp_mail
|
||||
</span>
|
||||
<div class="wbf-sdivider"></div>
|
||||
<span class="wbf-sbadge <?php echo $gallery_active?'wbf-sbadge--ok':'wbf-sbadge--err'; ?>">
|
||||
<i class="fas fa-images"></i> Galerie: <?php echo $gallery_active?'Aktiv':'Nicht aktiv'; ?>
|
||||
</span>
|
||||
<span class="wbf-sbadge <?php echo $shop_active?'wbf-sbadge--ok':'wbf-sbadge--err'; ?>">
|
||||
<i class="fas fa-shopping-cart"></i> Shop: <?php echo $shop_active?'Aktiv':'Nicht aktiv'; ?>
|
||||
</span>
|
||||
<div class="wbf-sdivider"></div>
|
||||
<?php if (empty($missing)): ?>
|
||||
<span class="wbf-sbadge wbf-sbadge--ok"><i class="fas fa-table-columns"></i> <?php echo count($exp_tables); ?> Tabellen OK</span>
|
||||
<?php else: ?>
|
||||
<span class="wbf-sbadge wbf-sbadge--err"><i class="fas fa-triangle-exclamation"></i> Fehlende Tabellen: <?php echo esc_html(implode(', ',$missing)); ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="wbf-sdivider"></div>
|
||||
<?php if ( ! $mc_enabled ) : ?>
|
||||
<span class="wbf-sbadge" style="color:#94a3b8;border-color:#e2e8f0;background:#f8fafc" title="MC Bridge in den Einstellungen aktivieren">
|
||||
<i class="fas fa-cubes"></i> MC Bridge: Aus
|
||||
</span>
|
||||
<?php elseif ( empty( $mc_api_url ) ) : ?>
|
||||
<span class="wbf-sbadge wbf-sbadge--warn" title="Keine API-URL konfiguriert">
|
||||
<i class="fas fa-cubes"></i> StatusAPI: Nicht konfiguriert
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span id="wbf-mc-status-badge" class="wbf-sbadge wbf-sbadge--err" title="Verbindung wird geprüft...">
|
||||
<i class="fas fa-spinner fa-spin"></i> StatusAPI: Prüfe...
|
||||
</span>
|
||||
<script>
|
||||
(function() {
|
||||
var badge = document.getElementById('wbf-mc-status-badge');
|
||||
if (!badge) return;
|
||||
var url = <?php echo json_encode( rest_url( 'mc-bridge/v1/status' ) ); ?>;
|
||||
fetch(url, { cache: 'no-store' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d && d.success && d.enabled) {
|
||||
badge.className = 'wbf-sbadge wbf-sbadge--ok';
|
||||
badge.title = 'MC Bridge aktiv — Verbindung hergestellt';
|
||||
badge.innerHTML = '<i class="fas fa-cubes"></i> StatusAPI: Verbunden';
|
||||
} else {
|
||||
badge.className = 'wbf-sbadge wbf-sbadge--err';
|
||||
badge.title = 'MC Bridge deaktiviert oder Fehler';
|
||||
badge.innerHTML = '<i class="fas fa-cubes"></i> StatusAPI: Nicht verbunden';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
badge.className = 'wbf-sbadge wbf-sbadge--err';
|
||||
badge.title = 'WordPress REST-Endpoint nicht erreichbar';
|
||||
badge.innerHTML = '<i class="fas fa-cubes"></i> StatusAPI: Nicht verbunden';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<span style="margin-left:auto;font-size:.7rem;color:#94a3b8"><?php echo $online_count; ?> gerade online</span>
|
||||
</div>
|
||||
|
||||
@@ -1345,15 +1402,61 @@ function wbf_admin_members() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── Admin: 2FA eines Users zurücksetzen ──────────────────────────────────
|
||||
if ( isset( $_POST['wbf_admin_reset_2fa'] ) && check_admin_referer( 'wbf_admin_2fa_nonce' ) ) {
|
||||
if ( current_user_can('manage_options') && class_exists('WBF_TOTP') ) {
|
||||
$uid = (int) ( $_POST['user_id'] ?? 0 );
|
||||
if ( $uid ) {
|
||||
$target = WBF_DB::get_user( $uid );
|
||||
if ( $target && $target->role !== WBF_Roles::SUPERADMIN ) {
|
||||
WBF_TOTP::disable_for( $uid );
|
||||
echo '<div class="notice notice-success is-dismissible"><p>2FA für <strong>'
|
||||
. esc_html($target->display_name) . '</strong> zurückgesetzt.</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$members = WBF_DB::get_all_users( 200 );
|
||||
$members = WBF_DB::get_all_users( 200 );
|
||||
$s_discord = wbf_get_settings();
|
||||
$dc_sync_on = ( $s_discord['discord_role_sync'] ?? '0' ) === '1' && trim( $s_discord['discord_bot_token'] ?? '' );
|
||||
|
||||
// Discord-Meta aller User vorladen (1 Query statt N)
|
||||
$dc_meta = [];
|
||||
if ( $dc_sync_on ) {
|
||||
global $wpdb;
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT user_id,
|
||||
MAX(CASE WHEN meta_key='discord_user_id' THEN meta_value END) AS discord_uid,
|
||||
MAX(CASE WHEN meta_key='discord_username' THEN meta_value END) AS discord_name
|
||||
FROM {$wpdb->prefix}forum_user_meta
|
||||
WHERE meta_key IN ('discord_user_id','discord_username')
|
||||
GROUP BY user_id"
|
||||
);
|
||||
foreach ( $rows as $r ) {
|
||||
$dc_meta[ (int)$r->user_id ] = $r;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1 style="display:flex;align-items:center;justify-content:space-between">
|
||||
<h1 style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
|
||||
<span>Mitglieder</span>
|
||||
<button type="button" class="button button-primary" onclick="document.getElementById('wbf-create-user-box').style.display=document.getElementById('wbf-create-user-box').style.display==='none'?'block':'none'">
|
||||
+ Neuen Nutzer anlegen
|
||||
</button>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<?php if ( $dc_sync_on ): ?>
|
||||
<button type="button" id="wbf-discord-sync-all-btn" class="button"
|
||||
style="background:#5865f2;color:#fff;border-color:#4752c4;display:inline-flex;align-items:center;gap:5px"
|
||||
title="Synchronisiert Discord-Rollen aller verknüpften Nutzer (Discord → Forum)">
|
||||
<span class="dashicons dashicons-update" id="wbf-sync-icon" style="margin-top:3px"></span>
|
||||
Discord-Rollen synchronisieren
|
||||
</button>
|
||||
<span id="wbf-discord-sync-result" style="font-weight:600;font-size:.85rem"></span>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="button button-primary"
|
||||
onclick="document.getElementById('wbf-create-user-box').style.display=
|
||||
document.getElementById('wbf-create-user-box').style.display==='none'?'block':'none'">
|
||||
+ Neuen Nutzer anlegen
|
||||
</button>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<!-- Neuen Nutzer anlegen -->
|
||||
@@ -1409,6 +1512,7 @@ function wbf_admin_members() {
|
||||
<th>#</th><th>Nutzer</th><th>E-Mail</th>
|
||||
<th>Aktuelle Rolle</th><th>Beiträge</th>
|
||||
<th>Registriert</th><th>Rolle ändern</th>
|
||||
<?php if ( $dc_sync_on ): ?><th style="color:#5865f2"><i class="fab fa-discord"></i> Discord</th><?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -1417,9 +1521,17 @@ function wbf_admin_members() {
|
||||
$color = esc_attr( $role['color'] );
|
||||
$bg = esc_attr( $role['bg_color'] );
|
||||
$icon = esc_attr( $role['icon'] ?? 'fas fa-user' );
|
||||
$is_sa = ( $m->role === WBF_Roles::SUPERADMIN );
|
||||
// Nur sperren wenn dieser Forum-User wirklich dem WP-Superadmin (ID 1) entspricht.
|
||||
// Reine Rollen-Prüfung reicht nicht — sonst kann man versehentlich
|
||||
// zugewiesene superadmin-Rollen nicht mehr korrigieren.
|
||||
$wp_sa_data = get_userdata( WBF_Roles::get_wp_superadmin_id() );
|
||||
$is_sa = ( $m->role === WBF_Roles::SUPERADMIN )
|
||||
&& $wp_sa_data
|
||||
&& ( strtolower($m->email) === strtolower($wp_sa_data->user_email) );
|
||||
$ban_reason = esc_attr( $m->ban_reason ?? '' );
|
||||
$opts = '';
|
||||
$dc_user = $dc_meta[ (int)$m->id ] ?? null;
|
||||
$has_dc = $dc_sync_on && $dc_user && ! empty( $dc_user->discord_uid );
|
||||
foreach ( $roles as $k => $r ) {
|
||||
if ( $k === WBF_Roles::SUPERADMIN ) continue;
|
||||
$sel = $m->role === $k ? ' selected' : '';
|
||||
@@ -1438,13 +1550,13 @@ function wbf_admin_members() {
|
||||
<span class="wbf-role-preview" style="color:<?php echo $color; ?>;background:<?php echo $bg; ?>;border-color:<?php echo $color; ?>">
|
||||
<i class="<?php echo $icon; ?>"></i> <?php echo esc_html( $role['label'] ); ?>
|
||||
</span>
|
||||
<?php if ( $is_sa ) : ?><em style="color:#999;font-size:.8em">(WP-Admin)</em><?php endif; ?>
|
||||
<?php if ( $is_sa ) : ?><em style="color:#999;font-size:.8em">(Haupt-Admin)</em><?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo esc_html( $m->post_count ); ?></td>
|
||||
<td><?php echo esc_html( date( 'd.m.Y', strtotime( $m->registered ) ) ); ?></td>
|
||||
<td>
|
||||
<?php if ( $is_sa ) : ?>
|
||||
<em style="color:#999">Automatisch (WP-Admin)</em>
|
||||
<em style="color:#999">Gesperrt — Haupt-Superadmin (WP User ID <?php echo (int) WBF_Roles::get_wp_superadmin_id(); ?>)</em>
|
||||
<?php else : ?>
|
||||
<form method="post" style="display:flex;flex-direction:column;gap:5px">
|
||||
<?php wp_nonce_field( 'wbf_member_role_nonce' ); ?>
|
||||
@@ -1504,6 +1616,30 @@ function wbf_admin_members() {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 2FA Admin-Panel -->
|
||||
<?php if ( class_exists('WBF_TOTP') && ! $is_sa ) : ?>
|
||||
<div id="wbf-2fa-admin-<?php echo (int)$m->id; ?>"
|
||||
style="margin-top:6px;padding:8px 10px;background:#fefce8;border:1px solid #fde68a;border-radius:4px;font-size:12px">
|
||||
<strong style="color:#92400e"><i class="fas fa-shield-halved"></i> 2FA-Status:</strong>
|
||||
<?php if ( WBF_TOTP::is_enabled_for($m->id) ) : ?>
|
||||
<span style="color:#16a34a;font-weight:600"> ✔ Aktiv</span>
|
||||
<form method="post" style="display:inline;margin-left:10px">
|
||||
<?php wp_nonce_field( 'wbf_admin_2fa_nonce' ); ?>
|
||||
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
|
||||
<button type="submit" name="wbf_admin_reset_2fa"
|
||||
class="button button-small"
|
||||
style="color:#dc2626;border-color:rgba(220,38,38,.4);font-size:11px"
|
||||
onclick="return confirm('2FA für <?php echo esc_js($m->display_name); ?> zurücksetzen?')">
|
||||
<span class="dashicons dashicons-unlock" style="font-size:13px;width:13px;height:13px;vertical-align:-2px"></span>
|
||||
2FA zurücksetzen
|
||||
</button>
|
||||
</form>
|
||||
<?php else : ?>
|
||||
<span style="color:#9ca3af"> — Nicht aktiv</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Inline Profil-Editor -->
|
||||
<div id="wbf-edit-user-<?php echo (int)$m->id; ?>" style="display:none;margin-top:8px;padding:12px;background:#f9f9f9;border:1px solid #ddd;border-radius:4px;max-width:480px">
|
||||
<strong style="font-size:13px">Profil bearbeiten: <?php echo esc_html($m->display_name); ?></strong>
|
||||
@@ -1711,11 +1847,127 @@ function wbf_admin_members() {
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<?php if ( $dc_sync_on ) : ?>
|
||||
<td style="white-space:nowrap;min-width:140px;vertical-align:top;padding-top:8px">
|
||||
<?php if ( $has_dc ) : ?>
|
||||
<div style="display:flex;flex-direction:column;gap:5px">
|
||||
<span style="font-size:.8rem;color:#5865f2;font-weight:600">
|
||||
<i class="fab fa-discord"></i>
|
||||
<?php echo esc_html( $dc_user->discord_name ?: $dc_user->discord_uid ); ?>
|
||||
</span>
|
||||
<button type="button"
|
||||
class="button button-small wbf-dc-sync-user"
|
||||
data-uid="<?php echo (int)$m->id; ?>"
|
||||
data-nonce="<?php echo wp_create_nonce('wbf_nonce'); ?>"
|
||||
style="color:#5865f2;border-color:#5865f2;font-size:.75rem;height:24px;line-height:22px">
|
||||
<span class="dashicons dashicons-update" style="font-size:12px;width:12px;height:12px;margin-top:5px"></span>
|
||||
Sync
|
||||
</button>
|
||||
<span class="wbf-dc-user-result" style="font-size:.75rem;font-weight:600"></span>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<span style="font-size:.78rem;color:#9ca3af">Nicht verknüpft</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ( $dc_sync_on ) : ?>
|
||||
<style>
|
||||
@keyframes wbf-spin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
|
||||
.wbf-spinning { animation: wbf-spin .8s linear infinite; display:inline-block; }
|
||||
</style>
|
||||
<script>
|
||||
(function(){
|
||||
var nonce = '<?php echo wp_create_nonce("wbf_nonce"); ?>';
|
||||
|
||||
// ── Bulk-Sync ──────────────────────────────────────────────────────────
|
||||
var allBtn = document.getElementById('wbf-discord-sync-all-btn');
|
||||
var allRes = document.getElementById('wbf-discord-sync-result');
|
||||
var allIcon = document.getElementById('wbf-sync-icon');
|
||||
|
||||
if (allBtn) {
|
||||
allBtn.addEventListener('click', function() {
|
||||
allBtn.disabled = true;
|
||||
if (allIcon) allIcon.classList.add('wbf-spinning');
|
||||
allRes.style.color = '#374151';
|
||||
allRes.textContent = '⏳ Sync läuft…';
|
||||
|
||||
fetch(ajaxurl, {
|
||||
method : 'POST',
|
||||
headers : {'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body : 'action=wbf_manual_discord_sync&nonce=' + nonce
|
||||
})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d) {
|
||||
allBtn.disabled = false;
|
||||
if (allIcon) allIcon.classList.remove('wbf-spinning');
|
||||
if (d.success) {
|
||||
allRes.style.color = '#16a34a';
|
||||
allRes.textContent = '✅ ' + (d.data.message || 'Fertig!');
|
||||
// Seite neu laden damit neue Rollen sichtbar werden
|
||||
setTimeout(function(){ location.reload(); }, 1800);
|
||||
} else {
|
||||
allRes.style.color = '#dc2626';
|
||||
allRes.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
allBtn.disabled = false;
|
||||
if (allIcon) allIcon.classList.remove('wbf-spinning');
|
||||
allRes.style.color = '#dc2626';
|
||||
allRes.textContent = '❌ Netzwerkfehler';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Pro-Nutzer-Sync ───────────────────────────────────────────────────
|
||||
document.querySelectorAll('.wbf-dc-sync-user').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var uid = btn.dataset.uid;
|
||||
var icon = btn.querySelector('.dashicons');
|
||||
var result = btn.closest('div').querySelector('.wbf-dc-user-result');
|
||||
|
||||
btn.disabled = true;
|
||||
if (icon) icon.classList.add('wbf-spinning');
|
||||
if (result) { result.style.color='#374151'; result.textContent='⏳'; }
|
||||
|
||||
fetch(ajaxurl, {
|
||||
method : 'POST',
|
||||
headers : {'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body : 'action=wbf_discord_sync_user&nonce=' + nonce + '&user_id=' + uid
|
||||
})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false;
|
||||
if (icon) icon.classList.remove('wbf-spinning');
|
||||
if (d.success) {
|
||||
if (result) { result.style.color='#16a34a'; result.textContent='✅ OK'; }
|
||||
// Rollenbadge in dieser Zeile nach 1s aktualisieren (Seitenreload)
|
||||
setTimeout(function(){ location.reload(); }, 1200);
|
||||
} else {
|
||||
if (result) {
|
||||
result.style.color = '#dc2626';
|
||||
result.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
btn.disabled = false;
|
||||
if (icon) icon.classList.remove('wbf-spinning');
|
||||
if (result) { result.style.color='#dc2626'; result.textContent='❌ Netzwerkfehler'; }
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
@@ -3191,6 +3443,8 @@ function wbf_admin_profile_fields() {
|
||||
|
||||
<!-- ── Felder je Kategorie ───────────────────────────────── -->
|
||||
<?php
|
||||
// Globaler Feld-Index — synchronisiert Checkboxen mit den sequentiellen []‑Arrays
|
||||
$wbf_fidx = 0;
|
||||
// Alle Kategorien + "Ohne Kategorie" am Ende ausgeben
|
||||
$all_sections = $cats;
|
||||
if ( isset($by_cat['__none__']) ) {
|
||||
@@ -3234,7 +3488,8 @@ function wbf_admin_profile_fields() {
|
||||
<?php
|
||||
endif;
|
||||
foreach ( $c_fields as $i_f => $f ):
|
||||
$fi = 'fi_' . $f['key'];
|
||||
$fi = $wbf_fidx;
|
||||
$wbf_fidx++;
|
||||
?>
|
||||
<tr class="wbf-field-row" style="background:#fff">
|
||||
<td style="padding:6px 8px">
|
||||
@@ -3268,9 +3523,11 @@ function wbf_admin_profile_fields() {
|
||||
<span class="wbf-options-placeholder" style="color:#ccc;font-size:.75rem;<?php echo ($f['type']??'text')==='select'?'display:none':''; ?>">—</span>
|
||||
</td>
|
||||
<td style="text-align:center;padding:6px 8px">
|
||||
<input type="hidden" name="field_required[<?php echo $fi; ?>]" value="0">
|
||||
<input type="checkbox" name="field_required[<?php echo $fi; ?>]" value="1" <?php checked($f['required']??0,1); ?>>
|
||||
</td>
|
||||
<td style="text-align:center;padding:6px 8px">
|
||||
<input type="hidden" name="field_public[<?php echo $fi; ?>]" value="0">
|
||||
<input type="checkbox" name="field_public[<?php echo $fi; ?>]" value="1" <?php checked($f['public']??1,1); ?>>
|
||||
</td>
|
||||
<td style="padding:6px 8px">
|
||||
@@ -3318,7 +3575,7 @@ function wbf_admin_profile_fields() {
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var wbfRowCount = <?php echo count($fields) + 100; ?>;
|
||||
var wbfRowCount = <?php echo $wbf_fidx; ?>;
|
||||
|
||||
function wbfRemoveRow(btn) {
|
||||
var tr = btn.closest('tr');
|
||||
@@ -3359,8 +3616,8 @@ function wbf_admin_profile_fields() {
|
||||
'<textarea name="field_options[]" rows="2" placeholder="Option 1\nOption 2" style="width:100%;font-size:.78rem;display:none" class="wbf-options-field"></textarea>' +
|
||||
'<span class="wbf-options-placeholder" style="color:#ccc;font-size:.75rem">—</span>' +
|
||||
'</td>' +
|
||||
'<td style="text-align:center;padding:6px 8px"><input type="checkbox" name="field_required[new_' + i + ']" value="1"></td>' +
|
||||
'<td style="text-align:center;padding:6px 8px"><input type="checkbox" name="field_public[new_' + i + ']" value="1" checked></td>' +
|
||||
'<td style="text-align:center;padding:6px 8px"><input type="hidden" name="field_required[' + i + ']" value="0"><input type="checkbox" name="field_required[' + i + ']" value="1"></td>' +
|
||||
'<td style="text-align:center;padding:6px 8px"><input type="hidden" name="field_public[' + i + ']" value="0"><input type="checkbox" name="field_public[' + i + ']" value="1" checked></td>' +
|
||||
'<td style="padding:6px 8px"><select name="field_category[]" style="width:100%;font-size:.82rem">' + catOpts + '</select></td>' +
|
||||
'<td style="padding:6px 8px"><button type="button" class="button" onclick="wbfRemoveRow(this)" style="color:#dc2626;border-color:#fca5a5;padding:2px 7px">✕</button></td>';
|
||||
tbody.appendChild(tr);
|
||||
@@ -3838,3 +4095,231 @@ function wbf_admin_wordfilter() {
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Discord-Bot-Verbindungstest (Admin AJAX) ──────────────────────────────────
|
||||
add_action('wp_ajax_wbf_discord_test', function() {
|
||||
if ( ! current_user_can('manage_options') ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
|
||||
check_ajax_referer('wbf_discord_test', 'nonce');
|
||||
|
||||
$s = wbf_get_settings();
|
||||
$token = trim($s['discord_bot_token'] ?? '');
|
||||
$guild = trim($s['discord_guild_id'] ?? '');
|
||||
|
||||
if ( ! $token ) {
|
||||
wp_send_json_error(['message' => 'Kein Bot-Token gespeichert.']);
|
||||
}
|
||||
|
||||
// Bot-Info abrufen (@me)
|
||||
$res = wp_remote_get('https://discord.com/api/v10/users/@me', [
|
||||
'timeout' => 8,
|
||||
'headers' => [
|
||||
'Authorization' => 'Bot ' . $token,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
]);
|
||||
|
||||
if ( is_wp_error($res) ) {
|
||||
wp_send_json_error(['message' => 'HTTP-Fehler: ' . $res->get_error_message()]);
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($res);
|
||||
$body = json_decode(wp_remote_retrieve_body($res), true);
|
||||
|
||||
if ( $code !== 200 || empty($body['id']) ) {
|
||||
$err = $body['message'] ?? 'Unbekannter Fehler (HTTP ' . $code . ')';
|
||||
wp_send_json_error(['message' => 'Discord API: ' . $err]);
|
||||
}
|
||||
|
||||
$bot_name = ($body['username'] ?? 'Unbekannt') . '#' . ($body['discriminator'] ?? '0');
|
||||
|
||||
// Guild-Prüfung falls Guild-ID angegeben
|
||||
$guild_info = '';
|
||||
if ( $guild ) {
|
||||
$gr = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}", [
|
||||
'timeout' => 6,
|
||||
'headers' => ['Authorization' => 'Bot ' . $token],
|
||||
]);
|
||||
if ( ! is_wp_error($gr) && wp_remote_retrieve_response_code($gr) === 200 ) {
|
||||
$gd = json_decode(wp_remote_retrieve_body($gr), true);
|
||||
$guild_info = ' | Server: ' . ($gd['name'] ?? $guild);
|
||||
} else {
|
||||
$guild_info = ' | ⚠️ Server nicht gefunden oder Bot kein Mitglied';
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success(['message' => 'Bot: ' . $bot_name . $guild_info]);
|
||||
});
|
||||
|
||||
// ── Discord-Cron: Rollen synchronisieren ──────────────────────────────────────
|
||||
add_action('wbf_discord_role_sync', 'wbf_run_discord_role_sync');
|
||||
|
||||
if ( ! wp_next_scheduled('wbf_discord_role_sync') ) {
|
||||
wp_schedule_event(time(), 'hourly', 'wbf_discord_role_sync');
|
||||
}
|
||||
|
||||
function wbf_run_discord_role_sync() {
|
||||
$s = wbf_get_settings();
|
||||
if ( ($s['discord_role_sync'] ?? '0') !== '1' ) return;
|
||||
$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) ) return;
|
||||
|
||||
global $wpdb;
|
||||
// Alle verifizierten Discord-User holen (discord_user_id in user_meta gesetzt)
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT um.user_id, um.meta_value AS discord_user_id
|
||||
FROM {$wpdb->prefix}forum_user_meta um
|
||||
WHERE um.meta_key = 'discord_user_id' AND um.meta_value != ''"
|
||||
);
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
wbf_sync_discord_role_for_user((int)$row->user_id, $row->discord_user_id, $token, $guild, $role_map);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronisiert die Discord-Serverrolle eines einzelnen Nutzers mit der Forum-Rolle.
|
||||
*/
|
||||
function wbf_sync_discord_role_for_user($forum_user_id, $discord_user_id, $token, $guild, $role_map) {
|
||||
// Guild-Member-Info 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 ) return;
|
||||
|
||||
$member = json_decode(wp_remote_retrieve_body($res), true);
|
||||
$user_roles = $member['roles'] ?? [];
|
||||
|
||||
// Rollen-Map prüfen — erster Treffer gewinnt (Reihenfolge = Priorität)
|
||||
foreach ( $role_map as $dc_role_id => $forum_role ) {
|
||||
if ( in_array((string)$dc_role_id, array_map('strval', $user_roles), true) ) {
|
||||
$forum_user = WBF_DB::get_user($forum_user_id);
|
||||
if ( $forum_user && $forum_user->role !== 'superadmin' && $forum_user->role !== $forum_role ) {
|
||||
WBF_DB::update_user($forum_user_id, ['role' => $forum_role]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Discord-Admin-Seite ───────────────────────────────────────────────────────
|
||||
if ( ! function_exists('wbf_admin_discord') ) {
|
||||
function wbf_admin_discord() {
|
||||
if ( ! current_user_can('manage_options') ) return;
|
||||
$s = wbf_get_settings();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>🎮 Discord-Integration</h1>
|
||||
<p>Konfiguriere den Discord-Bot und die Rollen-Synchronisation.
|
||||
Einstellungen werden in <a href="admin.php?page=wbf-settings">Einstellungen → Discord-Integration</a> gespeichert.</p>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;max-width:900px;margin-top:1.5rem">
|
||||
|
||||
<!-- Status-Panel -->
|
||||
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px">
|
||||
<h3 style="margin-top:0">🔌 Bot-Status</h3>
|
||||
<?php
|
||||
$token = trim($s['discord_bot_token'] ?? '');
|
||||
$guild = trim($s['discord_guild_id'] ?? '');
|
||||
if (!$token): ?>
|
||||
<p style="color:#dc2626"><i class="dashicons dashicons-warning"></i> Kein Bot-Token konfiguriert.</p>
|
||||
<a href="admin.php?page=wbf-settings#discord" class="button button-primary">Jetzt einrichten</a>
|
||||
<?php else: ?>
|
||||
<p style="color:#16a34a;font-weight:600">✅ Bot-Token gespeichert</p>
|
||||
<p style="color:<?php echo $guild ? '#16a34a' : '#f59e0b'; ?>">
|
||||
<?php echo $guild ? '✅ Guild-ID: <code>' . esc_html($guild) . '</code>' : '⚠️ Keine Guild-ID gesetzt'; ?>
|
||||
</p>
|
||||
<p style="color:<?php echo ($s['discord_role_sync']??'0')==='1' ? '#16a34a' : '#9ca3af'; ?>">
|
||||
Rollen-Sync: <strong><?php echo ($s['discord_role_sync']??'0')==='1' ? 'Aktiv' : 'Deaktiviert'; ?></strong>
|
||||
</p>
|
||||
<button type="button" class="button button-secondary" id="wbf-discord-test-btn2">
|
||||
🔌 Verbindung testen
|
||||
</button>
|
||||
<span id="wbf-discord-test-result2" style="margin-left:10px;font-weight:600"></span>
|
||||
<script>
|
||||
document.getElementById('wbf-discord-test-btn2').addEventListener('click', function(){
|
||||
var btn = this, res = document.getElementById('wbf-discord-test-result2');
|
||||
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){
|
||||
res.style.color = d.success ? '#16a34a' : '#dc2626';
|
||||
res.textContent = d.success ? '✅ '+(d.data.message||'OK') : '❌ '+((d.data&&d.data.message)||'Fehler');
|
||||
btn.disabled = false;
|
||||
}).catch(function(){ res.style.color='#dc2626'; res.textContent='❌ Netzwerkfehler'; btn.disabled=false; });
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Rollen-Map-Übersicht -->
|
||||
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px">
|
||||
<h3 style="margin-top:0">🔗 Aktive Rollen-Zuordnungen</h3>
|
||||
<?php
|
||||
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
|
||||
$all_roles = WBF_Roles::get_all();
|
||||
if (empty($role_map)): ?>
|
||||
<p style="color:#9ca3af">Keine Zuordnungen konfiguriert.</p>
|
||||
<a href="admin.php?page=wbf-settings" class="button">Jetzt einrichten</a>
|
||||
<?php else: ?>
|
||||
<table class="widefat striped" style="font-size:.85rem">
|
||||
<thead><tr><th>Discord Rollen-ID</th><th>Forum-Rolle</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($role_map as $dc_id => $fr_key):
|
||||
$fr_label = $all_roles[$fr_key]['label'] ?? $fr_key; ?>
|
||||
<tr>
|
||||
<td><code><?php echo esc_html($dc_id); ?></code></td>
|
||||
<td><?php echo WBF_Roles::badge($fr_key); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="admin.php?page=wbf-settings" class="button" style="margin-top:.75rem">Bearbeiten</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Verknüpfte Discord-Nutzer -->
|
||||
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px;max-width:900px;margin-top:20px">
|
||||
<h3 style="margin-top:0">👥 Verknüpfte Forum-Nutzer</h3>
|
||||
<?php
|
||||
global $wpdb;
|
||||
$linked = $wpdb->get_results(
|
||||
"SELECT fu.id, fu.username, fu.display_name, fu.role,
|
||||
MAX(CASE WHEN um.meta_key='discord_username' THEN um.meta_value END) AS discord_name,
|
||||
MAX(CASE WHEN um.meta_key='discord_user_id' THEN um.meta_value END) AS discord_uid
|
||||
FROM {$wpdb->prefix}forum_users fu
|
||||
JOIN {$wpdb->prefix}forum_user_meta um ON um.user_id = fu.id
|
||||
WHERE um.meta_key IN ('discord_username','discord_user_id')
|
||||
GROUP BY fu.id
|
||||
HAVING discord_name != '' AND discord_name IS NOT NULL
|
||||
ORDER BY fu.username"
|
||||
);
|
||||
if (empty($linked)): ?>
|
||||
<p style="color:#9ca3af">Noch keine verknüpften Nutzer.</p>
|
||||
<?php else: ?>
|
||||
<table class="widefat striped" style="font-size:.85rem">
|
||||
<thead><tr><th>Forum-Nutzer</th><th>Rolle</th><th>Discord-Name</th><th>Discord-ID</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($linked as $u): ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html($u->display_name); ?></strong>
|
||||
<span style="color:#9ca3af"> @<?php echo esc_html($u->username); ?></span></td>
|
||||
<td><?php echo WBF_Roles::badge($u->role); ?></td>
|
||||
<td><i class="fab fa-discord" style="color:#5865f2"></i> <?php echo esc_html($u->discord_name ?: '–'); ?></td>
|
||||
<td><code style="font-size:.78rem"><?php echo esc_html($u->discord_uid ?: '–'); ?></code></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
89
admin/forum-settings-mc-section.php
Normal file
89
admin/forum-settings-mc-section.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* MC Bridge — Einstellungs-Sektion für den Forum-Admin
|
||||
*
|
||||
* Diesen Block in forum-settings.php im Admin-Formular einfügen,
|
||||
* z.B. nach der Discord-Sektion.
|
||||
*/
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
?>
|
||||
|
||||
<!-- ═══ Minecraft Bridge ═══ -->
|
||||
<div class="wbf-settings-section">
|
||||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||
<span style="font-size:1.3em">⛏️</span> Minecraft Bridge
|
||||
</h3>
|
||||
<p class="description" style="margin-bottom:16px;color:#9ca3af">
|
||||
Verbindet das Forum mit deinem Minecraft-Server (BungeeCord StatusAPI Plugin).
|
||||
Spieler können ihren Forum-Account verknüpfen und erhalten Ingame-Benachrichtigungen
|
||||
bei neuen Antworten, Erwähnungen und Privatnachrichten.
|
||||
</p>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th>Aktiviert</th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="wbf_settings[mc_bridge_enabled]" value="1"
|
||||
<?php checked( ! empty( $s['mc_bridge_enabled'] ) ); ?>>
|
||||
MC Bridge aktivieren
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>StatusAPI URL</th>
|
||||
<td>
|
||||
<input type="url" name="wbf_settings[mc_bridge_api_url]"
|
||||
value="<?php echo esc_attr( $s['mc_bridge_api_url'] ?? '' ); ?>"
|
||||
class="regular-text"
|
||||
placeholder="http://192.168.1.100:9191">
|
||||
<p class="description">
|
||||
Die URL deines BungeeCord StatusAPI Servers (IP + Port).
|
||||
Beispiel: <code>http://dein-server:9191</code>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API Secret</th>
|
||||
<td>
|
||||
<input type="password" name="wbf_settings[mc_bridge_api_secret]"
|
||||
value="<?php echo esc_attr( $s['mc_bridge_api_secret'] ?? '' ); ?>"
|
||||
class="regular-text"
|
||||
autocomplete="new-password">
|
||||
<p class="description">
|
||||
Gemeinsames Passwort für die API-Kommunikation.
|
||||
Muss identisch sein mit <code>forum.api_secret</code> in der
|
||||
<code>verify.properties</code> des StatusAPI Plugins.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Verbindungstest</th>
|
||||
<td>
|
||||
<button type="button" id="wbf-mc-test-btn" class="button"
|
||||
onclick="wbfTestMcConnection()">
|
||||
🔌 Verbindung testen
|
||||
</button>
|
||||
<span id="wbf-mc-test-result" style="margin-left:10px"></span>
|
||||
<script>
|
||||
function wbfTestMcConnection() {
|
||||
var btn = document.getElementById('wbf-mc-test-btn');
|
||||
var result = document.getElementById('wbf-mc-test-result');
|
||||
var url = document.querySelector('input[name="wbf_settings[mc_bridge_api_url]"]').value;
|
||||
if (!url) { result.textContent = '❌ Bitte erst eine URL eingeben.'; return; }
|
||||
btn.disabled = true;
|
||||
result.textContent = '⏳ Teste...';
|
||||
fetch(url.replace(/\/$/, '') + '/forum/status')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.success) result.innerHTML = '✅ <strong>Verbunden!</strong> StatusAPI v' + (d.version || '?');
|
||||
else result.textContent = '⚠️ Erreichbar aber Fehler: ' + JSON.stringify(d);
|
||||
})
|
||||
.catch(function(e) { result.textContent = '❌ Nicht erreichbar: ' + e.message; })
|
||||
.finally(function() { btn.disabled = false; });
|
||||
}
|
||||
</script>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -54,12 +54,24 @@ if ( ! function_exists('wbf_get_settings') ) {
|
||||
'rules_content' => "**1. Respektvoller Umgang**\nBehandle alle Mitglieder freundlich und respektvoll. Beleidigungen, Mobbing und Diskriminierung sind nicht toleriert.\n\n**2. Keine Spam-Inhalte**\nWerbung, Spam und irrelevante Links sind verboten.\n\n**3. Keine illegalen Inhalte**\nJegliche Inhalte, die gegen geltendes Recht verstoßen, sind streng verboten.\n\n**4. Themenrelevanz**\nBeiträge sollten zur jeweiligen Kategorie passen.\n\n**5. Urheberrecht**\nVeröffentliche keine Inhalte, an denen du keine Rechte besitzt.\n\n**6. Datenschutz**\nTeile keine persönlichen Daten anderer Personen ohne deren Zustimmung.\n\n**7. Moderations-Entscheidungen**\nEntscheidungen der Moderatoren sind zu respektieren. Bei Fragen wende dich direkt ans Team.\n\nVerstöße können zur Verwarnung oder dauerhaften Sperrung führen.",
|
||||
// Ignore/Block-System: Rollen die nicht geblockt werden können (kommagetrennte Schlüssel)
|
||||
'ignore_blocked_roles' => 'superadmin,admin,moderator',
|
||||
// Discord-Integration
|
||||
'discord_bot_token' => '',
|
||||
'discord_guild_id' => '',
|
||||
'discord_client_id' => '',
|
||||
'discord_client_secret' => '',
|
||||
'discord_role_sync' => '0', // Rollen-Sync aktiviert?
|
||||
'discord_role_map' => '', // JSON: {"discord_role_id":"forum_role_key"}
|
||||
// Minecraft Bridge
|
||||
'mc_bridge_enabled' => '0',
|
||||
'mc_bridge_api_url' => '',
|
||||
'mc_bridge_api_secret' => '',
|
||||
];
|
||||
|
||||
$saved = get_option( 'wbf_settings', [] );
|
||||
|
||||
// Fehlende Keys mit Defaults auffüllen, leere Strings ignorieren
|
||||
return array_merge( $defaults, array_filter( (array) $saved, 'strlen' ) );
|
||||
// Keine Filterung mehr, damit auch bewusst geleerte Felder gespeichert werden
|
||||
return array_merge( $defaults, (array) $saved );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +142,34 @@ 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'];
|
||||
$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';
|
||||
}
|
||||
@@ -150,6 +187,12 @@ function wbf_admin_settings() {
|
||||
$settings['ignore_blocked_roles'] = implode( ',', $checked_roles );
|
||||
|
||||
update_option( 'wbf_settings', $settings );
|
||||
|
||||
// Superadmin WP-User-ID separat speichern (außerhalb von wbf_settings)
|
||||
$sa_wp_id = (int) ( $_POST['superadmin_wp_id'] ?? 1 );
|
||||
if ( $sa_wp_id < 1 ) $sa_wp_id = 1;
|
||||
update_option( 'wbf_superadmin_wp_id', $sa_wp_id );
|
||||
|
||||
echo '<div class="notice notice-success is-dismissible"><p>✅ Einstellungen gespeichert!</p></div>';
|
||||
}
|
||||
|
||||
@@ -251,6 +294,40 @@ function wbf_admin_settings() {
|
||||
🔒 Sicherheit
|
||||
</h2>
|
||||
<table class="form-table" role="presentation">
|
||||
|
||||
<!-- ── Superadmin WP-User-ID ─────────────────── -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbf_superadmin_wp_id">Superadmin WordPress-User-ID</label>
|
||||
</th>
|
||||
<td>
|
||||
<?php
|
||||
$sa_id = (int) get_option( 'wbf_superadmin_wp_id', 1 );
|
||||
$sa_wpuser = get_userdata( $sa_id );
|
||||
?>
|
||||
<input type="number" id="wbf_superadmin_wp_id" name="superadmin_wp_id"
|
||||
value="<?php echo $sa_id; ?>"
|
||||
min="1" step="1"
|
||||
style="width:80px">
|
||||
<?php if ( $sa_wpuser ) : ?>
|
||||
<span style="margin-left:10px;color:#16a34a;font-weight:600">
|
||||
✅ <?php echo esc_html( $sa_wpuser->display_name ); ?>
|
||||
<<?php echo esc_html( $sa_wpuser->user_email ); ?>>
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span style="margin-left:10px;color:#dc2626;font-weight:600">
|
||||
⚠️ Kein WordPress-User mit dieser ID gefunden!
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<p class="description">
|
||||
Nur dieser WordPress-User erhält automatisch die Forum-Rolle <strong>Superadmin</strong>
|
||||
und kann sie nicht verlieren. Alle anderen WordPress-Admins können normale Forum-Rollen
|
||||
haben und im Mitglieder-Bereich frei zugewiesen werden.<br>
|
||||
<em>Standard: 1 (erster bei der WP-Installation angelegter User)</em>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbf_auto_logout_minutes">Auto-Logout nach Inaktivität</label>
|
||||
@@ -499,6 +576,280 @@ function wbf_admin_settings() {
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
DISCORD-INTEGRATION
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<h2 style="border-bottom:1px solid #ddd;padding-bottom:.4rem;margin-top:2rem">
|
||||
<span style="color:#5865f2">🎮</span> Discord-Integration
|
||||
</h2>
|
||||
<p class="description" style="margin-bottom:1rem">
|
||||
Bot-Token und Guild-ID findest du im <a href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</a>.
|
||||
Der Bot muss Mitglied deines Servers sein und die Berechtigung <strong>Direct Messages lesen/senden</strong> sowie
|
||||
<strong>Server-Mitglieder verwalten</strong> besitzen.
|
||||
</p>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><label for="wbf_discord_bot_token">Bot-Token</label></th>
|
||||
<td>
|
||||
<input type="password" id="wbf_discord_bot_token" name="discord_bot_token"
|
||||
value="<?php echo esc_attr($s['discord_bot_token']); ?>"
|
||||
class="regular-text" autocomplete="off" placeholder="Bot-Token aus dem Developer Portal">
|
||||
<p class="description">Niemals öffentlich teilen! Wird verschlüsselt in der Datenbank gespeichert.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="wbf_discord_guild_id">Server-ID (Guild ID)</label></th>
|
||||
<td>
|
||||
<input type="text" id="wbf_discord_guild_id" name="discord_guild_id"
|
||||
value="<?php echo esc_attr($s['discord_guild_id']); ?>"
|
||||
class="regular-text" placeholder="z. B. 123456789012345678">
|
||||
<p class="description">Rechtsklick auf deinen Server → ID kopieren (Entwicklermodus muss aktiv sein).</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="wbf_discord_client_id">Client ID (optional)</label></th>
|
||||
<td>
|
||||
<input type="text" id="wbf_discord_client_id" name="discord_client_id"
|
||||
value="<?php echo esc_attr($s['discord_client_id']); ?>"
|
||||
class="regular-text" placeholder="Application ID">
|
||||
<p class="description">Für zukünftige OAuth2-Unterstützung. Aktuell optional.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="wbf_discord_client_secret">Client Secret (optional)</label></th>
|
||||
<td>
|
||||
<input type="password" id="wbf_discord_client_secret" name="discord_client_secret"
|
||||
value="<?php echo esc_attr($s['discord_client_secret']); ?>"
|
||||
class="regular-text" autocomplete="off" placeholder="Client Secret">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Rollen-Sync aktivieren</th>
|
||||
<td>
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" name="discord_role_sync" value="1"
|
||||
<?php checked('1', $s['discord_role_sync'] ?? '0'); ?>>
|
||||
Discord-Serverrollen automatisch auf Forum-Rollen mappen
|
||||
</label>
|
||||
<p class="description">
|
||||
Wenn aktiviert, wird bei jedem Login und stündlich per Cron die Discord-Rolle des Nutzers
|
||||
geprüft und die Forum-Rolle entsprechend der unten definierten Zuordnung aktualisiert.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Discord Rollen-Map -->
|
||||
<h3 style="margin-top:1.5rem">🔗 Discord-Rollen → Forum-Rollen Zuordnung</h3>
|
||||
<p class="description" style="margin-bottom:.75rem">
|
||||
Trage die Discord-Rollen-ID und die gewünschte Forum-Rolle ein.
|
||||
Mehrere Einträge werden der Reihe nach geprüft — der erste Treffer gewinnt.
|
||||
</p>
|
||||
<?php
|
||||
$role_map_raw = $s['discord_role_map'] ?? '{}';
|
||||
$role_map = json_decode($role_map_raw, true) ?: [];
|
||||
$forum_roles = WBF_Roles::get_sorted();
|
||||
// Sicherstellen dass mindestens eine leere Zeile zum Hinzufügen da ist
|
||||
if ( empty($role_map) ) $role_map[''] = '';
|
||||
?>
|
||||
<table class="widefat" id="wbf-discord-role-map" style="max-width:680px;margin-bottom:.75rem">
|
||||
<thead><tr>
|
||||
<th style="width:50%">Discord Rollen-ID</th>
|
||||
<th style="width:40%">Forum-Rolle</th>
|
||||
<th style="width:10%"></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ( $role_map as $dc_id => $fr_key ) : ?>
|
||||
<tr class="wbf-role-map-row">
|
||||
<td><input type="text" name="discord_role_id[]"
|
||||
value="<?php echo esc_attr($dc_id); ?>"
|
||||
placeholder="Discord Rollen-ID"
|
||||
class="widefat" style="font-family:monospace"></td>
|
||||
<td>
|
||||
<select name="discord_forum_role[]" class="widefat">
|
||||
<option value="">— wählen —</option>
|
||||
<?php foreach ( $forum_roles as $rk => $role ) :
|
||||
if ( $rk === 'superadmin' ) continue; ?>
|
||||
<option value="<?php echo esc_attr($rk); ?>"
|
||||
<?php selected($rk, $fr_key); ?>>
|
||||
<?php echo esc_html($role['label']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td><button type="button" class="button button-small wbf-rm-role-row"
|
||||
style="color:#c00">✕</button></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="button" id="wbf-add-role-row">+ Zeile hinzufügen</button>
|
||||
<script>
|
||||
(function(){
|
||||
document.getElementById('wbf-add-role-row').addEventListener('click', function(){
|
||||
var tbody = document.querySelector('#wbf-discord-role-map tbody');
|
||||
var row = document.querySelector('.wbf-role-map-row').cloneNode(true);
|
||||
row.querySelectorAll('input').forEach(function(i){i.value='';});
|
||||
row.querySelectorAll('select').forEach(function(s){s.selectedIndex=0;});
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
document.addEventListener('click', function(e){
|
||||
if (e.target.classList.contains('wbf-rm-role-row')) {
|
||||
var rows = document.querySelectorAll('.wbf-role-map-row');
|
||||
if (rows.length > 1) e.target.closest('tr').remove();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Test-Verbindung -->
|
||||
<div style="margin-top:1.25rem;padding:1rem;background:#f0f7ff;border:1px solid #c3dafe;border-radius:6px;max-width:680px">
|
||||
<strong>🔌 Verbindungstest</strong><br>
|
||||
<p style="margin:.4rem 0 .75rem;color:#374151;font-size:.9rem">
|
||||
Speichere zuerst die Einstellungen, dann klicke „Testen" um zu prüfen ob der Bot erreichbar ist.
|
||||
</p>
|
||||
<button type="button" class="button button-secondary" id="wbf-discord-test-btn">
|
||||
🔌 Discord-Verbindung testen
|
||||
</button>
|
||||
<span id="wbf-discord-test-result" style="margin-left:10px;font-weight:600"></span>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('wbf-discord-test-btn').addEventListener('click', function(){
|
||||
var btn = this;
|
||||
var res = document.getElementById('wbf-discord-test-result');
|
||||
btn.disabled = true;
|
||||
res.textContent = '⏳ Teste…';
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body: 'action=wbf_discord_test&nonce=<?php echo wp_create_nonce("wbf_discord_test"); ?>'
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(function(d){
|
||||
if (d.success) {
|
||||
res.style.color = '#16a34a';
|
||||
res.textContent = '✅ ' + (d.data.message || 'Verbunden!');
|
||||
} else {
|
||||
res.style.color = '#dc2626';
|
||||
res.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
|
||||
}
|
||||
btn.disabled = false;
|
||||
})
|
||||
.catch(function(){ res.style.color='#dc2626'; res.textContent='❌ Netzwerkfehler'; btn.disabled=false; });
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
Minecraft Bridge
|
||||
════════════════════════════════════════════════════════════ -->
|
||||
<div class="wbf-settings-box" style="margin-top:2rem;padding:1.5rem;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb">
|
||||
<h2 style="margin-top:0;display:flex;align-items:center;gap:8px">
|
||||
<span style="font-size:1.3em">⛏️</span> Minecraft Bridge
|
||||
</h2>
|
||||
<p class="description" style="margin-bottom:1.2rem;color:#6b7280">
|
||||
Verbindet das Forum mit deinem BungeeCord-Server (StatusAPI Plugin).
|
||||
Spieler können ihren Forum-Account mit <code>/forumlink <token></code> verknüpfen
|
||||
und erhalten dann Ingame-Benachrichtigungen bei neuen Antworten, Erwähnungen und PNs.
|
||||
</p>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">Aktiviert</th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="mc_bridge_enabled" value="1"
|
||||
<?php checked( '1', $s['mc_bridge_enabled'] ?? '0' ); ?>>
|
||||
Minecraft Bridge aktivieren
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="wbf_mc_api_url">StatusAPI URL</label></th>
|
||||
<td>
|
||||
<input type="url" id="wbf_mc_api_url" name="mc_bridge_api_url"
|
||||
value="<?php echo esc_attr( $s['mc_bridge_api_url'] ?? '' ); ?>"
|
||||
class="regular-text"
|
||||
placeholder="http://dein-server:9191">
|
||||
<p class="description">
|
||||
IP + Port deines BungeeCord StatusAPI Servers.
|
||||
Beispiel: <code>http://192.168.1.100:9191</code>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="wbf_mc_api_secret">API Secret</label></th>
|
||||
<td>
|
||||
<input type="password" id="wbf_mc_api_secret" name="mc_bridge_api_secret"
|
||||
value="<?php echo esc_attr( $s['mc_bridge_api_secret'] ?? '' ); ?>"
|
||||
class="regular-text"
|
||||
autocomplete="new-password"
|
||||
placeholder="Gemeinsames Passwort">
|
||||
<p class="description">
|
||||
Muss identisch sein mit <code>forum.api_secret</code> in der
|
||||
<code>verify.properties</code> des StatusAPI Plugins.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Verbindungstest</th>
|
||||
<td>
|
||||
<button type="button" id="wbf-mc-test-btn" class="button"
|
||||
onclick="wbfTestMcConnection()">
|
||||
🔌 Verbindung testen
|
||||
</button>
|
||||
<span id="wbf-mc-test-result" style="margin-left:10px;font-weight:600"></span>
|
||||
<script>
|
||||
function wbfTestMcConnection() {
|
||||
var btn = document.getElementById('wbf-mc-test-btn');
|
||||
var result = document.getElementById('wbf-mc-test-result');
|
||||
var url = document.getElementById('wbf_mc_api_url').value.replace(/\/$/, '');
|
||||
if (!url) { result.style.color='#dc2626'; result.textContent = '❌ Bitte erst eine URL eingeben.'; return; }
|
||||
btn.disabled = true;
|
||||
result.style.color = '#6b7280';
|
||||
result.textContent = '⏳ Teste Verbindung...';
|
||||
// Test gegen WordPress REST-Endpoint (sicherer als direkter BungeeCord-Aufruf vom Browser)
|
||||
fetch('<?php echo esc_url( rest_url("mc-bridge/v1/status") ); ?>')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.success) {
|
||||
result.style.color = '#16a34a';
|
||||
result.innerHTML = '✅ <strong>WordPress-Endpoint aktiv!</strong> Plugin v' + (d.version || '?');
|
||||
} else {
|
||||
result.style.color = '#dc2626';
|
||||
result.textContent = '⚠️ Endpoint antwortet, aber Fehler: ' + JSON.stringify(d);
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
result.style.color = '#dc2626';
|
||||
result.textContent = '❌ Nicht erreichbar: ' + e.message;
|
||||
})
|
||||
.finally(function() { btn.disabled = false; });
|
||||
}
|
||||
</script>
|
||||
<p class="description" style="margin-top:.5rem">
|
||||
Testet ob der WordPress REST-Endpoint <code>/wp-json/mc-bridge/v1/status</code> erreichbar ist.
|
||||
Danach in <code>verify.properties</code>: <code>forum.wp_url</code> und <code>forum.api_secret</code> eintragen.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="background:#fffbeb;border:1px solid #fcd34d;border-radius:6px;padding:1rem;margin-top:1rem;font-size:.875rem">
|
||||
<strong>⚙️ Einrichtung in 3 Schritten:</strong>
|
||||
<ol style="margin:.5rem 0 0 1.2rem;padding:0;line-height:1.8">
|
||||
<li>API Secret hier festlegen und Einstellungen speichern.</li>
|
||||
<li>In <code>verify.properties</code> des BungeeCord-Plugins setzen:
|
||||
<br><code>forum.enabled=true</code>
|
||||
<br><code>forum.wp_url=<?php echo esc_html( get_site_url() ); ?></code>
|
||||
<br><code>forum.api_secret=DEIN_SECRET</code>
|
||||
</li>
|
||||
<li>Spieler können sich nun mit <strong><code>/forumlink <token></code></strong> ingame verknüpfen.
|
||||
Den Token generieren sie in ihrem Forum-Profil unter dem Tab <em>Verbindungen</em>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php submit_button(
|
||||
'💾 Einstellungen speichern',
|
||||
'primary',
|
||||
@@ -506,7 +857,6 @@ function wbf_admin_settings() {
|
||||
true,
|
||||
[ 'style' => 'margin-top:1rem' ]
|
||||
); ?>
|
||||
</form>
|
||||
|
||||
<!-- ── Vorschau-Tabelle ──────────────────────────────── -->
|
||||
<hr style="margin-top:2.5rem">
|
||||
|
||||
@@ -125,18 +125,22 @@ class WBF_Setup {
|
||||
$page_title = sanitize_text_field($_POST['page_title'] ?? 'Forum');
|
||||
|
||||
if ($create_page) {
|
||||
$existing = get_posts(['post_type'=>'page','s'=>$page_title,'posts_per_page'=>1]);
|
||||
if (empty($existing)) {
|
||||
$page_id = wp_insert_post([
|
||||
'post_title' => $page_title,
|
||||
'post_content' => '[business_forum]',
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'page',
|
||||
]);
|
||||
if ($page_id) {
|
||||
update_option('wbf_forum_page_id', $page_id);
|
||||
$success = get_permalink($page_id);
|
||||
}
|
||||
global $wpdb;
|
||||
$existing_id = $wpdb->get_var( $wpdb->prepare(
|
||||
"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status = 'publish' AND post_content LIKE %s LIMIT 1",
|
||||
'%[business_forum]%'
|
||||
) );
|
||||
if (empty($existing_id)) {
|
||||
$page_id = wp_insert_post([
|
||||
'post_title' => $page_title,
|
||||
'post_content' => '[business_forum]',
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'page',
|
||||
]);
|
||||
if ($page_id) {
|
||||
update_option('wbf_forum_page_id', $page_id);
|
||||
$success = get_permalink($page_id);
|
||||
}
|
||||
} else {
|
||||
$success = get_permalink($existing[0]->ID);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,68 @@
|
||||
/* Shop Orders (Profil) */
|
||||
.wbf-shop-orders-list {
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
.wbf-shop-orders-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--c-surface);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
font-size: .97em;
|
||||
}
|
||||
.wbf-shop-orders-table th, .wbf-shop-orders-table td {
|
||||
padding: .65em 1em;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
}
|
||||
.wbf-shop-orders-table th {
|
||||
background: var(--c-surface2);
|
||||
color: var(--c-text-dim);
|
||||
font-weight: 600;
|
||||
font-size: .93em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
.wbf-shop-orders-table tr:last-child td { border-bottom: none; }
|
||||
.wbf-shop-order-row {
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
}
|
||||
.wbf-shop-order-row:hover {
|
||||
background: var(--c-primary-l);
|
||||
}
|
||||
.wbf-shop-order-details-inner {
|
||||
background: var(--c-bg2);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 1em 1.2em;
|
||||
margin: .2em 0 .5em 0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.08);
|
||||
color: var(--c-text);
|
||||
font-size: .98em;
|
||||
}
|
||||
.wbf-shop-order-cancelled td, .wbf-shop-order-cancelled .wbf-shop-order-details-inner {
|
||||
color: var(--c-danger);
|
||||
text-decoration: line-through;
|
||||
opacity: .7;
|
||||
}
|
||||
.wbf-shop-order-details {
|
||||
background: var(--c-surface2);
|
||||
transition: display .2s;
|
||||
}
|
||||
.wbf-shop-order-toggle {
|
||||
background: var(--c-surface2);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-muted);
|
||||
border-radius: 6px;
|
||||
padding: 2px 10px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
.wbf-shop-order-toggle:hover {
|
||||
background: var(--c-primary-l);
|
||||
color: var(--c-primary);
|
||||
}
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
WP Business Forum — Minecraft Modern Dark Theme
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
@@ -416,14 +481,57 @@ a.wbf-btn--primary:hover {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Banner */
|
||||
.wbf-profile-banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0a1628 0%, #162040 50%, #0d1a30 100%);
|
||||
}
|
||||
.wbf-profile-banner__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.wbf-profile-banner__placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #0a1628 0%, #162040 50%, #0d1a30 100%);
|
||||
}
|
||||
.wbf-banner-upload-btn {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0,0,0,.55);
|
||||
backdrop-filter: blur(6px);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background .2s, transform .15s;
|
||||
font-size: .8rem;
|
||||
border: 1px solid rgba(255,255,255,.15);
|
||||
}
|
||||
.wbf-banner-upload-btn:hover {
|
||||
background: rgba(0,180,216,.5);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.wbf-profile-sidebar__avatar-wrap {
|
||||
position: relative;
|
||||
padding: 2rem 0 1.25rem;
|
||||
padding: 0 0 1.25rem;
|
||||
margin-top: -45px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: linear-gradient(160deg, #0d1117, #161d2e);
|
||||
border-bottom: 1px solid rgba(0,180,216,.12);
|
||||
z-index: 2;
|
||||
}
|
||||
.wbf-profile-sidebar__avatar {
|
||||
width: 100px; height: 100px;
|
||||
@@ -557,6 +665,235 @@ a.wbf-btn--primary:hover {
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
/* ── Verbindungen / Connection Cards ────────────────────────────────────── */
|
||||
.wbf-connection-card {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 0 1.1rem;
|
||||
padding: 1.4rem 1.25rem;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
transition: background .15s;
|
||||
}
|
||||
.wbf-connection-card:last-child { border-bottom: none; }
|
||||
.wbf-connection-card:hover { background: rgba(255,255,255,.025); }
|
||||
|
||||
.wbf-connection-card__icon {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.35rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Titelzeile — rechts oben */
|
||||
.wbf-connection-card__head {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .6rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.wbf-connection-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wbf-connection-card__title {
|
||||
font-weight: 700;
|
||||
font-size: .92rem;
|
||||
color: var(--c-text);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wbf-connection-card__desc {
|
||||
font-size: .82rem;
|
||||
color: var(--c-muted);
|
||||
line-height: 1.55;
|
||||
margin-bottom: .85rem;
|
||||
}
|
||||
|
||||
/* Content-Bereich — grid row 2, rechte Spalte */
|
||||
.wbf-connection-card__content {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
/* Plugin-Output normalisieren */
|
||||
.wbf-connection-card__content p {
|
||||
font-size: .8rem;
|
||||
color: var(--c-muted);
|
||||
line-height: 1.5;
|
||||
margin: 0 0 .7rem;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
.wbf-connection-card__content label {
|
||||
display: block;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
margin-bottom: .35rem;
|
||||
}
|
||||
.wbf-connection-card__content .wbf-mc-row,
|
||||
.wbf-connection-card__content > form,
|
||||
.wbf-connection-card__content > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .55rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Input + Button auf einer Linie halten */
|
||||
.wbf-connect-row {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: .55rem !important;
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
.wbf-connect-row input[type="text"] {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: none !important;
|
||||
}
|
||||
.wbf-connect-row button,
|
||||
.wbf-connect-row .wbf-btn {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Alle Verknüpfen-Buttons in Connection-Cards vereinheitlichen */
|
||||
.wbf-connection-card__content .wbf-btn,
|
||||
.wbf-connection-card__content button:not(.wbf-bb-spoiler__btn) {
|
||||
padding: .5rem 1rem !important;
|
||||
font-size: .83rem !important;
|
||||
font-weight: 600 !important;
|
||||
height: 2.25rem !important;
|
||||
line-height: 1 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: .4rem !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
border: 1.5px solid transparent !important;
|
||||
cursor: pointer !important;
|
||||
font-family: inherit !important;
|
||||
transition: var(--transition) !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
/* Primär (Verknüpfen / Code senden) */
|
||||
.wbf-connection-card__content .wbf-btn--primary,
|
||||
.wbf-connection-card__content button.wbf-btn--primary {
|
||||
background: var(--c-primary) !important;
|
||||
color: #fff !important;
|
||||
border-color: var(--c-primary) !important;
|
||||
box-shadow: 0 0 10px rgba(0,180,216,.25) !important;
|
||||
}
|
||||
.wbf-connection-card__content .wbf-btn--primary:hover {
|
||||
background: var(--c-primary-d) !important;
|
||||
border-color: var(--c-primary-d) !important;
|
||||
}
|
||||
/* Ghost (Zurück / Trennen) */
|
||||
.wbf-connection-card__content .wbf-btn--ghost,
|
||||
.wbf-connection-card__content button.wbf-btn--ghost {
|
||||
background: transparent !important;
|
||||
color: var(--c-text-dim) !important;
|
||||
border-color: var(--c-border-d) !important;
|
||||
}
|
||||
.wbf-connection-card__content .wbf-btn--ghost:hover {
|
||||
border-color: var(--c-primary) !important;
|
||||
color: var(--c-primary) !important;
|
||||
}
|
||||
/* MC-Plugin: dessen Button via content selector normalisieren */
|
||||
.wbf-connection-card__content input[type="submit"],
|
||||
.wbf-connection-card__content .mc-connect-btn {
|
||||
padding: .5rem 1rem !important;
|
||||
height: 2.25rem !important;
|
||||
font-size: .83rem !important;
|
||||
font-weight: 600 !important;
|
||||
background: var(--c-primary) !important;
|
||||
color: #fff !important;
|
||||
border: 1.5px solid var(--c-primary) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
cursor: pointer !important;
|
||||
font-family: inherit !important;
|
||||
white-space: nowrap !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
.wbf-connection-card__content input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
max-width: 280px;
|
||||
padding: .45rem .75rem;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
background: var(--c-bg);
|
||||
color: var(--c-text);
|
||||
font-size: .88rem;
|
||||
}
|
||||
.wbf-connection-card__content input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--c-primary);
|
||||
box-shadow: 0 0 0 3px rgba(0,180,216,.15);
|
||||
}
|
||||
.wbf-connection-card__content button,
|
||||
.wbf-connection-card__content .wbf-btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Status-Badge in der Card (verbunden / nicht verbunden) */
|
||||
.wbf-connection-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
font-size: .78rem;
|
||||
font-weight: 600;
|
||||
padding: .25rem .65rem;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.wbf-connection-badge--connected {
|
||||
color: #16a34a;
|
||||
background: rgba(22,163,74,.12);
|
||||
border: 1px solid rgba(22,163,74,.25);
|
||||
}
|
||||
.wbf-connection-badge--disconnected {
|
||||
color: var(--c-muted);
|
||||
background: rgba(148,163,184,.08);
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* Discord-spezifische Farben */
|
||||
.wbf-connection-card--discord .wbf-connection-card__icon {
|
||||
background: rgba(88,101,242,.15);
|
||||
border-color: rgba(88,101,242,.3);
|
||||
}
|
||||
.wbf-connection-card--discord .wbf-connection-card__icon i {
|
||||
color: #5865f2;
|
||||
}
|
||||
|
||||
/* Verbunden-Info */
|
||||
.wbf-discord-linked-name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
font-weight: 700;
|
||||
font-size: .92rem;
|
||||
color: var(--c-text);
|
||||
padding: .35rem .75rem;
|
||||
background: rgba(88,101,242,.1);
|
||||
border: 1px solid rgba(88,101,242,.25);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.wbf-profile-edit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -1816,10 +2153,15 @@ select.wbf-cf-input option { background: var(--c-surface2); color: var(--c-text)
|
||||
text-align: left;
|
||||
}
|
||||
.wbf-profile-sidebar__avatar-wrap {
|
||||
padding: 1.25rem;
|
||||
padding: 0 0 1rem;
|
||||
margin-top: -45px;
|
||||
width: auto;
|
||||
border-bottom: none;
|
||||
border-right: 1px solid var(--c-border);
|
||||
border-right: none;
|
||||
}
|
||||
.wbf-profile-banner {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
.wbf-profile-sidebar__identity {
|
||||
align-items: flex-start;
|
||||
@@ -3718,3 +4060,229 @@ select.wbf-cf-input option { background: var(--c-surface2); color: var(--c-text)
|
||||
.wbf-prof__stat-cards { flex-direction: column; }
|
||||
.wbf-prof__badges { grid-template-columns: repeat(3,1fr); }
|
||||
}
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
2FA — Zwei-Faktor-Authentifizierung
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Badge im Profil-Card-Header */
|
||||
.wbf-2fa-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: .72rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
margin-left: auto;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
.wbf-2fa-badge--on {
|
||||
background: rgba(34,197,94,.15);
|
||||
color: #16a34a;
|
||||
border: 1px solid rgba(34,197,94,.3);
|
||||
}
|
||||
.wbf-2fa-badge--off {
|
||||
background: rgba(156,163,175,.12);
|
||||
color: var(--c-muted);
|
||||
border: 1px solid rgba(156,163,175,.2);
|
||||
}
|
||||
|
||||
/* QR-Code Container */
|
||||
/* QR-Code — QRCode.js erzeugt img UND canvas; img ausblenden, nur canvas zeigen */
|
||||
#wbf2faQr {
|
||||
display: inline-block;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
line-height: 0;
|
||||
box-shadow: 0 1px 6px rgba(0,0,0,.15);
|
||||
align-self: flex-start;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Das leere Duplikat-img von QRCode.js komplett verstecken */
|
||||
#wbf2faQr img {
|
||||
display: none !important;
|
||||
}
|
||||
/* Nur das canvas anzeigen — exakte quadratische Größe erzwingen */
|
||||
#wbf2faQr canvas {
|
||||
display: block !important;
|
||||
width: 200px !important;
|
||||
height: 200px !important;
|
||||
}
|
||||
|
||||
/* Secret-Code (manuelle Eingabe) */
|
||||
#wbf2faSecret {
|
||||
display: inline-block;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: .9rem;
|
||||
letter-spacing: .12em;
|
||||
background: var(--c-bg-2, rgba(255,255,255,.05));
|
||||
border: 1px solid rgba(255,255,255,.1);
|
||||
padding: 5px 12px;
|
||||
border-radius: 6px;
|
||||
user-select: all;
|
||||
cursor: text;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 2FA-Login-Step (im Auth-Modal) */
|
||||
.wbf-2fa-login-step {
|
||||
animation: wbfFadeIn .2s ease;
|
||||
}
|
||||
.wbf-2fa-login-step input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family: monospace;
|
||||
text-align: center;
|
||||
font-size: 1.4rem;
|
||||
letter-spacing: .3em;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid rgba(234,179,8,.4);
|
||||
border-radius: 7px;
|
||||
background: var(--c-bg, #1e2535);
|
||||
color: var(--c-text, #e2e8f0);
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.wbf-2fa-login-step input:focus {
|
||||
outline: none;
|
||||
border-color: #eab308;
|
||||
box-shadow: 0 0 0 2px rgba(234,179,8,.2);
|
||||
}
|
||||
|
||||
/* Verify-Code-Input im Profil */
|
||||
#wbf2faVerifyCode {
|
||||
font-family: monospace;
|
||||
letter-spacing: .25em;
|
||||
font-size: 1.3rem;
|
||||
text-align: center;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
#wbf2faVerifyCode:focus {
|
||||
border-color: var(--c-primary) !important;
|
||||
box-shadow: 0 0 0 2px rgba(var(--c-primary-rgb, 99,102,241), .2);
|
||||
}
|
||||
|
||||
/* Schritt-3-Erfolgs-Animation */
|
||||
#wbf2faStep3 {
|
||||
animation: wbfFadeIn .3s ease;
|
||||
}
|
||||
|
||||
@keyframes wbfFadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── 2FA Login Modal ─────────────────────────────────────────────────────── */
|
||||
.wbf-2fa-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
backdrop-filter: blur(0px);
|
||||
transition: background .25s ease, backdrop-filter .25s ease;
|
||||
padding: 16px;
|
||||
}
|
||||
.wbf-2fa-modal-overlay.wbf-2fa-modal--visible {
|
||||
background: rgba(0, 0, 0, .65);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.wbf-2fa-modal-box {
|
||||
background: var(--c-bg-2, #1e2535);
|
||||
border: 1px solid rgba(234,179,8,.35);
|
||||
border-radius: 14px;
|
||||
padding: 28px 28px 24px;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,.5), 0 0 0 1px rgba(234,179,8,.1);
|
||||
transform: translateY(20px) scale(.97);
|
||||
opacity: 0;
|
||||
transition: transform .25s cubic-bezier(.34,1.3,.64,1), opacity .2s ease;
|
||||
}
|
||||
.wbf-2fa-modal--visible .wbf-2fa-modal-box {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Modal Header */
|
||||
.wbf-2fa-modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.wbf-2fa-modal-icon {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wbf-2fa-modal-title {
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--c-text, #e2e8f0);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.wbf-2fa-modal-sub {
|
||||
margin: 0;
|
||||
font-size: .82rem;
|
||||
color: var(--c-muted, #94a3b8);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Code-Eingabe im Modal */
|
||||
.wbf-2fa-modal-box .wbf-2fa-code-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 16px;
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: .35em;
|
||||
text-align: center;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
background: var(--c-bg, #141928);
|
||||
color: var(--c-text, #e2e8f0);
|
||||
border: 2px solid rgba(234,179,8,.3);
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.wbf-2fa-modal-box .wbf-2fa-code-input:focus {
|
||||
border-color: #eab308;
|
||||
box-shadow: 0 0 0 3px rgba(234,179,8,.2);
|
||||
}
|
||||
.wbf-2fa-modal-box .wbf-2fa-code-input::placeholder {
|
||||
color: rgba(148,163,184,.35);
|
||||
letter-spacing: .25em;
|
||||
}
|
||||
|
||||
/* Buttons im Modal */
|
||||
.wbf-2fa-modal-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.wbf-2fa-modal-actions .wbf-btn--primary {
|
||||
flex: 1;
|
||||
}
|
||||
.wbf-2fa-modal-actions .wbf-2fa-cancel-btn {
|
||||
font-size: .83rem;
|
||||
padding: 7px 14px;
|
||||
opacity: .65;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.wbf-2fa-modal-actions .wbf-2fa-cancel-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Fehlermeldung */
|
||||
.wbf-2fa-modal-box .wbf-2fa-msg {
|
||||
display: block;
|
||||
min-height: 1.2em;
|
||||
margin-top: 10px;
|
||||
font-size: .82rem;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -526,6 +536,13 @@
|
||||
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);
|
||||
@@ -609,6 +626,48 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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', '');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
FEATURE: Ungelesene Beiträge
|
||||
══════════════════════════════════════════════════════════ */
|
||||
@@ -1342,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();
|
||||
});
|
||||
}
|
||||
@@ -1357,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);
|
||||
}
|
||||
|
||||
@@ -1383,6 +1446,7 @@
|
||||
clearInterval(wbfCountTimer);
|
||||
$wbfToast.fadeOut(200);
|
||||
wbfWarning = false;
|
||||
wbfLogoutFired = false; // Guard zurücksetzen
|
||||
wbfResetIdleTimer();
|
||||
});
|
||||
|
||||
@@ -2163,4 +2227,132 @@
|
||||
$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:
|
||||
@@ -7,7 +7,7 @@ class WBF_Ajax {
|
||||
$actions = [
|
||||
'wbf_login', 'wbf_register', 'wbf_logout',
|
||||
'wbf_new_thread', 'wbf_new_post', 'wbf_toggle_like',
|
||||
'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image',
|
||||
'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image', 'wbf_upload_banner',
|
||||
'wbf_forgot_password', 'wbf_reset_password', 'wbf_load_more_messages',
|
||||
'wbf_create_invite', 'wbf_delete_invite',
|
||||
'wbf_toggle_subscribe', 'wbf_restore_content', 'wbf_toggle_profile_visibility',
|
||||
@@ -21,6 +21,13 @@ class WBF_Ajax {
|
||||
'wbf_toggle_ignore',
|
||||
'wbf_change_email',
|
||||
'wbf_save_notification_prefs',
|
||||
'wbf_save_discord',
|
||||
'wbf_discord_send_code',
|
||||
'wbf_discord_verify_code',
|
||||
'wbf_2fa_setup_begin',
|
||||
'wbf_2fa_setup_verify',
|
||||
'wbf_2fa_disable',
|
||||
'wbf_2fa_verify_login',
|
||||
];
|
||||
foreach ($actions as $action) {
|
||||
add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]);
|
||||
@@ -56,7 +63,8 @@ class WBF_Ajax {
|
||||
// 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
|
||||
@@ -66,6 +74,9 @@ class WBF_Ajax {
|
||||
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']) ) {
|
||||
@@ -137,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']);
|
||||
}
|
||||
@@ -185,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'] ?? '' );
|
||||
@@ -507,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() {
|
||||
@@ -1447,6 +1511,319 @@ class WBF_Ajax {
|
||||
] );
|
||||
}
|
||||
|
||||
// ── Discord: Verifikations-Code per Bot-DM senden ─────────────────────────
|
||||
|
||||
public static function handle_discord_send_code() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||
|
||||
$s = wbf_get_settings();
|
||||
$token = trim($s['discord_bot_token'] ?? '');
|
||||
$guild = trim($s['discord_guild_id'] ?? '');
|
||||
|
||||
if ( ! $token ) {
|
||||
wp_send_json_error(['message' => 'Discord-Bot ist noch nicht konfiguriert. Bitte wende dich an einen Admin.']);
|
||||
}
|
||||
|
||||
$username_input = sanitize_text_field($_POST['discord_username'] ?? '');
|
||||
if ( ! $username_input ) {
|
||||
wp_send_json_error(['message' => 'Bitte Discord-Benutzername eingeben.']);
|
||||
}
|
||||
|
||||
// Nutzer auf dem Guild suchen (nach Username oder per Search)
|
||||
$discord_user_id = self::discord_find_user_id($username_input, $token, $guild);
|
||||
if ( ! $discord_user_id ) {
|
||||
wp_send_json_error(['message' => 'Discord-Nutzer nicht auf dem Server gefunden. Stelle sicher, dass du Mitglied des Servers bist.']);
|
||||
}
|
||||
|
||||
// Verifikations-Code generieren (6-stellig)
|
||||
$code = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 6));
|
||||
$expires = time() + 600; // 10 Minuten
|
||||
|
||||
// Code + Discord-User-ID temporär speichern
|
||||
WBF_DB::set_user_meta($user->id, 'discord_verify_code', $code);
|
||||
WBF_DB::set_user_meta($user->id, 'discord_verify_expires', (string)$expires);
|
||||
WBF_DB::set_user_meta($user->id, 'discord_verify_pending_id', $discord_user_id);
|
||||
|
||||
// DM senden
|
||||
$sent = self::discord_send_dm($discord_user_id, $token,
|
||||
"🔐 **Dein Verifikationscode für " . get_bloginfo('name') . ":**\n\n" .
|
||||
"```" . $code . "```\n" .
|
||||
"Gib diesen Code im Forum ein. Er ist **10 Minuten** gültig.\n" .
|
||||
"_Falls du diese Nachricht nicht erwartet hast, ignoriere sie einfach._"
|
||||
);
|
||||
|
||||
if ( ! $sent ) {
|
||||
wp_send_json_error(['message' => 'DM konnte nicht gesendet werden. Stelle sicher, dass du DMs von Server-Mitgliedern zulässt.']);
|
||||
}
|
||||
|
||||
wp_send_json_success(['message' => '✅ Code gesendet! Prüfe deine Discord-DMs und gib den 6-stelligen Code ein.', 'step' => 'enter_code']);
|
||||
}
|
||||
|
||||
// ── Discord: Code überprüfen + Verbindung herstellen ─────────────────────
|
||||
|
||||
public static function handle_discord_verify_code() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||
|
||||
$code_input = strtoupper(sanitize_text_field($_POST['verify_code'] ?? ''));
|
||||
$meta = WBF_DB::get_user_meta($user->id);
|
||||
|
||||
$stored_code = strtoupper($meta['discord_verify_code'] ?? '');
|
||||
$expires = (int)($meta['discord_verify_expires'] ?? 0);
|
||||
$discord_uid = $meta['discord_verify_pending_id'] ?? '';
|
||||
|
||||
if ( ! $stored_code || ! $discord_uid ) {
|
||||
wp_send_json_error(['message' => 'Kein offener Verifizierungs-Vorgang. Bitte erneut starten.']);
|
||||
}
|
||||
if ( time() > $expires ) {
|
||||
wp_send_json_error(['message' => 'Code abgelaufen. Bitte erneut einen Code anfordern.']);
|
||||
}
|
||||
if ( ! hash_equals($stored_code, $code_input) ) {
|
||||
wp_send_json_error(['message' => 'Falscher Code. Bitte erneut versuchen.']);
|
||||
}
|
||||
|
||||
// Discord-Username abrufen (für Anzeige)
|
||||
$s = wbf_get_settings();
|
||||
$token = trim($s['discord_bot_token'] ?? '');
|
||||
$display_name = $discord_uid;
|
||||
if ( $token ) {
|
||||
$res = wp_remote_get("https://discord.com/api/v10/users/{$discord_uid}", [
|
||||
'timeout' => 5,
|
||||
'headers' => ['Authorization' => 'Bot ' . $token],
|
||||
]);
|
||||
if ( ! is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200 ) {
|
||||
$d = json_decode(wp_remote_retrieve_body($res), true);
|
||||
$display_name = $d['global_name'] ?? $d['username'] ?? $discord_uid;
|
||||
}
|
||||
}
|
||||
|
||||
// Speichern
|
||||
WBF_DB::set_user_meta($user->id, 'discord_user_id', $discord_uid);
|
||||
WBF_DB::set_user_meta($user->id, 'discord_username', $display_name);
|
||||
// Temp-Daten löschen
|
||||
WBF_DB::set_user_meta($user->id, 'discord_verify_code', '');
|
||||
WBF_DB::set_user_meta($user->id, 'discord_verify_expires', '');
|
||||
WBF_DB::set_user_meta($user->id, 'discord_verify_pending_id', '');
|
||||
|
||||
// Rollen-Sync direkt nach Verifikation
|
||||
$guild = trim($s['discord_guild_id'] ?? '');
|
||||
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
|
||||
if ( ($s['discord_role_sync'] ?? '0') === '1' && $token && $guild && $role_map ) {
|
||||
wbf_sync_discord_role_for_user($user->id, $discord_uid, $token, $guild, $role_map);
|
||||
}
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => '🎉 Discord erfolgreich verknüpft!',
|
||||
'connected' => true,
|
||||
'display_name' => esc_html($display_name),
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Discord: Verbindung trennen ───────────────────────────────────────────
|
||||
|
||||
public static function handle_save_discord() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||
|
||||
$action = sanitize_key( $_POST['sub_action'] ?? 'save' );
|
||||
|
||||
if ( $action === 'disconnect' ) {
|
||||
WBF_DB::set_user_meta($user->id, 'discord_username', '');
|
||||
WBF_DB::set_user_meta($user->id, 'discord_user_id', '');
|
||||
wp_send_json_success(['message' => 'Discord-Verbindung getrennt.', 'connected' => false]);
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => 'Unbekannte Aktion.']);
|
||||
}
|
||||
|
||||
// ── Discord Hilfsmethoden ─────────────────────────────────────────────────
|
||||
|
||||
private static function discord_find_user_id($username_input, $token, $guild) {
|
||||
if ( ! $guild ) return null;
|
||||
// Guild-Member-Search (max. 1 Treffer)
|
||||
$search = rawurlencode($username_input);
|
||||
$res = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}/members/search?query={$search}&limit=5", [
|
||||
'timeout' => 6,
|
||||
'headers' => ['Authorization' => 'Bot ' . $token],
|
||||
]);
|
||||
if ( is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200 ) return null;
|
||||
$members = json_decode(wp_remote_retrieve_body($res), true);
|
||||
if ( empty($members) ) return null;
|
||||
// Exakten Treffer bevorzugen
|
||||
$input_lower = strtolower($username_input);
|
||||
foreach ( $members as $m ) {
|
||||
$uname = strtolower($m['user']['username'] ?? '');
|
||||
$global = strtolower($m['user']['global_name'] ?? '');
|
||||
if ( $uname === $input_lower || $global === $input_lower ) {
|
||||
return $m['user']['id'];
|
||||
}
|
||||
}
|
||||
// Erster Treffer als Fallback
|
||||
return $members[0]['user']['id'] ?? null;
|
||||
}
|
||||
|
||||
private static function discord_send_dm($user_id, $token, $message) {
|
||||
// DM-Channel erstellen
|
||||
$ch_res = wp_remote_post('https://discord.com/api/v10/users/@me/channels', [
|
||||
'timeout' => 6,
|
||||
'headers' => ['Authorization' => 'Bot ' . $token, 'Content-Type' => 'application/json'],
|
||||
'body' => json_encode(['recipient_id' => $user_id]),
|
||||
]);
|
||||
if ( is_wp_error($ch_res) || wp_remote_retrieve_response_code($ch_res) !== 200 ) return false;
|
||||
$channel = json_decode(wp_remote_retrieve_body($ch_res), true);
|
||||
$ch_id = $channel['id'] ?? '';
|
||||
if ( ! $ch_id ) return false;
|
||||
|
||||
// Nachricht senden
|
||||
$msg_res = wp_remote_post("https://discord.com/api/v10/channels/{$ch_id}/messages", [
|
||||
'timeout' => 6,
|
||||
'headers' => ['Authorization' => 'Bot ' . $token, 'Content-Type' => 'application/json'],
|
||||
'body' => json_encode(['content' => $message]),
|
||||
]);
|
||||
return ( ! is_wp_error($msg_res) && wp_remote_retrieve_response_code($msg_res) === 200 );
|
||||
}
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ── 2FA / TOTP ────────────────────────────────────────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Setup-Schritt 1: Neues Secret generieren und als "pending" speichern.
|
||||
* Gibt Secret (zur manuellen Eingabe) und otpauth:// URI (für QR-Code) zurück.
|
||||
*/
|
||||
public static function handle_2fa_setup_begin() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
|
||||
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
|
||||
|
||||
$secret = WBF_TOTP::generate_secret();
|
||||
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_PENDING, $secret );
|
||||
|
||||
wp_send_json_success( [
|
||||
"secret" => $secret,
|
||||
"uri" => WBF_TOTP::get_otpauth_uri( $user->username, $secret ),
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup-Schritt 2: Code verifizieren und 2FA aktivieren.
|
||||
*/
|
||||
public static function handle_2fa_setup_verify() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
|
||||
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
|
||||
|
||||
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
|
||||
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_PENDING );
|
||||
|
||||
if ( empty($secret) ) {
|
||||
wp_send_json_error( ["message" => "Kein ausstehender 2FA-Setup. Bitte neu starten."] );
|
||||
}
|
||||
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
|
||||
wp_send_json_error( ["message" => "Ungültiger Code. Bitte Uhrzeit prüfen und erneut versuchen."] );
|
||||
}
|
||||
|
||||
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_SECRET, $secret );
|
||||
global $wpdb;
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_user_meta",
|
||||
["user_id" => $user->id, "meta_key" => WBF_TOTP::META_PENDING], ["%d", "%s"] );
|
||||
|
||||
wp_send_json_success( ["message" => "2FA erfolgreich aktiviert!"] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 2FA deaktivieren (User-seitig).
|
||||
* Erfordert aktuelles Passwort + gültigen TOTP-Code.
|
||||
*/
|
||||
public static function handle_2fa_disable() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
|
||||
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
|
||||
|
||||
$password = $_POST["password"] ?? "";
|
||||
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
|
||||
|
||||
$fresh = WBF_DB::get_user( $user->id );
|
||||
if ( ! $fresh || ! password_verify( $password, $fresh->password ) ) {
|
||||
wp_send_json_error( ["message" => "Falsches Passwort."] );
|
||||
}
|
||||
|
||||
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
|
||||
if ( empty($secret) ) {
|
||||
wp_send_json_error( ["message" => "2FA ist nicht aktiv."] );
|
||||
}
|
||||
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
|
||||
wp_send_json_error( ["message" => "Ungültiger Authenticator-Code."] );
|
||||
}
|
||||
|
||||
WBF_TOTP::disable_for( $user->id );
|
||||
wp_send_json_success( ["message" => "2FA wurde deaktiviert."] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Login-Schritt 2: TOTP-Code nach erfolgreichem Passwort prüfen.
|
||||
* Kein Nonce — ausstehende Session-ID ist der Auth-Beweis.
|
||||
* Brute-Force-Schutz: max. 5 Versuche / IP / 10 Min.
|
||||
*/
|
||||
public static function handle_2fa_verify_login() {
|
||||
WBF_Auth::init();
|
||||
|
||||
$ip_key = "wbf_2fa_fail_" . md5( $_SERVER["REMOTE_ADDR"] ?? "unknown" );
|
||||
$fails = (int) get_transient( $ip_key );
|
||||
if ( $fails >= 5 ) {
|
||||
wp_send_json_error( ["message" => "Zu viele Fehlversuche. Bitte warte 10 Minuten.", "locked" => true] );
|
||||
}
|
||||
|
||||
$pending = (int) ( $_SESSION[ WBF_TOTP::SESSION_PENDING ] ?? 0 );
|
||||
if ( ! $pending ) {
|
||||
wp_send_json_error( ["message" => "Keine ausstehende Anmeldung. Bitte erneut einloggen."] );
|
||||
}
|
||||
|
||||
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
|
||||
$user = WBF_DB::get_user( $pending );
|
||||
|
||||
if ( ! $user ) {
|
||||
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
|
||||
wp_send_json_error( ["message" => "Ungültige Sitzung."] );
|
||||
}
|
||||
|
||||
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
|
||||
if ( empty($secret) || ! WBF_TOTP::verify( $secret, $code ) ) {
|
||||
set_transient( $ip_key, $fails + 1, 10 * MINUTE_IN_SECONDS );
|
||||
wp_send_json_error( ["message" => "Ungültiger Code. Bitte erneut versuchen."] );
|
||||
}
|
||||
|
||||
delete_transient( $ip_key );
|
||||
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
|
||||
|
||||
if ( WBF_Roles::level($user->role) < 0 ) {
|
||||
wp_send_json_error( ["message" => "Dein Konto ist gesperrt."] );
|
||||
}
|
||||
|
||||
if ( session_id() ) session_regenerate_id( true );
|
||||
$_SESSION[ WBF_Auth::SESSION_KEY ] = $user->id;
|
||||
WBF_DB::touch_last_active( $user->id );
|
||||
|
||||
if ( ! empty( $_SESSION["wbf_2fa_remember"] ) ) {
|
||||
WBF_Auth::set_remember_cookie( $user->id );
|
||||
unset( $_SESSION["wbf_2fa_remember"] );
|
||||
}
|
||||
|
||||
wp_send_json_success( [
|
||||
"display_name" => $user->display_name,
|
||||
"avatar_url" => $user->avatar_url,
|
||||
"user_id" => $user->id,
|
||||
] );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
add_action( 'init', [ 'WBF_Ajax', 'init' ] );
|
||||
@@ -3,7 +3,7 @@ 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,
|
||||
@@ -11,8 +11,6 @@ class WBF_Auth {
|
||||
// Lösung: headers_sent() prüfen + session_start() mit Cookie-Optionen aufrufen.
|
||||
if ( ! session_id() ) {
|
||||
if ( headers_sent() ) {
|
||||
// Headers bereits gesendet — Session kann nicht sicher gestartet werden.
|
||||
// Passiert z.B. wenn WP_DEBUG=true und PHP Notices vor dem Hook ausgegeben hat.
|
||||
return;
|
||||
}
|
||||
$session_opts = [
|
||||
@@ -20,7 +18,6 @@ class WBF_Auth {
|
||||
'cookie_samesite' => 'Lax',
|
||||
'use_strict_mode' => true,
|
||||
];
|
||||
// cookie_secure nur setzen wenn HTTPS aktiv — verhindert Session-Verlust bei HTTP
|
||||
if ( is_ssl() || ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ) {
|
||||
$session_opts['cookie_secure'] = true;
|
||||
}
|
||||
@@ -50,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 ) {
|
||||
@@ -60,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() ) {
|
||||
@@ -70,22 +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 Fixation verhindern
|
||||
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 Fixation verhindern
|
||||
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 );
|
||||
@@ -115,7 +123,7 @@ class WBF_Auth {
|
||||
'avatar_url' => $avatar,
|
||||
));
|
||||
|
||||
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
|
||||
if ( session_id() ) session_regenerate_id( true );
|
||||
$_SESSION[ self::SESSION_KEY ] = $id;
|
||||
return array('success'=>true,'user'=>WBF_DB::get_user($id));
|
||||
}
|
||||
@@ -124,10 +132,14 @@ class WBF_Auth {
|
||||
self::init();
|
||||
$user_id = $_SESSION[ self::SESSION_KEY ] ?? 0;
|
||||
unset( $_SESSION[ self::SESSION_KEY ] );
|
||||
// 2FA-Pending-State ebenfalls löschen
|
||||
if ( class_exists('WBF_TOTP') ) {
|
||||
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
|
||||
unset( $_SESSION['wbf_2fa_remember'] );
|
||||
}
|
||||
if ( $user_id ) {
|
||||
WBF_DB::delete_remember_token( (int)$user_id );
|
||||
}
|
||||
// Remove cookie
|
||||
if ( isset($_COOKIE['wbf_remember']) ) {
|
||||
setcookie( 'wbf_remember', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
|
||||
}
|
||||
|
||||
@@ -123,12 +123,23 @@ class WBF_BBCode {
|
||||
$s
|
||||
);
|
||||
|
||||
// [size=small|large|xlarge]
|
||||
// [size=small|large|xlarge] oder [size=1–7] (klassisches BBCode)
|
||||
$s = preg_replace_callback(
|
||||
'/\[size=(small|large|xlarge)\](.*?)\[\/size\]/is',
|
||||
'/\[size=([a-zA-Z0-9]+)\](.*?)\[\/size\]/is',
|
||||
function ( $m ) {
|
||||
$map = [ 'small' => '.8em', 'large' => '1.2em', 'xlarge' => '1.5em' ];
|
||||
return '<span style="font-size:' . $map[$m[1]] . '">' . $m[2] . '</span>';
|
||||
$val = strtolower( $m[1] );
|
||||
// Benannte Größen
|
||||
$named = [ 'small' => '.8em', 'large' => '1.2em', 'xlarge' => '1.5em' ];
|
||||
if ( isset( $named[ $val ] ) ) {
|
||||
$size = $named[ $val ];
|
||||
// Numerische Größen 1–7 (klassisches BBCode-Schema)
|
||||
} elseif ( ctype_digit( $val ) && (int)$val >= 1 && (int)$val <= 7 ) {
|
||||
$num_map = [ 1 => '.7em', 2 => '.85em', 3 => '1em', 4 => '1.2em', 5 => '1.4em', 6 => '1.6em', 7 => '2em' ];
|
||||
$size = $num_map[ (int)$val ];
|
||||
} else {
|
||||
return $m[2]; // Unbekannter Wert → nur Text
|
||||
}
|
||||
return '<span style="font-size:' . $size . '">' . $m[2] . '</span>';
|
||||
},
|
||||
$s
|
||||
);
|
||||
|
||||
@@ -151,7 +151,6 @@ class WBF_DB {
|
||||
dbDelta( $sql_threads );
|
||||
dbDelta( $sql_posts );
|
||||
dbDelta( $sql_likes );
|
||||
dbDelta( $sql_reports );
|
||||
dbDelta( $sql_tags );
|
||||
dbDelta( $sql_thread_tags );
|
||||
dbDelta( $sql_messages );
|
||||
@@ -174,6 +173,8 @@ class WBF_DB {
|
||||
// Zeitlich begrenzte Sperren
|
||||
self::maybe_add_column("{$wpdb->prefix}forum_users", 'ban_until', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN ban_until DATETIME DEFAULT NULL");
|
||||
self::maybe_add_column("{$wpdb->prefix}forum_users", 'pre_ban_role', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN pre_ban_role VARCHAR(20) DEFAULT 'member'");
|
||||
// Profilbanner
|
||||
self::maybe_add_column("{$wpdb->prefix}forum_users", 'banner_url', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN banner_url VARCHAR(255) DEFAULT ''");
|
||||
// Thread-Abonnements
|
||||
$sql_subscriptions = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_subscriptions (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
@@ -199,7 +200,6 @@ class WBF_DB {
|
||||
) $charset;";
|
||||
|
||||
// Ensure reports + notifications tables exist on existing installs
|
||||
dbDelta( $sql_reports );
|
||||
dbDelta( $sql_notifications );
|
||||
|
||||
// Einladungs-Tabelle
|
||||
@@ -354,6 +354,67 @@ class WBF_DB {
|
||||
public static function update_user( $id, $data ) {
|
||||
global $wpdb;
|
||||
$wpdb->update("{$wpdb->prefix}forum_users", $data, ['id' => $id]);
|
||||
|
||||
// --- Discord-Rollen-Sync nach Rollenänderung ---
|
||||
if (isset($data['role'])) {
|
||||
// Discord-User-ID holen
|
||||
$discord_user_id = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT meta_value FROM {$wpdb->prefix}forum_user_meta WHERE user_id = %d AND meta_key = 'discord_user_id'",
|
||||
$id
|
||||
));
|
||||
if ($discord_user_id) {
|
||||
// Einstellungen laden
|
||||
$s = function_exists('wbf_get_settings') ? wbf_get_settings() : [];
|
||||
$token = trim($s['discord_bot_token'] ?? '');
|
||||
$guild = trim($s['discord_guild_id'] ?? '');
|
||||
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
|
||||
if ($token && $guild && !empty($role_map)) {
|
||||
// Ziel-Discord-Rolle anhand Mapping finden
|
||||
$target_discord_role = null;
|
||||
foreach ($role_map as $dc_role_id => $forum_role) {
|
||||
if ($forum_role === $data['role']) {
|
||||
$target_discord_role = (string)$dc_role_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($target_discord_role) {
|
||||
// Aktuelle Rollen des Users abrufen
|
||||
$res = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}/members/{$discord_user_id}", [
|
||||
'timeout' => 6,
|
||||
'headers' => ['Authorization' => 'Bot ' . $token],
|
||||
]);
|
||||
if (!is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200) {
|
||||
$member = json_decode(wp_remote_retrieve_body($res), true);
|
||||
$user_roles = $member['roles'] ?? [];
|
||||
// Alle gemappten Discord-Rollen entfernen, außer Zielrolle
|
||||
$remove_roles = [];
|
||||
foreach ($role_map as $dc_role_id => $forum_role) {
|
||||
if ((string)$dc_role_id !== $target_discord_role && in_array((string)$dc_role_id, $user_roles, true)) {
|
||||
$remove_roles[] = (string)$dc_role_id;
|
||||
}
|
||||
}
|
||||
// Zielrolle hinzufügen, falls nicht vorhanden
|
||||
if (!in_array($target_discord_role, $user_roles, true)) {
|
||||
$user_roles[] = $target_discord_role;
|
||||
}
|
||||
// Entfernte Rollen rausnehmen
|
||||
$user_roles = array_values(array_diff($user_roles, $remove_roles));
|
||||
// PATCH an Discord senden
|
||||
$body = json_encode(['roles' => array_values($user_roles)]);
|
||||
wp_remote_request("https://discord.com/api/v10/guilds/{$guild}/members/{$discord_user_id}", [
|
||||
'method' => 'PATCH',
|
||||
'timeout' => 6,
|
||||
'headers' => [
|
||||
'Authorization' => 'Bot ' . $token,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => $body,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function get_all_users( $limit = 100, $offset = 0 ) {
|
||||
@@ -541,10 +602,24 @@ class WBF_DB {
|
||||
) );
|
||||
}
|
||||
}
|
||||
// Posts zählen, User-IDs sammeln
|
||||
$posts = $wpdb->get_results($wpdb->prepare("SELECT user_id FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $id));
|
||||
$post_count = count($posts);
|
||||
$user_post_counts = [];
|
||||
foreach ($posts as $p) {
|
||||
$uid = (int)$p->user_id;
|
||||
if (!isset($user_post_counts[$uid])) $user_post_counts[$uid] = 0;
|
||||
$user_post_counts[$uid]++;
|
||||
}
|
||||
// Posts löschen
|
||||
$wpdb->delete("{$wpdb->prefix}forum_posts", ['thread_id' => $id]);
|
||||
$wpdb->delete("{$wpdb->prefix}forum_threads", ['id' => $id]);
|
||||
// Zähler anpassen
|
||||
if ( $thread->status !== 'archived' ) {
|
||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d", $thread->category_id));
|
||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0), post_count=GREATEST(post_count-%d,0) WHERE id=%d", $post_count, $thread->category_id));
|
||||
}
|
||||
foreach ($user_post_counts as $uid => $cnt) {
|
||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=GREATEST(post_count-%d,0) WHERE id=%d", $cnt, $uid));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,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 ) {
|
||||
@@ -1190,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;
|
||||
@@ -1205,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
|
||||
) );
|
||||
}
|
||||
|
||||
@@ -1397,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 ) {
|
||||
@@ -1524,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(
|
||||
|
||||
@@ -130,7 +130,7 @@ class WBF_Export {
|
||||
|
||||
case 'users':
|
||||
$data['users'] = $wpdb->get_results(
|
||||
"SELECT id, username, email, password, display_name, avatar_url,
|
||||
"SELECT id, username, email, password, display_name, avatar_url, banner_url,
|
||||
bio, signature, role, pre_ban_role, ban_reason, ban_until,
|
||||
post_count, registered, last_active, profile_public,
|
||||
reset_token, reset_token_expires
|
||||
|
||||
480
includes/class-forum-mc-bridge.php
Normal file
480
includes/class-forum-mc-bridge.php
Normal file
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
/**
|
||||
* WBF_MC_Bridge — Minecraft ↔ Forum Verknüpfung & Ingame-Benachrichtigungen
|
||||
*
|
||||
* Dieses Modul verbindet das WP Business Forum mit dem BungeeCord StatusAPI Plugin.
|
||||
*
|
||||
* Features:
|
||||
* - Account-Verknüpfung: Forum-User ↔ MC-UUID (über Token-System)
|
||||
* - Push-Benachrichtigungen: Neue Antwort/Erwähnung/PN → Ingame-Nachricht
|
||||
* - REST API Endpoints für die BungeeCord-Seite
|
||||
*
|
||||
* Einbindung in wp-business-forum.php:
|
||||
* require_once WBF_PATH . 'includes/class-forum-mc-bridge.php';
|
||||
*
|
||||
* Konfiguration in WBF-Einstellungen (Admin → Forum → Einstellungen):
|
||||
* mc_bridge_enabled = true/false
|
||||
* mc_bridge_api_url = http://server-ip:9191 (StatusAPI URL)
|
||||
* mc_bridge_api_secret = Shared Secret für API-Authentifizierung
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
class WBF_MC_Bridge {
|
||||
|
||||
/** Meta-Keys in forum_user_meta */
|
||||
const META_MC_UUID = 'mc_uuid';
|
||||
const META_MC_NAME = 'mc_name';
|
||||
const META_LINK_TOKEN = 'mc_link_token';
|
||||
const META_LINK_EXPIRY = 'mc_link_token_expires';
|
||||
|
||||
/**
|
||||
* Hooks registrieren — wird beim Plugin-Laden aufgerufen.
|
||||
*/
|
||||
public static function init() {
|
||||
// Hook: Wird in der modifizierten WBF_DB::create_notification() gefeuert
|
||||
add_action( 'wbf_notification_created', [ __CLASS__, 'on_notification' ], 10, 4 );
|
||||
|
||||
// REST API Endpoints für BungeeCord
|
||||
add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] );
|
||||
|
||||
// AJAX: Token generieren (für eingeloggte Forum-User)
|
||||
add_action( 'wp_ajax_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
|
||||
add_action( 'wp_ajax_nopriv_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
|
||||
|
||||
// AJAX: Verknüpfung lösen
|
||||
add_action( 'wp_ajax_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
|
||||
add_action( 'wp_ajax_nopriv_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
|
||||
|
||||
// AJAX: Link-Status prüfen (Polling im Profil nach Token-Generierung)
|
||||
add_action( 'wp_ajax_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
|
||||
add_action( 'wp_ajax_nopriv_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ── Einstellungen ─────────────────────────────────────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Prüft ob die MC-Bridge aktiviert ist.
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||
return ! empty( $s['mc_bridge_enabled'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die StatusAPI-URL zurück (z.B. http://192.168.1.100:9191).
|
||||
*/
|
||||
private static function get_api_url() {
|
||||
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||
return rtrim( $s['mc_bridge_api_url'] ?? '', '/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das Shared Secret zurück.
|
||||
*/
|
||||
private static function get_api_secret() {
|
||||
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||
return $s['mc_bridge_api_secret'] ?? '';
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ── Notification Hook ─────────────────────────────────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Wird aufgerufen wenn eine Forum-Notification erstellt wird.
|
||||
* Prüft ob der Empfänger eine MC-UUID hat und pusht die Nachricht an BungeeCord.
|
||||
*
|
||||
* @param int $user_id Forum-User ID des Empfängers
|
||||
* @param string $type Typ: 'reply', 'mention', 'message'
|
||||
* @param int $object_id Thread-ID (bei reply/mention) oder Message-ID (bei message)
|
||||
* @param int $actor_id Forum-User ID des Auslösers
|
||||
*/
|
||||
public static function on_notification( $user_id, $type, $object_id, $actor_id ) {
|
||||
if ( ! self::is_enabled() ) return;
|
||||
|
||||
$api_url = self::get_api_url();
|
||||
if ( empty( $api_url ) ) return;
|
||||
|
||||
// MC-UUID des Empfängers prüfen
|
||||
$mc_uuid = WBF_DB::get_user_meta_single( $user_id, self::META_MC_UUID );
|
||||
if ( empty( $mc_uuid ) ) return;
|
||||
|
||||
// Actor-Info laden
|
||||
$actor = WBF_DB::get_user( (int) $actor_id );
|
||||
$actor_name = $actor ? $actor->display_name : 'Unbekannt';
|
||||
|
||||
// Kontext-Daten sammeln
|
||||
$title = '';
|
||||
$url = '';
|
||||
$forum_url = wbf_get_forum_url();
|
||||
|
||||
switch ( $type ) {
|
||||
case 'reply':
|
||||
case 'mention':
|
||||
$thread = WBF_DB::get_thread( (int) $object_id );
|
||||
if ( $thread ) {
|
||||
$title = $thread->title;
|
||||
$url = $forum_url . '?forum_thread=' . (int) $thread->id;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'message':
|
||||
$title = 'Neue Privatnachricht';
|
||||
$url = $forum_url . '?forum_dm=1';
|
||||
break;
|
||||
}
|
||||
|
||||
// Push an BungeeCord senden
|
||||
self::push_to_bungee( $mc_uuid, $type, $title, $actor_name, $url, $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet die Benachrichtigung per HTTP POST an den BungeeCord StatusAPI Server.
|
||||
*/
|
||||
private static function push_to_bungee( $mc_uuid, $type, $title, $author, $url, $wp_user_id ) {
|
||||
$api_url = self::get_api_url();
|
||||
$secret = self::get_api_secret();
|
||||
|
||||
$payload = wp_json_encode( [
|
||||
'player_uuid' => $mc_uuid,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'author' => $author,
|
||||
'url' => $url,
|
||||
'wp_user_id' => (int) $wp_user_id,
|
||||
] );
|
||||
|
||||
$args = [
|
||||
'method' => 'POST',
|
||||
'timeout' => 5,
|
||||
'blocking' => false, // Non-blocking — Seite wartet nicht auf Antwort
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json; charset=UTF-8',
|
||||
'X-Api-Key' => $secret,
|
||||
],
|
||||
'body' => $payload,
|
||||
'sslverify' => false, // Lokales Netzwerk braucht kein SSL
|
||||
];
|
||||
|
||||
wp_remote_post( $api_url . '/forum/notify', $args );
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ── REST API (für BungeeCord → WordPress) ─────────────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
public static function register_rest_routes() {
|
||||
// POST /wp-json/mc-bridge/v1/verify-link
|
||||
// BungeeCord schickt Token + MC-UUID → WP verifiziert und speichert
|
||||
register_rest_route( 'mc-bridge/v1', '/verify-link', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [ __CLASS__, 'rest_verify_link' ],
|
||||
'permission_callback' => '__return_true',
|
||||
] );
|
||||
|
||||
// POST /wp-json/mc-bridge/v1/unlink
|
||||
// BungeeCord kann Verknüpfung auch von der MC-Seite lösen
|
||||
register_rest_route( 'mc-bridge/v1', '/unlink', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [ __CLASS__, 'rest_unlink' ],
|
||||
'permission_callback' => '__return_true',
|
||||
] );
|
||||
|
||||
// GET /wp-json/mc-bridge/v1/status
|
||||
// Verbindungstest
|
||||
register_rest_route( 'mc-bridge/v1', '/status', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [ __CLASS__, 'rest_status' ],
|
||||
'permission_callback' => '__return_true',
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* REST: Verknüpfung bestätigen.
|
||||
* BungeeCord sendet: { "token": "...", "mc_uuid": "...", "mc_name": "..." }
|
||||
*/
|
||||
public static function rest_verify_link( $request ) {
|
||||
// Rate Limiting: max 10 Versuche pro IP pro Minute
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$limit_key = 'wbf_mc_link_' . md5($ip);
|
||||
$attempts = (int) get_transient($limit_key);
|
||||
if ($attempts >= 10) {
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'rate_limited',
|
||||
'message' => 'Zu viele Versuche. Bitte warte eine Minute.'
|
||||
], 429);
|
||||
}
|
||||
set_transient($limit_key, $attempts + 1, 60);
|
||||
// API-Secret prüfen
|
||||
$secret = self::get_api_secret();
|
||||
if ( ! empty( $secret ) ) {
|
||||
$provided = $request->get_header( 'X-Api-Key' );
|
||||
if ( $provided !== $secret ) {
|
||||
return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 );
|
||||
}
|
||||
}
|
||||
|
||||
$token = sanitize_text_field( $request->get_param( 'token' ) );
|
||||
$mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) );
|
||||
$mc_name = sanitize_text_field( $request->get_param( 'mc_name' ) );
|
||||
|
||||
if ( empty( $token ) || empty( $mc_uuid ) ) {
|
||||
return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_fields' ], 400 );
|
||||
}
|
||||
|
||||
// Token in forum_user_meta suchen
|
||||
global $wpdb;
|
||||
$meta_row = $wpdb->get_row( $wpdb->prepare(
|
||||
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
|
||||
self::META_LINK_TOKEN, $token
|
||||
) );
|
||||
|
||||
if ( ! $meta_row ) {
|
||||
return new WP_REST_Response( [ 'success' => false, 'error' => 'invalid_token' ], 404 );
|
||||
}
|
||||
|
||||
$forum_user_id = (int) $meta_row->user_id;
|
||||
|
||||
// Ablauf prüfen
|
||||
$expiry_meta = WBF_DB::get_user_meta( $forum_user_id );
|
||||
$expiry = $expiry_meta[ self::META_LINK_EXPIRY ] ?? '0';
|
||||
if ( (int) $expiry < time() ) {
|
||||
// Token abgelaufen — aufräumen
|
||||
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' );
|
||||
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' );
|
||||
return new WP_REST_Response( [ 'success' => false, 'error' => 'token_expired' ], 410 );
|
||||
}
|
||||
|
||||
// Prüfen ob diese MC-UUID bereits mit einem anderen Account verknüpft ist
|
||||
$existing = $wpdb->get_row( $wpdb->prepare(
|
||||
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
|
||||
self::META_MC_UUID, $mc_uuid
|
||||
) );
|
||||
if ( $existing && (int) $existing->user_id !== $forum_user_id ) {
|
||||
return new WP_REST_Response( [
|
||||
'success' => false,
|
||||
'error' => 'uuid_already_linked',
|
||||
'message' => 'Diese Minecraft-UUID ist bereits mit einem anderen Forum-Account verknüpft.',
|
||||
], 409 );
|
||||
}
|
||||
|
||||
// Verknüpfung speichern
|
||||
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, $mc_uuid );
|
||||
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, $mc_name );
|
||||
|
||||
// Token aufräumen
|
||||
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' );
|
||||
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' );
|
||||
|
||||
// Forum-User-Info für BungeeCord zurückgeben
|
||||
$forum_user = WBF_DB::get_user( $forum_user_id );
|
||||
|
||||
return new WP_REST_Response( [
|
||||
'success' => true,
|
||||
'forum_user_id' => $forum_user_id,
|
||||
'display_name' => $forum_user ? $forum_user->display_name : '',
|
||||
'username' => $forum_user ? $forum_user->username : '',
|
||||
], 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* REST: Verknüpfung lösen (von BungeeCord-Seite).
|
||||
*/
|
||||
public static function rest_unlink( $request ) {
|
||||
$secret = self::get_api_secret();
|
||||
if ( ! empty( $secret ) ) {
|
||||
$provided = $request->get_header( 'X-Api-Key' );
|
||||
if ( $provided !== $secret ) {
|
||||
return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 );
|
||||
}
|
||||
}
|
||||
|
||||
$mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) );
|
||||
if ( empty( $mc_uuid ) ) {
|
||||
return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_mc_uuid' ], 400 );
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$meta_row = $wpdb->get_row( $wpdb->prepare(
|
||||
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
|
||||
self::META_MC_UUID, $mc_uuid
|
||||
) );
|
||||
|
||||
if ( ! $meta_row ) {
|
||||
return new WP_REST_Response( [ 'success' => false, 'error' => 'not_linked' ], 404 );
|
||||
}
|
||||
|
||||
$forum_user_id = (int) $meta_row->user_id;
|
||||
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, '' );
|
||||
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, '' );
|
||||
|
||||
return new WP_REST_Response( [ 'success' => true ], 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* REST: Status-Endpoint für Verbindungstest.
|
||||
*/
|
||||
public static function rest_status( $request ) {
|
||||
return new WP_REST_Response( [
|
||||
'success' => true,
|
||||
'enabled' => self::is_enabled(),
|
||||
'version' => defined( 'WBF_VERSION' ) ? WBF_VERSION : '?',
|
||||
'plugin' => 'WP Business Forum',
|
||||
], 200 );
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ── AJAX: Token generieren ────────────────────────────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Generiert einen 8-stelligen Verknüpfungs-Token (15 Minuten gültig).
|
||||
* Der User gibt diesen Token dann ingame mit /forumlink <token> ein.
|
||||
*/
|
||||
public static function ajax_generate_token() {
|
||||
check_ajax_referer( 'wbf_nonce', 'nonce' );
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) {
|
||||
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] );
|
||||
}
|
||||
|
||||
// Prüfen ob bereits verknüpft
|
||||
$existing_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
|
||||
if ( ! empty( $existing_uuid ) ) {
|
||||
$mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME );
|
||||
wp_send_json_error( [
|
||||
'message' => 'Dein Account ist bereits mit ' . esc_html( $mc_name ?: $existing_uuid ) . ' verknüpft.',
|
||||
'linked' => true,
|
||||
'mc_name' => $mc_name,
|
||||
'mc_uuid' => $existing_uuid,
|
||||
] );
|
||||
}
|
||||
|
||||
// Token generieren: 8 Zeichen, alphanumerisch, uppercase
|
||||
$token = strtoupper( substr( bin2hex( random_bytes( 5 ) ), 0, 8 ) );
|
||||
$expiry = time() + ( 15 * 60 ); // 15 Minuten
|
||||
|
||||
WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, $token );
|
||||
WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, (string) $expiry );
|
||||
|
||||
wp_send_json_success( [
|
||||
'token' => $token,
|
||||
'expires_in' => 15, // Minuten
|
||||
'command' => '/forumlink ' . $token,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Verknüpfung lösen (von der Forum-Seite).
|
||||
*/
|
||||
public static function ajax_unlink() {
|
||||
check_ajax_referer( 'wbf_nonce', 'nonce' );
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) {
|
||||
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] );
|
||||
}
|
||||
|
||||
$mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
|
||||
|
||||
// Meta löschen
|
||||
WBF_DB::set_user_meta( $user->id, self::META_MC_UUID, '' );
|
||||
WBF_DB::set_user_meta( $user->id, self::META_MC_NAME, '' );
|
||||
WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, '' );
|
||||
WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, '' );
|
||||
|
||||
// Optional: BungeeCord informieren
|
||||
if ( ! empty( $mc_uuid ) && self::is_enabled() ) {
|
||||
$api_url = self::get_api_url();
|
||||
$secret = self::get_api_secret();
|
||||
if ( ! empty( $api_url ) ) {
|
||||
wp_remote_post( $api_url . '/forum/unlink', [
|
||||
'method' => 'POST',
|
||||
'timeout' => 3,
|
||||
'blocking' => false,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Api-Key' => $secret,
|
||||
],
|
||||
'body' => wp_json_encode( [ 'mc_uuid' => $mc_uuid ] ),
|
||||
'sslverify' => false,
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success( [ 'message' => 'Minecraft-Verknüpfung wurde aufgehoben.' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Verknüpfungs-Status prüfen.
|
||||
* Wird vom Frontend-Polling alle 5 Sekunden nach Token-Generierung aufgerufen.
|
||||
* Gibt zurück ob der User bereits verknüpft ist (BungeeCord hat verify-link gesendet).
|
||||
*/
|
||||
public static function ajax_link_status() {
|
||||
check_ajax_referer( 'wbf_nonce', 'nonce' );
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) {
|
||||
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.', 'linked' => false ] );
|
||||
return;
|
||||
}
|
||||
$mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
|
||||
$mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME );
|
||||
if ( ! empty( $mc_uuid ) ) {
|
||||
wp_send_json_success( [
|
||||
'linked' => true,
|
||||
'mc_uuid' => $mc_uuid,
|
||||
'mc_name' => $mc_name,
|
||||
] );
|
||||
} else {
|
||||
wp_send_json_success( [ 'linked' => false ] );
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Gibt die MC-UUID eines Forum-Users zurück (oder leer).
|
||||
*/
|
||||
public static function get_mc_uuid( $forum_user_id ) {
|
||||
return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_UUID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den MC-Namen eines Forum-Users zurück (oder leer).
|
||||
*/
|
||||
public static function get_mc_name( $forum_user_id ) {
|
||||
return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Forum-User mit MC verknüpft ist.
|
||||
*/
|
||||
public static function is_linked( $forum_user_id ) {
|
||||
$uuid = self::get_mc_uuid( $forum_user_id );
|
||||
return ! empty( $uuid );
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML-Badge für Profilansicht: Zeigt MC-Verknüpfung an.
|
||||
*/
|
||||
public static function profile_badge( $forum_user_id ) {
|
||||
if ( ! self::is_enabled() ) return '';
|
||||
$mc_name = self::get_mc_name( $forum_user_id );
|
||||
if ( empty( $mc_name ) ) return '';
|
||||
|
||||
$name_esc = esc_html( $mc_name );
|
||||
$head_url = 'https://mc-heads.net/avatar/' . urlencode( $mc_name ) . '/24';
|
||||
return "<span class=\"wbf-mc-badge\" title=\"Minecraft: {$name_esc}\">"
|
||||
. "<img src=\"{$head_url}\" alt=\"\" width=\"16\" height=\"16\" style=\"border-radius:2px;vertical-align:middle;margin-right:4px\">"
|
||||
. "<span style=\"color:#55ff55\">{$name_esc}</span>"
|
||||
. "</span>";
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisierung
|
||||
add_action( 'init', [ 'WBF_MC_Bridge', 'init' ], 20 );
|
||||
@@ -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 ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -707,7 +707,7 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($thread->signature)): ?>
|
||||
<div class="wbf-signature"><div class="wbf-signature__divider"></div><?php echo nl2br(esc_html($thread->signature)); ?></div>
|
||||
<div class="wbf-signature"><div class="wbf-signature__divider"></div><?php echo WBF_BBCode::render($thread->signature); ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="wbf-post__footer">
|
||||
<span class="wbf-post__date"><?php echo self::time_ago($thread->created_at); ?></span>
|
||||
@@ -844,7 +844,7 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($post->signature)): ?>
|
||||
<div class="wbf-signature"><div class="wbf-signature__divider"></div><?php echo nl2br(esc_html($post->signature)); ?></div>
|
||||
<div class="wbf-signature"><div class="wbf-signature__divider"></div><?php echo WBF_BBCode::render($post->signature); ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="wbf-post__footer">
|
||||
<span class="wbf-post__date"><?php echo self::time_ago($post->created_at); ?></span>
|
||||
@@ -928,12 +928,19 @@ class WBF_Shortcodes {
|
||||
// Aktiven Tab aus URL lesen (tab=1|2|3), Standard: 1 für eigenes, 2 für fremdes
|
||||
// Tab-ID: numerisch (1–4) oder String-Slug (z.B. 'mc' von der Forum-Bridge)
|
||||
$ptab_raw = $_GET['ptab'] ?? ($is_own ? 1 : 2);
|
||||
$shop_active = class_exists('WIS_DB');
|
||||
$shop_tab_id = 'shop';
|
||||
$allowed_tabs = [1,2,3,4];
|
||||
if ($is_own && $shop_active) $allowed_tabs[] = $shop_tab_id;
|
||||
$active_tab = ctype_digit( (string) $ptab_raw ) ? (int) $ptab_raw : sanitize_key( $ptab_raw );
|
||||
if ( is_int($active_tab) && ! in_array($active_tab, [1,2,3,4]) ) {
|
||||
if (is_int($active_tab) && !in_array($active_tab, [1,2,3,4])) {
|
||||
$active_tab = $is_own ? 1 : 2;
|
||||
}
|
||||
// Tab 1, 3, 4 und String-Tabs nur für eigenes Profil (außer Tab 2 = Aktivität)
|
||||
if ( ! $is_own && $active_tab !== 2 ) $active_tab = 2;
|
||||
if (!is_int($active_tab) && $active_tab !== $shop_tab_id && $active_tab !== 'mc') {
|
||||
$active_tab = $is_own ? 1 : 2;
|
||||
}
|
||||
// Tab 1, 3, 4, "shop" und String-Tabs nur für eigenes Profil (außer Tab 2 = Aktivität)
|
||||
if (!$is_own && $active_tab !== 2) $active_tab = 2;
|
||||
|
||||
ob_start(); ?>
|
||||
<div class="wbf-wrap">
|
||||
@@ -948,6 +955,21 @@ class WBF_Shortcodes {
|
||||
|
||||
<!-- ── SIDEBAR ─────────────────────────────────────────── -->
|
||||
<aside class="wbf-profile-sidebar">
|
||||
<!-- Banner -->
|
||||
<div class="wbf-profile-banner" id="wbfProfileBannerWrap">
|
||||
<?php if ( ! empty($profile->banner_url) ) : ?>
|
||||
<img src="<?php echo esc_url($profile->banner_url); ?>"
|
||||
alt="" id="wbfProfileBanner" class="wbf-profile-banner__img">
|
||||
<?php else : ?>
|
||||
<div class="wbf-profile-banner__placeholder"></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($is_own) : ?>
|
||||
<label class="wbf-banner-upload-btn" title="Banner ändern">
|
||||
<i class="fas fa-image"></i>
|
||||
<input type="file" id="wbfBannerFile" accept="image/*" style="display:none">
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wbf-profile-sidebar__avatar-wrap">
|
||||
<img src="<?php echo esc_url($profile->avatar_url); ?>"
|
||||
alt="<?php echo esc_attr($profile->display_name); ?>"
|
||||
@@ -994,13 +1016,13 @@ class WBF_Shortcodes {
|
||||
<?php if (!empty($profile->bio)): ?>
|
||||
<div class="wbf-profile-sidebar__section">
|
||||
<span class="wbf-profile-sidebar__section-label"><i class="fas fa-align-left"></i> Bio</span>
|
||||
<p><?php echo nl2br(esc_html($profile->bio)); ?></p>
|
||||
<div class="wbf-profile-sidebar__bio-text"><?php echo WBF_BBCode::render($profile->bio); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($profile->signature)): ?>
|
||||
<div class="wbf-profile-sidebar__section">
|
||||
<span class="wbf-profile-sidebar__section-label"><i class="fas fa-pen-nib"></i> Signatur</span>
|
||||
<p class="wbf-profile-sidebar__sig"><?php echo nl2br(esc_html($profile->signature)); ?></p>
|
||||
<div class="wbf-profile-sidebar__sig"><?php echo WBF_BBCode::render($profile->signature); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- Öffentliche Custom Fields — nach Kategorie gruppiert -->
|
||||
@@ -1113,12 +1135,131 @@ class WBF_Shortcodes {
|
||||
class="wbf-profile-tab<?php echo $active_tab===4?' active':''; ?>">
|
||||
<i class="fas fa-lock"></i> Sicherheit
|
||||
</a>
|
||||
<?php if ( class_exists('MC_Gallery_Forum_Bridge') ) : ?>
|
||||
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=mc"
|
||||
class="wbf-profile-tab<?php echo $active_tab==='mc'?' active':''; ?>">
|
||||
<i class="fas fa-cubes"></i> Minecraft
|
||||
<?php if ($shop_active): ?>
|
||||
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=shop"
|
||||
class="wbf-profile-tab<?php echo $active_tab==='shop'?' active':''; ?>">
|
||||
<i class="fas fa-shopping-cart"></i> Käufe
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// „Verbindungen" Tab — immer sichtbar (Discord eingebaut, MC optional)
|
||||
$wbf_has_connections = true;
|
||||
if ( $wbf_has_connections ) : ?>
|
||||
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=mc"
|
||||
class="wbf-profile-tab<?php echo $active_tab==='mc'?' active':''; ?>">
|
||||
<i class="fas fa-plug"></i> Verbindungen
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
TAB SHOP — Käufe (Shop-Plugin)
|
||||
══════════════════════════════════════════════════ -->
|
||||
<?php if ($is_own && $active_tab === 'shop' && $shop_active): ?>
|
||||
<div class="wbf-profile-card">
|
||||
<div class="wbf-profile-card__header">
|
||||
<i class="fas fa-shopping-cart"></i> Deine Käufe
|
||||
</div>
|
||||
<div class="wbf-profile-card__body">
|
||||
<?php
|
||||
$orders = [];
|
||||
if (class_exists('WIS_DB')) {
|
||||
global $wpdb;
|
||||
$username = $profile->username;
|
||||
$orders = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}wis_orders WHERE player_name = %s ORDER BY created_at DESC",
|
||||
$username
|
||||
));
|
||||
}
|
||||
?>
|
||||
<?php if (empty($orders)): ?>
|
||||
<p class="wbf-profile-empty">Du hast noch keine Käufe getätigt.</p>
|
||||
<?php else: ?>
|
||||
<div class="wbf-shop-orders-list">
|
||||
<table class="wbf-shop-orders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left">Datum</th>
|
||||
<th style="text-align:center">Anzahl</th>
|
||||
<th style="text-align:right">Gesamtpreis</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($orders as $i => $order):
|
||||
$is_cancelled = strtolower($order->status) === 'cancelled' || strtolower($order->status) === 'storniert';
|
||||
$row_class = $is_cancelled ? 'wbf-shop-order-cancelled' : '';
|
||||
?>
|
||||
<tr class="wbf-shop-order-row <?php echo $row_class; ?>" data-idx="<?php echo $i; ?>">
|
||||
<td><?php echo date_i18n('d.m.Y H:i', strtotime($order->created_at)); ?></td>
|
||||
<td style="text-align:center"><?php echo (int)$order->quantity; ?></td>
|
||||
<td style="text-align:right"><?php echo number_format($order->price * $order->quantity); ?> <?php echo esc_html(get_option('wis_currency_name', 'Coins')); ?></td>
|
||||
<td style="text-align:center">
|
||||
<button class="wbf-btn wbf-btn--sm wbf-shop-order-toggle" data-idx="<?php echo $i; ?>"><i class="fas fa-chevron-down"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="wbf-shop-order-details" id="wbf-shop-order-details-<?php echo $i; ?>" style="display:none">
|
||||
<td colspan="4">
|
||||
<div class="wbf-shop-order-details-inner">
|
||||
<?php /* Artikel-Zeile entfernt, da Item-Liste folgt */ ?>
|
||||
<strong>Einzelpreis:</strong> <?php echo number_format($order->price); ?> <?php echo esc_html(get_option('wis_currency_name', 'Coins')); ?><br>
|
||||
<strong>Status:</strong> <?php echo esc_html(ucfirst($order->status)); ?><br>
|
||||
<?php if (!empty($order->server)): ?><strong>Server:</strong> <?php echo esc_html($order->server); ?><br><?php endif; ?>
|
||||
<?php
|
||||
// Antwort als JSON-Items/Coupon anzeigen
|
||||
$response = $order->response;
|
||||
$decoded = null;
|
||||
if (!empty($response)) {
|
||||
$decoded = json_decode($response, true);
|
||||
}
|
||||
if (is_array($decoded) && isset($decoded['items'])) {
|
||||
echo '<strong>Gekaufte Items:</strong><ul style="margin:.3em 0 .7em 1.2em">';
|
||||
foreach ($decoded['items'] as $item) {
|
||||
$item_id = isset($item['id']) ? $item['id'] : '';
|
||||
$item_id = preg_replace('/^minecraft:/', '', $item_id);
|
||||
$amount = isset($item['amount']) ? (int)$item['amount'] : 1;
|
||||
echo '<li><span style="color:var(--c-primary)">' . esc_html($item_id) . '</span> <span style="color:var(--c-muted)">x' . $amount . '</span></li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
if (isset($decoded['coupon']['code'])) {
|
||||
$c = $decoded['coupon'];
|
||||
echo '<div style="margin:.2em 0 .5em 0"><strong>Coupon:</strong> <span style="color:var(--c-success)">' . esc_html($c['code']) . '</span>';
|
||||
if (isset($c['discount'])) echo ' <span style="color:var(--c-muted)">(' . intval($c['discount']) . '% Rabatt)</span>';
|
||||
echo '</div>';
|
||||
}
|
||||
} elseif (!empty($response)) {
|
||||
echo '<strong>Antwort:</strong> ' . esc_html($response) . '<br>';
|
||||
}
|
||||
?>
|
||||
<span style="font-size:.85em;color:var(--c-muted)">Bestell-ID: <?php echo (int)$order->id; ?></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.wbf-shop-order-toggle').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var idx = btn.getAttribute('data-idx');
|
||||
var details = document.getElementById('wbf-shop-order-details-' + idx);
|
||||
if (details.style.display === 'none') {
|
||||
details.style.display = '';
|
||||
btn.querySelector('i').classList.remove('fa-chevron-down');
|
||||
btn.querySelector('i').classList.add('fa-chevron-up');
|
||||
} else {
|
||||
details.style.display = 'none';
|
||||
btn.querySelector('i').classList.remove('fa-chevron-up');
|
||||
btn.querySelector('i').classList.add('fa-chevron-down');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -1139,10 +1280,12 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
<div class="wbf-form-row">
|
||||
<label>Bio</label>
|
||||
<?php self::render_editor_toolbar('wbfEditBio'); ?>
|
||||
<textarea id="wbfEditBio" rows="2"><?php echo esc_textarea($profile->bio); ?></textarea>
|
||||
</div>
|
||||
<div class="wbf-form-row">
|
||||
<label>Signatur <small>(max. 300 Zeichen)</small></label>
|
||||
<?php self::render_editor_toolbar('wbfEditSignature'); ?>
|
||||
<textarea id="wbfEditSignature" rows="2" maxlength="300" placeholder="Deine Signatur…"><?php echo esc_textarea($profile->signature ?? ''); ?></textarea>
|
||||
<div class="wbf-sig-counter"><span id="wbfSigCount"><?php echo mb_strlen($profile->signature??''); ?></span>/300</div>
|
||||
</div>
|
||||
@@ -1532,26 +1675,496 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
2FA — Zwei-Faktor-Authentifizierung (TOTP)
|
||||
══════════════════════════════════════════════════ -->
|
||||
<?php if ( class_exists('WBF_TOTP') ) :
|
||||
$wbf_2fa_active = WBF_TOTP::is_enabled_for($current->id);
|
||||
?>
|
||||
<div class="wbf-profile-card wbf-2fa-card" id="wbf2faCard">
|
||||
<div class="wbf-profile-card__header" style="background:rgba(234,179,8,.07);border-bottom-color:rgba(234,179,8,.2)">
|
||||
<i class="fas fa-shield-halved" style="color:#eab308"></i>
|
||||
Zwei-Faktor-Authentifizierung (2FA)
|
||||
<?php if ( $wbf_2fa_active ): ?>
|
||||
<span class="wbf-2fa-badge wbf-2fa-badge--on"><i class="fas fa-check-circle"></i> Aktiv</span>
|
||||
<?php else: ?>
|
||||
<span class="wbf-2fa-badge wbf-2fa-badge--off"><i class="fas fa-circle-xmark"></i> Inaktiv</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wbf-profile-card__body">
|
||||
|
||||
<!-- ── 2FA bereits aktiv: Deaktivierungs-Formular ── -->
|
||||
<?php if ( $wbf_2fa_active ): ?>
|
||||
<div id="wbf2faActive">
|
||||
<p style="color:var(--c-muted);font-size:.85rem;margin-bottom:1rem">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Dein Account ist mit einem Authenticator gesichert.
|
||||
Zum Deaktivieren Passwort und aktuellen Code eingeben.
|
||||
</p>
|
||||
<div class="wbf-profile-edit-grid">
|
||||
<div class="wbf-form-row">
|
||||
<label>Aktuelles Passwort</label>
|
||||
<input type="password" id="wbf2faDisablePw" placeholder="••••••" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="wbf-form-row">
|
||||
<label>Authenticator-Code</label>
|
||||
<input type="text" id="wbf2faDisableCode" placeholder="123456"
|
||||
maxlength="6" inputmode="numeric" autocomplete="one-time-code"
|
||||
style="letter-spacing:.2em;font-size:1.15rem;font-family:monospace">
|
||||
</div>
|
||||
</div>
|
||||
<div class="wbf-profile-card__footer">
|
||||
<button class="wbf-btn" id="wbf2faDisableBtn"
|
||||
style="background:rgba(220,38,38,.1);color:#dc2626;border-color:rgba(220,38,38,.3)">
|
||||
<i class="fas fa-shield-xmark"></i> 2FA deaktivieren
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbf2faDisableMsg"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<!-- ── 2FA noch nicht aktiv: Setup-Wizard ── -->
|
||||
<div id="wbf2faInactive">
|
||||
<p style="color:var(--c-muted);font-size:.85rem;margin-bottom:1rem">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Schütze deinen Account zusätzlich mit einer Authenticator-App
|
||||
(Google Authenticator, Aegis, Bitwarden, Authy, 2FAS…).
|
||||
</p>
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbf2faStartBtn">
|
||||
<i class="fas fa-shield-halved"></i> 2FA einrichten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 1: QR-Code scannen -->
|
||||
<div id="wbf2faStep1" style="display:none">
|
||||
<p style="font-size:.85rem;color:var(--c-muted);margin-bottom:.75rem">
|
||||
<strong>Schritt 1:</strong> Scanne diesen QR-Code mit deiner Authenticator-App.
|
||||
</p>
|
||||
<div id="wbf2faQr" style="display:inline-block;padding:10px;background:#fff;border-radius:8px;margin-bottom:.75rem"></div>
|
||||
<p style="font-size:.8rem;color:var(--c-muted);margin-bottom:.5rem">
|
||||
Kein QR-Scanner? Gib diesen Code manuell ein:
|
||||
</p>
|
||||
<code id="wbf2faSecret" style="font-size:.9rem;letter-spacing:.1em;background:var(--c-bg-2);padding:4px 10px;border-radius:4px;user-select:all"></code>
|
||||
<div class="wbf-profile-card__footer" style="margin-top:1rem">
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbf2faToStep2">
|
||||
Weiter <i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 2: Code bestätigen -->
|
||||
<div id="wbf2faStep2" style="display:none">
|
||||
<p style="font-size:.85rem;color:var(--c-muted);margin-bottom:.75rem">
|
||||
<strong>Schritt 2:</strong> Gib den 6-stelligen Code aus deiner App ein.
|
||||
</p>
|
||||
<div class="wbf-form-row" style="max-width:220px">
|
||||
<label>Bestätigungs-Code</label>
|
||||
<input type="text" id="wbf2faVerifyCode" placeholder="123456"
|
||||
maxlength="6" inputmode="numeric" autocomplete="one-time-code"
|
||||
style="letter-spacing:.25em;font-size:1.3rem;text-align:center;font-family:monospace">
|
||||
</div>
|
||||
<div class="wbf-profile-card__footer">
|
||||
<button class="wbf-btn" id="wbf2faBackBtn" style="opacity:.7">
|
||||
<i class="fas fa-arrow-left"></i> Zurück
|
||||
</button>
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbf2faVerifyBtn">
|
||||
<i class="fas fa-check"></i> Bestätigen & aktivieren
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbf2faVerifyMsg"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 3: Erfolg -->
|
||||
<div id="wbf2faStep3" style="display:none;text-align:center;padding:1.5rem 0">
|
||||
<div style="font-size:2.5rem;margin-bottom:.5rem">🔒</div>
|
||||
<strong style="font-size:1rem;color:var(--c-text)">2FA erfolgreich aktiviert!</strong>
|
||||
<p style="font-size:.85rem;color:var(--c-muted);margin:.5rem 0 0">
|
||||
Ab jetzt wird beim Login ein Code aus deiner App abgefragt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php endif; // 2fa_active ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; // class_exists WBF_TOTP ?>
|
||||
|
||||
<?php endif; /* end Tab 4 */ ?>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
TAB MC — Minecraft-Konto verknüpfen (Bridge)
|
||||
Wird nur gerendert wenn MC Gallery Forum Bridge aktiv ist.
|
||||
TAB MC — Verbindungen (Externe Dienste verknüpfen)
|
||||
Wird nur gerendert wenn mind. eine Integration aktiv ist.
|
||||
Neue Integrationen: einfach weiteres .wbf-connection-card-Block
|
||||
via apply_filters('wbf_profile_connections', ...) hinzufügen.
|
||||
══════════════════════════════════════════════════ -->
|
||||
<?php if ( $is_own && $active_tab === 'mc' && class_exists('MC_Gallery_Forum_Bridge') ) :
|
||||
echo apply_filters('wbf_profile_tab_content', '', 'minecraft', $profile);
|
||||
endif; /* end Tab MC */ ?>
|
||||
<?php if ( $is_own && $active_tab === 'mc' ) : ?>
|
||||
|
||||
<div class="wbf-profile-card">
|
||||
<div class="wbf-profile-card__header">
|
||||
<i class="fas fa-plug"></i> Verbundene Dienste
|
||||
</div>
|
||||
<div class="wbf-profile-card__body wbf-connections-body">
|
||||
|
||||
<?php if ( class_exists('MC_Gallery_Forum_Bridge') ) :
|
||||
$mc_content = apply_filters('wbf_profile_tab_content', '', 'minecraft', $profile);
|
||||
?>
|
||||
<div class="wbf-connection-card">
|
||||
<div class="wbf-connection-card__icon" style="background:rgba(101,163,13,.15);border-color:rgba(101,163,13,.3)">
|
||||
<i class="fas fa-cubes" style="color:#65a30d"></i>
|
||||
</div>
|
||||
<div class="wbf-connection-card__head">
|
||||
<span class="wbf-connection-card__title">Gallerie Verbindung</span>
|
||||
</div>
|
||||
<div class="wbf-connection-card__content">
|
||||
<?php echo $mc_content; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// ── StatusAPI Bridge: Account-Verknüpfung & Ingame-Benachrichtigungen ──
|
||||
$mc_enabled = class_exists( 'WBF_MC_Bridge' ) && WBF_MC_Bridge::is_enabled();
|
||||
$mc_uuid = $mc_enabled ? WBF_MC_Bridge::get_mc_uuid( $profile->id ) : '';
|
||||
$mc_name = $mc_enabled ? WBF_MC_Bridge::get_mc_name( $profile->id ) : '';
|
||||
$mc_linked = ! empty( $mc_uuid );
|
||||
?>
|
||||
<div class="wbf-connection-card">
|
||||
<div class="wbf-connection-card__icon" style="background:rgba(101,163,13,.15);border-color:rgba(101,163,13,.3)">
|
||||
<i class="fas fa-cubes" style="color:#65a30d"></i>
|
||||
</div>
|
||||
<div class="wbf-connection-card__head">
|
||||
<span class="wbf-connection-card__title">Minecraft InGame Verbindung</span>
|
||||
<?php if ( ! $mc_enabled ) : ?>
|
||||
<span class="wbf-connection-badge" style="color:#9ca3af;background:rgba(156,163,175,.1);border-color:rgba(156,163,175,.3)">
|
||||
<i class="fas fa-circle-xmark"></i> Nicht konfiguriert
|
||||
</span>
|
||||
<?php elseif ( $mc_linked ) : ?>
|
||||
<span class="wbf-connection-badge wbf-connection-badge--connected">
|
||||
<i class="fas fa-check-circle"></i> Verbunden
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span class="wbf-connection-badge wbf-connection-badge--disconnected">
|
||||
<i class="fas fa-circle-xmark"></i> Nicht verbunden
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wbf-connection-card__content">
|
||||
<?php if ( ! $mc_enabled ) : ?>
|
||||
<p class="wbf-connection-card__desc" style="color:var(--c-muted)">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Die Minecraft Bridge ist noch nicht eingerichtet.
|
||||
Ein Admin muss sie zuerst in den Forum-Einstellungen aktivieren.
|
||||
</p>
|
||||
<?php elseif ( $mc_linked ) : ?>
|
||||
<div class="wbf-mc-linked-info" style="display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem">
|
||||
<img src="https://mc-heads.net/avatar/<?php echo urlencode( $mc_name ?: $mc_uuid ); ?>/40"
|
||||
alt="" width="40" height="40"
|
||||
style="border-radius:4px;image-rendering:pixelated">
|
||||
<div>
|
||||
<strong style="color:var(--c-text)"><?php echo esc_html( $mc_name ?: $mc_uuid ); ?></strong><br>
|
||||
<small style="color:var(--c-muted);font-size:.75rem"><?php echo esc_html( $mc_uuid ); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:.82rem;color:var(--c-muted);margin:.25rem 0 .75rem">
|
||||
<i class="fas fa-bell" style="color:#65a30d"></i>
|
||||
Du erhältst Ingame-Benachrichtigungen bei Antworten, Erwähnungen und PNs.
|
||||
</p>
|
||||
<div id="wbf-mc-msg" style="font-size:.82rem;margin-bottom:.5rem"></div>
|
||||
<button type="button" class="wbf-btn wbf-btn--ghost" id="wbf-mc-unlink-btn"
|
||||
onclick="wbfMcUnlink()">
|
||||
<i class="fas fa-unlink"></i> Verknüpfung aufheben
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<p class="wbf-connection-card__desc">
|
||||
Verknüpfe deinen Minecraft-Account für Ingame-Benachrichtigungen
|
||||
bei neuen Antworten, Erwähnungen und Privatnachrichten.
|
||||
</p>
|
||||
<p style="font-size:.82rem;color:var(--c-muted);margin-bottom:.75rem">
|
||||
<i class="fas fa-terminal"></i>
|
||||
Schritt 1: Token generieren →
|
||||
Schritt 2: <code>/forumlink <token></code> ingame eingeben
|
||||
</p>
|
||||
<div id="wbf-mc-token-box" style="display:none;background:var(--c-surface);border:1px solid var(--c-border);border-radius:8px;padding:.85rem 1rem;margin-bottom:.75rem">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin-bottom:.4rem">
|
||||
<span style="font-size:.75rem;color:var(--c-muted);text-transform:uppercase;letter-spacing:.05em">Dein Token</span>
|
||||
<span id="wbf-mc-token-timer" style="font-size:.75rem;color:#f97316;font-weight:600"></span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.5rem">
|
||||
<code id="wbf-mc-token-value"
|
||||
style="font-size:1.4rem;letter-spacing:.25em;font-weight:700;color:var(--c-accent);flex:1"></code>
|
||||
<button type="button" class="wbf-btn wbf-btn--sm" id="wbf-mc-copy-btn"
|
||||
onclick="wbfMcCopyToken()" title="Befehl kopieren">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-top:.5rem;font-size:.8rem;color:var(--c-muted)">
|
||||
Ingame eingeben: <code id="wbf-mc-cmd-value">/forumlink </code>
|
||||
</div>
|
||||
<div style="margin-top:.5rem">
|
||||
<div style="height:4px;border-radius:2px;background:var(--c-border);overflow:hidden">
|
||||
<div id="wbf-mc-token-progress"
|
||||
style="height:100%;background:#65a30d;transition:width 1s linear;width:100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wbf-mc-msg" style="font-size:.82rem;margin-bottom:.5rem"></div>
|
||||
<button type="button" class="wbf-btn wbf-btn--primary" id="wbf-mc-gen-btn"
|
||||
onclick="wbfMcGenerateToken()">
|
||||
<i class="fas fa-key"></i> Token generieren
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var _pollInterval = null;
|
||||
var _timerInterval = null;
|
||||
var _expiry = 0;
|
||||
|
||||
window.wbfMcGenerateToken = function() {
|
||||
var btn = document.getElementById('wbf-mc-gen-btn');
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generiere...';
|
||||
msg.textContent = '';
|
||||
jQuery.post(WBF.ajax_url, { action: 'wbf_mc_generate_token', nonce: WBF.nonce }, function(r) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-rotate"></i> Neuen Token generieren';
|
||||
if (r.success) {
|
||||
var token = r.data.token;
|
||||
_expiry = Math.floor(Date.now() / 1000) + ((r.data.expires_in || 15) * 60);
|
||||
document.getElementById('wbf-mc-token-value').textContent = token;
|
||||
document.getElementById('wbf-mc-cmd-value').textContent = '/forumlink ' + token;
|
||||
document.getElementById('wbf-mc-token-box').style.display = 'block';
|
||||
wbfMcStartTimer((r.data.expires_in || 15) * 60);
|
||||
wbfMcStartPolling();
|
||||
} else {
|
||||
if (r.data && r.data.linked) {
|
||||
msg.innerHTML = '<span style="color:#16a34a"><i class="fas fa-check-circle"></i> ' + (r.data.message || 'Bereits verknüpft.') + '</span>';
|
||||
setTimeout(function(){ location.reload(); }, 1500);
|
||||
} else {
|
||||
msg.innerHTML = '<span style="color:#dc2626"><i class="fas fa-circle-xmark"></i> ' + (r.data && r.data.message ? r.data.message : 'Fehler') + '</span>';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function wbfMcStartTimer(seconds) {
|
||||
clearInterval(_timerInterval);
|
||||
var timerEl = document.getElementById('wbf-mc-token-timer');
|
||||
var progressEl = document.getElementById('wbf-mc-token-progress');
|
||||
var total = seconds;
|
||||
_timerInterval = setInterval(function() {
|
||||
var remaining = _expiry - Math.floor(Date.now() / 1000);
|
||||
if (remaining <= 0) {
|
||||
clearInterval(_timerInterval); clearInterval(_pollInterval);
|
||||
if (timerEl) timerEl.textContent = 'Abgelaufen';
|
||||
if (progressEl) progressEl.style.width = '0%';
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
if (msg) msg.innerHTML = '<span style="color:#f97316"><i class="fas fa-clock"></i> Token abgelaufen — bitte neuen generieren.</span>';
|
||||
return;
|
||||
}
|
||||
var m = Math.floor(remaining / 60), s = remaining % 60;
|
||||
if (timerEl) timerEl.textContent = m + ':' + (s < 10 ? '0' : '') + s;
|
||||
if (progressEl) {
|
||||
progressEl.style.width = Math.max(0, (remaining / total) * 100) + '%';
|
||||
progressEl.style.background = remaining < 60 ? '#dc2626' : remaining < 180 ? '#f97316' : '#65a30d';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function wbfMcStartPolling() {
|
||||
clearInterval(_pollInterval);
|
||||
_pollInterval = setInterval(function() {
|
||||
jQuery.post(WBF.ajax_url, { action: 'wbf_mc_link_status', nonce: WBF.nonce }, function(r) {
|
||||
if (r.success && r.data && r.data.linked) {
|
||||
clearInterval(_pollInterval); clearInterval(_timerInterval);
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
if (msg) msg.innerHTML = '<span style="color:#16a34a"><i class="fas fa-check-circle"></i> ✓ Verknüpft mit <strong>' + (r.data.mc_name || r.data.mc_uuid) + '</strong>! Seite lädt neu...</span>';
|
||||
setTimeout(function(){ location.reload(); }, 1800);
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
window.wbfMcCopyToken = function() {
|
||||
var cmd = document.getElementById('wbf-mc-cmd-value').textContent;
|
||||
var btn = document.getElementById('wbf-mc-copy-btn');
|
||||
var done = function() { btn.innerHTML = '<i class="fas fa-check"></i>'; btn.style.color = '#16a34a'; setTimeout(function(){ btn.innerHTML = '<i class="fas fa-copy"></i>'; btn.style.color = ''; }, 2000); };
|
||||
if (navigator.clipboard) { navigator.clipboard.writeText(cmd).then(done); }
|
||||
else { var ta = document.createElement('textarea'); ta.value = cmd; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); done(); }
|
||||
};
|
||||
|
||||
window.wbfMcUnlink = function() {
|
||||
if (!confirm('Minecraft-Verknüpfung wirklich aufheben?')) return;
|
||||
var btn = document.getElementById('wbf-mc-unlink-btn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Trenne...';
|
||||
jQuery.post(WBF.ajax_url, { action: 'wbf_mc_unlink', nonce: WBF.nonce }, function(r) {
|
||||
if (r.success) {
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
if (msg) msg.innerHTML = '<span style="color:#16a34a"><i class="fas fa-check"></i> ' + (r.data.message || 'Verknüpfung aufgehoben.') + '</span>';
|
||||
setTimeout(function(){ location.reload(); }, 1200);
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-unlink"></i> Verknüpfung aufheben';
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
if (msg) msg.innerHTML = '<span style="color:#dc2626">Fehler: ' + (r.data && r.data.message ? r.data.message : 'Unbekannt') + '</span>';
|
||||
}
|
||||
});
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
// ── Discord-Card (eingebaut, kein extra Plugin nötig) ──────────────
|
||||
$discord_meta = WBF_DB::get_user_meta( $profile->id );
|
||||
$discord_current = trim( $discord_meta['discord_username'] ?? '' );
|
||||
$discord_connected = $discord_current !== '';
|
||||
?>
|
||||
<?php
|
||||
$s = wbf_get_settings();
|
||||
$discord_bot_configured = ! empty( trim( $s['discord_bot_token'] ?? '' ) );
|
||||
?>
|
||||
<div class="wbf-connection-card wbf-connection-card--discord">
|
||||
<div class="wbf-connection-card__icon">
|
||||
<i class="fab fa-discord"></i>
|
||||
</div>
|
||||
<div class="wbf-connection-card__head">
|
||||
<span class="wbf-connection-card__title">Discord</span>
|
||||
<?php if ( $discord_connected ) : ?>
|
||||
<span class="wbf-connection-badge wbf-connection-badge--connected">
|
||||
<i class="fas fa-check-circle"></i> Verbunden
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span class="wbf-connection-badge wbf-connection-badge--disconnected">
|
||||
<i class="fas fa-circle-xmark"></i> Nicht verbunden
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="wbf-connection-card__content">
|
||||
|
||||
<?php if ( $discord_connected ) : ?>
|
||||
<!-- ── Bereits verbunden ── -->
|
||||
<div class="wbf-discord-connected-info">
|
||||
<span class="wbf-discord-linked-name">
|
||||
<i class="fab fa-discord" style="color:#5865f2"></i>
|
||||
<?php echo esc_html( $discord_current ); ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="wbf-connect-row" style="margin-top:.75rem">
|
||||
<button type="button" class="wbf-btn wbf-btn--primary" id="wbf-discord-relink">
|
||||
<i class="fas fa-rotate"></i> Neu verknüpfen
|
||||
</button>
|
||||
<button type="button" class="wbf-btn wbf-btn--ghost" id="wbf-discord-disconnect">
|
||||
<i class="fas fa-unlink"></i> Trennen
|
||||
</button>
|
||||
</div>
|
||||
<div id="wbf-discord-msg" style="margin-top:.5rem;font-size:.82rem"></div>
|
||||
|
||||
<!-- Formular (standardmäßig ausgeblendet, bei "Neu verknüpfen" sichtbar) -->
|
||||
<div id="wbf-discord-form" style="display:none;margin-top:1rem">
|
||||
<?php self::render_discord_form( $discord_bot_configured ); ?>
|
||||
</div>
|
||||
|
||||
<?php else : ?>
|
||||
<!-- ── Noch nicht verbunden ── -->
|
||||
<p class="wbf-connection-card__desc">
|
||||
Verknüpfe deinen Discord-Account mit deinem Profil.
|
||||
<?php if ( $discord_bot_configured ) : ?>
|
||||
Ein Bestätigungs-Code wird dir per Discord-DM zugeschickt.
|
||||
<?php else : ?>
|
||||
<em style="color:var(--c-muted)">(Bot noch nicht konfiguriert – wende dich an einen Admin.)</em>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<div id="wbf-discord-msg" style="margin-top:.3rem;font-size:.82rem"></div>
|
||||
<div id="wbf-discord-form">
|
||||
<?php self::render_discord_form( $discord_bot_configured ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Hook für weitere Verbindungen (z.B. Steam, Twitch, …)
|
||||
// Nutzung: add_filter('wbf_profile_connections', function($html, $profile) {
|
||||
// return $html . '<div class="wbf-connection-card">…</div>';
|
||||
// }, 10, 2);
|
||||
echo apply_filters('wbf_profile_connections', '', $profile);
|
||||
?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; /* end Tab MC */ ?>
|
||||
|
||||
</div><!-- /.wbf-profile-main -->
|
||||
</div><!-- /.wbf-profile-layout -->
|
||||
</div>
|
||||
</div>
|
||||
<?php self::render_auth_modal(); ?>
|
||||
<?php return ob_get_clean();
|
||||
}
|
||||
|
||||
|
||||
// ── TAG PAGE ─────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Discord Verifikations-Formular (3-Schritt) ────────────────────────────
|
||||
|
||||
private static function render_discord_form( $bot_configured ) { ?>
|
||||
<?php if ( ! $bot_configured ) : ?>
|
||||
<p style="color:var(--c-muted);font-size:.83rem;margin:0">
|
||||
<i class="fas fa-triangle-exclamation"></i>
|
||||
Discord-Bot noch nicht eingerichtet. Bitte Admin kontaktieren.
|
||||
</p>
|
||||
<?php else : ?>
|
||||
<!-- Schritt 1: Benutzername eingeben -->
|
||||
<div id="wbf-dc-step1">
|
||||
<label>DISCORD-BENUTZERNAME</label>
|
||||
<div class="wbf-connect-row">
|
||||
<input type="text"
|
||||
id="wbf-discord-input"
|
||||
placeholder="z. B. MvViper"
|
||||
maxlength="40"
|
||||
autocomplete="off">
|
||||
<button type="button" class="wbf-btn wbf-btn--primary" id="wbf-discord-send-code">
|
||||
<i class="fab fa-discord"></i> Code senden
|
||||
</button>
|
||||
</div>
|
||||
<p style="font-size:.78rem;color:var(--c-muted);margin:.45rem 0 0">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Du musst Mitglied unseres Discord-Servers sein und DMs erlauben.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 2: Code eingeben (zunächst ausgeblendet) -->
|
||||
<div id="wbf-dc-step2" style="display:none;margin-top:.9rem">
|
||||
<label>BESTÄTIGUNGS-CODE (aus Discord-DM)</label>
|
||||
<div class="wbf-connect-row">
|
||||
<input type="text"
|
||||
id="wbf-discord-code-input"
|
||||
placeholder="A1B2C3"
|
||||
maxlength="6"
|
||||
autocomplete="off"
|
||||
style="font-family:monospace;letter-spacing:.15em;text-transform:uppercase;max-width:140px">
|
||||
<button type="button" class="wbf-btn wbf-btn--primary" id="wbf-discord-verify">
|
||||
<i class="fas fa-check"></i> Bestätigen
|
||||
</button>
|
||||
<button type="button" class="wbf-btn wbf-btn--ghost" id="wbf-discord-code-back">
|
||||
<i class="fas fa-arrow-left"></i> Zurück
|
||||
</button>
|
||||
</div>
|
||||
<p style="font-size:.78rem;color:var(--c-muted);margin:.45rem 0 0">
|
||||
<i class="fas fa-clock"></i> Code ist 10 Minuten gültig.
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php }
|
||||
|
||||
private static function view_tag() {
|
||||
$slug = sanitize_title( $_GET['forum_tag'] ?? '' );
|
||||
$tag = WBF_DB::get_tag($slug);
|
||||
|
||||
179
includes/class-forum-totp.php
Normal file
179
includes/class-forum-totp.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
/**
|
||||
* WBF_TOTP — RFC 6238 Time-based One-Time Password (TOTP)
|
||||
*
|
||||
* Keine externe Bibliothek nötig — reines PHP 7.0+.
|
||||
* Kompatibel mit: Google Authenticator, Aegis, Authy, Bitwarden, 2FAS, etc.
|
||||
*
|
||||
* Secrets werden in forum_user_meta gespeichert (meta_key = 'totp_secret').
|
||||
* Kein Schema-Change an der Haupt-Usertabelle nötig.
|
||||
*/
|
||||
class WBF_TOTP {
|
||||
|
||||
const DIGITS = 6;
|
||||
const PERIOD = 30; // Sekunden pro Schritt
|
||||
const WINDOW = 1; // ±1 Step Toleranz (= ±30 s Uhrabweichung OK)
|
||||
const SECRET_LEN = 20; // Bytes → 32 Base32-Zeichen
|
||||
|
||||
// Meta-Keys
|
||||
const META_SECRET = 'totp_secret';
|
||||
const META_PENDING = 'totp_secret_pending';
|
||||
|
||||
// Session-Key für ausstehenden Login
|
||||
const SESSION_PENDING = 'wbf_2fa_pending';
|
||||
|
||||
// ── Secret ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Erzeugt einen neuen, kryptografisch sicheren Base32-Secret.
|
||||
* @return string z.B. "JBSWY3DPEBLW64TMMQ======"
|
||||
*/
|
||||
public static function generate_secret() {
|
||||
return self::base32_encode( random_bytes( self::SECRET_LEN ) );
|
||||
}
|
||||
|
||||
// ── Verifikation ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Prüft ob $code für $secret zum aktuellen Zeitfenster passt.
|
||||
*
|
||||
* @param string $secret Base32-Secret des Users
|
||||
* @param string $code 6-stelliger Code aus der Authenticator-App
|
||||
* @param int $window Anzahl Steps Toleranz (default = 1 = ±30 s)
|
||||
* @return bool
|
||||
*/
|
||||
public static function verify( $secret, $code, $window = self::WINDOW ) {
|
||||
// Leerzeichen tolerieren (z.B. "123 456")
|
||||
$code = preg_replace( '/\s+/', '', (string) $code );
|
||||
if ( strlen($code) !== self::DIGITS ) return false;
|
||||
if ( ! ctype_digit($code) ) return false;
|
||||
|
||||
$key = self::base32_decode( $secret );
|
||||
if ( empty($key) ) return false;
|
||||
|
||||
$ts = (int) floor( time() / self::PERIOD );
|
||||
|
||||
for ( $i = -$window; $i <= $window; $i++ ) {
|
||||
$expected = self::hotp( $key, $ts + $i );
|
||||
// Timing-safe Vergleich
|
||||
if ( hash_equals( $expected, $code ) ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── HOTP-Kern (RFC 4226) ─────────────────────────────────────────────────
|
||||
|
||||
private static function hotp( $key, $counter ) {
|
||||
// 64-bit Big-Endian Counter
|
||||
$msg = pack( 'N', 0 ) . pack( 'N', $counter );
|
||||
|
||||
$hash = hash_hmac( 'sha1', $msg, $key, true );
|
||||
$offset = ord( $hash[19] ) & 0x0f;
|
||||
|
||||
$code = (
|
||||
( ord($hash[$offset ]) & 0x7f ) << 24 |
|
||||
( ord($hash[$offset + 1]) & 0xff ) << 16 |
|
||||
( ord($hash[$offset + 2]) & 0xff ) << 8 |
|
||||
( ord($hash[$offset + 3]) & 0xff )
|
||||
) % ( 10 ** self::DIGITS );
|
||||
|
||||
return str_pad( (string) $code, self::DIGITS, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
// ── otpauth:// URI ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt die otpauth:// URI zurück — wird vom QR-Code-Generator verwendet.
|
||||
*
|
||||
* @param string $username Forum-Benutzername
|
||||
* @param string $secret Base32-Secret
|
||||
* @param string|null $issuer Anzeigename in der App (default: Blogname)
|
||||
* @return string
|
||||
*/
|
||||
public static function get_otpauth_uri( $username, $secret, $issuer = null ) {
|
||||
if ( ! $issuer ) {
|
||||
$issuer = html_entity_decode( get_bloginfo('name'), ENT_QUOTES ) ?: 'WP Business Forum';
|
||||
}
|
||||
$label = rawurlencode( $issuer . ':' . $username );
|
||||
return 'otpauth://totp/' . $label . '?'
|
||||
. 'secret=' . rawurlencode( $secret )
|
||||
. '&issuer=' . rawurlencode( $issuer )
|
||||
. '&algorithm=SHA1'
|
||||
. '&digits=' . self::DIGITS
|
||||
. '&period=' . self::PERIOD;
|
||||
}
|
||||
|
||||
// ── User-Helfer ───────────────────────────────────────────────────────────
|
||||
|
||||
/** Ist 2FA für diesen User aktiv? */
|
||||
public static function is_enabled_for( $user_id ) {
|
||||
$s = WBF_DB::get_user_meta_single( (int) $user_id, self::META_SECRET );
|
||||
return ! empty( $s );
|
||||
}
|
||||
|
||||
/**
|
||||
* 2FA für einen User deaktivieren (löscht Secret + ggf. pending Secret).
|
||||
* Kann von Admin und User selbst (nach Verifikation) aufgerufen werden.
|
||||
*/
|
||||
public static function disable_for( $user_id ) {
|
||||
global $wpdb;
|
||||
$uid = (int) $user_id;
|
||||
$wpdb->delete(
|
||||
"{$wpdb->prefix}forum_user_meta",
|
||||
[ 'user_id' => $uid, 'meta_key' => self::META_SECRET ],
|
||||
[ '%d', '%s' ]
|
||||
);
|
||||
$wpdb->delete(
|
||||
"{$wpdb->prefix}forum_user_meta",
|
||||
[ 'user_id' => $uid, 'meta_key' => self::META_PENDING ],
|
||||
[ '%d', '%s' ]
|
||||
);
|
||||
}
|
||||
|
||||
// ── Base32 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static $b32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
|
||||
public static function base32_encode( $input ) {
|
||||
$output = '';
|
||||
$buf = 0;
|
||||
$buf_bits = 0;
|
||||
for ( $i = 0, $len = strlen($input); $i < $len; $i++ ) {
|
||||
$buf = ( $buf << 8 ) | ord( $input[$i] );
|
||||
$buf_bits += 8;
|
||||
while ( $buf_bits >= 5 ) {
|
||||
$buf_bits -= 5;
|
||||
$output .= self::$b32[ ( $buf >> $buf_bits ) & 0x1f ];
|
||||
}
|
||||
}
|
||||
if ( $buf_bits > 0 ) {
|
||||
$output .= self::$b32[ ( $buf << ( 5 - $buf_bits ) ) & 0x1f ];
|
||||
}
|
||||
// Padding to multiple of 8
|
||||
while ( strlen($output) % 8 !== 0 ) $output .= '=';
|
||||
return $output;
|
||||
}
|
||||
|
||||
public static function base32_decode( $input ) {
|
||||
// Leerzeichen & Padding entfernen, Uppercase
|
||||
$input = strtoupper( preg_replace( '/\s+/', '', $input ) );
|
||||
$input = rtrim( $input, '=' );
|
||||
$map = array_flip( str_split( self::$b32 ) );
|
||||
|
||||
$output = '';
|
||||
$buf = 0;
|
||||
$bits = 0;
|
||||
for ( $i = 0, $len = strlen($input); $i < $len; $i++ ) {
|
||||
if ( ! isset( $map[ $input[$i] ] ) ) continue; // ungültiges Zeichen ignorieren
|
||||
$buf = ( $buf << 5 ) | $map[ $input[$i] ];
|
||||
$bits += 5;
|
||||
if ( $bits >= 8 ) {
|
||||
$bits -= 8;
|
||||
$output .= chr( ( $buf >> $bits ) & 0xff );
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
32
includes/forum-statusapi.php
Normal file
32
includes/forum-statusapi.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
// Ingame-Benachrichtigung via StatusAPI
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
|
||||
|
||||
function wbf_notify_ingame($player, $message) {
|
||||
// Einstellungen laden
|
||||
$settings = function_exists('wbf_get_settings') ? wbf_get_settings() : [];
|
||||
$enabled = !empty($settings['mc_bridge_enabled']);
|
||||
$api_url = trim($settings['mc_bridge_api_url'] ?? '');
|
||||
$api_secret = trim($settings['mc_bridge_api_secret'] ?? '');
|
||||
if (!$enabled || !$api_url || !$api_secret) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = rtrim($api_url, '/') . '/notify-pn';
|
||||
$data = [
|
||||
'player' => $player,
|
||||
'message' => $message
|
||||
];
|
||||
$args = [
|
||||
'body' => json_encode($data),
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-API-Key' => $api_secret,
|
||||
],
|
||||
'timeout' => 2,
|
||||
'data_format' => 'body',
|
||||
];
|
||||
wp_remote_post($url, $args);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* 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.3
|
||||
* Version: 1.0.5
|
||||
* Author: M_Viper
|
||||
* Author URI: https://m-viper.de
|
||||
* Text Domain: wp-business-forum
|
||||
@@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
define( 'WBF_PATH', plugin_dir_path( __FILE__ ) );
|
||||
define( 'WBF_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'WBF_VERSION', '1.0.2' );
|
||||
define( 'WBF_VERSION', '1.0.5' );
|
||||
|
||||
require_once WBF_PATH . 'includes/class-forum-db.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-roles.php';
|
||||
@@ -24,6 +24,9 @@ require_once WBF_PATH . 'includes/class-forum-auth.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-shortcodes.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-ajax.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-export.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-mc-bridge.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-totp.php';
|
||||
require_once WBF_PATH . 'includes/forum-statusapi.php';
|
||||
require_once WBF_PATH . 'admin/forum-admin.php';
|
||||
require_once WBF_PATH . 'admin/forum-settings.php';
|
||||
require_once WBF_PATH . 'admin/forum-setup.php';
|
||||
@@ -103,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('/');
|
||||
}
|
||||
@@ -119,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 );
|
||||
@@ -286,3 +292,301 @@ add_action( 'admin_init', function() {
|
||||
wp_safe_redirect( remove_query_arg(['wbf_refresh_update','_wpnonce']) );
|
||||
exit;
|
||||
} );
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// ── 2FA Inline-JavaScript ─────────────────────────────────────────────────────
|
||||
// Liefert das JS für: Login-2FA-Step, Profil-Setup-Wizard, Deaktivierung
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function wbf_get_2fa_inline_js() {
|
||||
return <<<'JS'
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
2FA — Login-Flow
|
||||
Wenn der Server 2fa_required:true zurückgibt, zeigt das
|
||||
Login-Formular eine Code-Eingabe anstatt die Fehlermeldung.
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
|
||||
// Original-Login-Handler überschreiben um 2FA abzufangen
|
||||
$(document).off('click', '.wbf-login-submit-btn');
|
||||
$(document).on('click', '.wbf-login-submit-btn', function () {
|
||||
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
var $box = $(this).closest('.wbf-auth-box');
|
||||
|
||||
// 2FA-Panel verstecken falls sichtbar
|
||||
$box.find('.wbf-2fa-login-step').remove();
|
||||
|
||||
$.post(WBF.ajax_url, {
|
||||
action: 'wbf_login',
|
||||
nonce: WBF.nonce,
|
||||
username: $box.find('.wbf-field-username').val(),
|
||||
password: $box.find('.wbf-field-password').val(),
|
||||
remember_me: $box.find('.wbf-field-remember').is(':checked') ? '1' : ''
|
||||
}, function (res) {
|
||||
if (res && res.success) {
|
||||
location.reload();
|
||||
} else if (res && res.data && res.data['2fa_required']) {
|
||||
// 2FA erforderlich — Code-Eingabe einblenden
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-sign-in-alt"></i> Einloggen');
|
||||
wbfShow2faLoginStep($box);
|
||||
} else {
|
||||
var msg = (res && res.data && res.data.message) ? res.data.message : 'Fehler';
|
||||
$box.find('.wbf-login-msg').text(msg).css('color', '#f05252').show();
|
||||
setTimeout(function () { $box.find('.wbf-login-msg').fadeOut(); }, 4000);
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-sign-in-alt"></i> Einloggen');
|
||||
}
|
||||
}, 'json').fail(function (xhr) {
|
||||
$box.find('.wbf-login-msg').text('Verbindungsfehler (' + xhr.status + ')').css('color', '#f05252').show();
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-sign-in-alt"></i> Einloggen');
|
||||
});
|
||||
});
|
||||
|
||||
function wbfShow2faLoginStep($box) {
|
||||
// Altes Modal entfernen falls vorhanden
|
||||
$('#wbf2faLoginModal').remove();
|
||||
|
||||
var modal =
|
||||
'<div id="wbf2faLoginModal" class="wbf-2fa-modal-overlay">' +
|
||||
'<div class="wbf-2fa-modal-box">' +
|
||||
'<div class="wbf-2fa-modal-header">' +
|
||||
'<span class="wbf-2fa-modal-icon">🛡️</span>' +
|
||||
'<div>' +
|
||||
'<strong class="wbf-2fa-modal-title">Zwei-Faktor-Authentifizierung</strong>' +
|
||||
'<p class="wbf-2fa-modal-sub">Gib den Code aus deiner Authenticator-App ein.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<input type="text" class="wbf-2fa-code-input" placeholder="1 2 3 4 5 6"' +
|
||||
' maxlength="7" inputmode="numeric" autocomplete="one-time-code">' +
|
||||
'<div class="wbf-2fa-modal-actions">' +
|
||||
'<button class="wbf-btn wbf-btn--primary wbf-2fa-submit-btn">' +
|
||||
'<i class="fas fa-check"></i> Bestätigen' +
|
||||
'</button>' +
|
||||
'<button class="wbf-btn wbf-2fa-cancel-btn">' +
|
||||
'<i class="fas fa-xmark"></i> Abbrechen' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<span class="wbf-2fa-msg"></span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
$('body').append(modal);
|
||||
// Kurze Verzögerung für CSS-Transition
|
||||
setTimeout(function () {
|
||||
$('#wbf2faLoginModal').addClass('wbf-2fa-modal--visible');
|
||||
$('#wbf2faLoginModal .wbf-2fa-code-input').focus();
|
||||
}, 20);
|
||||
}
|
||||
|
||||
// 2FA-Code absenden
|
||||
$(document).on('click', '.wbf-2fa-submit-btn', function () {
|
||||
var $step = $(this).closest('.wbf-2fa-modal-box');
|
||||
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
var code = $step.find('.wbf-2fa-code-input').val().replace(/\s+/g, '');
|
||||
|
||||
if (code.length !== 6) {
|
||||
$step.find('.wbf-2fa-msg').text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show();
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Code bestätigen');
|
||||
return;
|
||||
}
|
||||
|
||||
$.post(WBF.ajax_url, {
|
||||
action: 'wbf_2fa_verify_login',
|
||||
code: code
|
||||
}, function (res) {
|
||||
if (res && res.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
var msg = (res && res.data && res.data.message) ? res.data.message : 'Ungültiger Code.';
|
||||
$step.find('.wbf-2fa-msg').text(msg).css('color', '#f05252').show();
|
||||
$step.find('.wbf-2fa-code-input').val('').focus();
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Code bestätigen');
|
||||
}
|
||||
}, 'json').fail(function (xhr) {
|
||||
$step.find('.wbf-2fa-msg').text('Verbindungsfehler.').css('color', '#f05252').show();
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Code bestätigen');
|
||||
});
|
||||
});
|
||||
|
||||
// Enter-Taste im Code-Feld
|
||||
$(document).on('keydown', '.wbf-2fa-code-input', function (e) {
|
||||
if (e.key === 'Enter') $(this).closest('.wbf-2fa-login-step').find('.wbf-2fa-submit-btn').click();
|
||||
});
|
||||
|
||||
// Abbrechen: 2FA-Modal schließen
|
||||
$(document).on('click', '.wbf-2fa-cancel-btn', function () {
|
||||
var $modal = $('#wbf2faLoginModal');
|
||||
$modal.removeClass('wbf-2fa-modal--visible');
|
||||
setTimeout(function () { $modal.remove(); }, 250);
|
||||
});
|
||||
|
||||
// Klick außerhalb des Modals schließt es
|
||||
$(document).on('click', '#wbf2faLoginModal', function (e) {
|
||||
if ($(e.target).is('#wbf2faLoginModal')) {
|
||||
var $modal = $(this);
|
||||
$modal.removeClass('wbf-2fa-modal--visible');
|
||||
setTimeout(function () { $modal.remove(); }, 250);
|
||||
}
|
||||
});
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
2FA — Profil-Setup-Wizard
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
|
||||
// Schritt 1 starten: Secret + QR generieren
|
||||
$(document).on('click', '#wbf2faStartBtn', function () {
|
||||
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Lädt…');
|
||||
|
||||
$.post(WBF.ajax_url, {
|
||||
action: 'wbf_2fa_setup_begin',
|
||||
nonce: WBF.nonce
|
||||
}, function (res) {
|
||||
if (!res || !res.success) {
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-shield-halved"></i> 2FA einrichten');
|
||||
alert((res && res.data && res.data.message) ? res.data.message : 'Fehler');
|
||||
return;
|
||||
}
|
||||
var secret = res.data.secret;
|
||||
var uri = res.data.uri;
|
||||
|
||||
// QR-Code rendern (qrcodejs)
|
||||
$('#wbf2faQr').empty();
|
||||
if (typeof QRCode !== 'undefined') {
|
||||
// QR-Code in isolierten Wrapper einbetten (kein Flex-Kontext)
|
||||
var qrEl = document.getElementById('wbf2faQr');
|
||||
qrEl.innerHTML = '';
|
||||
|
||||
new QRCode(qrEl, {
|
||||
text: uri,
|
||||
width: 200,
|
||||
height: 200,
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
});
|
||||
|
||||
// Kein JS-Eingriff nötig — CSS übernimmt Größe + img-Verstecken
|
||||
} else {
|
||||
// Fallback: Link anzeigen
|
||||
$('#wbf2faQr').html(
|
||||
'<a href="' + uri + '" style="font-size:.75rem;word-break:break-all">otpauth Link</a>'
|
||||
);
|
||||
}
|
||||
// Secret für manuelle Eingabe formatiert anzeigen (Leerzeichen alle 4 Zeichen)
|
||||
var fmt = secret.replace(/=/g, '').replace(/(.{4})/g, '$1 ').trim();
|
||||
$('#wbf2faSecret').text(fmt);
|
||||
|
||||
// Panels tauschen
|
||||
$('#wbf2faInactive').hide();
|
||||
$('#wbf2faStep1').fadeIn(200);
|
||||
}, 'json').fail(function () {
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-shield-halved"></i> 2FA einrichten');
|
||||
alert('Verbindungsfehler. Bitte Seite neu laden.');
|
||||
});
|
||||
});
|
||||
|
||||
// Weiter zu Schritt 2
|
||||
$(document).on('click', '#wbf2faToStep2', function () {
|
||||
$('#wbf2faStep1').hide();
|
||||
$('#wbf2faStep2').fadeIn(200);
|
||||
$('#wbf2faVerifyCode').focus();
|
||||
});
|
||||
|
||||
// Zurück zu Schritt 1
|
||||
$(document).on('click', '#wbf2faBackBtn', function () {
|
||||
$('#wbf2faStep2').hide();
|
||||
$('#wbf2faStep1').fadeIn(200);
|
||||
});
|
||||
|
||||
// Schritt 2: Code bestätigen und 2FA aktivieren
|
||||
$(document).on('click', '#wbf2faVerifyBtn', function () {
|
||||
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
var $msg = $('#wbf2faVerifyMsg');
|
||||
var code = $('#wbf2faVerifyCode').val().replace(/\s+/g, '');
|
||||
|
||||
if (code.length !== 6) {
|
||||
$msg.text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show();
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen & aktivieren');
|
||||
return;
|
||||
}
|
||||
|
||||
$.post(WBF.ajax_url, {
|
||||
action: 'wbf_2fa_setup_verify',
|
||||
nonce: WBF.nonce,
|
||||
code: code
|
||||
}, function (res) {
|
||||
if (res && res.success) {
|
||||
$('#wbf2faStep2').hide();
|
||||
$('#wbf2faStep3').fadeIn(300);
|
||||
// Badge im Header aktualisieren
|
||||
$('#wbf2faCard .wbf-2fa-badge')
|
||||
.removeClass('wbf-2fa-badge--off')
|
||||
.addClass('wbf-2fa-badge--on')
|
||||
.html('<i class="fas fa-check-circle"></i> Aktiv');
|
||||
// Nach 2 Sek. Seite neu laden damit der Header-Status stimmt
|
||||
setTimeout(function () { location.reload(); }, 2500);
|
||||
} else {
|
||||
var msg = (res && res.data && res.data.message) ? res.data.message : 'Ungültiger Code.';
|
||||
$msg.text(msg).css('color', '#f05252').show();
|
||||
$('#wbf2faVerifyCode').val('').focus();
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen & aktivieren');
|
||||
}
|
||||
}, 'json').fail(function () {
|
||||
$msg.text('Verbindungsfehler.').css('color', '#f05252').show();
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen & aktivieren');
|
||||
});
|
||||
});
|
||||
|
||||
// Enter-Taste im Verifikationsfeld
|
||||
$(document).on('keydown', '#wbf2faVerifyCode', function (e) {
|
||||
if (e.key === 'Enter') $('#wbf2faVerifyBtn').click();
|
||||
});
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
2FA — Deaktivierung (Profil)
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
|
||||
$(document).on('click', '#wbf2faDisableBtn', function () {
|
||||
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
var $msg = $('#wbf2faDisableMsg');
|
||||
var pw = $('#wbf2faDisablePw').val();
|
||||
var code = $('#wbf2faDisableCode').val().replace(/\s+/g, '');
|
||||
|
||||
if (!pw) {
|
||||
$msg.text('Bitte Passwort eingeben.').css('color', '#f05252').show();
|
||||
$btn.prop('disabled', false)
|
||||
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
|
||||
return;
|
||||
}
|
||||
if (code.length !== 6) {
|
||||
$msg.text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show();
|
||||
$btn.prop('disabled', false)
|
||||
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
|
||||
return;
|
||||
}
|
||||
|
||||
$.post(WBF.ajax_url, {
|
||||
action: 'wbf_2fa_disable',
|
||||
nonce: WBF.nonce,
|
||||
password: pw,
|
||||
code: code
|
||||
}, function (res) {
|
||||
if (res && res.success) {
|
||||
$msg.text('✔ ' + (res.data.message || '2FA deaktiviert.')).css('color', '#56cf7e').show();
|
||||
setTimeout(function () { location.reload(); }, 1500);
|
||||
} else {
|
||||
var msg = (res && res.data && res.data.message) ? res.data.message : 'Fehler.';
|
||||
$msg.text(msg).css('color', '#f05252').show();
|
||||
$('#wbf2faDisableCode').val('').focus();
|
||||
$btn.prop('disabled', false)
|
||||
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
|
||||
}
|
||||
}, 'json').fail(function () {
|
||||
$msg.text('Verbindungsfehler.').css('color', '#f05252').show();
|
||||
$btn.prop('disabled', false)
|
||||
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
|
||||
});
|
||||
});
|
||||
|
||||
}(jQuery));
|
||||
JS;
|
||||
}
|
||||
Reference in New Issue
Block a user