Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3583a6231 | |||
| f4d0ec73c0 | |||
| 56f8c01b52 | |||
| 0efd52d893 | |||
| 605df075cd | |||
| 94f1ac46aa | |||
| 8c2955a2cf | |||
| 689fd0c77b | |||
| e2c4e31b4b | |||
| bd87c795f9 | |||
| 989db0786a | |||
| 4a593677dd | |||
| 43fcc6cb95 | |||
| 1c229ab72b | |||
| 781dbf9f41 | |||
| 3b7fd16301 | |||
| 3ea89e9841 | |||
| 65d2371239 | |||
| ead2f3a62a | |||
| dfdc74bcf9 | |||
| 44672a61aa | |||
| 2adba16d29 | |||
| 290279df1c |
580
README.md
580
README.md
@@ -1,290 +1,290 @@
|
|||||||
# WP Business Forum - Anwender README
|
# WP Business Forum - Anwender README
|
||||||
|
|
||||||
WP Business Forum bringt ein modernes, eigenständiges Community-Forum direkt in deine WordPress-Website.
|
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
|
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
|
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
|
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.
|
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
|
Wenn du eine professionelle Community mit klaren Rechten, direkter Nutzerkommunikation und
|
||||||
strukturierter Moderation aufbauen willst, ist WP Business Forum dafür ausgelegt.
|
strukturierter Moderation aufbauen willst, ist WP Business Forum dafür ausgelegt.
|
||||||
|
|
||||||
## Inhalt
|
## Inhalt
|
||||||
1. Über das Plugin
|
1. Über das Plugin
|
||||||
2. Funktionsübersicht
|
2. Funktionsübersicht
|
||||||
3. Voraussetzungen
|
3. Voraussetzungen
|
||||||
4. Installation
|
4. Installation
|
||||||
5. Ersteinrichtung (Setup-Wizard)
|
5. Ersteinrichtung (Setup-Wizard)
|
||||||
6. Forum-Seite einbinden
|
6. Forum-Seite einbinden
|
||||||
7. Bedienung im Frontend (Mitglieder)
|
7. Bedienung im Frontend (Mitglieder)
|
||||||
8. Moderation und Verwaltung
|
8. Moderation und Verwaltung
|
||||||
9. Einstellungen im Detail
|
9. Einstellungen im Detail
|
||||||
10. Export, Import und Deinstallation
|
10. Export, Import und Deinstallation
|
||||||
11. FAQ / Troubleshooting
|
11. FAQ / Troubleshooting
|
||||||
|
|
||||||
## 1) Über das Plugin
|
## 1) Über das Plugin
|
||||||
WP Business Forum ist ein eigenständiges Foren-System für WordPress mit:
|
WP Business Forum ist ein eigenständiges Foren-System für WordPress mit:
|
||||||
- eigenem Forum-Login (unabhängig vom WP-Login)
|
- eigenem Forum-Login (unabhängig vom WP-Login)
|
||||||
- Rollen- und Rechteverwaltung
|
- Rollen- und Rechteverwaltung
|
||||||
- Kategorien mit Hierarchie
|
- Kategorien mit Hierarchie
|
||||||
- Moderationswerkzeugen
|
- Moderationswerkzeugen
|
||||||
- Direktnachrichten, Benachrichtigungen, Meldesystem
|
- Direktnachrichten, Benachrichtigungen, Meldesystem
|
||||||
- Umfragen, Tags, Reaktionen, Lesezeichen
|
- Umfragen, Tags, Reaktionen, Lesezeichen
|
||||||
|
|
||||||
Das Forum wird per Shortcode in eine WordPress-Seite eingebunden.
|
Das Forum wird per Shortcode in eine WordPress-Seite eingebunden.
|
||||||
|
|
||||||
## 2) Funktionsübersicht
|
## 2) Funktionsübersicht
|
||||||
### Für Mitglieder
|
### Für Mitglieder
|
||||||
- Registrieren / Einloggen / Logout
|
- Registrieren / Einloggen / Logout
|
||||||
- Passwort vergessen und Reset per E-Mail
|
- Passwort vergessen und Reset per E-Mail
|
||||||
- Threads erstellen, antworten, bearbeiten
|
- Threads erstellen, antworten, bearbeiten
|
||||||
- Likes und Emoji-Reaktionen
|
- Likes und Emoji-Reaktionen
|
||||||
- Tags und Thread-Präfixe
|
- Tags und Thread-Präfixe
|
||||||
- Umfragen erstellen und abstimmen
|
- Umfragen erstellen und abstimmen
|
||||||
- Lesezeichen setzen
|
- Lesezeichen setzen
|
||||||
- Nutzer erwähnen mit @mention
|
- Nutzer erwähnen mit @mention
|
||||||
- Private Nachrichten (DM)
|
- Private Nachrichten (DM)
|
||||||
- Profil mit Avatar, Bio, Signatur und eigenen Profilfeldern
|
- Profil mit Avatar, Bio, Signatur und eigenen Profilfeldern
|
||||||
- Mitgliederliste und Suchfunktion
|
- Mitgliederliste und Suchfunktion
|
||||||
|
|
||||||
### Für Moderation / Admin
|
### Für Moderation / Admin
|
||||||
- Threads pinnen, schließen, archivieren, verschieben, löschen
|
- Threads pinnen, schließen, archivieren, verschieben, löschen
|
||||||
- Beiträge löschen
|
- Beiträge löschen
|
||||||
- Meldungen (Reports) bearbeiten
|
- Meldungen (Reports) bearbeiten
|
||||||
- Kategorien und Rollen verwalten
|
- Kategorien und Rollen verwalten
|
||||||
- Einladungssystem für Registrierung
|
- Einladungssystem für Registrierung
|
||||||
- Wartungsmodus
|
- Wartungsmodus
|
||||||
- Wortfilter
|
- Wortfilter
|
||||||
- Statistiken
|
- Statistiken
|
||||||
- Papierkorb / Wiederherstellung
|
- Papierkorb / Wiederherstellung
|
||||||
- Export / Import
|
- Export / Import
|
||||||
|
|
||||||
## 3) Voraussetzungen
|
## 3) Voraussetzungen
|
||||||
- Laufende WordPress-Installation
|
- Laufende WordPress-Installation
|
||||||
- Schreibrechte für WordPress-Uploads (für Avatar-/Bild-Uploads)
|
- Schreibrechte für WordPress-Uploads (für Avatar-/Bild-Uploads)
|
||||||
- Funktionierende E-Mail-Zustellung in WordPress (für Passwort-Reset und Benachrichtigungen)
|
- Funktionierende E-Mail-Zustellung in WordPress (für Passwort-Reset und Benachrichtigungen)
|
||||||
|
|
||||||
Hinweis: Das Plugin nutzt eigene Datenbanktabellen (Präfix `wp_forum_*` bzw. mit deinem Tabellenpräfix).
|
Hinweis: Das Plugin nutzt eigene Datenbanktabellen (Präfix `wp_forum_*` bzw. mit deinem Tabellenpräfix).
|
||||||
|
|
||||||
## 4) Installation
|
## 4) Installation
|
||||||
1. Plugin-Ordner `wp-business-forum` in `wp-content/plugins/` kopieren.
|
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.
|
3. Nach der Aktivierung startet einmalig der Setup-Wizard.
|
||||||
|
|
||||||
## 5) Ersteinrichtung (Setup-Wizard)
|
## 5) Ersteinrichtung (Setup-Wizard)
|
||||||
Nach Aktivierung führt der Wizard durch 3 Schritte:
|
Nach Aktivierung führt der Wizard durch 3 Schritte:
|
||||||
|
|
||||||
1. Superadmin-Konto erstellen oder bestehendes Forum-Konto hochstufen
|
1. Superadmin-Konto erstellen oder bestehendes Forum-Konto hochstufen
|
||||||
2. Optional automatisch eine Forum-Seite erzeugen
|
2. Optional automatisch eine Forum-Seite erzeugen
|
||||||
3. Abschluss
|
3. Abschluss
|
||||||
|
|
||||||
Wichtig:
|
Wichtig:
|
||||||
- Der Superadmin ist fest mit dem WordPress-Admin verknüpft.
|
- Der Superadmin ist fest mit dem WordPress-Admin verknüpft.
|
||||||
- Wenn noch kein Superadmin existiert, erscheint im Backend ein Hinweisbanner.
|
- Wenn noch kein Superadmin existiert, erscheint im Backend ein Hinweisbanner.
|
||||||
|
|
||||||
## 6) Forum-Seite einbinden
|
## 6) Forum-Seite einbinden
|
||||||
Das Forum wird mit folgendem Shortcode auf einer WordPress-Seite angezeigt:
|
Das Forum wird mit folgendem Shortcode auf einer WordPress-Seite angezeigt:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[business_forum]
|
[business_forum]
|
||||||
```
|
```
|
||||||
|
|
||||||
Empfohlen:
|
Empfohlen:
|
||||||
- Eine eigene Seite (z. B. "Forum") anlegen
|
- Eine eigene Seite (z. B. "Forum") anlegen
|
||||||
- Nur diesen Shortcode als Seiteninhalt verwenden
|
- Nur diesen Shortcode als Seiteninhalt verwenden
|
||||||
|
|
||||||
## 7) Bedienung im Frontend (Mitglieder)
|
## 7) Bedienung im Frontend (Mitglieder)
|
||||||
### 7.1 Registrierung und Login
|
### 7.1 Registrierung und Login
|
||||||
- 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.
|
- Optional müssen Nutzer die Forum-Regeln akzeptieren.
|
||||||
- Spam-Schutz bei Registrierung:
|
- Spam-Schutz bei Registrierung:
|
||||||
- Honeypot-Feld
|
- Honeypot-Feld
|
||||||
- Mindestzeit bis Formular-Absenden
|
- Mindestzeit bis Formular-Absenden
|
||||||
- Login unterstützt "Angemeldet bleiben" (Remember-Me Cookie).
|
- Login unterstützt "Angemeldet bleiben" (Remember-Me Cookie).
|
||||||
|
|
||||||
### 7.2 Kategorien und Threads
|
### 7.2 Kategorien und Threads
|
||||||
- Kategorien können verschachtelt sein (Hauptkategorie + Unterkategorien).
|
- Kategorien können verschachtelt sein (Hauptkategorie + Unterkategorien).
|
||||||
- Sichtbarkeit kann rollenbasiert sein.
|
- Sichtbarkeit kann rollenbasiert sein.
|
||||||
- Threads können folgende Zustände haben:
|
- Threads können folgende Zustände haben:
|
||||||
- offen
|
- offen
|
||||||
- geschlossen
|
- geschlossen
|
||||||
- archiviert
|
- archiviert
|
||||||
- gepinnt
|
- gepinnt
|
||||||
|
|
||||||
### 7.3 Thread erstellen
|
### 7.3 Thread erstellen
|
||||||
- Mindestlänge Titel: 5 Zeichen
|
- Mindestlänge Titel: 5 Zeichen
|
||||||
- Mindestlänge Inhalt: 10 Zeichen (bei normalem Thread)
|
- Mindestlänge Inhalt: 10 Zeichen (bei normalem Thread)
|
||||||
- Tags können vergeben werden
|
- Tags können vergeben werden
|
||||||
- Optional kann ein Thread-Präfix gesetzt werden
|
- Optional kann ein Thread-Präfix gesetzt werden
|
||||||
- Optional kann direkt eine Umfrage erstellt werden
|
- Optional kann direkt eine Umfrage erstellt werden
|
||||||
|
|
||||||
### 7.4 Antworten und Bearbeiten
|
### 7.4 Antworten und Bearbeiten
|
||||||
- Antworten mit BBCode-Unterstützung
|
- Antworten mit BBCode-Unterstützung
|
||||||
- Flood-Control: konfigurierbare Wartezeit zwischen Posts
|
- Flood-Control: konfigurierbare Wartezeit zwischen Posts
|
||||||
- Eigene Posts nur innerhalb des eingestellten Bearbeitungsfensters (z. B. 30 Minuten)
|
- Eigene Posts nur innerhalb des eingestellten Bearbeitungsfensters (z. B. 30 Minuten)
|
||||||
- Moderation kann unabhängig davon eingreifen
|
- Moderation kann unabhängig davon eingreifen
|
||||||
|
|
||||||
### 7.5 Umfragen
|
### 7.5 Umfragen
|
||||||
- Umfrage direkt beim Thread-Erstellen oder nachträglich im Thread
|
- Umfrage direkt beim Thread-Erstellen oder nachträglich im Thread
|
||||||
- 2 bis 10 Antwortoptionen
|
- 2 bis 10 Antwortoptionen
|
||||||
- Optional Mehrfachauswahl
|
- Optional Mehrfachauswahl
|
||||||
- Optional Enddatum
|
- Optional Enddatum
|
||||||
- Nach Abstimmung werden Ergebnisse direkt angezeigt
|
- Nach Abstimmung werden Ergebnisse direkt angezeigt
|
||||||
|
|
||||||
### 7.6 Reaktionen, Likes, Lesezeichen
|
### 7.6 Reaktionen, Likes, Lesezeichen
|
||||||
- Likes auf Thread/Beitrag
|
- Likes auf Thread/Beitrag
|
||||||
- Emoji-Reaktionen (adminseitig konfigurierbar)
|
- Emoji-Reaktionen (adminseitig konfigurierbar)
|
||||||
- Lesezeichen für Threads (im Profil einsehbar)
|
- Lesezeichen für Threads (im Profil einsehbar)
|
||||||
|
|
||||||
### 7.7 Private Nachrichten (DM)
|
### 7.7 Private Nachrichten (DM)
|
||||||
- 1:1 Nachrichten zwischen Mitgliedern
|
- 1:1 Nachrichten zwischen Mitgliedern
|
||||||
- Inbox-Ansicht und Konversation
|
- Inbox-Ansicht und Konversation
|
||||||
- Ungelesene Nachrichten werden gezählt
|
- Ungelesene Nachrichten werden gezählt
|
||||||
- Optional E-Mail-Hinweis bei neuer Nachricht
|
- Optional E-Mail-Hinweis bei neuer Nachricht
|
||||||
|
|
||||||
### 7.8 Benachrichtigungen
|
### 7.8 Benachrichtigungen
|
||||||
Benachrichtigungen bei:
|
Benachrichtigungen bei:
|
||||||
- Antworten auf abonnierte / relevante Threads
|
- Antworten auf abonnierte / relevante Threads
|
||||||
- @Erwähnungen
|
- @Erwähnungen
|
||||||
- neuen privaten Nachrichten
|
- neuen privaten Nachrichten
|
||||||
|
|
||||||
### 7.9 Profil
|
### 7.9 Profil
|
||||||
Mitglieder können:
|
Mitglieder können:
|
||||||
- Anzeigenamen, Bio und Signatur pflegen
|
- Anzeigenamen, Bio und Signatur pflegen
|
||||||
- Avatar hochladen
|
- Avatar hochladen
|
||||||
- Passwort ändern
|
- Passwort ändern
|
||||||
- eigene Profil-Sichtbarkeit umschalten
|
- eigene Profil-Sichtbarkeit umschalten
|
||||||
- benutzerdefinierte Profilfelder ausfüllen (falls aktiviert)
|
- benutzerdefinierte Profilfelder ausfüllen (falls aktiviert)
|
||||||
|
|
||||||
Upload-Limits:
|
Upload-Limits:
|
||||||
- Avatar: max. 2 MB (JPG/PNG/GIF/WebP)
|
- Avatar: max. 2 MB (JPG/PNG/GIF/WebP)
|
||||||
- Bild im Beitrag: max. 5 MB (JPG/PNG/GIF/WebP)
|
- Bild im Beitrag: max. 5 MB (JPG/PNG/GIF/WebP)
|
||||||
|
|
||||||
### 7.10 Passwort vergessen
|
### 7.10 Passwort vergessen
|
||||||
- Über "Passwort vergessen" kann ein Reset-Link per E-Mail angefordert werden.
|
- Über "Passwort vergessen" kann ein Reset-Link per E-Mail angefordert werden.
|
||||||
- Das Zurücksetzen erfolgt über einen zeitlich gültigen Token.
|
- Das Zurücksetzen erfolgt über einen zeitlich gültigen Token.
|
||||||
|
|
||||||
## 8) Moderation und Verwaltung
|
## 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 Unterseiten:
|
||||||
|
|
||||||
- Übersicht: Dashboard mit Kennzahlen und Aktivitäten
|
- Übersicht: Dashboard mit Kennzahlen und Aktivitäten
|
||||||
- Kategorien: Struktur und Sichtbarkeit verwalten
|
- Kategorien: Struktur und Sichtbarkeit verwalten
|
||||||
- Rollen: Rollen/Permissions anpassen
|
- Rollen: Rollen/Permissions anpassen
|
||||||
- Level: Beitragsbasierte Rangstufen
|
- Level: Beitragsbasierte Rangstufen
|
||||||
- Mitglieder: Nutzer verwalten
|
- Mitglieder: Nutzer verwalten
|
||||||
- Meldungen: gemeldete Inhalte bearbeiten
|
- Meldungen: gemeldete Inhalte bearbeiten
|
||||||
- Profilfelder: eigene Felder definieren
|
- Profilfelder: eigene Felder definieren
|
||||||
- Einstellungen: Texte, Sicherheit, Registrierung, Regeln, Wartung
|
- Einstellungen: Texte, Sicherheit, Registrierung, Regeln, Wartung
|
||||||
- Reaktionen: erlaubte Emoji-Reaktionen
|
- Reaktionen: erlaubte Emoji-Reaktionen
|
||||||
- Einladungen: Invite-Codes erstellen und verwalten
|
- Einladungen: Invite-Codes erstellen und verwalten
|
||||||
- Statistiken: Forum-Auswertung
|
- Statistiken: Forum-Auswertung
|
||||||
- Papierkorb: gelöschte Inhalte wiederherstellen
|
- Papierkorb: gelöschte Inhalte wiederherstellen
|
||||||
- Thread-Präfixe: Label für Threads verwalten
|
- Thread-Präfixe: Label für Threads verwalten
|
||||||
- Wortfilter: unerwünschte Begriffe ersetzen/filtern
|
- Wortfilter: unerwünschte Begriffe ersetzen/filtern
|
||||||
- Export / Import: Backup und Wiederherstellung
|
- Export / Import: Backup und Wiederherstellung
|
||||||
- Deinstallieren: komplette Löschung des Plugins inkl. Daten
|
- Deinstallieren: komplette Löschung des Plugins inkl. Daten
|
||||||
|
|
||||||
## 9) Einstellungen im Detail
|
## 9) Einstellungen im Detail
|
||||||
Unter Business Forum > Einstellungen:
|
Unter Business Forum > Einstellungen:
|
||||||
|
|
||||||
### 9.1 Texte und UI
|
### 9.1 Texte und UI
|
||||||
- Hero-Titel/Untertitel
|
- Hero-Titel/Untertitel
|
||||||
- Topbar-Brand
|
- Topbar-Brand
|
||||||
- Label für Statistik
|
- Label für Statistik
|
||||||
- Abschnittstitel
|
- Abschnittstitel
|
||||||
- Buttontexte
|
- Buttontexte
|
||||||
- Sidebar-Titel
|
- Sidebar-Titel
|
||||||
|
|
||||||
### 9.2 Sicherheit
|
### 9.2 Sicherheit
|
||||||
- Auto-Logout nach Inaktivität (0 = deaktiviert)
|
- Auto-Logout nach Inaktivität (0 = deaktiviert)
|
||||||
- Post-Bearbeitungslimit
|
- Post-Bearbeitungslimit
|
||||||
- Spam-Mindestzeit bei Registrierung
|
- Spam-Mindestzeit bei Registrierung
|
||||||
- Flood-Control Intervall
|
- Flood-Control Intervall
|
||||||
- Profil-Sichtbarkeit (Standard)
|
- Profil-Sichtbarkeit (Standard)
|
||||||
|
|
||||||
### 9.3 Registrierung
|
### 9.3 Registrierung
|
||||||
- Modus:
|
- Modus:
|
||||||
- offen
|
- offen
|
||||||
- nur Einladung
|
- nur Einladung
|
||||||
- deaktiviert
|
- deaktiviert
|
||||||
- Freitext-Hinweis für Einladungsmode
|
- Freitext-Hinweis für Einladungsmode
|
||||||
|
|
||||||
### 9.4 Wartungsmodus
|
### 9.4 Wartungsmodus
|
||||||
- Forum für normale Nutzer sperren
|
- Forum für normale Nutzer sperren
|
||||||
- Moderation/Admin behalten Zugriff
|
- Moderation/Admin behalten Zugriff
|
||||||
- Eigener Wartungs-Titel und Hinweistext
|
- Eigener Wartungs-Titel und Hinweistext
|
||||||
|
|
||||||
### 9.5 Forum-Regeln / Nutzungsbedingungen
|
### 9.5 Forum-Regeln / Nutzungsbedingungen
|
||||||
- Regelseite aktivieren/deaktivieren
|
- Regelseite aktivieren/deaktivieren
|
||||||
- Akzeptierung bei Registrierung optional verpflichtend
|
- Akzeptierung bei Registrierung optional verpflichtend
|
||||||
- Titel und Inhalt frei editierbar
|
- Titel und Inhalt frei editierbar
|
||||||
|
|
||||||
## 10) Export, Import und Deinstallation
|
## 10) Export, Import und Deinstallation
|
||||||
### 10.1 Export / Import
|
### 10.1 Export / Import
|
||||||
Exportierbare Bereiche (je nach Auswahl):
|
Exportierbare Bereiche (je nach Auswahl):
|
||||||
- Einstellungen
|
- Einstellungen
|
||||||
- Rollen und Level
|
- Rollen und Level
|
||||||
- Kategorien
|
- Kategorien
|
||||||
- Nutzer und User-Meta
|
- Nutzer und User-Meta
|
||||||
- Threads und Posts
|
- Threads und Posts
|
||||||
- Interaktionen (Likes/Reaktionen/Benachrichtigungen)
|
- Interaktionen (Likes/Reaktionen/Benachrichtigungen)
|
||||||
- Nachrichten
|
- Nachrichten
|
||||||
- Meldungen
|
- Meldungen
|
||||||
- Einladungen
|
- Einladungen
|
||||||
|
|
||||||
Empfehlung:
|
Empfehlung:
|
||||||
- Vor großen Änderungen immer einen Voll-Export speichern.
|
- Vor großen Änderungen immer einen Voll-Export speichern.
|
||||||
|
|
||||||
### 10.2 Deinstallation (wichtig)
|
### 10.2 Deinstallation (wichtig)
|
||||||
Beim Löschen des Plugins werden komplett entfernt:
|
Beim Löschen des Plugins werden komplett entfernt:
|
||||||
- alle Forum-Datenbanktabellen
|
- alle Forum-Datenbanktabellen
|
||||||
- relevante Plugin-Optionen
|
- relevante Plugin-Optionen
|
||||||
- Transients
|
- Transients
|
||||||
- geplanter Cron-Job
|
- geplanter Cron-Job
|
||||||
- automatisch erstellte Forum-Seite
|
- automatisch erstellte Forum-Seite
|
||||||
- zugehörige Upload-Unterverzeichnisse
|
- zugehörige Upload-Unterverzeichnisse
|
||||||
|
|
||||||
Das ist eine echte Datenlöschung. Vorher immer Backup erstellen.
|
Das ist eine echte Datenlöschung. Vorher immer Backup erstellen.
|
||||||
|
|
||||||
## 11) FAQ / Troubleshooting
|
## 11) FAQ / Troubleshooting
|
||||||
### Login funktioniert nicht
|
### Login funktioniert nicht
|
||||||
- Prüfen, ob das Konto gesperrt ist
|
- Prüfen, ob das Konto gesperrt ist
|
||||||
- Bei zeitlicher Sperre Ablaufzeit abwarten
|
- Bei zeitlicher Sperre Ablaufzeit abwarten
|
||||||
- Bei Registrierung "Nur Einladung" gültigen Invite-Code nutzen
|
- Bei Registrierung "Nur Einladung" gültigen Invite-Code nutzen
|
||||||
|
|
||||||
### Registrierung nicht sichtbar
|
### Registrierung nicht sichtbar
|
||||||
- In Einstellungen den Registrierungsmodus prüfen
|
- In Einstellungen den Registrierungsmodus prüfen
|
||||||
- Bei deaktiviertem Modus ist keine Selbstregistrierung möglich
|
- Bei deaktiviertem Modus ist keine Selbstregistrierung möglich
|
||||||
|
|
||||||
### Keine E-Mails kommen an
|
### Keine E-Mails kommen an
|
||||||
- WordPress-Mailversand prüfen (SMTP Plugin empfohlen)
|
- WordPress-Mailversand prüfen (SMTP Plugin empfohlen)
|
||||||
- Admin-E-Mail in WordPress kontrollieren
|
- Admin-E-Mail in WordPress kontrollieren
|
||||||
|
|
||||||
### Upload von Bildern/Avatar scheitert
|
### Upload von Bildern/Avatar scheitert
|
||||||
- Dateityp prüfen (nur JPG/PNG/GIF/WebP)
|
- Dateityp prüfen (nur JPG/PNG/GIF/WebP)
|
||||||
- Dateigröße prüfen (Avatar 2 MB, Beitrag 5 MB)
|
- Dateigröße prüfen (Avatar 2 MB, Beitrag 5 MB)
|
||||||
- Schreibrechte in Uploads prüfen
|
- Schreibrechte in Uploads prüfen
|
||||||
|
|
||||||
### Benutzer werden automatisch ausgeloggt
|
### Benutzer werden automatisch ausgeloggt
|
||||||
- Auto-Logout in den Forum-Einstellungen prüfen
|
- Auto-Logout in den Forum-Einstellungen prüfen
|
||||||
|
|
||||||
### Forum ist plötzlich "offline"
|
### Forum ist plötzlich "offline"
|
||||||
- Wartungsmodus in den Einstellungen deaktivieren
|
- Wartungsmodus in den Einstellungen deaktivieren
|
||||||
|
|
||||||
### Suche liefert keine Ergebnisse
|
### Suche liefert keine Ergebnisse
|
||||||
- Suchbegriff muss mindestens 2 Zeichen haben
|
- Suchbegriff muss mindestens 2 Zeichen haben
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Kurz-Checkliste für den Live-Betrieb
|
## Kurz-Checkliste für den Live-Betrieb
|
||||||
1. Setup-Wizard abschließen
|
1. Setup-Wizard abschließen
|
||||||
2. Forum-Seite mit `[business_forum]` bereitstellen
|
2. Forum-Seite mit `[business_forum]` bereitstellen
|
||||||
3. Rollen und Kategorien final konfigurieren
|
3. Rollen und Kategorien final konfigurieren
|
||||||
4. Registrierungsmodus festlegen
|
4. Registrierungsmodus festlegen
|
||||||
5. Regeln/Nutzungsbedingungen hinterlegen
|
5. Regeln/Nutzungsbedingungen hinterlegen
|
||||||
6. E-Mail-Versand testen
|
6. E-Mail-Versand testen
|
||||||
7. Backup-Export erstellen
|
7. Backup-Export erstellen
|
||||||
|
|
||||||
Viel Erfolg mit deinem Forum!
|
Viel Erfolg mit deinem Forum!
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
89
admin/forum-settings-mc-section.php
Normal file
89
admin/forum-settings-mc-section.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* MC Bridge — Einstellungs-Sektion für den Forum-Admin
|
||||||
|
*
|
||||||
|
* Diesen Block in forum-settings.php im Admin-Formular einfügen,
|
||||||
|
* z.B. nach der Discord-Sektion.
|
||||||
|
*/
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- ═══ Minecraft Bridge ═══ -->
|
||||||
|
<div class="wbf-settings-section">
|
||||||
|
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||||
|
<span style="font-size:1.3em">⛏️</span> Minecraft Bridge
|
||||||
|
</h3>
|
||||||
|
<p class="description" style="margin-bottom:16px;color:#9ca3af">
|
||||||
|
Verbindet das Forum mit deinem Minecraft-Server (BungeeCord StatusAPI Plugin).
|
||||||
|
Spieler können ihren Forum-Account verknüpfen und erhalten Ingame-Benachrichtigungen
|
||||||
|
bei neuen Antworten, Erwähnungen und Privatnachrichten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th>Aktiviert</th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wbf_settings[mc_bridge_enabled]" value="1"
|
||||||
|
<?php checked( ! empty( $s['mc_bridge_enabled'] ) ); ?>>
|
||||||
|
MC Bridge aktivieren
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>StatusAPI URL</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wbf_settings[mc_bridge_api_url]"
|
||||||
|
value="<?php echo esc_attr( $s['mc_bridge_api_url'] ?? '' ); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
placeholder="http://192.168.1.100:9191">
|
||||||
|
<p class="description">
|
||||||
|
Die URL deines BungeeCord StatusAPI Servers (IP + Port).
|
||||||
|
Beispiel: <code>http://dein-server:9191</code>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>API Secret</th>
|
||||||
|
<td>
|
||||||
|
<input type="password" name="wbf_settings[mc_bridge_api_secret]"
|
||||||
|
value="<?php echo esc_attr( $s['mc_bridge_api_secret'] ?? '' ); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
autocomplete="new-password">
|
||||||
|
<p class="description">
|
||||||
|
Gemeinsames Passwort für die API-Kommunikation.
|
||||||
|
Muss identisch sein mit <code>forum.api_secret</code> in der
|
||||||
|
<code>verify.properties</code> des StatusAPI Plugins.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Verbindungstest</th>
|
||||||
|
<td>
|
||||||
|
<button type="button" id="wbf-mc-test-btn" class="button"
|
||||||
|
onclick="wbfTestMcConnection()">
|
||||||
|
🔌 Verbindung testen
|
||||||
|
</button>
|
||||||
|
<span id="wbf-mc-test-result" style="margin-left:10px"></span>
|
||||||
|
<script>
|
||||||
|
function wbfTestMcConnection() {
|
||||||
|
var btn = document.getElementById('wbf-mc-test-btn');
|
||||||
|
var result = document.getElementById('wbf-mc-test-result');
|
||||||
|
var url = document.querySelector('input[name="wbf_settings[mc_bridge_api_url]"]').value;
|
||||||
|
if (!url) { result.textContent = '❌ Bitte erst eine URL eingeben.'; return; }
|
||||||
|
btn.disabled = true;
|
||||||
|
result.textContent = '⏳ Teste...';
|
||||||
|
fetch(url.replace(/\/$/, '') + '/forum/status')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (d.success) result.innerHTML = '✅ <strong>Verbunden!</strong> StatusAPI v' + (d.version || '?');
|
||||||
|
else result.textContent = '⚠️ Erreichbar aber Fehler: ' + JSON.stringify(d);
|
||||||
|
})
|
||||||
|
.catch(function(e) { result.textContent = '❌ Nicht erreichbar: ' + e.message; })
|
||||||
|
.finally(function() { btn.disabled = false; });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -52,17 +52,63 @@ if ( ! function_exists('wbf_get_settings') ) {
|
|||||||
'rules_accept_required' => '1',
|
'rules_accept_required' => '1',
|
||||||
'rules_title' => 'Forum-Regeln & Nutzungsbedingungen',
|
'rules_title' => 'Forum-Regeln & Nutzungsbedingungen',
|
||||||
'rules_content' => "**1. Respektvoller Umgang**\nBehandle alle Mitglieder freundlich und respektvoll. Beleidigungen, Mobbing und Diskriminierung sind nicht toleriert.\n\n**2. Keine Spam-Inhalte**\nWerbung, Spam und irrelevante Links sind verboten.\n\n**3. Keine illegalen Inhalte**\nJegliche Inhalte, die gegen geltendes Recht verstoßen, sind streng verboten.\n\n**4. Themenrelevanz**\nBeiträge sollten zur jeweiligen Kategorie passen.\n\n**5. Urheberrecht**\nVeröffentliche keine Inhalte, an denen du keine Rechte besitzt.\n\n**6. Datenschutz**\nTeile keine persönlichen Daten anderer Personen ohne deren Zustimmung.\n\n**7. Moderations-Entscheidungen**\nEntscheidungen der Moderatoren sind zu respektieren. Bei Fragen wende dich direkt ans Team.\n\nVerstöße können zur Verwarnung oder dauerhaften Sperrung führen.",
|
'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', [] );
|
$saved = get_option( 'wbf_settings', [] );
|
||||||
|
|
||||||
// Fehlende Keys mit Defaults auffüllen, leere Strings ignorieren
|
// Fehlende Keys mit Defaults auffüllen, leere Strings ignorieren
|
||||||
return array_merge( $defaults, array_filter( (array) $saved, 'strlen' ) );
|
// Keine Filterung mehr, damit auch bewusst geleerte Felder gespeichert werden
|
||||||
|
return array_merge( $defaults, (array) $saved );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Admin-Seite ───────────────────────────────────────────────────────────────
|
// ── Admin-Seite ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt ein Array der Rollen-Keys zurück die nicht geblockt/ignoriert werden können.
|
||||||
|
* Superadmin ist immer enthalten — unabhängig von der Einstellung.
|
||||||
|
*
|
||||||
|
* @return string[] z.B. ['superadmin', 'admin', 'moderator']
|
||||||
|
*/
|
||||||
|
if ( ! function_exists('wbf_get_ignore_blocked_roles') ) {
|
||||||
|
function wbf_get_ignore_blocked_roles() {
|
||||||
|
$raw = wbf_get_settings()['ignore_blocked_roles'] ?? 'superadmin,admin,moderator';
|
||||||
|
$keys = array_filter( array_map( 'trim', explode( ',', $raw ) ) );
|
||||||
|
// superadmin immer schützen
|
||||||
|
if ( ! in_array('superadmin', $keys, true) ) {
|
||||||
|
$keys[] = 'superadmin';
|
||||||
|
}
|
||||||
|
return array_values( $keys );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob ein User ignoriert/geblockt werden darf.
|
||||||
|
*
|
||||||
|
* @param object $target Forum-User-Objekt
|
||||||
|
* @return bool true = darf ignoriert werden, false = nicht erlaubt
|
||||||
|
*/
|
||||||
|
if ( ! function_exists('wbf_can_be_ignored') ) {
|
||||||
|
function wbf_can_be_ignored( $target ) {
|
||||||
|
if ( ! $target ) return false;
|
||||||
|
$blocked_roles = wbf_get_ignore_blocked_roles();
|
||||||
|
return ! in_array( $target->role, $blocked_roles, true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! function_exists('wbf_admin_settings') ) {
|
if ( ! function_exists('wbf_admin_settings') ) {
|
||||||
function wbf_admin_settings() {
|
function wbf_admin_settings() {
|
||||||
|
|
||||||
@@ -96,7 +142,57 @@ function wbf_admin_settings() {
|
|||||||
// rules_content separat (nicht in $fields, da textarea mit eigener Behandlung)
|
// rules_content separat (nicht in $fields, da textarea mit eigener Behandlung)
|
||||||
$settings['rules_content'] = sanitize_textarea_field( $_POST['rules_content'] ?? '' );
|
$settings['rules_content'] = sanitize_textarea_field( $_POST['rules_content'] ?? '' );
|
||||||
|
|
||||||
|
// Discord-Einstellungen gesondert speichern (sensitiv — niemals in wbf_settings öffentlich)
|
||||||
|
$discord_fields = ['discord_bot_token', 'discord_guild_id', 'discord_client_id', 'discord_client_secret'];
|
||||||
|
foreach ( $discord_fields as $df ) {
|
||||||
|
$settings[$df] = sanitize_text_field( $_POST[$df] ?? '' );
|
||||||
|
}
|
||||||
|
$settings['discord_role_sync'] = isset($_POST['discord_role_sync']) && $_POST['discord_role_sync'] === '1' ? '1' : '0';
|
||||||
|
|
||||||
|
// Discord-Rollen-Map: Array von discord_role_id => forum_role_key
|
||||||
|
$role_map = [];
|
||||||
|
$dc_ids = array_map('sanitize_text_field', (array)($_POST['discord_role_id'] ?? []));
|
||||||
|
$fr_keys = array_map('sanitize_key', (array)($_POST['discord_forum_role'] ?? []));
|
||||||
|
$valid_roles = array_keys(WBF_Roles::get_all());
|
||||||
|
foreach ( $dc_ids as $i => $dc_id ) {
|
||||||
|
$dc_id = trim($dc_id);
|
||||||
|
$fr_key = $fr_keys[$i] ?? '';
|
||||||
|
if ( $dc_id !== '' && in_array($fr_key, $valid_roles, true) ) {
|
||||||
|
$role_map[$dc_id] = $fr_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$settings['discord_role_map'] = json_encode($role_map);
|
||||||
|
|
||||||
|
// ── Minecraft Bridge ──────────────────────────────────────────────────
|
||||||
|
$settings['mc_bridge_api_url'] = esc_url_raw( trim( $_POST['mc_bridge_api_url'] ?? '' ) );
|
||||||
|
$settings['mc_bridge_api_secret'] = sanitize_text_field( $_POST['mc_bridge_api_secret'] ?? '' );
|
||||||
|
|
||||||
|
// Checkbox-Felder explizit als '0' speichern wenn nicht angehakt,
|
||||||
|
// damit array_filter(...,'strlen') sie nicht wegwirft und der Default '1' greift.
|
||||||
|
$checkbox_fields = ['maintenance_mode', 'rules_enabled', 'rules_accept_required', 'mc_bridge_enabled'];
|
||||||
|
foreach ( $checkbox_fields as $cb ) {
|
||||||
|
$settings[$cb] = isset($_POST[$cb]) && $_POST[$cb] === '1' ? '1' : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_blocked_roles: kommagetrennte Liste der gewählten Rollen-Keys
|
||||||
|
$all_role_keys = array_keys( WBF_Roles::get_all() );
|
||||||
|
$checked_roles = array_intersect(
|
||||||
|
array_map( 'sanitize_key', (array)( $_POST['ignore_blocked_roles'] ?? [] ) ),
|
||||||
|
$all_role_keys
|
||||||
|
);
|
||||||
|
// superadmin ist immer blockiert — kann nicht entfernt werden
|
||||||
|
if ( ! in_array('superadmin', $checked_roles, true) ) {
|
||||||
|
$checked_roles[] = 'superadmin';
|
||||||
|
}
|
||||||
|
$settings['ignore_blocked_roles'] = implode( ',', $checked_roles );
|
||||||
|
|
||||||
update_option( 'wbf_settings', $settings );
|
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>';
|
echo '<div class="notice notice-success is-dismissible"><p>✅ Einstellungen gespeichert!</p></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +294,40 @@ function wbf_admin_settings() {
|
|||||||
🔒 Sicherheit
|
🔒 Sicherheit
|
||||||
</h2>
|
</h2>
|
||||||
<table class="form-table" role="presentation">
|
<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>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="wbf_auto_logout_minutes">Auto-Logout nach Inaktivität</label>
|
<label for="wbf_auto_logout_minutes">Auto-Logout nach Inaktivität</label>
|
||||||
@@ -401,6 +531,325 @@ function wbf_admin_settings() {
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<!-- ── Ignore / Block-System ────────────────────────── -->
|
||||||
|
<h2 style="border-bottom:1px solid #ddd;padding-bottom:.4rem;margin-top:1.5rem">
|
||||||
|
🚫 Ignore / Block-System
|
||||||
|
</h2>
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label>Nicht blockierbare Rollen</label></th>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$blocked_roles = array_filter( array_map('trim', explode(',', $s['ignore_blocked_roles'] ?? 'superadmin,admin,moderator')) );
|
||||||
|
$all_roles = WBF_Roles::get_sorted();
|
||||||
|
foreach ( $all_roles as $key => $role ):
|
||||||
|
$is_superadmin = ($key === 'superadmin');
|
||||||
|
$is_checked = in_array($key, $blocked_roles, true);
|
||||||
|
$rc = esc_attr($role['color']);
|
||||||
|
$rb = esc_attr($role['bg_color']);
|
||||||
|
?>
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;margin-bottom:7px;cursor:<?php echo $is_superadmin?'not-allowed':'pointer'; ?>">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="ignore_blocked_roles[]"
|
||||||
|
value="<?php echo esc_attr($key); ?>"
|
||||||
|
<?php checked($is_checked, true); ?>
|
||||||
|
<?php echo $is_superadmin ? 'disabled' : ''; ?>
|
||||||
|
style="width:16px;height:16px;accent-color:<?php echo $rc; ?>">
|
||||||
|
<?php if ($is_superadmin): ?>
|
||||||
|
<!-- superadmin immer als hidden mitschicken da disabled nicht übermittelt wird -->
|
||||||
|
<input type="hidden" name="ignore_blocked_roles[]" value="superadmin">
|
||||||
|
<?php endif; ?>
|
||||||
|
<span style="display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:20px;font-size:.78rem;font-weight:700;color:<?php echo $rc; ?>;background:<?php echo $rb; ?>;border:1px solid <?php echo $rc; ?>">
|
||||||
|
<i class="<?php echo esc_attr($role['icon'] ?? 'fas fa-user'); ?>"></i>
|
||||||
|
<?php echo esc_html($role['label']); ?>
|
||||||
|
</span>
|
||||||
|
<?php if ($is_superadmin): ?>
|
||||||
|
<span style="font-size:.72rem;color:#999">(immer geschützt)</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<p class="description" style="margin-top:8px">
|
||||||
|
Nutzer mit diesen Rollen können von anderen Mitgliedern <strong>nicht</strong> geblockt oder ignoriert werden.
|
||||||
|
Superadmin ist permanent geschützt und kann nicht abgewählt werden.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════
|
||||||
|
DISCORD-INTEGRATION
|
||||||
|
══════════════════════════════════════════════════════════ -->
|
||||||
|
<h2 style="border-bottom:1px solid #ddd;padding-bottom:.4rem;margin-top:2rem">
|
||||||
|
<span style="color:#5865f2">🎮</span> Discord-Integration
|
||||||
|
</h2>
|
||||||
|
<p class="description" style="margin-bottom:1rem">
|
||||||
|
Bot-Token und Guild-ID findest du im <a href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</a>.
|
||||||
|
Der Bot muss Mitglied deines Servers sein und die Berechtigung <strong>Direct Messages lesen/senden</strong> sowie
|
||||||
|
<strong>Server-Mitglieder verwalten</strong> besitzen.
|
||||||
|
</p>
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="wbf_discord_bot_token">Bot-Token</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="password" id="wbf_discord_bot_token" name="discord_bot_token"
|
||||||
|
value="<?php echo esc_attr($s['discord_bot_token']); ?>"
|
||||||
|
class="regular-text" autocomplete="off" placeholder="Bot-Token aus dem Developer Portal">
|
||||||
|
<p class="description">Niemals öffentlich teilen! Wird verschlüsselt in der Datenbank gespeichert.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="wbf_discord_guild_id">Server-ID (Guild ID)</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="wbf_discord_guild_id" name="discord_guild_id"
|
||||||
|
value="<?php echo esc_attr($s['discord_guild_id']); ?>"
|
||||||
|
class="regular-text" placeholder="z. B. 123456789012345678">
|
||||||
|
<p class="description">Rechtsklick auf deinen Server → ID kopieren (Entwicklermodus muss aktiv sein).</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="wbf_discord_client_id">Client ID (optional)</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="wbf_discord_client_id" name="discord_client_id"
|
||||||
|
value="<?php echo esc_attr($s['discord_client_id']); ?>"
|
||||||
|
class="regular-text" placeholder="Application ID">
|
||||||
|
<p class="description">Für zukünftige OAuth2-Unterstützung. Aktuell optional.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="wbf_discord_client_secret">Client Secret (optional)</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="password" id="wbf_discord_client_secret" name="discord_client_secret"
|
||||||
|
value="<?php echo esc_attr($s['discord_client_secret']); ?>"
|
||||||
|
class="regular-text" autocomplete="off" placeholder="Client Secret">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Rollen-Sync aktivieren</th>
|
||||||
|
<td>
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||||
|
<input type="checkbox" name="discord_role_sync" value="1"
|
||||||
|
<?php checked('1', $s['discord_role_sync'] ?? '0'); ?>>
|
||||||
|
Discord-Serverrollen automatisch auf Forum-Rollen mappen
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
Wenn aktiviert, wird bei jedem Login und stündlich per Cron die Discord-Rolle des Nutzers
|
||||||
|
geprüft und die Forum-Rolle entsprechend der unten definierten Zuordnung aktualisiert.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Discord Rollen-Map -->
|
||||||
|
<h3 style="margin-top:1.5rem">🔗 Discord-Rollen → Forum-Rollen Zuordnung</h3>
|
||||||
|
<p class="description" style="margin-bottom:.75rem">
|
||||||
|
Trage die Discord-Rollen-ID und die gewünschte Forum-Rolle ein.
|
||||||
|
Mehrere Einträge werden der Reihe nach geprüft — der erste Treffer gewinnt.
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
$role_map_raw = $s['discord_role_map'] ?? '{}';
|
||||||
|
$role_map = json_decode($role_map_raw, true) ?: [];
|
||||||
|
$forum_roles = WBF_Roles::get_sorted();
|
||||||
|
// Sicherstellen dass mindestens eine leere Zeile zum Hinzufügen da ist
|
||||||
|
if ( empty($role_map) ) $role_map[''] = '';
|
||||||
|
?>
|
||||||
|
<table class="widefat" id="wbf-discord-role-map" style="max-width:680px;margin-bottom:.75rem">
|
||||||
|
<thead><tr>
|
||||||
|
<th style="width:50%">Discord Rollen-ID</th>
|
||||||
|
<th style="width:40%">Forum-Rolle</th>
|
||||||
|
<th style="width:10%"></th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $role_map as $dc_id => $fr_key ) : ?>
|
||||||
|
<tr class="wbf-role-map-row">
|
||||||
|
<td><input type="text" name="discord_role_id[]"
|
||||||
|
value="<?php echo esc_attr($dc_id); ?>"
|
||||||
|
placeholder="Discord Rollen-ID"
|
||||||
|
class="widefat" style="font-family:monospace"></td>
|
||||||
|
<td>
|
||||||
|
<select name="discord_forum_role[]" class="widefat">
|
||||||
|
<option value="">— wählen —</option>
|
||||||
|
<?php foreach ( $forum_roles as $rk => $role ) :
|
||||||
|
if ( $rk === 'superadmin' ) continue; ?>
|
||||||
|
<option value="<?php echo esc_attr($rk); ?>"
|
||||||
|
<?php selected($rk, $fr_key); ?>>
|
||||||
|
<?php echo esc_html($role['label']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td><button type="button" class="button button-small wbf-rm-role-row"
|
||||||
|
style="color:#c00">✕</button></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button type="button" class="button" id="wbf-add-role-row">+ Zeile hinzufügen</button>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
document.getElementById('wbf-add-role-row').addEventListener('click', function(){
|
||||||
|
var tbody = document.querySelector('#wbf-discord-role-map tbody');
|
||||||
|
var row = document.querySelector('.wbf-role-map-row').cloneNode(true);
|
||||||
|
row.querySelectorAll('input').forEach(function(i){i.value='';});
|
||||||
|
row.querySelectorAll('select').forEach(function(s){s.selectedIndex=0;});
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
document.addEventListener('click', function(e){
|
||||||
|
if (e.target.classList.contains('wbf-rm-role-row')) {
|
||||||
|
var rows = document.querySelectorAll('.wbf-role-map-row');
|
||||||
|
if (rows.length > 1) e.target.closest('tr').remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test-Verbindung -->
|
||||||
|
<div style="margin-top:1.25rem;padding:1rem;background:#f0f7ff;border:1px solid #c3dafe;border-radius:6px;max-width:680px">
|
||||||
|
<strong>🔌 Verbindungstest</strong><br>
|
||||||
|
<p style="margin:.4rem 0 .75rem;color:#374151;font-size:.9rem">
|
||||||
|
Speichere zuerst die Einstellungen, dann klicke „Testen" um zu prüfen ob der Bot erreichbar ist.
|
||||||
|
</p>
|
||||||
|
<button type="button" class="button button-secondary" id="wbf-discord-test-btn">
|
||||||
|
🔌 Discord-Verbindung testen
|
||||||
|
</button>
|
||||||
|
<span id="wbf-discord-test-result" style="margin-left:10px;font-weight:600"></span>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.getElementById('wbf-discord-test-btn').addEventListener('click', function(){
|
||||||
|
var btn = this;
|
||||||
|
var res = document.getElementById('wbf-discord-test-result');
|
||||||
|
btn.disabled = true;
|
||||||
|
res.textContent = '⏳ Teste…';
|
||||||
|
fetch(ajaxurl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/x-www-form-urlencoded'},
|
||||||
|
body: 'action=wbf_discord_test&nonce=<?php echo wp_create_nonce("wbf_discord_test"); ?>'
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(function(d){
|
||||||
|
if (d.success) {
|
||||||
|
res.style.color = '#16a34a';
|
||||||
|
res.textContent = '✅ ' + (d.data.message || 'Verbunden!');
|
||||||
|
} else {
|
||||||
|
res.style.color = '#dc2626';
|
||||||
|
res.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
|
||||||
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
})
|
||||||
|
.catch(function(){ res.style.color='#dc2626'; res.textContent='❌ Netzwerkfehler'; btn.disabled=false; });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════
|
||||||
|
Minecraft Bridge
|
||||||
|
════════════════════════════════════════════════════════════ -->
|
||||||
|
<div class="wbf-settings-box" style="margin-top:2rem;padding:1.5rem;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb">
|
||||||
|
<h2 style="margin-top:0;display:flex;align-items:center;gap:8px">
|
||||||
|
<span style="font-size:1.3em">⛏️</span> Minecraft Bridge
|
||||||
|
</h2>
|
||||||
|
<p class="description" style="margin-bottom:1.2rem;color:#6b7280">
|
||||||
|
Verbindet das Forum mit deinem BungeeCord-Server (StatusAPI Plugin).
|
||||||
|
Spieler können ihren Forum-Account mit <code>/forumlink <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(
|
<?php submit_button(
|
||||||
'💾 Einstellungen speichern',
|
'💾 Einstellungen speichern',
|
||||||
'primary',
|
'primary',
|
||||||
@@ -408,7 +857,6 @@ function wbf_admin_settings() {
|
|||||||
true,
|
true,
|
||||||
[ 'style' => 'margin-top:1rem' ]
|
[ 'style' => 'margin-top:1rem' ]
|
||||||
); ?>
|
); ?>
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- ── Vorschau-Tabelle ──────────────────────────────── -->
|
<!-- ── Vorschau-Tabelle ──────────────────────────────── -->
|
||||||
<hr style="margin-top:2.5rem">
|
<hr style="margin-top:2.5rem">
|
||||||
|
|||||||
@@ -125,18 +125,22 @@ class WBF_Setup {
|
|||||||
$page_title = sanitize_text_field($_POST['page_title'] ?? 'Forum');
|
$page_title = sanitize_text_field($_POST['page_title'] ?? 'Forum');
|
||||||
|
|
||||||
if ($create_page) {
|
if ($create_page) {
|
||||||
$existing = get_posts(['post_type'=>'page','s'=>$page_title,'posts_per_page'=>1]);
|
global $wpdb;
|
||||||
if (empty($existing)) {
|
$existing_id = $wpdb->get_var( $wpdb->prepare(
|
||||||
$page_id = wp_insert_post([
|
"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status = 'publish' AND post_content LIKE %s LIMIT 1",
|
||||||
'post_title' => $page_title,
|
'%[business_forum]%'
|
||||||
'post_content' => '[business_forum]',
|
) );
|
||||||
'post_status' => 'publish',
|
if (empty($existing_id)) {
|
||||||
'post_type' => 'page',
|
$page_id = wp_insert_post([
|
||||||
]);
|
'post_title' => $page_title,
|
||||||
if ($page_id) {
|
'post_content' => '[business_forum]',
|
||||||
update_option('wbf_forum_page_id', $page_id);
|
'post_status' => 'publish',
|
||||||
$success = get_permalink($page_id);
|
'post_type' => 'page',
|
||||||
}
|
]);
|
||||||
|
if ($page_id) {
|
||||||
|
update_option('wbf_forum_page_id', $page_id);
|
||||||
|
$success = get_permalink($page_id);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$success = get_permalink($existing[0]->ID);
|
$success = get_permalink($existing[0]->ID);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,10 @@
|
|||||||
|
|
||||||
/* ── Utilities ──────────────────────────────────────────────── */
|
/* ── Utilities ──────────────────────────────────────────────── */
|
||||||
function wbfPost(action, data, cb, errCb) {
|
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.action = action;
|
||||||
data.nonce = WBF.nonce;
|
data.nonce = WBF.nonce;
|
||||||
$.post(WBF.ajax_url, data, function (res) {
|
$.post(WBF.ajax_url, data, function (res) {
|
||||||
@@ -52,12 +56,18 @@
|
|||||||
/* ── Registrieren ───────────────────────────────────────────── */
|
/* ── Registrieren ───────────────────────────────────────────── */
|
||||||
$(document).on('click', '.wbf-reg-submit-btn', function () {
|
$(document).on('click', '.wbf-reg-submit-btn', function () {
|
||||||
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
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', {
|
wbfPost('wbf_register', {
|
||||||
username: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-user').val(),
|
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(),
|
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(),
|
email: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-email').val(),
|
||||||
password: $(this).closest('.wbf-auth-box').find('.wbf-field-reg-pass').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' : ''
|
rules_accepted: $(this).closest('.wbf-auth-box').find('.wbf-field-rules-accept').is(':checked') ? '1' : ''
|
||||||
}, function () {
|
}, function () {
|
||||||
location.reload();
|
location.reload();
|
||||||
@@ -510,23 +520,63 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── Profil speichern ───────────────────────────────────────── */
|
/* ── Profil speichern (alles auf einmal) ───────────────────── */
|
||||||
$(document).on('click', '#wbfSaveProfile, #wbfSaveProfileCf', function () {
|
$(document).on('click', '#wbfSaveProfile', function () {
|
||||||
var $btn = $(this).prop('disabled', true);
|
var $btn = $(this).prop('disabled', true);
|
||||||
var $msg = $(this).siblings('.wbf-msg').length ? $(this).siblings('.wbf-msg') : $('#wbfProfileMsg');
|
var $msg = $('#wbfProfileMsg');
|
||||||
var data = {
|
var data = {
|
||||||
display_name: $('#wbfEditName').val(),
|
display_name: $('#wbfEditName').val(),
|
||||||
bio: $('#wbfEditBio').val(),
|
bio: $('#wbfEditBio').val(),
|
||||||
signature: $('#wbfEditSignature').val(),
|
signature: $('#wbfEditSignature').val()
|
||||||
new_password: $('#wbfNewPassword').val()
|
|
||||||
};
|
};
|
||||||
// Benutzerdefinierte Profilfelder einsammeln
|
// Alle benutzerdefinierten Felder (alle Kategorien) einsammeln
|
||||||
$('.wbf-cf-input').each(function () {
|
$('.wbf-cf-input').each(function () {
|
||||||
data[$(this).data('field')] = $(this).val();
|
data[$(this).data('field')] = $(this).val();
|
||||||
});
|
});
|
||||||
wbfPost('wbf_update_profile', data, function (d) {
|
wbfPost('wbf_update_profile', data, function (d) {
|
||||||
showMsg($msg, d.message, true);
|
showMsg($msg, d.message, true);
|
||||||
$btn.prop('disabled', false);
|
$btn.prop('disabled', false);
|
||||||
|
// Bio und Signatur sofort aktualisieren (ohne Reload)
|
||||||
|
if (typeof data.bio !== 'undefined') {
|
||||||
|
$('.wbf-profile-sidebar__bio-text').text(data.bio);
|
||||||
|
}
|
||||||
|
if (typeof data.signature !== 'undefined') {
|
||||||
|
$('.wbf-profile-sidebar__sig').text(data.signature);
|
||||||
|
}
|
||||||
|
}, function (d) {
|
||||||
|
showMsg($msg, d.message || 'Fehler', false);
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Passwort ändern ────────────────────────────────────────── */
|
||||||
|
$(document).on('click', '#wbfSavePassword', function () {
|
||||||
|
var $btn = $(this).prop('disabled', true);
|
||||||
|
var $msg = $('#wbfPasswordMsg');
|
||||||
|
var cur = $('#wbfCurrentPassword').val();
|
||||||
|
var pw1 = $('#wbfNewPassword').val();
|
||||||
|
var pw2 = $('#wbfNewPassword2').val();
|
||||||
|
|
||||||
|
if (!cur) {
|
||||||
|
showMsg($msg, 'Bitte aktuelles Passwort eingeben.', false);
|
||||||
|
return $btn.prop('disabled', false);
|
||||||
|
}
|
||||||
|
if (pw1.length < 6) {
|
||||||
|
showMsg($msg, 'Neues Passwort mindestens 6 Zeichen.', false);
|
||||||
|
return $btn.prop('disabled', false);
|
||||||
|
}
|
||||||
|
if (pw1 !== pw2) {
|
||||||
|
showMsg($msg, 'Die Passwörter stimmen nicht überein.', false);
|
||||||
|
return $btn.prop('disabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
wbfPost('wbf_update_profile', {
|
||||||
|
current_password: cur,
|
||||||
|
new_password: pw1
|
||||||
|
}, function (d) {
|
||||||
|
showMsg($msg, d.message, true);
|
||||||
|
$('#wbfCurrentPassword, #wbfNewPassword, #wbfNewPassword2').val('');
|
||||||
|
$btn.prop('disabled', false);
|
||||||
}, function (d) {
|
}, function (d) {
|
||||||
showMsg($msg, d.message || 'Fehler', false);
|
showMsg($msg, d.message || 'Fehler', false);
|
||||||
$btn.prop('disabled', false);
|
$btn.prop('disabled', false);
|
||||||
@@ -542,10 +592,16 @@
|
|||||||
$(document).on('change', '#wbfAvatarFile', function () {
|
$(document).on('change', '#wbfAvatarFile', function () {
|
||||||
var file = this.files[0];
|
var file = this.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
// Sofort-Vorschau — synchron, kein Callback, kein Warten
|
||||||
|
var objectUrl = URL.createObjectURL(file);
|
||||||
|
$('#wbfProfileAvatar').attr('src', objectUrl).css('opacity', '.6');
|
||||||
|
|
||||||
var fd = new FormData();
|
var fd = new FormData();
|
||||||
fd.append('action', 'wbf_upload_avatar');
|
fd.append('action', 'wbf_upload_avatar');
|
||||||
fd.append('nonce', WBF.nonce);
|
fd.append('nonce', WBF.nonce);
|
||||||
fd.append('avatar', file);
|
fd.append('avatar', file);
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: WBF.ajax_url,
|
url: WBF.ajax_url,
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
@@ -553,9 +609,61 @@
|
|||||||
processData: false,
|
processData: false,
|
||||||
contentType: false,
|
contentType: false,
|
||||||
success: function (res) {
|
success: function (res) {
|
||||||
|
$('#wbfProfileAvatar').css('opacity', '1');
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
$('.wbf-profile-page__avatar').attr('src', res.data.avatar_url);
|
// Object-URL freigeben, endgültige Server-URL setzen
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
var finalUrl = res.data.avatar_url + '?v=' + Date.now();
|
||||||
|
$('#wbfProfileAvatar').attr('src', finalUrl);
|
||||||
|
// Topbar-Avatar ebenfalls aktualisieren
|
||||||
|
$('.wbf-topbar__user img').attr('src', finalUrl);
|
||||||
|
$('.wbf-profile-widget__avatar img').attr('src', finalUrl);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$('#wbfProfileAvatar').css('opacity', '1');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Banner-Upload ─────────────────────────────────────────────────────────
|
||||||
|
$(document).on('change', '#wbfBannerFile', function () {
|
||||||
|
var file = this.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Sofort-Vorschau
|
||||||
|
var objectUrl = URL.createObjectURL(file);
|
||||||
|
var $wrap = $('#wbfProfileBannerWrap');
|
||||||
|
var $existing = $wrap.find('.wbf-profile-banner__img');
|
||||||
|
|
||||||
|
// Falls noch kein Banner-Bild existiert, eins einfügen
|
||||||
|
if ($existing.length === 0) {
|
||||||
|
$wrap.prepend('<img src="' + objectUrl + '" alt="" id="wbfProfileBanner" class="wbf-profile-banner__img" style="opacity:.4">');
|
||||||
|
} else {
|
||||||
|
$existing.attr('src', objectUrl).css('opacity', '.4');
|
||||||
|
}
|
||||||
|
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('action', 'wbf_upload_banner');
|
||||||
|
fd.append('nonce', WBF.nonce);
|
||||||
|
fd.append('banner', file);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: WBF.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: fd,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
success: function (res) {
|
||||||
|
var $img = $wrap.find('.wbf-profile-banner__img');
|
||||||
|
if (res.success) {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
$img.attr('src', res.data.banner_url + '?v=' + Date.now());
|
||||||
|
}
|
||||||
|
$img.css('opacity', '');
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$wrap.find('.wbf-profile-banner__img').css('opacity', '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1091,7 +1199,7 @@
|
|||||||
var html = '';
|
var html = '';
|
||||||
d.notifications.forEach(function (n) {
|
d.notifications.forEach(function (n) {
|
||||||
var isUnread = n.is_read == 0;
|
var isUnread = n.is_read == 0;
|
||||||
var avatar = n.actor_avatar || '';
|
var avatar = $('<img>').attr('src', n.actor_avatar || '').attr('alt', '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
|
||||||
var base = WBF.forum_url || window.location.href.split('?')[0];
|
var base = WBF.forum_url || window.location.href.split('?')[0];
|
||||||
var sep = base.indexOf('?') !== -1 ? '&' : '?';
|
var sep = base.indexOf('?') !== -1 ? '&' : '?';
|
||||||
var actor = '<strong>' + $('<span>').text(n.actor_name).html() + '</strong>';
|
var actor = '<strong>' + $('<span>').text(n.actor_name).html() + '</strong>';
|
||||||
@@ -1118,7 +1226,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html += '<a class="wbf-notif-item' + (isUnread ? ' wbf-notif-item--unread' : '') + '" href="' + url + '">' +
|
html += '<a class="wbf-notif-item' + (isUnread ? ' wbf-notif-item--unread' : '') + '" href="' + url + '">' +
|
||||||
'<div class="wbf-notif-item__avatar"><img src="' + avatar + '" alt=""></div>' +
|
'<div class="wbf-notif-item__avatar">' + avatar + '</div>' +
|
||||||
'<div class="wbf-notif-item__body">' +
|
'<div class="wbf-notif-item__body">' +
|
||||||
'<div class="wbf-notif-item__text">' + text +
|
'<div class="wbf-notif-item__text">' + text +
|
||||||
(sub ? '<br><span style="color:var(--c-muted);font-size:.78rem">' + $('<span>').text(sub).html() + '</span>' : '') +
|
(sub ? '<br><span style="color:var(--c-muted);font-size:.78rem">' + $('<span>').text(sub).html() + '</span>' : '') +
|
||||||
@@ -1218,7 +1326,7 @@
|
|||||||
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Loeschen"><i class="fas fa-trash-can"></i></button>';
|
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Loeschen"><i class="fas fa-trash-can"></i></button>';
|
||||||
var html = '<div class="' + cls + '" data-msg-id="' + m.id + '">';
|
var html = '<div class="' + cls + '" data-msg-id="' + m.id + '">';
|
||||||
if (!isMine) {
|
if (!isMine) {
|
||||||
html += '<img src="' + (m.sender_avatar||'') + '" class="wbf-dm-inbox-item__avatar">';
|
html += $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
|
||||||
}
|
}
|
||||||
html += '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
|
html += '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
|
||||||
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
|
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
|
||||||
@@ -1293,12 +1401,16 @@
|
|||||||
'</div>'
|
'</div>'
|
||||||
].join('')).appendTo('body');
|
].join('')).appendTo('body');
|
||||||
|
|
||||||
|
var wbfLogoutFired = false; // Guard gegen doppelten Logout-Call
|
||||||
|
|
||||||
function wbfDoLogout() {
|
function wbfDoLogout() {
|
||||||
|
if (wbfLogoutFired) return; // doppelten Aufruf verhindern
|
||||||
|
wbfLogoutFired = true;
|
||||||
clearTimeout(wbfIdleTimer);
|
clearTimeout(wbfIdleTimer);
|
||||||
clearTimeout(wbfWarnTimer);
|
clearTimeout(wbfWarnTimer);
|
||||||
clearInterval(wbfCountTimer);
|
clearInterval(wbfCountTimer);
|
||||||
$wbfToast.hide();
|
$wbfToast.hide();
|
||||||
wbfPost('wbf_logout', {}, function () {
|
wbfPost('wbf_logout', { nonce: WBF.nonce }, function () {
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1308,23 +1420,23 @@
|
|||||||
var secs = 30;
|
var secs = 30;
|
||||||
$('#wbfIdleCountdown').text(secs);
|
$('#wbfIdleCountdown').text(secs);
|
||||||
$wbfToast.fadeIn(200);
|
$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 () {
|
wbfCountTimer = setInterval(function () {
|
||||||
secs--;
|
secs--;
|
||||||
$('#wbfIdleCountdown').text(secs);
|
$('#wbfIdleCountdown').text(Math.max(0, secs));
|
||||||
if (secs <= 0) {
|
if (secs <= 0) {
|
||||||
clearInterval(wbfCountTimer);
|
clearInterval(wbfCountTimer);
|
||||||
wbfDoLogout();
|
wbfDoLogout();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
// Auto-logout after warning period
|
|
||||||
wbfIdleTimer = setTimeout(wbfDoLogout, wbfWarnMs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function wbfResetIdleTimer() {
|
function wbfResetIdleTimer() {
|
||||||
if (wbfWarning) return; // Nutzer hat aktiv Warnung bestätigt — nicht resetten
|
if (wbfWarning) return; // Nutzer hat aktiv Warnung bestätigt — nicht resetten
|
||||||
clearTimeout(wbfIdleTimer);
|
clearTimeout(wbfIdleTimer);
|
||||||
clearTimeout(wbfWarnTimer);
|
clearTimeout(wbfWarnTimer);
|
||||||
// Warn 30 sec before timeout
|
// Warnung 30 Sek. vor Ablauf zeigen
|
||||||
wbfWarnTimer = setTimeout(wbfShowWarning, wbfIdleMs - wbfWarnMs);
|
wbfWarnTimer = setTimeout(wbfShowWarning, wbfIdleMs - wbfWarnMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1334,6 +1446,7 @@
|
|||||||
clearInterval(wbfCountTimer);
|
clearInterval(wbfCountTimer);
|
||||||
$wbfToast.fadeOut(200);
|
$wbfToast.fadeOut(200);
|
||||||
wbfWarning = false;
|
wbfWarning = false;
|
||||||
|
wbfLogoutFired = false; // Guard zurücksetzen
|
||||||
wbfResetIdleTimer();
|
wbfResetIdleTimer();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1452,7 +1565,7 @@
|
|||||||
var html = '';
|
var html = '';
|
||||||
d.users.forEach(function(u) {
|
d.users.forEach(function(u) {
|
||||||
html += '<div class="wbf-mention-item" data-username="' + $('<span>').text(u.username).html() + '">'
|
html += '<div class="wbf-mention-item" data-username="' + $('<span>').text(u.username).html() + '">'
|
||||||
+ '<img src="' + (u.avatar_url || '') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
|
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
|
||||||
+ '<span>' + $('<span>').text(u.display_name).html() + '</span>'
|
+ '<span>' + $('<span>').text(u.display_name).html() + '</span>'
|
||||||
+ '<small>@' + $('<span>').text(u.username).html() + '</small>'
|
+ '<small>@' + $('<span>').text(u.username).html() + '</small>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
@@ -1500,7 +1613,7 @@
|
|||||||
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Löschen"><i class="fas fa-trash-can"></i></button>';
|
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Löschen"><i class="fas fa-trash-can"></i></button>';
|
||||||
if (!isMine) {
|
if (!isMine) {
|
||||||
html += '<div class="' + cls + '" data-msg-id="' + m.id + '">'
|
html += '<div class="' + cls + '" data-msg-id="' + m.id + '">'
|
||||||
+ '<img src="' + (m.sender_avatar || '') + '" class="wbf-dm-msg__avatar">'
|
+ $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-msg__avatar')[0].outerHTML
|
||||||
+ '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
|
+ '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
|
||||||
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
|
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
|
||||||
+ '</div><div class="wbf-dm-msg__time">' + time + delBtn + '</div></div></div>';
|
+ '</div><div class="wbf-dm-msg__time">' + time + delBtn + '</div></div></div>';
|
||||||
@@ -1524,7 +1637,7 @@
|
|||||||
var href = window.location.pathname + '?forum_dm=inbox&with=' + conv.partner_id;
|
var href = window.location.pathname + '?forum_dm=inbox&with=' + conv.partner_id;
|
||||||
var unread = parseInt(conv.unread_cnt) > 0;
|
var unread = parseInt(conv.unread_cnt) > 0;
|
||||||
html += '<a class="wbf-dm-inbox-item' + (unread ? ' wbf-dm-inbox-item--unread' : '') + '" href="' + href + '">'
|
html += '<a class="wbf-dm-inbox-item' + (unread ? ' wbf-dm-inbox-item--unread' : '') + '" href="' + href + '">'
|
||||||
+ '<img src="' + (conv.partner_avatar || '') + '" class="wbf-dm-inbox-item__avatar">'
|
+ $('<img>').attr('src', conv.partner_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
|
||||||
+ '<div class="wbf-dm-inbox-item__body">'
|
+ '<div class="wbf-dm-inbox-item__body">'
|
||||||
+ '<span class="wbf-dm-inbox-item__name">' + $('<span>').text(conv.partner_name).html() + '</span>'
|
+ '<span class="wbf-dm-inbox-item__name">' + $('<span>').text(conv.partner_name).html() + '</span>'
|
||||||
+ (unread ? '<span class="wbf-dm-inbox-item__badge">' + conv.unread_cnt + '</span>' : '')
|
+ (unread ? '<span class="wbf-dm-inbox-item__badge">' + conv.unread_cnt + '</span>' : '')
|
||||||
@@ -1550,7 +1663,7 @@
|
|||||||
var backUrl = window.location.pathname + '?forum_dm=inbox';
|
var backUrl = window.location.pathname + '?forum_dm=inbox';
|
||||||
$('#wbfDmHeader').html(
|
$('#wbfDmHeader').html(
|
||||||
'<a href="' + backUrl + '" class="wbf-dm-back-btn" title="Zurück zur Inbox"><i class="fas fa-arrow-left"></i></a>'
|
'<a href="' + backUrl + '" class="wbf-dm-back-btn" title="Zurück zur Inbox"><i class="fas fa-arrow-left"></i></a>'
|
||||||
+ '<img src="' + (p.avatar_url||'') + '" class="wbf-dm-inbox-item__avatar">'
|
+ $('<img>').attr('src', p.avatar_url || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
|
||||||
+ '<strong>' + $('<span>').text(p.display_name).html() + '</strong>'
|
+ '<strong>' + $('<span>').text(p.display_name).html() + '</strong>'
|
||||||
+ '<a href="?forum_profile=' + p.id + '" style="font-size:.78rem;color:var(--c-muted);text-decoration:none">@' + $('<span>').text(p.username).html() + '</a>'
|
+ '<a href="?forum_profile=' + p.id + '" style="font-size:.78rem;color:var(--c-muted);text-decoration:none">@' + $('<span>').text(p.username).html() + '</a>'
|
||||||
);
|
);
|
||||||
@@ -1634,7 +1747,7 @@
|
|||||||
var html = '';
|
var html = '';
|
||||||
d.users.forEach(function(u) {
|
d.users.forEach(function(u) {
|
||||||
html += '<div class="wbf-tag-suggest-item" data-id="' + u.id + '" data-name="' + $('<span>').text(u.display_name).html() + '">'
|
html += '<div class="wbf-tag-suggest-item" data-id="' + u.id + '" data-name="' + $('<span>').text(u.display_name).html() + '">'
|
||||||
+ '<img src="' + (u.avatar_url||'') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
|
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
|
||||||
+ $('<span>').text(u.display_name).html()
|
+ $('<span>').text(u.display_name).html()
|
||||||
+ '<span style="color:var(--c-muted);font-size:.75rem">@' + $('<span>').text(u.username).html() + '</span>'
|
+ '<span style="color:var(--c-muted);font-size:.75rem">@' + $('<span>').text(u.username).html() + '</span>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
@@ -1944,6 +2057,55 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/* ── E-Mail-Adresse ändern ──────────────────────────────────────────── */
|
||||||
|
$(document).on('click', '#wbfSaveEmail', function () {
|
||||||
|
var $btn = $(this);
|
||||||
|
var email = $('#wbfNewEmail').val().trim();
|
||||||
|
var password = $('#wbfEmailPassword').val();
|
||||||
|
var $msg = $('#wbfEmailMsg');
|
||||||
|
if (!email) { $msg.removeClass('wbf-ok').addClass('wbf-err').text('Bitte E-Mail eingeben.'); return; }
|
||||||
|
if (!password) { $msg.removeClass('wbf-ok').addClass('wbf-err').text('Bitte Passwort eingeben.'); return; }
|
||||||
|
$btn.prop('disabled', true);
|
||||||
|
wbfPost('wbf_change_email', { new_email: email, password: password }, function (d) {
|
||||||
|
$msg.removeClass('wbf-err').addClass('wbf-ok').text(d.message || 'E-Mail geaendert.');
|
||||||
|
$('#wbfNewEmail').val('');
|
||||||
|
$('#wbfEmailPassword').val('');
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
}, function (d) {
|
||||||
|
$msg.removeClass('wbf-ok').addClass('wbf-err').text(d.message || 'Fehler.');
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Toggle-Switch (Notification Prefs) ─────────────────────────────── */
|
||||||
|
$(document).on('click', '.wbf-toggle', function () {
|
||||||
|
var $t = $(this);
|
||||||
|
var on = String($t.data('state')) === '1';
|
||||||
|
var val = on ? '0' : '1';
|
||||||
|
$t.data('state', val).attr('data-state', val);
|
||||||
|
if (val === '1') { $t.addClass('wbf-toggle--on'); }
|
||||||
|
else { $t.removeClass('wbf-toggle--on'); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Benachrichtigungs-Einstellungen speichern ───────────────────────── */
|
||||||
|
$(document).on('click', '#wbfSaveNotifPrefs', function () {
|
||||||
|
var $btn = $(this);
|
||||||
|
var $msg = $('#wbfNotifPrefsMsg');
|
||||||
|
$btn.prop('disabled', true);
|
||||||
|
wbfPost('wbf_save_notification_prefs', {
|
||||||
|
notify_reply: String($('#wbfNotifReply').data('state')) === '1' ? '1' : '0',
|
||||||
|
notify_mention: String($('#wbfNotifMention').data('state')) === '1' ? '1' : '0',
|
||||||
|
notify_message: String($('#wbfNotifMessage').data('state')) === '1' ? '1' : '0'
|
||||||
|
}, function (d) {
|
||||||
|
$msg.removeClass('wbf-err').addClass('wbf-ok').text(d.message || 'Gespeichert!');
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
}, function (d) {
|
||||||
|
$msg.removeClass('wbf-ok').addClass('wbf-err').text(d.message || 'Fehler.');
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* ── Lesezeichen ────────────────────────────────────────────────────── */
|
/* ── Lesezeichen ────────────────────────────────────────────────────── */
|
||||||
$(document).on('click', '.wbf-bookmark-btn', function () {
|
$(document).on('click', '.wbf-bookmark-btn', function () {
|
||||||
var $btn = $(this);
|
var $btn = $(this);
|
||||||
@@ -1959,4 +2121,238 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}(jQuery));
|
/* ── Nutzer ignorieren / Ignorierung aufheben ────────────────────────── */
|
||||||
|
$(document).on('click', '.wbf-ignore-btn', function () {
|
||||||
|
var $btn = $(this);
|
||||||
|
var ignoredId = parseInt($btn.data('id'), 10);
|
||||||
|
var name = $btn.data('name') || 'diesen Nutzer';
|
||||||
|
var isIgnored = String($btn.data('ignored')) === '1';
|
||||||
|
|
||||||
|
// Bestätigung nur beim Ignorieren, nicht beim Entblocken
|
||||||
|
if (!isIgnored) {
|
||||||
|
if (!confirm(name + ' ignorieren?\n\nDessen Beiträge werden in Threads ausgeblendet und DMs werden blockiert.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$btn.prop('disabled', true);
|
||||||
|
|
||||||
|
wbfPost('wbf_toggle_ignore', { ignored_id: ignoredId }, function (d) {
|
||||||
|
var nowIgnored = d.ignored;
|
||||||
|
|
||||||
|
// Alle Buttons mit dieser User-ID auf der Seite aktualisieren
|
||||||
|
$('.wbf-ignore-btn[data-id="' + ignoredId + '"]').each(function () {
|
||||||
|
var $b = $(this);
|
||||||
|
$b.data('ignored', nowIgnored ? '1' : '0');
|
||||||
|
$b.attr('data-ignored', nowIgnored ? '1' : '0');
|
||||||
|
|
||||||
|
// Icon + Label aktualisieren
|
||||||
|
$b.find('i').attr('class', 'fas fa-' + (nowIgnored ? 'eye' : 'eye-slash'));
|
||||||
|
|
||||||
|
// Button-Variante (Post-Footer, klein ohne wbf-btn)
|
||||||
|
if (!$b.hasClass('wbf-btn')) {
|
||||||
|
$b.text('');
|
||||||
|
$b.append('<i class="fas fa-' + (nowIgnored ? 'eye' : 'eye-slash') + '"></i> ' + (nowIgnored ? 'Entblocken' : 'Ignorieren'));
|
||||||
|
} else {
|
||||||
|
// Profil-Variante mit wbf-btn
|
||||||
|
$b.html('<i class="fas fa-' + (nowIgnored ? 'eye' : 'eye-slash') + '"></i> ' + (nowIgnored ? 'Ignorierung aufheben' : 'Nutzer ignorieren'));
|
||||||
|
}
|
||||||
|
$b.prop('disabled', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Posts des Users auf der aktuellen Seite ein-/ausblenden
|
||||||
|
$('.wbf-post, .wbf-post--op').each(function () {
|
||||||
|
var $post = $(this);
|
||||||
|
// Buttons innerhalb dieses Posts mit der User-ID suchen
|
||||||
|
var $ib = $post.find('.wbf-ignore-btn[data-id="' + ignoredId + '"]');
|
||||||
|
if (!$ib.length) return;
|
||||||
|
|
||||||
|
if (nowIgnored) {
|
||||||
|
// Ignoriert → Overlay zeigen wenn noch nicht vorhanden
|
||||||
|
if (!$post.find('.wbf-ignored-bar').length) {
|
||||||
|
var barHtml = '<div class="wbf-ignored-bar">' +
|
||||||
|
'<span><i class="fas fa-eye-slash"></i> Beitrag von ignoriertem Nutzer: <strong>' +
|
||||||
|
$('<span>').text(name).html() + '</strong></span>' +
|
||||||
|
'<button class="wbf-show-ignored-btn" type="button">Trotzdem anzeigen</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="wbf-ignored-content" style="display:none">';
|
||||||
|
$post.addClass('wbf-post--ignored');
|
||||||
|
$post.prepend(barHtml);
|
||||||
|
// Restlichen Inhalt in ignored-content verschieben
|
||||||
|
$post.children(':not(.wbf-ignored-bar):not(.wbf-ignored-content)').wrapAll('<div class="wbf-ignored-content-inner">');
|
||||||
|
$post.find('.wbf-ignored-content').append($post.find('.wbf-ignored-content-inner').children());
|
||||||
|
$post.find('.wbf-ignored-content-inner').remove();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Entblockt → Overlay entfernen
|
||||||
|
var $bar = $post.find('.wbf-ignored-bar');
|
||||||
|
var $content = $post.find('.wbf-ignored-content');
|
||||||
|
if ($bar.length) {
|
||||||
|
// Inhalt wieder nach oben holen
|
||||||
|
$content.children().unwrap();
|
||||||
|
$bar.remove();
|
||||||
|
$post.removeClass('wbf-post--ignored');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ignore-Liste im Profil aktualisieren (falls Nutzer auf eigener Profil-Seite)
|
||||||
|
if (!nowIgnored) {
|
||||||
|
// Eintrag aus der Liste entfernen
|
||||||
|
$('#wbf-ignore-item-' + ignoredId).fadeOut(300, function () {
|
||||||
|
$(this).remove();
|
||||||
|
var remaining = $('#wbfIgnoreList .wbf-ignore-item').length;
|
||||||
|
$('#wbfIgnoreCount').text(remaining);
|
||||||
|
if (remaining === 0) {
|
||||||
|
$('#wbfIgnoreList').replaceWith('<p class="wbf-profile-empty" id="wbfIgnoreEmpty">Du ignorierst niemanden.</p>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast-Meldung
|
||||||
|
var $t = $('<div class="wbf-toast">' + (d.message || (nowIgnored ? name + ' ignoriert.' : 'Ignorierung aufgehoben.')) + '</div>').appendTo('body');
|
||||||
|
setTimeout(function () { $t.remove(); }, 3000);
|
||||||
|
|
||||||
|
}, function () {
|
||||||
|
// Fehler-Callback
|
||||||
|
$('.wbf-ignore-btn[data-id="' + ignoredId + '"]').prop('disabled', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* "Trotzdem anzeigen" — eingeklappten ignorierten Post aufdecken */
|
||||||
|
$(document).on('click', '.wbf-show-ignored-btn', function () {
|
||||||
|
var $bar = $(this).closest('.wbf-ignored-bar');
|
||||||
|
var $content = $bar.next('.wbf-ignored-content');
|
||||||
|
$content.slideDown(200);
|
||||||
|
$bar.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ── Discord-Integration (3-Schritt Verifikation) ─────────────────────────
|
||||||
|
|
||||||
|
var wbfDcStep = 1; // aktueller Schritt
|
||||||
|
|
||||||
|
function wbfDcMsg(text, color) {
|
||||||
|
var $m = $('#wbf-discord-msg');
|
||||||
|
$m.css('color', color || 'var(--c-muted)').html(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wbfDcSetBadge(connected) {
|
||||||
|
var $badge = $('.wbf-connection-card--discord .wbf-connection-badge');
|
||||||
|
if (connected) {
|
||||||
|
$badge.removeClass('wbf-connection-badge--disconnected')
|
||||||
|
.addClass('wbf-connection-badge--connected')
|
||||||
|
.html('<i class="fas fa-check-circle"></i> Verbunden');
|
||||||
|
} else {
|
||||||
|
$badge.removeClass('wbf-connection-badge--connected')
|
||||||
|
.addClass('wbf-connection-badge--disconnected')
|
||||||
|
.html('<i class="fas fa-circle-xmark"></i> Nicht verbunden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schritt 1 → Code senden
|
||||||
|
$(document).on('click', '#wbf-discord-send-code', function () {
|
||||||
|
var username = $('#wbf-discord-input').val().trim();
|
||||||
|
if (!username) { wbfDcMsg('<i class="fas fa-triangle-exclamation"></i> Bitte Benutzername eingeben.', '#f97316'); return; }
|
||||||
|
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Sende…');
|
||||||
|
wbfDcMsg('');
|
||||||
|
$.post(WBF.ajax_url, {
|
||||||
|
action: 'wbf_discord_send_code',
|
||||||
|
nonce: WBF.nonce,
|
||||||
|
discord_username: username,
|
||||||
|
}, function (res) {
|
||||||
|
$btn.prop('disabled', false).html('<i class="fab fa-discord"></i> Code senden');
|
||||||
|
if (res.success) {
|
||||||
|
wbfDcMsg('<i class="fas fa-check" style="color:#16a34a"></i> ' + (res.data.message || 'Code gesendet!'), '#16a34a');
|
||||||
|
$('#wbf-dc-step1').slideUp(200, function () { $('#wbf-dc-step2').slideDown(200); });
|
||||||
|
$('#wbf-discord-code-input').val('').focus();
|
||||||
|
wbfDcStep = 2;
|
||||||
|
} else {
|
||||||
|
wbfDcMsg('<i class="fas fa-circle-xmark"></i> ' + ((res.data && res.data.message) || 'Fehler.'), '#dc2626');
|
||||||
|
}
|
||||||
|
}).fail(function () {
|
||||||
|
$btn.prop('disabled', false).html('<i class="fab fa-discord"></i> Code senden');
|
||||||
|
wbfDcMsg('<i class="fas fa-circle-xmark"></i> Netzwerkfehler.', '#dc2626');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schritt 2 → Code bestätigen
|
||||||
|
$(document).on('click', '#wbf-discord-verify', function () {
|
||||||
|
var code = $('#wbf-discord-code-input').val().trim().toUpperCase();
|
||||||
|
if (code.length < 4) { wbfDcMsg('<i class="fas fa-triangle-exclamation"></i> Bitte Code eingeben.', '#f97316'); return; }
|
||||||
|
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Prüfe…');
|
||||||
|
$.post(WBF.ajax_url, {
|
||||||
|
action: 'wbf_discord_verify_code',
|
||||||
|
nonce: WBF.nonce,
|
||||||
|
verify_code: code,
|
||||||
|
}, function (res) {
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen');
|
||||||
|
if (res.success) {
|
||||||
|
wbfDcMsg('<i class="fas fa-check-circle"></i> ' + (res.data.message || 'Verbunden!'), '#16a34a');
|
||||||
|
wbfDcSetBadge(true);
|
||||||
|
// UI auf "Verbunden"-Ansicht umschalten
|
||||||
|
var name = res.data.display_name || '';
|
||||||
|
$('#wbf-discord-form').slideUp(200);
|
||||||
|
// Verbunden-Info einfügen/aktualisieren
|
||||||
|
var $info = $('.wbf-discord-connected-info');
|
||||||
|
if ($info.length) {
|
||||||
|
$info.find('.wbf-discord-linked-name').html('<i class="fab fa-discord" style="color:#5865f2"></i> ' + $('<span>').text(name).html());
|
||||||
|
} else {
|
||||||
|
// Frisch laden damit die PHP-Struktur stimmt
|
||||||
|
setTimeout(function(){ location.reload(); }, 1200);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wbfDcMsg('<i class="fas fa-circle-xmark"></i> ' + ((res.data && res.data.message) || 'Fehler.'), '#dc2626');
|
||||||
|
}
|
||||||
|
}).fail(function () {
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen');
|
||||||
|
wbfDcMsg('<i class="fas fa-circle-xmark"></i> Netzwerkfehler.', '#dc2626');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter-Taste auf Code-Feld
|
||||||
|
$(document).on('keydown', '#wbf-discord-code-input', function (e) {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); $('#wbf-discord-verify').trigger('click'); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter-Taste auf Username-Feld
|
||||||
|
$(document).on('keydown', '#wbf-discord-input', function (e) {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); $('#wbf-discord-send-code').trigger('click'); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// „Zurück" in Schritt 2
|
||||||
|
$(document).on('click', '#wbf-discord-code-back', function () {
|
||||||
|
$('#wbf-dc-step2').slideUp(200, function () { $('#wbf-dc-step1').slideDown(200); });
|
||||||
|
wbfDcMsg('');
|
||||||
|
wbfDcStep = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// „Neu verknüpfen" bei bereits verbundenem Account
|
||||||
|
$(document).on('click', '#wbf-discord-relink', function () {
|
||||||
|
$('#wbf-discord-form').slideDown(200);
|
||||||
|
$('#wbf-discord-input').val('').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verbindung trennen
|
||||||
|
$(document).on('click', '#wbf-discord-disconnect', function () {
|
||||||
|
if (!confirm('Discord-Verbindung wirklich trennen?')) return;
|
||||||
|
var $btn = $(this).prop('disabled', true);
|
||||||
|
$.post(WBF.ajax_url, {
|
||||||
|
action: 'wbf_save_discord',
|
||||||
|
nonce: WBF.nonce,
|
||||||
|
sub_action: 'disconnect',
|
||||||
|
}, function (res) {
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
if (res.success) {
|
||||||
|
wbfDcMsg('<i class="fas fa-check"></i> ' + (res.data.message || 'Getrennt.'), '#16a34a');
|
||||||
|
wbfDcSetBadge(false);
|
||||||
|
setTimeout(function () { location.reload(); }, 900);
|
||||||
|
} else {
|
||||||
|
wbfDcMsg('<i class="fas fa-circle-xmark"></i> ' + ((res.data && res.data.message) || 'Fehler.'), '#dc2626');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}(jQuery));
|
||||||
|
// Overwrite last line — Discord handlers appended via patch:
|
||||||
@@ -7,7 +7,7 @@ class WBF_Ajax {
|
|||||||
$actions = [
|
$actions = [
|
||||||
'wbf_login', 'wbf_register', 'wbf_logout',
|
'wbf_login', 'wbf_register', 'wbf_logout',
|
||||||
'wbf_new_thread', 'wbf_new_post', 'wbf_toggle_like',
|
'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_forgot_password', 'wbf_reset_password', 'wbf_load_more_messages',
|
||||||
'wbf_create_invite', 'wbf_delete_invite',
|
'wbf_create_invite', 'wbf_delete_invite',
|
||||||
'wbf_toggle_subscribe', 'wbf_restore_content', 'wbf_toggle_profile_visibility',
|
'wbf_toggle_subscribe', 'wbf_restore_content', 'wbf_toggle_profile_visibility',
|
||||||
@@ -18,6 +18,16 @@ class WBF_Ajax {
|
|||||||
'wbf_create_poll',
|
'wbf_create_poll',
|
||||||
'wbf_toggle_bookmark',
|
'wbf_toggle_bookmark',
|
||||||
'wbf_set_thread_prefix',
|
'wbf_set_thread_prefix',
|
||||||
|
'wbf_toggle_ignore',
|
||||||
|
'wbf_change_email',
|
||||||
|
'wbf_save_notification_prefs',
|
||||||
|
'wbf_save_discord',
|
||||||
|
'wbf_discord_send_code',
|
||||||
|
'wbf_discord_verify_code',
|
||||||
|
'wbf_2fa_setup_begin',
|
||||||
|
'wbf_2fa_setup_verify',
|
||||||
|
'wbf_2fa_disable',
|
||||||
|
'wbf_2fa_verify_login',
|
||||||
];
|
];
|
||||||
foreach ($actions as $action) {
|
foreach ($actions as $action) {
|
||||||
add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]);
|
add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]);
|
||||||
@@ -40,23 +50,50 @@ class WBF_Ajax {
|
|||||||
// ── Auth ──────────────────────────────────────────────────────────────────
|
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public static function handle_login() {
|
public static function handle_login() {
|
||||||
|
// Brute-Force-Schutz: max. 10 Versuche pro IP in 15 Minuten
|
||||||
|
$ip_key = 'wbf_login_fail_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
|
||||||
|
$fails = (int) get_transient( $ip_key );
|
||||||
|
if ( $fails >= 10 ) {
|
||||||
|
wp_send_json_error([
|
||||||
|
'message' => 'Zu viele fehlgeschlagene Loginversuche. Bitte warte 15 Minuten.',
|
||||||
|
'locked' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Login braucht keinen Nonce — Credentials sind die Authentifizierung
|
// Login braucht keinen Nonce — Credentials sind die Authentifizierung
|
||||||
$result = WBF_Auth::login(
|
$result = WBF_Auth::login(
|
||||||
sanitize_text_field($_POST['username'] ?? ''),
|
sanitize_text_field($_POST['username'] ?? ''),
|
||||||
$_POST['password'] ?? ''
|
$_POST['password'] ?? '',
|
||||||
|
! empty($_POST['remember_me'])
|
||||||
);
|
);
|
||||||
if ($result['success']) {
|
if ($result['success']) {
|
||||||
|
// Erfolgreicher Login: Fehlzähler löschen
|
||||||
|
delete_transient( $ip_key );
|
||||||
$u = $result['user'];
|
$u = $result['user'];
|
||||||
if ( ! empty($_POST['remember_me']) ) {
|
if ( ! empty($_POST['remember_me']) ) {
|
||||||
WBF_Auth::set_remember_cookie($u->id);
|
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]);
|
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 {
|
} else {
|
||||||
|
// Fehlversuch zählen — außer bei gesperrten Konten (kein Passwortfehler)
|
||||||
|
if ( empty($result['banned']) ) {
|
||||||
|
set_transient( $ip_key, $fails + 1, 15 * MINUTE_IN_SECONDS );
|
||||||
|
}
|
||||||
wp_send_json_error($result);
|
wp_send_json_error($result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function handle_register() {
|
public static function handle_register() {
|
||||||
|
// Brute-Force/Spam-Schutz: max. 5 Registrierungen pro IP pro Stunde
|
||||||
|
$reg_ip_key = 'wbf_reg_ip_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
|
||||||
|
$reg_fails = (int) get_transient( $reg_ip_key );
|
||||||
|
if ( $reg_fails >= 5 ) {
|
||||||
|
wp_send_json_error(['message' => 'Zu viele Registrierungsversuche von dieser IP. Bitte warte eine Stunde.']);
|
||||||
|
}
|
||||||
|
|
||||||
// Spam-Schutz: Honeypot + Zeitlimit
|
// Spam-Schutz: Honeypot + Zeitlimit
|
||||||
if ( ! empty($_POST['wbf_website']) ) {
|
if ( ! empty($_POST['wbf_website']) ) {
|
||||||
wp_send_json_error(['message' => 'Spam erkannt.']);
|
wp_send_json_error(['message' => 'Spam erkannt.']);
|
||||||
@@ -95,6 +132,8 @@ class WBF_Ajax {
|
|||||||
sanitize_text_field($_POST['display_name'] ?? '')
|
sanitize_text_field($_POST['display_name'] ?? '')
|
||||||
);
|
);
|
||||||
if ($result['success']) {
|
if ($result['success']) {
|
||||||
|
// Registrierungs-Zähler für IP erhöhen
|
||||||
|
set_transient( $reg_ip_key, $reg_fails + 1, HOUR_IN_SECONDS );
|
||||||
$u = $result['user'];
|
$u = $result['user'];
|
||||||
// Einladungscode einlösen
|
// Einladungscode einlösen
|
||||||
$reg_mode2 = wbf_get_settings()['registration_mode'] ?? 'open';
|
$reg_mode2 = wbf_get_settings()['registration_mode'] ?? 'open';
|
||||||
@@ -109,7 +148,10 @@ class WBF_Ajax {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function handle_logout() {
|
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();
|
WBF_Auth::logout();
|
||||||
wp_send_json_success(['message' => 'logged_out']);
|
wp_send_json_success(['message' => 'logged_out']);
|
||||||
}
|
}
|
||||||
@@ -157,6 +199,10 @@ class WBF_Ajax {
|
|||||||
'content' => WBF_DB::apply_word_filter($content),
|
'content' => WBF_DB::apply_word_filter($content),
|
||||||
'prefix_id' => $prefix_id,
|
'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
|
// Tags speichern
|
||||||
$raw_tags = sanitize_text_field( $_POST['tags'] ?? '' );
|
$raw_tags = sanitize_text_field( $_POST['tags'] ?? '' );
|
||||||
@@ -223,9 +269,11 @@ class WBF_Ajax {
|
|||||||
}
|
}
|
||||||
// Thread-Abonnenten benachrichtigen
|
// Thread-Abonnenten benachrichtigen
|
||||||
$subscribers = WBF_DB::get_thread_subscribers($thread_id);
|
$subscribers = WBF_DB::get_thread_subscribers($thread_id);
|
||||||
|
// $notif_users is a flat array of IDs (from get_col) — cast to int for comparison
|
||||||
|
$notif_ids = array_map('intval', $notif_users);
|
||||||
foreach ($subscribers as $sub) {
|
foreach ($subscribers as $sub) {
|
||||||
if ((int)$sub->id === (int)$user->id) continue; // nicht sich selbst
|
if ((int)$sub->id === (int)$user->id) continue; // nicht sich selbst
|
||||||
if (in_array($sub->id, array_column($notif_users, 'id') ?: [])) continue; // schon benachrichtigt
|
if (in_array((int)$sub->id, $notif_ids, true)) continue; // schon benachrichtigt
|
||||||
self::send_notification_email($sub, 'reply', $user->display_name, [
|
self::send_notification_email($sub, 'reply', $user->display_name, [
|
||||||
'thread_id' => $thread_id,
|
'thread_id' => $thread_id,
|
||||||
'thread_title' => $thread->title,
|
'thread_title' => $thread->title,
|
||||||
@@ -369,6 +417,19 @@ class WBF_Ajax {
|
|||||||
|
|
||||||
if (!empty($_POST['new_password'])) {
|
if (!empty($_POST['new_password'])) {
|
||||||
if (strlen($_POST['new_password']) < 6) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']);
|
if (strlen($_POST['new_password']) < 6) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']);
|
||||||
|
// Sicherheit: aktuelles Passwort muss zur Bestätigung angegeben werden
|
||||||
|
$current_pw = $_POST['current_password'] ?? '';
|
||||||
|
if ( empty($current_pw) ) {
|
||||||
|
wp_send_json_error(['message'=>'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
|
||||||
|
}
|
||||||
|
if ( ! password_verify($current_pw, $user->password) ) {
|
||||||
|
wp_send_json_error(['message'=>'Aktuelles Passwort ist falsch.']);
|
||||||
|
}
|
||||||
|
// Bestätigungsfeld server-seitig prüfen
|
||||||
|
$new_pw2 = $_POST['new_password2'] ?? '';
|
||||||
|
if ( ! empty($new_pw2) && $new_pw2 !== $_POST['new_password'] ) {
|
||||||
|
wp_send_json_error(['message'=>'Die Passwörter stimmen nicht überein.']);
|
||||||
|
}
|
||||||
$update['password'] = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
|
$update['password'] = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,6 +455,15 @@ class WBF_Ajax {
|
|||||||
$value = sanitize_textarea_field( $raw );
|
$value = sanitize_textarea_field( $raw );
|
||||||
} elseif ( $def['type'] === 'number' ) {
|
} elseif ( $def['type'] === 'number' ) {
|
||||||
$value = is_numeric($raw) ? (string)(float)$raw : '';
|
$value = is_numeric($raw) ? (string)(float)$raw : '';
|
||||||
|
} elseif ( $def['type'] === 'date' ) {
|
||||||
|
// Datum validieren — nur YYYY-MM-DD, nicht in der Zukunft
|
||||||
|
$raw_date = sanitize_text_field( trim($raw) );
|
||||||
|
if ( preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw_date) ) {
|
||||||
|
$ts = strtotime($raw_date);
|
||||||
|
$value = ($ts && $ts <= time()) ? $raw_date : '';
|
||||||
|
} else {
|
||||||
|
$value = '';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$value = sanitize_text_field( $raw );
|
$value = sanitize_text_field( $raw );
|
||||||
}
|
}
|
||||||
@@ -411,9 +481,37 @@ class WBF_Ajax {
|
|||||||
if (empty($_FILES['avatar'])) wp_send_json_error(['message'=>'Keine Datei.']);
|
if (empty($_FILES['avatar'])) wp_send_json_error(['message'=>'Keine Datei.']);
|
||||||
|
|
||||||
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
|
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
|
||||||
$mime = $_FILES['avatar']['type'] ?? '';
|
|
||||||
if (!in_array($mime, $allowed_types)) wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
// Dateigröße vor dem MIME-Check prüfen
|
||||||
if ($_FILES['avatar']['size'] > 2 * 1024 * 1024) wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
|
if ( $_FILES['avatar']['size'] > 2 * 1024 * 1024 ) {
|
||||||
|
wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] kommt vom Client
|
||||||
|
// und ist beliebig fälschbar (z.B. PHP-Datei als image/jpeg getarnt).
|
||||||
|
// finfo_file() liest den echten Magic-Byte der temporären Datei.
|
||||||
|
$tmp = $_FILES['avatar']['tmp_name'] ?? '';
|
||||||
|
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
|
||||||
|
wp_send_json_error(['message'=>'Ungültiger Datei-Upload.']);
|
||||||
|
}
|
||||||
|
if ( function_exists('finfo_open') ) {
|
||||||
|
$finfo = finfo_open( FILEINFO_MIME_TYPE );
|
||||||
|
$real_mime = finfo_file( $finfo, $tmp );
|
||||||
|
finfo_close( $finfo );
|
||||||
|
} else {
|
||||||
|
// Fallback: exif_imagetype() wenn finfo nicht verfügbar
|
||||||
|
$et_map = [
|
||||||
|
IMAGETYPE_JPEG => 'image/jpeg',
|
||||||
|
IMAGETYPE_PNG => 'image/png',
|
||||||
|
IMAGETYPE_GIF => 'image/gif',
|
||||||
|
IMAGETYPE_WEBP => 'image/webp',
|
||||||
|
];
|
||||||
|
$et = @exif_imagetype( $tmp );
|
||||||
|
$real_mime = $et_map[$et] ?? '';
|
||||||
|
}
|
||||||
|
if ( ! in_array( $real_mime, $allowed_types, true ) ) {
|
||||||
|
wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
||||||
|
}
|
||||||
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||||
@@ -427,6 +525,52 @@ class WBF_Ajax {
|
|||||||
wp_send_json_success(['avatar_url'=>$url]);
|
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 ────────────────────────────────────────────────────────────────
|
// ── Report ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public static function handle_report_post() {
|
public static function handle_report_post() {
|
||||||
@@ -468,16 +612,36 @@ class WBF_Ajax {
|
|||||||
|
|
||||||
// Nur Bilder erlauben
|
// Nur Bilder erlauben
|
||||||
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
|
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
|
||||||
$mime = $_FILES['image']['type'] ?? '';
|
|
||||||
if ( ! in_array($mime, $allowed_types) ) {
|
|
||||||
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max 5 MB
|
// Max 5 MB — Größe zuerst prüfen bevor teure MIME-Erkennung läuft
|
||||||
if ( $_FILES['image']['size'] > 5 * 1024 * 1024 ) {
|
if ( $_FILES['image']['size'] > 5 * 1024 * 1024 ) {
|
||||||
wp_send_json_error(['message' => 'Maximale Dateigröße: 5 MB.']);
|
wp_send_json_error(['message' => 'Maximale Dateigröße: 5 MB.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] ist client-kontrolliert
|
||||||
|
// und kann beliebig auf 'image/jpeg' gesetzt werden, auch für .php-Dateien.
|
||||||
|
$tmp = $_FILES['image']['tmp_name'] ?? '';
|
||||||
|
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
|
||||||
|
wp_send_json_error(['message' => 'Ungültiger Datei-Upload.']);
|
||||||
|
}
|
||||||
|
if ( function_exists('finfo_open') ) {
|
||||||
|
$finfo = finfo_open( FILEINFO_MIME_TYPE );
|
||||||
|
$real_mime = finfo_file( $finfo, $tmp );
|
||||||
|
finfo_close( $finfo );
|
||||||
|
} else {
|
||||||
|
$et_map = [
|
||||||
|
IMAGETYPE_JPEG => 'image/jpeg',
|
||||||
|
IMAGETYPE_PNG => 'image/png',
|
||||||
|
IMAGETYPE_GIF => 'image/gif',
|
||||||
|
IMAGETYPE_WEBP => 'image/webp',
|
||||||
|
];
|
||||||
|
$et = @exif_imagetype( $tmp );
|
||||||
|
$real_mime = $et_map[$et] ?? '';
|
||||||
|
}
|
||||||
|
if ( ! in_array( $real_mime, $allowed_types, true ) ) {
|
||||||
|
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
||||||
|
}
|
||||||
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||||
@@ -543,7 +707,8 @@ class WBF_Ajax {
|
|||||||
self::verify();
|
self::verify();
|
||||||
$query = sanitize_text_field( $_POST['query'] ?? '' );
|
$query = sanitize_text_field( $_POST['query'] ?? '' );
|
||||||
if ( mb_strlen( $query ) < 2 ) wp_send_json_error(['message' => 'Suchbegriff zu kurz.']);
|
if ( mb_strlen( $query ) < 2 ) wp_send_json_error(['message' => 'Suchbegriff zu kurz.']);
|
||||||
$results = WBF_DB::search( $query, 40 );
|
$current_search = WBF_Auth::get_current_user();
|
||||||
|
$results = WBF_DB::search( $query, 40, $current_search );
|
||||||
wp_send_json_success(['results' => $results, 'query' => $query]);
|
wp_send_json_success(['results' => $results, 'query' => $query]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,6 +753,18 @@ class WBF_Ajax {
|
|||||||
$is_mod = WBF_DB::can($user, 'delete_post');
|
$is_mod = WBF_DB::can($user, 'delete_post');
|
||||||
if ( ! $is_own && ! $is_mod ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
|
if ( ! $is_own && ! $is_mod ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
|
||||||
|
|
||||||
|
// Post-Bearbeitungslimit prüfen — gilt auch für Thread-Erstbeiträge
|
||||||
|
// (spiegelt identisches Verhalten zu handle_edit_post() wider)
|
||||||
|
if ( $is_own && ! $is_mod ) {
|
||||||
|
$limit_min = (int)( wbf_get_settings()['post_edit_limit'] ?? 30 );
|
||||||
|
if ( $limit_min > 0 ) {
|
||||||
|
$age_min = ( time() - strtotime( $thread->created_at ) ) / 60;
|
||||||
|
if ( $age_min > $limit_min ) {
|
||||||
|
wp_send_json_error(['message' => "Bearbeitung nur innerhalb von {$limit_min} Minuten nach dem Erstellen möglich."]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$wpdb->update(
|
$wpdb->update(
|
||||||
"{$wpdb->prefix}forum_threads",
|
"{$wpdb->prefix}forum_threads",
|
||||||
@@ -682,6 +859,11 @@ class WBF_Ajax {
|
|||||||
if ($to_id === (int)$user->id) wp_send_json_error(['message'=>'Du kannst dir nicht selbst schreiben.']);
|
if ($to_id === (int)$user->id) wp_send_json_error(['message'=>'Du kannst dir nicht selbst schreiben.']);
|
||||||
if (!WBF_DB::get_user($to_id)) wp_send_json_error(['message'=>'Empfänger nicht gefunden.']);
|
if (!WBF_DB::get_user($to_id)) wp_send_json_error(['message'=>'Empfänger nicht gefunden.']);
|
||||||
|
|
||||||
|
// DM-Blockierung: Empfänger hat Sender ignoriert
|
||||||
|
if ( WBF_DB::is_ignored( $to_id, $user->id ) ) {
|
||||||
|
wp_send_json_error(['message' => 'Diese Person akzeptiert keine Nachrichten von dir.']);
|
||||||
|
}
|
||||||
|
|
||||||
$id = WBF_DB::send_message($user->id, $to_id, $content);
|
$id = WBF_DB::send_message($user->id, $to_id, $content);
|
||||||
// Notify recipient
|
// Notify recipient
|
||||||
WBF_DB::create_notification($to_id, 'message', $id, $user->id);
|
WBF_DB::create_notification($to_id, 'message', $id, $user->id);
|
||||||
@@ -739,14 +921,22 @@ class WBF_Ajax {
|
|||||||
// ── User-Autocomplete (für @Erwähnungen + DM) ─────────────────────────────
|
// ── User-Autocomplete (für @Erwähnungen + DM) ─────────────────────────────
|
||||||
|
|
||||||
public static function handle_user_suggest() {
|
public static function handle_user_suggest() {
|
||||||
|
// Nur eingeloggte Nutzer dürfen die User-Suche nutzen
|
||||||
|
// (verhindert Enumeration aller Usernamen + Rollendaten durch Gäste)
|
||||||
|
if ( ! WBF_Auth::is_forum_logged_in() ) {
|
||||||
|
wp_send_json_success(['users'=>[]]);
|
||||||
|
}
|
||||||
$q = sanitize_text_field($_POST['q'] ?? $_GET['q'] ?? '');
|
$q = sanitize_text_field($_POST['q'] ?? $_GET['q'] ?? '');
|
||||||
if (mb_strlen($q) < 1) wp_send_json_success(['users'=>[]]);
|
if (mb_strlen($q) < 1) wp_send_json_success(['users'=>[]]);
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$like = $wpdb->esc_like($q) . '%';
|
$like = $wpdb->esc_like($q) . '%';
|
||||||
|
// Rolle wird bewusst NICHT zurückgegeben — nicht für Autocomplete nötig
|
||||||
|
// und verhindert Informationsleck über Rollen-Verteilung im Forum.
|
||||||
$users = $wpdb->get_results($wpdb->prepare(
|
$users = $wpdb->get_results($wpdb->prepare(
|
||||||
"SELECT id, username, display_name, avatar_url, role
|
"SELECT id, username, display_name, avatar_url
|
||||||
FROM {$wpdb->prefix}forum_users
|
FROM {$wpdb->prefix}forum_users
|
||||||
WHERE username LIKE %s OR display_name LIKE %s
|
WHERE (username LIKE %s OR display_name LIKE %s)
|
||||||
|
AND role != 'banned'
|
||||||
ORDER BY display_name ASC LIMIT 8",
|
ORDER BY display_name ASC LIMIT 8",
|
||||||
$like, $like
|
$like, $like
|
||||||
));
|
));
|
||||||
@@ -817,6 +1007,13 @@ class WBF_Ajax {
|
|||||||
private static function send_notification_email( $to_user, $type, $actor_name, $extra = [] ) {
|
private static function send_notification_email( $to_user, $type, $actor_name, $extra = [] ) {
|
||||||
if ( ! $to_user || empty($to_user->email) ) return;
|
if ( ! $to_user || empty($to_user->email) ) return;
|
||||||
|
|
||||||
|
// Prüfen ob der User diesen Benachrichtigungstyp aktiviert hat
|
||||||
|
// Standard: alle aktiviert (1). User kann im Profil deaktivieren (0).
|
||||||
|
$pref_key = 'notify_' . $type; // notify_reply, notify_mention, notify_message
|
||||||
|
$meta = WBF_DB::get_user_meta( $to_user->id );
|
||||||
|
// Nur deaktivieren wenn explizit auf '0' gesetzt — Standard ist aktiviert
|
||||||
|
if ( isset($meta[$pref_key]) && $meta[$pref_key] === '0' ) return;
|
||||||
|
|
||||||
$blog_name = get_bloginfo('name');
|
$blog_name = get_bloginfo('name');
|
||||||
$forum_url = wbf_get_forum_url();
|
$forum_url = wbf_get_forum_url();
|
||||||
$from_email = get_option('admin_email');
|
$from_email = get_option('admin_email');
|
||||||
@@ -897,6 +1094,17 @@ class WBF_Ajax {
|
|||||||
$email = sanitize_email( $_POST['email'] ?? '' );
|
$email = sanitize_email( $_POST['email'] ?? '' );
|
||||||
if ( ! is_email($email) ) wp_send_json_error(['message'=>'Ungültige E-Mail-Adresse.']);
|
if ( ! is_email($email) ) wp_send_json_error(['message'=>'Ungültige E-Mail-Adresse.']);
|
||||||
|
|
||||||
|
// ── Rate-Limiting: max. 1 Reset-Mail pro E-Mail-Adresse alle 15 Minuten ──
|
||||||
|
// Verhindert, dass ein Angreifer tausende Reset-Mails pro Sekunde
|
||||||
|
// für beliebige Adressen triggert und den Mail-Server überlastet.
|
||||||
|
$rate_key = 'wbf_pwreset_' . md5( strtolower( $email ) );
|
||||||
|
if ( get_transient( $rate_key ) !== false ) {
|
||||||
|
// Immer Erfolg melden — kein Leak ob Rate-Limit oder kein Account
|
||||||
|
wp_send_json_success(['message'=>'Falls diese E-Mail registriert ist, wurde eine E-Mail gesendet.']);
|
||||||
|
}
|
||||||
|
// Cooldown setzen — 15 Minuten
|
||||||
|
set_transient( $rate_key, 1, 15 * MINUTE_IN_SECONDS );
|
||||||
|
|
||||||
$user = WBF_DB::get_user_by('email', $email);
|
$user = WBF_DB::get_user_by('email', $email);
|
||||||
// Immer Erfolg melden (kein User-Enumeration)
|
// Immer Erfolg melden (kein User-Enumeration)
|
||||||
if ( ! $user ) {
|
if ( ! $user ) {
|
||||||
@@ -922,7 +1130,13 @@ class WBF_Ajax {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function handle_reset_password() {
|
public static function handle_reset_password() {
|
||||||
self::verify();
|
// Kein self::verify() hier — Gäste haben keine Forum-Session.
|
||||||
|
// Das Reset-Token selbst authentifiziert die Anfrage.
|
||||||
|
// Wir prüfen trotzdem den WP-Nonce als CSRF-Schutz; dieser wird
|
||||||
|
// von wp_localize_script für alle Besucher (auch Gäste) generiert.
|
||||||
|
if ( ! check_ajax_referer( 'wbf_nonce', 'nonce', false ) ) {
|
||||||
|
wp_send_json_error(['message' => 'Sicherheitsfehler.']);
|
||||||
|
}
|
||||||
$token = sanitize_text_field( $_POST['token'] ?? '' );
|
$token = sanitize_text_field( $_POST['token'] ?? '' );
|
||||||
$password = $_POST['password'] ?? '';
|
$password = $_POST['password'] ?? '';
|
||||||
$password2= $_POST['password2'] ?? '';
|
$password2= $_POST['password2'] ?? '';
|
||||||
@@ -1041,6 +1255,12 @@ class WBF_Ajax {
|
|||||||
self::verify();
|
self::verify();
|
||||||
$user = WBF_Auth::get_current_user();
|
$user = WBF_Auth::get_current_user();
|
||||||
if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
|
if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
|
||||||
|
// Sicherstellen dass Spalte existiert (Schutz für bestehende Installs)
|
||||||
|
global $wpdb;
|
||||||
|
$cols = $wpdb->get_col( "DESCRIBE {$wpdb->prefix}forum_users" );
|
||||||
|
if ( ! in_array( 'profile_public', $cols ) ) {
|
||||||
|
$wpdb->query( "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) NOT NULL DEFAULT 1" );
|
||||||
|
}
|
||||||
$current = (int)($user->profile_public ?? 1);
|
$current = (int)($user->profile_public ?? 1);
|
||||||
$new = $current ? 0 : 1;
|
$new = $current ? 0 : 1;
|
||||||
WBF_DB::update_user($user->id, ['profile_public'=>$new]);
|
WBF_DB::update_user($user->id, ['profile_public'=>$new]);
|
||||||
@@ -1207,6 +1427,403 @@ class WBF_Ajax {
|
|||||||
wp_send_json_success(['prefix' => $prefix]);
|
wp_send_json_success(['prefix' => $prefix]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── E-Mail-Adresse ändern ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static function handle_change_email() {
|
||||||
|
self::verify();
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||||
|
|
||||||
|
$new_email = sanitize_email( $_POST['new_email'] ?? '' );
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
if ( ! is_email($new_email) ) {
|
||||||
|
wp_send_json_error(['message' => 'Ungültige E-Mail-Adresse.']);
|
||||||
|
}
|
||||||
|
if ( empty($password) ) {
|
||||||
|
wp_send_json_error(['message' => 'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
|
||||||
|
}
|
||||||
|
if ( ! password_verify($password, $user->password) ) {
|
||||||
|
wp_send_json_error(['message' => 'Falsches Passwort.']);
|
||||||
|
}
|
||||||
|
if ( strtolower($new_email) === strtolower($user->email) ) {
|
||||||
|
wp_send_json_error(['message' => 'Das ist bereits deine aktuelle E-Mail-Adresse.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob E-Mail bereits vergeben
|
||||||
|
$existing = WBF_DB::get_user_by('email', $new_email);
|
||||||
|
if ( $existing && (int)$existing->id !== (int)$user->id ) {
|
||||||
|
wp_send_json_error(['message' => 'Diese E-Mail-Adresse ist bereits registriert.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
WBF_DB::update_user($user->id, ['email' => $new_email]);
|
||||||
|
wp_send_json_success(['message' => 'E-Mail-Adresse erfolgreich geändert.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Benachrichtigungs-Einstellungen speichern ─────────────────────────────
|
||||||
|
|
||||||
|
public static function handle_save_notification_prefs() {
|
||||||
|
self::verify();
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||||
|
|
||||||
|
$allowed = ['notify_reply', 'notify_mention', 'notify_message'];
|
||||||
|
foreach ( $allowed as $key ) {
|
||||||
|
// 1 wenn Checkbox aktiviert, 0 wenn deaktiviert
|
||||||
|
$val = isset($_POST[$key]) && $_POST[$key] === '1' ? '1' : '0';
|
||||||
|
WBF_DB::set_user_meta($user->id, $key, $val);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(['message' => 'Benachrichtigungs-Einstellungen gespeichert.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── User ignorieren / Ignorierung aufheben ────────────────────────────────
|
||||||
|
|
||||||
|
public static function handle_toggle_ignore() {
|
||||||
|
self::verify();
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) wp_send_json_error( ['message' => 'Nicht eingeloggt.'] );
|
||||||
|
|
||||||
|
$ignored_id = (int)( $_POST['ignored_id'] ?? 0 );
|
||||||
|
if ( ! $ignored_id ) wp_send_json_error( ['message' => 'Ungültiger Nutzer.'] );
|
||||||
|
if ( $ignored_id === (int)$user->id ) {
|
||||||
|
wp_send_json_error( ['message' => 'Du kannst dich nicht selbst ignorieren.'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = WBF_DB::get_user( $ignored_id );
|
||||||
|
if ( ! $target ) wp_send_json_error( ['message' => 'Nutzer nicht gefunden.'] );
|
||||||
|
|
||||||
|
// Prüfen ob diese Rolle geblockt werden darf (konfigurierbar in den Einstellungen)
|
||||||
|
if ( ! wbf_can_be_ignored( $target ) ) {
|
||||||
|
$role_label = WBF_Roles::get($target->role)['label'] ?? $target->role;
|
||||||
|
wp_send_json_error( ['message' => 'Nutzer mit der Rolle "' . $role_label . '" können nicht ignoriert werden.'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$now_ignored = WBF_DB::toggle_ignore( $user->id, $ignored_id );
|
||||||
|
|
||||||
|
wp_send_json_success( [
|
||||||
|
'ignored' => $now_ignored,
|
||||||
|
'ignored_id' => $ignored_id,
|
||||||
|
'display_name' => $target->display_name,
|
||||||
|
'message' => $now_ignored
|
||||||
|
? esc_html( $target->display_name ) . ' wird jetzt ignoriert.'
|
||||||
|
: 'Ignorierung von ' . esc_html( $target->display_name ) . ' aufgehoben.',
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Discord: Verifikations-Code per Bot-DM senden ─────────────────────────
|
||||||
|
|
||||||
|
public static function handle_discord_send_code() {
|
||||||
|
self::verify();
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||||
|
|
||||||
|
$s = wbf_get_settings();
|
||||||
|
$token = trim($s['discord_bot_token'] ?? '');
|
||||||
|
$guild = trim($s['discord_guild_id'] ?? '');
|
||||||
|
|
||||||
|
if ( ! $token ) {
|
||||||
|
wp_send_json_error(['message' => 'Discord-Bot ist noch nicht konfiguriert. Bitte wende dich an einen Admin.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username_input = sanitize_text_field($_POST['discord_username'] ?? '');
|
||||||
|
if ( ! $username_input ) {
|
||||||
|
wp_send_json_error(['message' => 'Bitte Discord-Benutzername eingeben.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nutzer auf dem Guild suchen (nach Username oder per Search)
|
||||||
|
$discord_user_id = self::discord_find_user_id($username_input, $token, $guild);
|
||||||
|
if ( ! $discord_user_id ) {
|
||||||
|
wp_send_json_error(['message' => 'Discord-Nutzer nicht auf dem Server gefunden. Stelle sicher, dass du Mitglied des Servers bist.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifikations-Code generieren (6-stellig)
|
||||||
|
$code = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 6));
|
||||||
|
$expires = time() + 600; // 10 Minuten
|
||||||
|
|
||||||
|
// Code + Discord-User-ID temporär speichern
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_verify_code', $code);
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_verify_expires', (string)$expires);
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_verify_pending_id', $discord_user_id);
|
||||||
|
|
||||||
|
// DM senden
|
||||||
|
$sent = self::discord_send_dm($discord_user_id, $token,
|
||||||
|
"🔐 **Dein Verifikationscode für " . get_bloginfo('name') . ":**\n\n" .
|
||||||
|
"```" . $code . "```\n" .
|
||||||
|
"Gib diesen Code im Forum ein. Er ist **10 Minuten** gültig.\n" .
|
||||||
|
"_Falls du diese Nachricht nicht erwartet hast, ignoriere sie einfach._"
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! $sent ) {
|
||||||
|
wp_send_json_error(['message' => 'DM konnte nicht gesendet werden. Stelle sicher, dass du DMs von Server-Mitgliedern zulässt.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(['message' => '✅ Code gesendet! Prüfe deine Discord-DMs und gib den 6-stelligen Code ein.', 'step' => 'enter_code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Discord: Code überprüfen + Verbindung herstellen ─────────────────────
|
||||||
|
|
||||||
|
public static function handle_discord_verify_code() {
|
||||||
|
self::verify();
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||||
|
|
||||||
|
$code_input = strtoupper(sanitize_text_field($_POST['verify_code'] ?? ''));
|
||||||
|
$meta = WBF_DB::get_user_meta($user->id);
|
||||||
|
|
||||||
|
$stored_code = strtoupper($meta['discord_verify_code'] ?? '');
|
||||||
|
$expires = (int)($meta['discord_verify_expires'] ?? 0);
|
||||||
|
$discord_uid = $meta['discord_verify_pending_id'] ?? '';
|
||||||
|
|
||||||
|
if ( ! $stored_code || ! $discord_uid ) {
|
||||||
|
wp_send_json_error(['message' => 'Kein offener Verifizierungs-Vorgang. Bitte erneut starten.']);
|
||||||
|
}
|
||||||
|
if ( time() > $expires ) {
|
||||||
|
wp_send_json_error(['message' => 'Code abgelaufen. Bitte erneut einen Code anfordern.']);
|
||||||
|
}
|
||||||
|
if ( ! hash_equals($stored_code, $code_input) ) {
|
||||||
|
wp_send_json_error(['message' => 'Falscher Code. Bitte erneut versuchen.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discord-Username abrufen (für Anzeige)
|
||||||
|
$s = wbf_get_settings();
|
||||||
|
$token = trim($s['discord_bot_token'] ?? '');
|
||||||
|
$display_name = $discord_uid;
|
||||||
|
if ( $token ) {
|
||||||
|
$res = wp_remote_get("https://discord.com/api/v10/users/{$discord_uid}", [
|
||||||
|
'timeout' => 5,
|
||||||
|
'headers' => ['Authorization' => 'Bot ' . $token],
|
||||||
|
]);
|
||||||
|
if ( ! is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200 ) {
|
||||||
|
$d = json_decode(wp_remote_retrieve_body($res), true);
|
||||||
|
$display_name = $d['global_name'] ?? $d['username'] ?? $discord_uid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichern
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_user_id', $discord_uid);
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_username', $display_name);
|
||||||
|
// Temp-Daten löschen
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_verify_code', '');
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_verify_expires', '');
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_verify_pending_id', '');
|
||||||
|
|
||||||
|
// Rollen-Sync direkt nach Verifikation
|
||||||
|
$guild = trim($s['discord_guild_id'] ?? '');
|
||||||
|
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
|
||||||
|
if ( ($s['discord_role_sync'] ?? '0') === '1' && $token && $guild && $role_map ) {
|
||||||
|
wbf_sync_discord_role_for_user($user->id, $discord_uid, $token, $guild, $role_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success([
|
||||||
|
'message' => '🎉 Discord erfolgreich verknüpft!',
|
||||||
|
'connected' => true,
|
||||||
|
'display_name' => esc_html($display_name),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Discord: Verbindung trennen ───────────────────────────────────────────
|
||||||
|
|
||||||
|
public static function handle_save_discord() {
|
||||||
|
self::verify();
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||||
|
|
||||||
|
$action = sanitize_key( $_POST['sub_action'] ?? 'save' );
|
||||||
|
|
||||||
|
if ( $action === 'disconnect' ) {
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_username', '');
|
||||||
|
WBF_DB::set_user_meta($user->id, 'discord_user_id', '');
|
||||||
|
wp_send_json_success(['message' => 'Discord-Verbindung getrennt.', 'connected' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_error(['message' => 'Unbekannte Aktion.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Discord Hilfsmethoden ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static function discord_find_user_id($username_input, $token, $guild) {
|
||||||
|
if ( ! $guild ) return null;
|
||||||
|
// Guild-Member-Search (max. 1 Treffer)
|
||||||
|
$search = rawurlencode($username_input);
|
||||||
|
$res = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}/members/search?query={$search}&limit=5", [
|
||||||
|
'timeout' => 6,
|
||||||
|
'headers' => ['Authorization' => 'Bot ' . $token],
|
||||||
|
]);
|
||||||
|
if ( is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200 ) return null;
|
||||||
|
$members = json_decode(wp_remote_retrieve_body($res), true);
|
||||||
|
if ( empty($members) ) return null;
|
||||||
|
// Exakten Treffer bevorzugen
|
||||||
|
$input_lower = strtolower($username_input);
|
||||||
|
foreach ( $members as $m ) {
|
||||||
|
$uname = strtolower($m['user']['username'] ?? '');
|
||||||
|
$global = strtolower($m['user']['global_name'] ?? '');
|
||||||
|
if ( $uname === $input_lower || $global === $input_lower ) {
|
||||||
|
return $m['user']['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Erster Treffer als Fallback
|
||||||
|
return $members[0]['user']['id'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function discord_send_dm($user_id, $token, $message) {
|
||||||
|
// DM-Channel erstellen
|
||||||
|
$ch_res = wp_remote_post('https://discord.com/api/v10/users/@me/channels', [
|
||||||
|
'timeout' => 6,
|
||||||
|
'headers' => ['Authorization' => 'Bot ' . $token, 'Content-Type' => 'application/json'],
|
||||||
|
'body' => json_encode(['recipient_id' => $user_id]),
|
||||||
|
]);
|
||||||
|
if ( is_wp_error($ch_res) || wp_remote_retrieve_response_code($ch_res) !== 200 ) return false;
|
||||||
|
$channel = json_decode(wp_remote_retrieve_body($ch_res), true);
|
||||||
|
$ch_id = $channel['id'] ?? '';
|
||||||
|
if ( ! $ch_id ) return false;
|
||||||
|
|
||||||
|
// Nachricht senden
|
||||||
|
$msg_res = wp_remote_post("https://discord.com/api/v10/channels/{$ch_id}/messages", [
|
||||||
|
'timeout' => 6,
|
||||||
|
'headers' => ['Authorization' => 'Bot ' . $token, 'Content-Type' => 'application/json'],
|
||||||
|
'body' => json_encode(['content' => $message]),
|
||||||
|
]);
|
||||||
|
return ( ! is_wp_error($msg_res) && wp_remote_retrieve_response_code($msg_res) === 200 );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// ── 2FA / TOTP ────────────────────────────────────────────────────────────
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup-Schritt 1: Neues Secret generieren und als "pending" speichern.
|
||||||
|
* Gibt Secret (zur manuellen Eingabe) und otpauth:// URI (für QR-Code) zurück.
|
||||||
|
*/
|
||||||
|
public static function handle_2fa_setup_begin() {
|
||||||
|
self::verify();
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
|
||||||
|
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
|
||||||
|
|
||||||
|
$secret = WBF_TOTP::generate_secret();
|
||||||
|
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_PENDING, $secret );
|
||||||
|
|
||||||
|
wp_send_json_success( [
|
||||||
|
"secret" => $secret,
|
||||||
|
"uri" => WBF_TOTP::get_otpauth_uri( $user->username, $secret ),
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup-Schritt 2: Code verifizieren und 2FA aktivieren.
|
||||||
|
*/
|
||||||
|
public static function handle_2fa_setup_verify() {
|
||||||
|
self::verify();
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
|
||||||
|
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
|
||||||
|
|
||||||
|
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
|
||||||
|
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_PENDING );
|
||||||
|
|
||||||
|
if ( empty($secret) ) {
|
||||||
|
wp_send_json_error( ["message" => "Kein ausstehender 2FA-Setup. Bitte neu starten."] );
|
||||||
|
}
|
||||||
|
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
|
||||||
|
wp_send_json_error( ["message" => "Ungültiger Code. Bitte Uhrzeit prüfen und erneut versuchen."] );
|
||||||
|
}
|
||||||
|
|
||||||
|
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_SECRET, $secret );
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_user_meta",
|
||||||
|
["user_id" => $user->id, "meta_key" => WBF_TOTP::META_PENDING], ["%d", "%s"] );
|
||||||
|
|
||||||
|
wp_send_json_success( ["message" => "2FA erfolgreich aktiviert!"] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2FA deaktivieren (User-seitig).
|
||||||
|
* Erfordert aktuelles Passwort + gültigen TOTP-Code.
|
||||||
|
*/
|
||||||
|
public static function handle_2fa_disable() {
|
||||||
|
self::verify();
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
|
||||||
|
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
|
||||||
|
|
||||||
|
$password = $_POST["password"] ?? "";
|
||||||
|
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
|
||||||
|
|
||||||
|
$fresh = WBF_DB::get_user( $user->id );
|
||||||
|
if ( ! $fresh || ! password_verify( $password, $fresh->password ) ) {
|
||||||
|
wp_send_json_error( ["message" => "Falsches Passwort."] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
|
||||||
|
if ( empty($secret) ) {
|
||||||
|
wp_send_json_error( ["message" => "2FA ist nicht aktiv."] );
|
||||||
|
}
|
||||||
|
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
|
||||||
|
wp_send_json_error( ["message" => "Ungültiger Authenticator-Code."] );
|
||||||
|
}
|
||||||
|
|
||||||
|
WBF_TOTP::disable_for( $user->id );
|
||||||
|
wp_send_json_success( ["message" => "2FA wurde deaktiviert."] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login-Schritt 2: TOTP-Code nach erfolgreichem Passwort prüfen.
|
||||||
|
* Kein Nonce — ausstehende Session-ID ist der Auth-Beweis.
|
||||||
|
* Brute-Force-Schutz: max. 5 Versuche / IP / 10 Min.
|
||||||
|
*/
|
||||||
|
public static function handle_2fa_verify_login() {
|
||||||
|
WBF_Auth::init();
|
||||||
|
|
||||||
|
$ip_key = "wbf_2fa_fail_" . md5( $_SERVER["REMOTE_ADDR"] ?? "unknown" );
|
||||||
|
$fails = (int) get_transient( $ip_key );
|
||||||
|
if ( $fails >= 5 ) {
|
||||||
|
wp_send_json_error( ["message" => "Zu viele Fehlversuche. Bitte warte 10 Minuten.", "locked" => true] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$pending = (int) ( $_SESSION[ WBF_TOTP::SESSION_PENDING ] ?? 0 );
|
||||||
|
if ( ! $pending ) {
|
||||||
|
wp_send_json_error( ["message" => "Keine ausstehende Anmeldung. Bitte erneut einloggen."] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
|
||||||
|
$user = WBF_DB::get_user( $pending );
|
||||||
|
|
||||||
|
if ( ! $user ) {
|
||||||
|
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
|
||||||
|
wp_send_json_error( ["message" => "Ungültige Sitzung."] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
|
||||||
|
if ( empty($secret) || ! WBF_TOTP::verify( $secret, $code ) ) {
|
||||||
|
set_transient( $ip_key, $fails + 1, 10 * MINUTE_IN_SECONDS );
|
||||||
|
wp_send_json_error( ["message" => "Ungültiger Code. Bitte erneut versuchen."] );
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_transient( $ip_key );
|
||||||
|
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
|
||||||
|
|
||||||
|
if ( WBF_Roles::level($user->role) < 0 ) {
|
||||||
|
wp_send_json_error( ["message" => "Dein Konto ist gesperrt."] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( session_id() ) session_regenerate_id( true );
|
||||||
|
$_SESSION[ WBF_Auth::SESSION_KEY ] = $user->id;
|
||||||
|
WBF_DB::touch_last_active( $user->id );
|
||||||
|
|
||||||
|
if ( ! empty( $_SESSION["wbf_2fa_remember"] ) ) {
|
||||||
|
WBF_Auth::set_remember_cookie( $user->id );
|
||||||
|
unset( $_SESSION["wbf_2fa_remember"] );
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( [
|
||||||
|
"display_name" => $user->display_name,
|
||||||
|
"avatar_url" => $user->avatar_url,
|
||||||
|
"user_id" => $user->id,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add_action( 'init', [ 'WBF_Ajax', 'init' ] );
|
add_action( 'init', [ 'WBF_Ajax', 'init' ] );
|
||||||
@@ -3,11 +3,25 @@ if ( ! defined( 'ABSPATH' ) ) exit;
|
|||||||
|
|
||||||
class WBF_Auth {
|
class WBF_Auth {
|
||||||
|
|
||||||
const SESSION_KEY = 'wbf_forum_user';
|
const SESSION_KEY = 'wbf_forum_user';
|
||||||
|
|
||||||
public static function init() {
|
public static function init() {
|
||||||
|
// PHP 8.3: session_start() nach gesendeten Headers erzeugt E_WARNING,
|
||||||
|
// der direkt in den HTML-Output fließt und das Layout zerstört.
|
||||||
|
// Lösung: headers_sent() prüfen + session_start() mit Cookie-Optionen aufrufen.
|
||||||
if ( ! session_id() ) {
|
if ( ! session_id() ) {
|
||||||
session_start();
|
if ( headers_sent() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$session_opts = [
|
||||||
|
'cookie_httponly' => true,
|
||||||
|
'cookie_samesite' => 'Lax',
|
||||||
|
'use_strict_mode' => true,
|
||||||
|
];
|
||||||
|
if ( is_ssl() || ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ) {
|
||||||
|
$session_opts['cookie_secure'] = true;
|
||||||
|
}
|
||||||
|
session_start( $session_opts );
|
||||||
}
|
}
|
||||||
// Auto-login via Remember-Me cookie if not already logged in
|
// Auto-login via Remember-Me cookie if not already logged in
|
||||||
if ( empty( $_SESSION[ self::SESSION_KEY ] ) && isset( $_COOKIE['wbf_remember'] ) ) {
|
if ( empty( $_SESSION[ self::SESSION_KEY ] ) && isset( $_COOKIE['wbf_remember'] ) ) {
|
||||||
@@ -33,7 +47,7 @@ class WBF_Auth {
|
|||||||
return WBF_DB::get_user( (int) $_SESSION[ self::SESSION_KEY ] );
|
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();
|
self::init();
|
||||||
$user = WBF_DB::get_user_by( 'username', $username_or_email );
|
$user = WBF_DB::get_user_by( 'username', $username_or_email );
|
||||||
if ( ! $user ) {
|
if ( ! $user ) {
|
||||||
@@ -43,6 +57,19 @@ class WBF_Auth {
|
|||||||
if ( ! password_verify( $password, $user->password ) ) {
|
if ( ! password_verify( $password, $user->password ) ) {
|
||||||
return array( 'success' => false, 'message' => 'Falsches Passwort.' );
|
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 ) {
|
if ( WBF_Roles::level($user->role) < 0 ) {
|
||||||
// Zeitlich begrenzte Sperre prüfen — automatisch aufheben wenn abgelaufen
|
// Zeitlich begrenzte Sperre prüfen — automatisch aufheben wenn abgelaufen
|
||||||
if ( ! empty($user->ban_until) && strtotime($user->ban_until) <= time() ) {
|
if ( ! empty($user->ban_until) && strtotime($user->ban_until) <= time() ) {
|
||||||
@@ -53,20 +80,20 @@ class WBF_Auth {
|
|||||||
'ban_until' => null,
|
'ban_until' => null,
|
||||||
'pre_ban_role' => '',
|
'pre_ban_role' => '',
|
||||||
]);
|
]);
|
||||||
// Frisch laden und einloggen
|
|
||||||
$user = WBF_DB::get_user( $user->id );
|
$user = WBF_DB::get_user( $user->id );
|
||||||
|
if ( session_id() ) session_regenerate_id( true );
|
||||||
$_SESSION[ self::SESSION_KEY ] = $user->id;
|
$_SESSION[ self::SESSION_KEY ] = $user->id;
|
||||||
WBF_DB::touch_last_active( $user->id );
|
WBF_DB::touch_last_active( $user->id );
|
||||||
return array( 'success' => true, 'user' => $user );
|
return array( 'success' => true, 'user' => $user );
|
||||||
}
|
}
|
||||||
$reason = !empty($user->ban_reason) ? $user->ban_reason : 'Dein Konto wurde gesperrt.';
|
$reason = !empty($user->ban_reason) ? $user->ban_reason : 'Dein Konto wurde gesperrt.';
|
||||||
// Zeitstempel anhängen wenn temporäre Sperre
|
|
||||||
if ( ! empty($user->ban_until) ) {
|
if ( ! empty($user->ban_until) ) {
|
||||||
$until_fmt = date_i18n( 'd.m.Y \u\m H:i \U\h\r', strtotime($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 . ')';
|
$reason .= ' (Gesperrt bis: ' . $until_fmt . ')';
|
||||||
}
|
}
|
||||||
return array( 'success' => false, 'banned' => true, 'message' => $reason );
|
return array( 'success' => false, 'banned' => true, 'message' => $reason );
|
||||||
}
|
}
|
||||||
|
if ( session_id() ) session_regenerate_id( true );
|
||||||
$_SESSION[ self::SESSION_KEY ] = $user->id;
|
$_SESSION[ self::SESSION_KEY ] = $user->id;
|
||||||
WBF_DB::touch_last_active( $user->id );
|
WBF_DB::touch_last_active( $user->id );
|
||||||
return array( 'success' => true, 'user' => $user );
|
return array( 'success' => true, 'user' => $user );
|
||||||
@@ -96,6 +123,7 @@ class WBF_Auth {
|
|||||||
'avatar_url' => $avatar,
|
'avatar_url' => $avatar,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if ( session_id() ) session_regenerate_id( true );
|
||||||
$_SESSION[ self::SESSION_KEY ] = $id;
|
$_SESSION[ self::SESSION_KEY ] = $id;
|
||||||
return array('success'=>true,'user'=>WBF_DB::get_user($id));
|
return array('success'=>true,'user'=>WBF_DB::get_user($id));
|
||||||
}
|
}
|
||||||
@@ -104,10 +132,14 @@ class WBF_Auth {
|
|||||||
self::init();
|
self::init();
|
||||||
$user_id = $_SESSION[ self::SESSION_KEY ] ?? 0;
|
$user_id = $_SESSION[ self::SESSION_KEY ] ?? 0;
|
||||||
unset( $_SESSION[ self::SESSION_KEY ] );
|
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 ) {
|
if ( $user_id ) {
|
||||||
WBF_DB::delete_remember_token( (int)$user_id );
|
WBF_DB::delete_remember_token( (int)$user_id );
|
||||||
}
|
}
|
||||||
// Remove cookie
|
|
||||||
if ( isset($_COOKIE['wbf_remember']) ) {
|
if ( isset($_COOKIE['wbf_remember']) ) {
|
||||||
setcookie( 'wbf_remember', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
|
setcookie( 'wbf_remember', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,12 +123,23 @@ class WBF_BBCode {
|
|||||||
$s
|
$s
|
||||||
);
|
);
|
||||||
|
|
||||||
// [size=small|large|xlarge]
|
// [size=small|large|xlarge] oder [size=1–7] (klassisches BBCode)
|
||||||
$s = preg_replace_callback(
|
$s = preg_replace_callback(
|
||||||
'/\[size=(small|large|xlarge)\](.*?)\[\/size\]/is',
|
'/\[size=([a-zA-Z0-9]+)\](.*?)\[\/size\]/is',
|
||||||
function ( $m ) {
|
function ( $m ) {
|
||||||
$map = [ 'small' => '.8em', 'large' => '1.2em', 'xlarge' => '1.5em' ];
|
$val = strtolower( $m[1] );
|
||||||
return '<span style="font-size:' . $map[$m[1]] . '">' . $m[2] . '</span>';
|
// 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
|
$s
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ class WBF_DB {
|
|||||||
dbDelta( $sql_threads );
|
dbDelta( $sql_threads );
|
||||||
dbDelta( $sql_posts );
|
dbDelta( $sql_posts );
|
||||||
dbDelta( $sql_likes );
|
dbDelta( $sql_likes );
|
||||||
dbDelta( $sql_reports );
|
|
||||||
dbDelta( $sql_tags );
|
dbDelta( $sql_tags );
|
||||||
dbDelta( $sql_thread_tags );
|
dbDelta( $sql_thread_tags );
|
||||||
dbDelta( $sql_messages );
|
dbDelta( $sql_messages );
|
||||||
@@ -174,6 +173,8 @@ class WBF_DB {
|
|||||||
// Zeitlich begrenzte Sperren
|
// 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", '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'");
|
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
|
// Thread-Abonnements
|
||||||
$sql_subscriptions = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_subscriptions (
|
$sql_subscriptions = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_subscriptions (
|
||||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
@@ -199,7 +200,6 @@ class WBF_DB {
|
|||||||
) $charset;";
|
) $charset;";
|
||||||
|
|
||||||
// Ensure reports + notifications tables exist on existing installs
|
// Ensure reports + notifications tables exist on existing installs
|
||||||
dbDelta( $sql_reports );
|
|
||||||
dbDelta( $sql_notifications );
|
dbDelta( $sql_notifications );
|
||||||
|
|
||||||
// Einladungs-Tabelle
|
// Einladungs-Tabelle
|
||||||
@@ -279,6 +279,18 @@ class WBF_DB {
|
|||||||
) $charset;";
|
) $charset;";
|
||||||
dbDelta( $sql_bookmarks );
|
dbDelta( $sql_bookmarks );
|
||||||
|
|
||||||
|
// ── Ignore-Liste ──────────────────────────────────────────────────────
|
||||||
|
$sql_ignore = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_ignore_list (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
ignored_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY user_ignored (user_id, ignored_id),
|
||||||
|
KEY ignored_id (ignored_id)
|
||||||
|
) $charset;";
|
||||||
|
dbDelta( $sql_ignore );
|
||||||
|
|
||||||
// ── prefix_id zu threads ──────────────────────────────────────────────
|
// ── prefix_id zu threads ──────────────────────────────────────────────
|
||||||
self::maybe_add_column( "{$wpdb->prefix}forum_threads", 'prefix_id',
|
self::maybe_add_column( "{$wpdb->prefix}forum_threads", 'prefix_id',
|
||||||
"ALTER TABLE {$wpdb->prefix}forum_threads ADD COLUMN prefix_id BIGINT UNSIGNED DEFAULT NULL" );
|
"ALTER TABLE {$wpdb->prefix}forum_threads ADD COLUMN prefix_id BIGINT UNSIGNED DEFAULT NULL" );
|
||||||
@@ -342,6 +354,67 @@ class WBF_DB {
|
|||||||
public static function update_user( $id, $data ) {
|
public static function update_user( $id, $data ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$wpdb->update("{$wpdb->prefix}forum_users", $data, ['id' => $id]);
|
$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 ) {
|
public static function get_all_users( $limit = 100, $offset = 0 ) {
|
||||||
@@ -478,7 +551,7 @@ class WBF_DB {
|
|||||||
}
|
}
|
||||||
// Move post_count contribution too
|
// Move post_count contribution too
|
||||||
$post_count = (int)$wpdb->get_var($wpdb->prepare(
|
$post_count = (int)$wpdb->get_var($wpdb->prepare(
|
||||||
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id
|
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id
|
||||||
));
|
));
|
||||||
if ( $post_count > 0 ) {
|
if ( $post_count > 0 ) {
|
||||||
$wpdb->query($wpdb->prepare(
|
$wpdb->query($wpdb->prepare(
|
||||||
@@ -500,7 +573,7 @@ class WBF_DB {
|
|||||||
FROM {$wpdb->prefix}forum_threads t
|
FROM {$wpdb->prefix}forum_threads t
|
||||||
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
|
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
|
||||||
LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id
|
LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id
|
||||||
WHERE t.id = %d", $id
|
WHERE t.id = %d AND t.deleted_at IS NULL", $id
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,10 +602,24 @@ class WBF_DB {
|
|||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Posts zählen, User-IDs sammeln
|
||||||
|
$posts = $wpdb->get_results($wpdb->prepare("SELECT user_id FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $id));
|
||||||
|
$post_count = count($posts);
|
||||||
|
$user_post_counts = [];
|
||||||
|
foreach ($posts as $p) {
|
||||||
|
$uid = (int)$p->user_id;
|
||||||
|
if (!isset($user_post_counts[$uid])) $user_post_counts[$uid] = 0;
|
||||||
|
$user_post_counts[$uid]++;
|
||||||
|
}
|
||||||
|
// Posts löschen
|
||||||
$wpdb->delete("{$wpdb->prefix}forum_posts", ['thread_id' => $id]);
|
$wpdb->delete("{$wpdb->prefix}forum_posts", ['thread_id' => $id]);
|
||||||
$wpdb->delete("{$wpdb->prefix}forum_threads", ['id' => $id]);
|
$wpdb->delete("{$wpdb->prefix}forum_threads", ['id' => $id]);
|
||||||
|
// Zähler anpassen
|
||||||
if ( $thread->status !== 'archived' ) {
|
if ( $thread->status !== 'archived' ) {
|
||||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d", $thread->category_id));
|
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0), post_count=GREATEST(post_count-%d,0) WHERE id=%d", $post_count, $thread->category_id));
|
||||||
|
}
|
||||||
|
foreach ($user_post_counts as $uid => $cnt) {
|
||||||
|
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=GREATEST(post_count-%d,0) WHERE id=%d", $cnt, $uid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,7 +647,7 @@ class WBF_DB {
|
|||||||
|
|
||||||
public static function count_posts( $thread_id ) {
|
public static function count_posts( $thread_id ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id));
|
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function create_post( $data ) {
|
public static function create_post( $data ) {
|
||||||
@@ -623,15 +710,16 @@ class WBF_DB {
|
|||||||
FROM {$wpdb->prefix}forum_threads t
|
FROM {$wpdb->prefix}forum_threads t
|
||||||
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
|
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
|
||||||
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
|
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
|
||||||
AND t.status != 'archived' ORDER BY t.created_at DESC LIMIT %d", $limit
|
WHERE t.status != 'archived' AND t.deleted_at IS NULL
|
||||||
|
ORDER BY t.created_at DESC LIMIT %d", $limit
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function get_stats() {
|
public static function get_stats() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
return [
|
return [
|
||||||
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived'"),
|
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived' AND deleted_at IS NULL"),
|
||||||
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts"),
|
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL"),
|
||||||
'members' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users"),
|
'members' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users"),
|
||||||
'newest' => $wpdb->get_var("SELECT display_name FROM {$wpdb->prefix}forum_users ORDER BY registered DESC LIMIT 1"),
|
'newest' => $wpdb->get_var("SELECT display_name FROM {$wpdb->prefix}forum_users ORDER BY registered DESC LIMIT 1"),
|
||||||
];
|
];
|
||||||
@@ -718,9 +806,23 @@ class WBF_DB {
|
|||||||
|
|
||||||
// ── Suche ─────────────────────────────────────────────────────────────────
|
// ── Suche ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public static function search( $query, $limit = 30 ) {
|
public static function search( $query, $limit = 30, $user = null ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$like = '%' . $wpdb->esc_like( $query ) . '%';
|
$like = '%' . $wpdb->esc_like( $query ) . '%';
|
||||||
|
|
||||||
|
// Kategorie-Sichtbarkeit: Gäste und Member dürfen keine privaten Kategorien sehen
|
||||||
|
$user_level = $user ? WBF_Roles::level( $user->role ) : -99;
|
||||||
|
if ( $user_level >= 50 ) {
|
||||||
|
// Moderatoren+ sehen alles (inkl. soft-deleted ist extra)
|
||||||
|
$cat_filter = '';
|
||||||
|
} elseif ( $user ) {
|
||||||
|
// Eingeloggte Member/VIP: nur guest_visible oder eigene Rolle reicht
|
||||||
|
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role IN ('member','vip'))";
|
||||||
|
} else {
|
||||||
|
// Gäste: nur komplett öffentliche Kategorien
|
||||||
|
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role = 'member')";
|
||||||
|
}
|
||||||
|
|
||||||
return $wpdb->get_results( $wpdb->prepare(
|
return $wpdb->get_results( $wpdb->prepare(
|
||||||
"SELECT 'thread' AS result_type,
|
"SELECT 'thread' AS result_type,
|
||||||
t.id, t.title, t.content, t.created_at, t.reply_count,
|
t.id, t.title, t.content, t.created_at, t.reply_count,
|
||||||
@@ -729,7 +831,9 @@ class WBF_DB {
|
|||||||
FROM {$wpdb->prefix}forum_threads t
|
FROM {$wpdb->prefix}forum_threads t
|
||||||
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
|
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
|
||||||
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
|
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
|
||||||
WHERE (t.title LIKE %s OR t.content LIKE %s) AND t.status != 'archived'
|
WHERE (t.title LIKE %s OR t.content LIKE %s)
|
||||||
|
AND t.status != 'archived' AND t.deleted_at IS NULL
|
||||||
|
$cat_filter
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'post' AS result_type,
|
SELECT 'post' AS result_type,
|
||||||
p.id, t.title, p.content, p.created_at, 0 AS reply_count,
|
p.id, t.title, p.content, p.created_at, 0 AS reply_count,
|
||||||
@@ -739,7 +843,9 @@ class WBF_DB {
|
|||||||
JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id
|
JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id
|
||||||
JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
|
JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
|
||||||
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
|
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
|
||||||
WHERE p.content LIKE %s AND t.status != 'archived'
|
WHERE p.content LIKE %s
|
||||||
|
AND p.deleted_at IS NULL AND t.status != 'archived' AND t.deleted_at IS NULL
|
||||||
|
$cat_filter
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT %d",
|
LIMIT %d",
|
||||||
$like, $like, $like, $limit
|
$like, $like, $like, $limit
|
||||||
@@ -765,6 +871,8 @@ class WBF_DB {
|
|||||||
'object_id' => $object_id,
|
'object_id' => $object_id,
|
||||||
'actor_id' => $actor_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 ) {
|
public static function get_notifications( $user_id, $limit = 20 ) {
|
||||||
@@ -1159,12 +1267,13 @@ class WBF_DB {
|
|||||||
public static function create_remember_token( $user_id ) {
|
public static function create_remember_token( $user_id ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$token = bin2hex( random_bytes(32) );
|
$token = bin2hex( random_bytes(32) );
|
||||||
|
$token_hash = hash('sha256', $token);
|
||||||
$expires = date('Y-m-d H:i:s', strtotime('+30 days'));
|
$expires = date('Y-m-d H:i:s', strtotime('+30 days'));
|
||||||
// Delete existing tokens for this user first
|
// Delete existing tokens for this user first
|
||||||
$wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", ['user_id' => $user_id] );
|
$wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", ['user_id' => $user_id] );
|
||||||
$wpdb->insert( "{$wpdb->prefix}forum_remember_tokens", [
|
$wpdb->insert( "{$wpdb->prefix}forum_remember_tokens", [
|
||||||
'user_id' => $user_id,
|
'user_id' => $user_id,
|
||||||
'token' => $token,
|
'token' => $token_hash,
|
||||||
'expires_at' => $expires,
|
'expires_at' => $expires,
|
||||||
] );
|
] );
|
||||||
return $token;
|
return $token;
|
||||||
@@ -1174,10 +1283,11 @@ class WBF_DB {
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table = "{$wpdb->prefix}forum_remember_tokens";
|
$table = "{$wpdb->prefix}forum_remember_tokens";
|
||||||
if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return null;
|
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(
|
return $wpdb->get_row( $wpdb->prepare(
|
||||||
"SELECT user_id FROM {$wpdb->prefix}forum_remember_tokens
|
"SELECT user_id FROM {$wpdb->prefix}forum_remember_tokens
|
||||||
WHERE token=%s AND expires_at > NOW()",
|
WHERE token=%s AND expires_at > NOW()",
|
||||||
sanitize_text_field($token)
|
$token_hash
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1207,13 +1317,18 @@ class WBF_DB {
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
$token = bin2hex( random_bytes(32) );
|
$token = bin2hex( random_bytes(32) );
|
||||||
$hash = hash( 'sha256', $token );
|
$hash = hash( 'sha256', $token );
|
||||||
// Alte Tokens löschen
|
// Altes Token dieses Users zurücksetzen bevor ein neues gesetzt wird
|
||||||
$wpdb->delete( "{$wpdb->prefix}forum_users", [] ); // nur placeholder
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"UPDATE {$wpdb->prefix}forum_users
|
||||||
|
SET reset_token=NULL, reset_token_expires=NULL
|
||||||
|
WHERE id=%d",
|
||||||
|
(int) $user_id
|
||||||
|
) );
|
||||||
$wpdb->query( $wpdb->prepare(
|
$wpdb->query( $wpdb->prepare(
|
||||||
"UPDATE {$wpdb->prefix}forum_users
|
"UPDATE {$wpdb->prefix}forum_users
|
||||||
SET reset_token=%s, reset_token_expires=DATE_ADD(NOW(), INTERVAL 1 HOUR)
|
SET reset_token=%s, reset_token_expires=DATE_ADD(NOW(), INTERVAL 1 HOUR)
|
||||||
WHERE id=%d",
|
WHERE id=%d",
|
||||||
$hash, $user_id
|
$hash, (int) $user_id
|
||||||
) );
|
) );
|
||||||
return $token; // Klartext-Token → per E-Mail senden
|
return $token; // Klartext-Token → per E-Mail senden
|
||||||
}
|
}
|
||||||
@@ -1361,11 +1476,25 @@ class WBF_DB {
|
|||||||
|
|
||||||
public static function soft_delete_post( $post_id ) {
|
public static function soft_delete_post( $post_id ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
// Soft-Delete setzen
|
||||||
$wpdb->update(
|
$wpdb->update(
|
||||||
"{$wpdb->prefix}forum_posts",
|
"{$wpdb->prefix}forum_posts",
|
||||||
['deleted_at' => current_time('mysql')],
|
['deleted_at' => current_time('mysql')],
|
||||||
['id' => (int)$post_id]
|
['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 ) {
|
public static function restore_thread( $thread_id ) {
|
||||||
@@ -1458,6 +1587,25 @@ class WBF_DB {
|
|||||||
update_option( 'wbf_profile_fields', $fields );
|
update_option( 'wbf_profile_fields', $fields );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function get_profile_field_categories() {
|
||||||
|
$cats = get_option( 'wbf_profile_field_cats', null );
|
||||||
|
if ( $cats === null ) {
|
||||||
|
// Default-Kategorien beim ersten Aufruf
|
||||||
|
$defaults = [
|
||||||
|
[ 'id' => 'cat_allgemein', 'name' => 'Allgemein', 'icon' => '👤' ],
|
||||||
|
[ 'id' => 'cat_kontakt', 'name' => 'Kontakt', 'icon' => '✉️' ],
|
||||||
|
[ 'id' => 'cat_social', 'name' => 'Social Media', 'icon' => '🌐' ],
|
||||||
|
];
|
||||||
|
update_option( 'wbf_profile_field_cats', $defaults );
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
return is_array( $cats ) ? $cats : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function save_profile_field_categories( $cats ) {
|
||||||
|
update_option( 'wbf_profile_field_cats', $cats );
|
||||||
|
}
|
||||||
|
|
||||||
public static function get_user_meta( $user_id ) {
|
public static function get_user_meta( $user_id ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$rows = $wpdb->get_results( $wpdb->prepare(
|
$rows = $wpdb->get_results( $wpdb->prepare(
|
||||||
@@ -1469,6 +1617,18 @@ class WBF_DB {
|
|||||||
return $out;
|
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 ) {
|
public static function set_user_meta( $user_id, $key, $value ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$wpdb->replace(
|
$wpdb->replace(
|
||||||
@@ -1689,6 +1849,128 @@ class WBF_DB {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Ignore-Liste ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static function toggle_ignore( $user_id, $ignored_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$user_id = (int) $user_id;
|
||||||
|
$ignored_id = (int) $ignored_id;
|
||||||
|
if ( self::is_ignored( $user_id, $ignored_id ) ) {
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_ignore_list", [
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'ignored_id' => $ignored_id,
|
||||||
|
] );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$wpdb->replace( "{$wpdb->prefix}forum_ignore_list", [
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'ignored_id' => $ignored_id,
|
||||||
|
] );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function is_ignored( $user_id, $ignored_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = "{$wpdb->prefix}forum_ignore_list";
|
||||||
|
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return false;
|
||||||
|
return (bool) $wpdb->get_var( $wpdb->prepare(
|
||||||
|
"SELECT id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d AND ignored_id=%d",
|
||||||
|
(int) $user_id, (int) $ignored_id
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt alle ignorierten User-IDs als int-Array zurück */
|
||||||
|
public static function get_ignored_ids( $user_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = "{$wpdb->prefix}forum_ignore_list";
|
||||||
|
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return [];
|
||||||
|
$ids = $wpdb->get_col( $wpdb->prepare(
|
||||||
|
"SELECT ignored_id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d",
|
||||||
|
(int) $user_id
|
||||||
|
) );
|
||||||
|
return array_map( 'intval', $ids );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vollständige Ignore-Liste mit User-Daten */
|
||||||
|
public static function get_ignore_list( $user_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = "{$wpdb->prefix}forum_ignore_list";
|
||||||
|
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return [];
|
||||||
|
return $wpdb->get_results( $wpdb->prepare(
|
||||||
|
"SELECT u.id, u.username, u.display_name, u.avatar_url, u.role,
|
||||||
|
il.created_at AS ignored_since
|
||||||
|
FROM {$wpdb->prefix}forum_ignore_list il
|
||||||
|
JOIN {$wpdb->prefix}forum_users u ON u.id = il.ignored_id
|
||||||
|
WHERE il.user_id = %d
|
||||||
|
ORDER BY il.created_at DESC",
|
||||||
|
(int) $user_id
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DSGVO Art. 17: Konto vollständig löschen ──────────────────────────────
|
||||||
|
|
||||||
|
public static function delete_user_gdpr( $user_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$user_id = (int) $user_id;
|
||||||
|
$user = self::get_user( $user_id );
|
||||||
|
if ( ! $user ) return false;
|
||||||
|
if ( $user->role === 'superadmin' ) return false;
|
||||||
|
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'from_id' => $user_id ] );
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'to_id' => $user_id ] );
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", [ 'user_id' => $user_id ] );
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'user_id' => $user_id ] );
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'actor_id' => $user_id ] );
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_subscriptions", [ 'user_id' => $user_id ] );
|
||||||
|
|
||||||
|
$table_bm = "{$wpdb->prefix}forum_bookmarks";
|
||||||
|
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_bm'" ) === $table_bm ) {
|
||||||
|
$wpdb->delete( $table_bm, [ 'user_id' => $user_id ] );
|
||||||
|
}
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_likes", [ 'user_id' => $user_id ] );
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_reactions", [ 'user_id' => $user_id ] );
|
||||||
|
$wpdb->delete( "{$wpdb->prefix}forum_reports", [ 'reporter_id' => $user_id ] );
|
||||||
|
|
||||||
|
$table_pv = "{$wpdb->prefix}forum_poll_votes";
|
||||||
|
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_pv'" ) === $table_pv ) {
|
||||||
|
$wpdb->delete( $table_pv, [ 'user_id' => $user_id ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore-Liste beidseitig bereinigen
|
||||||
|
$table_il = "{$wpdb->prefix}forum_ignore_list";
|
||||||
|
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_il'" ) === $table_il ) {
|
||||||
|
$wpdb->delete( $table_il, [ 'user_id' => $user_id ] );
|
||||||
|
$wpdb->delete( $table_il, [ 'ignored_id' => $user_id ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_transient( 'wbf_flood_' . $user_id );
|
||||||
|
delete_transient( 'wbf_flood_ts_' . $user_id );
|
||||||
|
|
||||||
|
self::delete_user_meta_all( $user_id );
|
||||||
|
|
||||||
|
$anon_hash = substr( hash( 'sha256', $user_id . wp_salt() . microtime( true ) ), 0, 12 );
|
||||||
|
$wpdb->update(
|
||||||
|
"{$wpdb->prefix}forum_users",
|
||||||
|
[
|
||||||
|
'username' => 'deleted_' . $anon_hash,
|
||||||
|
'email' => 'deleted_' . $anon_hash . '@deleted.invalid',
|
||||||
|
'password' => '',
|
||||||
|
'display_name' => 'Gelöschter Nutzer',
|
||||||
|
'avatar_url' => '',
|
||||||
|
'bio' => '',
|
||||||
|
'signature' => '',
|
||||||
|
'ban_reason' => '',
|
||||||
|
'reset_token' => null,
|
||||||
|
'reset_token_expires' => null,
|
||||||
|
'pre_ban_role' => '',
|
||||||
|
'ban_until' => null,
|
||||||
|
'role' => 'banned',
|
||||||
|
],
|
||||||
|
[ 'id' => $user_id ]
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Wortfilter ────────────────────────────────────────────────────────────
|
// ── Wortfilter ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public static function get_word_filter() {
|
public static function get_word_filter() {
|
||||||
@@ -1711,25 +1993,29 @@ class WBF_DB {
|
|||||||
// ── Flood Control ─────────────────────────────────────────────────────────
|
// ── Flood Control ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public static function check_flood( $user_id ) {
|
public static function check_flood( $user_id ) {
|
||||||
|
$user_id = (int) $user_id;
|
||||||
|
if ( $user_id <= 0 ) return true; // kein eingeloggter User — kein Flood-Check
|
||||||
$interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 );
|
$interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 );
|
||||||
if ( $interval <= 0 ) return true; // deaktiviert
|
if ( $interval <= 0 ) return true; // deaktiviert
|
||||||
$key = 'wbf_flood_' . (int)$user_id;
|
$key = 'wbf_flood_' . (int)$user_id;
|
||||||
|
$ts_key = 'wbf_flood_ts_' . (int)$user_id;
|
||||||
$last = get_transient( $key );
|
$last = get_transient( $key );
|
||||||
if ( $last !== false ) {
|
if ( $last !== false ) {
|
||||||
return false; // noch gesperrt
|
return false; // noch gesperrt
|
||||||
}
|
}
|
||||||
set_transient( $key, time(), $interval );
|
set_transient( $key, 1, $interval );
|
||||||
|
set_transient( $ts_key, time(), $interval + 5 );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function flood_remaining( $user_id ) {
|
public static function flood_remaining( $user_id ) {
|
||||||
$interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 );
|
$interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 );
|
||||||
if ( $interval <= 0 ) return 0;
|
if ( $interval <= 0 ) return 0;
|
||||||
$key = 'wbf_flood_' . (int)$user_id;
|
$ts_key = 'wbf_flood_ts_' . (int)$user_id;
|
||||||
$last = get_transient( $key );
|
$sent = get_transient( $ts_key );
|
||||||
if ( $last === false ) return 0;
|
if ( $sent === false ) return 0;
|
||||||
// Transients speichern keine genaue Restzeit — wir schätzen über $interval
|
$remaining = $interval - ( time() - (int)$sent );
|
||||||
return $interval;
|
return max( 0, $remaining );
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
1204
includes/class-forum-export.php
Normal file
1204
includes/class-forum-export.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,12 +43,12 @@ class WBF_Levels {
|
|||||||
return $defaults;
|
return $defaults;
|
||||||
}
|
}
|
||||||
$levels = (array) $saved;
|
$levels = (array) $saved;
|
||||||
usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
|
usort( $levels, function($a, $b) { return (int)$a['min'] <=> (int)$b['min']; } );
|
||||||
return $levels;
|
return $levels;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function save( $levels ) {
|
public static function save( $levels ) {
|
||||||
usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
|
usort( $levels, function($a, $b) { return (int)$a['min'] <=> (int)$b['min']; } );
|
||||||
update_option( self::OPTION_KEY, $levels );
|
update_option( self::OPTION_KEY, $levels );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
480
includes/class-forum-mc-bridge.php
Normal file
480
includes/class-forum-mc-bridge.php
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WBF_MC_Bridge — Minecraft ↔ Forum Verknüpfung & Ingame-Benachrichtigungen
|
||||||
|
*
|
||||||
|
* Dieses Modul verbindet das WP Business Forum mit dem BungeeCord StatusAPI Plugin.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Account-Verknüpfung: Forum-User ↔ MC-UUID (über Token-System)
|
||||||
|
* - Push-Benachrichtigungen: Neue Antwort/Erwähnung/PN → Ingame-Nachricht
|
||||||
|
* - REST API Endpoints für die BungeeCord-Seite
|
||||||
|
*
|
||||||
|
* Einbindung in wp-business-forum.php:
|
||||||
|
* require_once WBF_PATH . 'includes/class-forum-mc-bridge.php';
|
||||||
|
*
|
||||||
|
* Konfiguration in WBF-Einstellungen (Admin → Forum → Einstellungen):
|
||||||
|
* mc_bridge_enabled = true/false
|
||||||
|
* mc_bridge_api_url = http://server-ip:9191 (StatusAPI URL)
|
||||||
|
* mc_bridge_api_secret = Shared Secret für API-Authentifizierung
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||||
|
|
||||||
|
class WBF_MC_Bridge {
|
||||||
|
|
||||||
|
/** Meta-Keys in forum_user_meta */
|
||||||
|
const META_MC_UUID = 'mc_uuid';
|
||||||
|
const META_MC_NAME = 'mc_name';
|
||||||
|
const META_LINK_TOKEN = 'mc_link_token';
|
||||||
|
const META_LINK_EXPIRY = 'mc_link_token_expires';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks registrieren — wird beim Plugin-Laden aufgerufen.
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
// Hook: Wird in der modifizierten WBF_DB::create_notification() gefeuert
|
||||||
|
add_action( 'wbf_notification_created', [ __CLASS__, 'on_notification' ], 10, 4 );
|
||||||
|
|
||||||
|
// REST API Endpoints für BungeeCord
|
||||||
|
add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] );
|
||||||
|
|
||||||
|
// AJAX: Token generieren (für eingeloggte Forum-User)
|
||||||
|
add_action( 'wp_ajax_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
|
||||||
|
add_action( 'wp_ajax_nopriv_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
|
||||||
|
|
||||||
|
// AJAX: Verknüpfung lösen
|
||||||
|
add_action( 'wp_ajax_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
|
||||||
|
add_action( 'wp_ajax_nopriv_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
|
||||||
|
|
||||||
|
// AJAX: Link-Status prüfen (Polling im Profil nach Token-Generierung)
|
||||||
|
add_action( 'wp_ajax_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
|
||||||
|
add_action( 'wp_ajax_nopriv_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// ── Einstellungen ─────────────────────────────────────────────────────────
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob die MC-Bridge aktiviert ist.
|
||||||
|
*/
|
||||||
|
public static function is_enabled() {
|
||||||
|
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||||
|
return ! empty( $s['mc_bridge_enabled'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die StatusAPI-URL zurück (z.B. http://192.168.1.100:9191).
|
||||||
|
*/
|
||||||
|
private static function get_api_url() {
|
||||||
|
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||||
|
return rtrim( $s['mc_bridge_api_url'] ?? '', '/' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt das Shared Secret zurück.
|
||||||
|
*/
|
||||||
|
private static function get_api_secret() {
|
||||||
|
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||||
|
return $s['mc_bridge_api_secret'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// ── Notification Hook ─────────────────────────────────────────────────────
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen wenn eine Forum-Notification erstellt wird.
|
||||||
|
* Prüft ob der Empfänger eine MC-UUID hat und pusht die Nachricht an BungeeCord.
|
||||||
|
*
|
||||||
|
* @param int $user_id Forum-User ID des Empfängers
|
||||||
|
* @param string $type Typ: 'reply', 'mention', 'message'
|
||||||
|
* @param int $object_id Thread-ID (bei reply/mention) oder Message-ID (bei message)
|
||||||
|
* @param int $actor_id Forum-User ID des Auslösers
|
||||||
|
*/
|
||||||
|
public static function on_notification( $user_id, $type, $object_id, $actor_id ) {
|
||||||
|
if ( ! self::is_enabled() ) return;
|
||||||
|
|
||||||
|
$api_url = self::get_api_url();
|
||||||
|
if ( empty( $api_url ) ) return;
|
||||||
|
|
||||||
|
// MC-UUID des Empfängers prüfen
|
||||||
|
$mc_uuid = WBF_DB::get_user_meta_single( $user_id, self::META_MC_UUID );
|
||||||
|
if ( empty( $mc_uuid ) ) return;
|
||||||
|
|
||||||
|
// Actor-Info laden
|
||||||
|
$actor = WBF_DB::get_user( (int) $actor_id );
|
||||||
|
$actor_name = $actor ? $actor->display_name : 'Unbekannt';
|
||||||
|
|
||||||
|
// Kontext-Daten sammeln
|
||||||
|
$title = '';
|
||||||
|
$url = '';
|
||||||
|
$forum_url = wbf_get_forum_url();
|
||||||
|
|
||||||
|
switch ( $type ) {
|
||||||
|
case 'reply':
|
||||||
|
case 'mention':
|
||||||
|
$thread = WBF_DB::get_thread( (int) $object_id );
|
||||||
|
if ( $thread ) {
|
||||||
|
$title = $thread->title;
|
||||||
|
$url = $forum_url . '?forum_thread=' . (int) $thread->id;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'message':
|
||||||
|
$title = 'Neue Privatnachricht';
|
||||||
|
$url = $forum_url . '?forum_dm=1';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push an BungeeCord senden
|
||||||
|
self::push_to_bungee( $mc_uuid, $type, $title, $actor_name, $url, $user_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet die Benachrichtigung per HTTP POST an den BungeeCord StatusAPI Server.
|
||||||
|
*/
|
||||||
|
private static function push_to_bungee( $mc_uuid, $type, $title, $author, $url, $wp_user_id ) {
|
||||||
|
$api_url = self::get_api_url();
|
||||||
|
$secret = self::get_api_secret();
|
||||||
|
|
||||||
|
$payload = wp_json_encode( [
|
||||||
|
'player_uuid' => $mc_uuid,
|
||||||
|
'type' => $type,
|
||||||
|
'title' => $title,
|
||||||
|
'author' => $author,
|
||||||
|
'url' => $url,
|
||||||
|
'wp_user_id' => (int) $wp_user_id,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
'method' => 'POST',
|
||||||
|
'timeout' => 5,
|
||||||
|
'blocking' => false, // Non-blocking — Seite wartet nicht auf Antwort
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json; charset=UTF-8',
|
||||||
|
'X-Api-Key' => $secret,
|
||||||
|
],
|
||||||
|
'body' => $payload,
|
||||||
|
'sslverify' => false, // Lokales Netzwerk braucht kein SSL
|
||||||
|
];
|
||||||
|
|
||||||
|
wp_remote_post( $api_url . '/forum/notify', $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// ── REST API (für BungeeCord → WordPress) ─────────────────────────────────
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public static function register_rest_routes() {
|
||||||
|
// POST /wp-json/mc-bridge/v1/verify-link
|
||||||
|
// BungeeCord schickt Token + MC-UUID → WP verifiziert und speichert
|
||||||
|
register_rest_route( 'mc-bridge/v1', '/verify-link', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => [ __CLASS__, 'rest_verify_link' ],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
] );
|
||||||
|
|
||||||
|
// POST /wp-json/mc-bridge/v1/unlink
|
||||||
|
// BungeeCord kann Verknüpfung auch von der MC-Seite lösen
|
||||||
|
register_rest_route( 'mc-bridge/v1', '/unlink', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => [ __CLASS__, 'rest_unlink' ],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
] );
|
||||||
|
|
||||||
|
// GET /wp-json/mc-bridge/v1/status
|
||||||
|
// Verbindungstest
|
||||||
|
register_rest_route( 'mc-bridge/v1', '/status', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => [ __CLASS__, 'rest_status' ],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST: Verknüpfung bestätigen.
|
||||||
|
* BungeeCord sendet: { "token": "...", "mc_uuid": "...", "mc_name": "..." }
|
||||||
|
*/
|
||||||
|
public static function rest_verify_link( $request ) {
|
||||||
|
// Rate Limiting: max 10 Versuche pro IP pro Minute
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
$limit_key = 'wbf_mc_link_' . md5($ip);
|
||||||
|
$attempts = (int) get_transient($limit_key);
|
||||||
|
if ($attempts >= 10) {
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'rate_limited',
|
||||||
|
'message' => 'Zu viele Versuche. Bitte warte eine Minute.'
|
||||||
|
], 429);
|
||||||
|
}
|
||||||
|
set_transient($limit_key, $attempts + 1, 60);
|
||||||
|
// API-Secret prüfen
|
||||||
|
$secret = self::get_api_secret();
|
||||||
|
if ( ! empty( $secret ) ) {
|
||||||
|
$provided = $request->get_header( 'X-Api-Key' );
|
||||||
|
if ( $provided !== $secret ) {
|
||||||
|
return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = sanitize_text_field( $request->get_param( 'token' ) );
|
||||||
|
$mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) );
|
||||||
|
$mc_name = sanitize_text_field( $request->get_param( 'mc_name' ) );
|
||||||
|
|
||||||
|
if ( empty( $token ) || empty( $mc_uuid ) ) {
|
||||||
|
return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_fields' ], 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token in forum_user_meta suchen
|
||||||
|
global $wpdb;
|
||||||
|
$meta_row = $wpdb->get_row( $wpdb->prepare(
|
||||||
|
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
|
||||||
|
self::META_LINK_TOKEN, $token
|
||||||
|
) );
|
||||||
|
|
||||||
|
if ( ! $meta_row ) {
|
||||||
|
return new WP_REST_Response( [ 'success' => false, 'error' => 'invalid_token' ], 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$forum_user_id = (int) $meta_row->user_id;
|
||||||
|
|
||||||
|
// Ablauf prüfen
|
||||||
|
$expiry_meta = WBF_DB::get_user_meta( $forum_user_id );
|
||||||
|
$expiry = $expiry_meta[ self::META_LINK_EXPIRY ] ?? '0';
|
||||||
|
if ( (int) $expiry < time() ) {
|
||||||
|
// Token abgelaufen — aufräumen
|
||||||
|
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' );
|
||||||
|
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' );
|
||||||
|
return new WP_REST_Response( [ 'success' => false, 'error' => 'token_expired' ], 410 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob diese MC-UUID bereits mit einem anderen Account verknüpft ist
|
||||||
|
$existing = $wpdb->get_row( $wpdb->prepare(
|
||||||
|
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
|
||||||
|
self::META_MC_UUID, $mc_uuid
|
||||||
|
) );
|
||||||
|
if ( $existing && (int) $existing->user_id !== $forum_user_id ) {
|
||||||
|
return new WP_REST_Response( [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'uuid_already_linked',
|
||||||
|
'message' => 'Diese Minecraft-UUID ist bereits mit einem anderen Forum-Account verknüpft.',
|
||||||
|
], 409 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verknüpfung speichern
|
||||||
|
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, $mc_uuid );
|
||||||
|
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, $mc_name );
|
||||||
|
|
||||||
|
// Token aufräumen
|
||||||
|
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' );
|
||||||
|
WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' );
|
||||||
|
|
||||||
|
// Forum-User-Info für BungeeCord zurückgeben
|
||||||
|
$forum_user = WBF_DB::get_user( $forum_user_id );
|
||||||
|
|
||||||
|
return new WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'forum_user_id' => $forum_user_id,
|
||||||
|
'display_name' => $forum_user ? $forum_user->display_name : '',
|
||||||
|
'username' => $forum_user ? $forum_user->username : '',
|
||||||
|
], 200 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST: Verknüpfung lösen (von BungeeCord-Seite).
|
||||||
|
*/
|
||||||
|
public static function rest_unlink( $request ) {
|
||||||
|
$secret = self::get_api_secret();
|
||||||
|
if ( ! empty( $secret ) ) {
|
||||||
|
$provided = $request->get_header( 'X-Api-Key' );
|
||||||
|
if ( $provided !== $secret ) {
|
||||||
|
return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) );
|
||||||
|
if ( empty( $mc_uuid ) ) {
|
||||||
|
return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_mc_uuid' ], 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$meta_row = $wpdb->get_row( $wpdb->prepare(
|
||||||
|
"SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s",
|
||||||
|
self::META_MC_UUID, $mc_uuid
|
||||||
|
) );
|
||||||
|
|
||||||
|
if ( ! $meta_row ) {
|
||||||
|
return new WP_REST_Response( [ 'success' => false, 'error' => 'not_linked' ], 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$forum_user_id = (int) $meta_row->user_id;
|
||||||
|
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, '' );
|
||||||
|
WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, '' );
|
||||||
|
|
||||||
|
return new WP_REST_Response( [ 'success' => true ], 200 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST: Status-Endpoint für Verbindungstest.
|
||||||
|
*/
|
||||||
|
public static function rest_status( $request ) {
|
||||||
|
return new WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'enabled' => self::is_enabled(),
|
||||||
|
'version' => defined( 'WBF_VERSION' ) ? WBF_VERSION : '?',
|
||||||
|
'plugin' => 'WP Business Forum',
|
||||||
|
], 200 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// ── AJAX: Token generieren ────────────────────────────────────────────────
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert einen 8-stelligen Verknüpfungs-Token (15 Minuten gültig).
|
||||||
|
* Der User gibt diesen Token dann ingame mit /forumlink <token> ein.
|
||||||
|
*/
|
||||||
|
public static function ajax_generate_token() {
|
||||||
|
check_ajax_referer( 'wbf_nonce', 'nonce' );
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) {
|
||||||
|
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob bereits verknüpft
|
||||||
|
$existing_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
|
||||||
|
if ( ! empty( $existing_uuid ) ) {
|
||||||
|
$mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME );
|
||||||
|
wp_send_json_error( [
|
||||||
|
'message' => 'Dein Account ist bereits mit ' . esc_html( $mc_name ?: $existing_uuid ) . ' verknüpft.',
|
||||||
|
'linked' => true,
|
||||||
|
'mc_name' => $mc_name,
|
||||||
|
'mc_uuid' => $existing_uuid,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token generieren: 8 Zeichen, alphanumerisch, uppercase
|
||||||
|
$token = strtoupper( substr( bin2hex( random_bytes( 5 ) ), 0, 8 ) );
|
||||||
|
$expiry = time() + ( 15 * 60 ); // 15 Minuten
|
||||||
|
|
||||||
|
WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, $token );
|
||||||
|
WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, (string) $expiry );
|
||||||
|
|
||||||
|
wp_send_json_success( [
|
||||||
|
'token' => $token,
|
||||||
|
'expires_in' => 15, // Minuten
|
||||||
|
'command' => '/forumlink ' . $token,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Verknüpfung lösen (von der Forum-Seite).
|
||||||
|
*/
|
||||||
|
public static function ajax_unlink() {
|
||||||
|
check_ajax_referer( 'wbf_nonce', 'nonce' );
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) {
|
||||||
|
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
|
||||||
|
|
||||||
|
// Meta löschen
|
||||||
|
WBF_DB::set_user_meta( $user->id, self::META_MC_UUID, '' );
|
||||||
|
WBF_DB::set_user_meta( $user->id, self::META_MC_NAME, '' );
|
||||||
|
WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, '' );
|
||||||
|
WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, '' );
|
||||||
|
|
||||||
|
// Optional: BungeeCord informieren
|
||||||
|
if ( ! empty( $mc_uuid ) && self::is_enabled() ) {
|
||||||
|
$api_url = self::get_api_url();
|
||||||
|
$secret = self::get_api_secret();
|
||||||
|
if ( ! empty( $api_url ) ) {
|
||||||
|
wp_remote_post( $api_url . '/forum/unlink', [
|
||||||
|
'method' => 'POST',
|
||||||
|
'timeout' => 3,
|
||||||
|
'blocking' => false,
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'X-Api-Key' => $secret,
|
||||||
|
],
|
||||||
|
'body' => wp_json_encode( [ 'mc_uuid' => $mc_uuid ] ),
|
||||||
|
'sslverify' => false,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( [ 'message' => 'Minecraft-Verknüpfung wurde aufgehoben.' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Verknüpfungs-Status prüfen.
|
||||||
|
* Wird vom Frontend-Polling alle 5 Sekunden nach Token-Generierung aufgerufen.
|
||||||
|
* Gibt zurück ob der User bereits verknüpft ist (BungeeCord hat verify-link gesendet).
|
||||||
|
*/
|
||||||
|
public static function ajax_link_status() {
|
||||||
|
check_ajax_referer( 'wbf_nonce', 'nonce' );
|
||||||
|
$user = WBF_Auth::get_current_user();
|
||||||
|
if ( ! $user ) {
|
||||||
|
wp_send_json_error( [ 'message' => 'Nicht eingeloggt.', 'linked' => false ] );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID );
|
||||||
|
$mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME );
|
||||||
|
if ( ! empty( $mc_uuid ) ) {
|
||||||
|
wp_send_json_success( [
|
||||||
|
'linked' => true,
|
||||||
|
'mc_uuid' => $mc_uuid,
|
||||||
|
'mc_name' => $mc_name,
|
||||||
|
] );
|
||||||
|
} else {
|
||||||
|
wp_send_json_success( [ 'linked' => false ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die MC-UUID eines Forum-Users zurück (oder leer).
|
||||||
|
*/
|
||||||
|
public static function get_mc_uuid( $forum_user_id ) {
|
||||||
|
return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_UUID );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den MC-Namen eines Forum-Users zurück (oder leer).
|
||||||
|
*/
|
||||||
|
public static function get_mc_name( $forum_user_id ) {
|
||||||
|
return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_NAME );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob ein Forum-User mit MC verknüpft ist.
|
||||||
|
*/
|
||||||
|
public static function is_linked( $forum_user_id ) {
|
||||||
|
$uuid = self::get_mc_uuid( $forum_user_id );
|
||||||
|
return ! empty( $uuid );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML-Badge für Profilansicht: Zeigt MC-Verknüpfung an.
|
||||||
|
*/
|
||||||
|
public static function profile_badge( $forum_user_id ) {
|
||||||
|
if ( ! self::is_enabled() ) return '';
|
||||||
|
$mc_name = self::get_mc_name( $forum_user_id );
|
||||||
|
if ( empty( $mc_name ) ) return '';
|
||||||
|
|
||||||
|
$name_esc = esc_html( $mc_name );
|
||||||
|
$head_url = 'https://mc-heads.net/avatar/' . urlencode( $mc_name ) . '/24';
|
||||||
|
return "<span class=\"wbf-mc-badge\" title=\"Minecraft: {$name_esc}\">"
|
||||||
|
. "<img src=\"{$head_url}\" alt=\"\" width=\"16\" height=\"16\" style=\"border-radius:2px;vertical-align:middle;margin-right:4px\">"
|
||||||
|
. "<span style=\"color:#55ff55\">{$name_esc}</span>"
|
||||||
|
. "</span>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisierung
|
||||||
|
add_action( 'init', [ 'WBF_MC_Bridge', 'init' ], 20 );
|
||||||
@@ -15,7 +15,7 @@ class WBF_Roles {
|
|||||||
private static function default_roles() {
|
private static function default_roles() {
|
||||||
return [
|
return [
|
||||||
'superadmin' => [
|
'superadmin' => [
|
||||||
'label' => 'Superadmin',
|
'label' => 'Admin',
|
||||||
'level' => 100,
|
'level' => 100,
|
||||||
'color' => '#e11d48',
|
'color' => '#e11d48',
|
||||||
'bg_color' => 'rgba(225,29,72,.15)',
|
'bg_color' => 'rgba(225,29,72,.15)',
|
||||||
@@ -108,7 +108,7 @@ class WBF_Roles {
|
|||||||
/** Nach Level sortiert (höchstes zuerst) */
|
/** Nach Level sortiert (höchstes zuerst) */
|
||||||
public static function get_sorted() {
|
public static function get_sorted() {
|
||||||
$all = self::get_all();
|
$all = self::get_all();
|
||||||
uasort($all, fn($a,$b) => $b['level'] <=> $a['level']);
|
uasort($all, function($a, $b) { return $b['level'] <=> $a['level']; });
|
||||||
return $all;
|
return $all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,20 +192,37 @@ class WBF_Roles {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ist der aktuelle WP-User der Seiteninhaber (Superadmin)? */
|
/**
|
||||||
public static function is_wp_superadmin() {
|
* Gibt die WP-User-ID des echten Superadmins zurück.
|
||||||
return current_user_can('administrator') || (is_multisite() && is_super_admin());
|
* 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() {
|
public static function sync_superadmin() {
|
||||||
if ( ! is_user_logged_in() ) return;
|
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();
|
$wp_user = wp_get_current_user();
|
||||||
$forum_user = WBF_DB::get_user_by('email', $wp_user->user_email);
|
$forum_user = WBF_DB::get_user_by( 'email', $wp_user->user_email );
|
||||||
if ( $forum_user && $forum_user->role !== self::SUPERADMIN ) {
|
if ( $forum_user && $forum_user->role !== self::SUPERADMIN ) {
|
||||||
WBF_DB::update_user($forum_user->id, ['role' => self::SUPERADMIN]);
|
WBF_DB::update_user( $forum_user->id, [ 'role' => self::SUPERADMIN ] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
179
includes/class-forum-totp.php
Normal file
179
includes/class-forum-totp.php
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WBF_TOTP — RFC 6238 Time-based One-Time Password (TOTP)
|
||||||
|
*
|
||||||
|
* Keine externe Bibliothek nötig — reines PHP 7.0+.
|
||||||
|
* Kompatibel mit: Google Authenticator, Aegis, Authy, Bitwarden, 2FAS, etc.
|
||||||
|
*
|
||||||
|
* Secrets werden in forum_user_meta gespeichert (meta_key = 'totp_secret').
|
||||||
|
* Kein Schema-Change an der Haupt-Usertabelle nötig.
|
||||||
|
*/
|
||||||
|
class WBF_TOTP {
|
||||||
|
|
||||||
|
const DIGITS = 6;
|
||||||
|
const PERIOD = 30; // Sekunden pro Schritt
|
||||||
|
const WINDOW = 1; // ±1 Step Toleranz (= ±30 s Uhrabweichung OK)
|
||||||
|
const SECRET_LEN = 20; // Bytes → 32 Base32-Zeichen
|
||||||
|
|
||||||
|
// Meta-Keys
|
||||||
|
const META_SECRET = 'totp_secret';
|
||||||
|
const META_PENDING = 'totp_secret_pending';
|
||||||
|
|
||||||
|
// Session-Key für ausstehenden Login
|
||||||
|
const SESSION_PENDING = 'wbf_2fa_pending';
|
||||||
|
|
||||||
|
// ── Secret ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erzeugt einen neuen, kryptografisch sicheren Base32-Secret.
|
||||||
|
* @return string z.B. "JBSWY3DPEBLW64TMMQ======"
|
||||||
|
*/
|
||||||
|
public static function generate_secret() {
|
||||||
|
return self::base32_encode( random_bytes( self::SECRET_LEN ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Verifikation ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob $code für $secret zum aktuellen Zeitfenster passt.
|
||||||
|
*
|
||||||
|
* @param string $secret Base32-Secret des Users
|
||||||
|
* @param string $code 6-stelliger Code aus der Authenticator-App
|
||||||
|
* @param int $window Anzahl Steps Toleranz (default = 1 = ±30 s)
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function verify( $secret, $code, $window = self::WINDOW ) {
|
||||||
|
// Leerzeichen tolerieren (z.B. "123 456")
|
||||||
|
$code = preg_replace( '/\s+/', '', (string) $code );
|
||||||
|
if ( strlen($code) !== self::DIGITS ) return false;
|
||||||
|
if ( ! ctype_digit($code) ) return false;
|
||||||
|
|
||||||
|
$key = self::base32_decode( $secret );
|
||||||
|
if ( empty($key) ) return false;
|
||||||
|
|
||||||
|
$ts = (int) floor( time() / self::PERIOD );
|
||||||
|
|
||||||
|
for ( $i = -$window; $i <= $window; $i++ ) {
|
||||||
|
$expected = self::hotp( $key, $ts + $i );
|
||||||
|
// Timing-safe Vergleich
|
||||||
|
if ( hash_equals( $expected, $code ) ) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HOTP-Kern (RFC 4226) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static function hotp( $key, $counter ) {
|
||||||
|
// 64-bit Big-Endian Counter
|
||||||
|
$msg = pack( 'N', 0 ) . pack( 'N', $counter );
|
||||||
|
|
||||||
|
$hash = hash_hmac( 'sha1', $msg, $key, true );
|
||||||
|
$offset = ord( $hash[19] ) & 0x0f;
|
||||||
|
|
||||||
|
$code = (
|
||||||
|
( ord($hash[$offset ]) & 0x7f ) << 24 |
|
||||||
|
( ord($hash[$offset + 1]) & 0xff ) << 16 |
|
||||||
|
( ord($hash[$offset + 2]) & 0xff ) << 8 |
|
||||||
|
( ord($hash[$offset + 3]) & 0xff )
|
||||||
|
) % ( 10 ** self::DIGITS );
|
||||||
|
|
||||||
|
return str_pad( (string) $code, self::DIGITS, '0', STR_PAD_LEFT );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── otpauth:// URI ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die otpauth:// URI zurück — wird vom QR-Code-Generator verwendet.
|
||||||
|
*
|
||||||
|
* @param string $username Forum-Benutzername
|
||||||
|
* @param string $secret Base32-Secret
|
||||||
|
* @param string|null $issuer Anzeigename in der App (default: Blogname)
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function get_otpauth_uri( $username, $secret, $issuer = null ) {
|
||||||
|
if ( ! $issuer ) {
|
||||||
|
$issuer = html_entity_decode( get_bloginfo('name'), ENT_QUOTES ) ?: 'WP Business Forum';
|
||||||
|
}
|
||||||
|
$label = rawurlencode( $issuer . ':' . $username );
|
||||||
|
return 'otpauth://totp/' . $label . '?'
|
||||||
|
. 'secret=' . rawurlencode( $secret )
|
||||||
|
. '&issuer=' . rawurlencode( $issuer )
|
||||||
|
. '&algorithm=SHA1'
|
||||||
|
. '&digits=' . self::DIGITS
|
||||||
|
. '&period=' . self::PERIOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── User-Helfer ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Ist 2FA für diesen User aktiv? */
|
||||||
|
public static function is_enabled_for( $user_id ) {
|
||||||
|
$s = WBF_DB::get_user_meta_single( (int) $user_id, self::META_SECRET );
|
||||||
|
return ! empty( $s );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2FA für einen User deaktivieren (löscht Secret + ggf. pending Secret).
|
||||||
|
* Kann von Admin und User selbst (nach Verifikation) aufgerufen werden.
|
||||||
|
*/
|
||||||
|
public static function disable_for( $user_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$uid = (int) $user_id;
|
||||||
|
$wpdb->delete(
|
||||||
|
"{$wpdb->prefix}forum_user_meta",
|
||||||
|
[ 'user_id' => $uid, 'meta_key' => self::META_SECRET ],
|
||||||
|
[ '%d', '%s' ]
|
||||||
|
);
|
||||||
|
$wpdb->delete(
|
||||||
|
"{$wpdb->prefix}forum_user_meta",
|
||||||
|
[ 'user_id' => $uid, 'meta_key' => self::META_PENDING ],
|
||||||
|
[ '%d', '%s' ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Base32 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static $b32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
|
||||||
|
public static function base32_encode( $input ) {
|
||||||
|
$output = '';
|
||||||
|
$buf = 0;
|
||||||
|
$buf_bits = 0;
|
||||||
|
for ( $i = 0, $len = strlen($input); $i < $len; $i++ ) {
|
||||||
|
$buf = ( $buf << 8 ) | ord( $input[$i] );
|
||||||
|
$buf_bits += 8;
|
||||||
|
while ( $buf_bits >= 5 ) {
|
||||||
|
$buf_bits -= 5;
|
||||||
|
$output .= self::$b32[ ( $buf >> $buf_bits ) & 0x1f ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( $buf_bits > 0 ) {
|
||||||
|
$output .= self::$b32[ ( $buf << ( 5 - $buf_bits ) ) & 0x1f ];
|
||||||
|
}
|
||||||
|
// Padding to multiple of 8
|
||||||
|
while ( strlen($output) % 8 !== 0 ) $output .= '=';
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function base32_decode( $input ) {
|
||||||
|
// Leerzeichen & Padding entfernen, Uppercase
|
||||||
|
$input = strtoupper( preg_replace( '/\s+/', '', $input ) );
|
||||||
|
$input = rtrim( $input, '=' );
|
||||||
|
$map = array_flip( str_split( self::$b32 ) );
|
||||||
|
|
||||||
|
$output = '';
|
||||||
|
$buf = 0;
|
||||||
|
$bits = 0;
|
||||||
|
for ( $i = 0, $len = strlen($input); $i < $len; $i++ ) {
|
||||||
|
if ( ! isset( $map[ $input[$i] ] ) ) continue; // ungültiges Zeichen ignorieren
|
||||||
|
$buf = ( $buf << 5 ) | $map[ $input[$i] ];
|
||||||
|
$bits += 5;
|
||||||
|
if ( $bits >= 8 ) {
|
||||||
|
$bits -= 8;
|
||||||
|
$output .= chr( ( $buf >> $bits ) & 0xff );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
includes/forum-statusapi.php
Normal file
32
includes/forum-statusapi.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
// Ingame-Benachrichtigung via StatusAPI
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function wbf_notify_ingame($player, $message) {
|
||||||
|
// Einstellungen laden
|
||||||
|
$settings = function_exists('wbf_get_settings') ? wbf_get_settings() : [];
|
||||||
|
$enabled = !empty($settings['mc_bridge_enabled']);
|
||||||
|
$api_url = trim($settings['mc_bridge_api_url'] ?? '');
|
||||||
|
$api_secret = trim($settings['mc_bridge_api_secret'] ?? '');
|
||||||
|
if (!$enabled || !$api_url || !$api_secret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = rtrim($api_url, '/') . '/notify-pn';
|
||||||
|
$data = [
|
||||||
|
'player' => $player,
|
||||||
|
'message' => $message
|
||||||
|
];
|
||||||
|
$args = [
|
||||||
|
'body' => json_encode($data),
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'X-API-Key' => $api_secret,
|
||||||
|
],
|
||||||
|
'timeout' => 2,
|
||||||
|
'data_format' => 'body',
|
||||||
|
];
|
||||||
|
wp_remote_post($url, $args);
|
||||||
|
}
|
||||||
221
uninstall.php
221
uninstall.php
@@ -1,108 +1,115 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* WP Business Forum — Uninstaller
|
* WP Business Forum — Uninstaller
|
||||||
* Wird automatisch aufgerufen wenn das Plugin über WP-Admin gelöscht wird.
|
* Wird automatisch aufgerufen wenn das Plugin über WP-Admin gelöscht wird.
|
||||||
* Entfernt: alle DB-Tabellen, wp_options, Transients, Cron-Jobs, Upload-Verzeichnis.
|
* Entfernt: alle DB-Tabellen, wp_options, Transients, Cron-Jobs, Upload-Verzeichnis.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Sicherheits-Check — nur via WordPress-Uninstall erlaubt
|
// Sicherheits-Check — nur via WordPress-Uninstall erlaubt
|
||||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// ── 1. Alle Datenbank-Tabellen löschen ───────────────────────────────────────
|
// ── 1. Alle Datenbank-Tabellen löschen ───────────────────────────────────────
|
||||||
// Reihenfolge beachten: abhängige Tabellen zuerst (Foreign Keys)
|
// Reihenfolge beachten: abhängige Tabellen zuerst (Foreign Keys)
|
||||||
$tables = [
|
$tables = [
|
||||||
'forum_poll_votes',
|
'forum_poll_votes',
|
||||||
'forum_polls',
|
'forum_polls',
|
||||||
'forum_reactions',
|
'forum_reactions',
|
||||||
'forum_notifications',
|
'forum_notifications',
|
||||||
'forum_subscriptions',
|
'forum_subscriptions',
|
||||||
'forum_invites',
|
'forum_bookmarks', // ← fehlte: Lesezeichen
|
||||||
'forum_thread_tags',
|
'forum_ignore_list', // ← Ignore/Block-Liste
|
||||||
'forum_tags',
|
'forum_invites',
|
||||||
'forum_reports',
|
'forum_thread_tags',
|
||||||
'forum_likes',
|
'forum_tags',
|
||||||
'forum_messages',
|
'forum_prefixes', // ← fehlte: Thread-Präfixe
|
||||||
'forum_remember_tokens',
|
'forum_reports',
|
||||||
'forum_user_meta',
|
'forum_likes',
|
||||||
'forum_posts',
|
'forum_messages',
|
||||||
'forum_threads',
|
'forum_remember_tokens',
|
||||||
'forum_categories',
|
'forum_user_meta',
|
||||||
'forum_users',
|
'forum_posts',
|
||||||
];
|
'forum_threads',
|
||||||
|
'forum_categories',
|
||||||
foreach ( $tables as $table ) {
|
'forum_users',
|
||||||
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}{$table}`" );
|
];
|
||||||
}
|
|
||||||
|
foreach ( $tables as $table ) {
|
||||||
// ── 2. Alle wp_options löschen ───────────────────────────────────────────────
|
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}{$table}`" );
|
||||||
$options = [
|
}
|
||||||
'wbf_settings',
|
|
||||||
'wbf_custom_roles',
|
// ── 2. Alle wp_options löschen ───────────────────────────────────────────────
|
||||||
'wbf_level_config',
|
$options = [
|
||||||
'wbf_levels_enabled',
|
'wbf_settings',
|
||||||
'wbf_profile_fields',
|
'wbf_custom_roles',
|
||||||
'wbf_reactions',
|
'wbf_level_config',
|
||||||
'wbf_forum_page_id',
|
'wbf_levels_enabled',
|
||||||
'wbf_superadmin_email',
|
'wbf_profile_fields',
|
||||||
'wbf_db_version',
|
'wbf_profile_field_cats',
|
||||||
];
|
'wbf_reactions',
|
||||||
|
'wbf_forum_page_id',
|
||||||
foreach ( $options as $option ) {
|
'wbf_superadmin_email',
|
||||||
delete_option( $option );
|
'wbf_db_version',
|
||||||
}
|
'wbf_word_filter',
|
||||||
|
];
|
||||||
// Multisite: Netzwerk-Optionen ebenfalls entfernen
|
|
||||||
if ( is_multisite() ) {
|
foreach ( $options as $option ) {
|
||||||
foreach ( $options as $option ) {
|
delete_option( $option );
|
||||||
delete_site_option( $option );
|
}
|
||||||
}
|
|
||||||
}
|
// Multisite: Netzwerk-Optionen ebenfalls entfernen
|
||||||
|
if ( is_multisite() ) {
|
||||||
// ── 3. Transients löschen ────────────────────────────────────────────────────
|
foreach ( $options as $option ) {
|
||||||
delete_transient( 'wbf_activation_redirect' );
|
delete_site_option( $option );
|
||||||
delete_transient( 'wbf_stats_cache' );
|
}
|
||||||
|
}
|
||||||
// Alle wbf_* Transients per LIKE-Query entfernen
|
|
||||||
$wpdb->query(
|
// ── 3. Transients löschen ────────────────────────────────────────────────────
|
||||||
"DELETE FROM `{$wpdb->options}`
|
delete_transient( 'wbf_activation_redirect' );
|
||||||
WHERE `option_name` LIKE '_transient_wbf_%'
|
delete_transient( 'wbf_stats_cache' );
|
||||||
OR `option_name` LIKE '_transient_timeout_wbf_%'"
|
delete_transient( 'wbf_update_check' );
|
||||||
);
|
|
||||||
|
// Alle wbf_* Transients per LIKE-Query entfernen (inkl. Update-Dismissed-Transients)
|
||||||
// ── 4. Geplante Cron-Jobs entfernen ──────────────────────────────────────────
|
$wpdb->query(
|
||||||
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
|
"DELETE FROM `{$wpdb->options}`
|
||||||
|
WHERE `option_name` LIKE '_transient_wbf_%'
|
||||||
// ── 5. Forum-Seite löschen (vom Setup-Wizard erstellt) ───────────────────────
|
OR `option_name` LIKE '_transient_timeout_wbf_%'"
|
||||||
$forum_page_id = get_option( 'wbf_forum_page_id' );
|
);
|
||||||
if ( $forum_page_id ) {
|
|
||||||
wp_delete_post( (int) $forum_page_id, true ); // true = dauerhaft löschen
|
// ── 4. Geplante Cron-Jobs entfernen ──────────────────────────────────────────
|
||||||
}
|
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
|
||||||
|
wp_clear_scheduled_hook( 'wbf_check_for_updates' );
|
||||||
// ── 6. Upload-Unterverzeichnis entfernen ─────────────────────────────────────
|
|
||||||
$upload_dir = wp_upload_dir();
|
// ── 5. Forum-Seite löschen (vom Setup-Wizard erstellt) ───────────────────────
|
||||||
$wbf_dir = trailingslashit( $upload_dir['basedir'] ) . 'wbf-avatars';
|
$forum_page_id = get_option( 'wbf_forum_page_id' );
|
||||||
if ( is_dir( $wbf_dir ) ) {
|
if ( $forum_page_id ) {
|
||||||
wbf_uninstall_rmdir( $wbf_dir );
|
wp_delete_post( (int) $forum_page_id, true ); // true = dauerhaft löschen
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ── 6. Upload-Unterverzeichnis entfernen ─────────────────────────────────────
|
||||||
* Hilfsfunktion: Verzeichnis rekursiv löschen.
|
$upload_dir = wp_upload_dir();
|
||||||
* Nur innerhalb des WP-Upload-Verzeichnisses erlaubt.
|
$wbf_dir = trailingslashit( $upload_dir['basedir'] ) . 'wbf-avatars';
|
||||||
*/
|
if ( is_dir( $wbf_dir ) ) {
|
||||||
function wbf_uninstall_rmdir( $dir ) {
|
wbf_uninstall_rmdir( $wbf_dir );
|
||||||
$upload_base = wp_upload_dir()['basedir'];
|
}
|
||||||
// Sicherheitscheck: nur Unterverzeichnisse von uploads/ löschen
|
|
||||||
if ( strpos( realpath( $dir ), realpath( $upload_base ) ) !== 0 ) {
|
/**
|
||||||
return;
|
* Hilfsfunktion: Verzeichnis rekursiv löschen.
|
||||||
}
|
* Nur innerhalb des WP-Upload-Verzeichnisses erlaubt.
|
||||||
$files = array_diff( scandir( $dir ), [ '.', '..' ] );
|
*/
|
||||||
foreach ( $files as $file ) {
|
function wbf_uninstall_rmdir( $dir ) {
|
||||||
$path = $dir . DIRECTORY_SEPARATOR . $file;
|
$upload_base = wp_upload_dir()['basedir'];
|
||||||
is_dir( $path ) ? wbf_uninstall_rmdir( $path ) : unlink( $path );
|
// Sicherheitscheck: nur Unterverzeichnisse von uploads/ löschen
|
||||||
}
|
if ( strpos( realpath( $dir ), realpath( $upload_base ) ) !== 0 ) {
|
||||||
rmdir( $dir );
|
return;
|
||||||
|
}
|
||||||
|
$files = array_diff( scandir( $dir ), [ '.', '..' ] );
|
||||||
|
foreach ( $files as $file ) {
|
||||||
|
$path = $dir . DIRECTORY_SEPARATOR . $file;
|
||||||
|
is_dir( $path ) ? wbf_uninstall_rmdir( $path ) : unlink( $path );
|
||||||
|
}
|
||||||
|
rmdir( $dir );
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/*
|
||||||
* Plugin Name: WP Business Forum
|
Plugin Name: WP Business Forum
|
||||||
* Plugin URI: https://git.viper.ipv64.net/M_Viper/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.
|
Description: Professionelles Forum mit eigenem Login, Rollen, Signaturen, Hierarchie und Moderations-Tools.
|
||||||
* Version: 1.0.1
|
Version: 1.0.5
|
||||||
* Author: M_Viper
|
Author: M_Viper
|
||||||
* Author URI: https://m-viper.de
|
Author URI: https://m-viper.de
|
||||||
* Text Domain: wp-business-forum
|
Requires at least: 6.8
|
||||||
*/
|
Tested up to: 6.8
|
||||||
|
PHP Version: 7.4
|
||||||
|
License: GPL2
|
||||||
|
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
Text Domain: wp-business-forum
|
||||||
|
Tags: forum, community, roles, moderation, shortcode, bbcode, admin, frontend, security, export-import
|
||||||
|
Support: [Discord Support](https://discord.com/invite/FdRs4BRd8D)
|
||||||
|
Support: [Telegram Support](https://t.me/M_Viper04)
|
||||||
|
*/
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||||
|
|
||||||
define( 'WBF_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'WBF_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
define( 'WBF_URL', plugin_dir_url( __FILE__ ) );
|
define( 'WBF_URL', plugin_dir_url( __FILE__ ) );
|
||||||
define( 'WBF_VERSION', '1.0.1' );
|
define( 'WBF_VERSION', '1.0.5' );
|
||||||
|
|
||||||
require_once WBF_PATH . 'includes/class-forum-db.php';
|
require_once WBF_PATH . 'includes/class-forum-db.php';
|
||||||
require_once WBF_PATH . 'includes/class-forum-roles.php';
|
require_once WBF_PATH . 'includes/class-forum-roles.php';
|
||||||
@@ -22,6 +30,10 @@ require_once WBF_PATH . 'includes/class-forum-bbcode.php';
|
|||||||
require_once WBF_PATH . 'includes/class-forum-auth.php';
|
require_once WBF_PATH . 'includes/class-forum-auth.php';
|
||||||
require_once WBF_PATH . 'includes/class-forum-shortcodes.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-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-admin.php';
|
||||||
require_once WBF_PATH . 'admin/forum-settings.php';
|
require_once WBF_PATH . 'admin/forum-settings.php';
|
||||||
require_once WBF_PATH . 'admin/forum-setup.php';
|
require_once WBF_PATH . 'admin/forum-setup.php';
|
||||||
@@ -33,6 +45,38 @@ register_activation_hook( __FILE__, function() {
|
|||||||
set_transient( 'wbf_activation_redirect', true, 30 );
|
set_transient( 'wbf_activation_redirect', true, 30 );
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Export / Import Hooks ─────────────────────────────────────────────────────
|
||||||
|
add_action( 'plugins_loaded', function() {
|
||||||
|
WBF_Export::hooks();
|
||||||
|
}, 5 );
|
||||||
|
|
||||||
|
// ── DB-Schema sicherstellen (läuft bei jedem Seitenaufruf, sehr günstig) ─────
|
||||||
|
// Stellt sicher dass neue Spalten auch auf bestehenden Installs vorhanden sind,
|
||||||
|
// ohne dass das Plugin erneut deaktiviert/aktiviert werden muss.
|
||||||
|
add_action( 'plugins_loaded', function() {
|
||||||
|
$db_ver = (int) get_option( 'wbf_db_version', 0 );
|
||||||
|
if ( $db_ver < 2 ) {
|
||||||
|
global $wpdb;
|
||||||
|
// profile_public: Sicherheits-kritisch — muss immer existieren
|
||||||
|
$cols = $wpdb->get_col( "DESCRIBE {$wpdb->prefix}forum_users" );
|
||||||
|
if ( ! in_array( 'profile_public', $cols ) ) {
|
||||||
|
$wpdb->query( "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) NOT NULL DEFAULT 1" );
|
||||||
|
// Alle bestehenden User explizit auf öffentlich setzen
|
||||||
|
$wpdb->query( "UPDATE {$wpdb->prefix}forum_users SET profile_public = 1 WHERE profile_public IS NULL" );
|
||||||
|
}
|
||||||
|
update_option( 'wbf_db_version', 2 );
|
||||||
|
}
|
||||||
|
}, 10 );
|
||||||
|
|
||||||
|
// ── Session frühzeitig starten (PHP 8.3 Fix) ────────────────────────────────
|
||||||
|
// session_start() MUSS vor jedem HTML-Output laufen.
|
||||||
|
// plugins_loaded (Prio 1) ist der früheste sichere Zeitpunkt in WordPress.
|
||||||
|
// Der 'init'-Hook (in class-forum-auth.php) läuft als Fallback weiterhin,
|
||||||
|
// aber dieser frühe Aufruf verhindert den PHP 8.3 E_WARNING "headers already sent".
|
||||||
|
add_action( 'plugins_loaded', function() {
|
||||||
|
WBF_Auth::init();
|
||||||
|
}, 1 );
|
||||||
|
|
||||||
// ── Superadmin-Sync ───────────────────────────────────────────────────────────
|
// ── Superadmin-Sync ───────────────────────────────────────────────────────────
|
||||||
add_action( 'wp_login', function() { WBF_Roles::sync_superadmin(); } );
|
add_action( 'wp_login', function() { WBF_Roles::sync_superadmin(); } );
|
||||||
add_action( 'init', function() { WBF_Roles::sync_superadmin(); } );
|
add_action( 'init', function() { WBF_Roles::sync_superadmin(); } );
|
||||||
@@ -57,6 +101,7 @@ if ( ! wp_next_scheduled( 'wbf_check_expired_bans' ) ) {
|
|||||||
|
|
||||||
register_deactivation_hook( __FILE__, function() {
|
register_deactivation_hook( __FILE__, function() {
|
||||||
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
|
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
|
||||||
|
wp_clear_scheduled_hook( 'wbf_check_for_updates' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
||||||
@@ -68,14 +113,13 @@ function wbf_get_forum_url() {
|
|||||||
$url = get_permalink( $page_id );
|
$url = get_permalink( $page_id );
|
||||||
if ( $url ) return $url;
|
if ( $url ) return $url;
|
||||||
}
|
}
|
||||||
// 2. Fallback: Seite mit [business_forum] Shortcode suchen
|
// 2. Fallback: Seite mit [business_forum] Shortcode suchen (direkt im post_content)
|
||||||
$pages = get_posts([
|
global $wpdb;
|
||||||
'post_type' => 'page',
|
$page_id = $wpdb->get_var( $wpdb->prepare(
|
||||||
'post_status' => 'publish',
|
"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status = 'publish' AND post_content LIKE %s LIMIT 1",
|
||||||
'posts_per_page' => 1,
|
'%[business_forum]%'
|
||||||
's' => 'business_forum',
|
) );
|
||||||
]);
|
if ( $page_id ) return get_permalink( $page_id );
|
||||||
if ( $pages ) return get_permalink( $pages[0]->ID );
|
|
||||||
// 3. Letzter Fallback: aktuelle Seite
|
// 3. Letzter Fallback: aktuelle Seite
|
||||||
return home_url('/');
|
return home_url('/');
|
||||||
}
|
}
|
||||||
@@ -84,6 +128,10 @@ function wbf_get_forum_url() {
|
|||||||
add_action( 'wp_enqueue_scripts', function() {
|
add_action( 'wp_enqueue_scripts', function() {
|
||||||
wp_enqueue_style( 'wbf-style', WBF_URL . 'assets/css/forum-style.css', [], WBF_VERSION );
|
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 );
|
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();
|
$wbf_user = WBF_Auth::get_current_user();
|
||||||
if ( $wbf_user ) {
|
if ( $wbf_user ) {
|
||||||
WBF_DB::touch_last_active( $wbf_user->id );
|
WBF_DB::touch_last_active( $wbf_user->id );
|
||||||
@@ -98,4 +146,454 @@ add_action( 'wp_enqueue_scripts', function() {
|
|||||||
'forum_url' => wbf_get_forum_url(),
|
'forum_url' => wbf_get_forum_url(),
|
||||||
'reactions' => WBF_DB::get_allowed_reactions(),
|
'reactions' => WBF_DB::get_allowed_reactions(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// ── Update-Checker ────────────────────────────────────────────────────────────
|
||||||
|
// Prüft täglich gegen die Gitea-Releases-API ob eine neue Version verfügbar ist.
|
||||||
|
// Releases-URL: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum/releases
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
define( 'WBF_UPDATE_API', 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/WP-Business-Forum/releases?limit=1&page=1' );
|
||||||
|
define( 'WBF_RELEASES_PAGE', 'https://git.viper.ipv64.net/M_Viper/WP-Business-Forum/releases' );
|
||||||
|
define( 'WBF_UPDATE_TRANSIENT','wbf_update_check' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt die neueste Release-Info von Gitea (gecacht per Transient, 12h).
|
||||||
|
* Gibt null zurück wenn kein Update verfügbar oder API nicht erreichbar.
|
||||||
|
*
|
||||||
|
* @return array|null ['version'=>string, 'url'=>string, 'name'=>string, 'published'=>string, 'body'=>string]
|
||||||
|
*/
|
||||||
|
function wbf_get_latest_release() {
|
||||||
|
$cached = get_transient( WBF_UPDATE_TRANSIENT );
|
||||||
|
if ( $cached !== false ) {
|
||||||
|
return $cached ?: null; // false = noch nie gecacht, '' = kein Update
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get( WBF_UPDATE_API, [
|
||||||
|
'timeout' => 8,
|
||||||
|
'user-agent' => 'WP-Business-Forum/' . WBF_VERSION . '; ' . get_bloginfo('url'),
|
||||||
|
'sslverify' => true,
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200 ) {
|
||||||
|
// Bei Fehler 1h warten bevor erneut versucht
|
||||||
|
set_transient( WBF_UPDATE_TRANSIENT, '', HOUR_IN_SECONDS );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$releases = json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( empty($releases) || ! is_array($releases) || empty($releases[0]) ) {
|
||||||
|
set_transient( WBF_UPDATE_TRANSIENT, '', 12 * HOUR_IN_SECONDS );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$latest = $releases[0];
|
||||||
|
$version = ltrim( $latest['tag_name'] ?? '', 'v' ); // "v1.2.0" → "1.2.0"
|
||||||
|
|
||||||
|
$info = [
|
||||||
|
'version' => $version,
|
||||||
|
'url' => $latest['html_url'] ?? WBF_RELEASES_PAGE,
|
||||||
|
'name' => $latest['name'] ?? $latest['tag_name'] ?? $version,
|
||||||
|
'published' => $latest['published_at'] ?? '',
|
||||||
|
'body' => wp_strip_all_tags( $latest['body'] ?? '' ),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 12 Stunden cachen
|
||||||
|
set_transient( WBF_UPDATE_TRANSIENT, $info, 12 * HOUR_IN_SECONDS );
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob ein Update verfügbar ist.
|
||||||
|
* Gibt die Release-Info zurück wenn Gitea-Version > installierte Version.
|
||||||
|
*/
|
||||||
|
function wbf_update_available() {
|
||||||
|
$latest = wbf_get_latest_release();
|
||||||
|
if ( ! $latest || empty($latest['version']) ) return null;
|
||||||
|
if ( version_compare( $latest['version'], WBF_VERSION, '>' ) ) {
|
||||||
|
return $latest;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cron: täglich Update prüfen (Cache warm halten) ──────────────────────────
|
||||||
|
add_action( 'wbf_check_for_updates', function() {
|
||||||
|
delete_transient( WBF_UPDATE_TRANSIENT );
|
||||||
|
wbf_get_latest_release();
|
||||||
|
} );
|
||||||
|
|
||||||
|
if ( ! wp_next_scheduled( 'wbf_check_for_updates' ) ) {
|
||||||
|
wp_schedule_event( time(), 'twicedaily', 'wbf_check_for_updates' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Admin-Notice wenn Update verfügbar ───────────────────────────────────────
|
||||||
|
add_action( 'admin_notices', function() {
|
||||||
|
if ( ! current_user_can('manage_options') ) return;
|
||||||
|
|
||||||
|
$update = wbf_update_available();
|
||||||
|
if ( ! $update ) return;
|
||||||
|
|
||||||
|
// Notice ausblenden wenn der User sie weggeklickt hat (per GET-Parameter)
|
||||||
|
if ( isset($_GET['wbf_dismiss_update']) && check_admin_referer('wbf_dismiss_update') ) {
|
||||||
|
set_transient( 'wbf_update_dismissed_' . WBF_VERSION, $update['version'], 7 * DAY_IN_SECONDS );
|
||||||
|
wp_safe_redirect( remove_query_arg(['wbf_dismiss_update','_wpnonce']) );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dismissed = get_transient( 'wbf_update_dismissed_' . WBF_VERSION );
|
||||||
|
if ( $dismissed === $update['version'] ) return;
|
||||||
|
|
||||||
|
$dismiss_url = wp_nonce_url(
|
||||||
|
add_query_arg('wbf_dismiss_update', '1'),
|
||||||
|
'wbf_dismiss_update'
|
||||||
|
);
|
||||||
|
$changelog_url = esc_url( $update['url'] );
|
||||||
|
$new_ver = esc_html( $update['version'] );
|
||||||
|
$cur_ver = esc_html( WBF_VERSION );
|
||||||
|
|
||||||
|
echo "
|
||||||
|
<div class=\"notice notice-warning is-dismissible\" style=\"border-left-color:#f59e0b;padding:12px 15px\">
|
||||||
|
<div style=\"display:flex;align-items:center;gap:14px;flex-wrap:wrap\">
|
||||||
|
<span style=\"font-size:1.6rem\">🔔</span>
|
||||||
|
<div>
|
||||||
|
<strong style=\"font-size:.95rem\">WP Business Forum — Update verfügbar!</strong>
|
||||||
|
<p style=\"margin:.3rem 0 0;color:#374151\">
|
||||||
|
Version <strong>{$new_ver}</strong> ist verfügbar. Du verwendest <strong>{$cur_ver}</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style=\"display:flex;gap:8px;margin-left:auto\">
|
||||||
|
<a href=\"{$changelog_url}\" target=\"_blank\" rel=\"noopener\"
|
||||||
|
class=\"button button-primary\" style=\"background:#f59e0b;border-color:#d97706\">
|
||||||
|
📋 Changelog & Download
|
||||||
|
</a>
|
||||||
|
<a href=\"" . esc_url($dismiss_url) . "\" class=\"button\">Später erinnern</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>";
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ── Update-Badge im WP-Admin-Menü ─────────────────────────────────────────────
|
||||||
|
add_action( 'admin_menu', function() {
|
||||||
|
$update = wbf_update_available();
|
||||||
|
if ( ! $update ) return;
|
||||||
|
global $menu;
|
||||||
|
if ( ! is_array($menu) ) return;
|
||||||
|
foreach ( $menu as &$item ) {
|
||||||
|
if ( isset($item[2]) && $item[2] === 'wbf-admin' ) {
|
||||||
|
$item[0] .= ' <span class="update-plugins"><span class="plugin-count">1</span></span>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 999 );
|
||||||
|
|
||||||
|
// ── Manuellen Cache-Reset erlauben (für die Admin-UI) ─────────────────────────
|
||||||
|
add_action( 'admin_init', function() {
|
||||||
|
if ( ! isset($_GET['wbf_refresh_update']) ) return;
|
||||||
|
if ( ! current_user_can('manage_options') ) return;
|
||||||
|
if ( ! check_admin_referer('wbf_refresh_update') ) return;
|
||||||
|
delete_transient( WBF_UPDATE_TRANSIENT );
|
||||||
|
wp_safe_redirect( remove_query_arg(['wbf_refresh_update','_wpnonce']) );
|
||||||
|
exit;
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// ── 2FA Inline-JavaScript ─────────────────────────────────────────────────────
|
||||||
|
// Liefert das JS für: Login-2FA-Step, Profil-Setup-Wizard, Deaktivierung
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function wbf_get_2fa_inline_js() {
|
||||||
|
return <<<'JS'
|
||||||
|
(function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════
|
||||||
|
2FA — Login-Flow
|
||||||
|
Wenn der Server 2fa_required:true zurückgibt, zeigt das
|
||||||
|
Login-Formular eine Code-Eingabe anstatt die Fehlermeldung.
|
||||||
|
══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
// Original-Login-Handler überschreiben um 2FA abzufangen
|
||||||
|
$(document).off('click', '.wbf-login-submit-btn');
|
||||||
|
$(document).on('click', '.wbf-login-submit-btn', function () {
|
||||||
|
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||||
|
var $box = $(this).closest('.wbf-auth-box');
|
||||||
|
|
||||||
|
// 2FA-Panel verstecken falls sichtbar
|
||||||
|
$box.find('.wbf-2fa-login-step').remove();
|
||||||
|
|
||||||
|
$.post(WBF.ajax_url, {
|
||||||
|
action: 'wbf_login',
|
||||||
|
nonce: WBF.nonce,
|
||||||
|
username: $box.find('.wbf-field-username').val(),
|
||||||
|
password: $box.find('.wbf-field-password').val(),
|
||||||
|
remember_me: $box.find('.wbf-field-remember').is(':checked') ? '1' : ''
|
||||||
|
}, function (res) {
|
||||||
|
if (res && res.success) {
|
||||||
|
location.reload();
|
||||||
|
} else if (res && res.data && res.data['2fa_required']) {
|
||||||
|
// 2FA erforderlich — Code-Eingabe einblenden
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-sign-in-alt"></i> Einloggen');
|
||||||
|
wbfShow2faLoginStep($box);
|
||||||
|
} else {
|
||||||
|
var msg = (res && res.data && res.data.message) ? res.data.message : 'Fehler';
|
||||||
|
$box.find('.wbf-login-msg').text(msg).css('color', '#f05252').show();
|
||||||
|
setTimeout(function () { $box.find('.wbf-login-msg').fadeOut(); }, 4000);
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-sign-in-alt"></i> Einloggen');
|
||||||
|
}
|
||||||
|
}, 'json').fail(function (xhr) {
|
||||||
|
$box.find('.wbf-login-msg').text('Verbindungsfehler (' + xhr.status + ')').css('color', '#f05252').show();
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-sign-in-alt"></i> Einloggen');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function wbfShow2faLoginStep($box) {
|
||||||
|
// Altes Modal entfernen falls vorhanden
|
||||||
|
$('#wbf2faLoginModal').remove();
|
||||||
|
|
||||||
|
var modal =
|
||||||
|
'<div id="wbf2faLoginModal" class="wbf-2fa-modal-overlay">' +
|
||||||
|
'<div class="wbf-2fa-modal-box">' +
|
||||||
|
'<div class="wbf-2fa-modal-header">' +
|
||||||
|
'<span class="wbf-2fa-modal-icon">🛡️</span>' +
|
||||||
|
'<div>' +
|
||||||
|
'<strong class="wbf-2fa-modal-title">Zwei-Faktor-Authentifizierung</strong>' +
|
||||||
|
'<p class="wbf-2fa-modal-sub">Gib den Code aus deiner Authenticator-App ein.</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<input type="text" class="wbf-2fa-code-input" placeholder="1 2 3 4 5 6"' +
|
||||||
|
' maxlength="7" inputmode="numeric" autocomplete="one-time-code">' +
|
||||||
|
'<div class="wbf-2fa-modal-actions">' +
|
||||||
|
'<button class="wbf-btn wbf-btn--primary wbf-2fa-submit-btn">' +
|
||||||
|
'<i class="fas fa-check"></i> Bestätigen' +
|
||||||
|
'</button>' +
|
||||||
|
'<button class="wbf-btn wbf-2fa-cancel-btn">' +
|
||||||
|
'<i class="fas fa-xmark"></i> Abbrechen' +
|
||||||
|
'</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<span class="wbf-2fa-msg"></span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
$('body').append(modal);
|
||||||
|
// Kurze Verzögerung für CSS-Transition
|
||||||
|
setTimeout(function () {
|
||||||
|
$('#wbf2faLoginModal').addClass('wbf-2fa-modal--visible');
|
||||||
|
$('#wbf2faLoginModal .wbf-2fa-code-input').focus();
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2FA-Code absenden
|
||||||
|
$(document).on('click', '.wbf-2fa-submit-btn', function () {
|
||||||
|
var $step = $(this).closest('.wbf-2fa-modal-box');
|
||||||
|
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||||
|
var code = $step.find('.wbf-2fa-code-input').val().replace(/\s+/g, '');
|
||||||
|
|
||||||
|
if (code.length !== 6) {
|
||||||
|
$step.find('.wbf-2fa-msg').text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show();
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Code bestätigen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.post(WBF.ajax_url, {
|
||||||
|
action: 'wbf_2fa_verify_login',
|
||||||
|
code: code
|
||||||
|
}, function (res) {
|
||||||
|
if (res && res.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
var msg = (res && res.data && res.data.message) ? res.data.message : 'Ungültiger Code.';
|
||||||
|
$step.find('.wbf-2fa-msg').text(msg).css('color', '#f05252').show();
|
||||||
|
$step.find('.wbf-2fa-code-input').val('').focus();
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Code bestätigen');
|
||||||
|
}
|
||||||
|
}, 'json').fail(function (xhr) {
|
||||||
|
$step.find('.wbf-2fa-msg').text('Verbindungsfehler.').css('color', '#f05252').show();
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Code bestätigen');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter-Taste im Code-Feld
|
||||||
|
$(document).on('keydown', '.wbf-2fa-code-input', function (e) {
|
||||||
|
if (e.key === 'Enter') $(this).closest('.wbf-2fa-login-step').find('.wbf-2fa-submit-btn').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Abbrechen: 2FA-Modal schließen
|
||||||
|
$(document).on('click', '.wbf-2fa-cancel-btn', function () {
|
||||||
|
var $modal = $('#wbf2faLoginModal');
|
||||||
|
$modal.removeClass('wbf-2fa-modal--visible');
|
||||||
|
setTimeout(function () { $modal.remove(); }, 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klick außerhalb des Modals schließt es
|
||||||
|
$(document).on('click', '#wbf2faLoginModal', function (e) {
|
||||||
|
if ($(e.target).is('#wbf2faLoginModal')) {
|
||||||
|
var $modal = $(this);
|
||||||
|
$modal.removeClass('wbf-2fa-modal--visible');
|
||||||
|
setTimeout(function () { $modal.remove(); }, 250);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════
|
||||||
|
2FA — Profil-Setup-Wizard
|
||||||
|
══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
// Schritt 1 starten: Secret + QR generieren
|
||||||
|
$(document).on('click', '#wbf2faStartBtn', function () {
|
||||||
|
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Lädt…');
|
||||||
|
|
||||||
|
$.post(WBF.ajax_url, {
|
||||||
|
action: 'wbf_2fa_setup_begin',
|
||||||
|
nonce: WBF.nonce
|
||||||
|
}, function (res) {
|
||||||
|
if (!res || !res.success) {
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-shield-halved"></i> 2FA einrichten');
|
||||||
|
alert((res && res.data && res.data.message) ? res.data.message : 'Fehler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var secret = res.data.secret;
|
||||||
|
var uri = res.data.uri;
|
||||||
|
|
||||||
|
// QR-Code rendern (qrcodejs)
|
||||||
|
$('#wbf2faQr').empty();
|
||||||
|
if (typeof QRCode !== 'undefined') {
|
||||||
|
// QR-Code in isolierten Wrapper einbetten (kein Flex-Kontext)
|
||||||
|
var qrEl = document.getElementById('wbf2faQr');
|
||||||
|
qrEl.innerHTML = '';
|
||||||
|
|
||||||
|
new QRCode(qrEl, {
|
||||||
|
text: uri,
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
correctLevel: QRCode.CorrectLevel.M
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kein JS-Eingriff nötig — CSS übernimmt Größe + img-Verstecken
|
||||||
|
} else {
|
||||||
|
// Fallback: Link anzeigen
|
||||||
|
$('#wbf2faQr').html(
|
||||||
|
'<a href="' + uri + '" style="font-size:.75rem;word-break:break-all">otpauth Link</a>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Secret für manuelle Eingabe formatiert anzeigen (Leerzeichen alle 4 Zeichen)
|
||||||
|
var fmt = secret.replace(/=/g, '').replace(/(.{4})/g, '$1 ').trim();
|
||||||
|
$('#wbf2faSecret').text(fmt);
|
||||||
|
|
||||||
|
// Panels tauschen
|
||||||
|
$('#wbf2faInactive').hide();
|
||||||
|
$('#wbf2faStep1').fadeIn(200);
|
||||||
|
}, 'json').fail(function () {
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-shield-halved"></i> 2FA einrichten');
|
||||||
|
alert('Verbindungsfehler. Bitte Seite neu laden.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Weiter zu Schritt 2
|
||||||
|
$(document).on('click', '#wbf2faToStep2', function () {
|
||||||
|
$('#wbf2faStep1').hide();
|
||||||
|
$('#wbf2faStep2').fadeIn(200);
|
||||||
|
$('#wbf2faVerifyCode').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zurück zu Schritt 1
|
||||||
|
$(document).on('click', '#wbf2faBackBtn', function () {
|
||||||
|
$('#wbf2faStep2').hide();
|
||||||
|
$('#wbf2faStep1').fadeIn(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schritt 2: Code bestätigen und 2FA aktivieren
|
||||||
|
$(document).on('click', '#wbf2faVerifyBtn', function () {
|
||||||
|
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||||
|
var $msg = $('#wbf2faVerifyMsg');
|
||||||
|
var code = $('#wbf2faVerifyCode').val().replace(/\s+/g, '');
|
||||||
|
|
||||||
|
if (code.length !== 6) {
|
||||||
|
$msg.text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show();
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen & aktivieren');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.post(WBF.ajax_url, {
|
||||||
|
action: 'wbf_2fa_setup_verify',
|
||||||
|
nonce: WBF.nonce,
|
||||||
|
code: code
|
||||||
|
}, function (res) {
|
||||||
|
if (res && res.success) {
|
||||||
|
$('#wbf2faStep2').hide();
|
||||||
|
$('#wbf2faStep3').fadeIn(300);
|
||||||
|
// Badge im Header aktualisieren
|
||||||
|
$('#wbf2faCard .wbf-2fa-badge')
|
||||||
|
.removeClass('wbf-2fa-badge--off')
|
||||||
|
.addClass('wbf-2fa-badge--on')
|
||||||
|
.html('<i class="fas fa-check-circle"></i> Aktiv');
|
||||||
|
// Nach 2 Sek. Seite neu laden damit der Header-Status stimmt
|
||||||
|
setTimeout(function () { location.reload(); }, 2500);
|
||||||
|
} else {
|
||||||
|
var msg = (res && res.data && res.data.message) ? res.data.message : 'Ungültiger Code.';
|
||||||
|
$msg.text(msg).css('color', '#f05252').show();
|
||||||
|
$('#wbf2faVerifyCode').val('').focus();
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen & aktivieren');
|
||||||
|
}
|
||||||
|
}, 'json').fail(function () {
|
||||||
|
$msg.text('Verbindungsfehler.').css('color', '#f05252').show();
|
||||||
|
$btn.prop('disabled', false).html('<i class="fas fa-check"></i> Bestätigen & aktivieren');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter-Taste im Verifikationsfeld
|
||||||
|
$(document).on('keydown', '#wbf2faVerifyCode', function (e) {
|
||||||
|
if (e.key === 'Enter') $('#wbf2faVerifyBtn').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════
|
||||||
|
2FA — Deaktivierung (Profil)
|
||||||
|
══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
$(document).on('click', '#wbf2faDisableBtn', function () {
|
||||||
|
var $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||||
|
var $msg = $('#wbf2faDisableMsg');
|
||||||
|
var pw = $('#wbf2faDisablePw').val();
|
||||||
|
var code = $('#wbf2faDisableCode').val().replace(/\s+/g, '');
|
||||||
|
|
||||||
|
if (!pw) {
|
||||||
|
$msg.text('Bitte Passwort eingeben.').css('color', '#f05252').show();
|
||||||
|
$btn.prop('disabled', false)
|
||||||
|
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code.length !== 6) {
|
||||||
|
$msg.text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show();
|
||||||
|
$btn.prop('disabled', false)
|
||||||
|
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.post(WBF.ajax_url, {
|
||||||
|
action: 'wbf_2fa_disable',
|
||||||
|
nonce: WBF.nonce,
|
||||||
|
password: pw,
|
||||||
|
code: code
|
||||||
|
}, function (res) {
|
||||||
|
if (res && res.success) {
|
||||||
|
$msg.text('✔ ' + (res.data.message || '2FA deaktiviert.')).css('color', '#56cf7e').show();
|
||||||
|
setTimeout(function () { location.reload(); }, 1500);
|
||||||
|
} else {
|
||||||
|
var msg = (res && res.data && res.data.message) ? res.data.message : 'Fehler.';
|
||||||
|
$msg.text(msg).css('color', '#f05252').show();
|
||||||
|
$('#wbf2faDisableCode').val('').focus();
|
||||||
|
$btn.prop('disabled', false)
|
||||||
|
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
|
||||||
|
}
|
||||||
|
}, 'json').fail(function () {
|
||||||
|
$msg.text('Verbindungsfehler.').css('color', '#f05252').show();
|
||||||
|
$btn.prop('disabled', false)
|
||||||
|
.html('<i class="fas fa-shield-xmark"></i> 2FA deaktivieren');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}(jQuery));
|
||||||
|
JS;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user