11 Commits
1.0.1 ... 1.0.3

14 changed files with 4733 additions and 586 deletions

370
README.md
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -52,6 +52,8 @@ if ( ! function_exists('wbf_get_settings') ) {
'rules_accept_required' => '1',
'rules_title' => 'Forum-Regeln & Nutzungsbedingungen',
'rules_content' => "**1. Respektvoller Umgang**\nBehandle alle Mitglieder freundlich und respektvoll. Beleidigungen, Mobbing und Diskriminierung sind nicht toleriert.\n\n**2. Keine Spam-Inhalte**\nWerbung, Spam und irrelevante Links sind verboten.\n\n**3. Keine illegalen Inhalte**\nJegliche Inhalte, die gegen geltendes Recht verstoßen, sind streng verboten.\n\n**4. Themenrelevanz**\nBeiträge sollten zur jeweiligen Kategorie passen.\n\n**5. Urheberrecht**\nVeröffentliche keine Inhalte, an denen du keine Rechte besitzt.\n\n**6. Datenschutz**\nTeile keine persönlichen Daten anderer Personen ohne deren Zustimmung.\n\n**7. Moderations-Entscheidungen**\nEntscheidungen der Moderatoren sind zu respektieren. Bei Fragen wende dich direkt ans Team.\n\nVerstöße können zur Verwarnung oder dauerhaften Sperrung führen.",
// Ignore/Block-System: Rollen die nicht geblockt werden können (kommagetrennte Schlüssel)
'ignore_blocked_roles' => 'superadmin,admin,moderator',
];
$saved = get_option( 'wbf_settings', [] );
@@ -63,6 +65,38 @@ if ( ! function_exists('wbf_get_settings') ) {
// ── Admin-Seite ───────────────────────────────────────────────────────────────
/**
* Gibt ein Array der Rollen-Keys zurück die nicht geblockt/ignoriert werden können.
* Superadmin ist immer enthalten — unabhängig von der Einstellung.
*
* @return string[] z.B. ['superadmin', 'admin', 'moderator']
*/
if ( ! function_exists('wbf_get_ignore_blocked_roles') ) {
function wbf_get_ignore_blocked_roles() {
$raw = wbf_get_settings()['ignore_blocked_roles'] ?? 'superadmin,admin,moderator';
$keys = array_filter( array_map( 'trim', explode( ',', $raw ) ) );
// superadmin immer schützen
if ( ! in_array('superadmin', $keys, true) ) {
$keys[] = 'superadmin';
}
return array_values( $keys );
}
}
/**
* Prüft ob ein User ignoriert/geblockt werden darf.
*
* @param object $target Forum-User-Objekt
* @return bool true = darf ignoriert werden, false = nicht erlaubt
*/
if ( ! function_exists('wbf_can_be_ignored') ) {
function wbf_can_be_ignored( $target ) {
if ( ! $target ) return false;
$blocked_roles = wbf_get_ignore_blocked_roles();
return ! in_array( $target->role, $blocked_roles, true );
}
}
if ( ! function_exists('wbf_admin_settings') ) {
function wbf_admin_settings() {
@@ -96,6 +130,25 @@ function wbf_admin_settings() {
// rules_content separat (nicht in $fields, da textarea mit eigener Behandlung)
$settings['rules_content'] = sanitize_textarea_field( $_POST['rules_content'] ?? '' );
// 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'];
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 );
echo '<div class="notice notice-success is-dismissible"><p>✅ Einstellungen gespeichert!</p></div>';
}
@@ -401,6 +454,51 @@ function wbf_admin_settings() {
</tr>
</table>
<!-- ── Ignore / Block-System ────────────────────────── -->
<h2 style="border-bottom:1px solid #ddd;padding-bottom:.4rem;margin-top:1.5rem">
🚫 Ignore / Block-System
</h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label>Nicht blockierbare Rollen</label></th>
<td>
<?php
$blocked_roles = array_filter( array_map('trim', explode(',', $s['ignore_blocked_roles'] ?? 'superadmin,admin,moderator')) );
$all_roles = WBF_Roles::get_sorted();
foreach ( $all_roles as $key => $role ):
$is_superadmin = ($key === 'superadmin');
$is_checked = in_array($key, $blocked_roles, true);
$rc = esc_attr($role['color']);
$rb = esc_attr($role['bg_color']);
?>
<label style="display:flex;align-items:center;gap:8px;margin-bottom:7px;cursor:<?php echo $is_superadmin?'not-allowed':'pointer'; ?>">
<input type="checkbox"
name="ignore_blocked_roles[]"
value="<?php echo esc_attr($key); ?>"
<?php checked($is_checked, true); ?>
<?php echo $is_superadmin ? 'disabled' : ''; ?>
style="width:16px;height:16px;accent-color:<?php echo $rc; ?>">
<?php if ($is_superadmin): ?>
<!-- superadmin immer als hidden mitschicken da disabled nicht übermittelt wird -->
<input type="hidden" name="ignore_blocked_roles[]" value="superadmin">
<?php endif; ?>
<span style="display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:20px;font-size:.78rem;font-weight:700;color:<?php echo $rc; ?>;background:<?php echo $rb; ?>;border:1px solid <?php echo $rc; ?>">
<i class="<?php echo esc_attr($role['icon'] ?? 'fas fa-user'); ?>"></i>
<?php echo esc_html($role['label']); ?>
</span>
<?php if ($is_superadmin): ?>
<span style="font-size:.72rem;color:#999">(immer geschützt)</span>
<?php endif; ?>
</label>
<?php endforeach; ?>
<p class="description" style="margin-top:8px">
Nutzer mit diesen Rollen können von anderen Mitgliedern <strong>nicht</strong> geblockt oder ignoriert werden.
Superadmin ist permanent geschützt und kann nicht abgewählt werden.
</p>
</td>
</tr>
</table>
<?php submit_button(
'💾 Einstellungen speichern',
'primary',

View File

@@ -148,15 +148,17 @@
border-color: var(--c-border-d);
}
.wbf-btn:hover { background: var(--c-surface); color: var(--c-text); border-color: var(--c-primary); }
.wbf-btn--primary {
background: var(--c-primary); color: #fff;
.wbf-btn--primary,
a.wbf-btn--primary {
background: var(--c-primary); color: #fff !important;
border-color: var(--c-primary);
box-shadow: 0 0 12px rgba(0,180,216,.3);
}
.wbf-btn--primary:hover {
.wbf-btn--primary:hover,
a.wbf-btn--primary:hover {
background: var(--c-primary-d); border-color: var(--c-primary-d);
box-shadow: 0 0 20px rgba(0,180,216,.45);
color: #fff;
color: #fff !important;
}
.wbf-btn--outline {
background: transparent; border-color: rgba(255,255,255,.18);
@@ -2831,4 +2833,888 @@ select.wbf-cf-input option { background: var(--c-surface2); color: var(--c-text)
border-color: #fbbf24;
color: #fbbf24;
background: rgba(251,191,36,.12);
}
/* ── Ignore / Block ────────────────────────────────────────────────────────── */
/* Eingeklappter Post-Wrapper */
.wbf-post--ignored {
background: transparent;
border: 1px solid rgba(148,163,184,.15);
border-radius: var(--radius-sm);
overflow: hidden;
}
/* Info-Bar die anstelle des Posts erscheint */
.wbf-ignored-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: .75rem;
padding: .6rem 1rem;
background: rgba(71,85,105,.18);
color: var(--c-muted);
font-size: .8rem;
flex-wrap: wrap;
}
.wbf-ignored-bar span {
display: flex;
align-items: center;
gap: .4rem;
}
.wbf-ignored-bar strong {
color: var(--c-text-dim);
}
/* "Trotzdem anzeigen"-Button in der ignored-bar */
.wbf-show-ignored-btn {
background: none;
border: 1px solid rgba(148,163,184,.3);
border-radius: var(--radius-sm);
color: var(--c-muted);
font-size: .75rem;
padding: 2px 8px;
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
flex-shrink: 0;
}
.wbf-show-ignored-btn:hover {
border-color: var(--c-primary);
color: var(--c-primary);
}
/* Ignore-Button in Post-Footer */
.wbf-ignore-btn {
background: none;
border: none;
cursor: pointer;
color: var(--c-muted);
padding: 2px 6px;
border-radius: 4px;
font-size: .82rem;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: .3rem;
}
.wbf-ignore-btn:hover {
color: #f97316;
background: rgba(249,115,22,.08);
}
/* Profil-Button-Variante (mit Border via wbf-btn--sm) */
.wbf-btn.wbf-ignore-btn {
border: 1.5px solid rgba(148,163,184,.3);
padding: .35rem .75rem;
}
.wbf-btn.wbf-ignore-btn:hover,
.wbf-btn.wbf-ignore-btn[data-ignored="1"] {
border-color: #f97316;
color: #f97316;
background: rgba(249,115,22,.08);
}
/* Ignorierte-Nutzer-Liste im Profil */
.wbf-ignore-list {
display: flex;
flex-direction: column;
gap: .5rem;
}
.wbf-ignore-item {
display: flex;
align-items: center;
gap: .75rem;
padding: .6rem .75rem;
border-radius: var(--radius-sm);
background: rgba(255,255,255,.03);
border: 1px solid var(--c-border);
transition: var(--transition);
}
.wbf-ignore-item:hover {
background: rgba(255,255,255,.05);
}
.wbf-ignore-item__avatar {
flex-shrink: 0;
text-decoration: none;
}
.wbf-ignore-item__info {
display: flex;
flex-direction: column;
gap: .15rem;
min-width: 0;
}
.wbf-ignore-item__name {
font-size: .88rem;
font-weight: 600;
color: var(--c-text);
text-decoration: none;
}
.wbf-ignore-item__name:hover {
color: var(--c-primary);
}
.wbf-ignore-item__since {
font-size: .73rem;
color: var(--c-muted);
}
/* ── Profil-Tabs ─────────────────────────────────────────────────────────── */
.wbf-profile-tabs {
display: flex;
gap: 0;
margin-bottom: 1.5rem;
border-bottom: 2px solid var(--c-border);
padding: 0;
overflow-x: auto;
scrollbar-width: none;
}
.wbf-profile-tabs::-webkit-scrollbar { display: none; }
.wbf-profile-tab {
display: inline-flex;
align-items: center;
gap: .45rem;
padding: .7rem 1.1rem;
font-size: .88rem;
font-weight: 600;
color: var(--c-muted);
text-decoration: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: color .15s, border-color .15s;
white-space: nowrap;
letter-spacing: .01em;
}
.wbf-profile-tab:hover {
color: var(--c-text);
border-bottom-color: rgba(255,255,255,.2);
}
.wbf-profile-tab.active {
color: var(--c-primary);
border-bottom-color: var(--c-primary);
}
.wbf-profile-tab i {
font-size: .8rem;
}
@media (max-width: 480px) {
.wbf-profile-tab {
padding: .6rem .75rem;
font-size: .82rem;
}
}
/* ── Notification Preferences ────────────────────────────────────────────── */
.wbf-notif-pref-list {
display: flex;
flex-direction: column;
gap: .5rem;
}
.wbf-notif-pref {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: .75rem 1rem;
background: rgba(255,255,255,.03);
border: 1px solid var(--c-border);
border-radius: var(--radius-sm);
cursor: pointer;
transition: var(--transition);
}
.wbf-notif-pref:hover {
background: rgba(255,255,255,.05);
}
.wbf-notif-pref__info {
display: flex;
flex-direction: column;
gap: .2rem;
}
.wbf-notif-pref__info span {
font-size: .88rem;
font-weight: 600;
color: var(--c-text);
display: flex;
align-items: center;
gap: .4rem;
}
.wbf-notif-pref__info span i {
color: var(--c-primary);
font-size: .78rem;
}
.wbf-notif-pref__info small {
font-size: .75rem;
color: var(--c-muted);
}
/* ── Toggle-Switch ───────────────────────────────────────────────────────── */
.wbf-toggle {
position: relative;
width: 42px;
height: 24px;
background: rgba(148,163,184,.25);
border-radius: 12px;
transition: background .2s;
flex-shrink: 0;
cursor: pointer;
border: 1px solid rgba(148,163,184,.2);
}
.wbf-toggle--on {
background: var(--c-primary);
border-color: var(--c-primary);
}
.wbf-toggle__knob {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
background: #fff;
border-radius: 50%;
transition: transform .2s;
box-shadow: 0 1px 3px rgba(0,0,0,.3);
}
.wbf-toggle--on .wbf-toggle__knob {
transform: translateX(18px);
}
/* ── Profil-Sidebar: Online-Status & Zuletzt aktiv ──────────────────────── */
.wbf-profile-online-badge {
display: inline-flex;
align-items: center;
gap: .35rem;
font-size: .75rem;
font-weight: 700;
color: #22c55e;
margin-top: .25rem;
letter-spacing: .02em;
}
.wbf-profile-online-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 6px #22c55e;
animation: wbf-pulse-green 2s infinite;
flex-shrink: 0;
}
@keyframes wbf-pulse-green {
0%, 100% { opacity: 1; box-shadow: 0 0 6px #22c55e; }
50% { opacity: .7; box-shadow: 0 0 10px #22c55e; }
}
.wbf-profile-lastseen {
display: inline-flex;
align-items: center;
gap: .3rem;
font-size: .73rem;
color: var(--c-muted);
margin-top: .25rem;
}
.wbf-profile-lastseen i {
font-size: .68rem;
opacity: .7;
}
/* ════════════════════════════════════════════════════════════════════
PROFIL v3 — Sidebar-Links Layout (wbf-prof)
════════════════════════════════════════════════════════════════════ */
.wbf-prof {
display: grid;
grid-template-columns: 260px 1fr;
gap: 1.5rem;
align-items: start;
margin-bottom: 2rem;
}
/* ── SIDEBAR ─────────────────────────────────────────────────────── */
.wbf-prof__sidebar {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius);
padding: 1.75rem 1.25rem 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: .75rem;
position: sticky;
top: 80px;
}
/* Avatar */
.wbf-prof__av-wrap {
position: relative;
width: 110px;
height: 110px;
margin-bottom: .25rem;
}
.wbf-prof__av-ring {
position: absolute;
inset: -4px;
border-radius: 50%;
background: conic-gradient(from 0deg, var(--c-primary), #6366f1, var(--c-primary));
opacity: .6;
animation: wbf-spin 8s linear infinite;
}
@keyframes wbf-spin { to { transform: rotate(360deg); } }
.wbf-prof__av {
position: relative;
width: 110px;
height: 110px;
border-radius: 50%;
border: 3px solid var(--c-surface);
object-fit: cover;
display: block;
background: var(--c-bg2);
box-shadow: 0 0 28px rgba(0,180,216,.25);
}
.wbf-prof__av-online {
position: absolute;
bottom: 5px; right: 5px;
width: 16px; height: 16px;
border-radius: 50%;
background: #22c55e;
border: 3px solid var(--c-surface);
box-shadow: 0 0 8px rgba(34,197,94,.7);
}
.wbf-prof__av-camera {
position: absolute;
inset: 0;
border-radius: 50%;
background: rgba(0,0,0,.6);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1.1rem;
cursor: pointer;
opacity: 0;
transition: opacity .18s;
}
.wbf-prof__av-wrap:hover .wbf-prof__av-camera { opacity: 1; }
/* Identity */
.wbf-prof__sb-name {
font-size: 1.1rem;
font-weight: 800;
color: var(--c-text);
letter-spacing: -.01em;
}
.wbf-prof__sb-role { margin-top: -.15rem; }
.wbf-prof__sb-status {
font-size: .75rem;
color: var(--c-muted);
display: flex;
align-items: center;
gap: .35rem;
justify-content: center;
}
.wbf-prof__sb-status--online { color: #22c55e; font-weight: 600; }
.wbf-prof__dot {
width: 7px; height: 7px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 6px rgba(34,197,94,.8);
animation: wbf-pulse 2s infinite;
}
/* Stats grid */
.wbf-prof__sb-stats {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: .5rem;
width: 100%;
margin: .25rem 0;
border-top: 1px solid var(--c-border);
border-bottom: 1px solid var(--c-border);
padding: .9rem 0;
}
.wbf-prof__sb-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: .2rem;
}
.wbf-prof__sb-stat span {
font-size: 1.15rem;
font-weight: 700;
color: var(--c-text);
line-height: 1;
}
.wbf-prof__sb-stat em {
font-style: normal;
font-size: .65rem;
color: var(--c-muted);
text-transform: uppercase;
letter-spacing: .06em;
}
/* Level progress */
.wbf-prof__sb-level {
width: 100%;
display: flex;
flex-direction: column;
gap: .35rem;
}
.wbf-prof__sb-level-labels {
display: flex;
justify-content: space-between;
font-size: .75rem;
font-weight: 600;
}
.wbf-prof__sb-level-xp {
color: var(--c-muted);
font-weight: 400;
}
.wbf-prof__sb-level-bar {
height: 6px;
background: rgba(255,255,255,.07);
border-radius: 6px;
overflow: hidden;
}
.wbf-prof__sb-level-bar > div {
height: 100%;
border-radius: 6px;
transition: width .6s ease;
}
.wbf-prof__sb-level-next {
font-size: .72rem;
color: var(--c-muted);
text-align: center;
}
/* Meta */
.wbf-prof__sb-meta {
display: flex;
align-items: center;
gap: .45rem;
font-size: .78rem;
color: var(--c-muted);
}
.wbf-prof__sb-meta i { color: var(--c-primary); }
/* Sidebar custom field blocks */
.wbf-prof__sb-block {
width: 100%;
border-top: 1px solid var(--c-border);
padding-top: .85rem;
display: flex;
flex-direction: column;
gap: .5rem;
text-align: left;
}
.wbf-prof__sb-block-title {
font-size: .65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .09em;
color: var(--c-primary);
margin-bottom: .15rem;
}
.wbf-prof__sb-field { display: flex; flex-direction: column; gap: .1rem; }
.wbf-prof__sb-field-lbl {
font-size: .72rem;
color: var(--c-muted);
font-weight: 600;
}
.wbf-prof__sb-field-lbl i { margin-right: .25rem; }
.wbf-prof__sb-field-val {
font-size: .82rem;
color: var(--c-text-dim);
word-break: break-all;
}
.wbf-prof__sb-field-val--link {
color: var(--c-primary);
text-decoration: none;
}
.wbf-prof__sb-field-val--link:hover { text-decoration: underline; }
/* ── MAIN ────────────────────────────────────────────────────────── */
.wbf-prof__main {
display: flex;
flex-direction: column;
gap: 0;
min-width: 0;
}
/* Header card */
.wbf-prof__header-card {
position: relative;
border-radius: var(--radius) var(--radius) 0 0;
overflow: hidden;
border: 1px solid var(--c-border);
border-bottom: none;
}
.wbf-prof__header-card-bg {
position: absolute; inset: 0;
background: linear-gradient(135deg, #0a1628 0%, #0d1f3c 50%, #061218 100%);
}
.wbf-prof__header-card-bg::after {
content: '';
position: absolute; inset: 0;
background: radial-gradient(ellipse 70% 120% at 85% 50%, rgba(0,180,216,.2) 0%, transparent 65%);
}
.wbf-prof__header-card-inner {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1.5rem 1.75rem;
flex-wrap: wrap;
}
.wbf-prof__header-name {
font-size: 1.5rem;
font-weight: 800;
color: #fff;
margin: 0 0 .35rem;
letter-spacing: -.02em;
}
.wbf-prof__header-sub {
display: flex;
align-items: center;
gap: .6rem;
margin-bottom: .5rem;
flex-wrap: wrap;
}
.wbf-prof__header-online {
display: inline-flex;
align-items: center;
gap: .35rem;
font-size: .72rem;
font-weight: 700;
color: #22c55e;
background: rgba(34,197,94,.12);
border: 1px solid rgba(34,197,94,.25);
border-radius: 20px;
padding: .15rem .65rem;
}
.wbf-prof__header-online span {
width: 6px; height: 6px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 5px rgba(34,197,94,.8);
animation: wbf-pulse 2s infinite;
}
.wbf-prof__header-bio {
font-size: .85rem;
color: rgba(255,255,255,.6);
margin: 0;
max-width: 420px;
}
.wbf-prof__header-btns {
display: flex;
gap: .6rem;
flex-wrap: wrap;
flex-shrink: 0;
}
/* Tabs */
.wbf-prof__tabs {
display: flex;
background: var(--c-surface);
border: 1px solid var(--c-border);
border-top: none;
padding: 0 .75rem;
overflow-x: auto;
scrollbar-width: none;
}
.wbf-prof__tabs::-webkit-scrollbar { display: none; }
.wbf-prof__tab {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .85rem 1rem;
font-size: .82rem;
font-weight: 600;
color: var(--c-muted);
text-decoration: none;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
white-space: nowrap;
transition: color .15s, border-color .15s;
}
.wbf-prof__tab:hover { color: var(--c-text); }
.wbf-prof__tab.active { color: var(--c-primary); border-bottom-color: var(--c-primary); }
.wbf-prof__tab i { font-size: .8rem; }
/* Tab body */
.wbf-prof__tab-body {
background: var(--c-bg);
border: 1px solid var(--c-border);
border-top: none;
border-radius: 0 0 var(--radius) var(--radius);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Section headers */
.wbf-prof__section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: .75rem;
}
.wbf-prof__section-header > span {
font-size: .82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .07em;
color: var(--c-text-dim);
display: flex;
align-items: center;
gap: .45rem;
}
.wbf-prof__section-header > span i { color: var(--c-primary); }
.wbf-prof__section-more {
font-size: .75rem;
color: var(--c-primary);
text-decoration: none;
display: flex;
align-items: center;
gap: .3rem;
opacity: .8;
transition: opacity .15s;
}
.wbf-prof__section-more:hover { opacity: 1; }
/* Post cards */
.wbf-prof__posts {
display: flex;
flex-direction: column;
gap: .6rem;
}
.wbf-prof__post-card {
display: block;
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius-sm);
text-decoration: none;
transition: border-color .15s, transform .12s;
overflow: hidden;
}
.wbf-prof__post-card:hover {
border-color: rgba(0,180,216,.4);
transform: translateX(3px);
}
.wbf-prof__post-card-inner {
display: flex;
align-items: flex-start;
gap: .9rem;
padding: .9rem 1.1rem;
}
.wbf-prof__post-card-icon { flex-shrink: 0; }
.wbf-prof__post-card-type-icon {
width: 36px; height: 36px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: .85rem;
}
.wbf-prof__post-card-type-icon--thread { background: rgba(0,180,216,.15); color: var(--c-primary); }
.wbf-prof__post-card-type-icon--reply { background: rgba(99,102,241,.15); color: #818cf8; }
.wbf-prof__post-card-body { flex: 1; min-width: 0; }
.wbf-prof__post-card-title {
font-size: .88rem;
font-weight: 600;
color: var(--c-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: .2rem;
}
.wbf-prof__post-card-preview {
font-size: .78rem;
color: var(--c-muted);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: .35rem;
}
.wbf-prof__post-card-meta {
display: flex;
gap: .75rem;
font-size: .72rem;
color: var(--c-muted);
flex-wrap: wrap;
align-items: center;
}
.wbf-prof__post-card-meta i { margin-right: .2rem; }
.wbf-prof__post-card-type { font-weight: 600; }
.wbf-prof__post-card-type--thread { color: var(--c-primary); }
.wbf-prof__post-card-type--reply { color: #818cf8; }
.wbf-prof__post-card-time {
font-size: .72rem;
color: var(--c-muted);
white-space: nowrap;
flex-shrink: 0;
display: flex;
align-items: center;
gap: .4rem;
margin-top: .2rem;
}
/* Overview 2-col grid */
.wbf-prof__overview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
align-items: start;
}
/* Stat cards */
.wbf-prof__stat-cards {
display: flex;
flex-direction: column;
gap: .65rem;
}
.wbf-prof__stat-card {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius-sm);
padding: .9rem 1rem;
position: relative;
overflow: hidden;
}
.wbf-prof__stat-card::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
}
.wbf-prof__stat-card--blue::before { background: var(--c-primary); }
.wbf-prof__stat-card--pink::before { background: #ec4899; }
.wbf-prof__stat-card--gold::before { background: #fbbf24; }
.wbf-prof__stat-card-icon {
font-size: 1rem;
margin-bottom: .3rem;
}
.wbf-prof__stat-card--blue .wbf-prof__stat-card-icon { color: var(--c-primary); }
.wbf-prof__stat-card--pink .wbf-prof__stat-card-icon { color: #ec4899; }
.wbf-prof__stat-card--gold .wbf-prof__stat-card-icon { color: #fbbf24; }
.wbf-prof__stat-card-val {
font-size: 1.4rem;
font-weight: 800;
color: var(--c-text);
line-height: 1.1;
}
.wbf-prof__stat-card-lbl {
font-size: .72rem;
color: var(--c-muted);
margin-top: .15rem;
}
.wbf-prof__stat-card-sub {
font-size: .7rem;
color: var(--c-muted);
margin-top: .3rem;
}
.wbf-prof__stat-card-bar {
height: 3px;
background: rgba(255,255,255,.07);
border-radius: 3px;
margin-top: .6rem;
overflow: hidden;
}
.wbf-prof__stat-card--blue .wbf-prof__stat-card-bar > div { background: var(--c-primary); height: 100%; border-radius: 3px; }
.wbf-prof__stat-card--pink .wbf-prof__stat-card-bar > div { background: #ec4899; height: 100%; border-radius: 3px; }
/* Badges */
.wbf-prof__badges {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: .65rem;
}
.wbf-prof__badge {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius-sm);
padding: .75rem .5rem;
text-align: center;
opacity: .45;
transition: opacity .15s;
}
.wbf-prof__badge--active { opacity: 1; }
.wbf-prof__badge-icon {
width: 40px; height: 40px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1rem;
margin: 0 auto .5rem;
}
.wbf-prof__badge-name {
font-size: .72rem;
font-weight: 700;
color: var(--c-text);
}
.wbf-prof__badge-sub {
font-size: .62rem;
color: var(--c-muted);
margin-top: .1rem;
}
/* Activity list */
.wbf-prof__activity-list {
display: flex;
flex-direction: column;
gap: 0;
}
.wbf-prof__activity-item {
display: flex;
gap: .75rem;
align-items: flex-start;
padding: .75rem 0;
border-bottom: 1px solid rgba(255,255,255,.04);
}
.wbf-prof__activity-item:last-child { border-bottom: none; }
.wbf-prof__activity-av { flex-shrink: 0; }
.wbf-prof__activity-body { flex: 1; min-width: 0; }
.wbf-prof__activity-title {
display: block;
font-size: .82rem;
font-weight: 600;
color: var(--c-text);
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: .2rem;
}
.wbf-prof__activity-title:hover { color: var(--c-primary); }
.wbf-prof__activity-meta {
font-size: .7rem;
color: var(--c-muted);
}
.wbf-prof__activity-meta i { margin-right: .2rem; }
.wbf-prof__activity-time {
font-size: .7rem;
color: var(--c-muted);
white-space: nowrap;
flex-shrink: 0;
}
/* Settings grid */
.wbf-prof__settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem;
align-items: start;
}
/* Mobile */
@media (max-width: 900px) {
.wbf-prof { grid-template-columns: 1fr; }
.wbf-prof__sidebar { position: static; }
.wbf-prof__overview-grid,
.wbf-prof__settings-grid { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
.wbf-prof__tab-body { padding: 1rem; }
.wbf-prof__header-card-inner { padding: 1.1rem; }
.wbf-prof__stat-cards { flex-direction: column; }
.wbf-prof__badges { grid-template-columns: repeat(3,1fr); }
}

View File

@@ -510,17 +510,16 @@
});
});
/* ── Profil speichern ───────────────────────────────────────── */
$(document).on('click', '#wbfSaveProfile, #wbfSaveProfileCf', function () {
/* ── Profil speichern (alles auf einmal) ───────────────────── */
$(document).on('click', '#wbfSaveProfile', function () {
var $btn = $(this).prop('disabled', true);
var $msg = $(this).siblings('.wbf-msg').length ? $(this).siblings('.wbf-msg') : $('#wbfProfileMsg');
var $msg = $('#wbfProfileMsg');
var data = {
display_name: $('#wbfEditName').val(),
bio: $('#wbfEditBio').val(),
signature: $('#wbfEditSignature').val(),
new_password: $('#wbfNewPassword').val()
signature: $('#wbfEditSignature').val()
};
// Benutzerdefinierte Profilfelder einsammeln
// Alle benutzerdefinierten Felder (alle Kategorien) einsammeln
$('.wbf-cf-input').each(function () {
data[$(this).data('field')] = $(this).val();
});
@@ -533,6 +532,40 @@
});
});
/* ── Passwort ändern ────────────────────────────────────────── */
$(document).on('click', '#wbfSavePassword', function () {
var $btn = $(this).prop('disabled', true);
var $msg = $('#wbfPasswordMsg');
var cur = $('#wbfCurrentPassword').val();
var pw1 = $('#wbfNewPassword').val();
var pw2 = $('#wbfNewPassword2').val();
if (!cur) {
showMsg($msg, 'Bitte aktuelles Passwort eingeben.', false);
return $btn.prop('disabled', false);
}
if (pw1.length < 6) {
showMsg($msg, 'Neues Passwort mindestens 6 Zeichen.', false);
return $btn.prop('disabled', false);
}
if (pw1 !== pw2) {
showMsg($msg, 'Die Passwörter stimmen nicht überein.', false);
return $btn.prop('disabled', false);
}
wbfPost('wbf_update_profile', {
current_password: cur,
new_password: pw1
}, function (d) {
showMsg($msg, d.message, true);
$('#wbfCurrentPassword, #wbfNewPassword, #wbfNewPassword2').val('');
$btn.prop('disabled', false);
}, function (d) {
showMsg($msg, d.message || 'Fehler', false);
$btn.prop('disabled', false);
});
});
/* ── Signatur Zeichenzähler ─────────────────────────────────── */
$(document).on('input', '#wbfEditSignature', function () {
$('#wbfSigCount').text($(this).val().length);
@@ -542,10 +575,16 @@
$(document).on('change', '#wbfAvatarFile', function () {
var file = this.files[0];
if (!file) return;
// Sofort-Vorschau — synchron, kein Callback, kein Warten
var objectUrl = URL.createObjectURL(file);
$('#wbfProfileAvatar').attr('src', objectUrl).css('opacity', '.6');
var fd = new FormData();
fd.append('action', 'wbf_upload_avatar');
fd.append('nonce', WBF.nonce);
fd.append('avatar', file);
$.ajax({
url: WBF.ajax_url,
type: 'POST',
@@ -553,9 +592,19 @@
processData: false,
contentType: false,
success: function (res) {
$('#wbfProfileAvatar').css('opacity', '1');
if (res.success) {
$('.wbf-profile-page__avatar').attr('src', res.data.avatar_url);
// Object-URL freigeben, endgültige Server-URL setzen
URL.revokeObjectURL(objectUrl);
var finalUrl = res.data.avatar_url + '?v=' + Date.now();
$('#wbfProfileAvatar').attr('src', finalUrl);
// Topbar-Avatar ebenfalls aktualisieren
$('.wbf-topbar__user img').attr('src', finalUrl);
$('.wbf-profile-widget__avatar img').attr('src', finalUrl);
}
},
error: function () {
$('#wbfProfileAvatar').css('opacity', '1');
}
});
});
@@ -1091,7 +1140,7 @@
var html = '';
d.notifications.forEach(function (n) {
var isUnread = n.is_read == 0;
var avatar = n.actor_avatar || '';
var avatar = $('<img>').attr('src', n.actor_avatar || '').attr('alt', '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
var base = WBF.forum_url || window.location.href.split('?')[0];
var sep = base.indexOf('?') !== -1 ? '&' : '?';
var actor = '<strong>' + $('<span>').text(n.actor_name).html() + '</strong>';
@@ -1118,7 +1167,7 @@
}
html += '<a class="wbf-notif-item' + (isUnread ? ' wbf-notif-item--unread' : '') + '" href="' + url + '">' +
'<div class="wbf-notif-item__avatar"><img src="' + avatar + '" alt=""></div>' +
'<div class="wbf-notif-item__avatar">' + avatar + '</div>' +
'<div class="wbf-notif-item__body">' +
'<div class="wbf-notif-item__text">' + text +
(sub ? '<br><span style="color:var(--c-muted);font-size:.78rem">' + $('<span>').text(sub).html() + '</span>' : '') +
@@ -1218,7 +1267,7 @@
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Loeschen"><i class="fas fa-trash-can"></i></button>';
var html = '<div class="' + cls + '" data-msg-id="' + m.id + '">';
if (!isMine) {
html += '<img src="' + (m.sender_avatar||'') + '" class="wbf-dm-inbox-item__avatar">';
html += $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
}
html += '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
@@ -1452,7 +1501,7 @@
var html = '';
d.users.forEach(function(u) {
html += '<div class="wbf-mention-item" data-username="' + $('<span>').text(u.username).html() + '">'
+ '<img src="' + (u.avatar_url || '') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
+ '<span>' + $('<span>').text(u.display_name).html() + '</span>'
+ '<small>@' + $('<span>').text(u.username).html() + '</small>'
+ '</div>';
@@ -1500,7 +1549,7 @@
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Löschen"><i class="fas fa-trash-can"></i></button>';
if (!isMine) {
html += '<div class="' + cls + '" data-msg-id="' + m.id + '">'
+ '<img src="' + (m.sender_avatar || '') + '" class="wbf-dm-msg__avatar">'
+ $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-msg__avatar')[0].outerHTML
+ '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
+ '</div><div class="wbf-dm-msg__time">' + time + delBtn + '</div></div></div>';
@@ -1524,7 +1573,7 @@
var href = window.location.pathname + '?forum_dm=inbox&with=' + conv.partner_id;
var unread = parseInt(conv.unread_cnt) > 0;
html += '<a class="wbf-dm-inbox-item' + (unread ? ' wbf-dm-inbox-item--unread' : '') + '" href="' + href + '">'
+ '<img src="' + (conv.partner_avatar || '') + '" class="wbf-dm-inbox-item__avatar">'
+ $('<img>').attr('src', conv.partner_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
+ '<div class="wbf-dm-inbox-item__body">'
+ '<span class="wbf-dm-inbox-item__name">' + $('<span>').text(conv.partner_name).html() + '</span>'
+ (unread ? '<span class="wbf-dm-inbox-item__badge">' + conv.unread_cnt + '</span>' : '')
@@ -1550,7 +1599,7 @@
var backUrl = window.location.pathname + '?forum_dm=inbox';
$('#wbfDmHeader').html(
'<a href="' + backUrl + '" class="wbf-dm-back-btn" title="Zurück zur Inbox"><i class="fas fa-arrow-left"></i></a>'
+ '<img src="' + (p.avatar_url||'') + '" class="wbf-dm-inbox-item__avatar">'
+ $('<img>').attr('src', p.avatar_url || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
+ '<strong>' + $('<span>').text(p.display_name).html() + '</strong>'
+ '<a href="?forum_profile=' + p.id + '" style="font-size:.78rem;color:var(--c-muted);text-decoration:none">@' + $('<span>').text(p.username).html() + '</a>'
);
@@ -1634,7 +1683,7 @@
var html = '';
d.users.forEach(function(u) {
html += '<div class="wbf-tag-suggest-item" data-id="' + u.id + '" data-name="' + $('<span>').text(u.display_name).html() + '">'
+ '<img src="' + (u.avatar_url||'') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
+ $('<span>').text(u.display_name).html()
+ '<span style="color:var(--c-muted);font-size:.75rem">@' + $('<span>').text(u.username).html() + '</span>'
+ '</div>';
@@ -1944,6 +1993,55 @@
});
});
/* ── E-Mail-Adresse ändern ──────────────────────────────────────────── */
$(document).on('click', '#wbfSaveEmail', function () {
var $btn = $(this);
var email = $('#wbfNewEmail').val().trim();
var password = $('#wbfEmailPassword').val();
var $msg = $('#wbfEmailMsg');
if (!email) { $msg.removeClass('wbf-ok').addClass('wbf-err').text('Bitte E-Mail eingeben.'); return; }
if (!password) { $msg.removeClass('wbf-ok').addClass('wbf-err').text('Bitte Passwort eingeben.'); return; }
$btn.prop('disabled', true);
wbfPost('wbf_change_email', { new_email: email, password: password }, function (d) {
$msg.removeClass('wbf-err').addClass('wbf-ok').text(d.message || 'E-Mail geaendert.');
$('#wbfNewEmail').val('');
$('#wbfEmailPassword').val('');
$btn.prop('disabled', false);
}, function (d) {
$msg.removeClass('wbf-ok').addClass('wbf-err').text(d.message || 'Fehler.');
$btn.prop('disabled', false);
});
});
/* ── Toggle-Switch (Notification Prefs) ─────────────────────────────── */
$(document).on('click', '.wbf-toggle', function () {
var $t = $(this);
var on = String($t.data('state')) === '1';
var val = on ? '0' : '1';
$t.data('state', val).attr('data-state', val);
if (val === '1') { $t.addClass('wbf-toggle--on'); }
else { $t.removeClass('wbf-toggle--on'); }
});
/* ── Benachrichtigungs-Einstellungen speichern ───────────────────────── */
$(document).on('click', '#wbfSaveNotifPrefs', function () {
var $btn = $(this);
var $msg = $('#wbfNotifPrefsMsg');
$btn.prop('disabled', true);
wbfPost('wbf_save_notification_prefs', {
notify_reply: String($('#wbfNotifReply').data('state')) === '1' ? '1' : '0',
notify_mention: String($('#wbfNotifMention').data('state')) === '1' ? '1' : '0',
notify_message: String($('#wbfNotifMessage').data('state')) === '1' ? '1' : '0'
}, function (d) {
$msg.removeClass('wbf-err').addClass('wbf-ok').text(d.message || 'Gespeichert!');
$btn.prop('disabled', false);
}, function (d) {
$msg.removeClass('wbf-ok').addClass('wbf-err').text(d.message || 'Fehler.');
$btn.prop('disabled', false);
});
});
/* ── Lesezeichen ────────────────────────────────────────────────────── */
$(document).on('click', '.wbf-bookmark-btn', function () {
var $btn = $(this);
@@ -1959,4 +2057,110 @@
});
});
/* ── 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();
});
}(jQuery));

View File

@@ -18,6 +18,9 @@ class WBF_Ajax {
'wbf_create_poll',
'wbf_toggle_bookmark',
'wbf_set_thread_prefix',
'wbf_toggle_ignore',
'wbf_change_email',
'wbf_save_notification_prefs',
];
foreach ($actions as $action) {
add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]);
@@ -40,23 +43,46 @@ class WBF_Ajax {
// ── Auth ──────────────────────────────────────────────────────────────────
public static function handle_login() {
// Brute-Force-Schutz: max. 10 Versuche pro IP in 15 Minuten
$ip_key = 'wbf_login_fail_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
$fails = (int) get_transient( $ip_key );
if ( $fails >= 10 ) {
wp_send_json_error([
'message' => 'Zu viele fehlgeschlagene Loginversuche. Bitte warte 15 Minuten.',
'locked' => true,
]);
}
// Login braucht keinen Nonce — Credentials sind die Authentifizierung
$result = WBF_Auth::login(
sanitize_text_field($_POST['username'] ?? ''),
$_POST['password'] ?? ''
);
if ($result['success']) {
// Erfolgreicher Login: Fehlzähler löschen
delete_transient( $ip_key );
$u = $result['user'];
if ( ! empty($_POST['remember_me']) ) {
WBF_Auth::set_remember_cookie($u->id);
}
wp_send_json_success(['display_name'=>$u->display_name,'avatar_url'=>$u->avatar_url,'user_id'=>$u->id]);
} else {
// Fehlversuch zählen — außer bei gesperrten Konten (kein Passwortfehler)
if ( empty($result['banned']) ) {
set_transient( $ip_key, $fails + 1, 15 * MINUTE_IN_SECONDS );
}
wp_send_json_error($result);
}
}
public static function handle_register() {
// Brute-Force/Spam-Schutz: max. 5 Registrierungen pro IP pro Stunde
$reg_ip_key = 'wbf_reg_ip_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
$reg_fails = (int) get_transient( $reg_ip_key );
if ( $reg_fails >= 5 ) {
wp_send_json_error(['message' => 'Zu viele Registrierungsversuche von dieser IP. Bitte warte eine Stunde.']);
}
// Spam-Schutz: Honeypot + Zeitlimit
if ( ! empty($_POST['wbf_website']) ) {
wp_send_json_error(['message' => 'Spam erkannt.']);
@@ -95,6 +121,8 @@ class WBF_Ajax {
sanitize_text_field($_POST['display_name'] ?? '')
);
if ($result['success']) {
// Registrierungs-Zähler für IP erhöhen
set_transient( $reg_ip_key, $reg_fails + 1, HOUR_IN_SECONDS );
$u = $result['user'];
// Einladungscode einlösen
$reg_mode2 = wbf_get_settings()['registration_mode'] ?? 'open';
@@ -223,9 +251,11 @@ class WBF_Ajax {
}
// Thread-Abonnenten benachrichtigen
$subscribers = WBF_DB::get_thread_subscribers($thread_id);
// $notif_users is a flat array of IDs (from get_col) — cast to int for comparison
$notif_ids = array_map('intval', $notif_users);
foreach ($subscribers as $sub) {
if ((int)$sub->id === (int)$user->id) continue; // nicht sich selbst
if (in_array($sub->id, array_column($notif_users, 'id') ?: [])) continue; // schon benachrichtigt
if (in_array((int)$sub->id, $notif_ids, true)) continue; // schon benachrichtigt
self::send_notification_email($sub, 'reply', $user->display_name, [
'thread_id' => $thread_id,
'thread_title' => $thread->title,
@@ -369,6 +399,19 @@ class WBF_Ajax {
if (!empty($_POST['new_password'])) {
if (strlen($_POST['new_password']) < 6) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']);
// Sicherheit: aktuelles Passwort muss zur Bestätigung angegeben werden
$current_pw = $_POST['current_password'] ?? '';
if ( empty($current_pw) ) {
wp_send_json_error(['message'=>'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
}
if ( ! password_verify($current_pw, $user->password) ) {
wp_send_json_error(['message'=>'Aktuelles Passwort ist falsch.']);
}
// Bestätigungsfeld server-seitig prüfen
$new_pw2 = $_POST['new_password2'] ?? '';
if ( ! empty($new_pw2) && $new_pw2 !== $_POST['new_password'] ) {
wp_send_json_error(['message'=>'Die Passwörter stimmen nicht überein.']);
}
$update['password'] = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
}
@@ -394,6 +437,15 @@ class WBF_Ajax {
$value = sanitize_textarea_field( $raw );
} elseif ( $def['type'] === 'number' ) {
$value = is_numeric($raw) ? (string)(float)$raw : '';
} elseif ( $def['type'] === 'date' ) {
// Datum validieren — nur YYYY-MM-DD, nicht in der Zukunft
$raw_date = sanitize_text_field( trim($raw) );
if ( preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw_date) ) {
$ts = strtotime($raw_date);
$value = ($ts && $ts <= time()) ? $raw_date : '';
} else {
$value = '';
}
} else {
$value = sanitize_text_field( $raw );
}
@@ -411,9 +463,37 @@ class WBF_Ajax {
if (empty($_FILES['avatar'])) wp_send_json_error(['message'=>'Keine Datei.']);
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
$mime = $_FILES['avatar']['type'] ?? '';
if (!in_array($mime, $allowed_types)) wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']);
if ($_FILES['avatar']['size'] > 2 * 1024 * 1024) wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
// Dateigröße vor dem MIME-Check prüfen
if ( $_FILES['avatar']['size'] > 2 * 1024 * 1024 ) {
wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
}
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] kommt vom Client
// und ist beliebig fälschbar (z.B. PHP-Datei als image/jpeg getarnt).
// finfo_file() liest den echten Magic-Byte der temporären Datei.
$tmp = $_FILES['avatar']['tmp_name'] ?? '';
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
wp_send_json_error(['message'=>'Ungültiger Datei-Upload.']);
}
if ( function_exists('finfo_open') ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$real_mime = finfo_file( $finfo, $tmp );
finfo_close( $finfo );
} else {
// Fallback: exif_imagetype() wenn finfo nicht verfügbar
$et_map = [
IMAGETYPE_JPEG => 'image/jpeg',
IMAGETYPE_PNG => 'image/png',
IMAGETYPE_GIF => 'image/gif',
IMAGETYPE_WEBP => 'image/webp',
];
$et = @exif_imagetype( $tmp );
$real_mime = $et_map[$et] ?? '';
}
if ( ! in_array( $real_mime, $allowed_types, true ) ) {
wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']);
}
require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -468,16 +548,36 @@ class WBF_Ajax {
// Nur Bilder erlauben
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
$mime = $_FILES['image']['type'] ?? '';
if ( ! in_array($mime, $allowed_types) ) {
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
}
// Max 5 MB
// Max 5 MB — Größe zuerst prüfen bevor teure MIME-Erkennung läuft
if ( $_FILES['image']['size'] > 5 * 1024 * 1024 ) {
wp_send_json_error(['message' => 'Maximale Dateigröße: 5 MB.']);
}
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] ist client-kontrolliert
// und kann beliebig auf 'image/jpeg' gesetzt werden, auch für .php-Dateien.
$tmp = $_FILES['image']['tmp_name'] ?? '';
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
wp_send_json_error(['message' => 'Ungültiger Datei-Upload.']);
}
if ( function_exists('finfo_open') ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$real_mime = finfo_file( $finfo, $tmp );
finfo_close( $finfo );
} else {
$et_map = [
IMAGETYPE_JPEG => 'image/jpeg',
IMAGETYPE_PNG => 'image/png',
IMAGETYPE_GIF => 'image/gif',
IMAGETYPE_WEBP => 'image/webp',
];
$et = @exif_imagetype( $tmp );
$real_mime = $et_map[$et] ?? '';
}
if ( ! in_array( $real_mime, $allowed_types, true ) ) {
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
}
require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
@@ -543,7 +643,8 @@ class WBF_Ajax {
self::verify();
$query = sanitize_text_field( $_POST['query'] ?? '' );
if ( mb_strlen( $query ) < 2 ) wp_send_json_error(['message' => 'Suchbegriff zu kurz.']);
$results = WBF_DB::search( $query, 40 );
$current_search = WBF_Auth::get_current_user();
$results = WBF_DB::search( $query, 40, $current_search );
wp_send_json_success(['results' => $results, 'query' => $query]);
}
@@ -588,6 +689,18 @@ class WBF_Ajax {
$is_mod = WBF_DB::can($user, 'delete_post');
if ( ! $is_own && ! $is_mod ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
// Post-Bearbeitungslimit prüfen — gilt auch für Thread-Erstbeiträge
// (spiegelt identisches Verhalten zu handle_edit_post() wider)
if ( $is_own && ! $is_mod ) {
$limit_min = (int)( wbf_get_settings()['post_edit_limit'] ?? 30 );
if ( $limit_min > 0 ) {
$age_min = ( time() - strtotime( $thread->created_at ) ) / 60;
if ( $age_min > $limit_min ) {
wp_send_json_error(['message' => "Bearbeitung nur innerhalb von {$limit_min} Minuten nach dem Erstellen möglich."]);
}
}
}
global $wpdb;
$wpdb->update(
"{$wpdb->prefix}forum_threads",
@@ -682,6 +795,11 @@ class WBF_Ajax {
if ($to_id === (int)$user->id) wp_send_json_error(['message'=>'Du kannst dir nicht selbst schreiben.']);
if (!WBF_DB::get_user($to_id)) wp_send_json_error(['message'=>'Empfänger nicht gefunden.']);
// DM-Blockierung: Empfänger hat Sender ignoriert
if ( WBF_DB::is_ignored( $to_id, $user->id ) ) {
wp_send_json_error(['message' => 'Diese Person akzeptiert keine Nachrichten von dir.']);
}
$id = WBF_DB::send_message($user->id, $to_id, $content);
// Notify recipient
WBF_DB::create_notification($to_id, 'message', $id, $user->id);
@@ -739,14 +857,22 @@ class WBF_Ajax {
// ── User-Autocomplete (für @Erwähnungen + DM) ─────────────────────────────
public static function handle_user_suggest() {
// Nur eingeloggte Nutzer dürfen die User-Suche nutzen
// (verhindert Enumeration aller Usernamen + Rollendaten durch Gäste)
if ( ! WBF_Auth::is_forum_logged_in() ) {
wp_send_json_success(['users'=>[]]);
}
$q = sanitize_text_field($_POST['q'] ?? $_GET['q'] ?? '');
if (mb_strlen($q) < 1) wp_send_json_success(['users'=>[]]);
global $wpdb;
$like = $wpdb->esc_like($q) . '%';
// Rolle wird bewusst NICHT zurückgegeben — nicht für Autocomplete nötig
// und verhindert Informationsleck über Rollen-Verteilung im Forum.
$users = $wpdb->get_results($wpdb->prepare(
"SELECT id, username, display_name, avatar_url, role
"SELECT id, username, display_name, avatar_url
FROM {$wpdb->prefix}forum_users
WHERE username LIKE %s OR display_name LIKE %s
WHERE (username LIKE %s OR display_name LIKE %s)
AND role != 'banned'
ORDER BY display_name ASC LIMIT 8",
$like, $like
));
@@ -817,6 +943,13 @@ class WBF_Ajax {
private static function send_notification_email( $to_user, $type, $actor_name, $extra = [] ) {
if ( ! $to_user || empty($to_user->email) ) return;
// Prüfen ob der User diesen Benachrichtigungstyp aktiviert hat
// Standard: alle aktiviert (1). User kann im Profil deaktivieren (0).
$pref_key = 'notify_' . $type; // notify_reply, notify_mention, notify_message
$meta = WBF_DB::get_user_meta( $to_user->id );
// Nur deaktivieren wenn explizit auf '0' gesetzt — Standard ist aktiviert
if ( isset($meta[$pref_key]) && $meta[$pref_key] === '0' ) return;
$blog_name = get_bloginfo('name');
$forum_url = wbf_get_forum_url();
$from_email = get_option('admin_email');
@@ -897,6 +1030,17 @@ class WBF_Ajax {
$email = sanitize_email( $_POST['email'] ?? '' );
if ( ! is_email($email) ) wp_send_json_error(['message'=>'Ungültige E-Mail-Adresse.']);
// ── Rate-Limiting: max. 1 Reset-Mail pro E-Mail-Adresse alle 15 Minuten ──
// Verhindert, dass ein Angreifer tausende Reset-Mails pro Sekunde
// für beliebige Adressen triggert und den Mail-Server überlastet.
$rate_key = 'wbf_pwreset_' . md5( strtolower( $email ) );
if ( get_transient( $rate_key ) !== false ) {
// Immer Erfolg melden — kein Leak ob Rate-Limit oder kein Account
wp_send_json_success(['message'=>'Falls diese E-Mail registriert ist, wurde eine E-Mail gesendet.']);
}
// Cooldown setzen — 15 Minuten
set_transient( $rate_key, 1, 15 * MINUTE_IN_SECONDS );
$user = WBF_DB::get_user_by('email', $email);
// Immer Erfolg melden (kein User-Enumeration)
if ( ! $user ) {
@@ -922,7 +1066,13 @@ class WBF_Ajax {
}
public static function handle_reset_password() {
self::verify();
// Kein self::verify() hier — Gäste haben keine Forum-Session.
// Das Reset-Token selbst authentifiziert die Anfrage.
// Wir prüfen trotzdem den WP-Nonce als CSRF-Schutz; dieser wird
// von wp_localize_script für alle Besucher (auch Gäste) generiert.
if ( ! check_ajax_referer( 'wbf_nonce', 'nonce', false ) ) {
wp_send_json_error(['message' => 'Sicherheitsfehler.']);
}
$token = sanitize_text_field( $_POST['token'] ?? '' );
$password = $_POST['password'] ?? '';
$password2= $_POST['password2'] ?? '';
@@ -1041,6 +1191,12 @@ class WBF_Ajax {
self::verify();
$user = WBF_Auth::get_current_user();
if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
// Sicherstellen dass Spalte existiert (Schutz für bestehende Installs)
global $wpdb;
$cols = $wpdb->get_col( "DESCRIBE {$wpdb->prefix}forum_users" );
if ( ! in_array( 'profile_public', $cols ) ) {
$wpdb->query( "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) NOT NULL DEFAULT 1" );
}
$current = (int)($user->profile_public ?? 1);
$new = $current ? 0 : 1;
WBF_DB::update_user($user->id, ['profile_public'=>$new]);
@@ -1207,6 +1363,90 @@ class WBF_Ajax {
wp_send_json_success(['prefix' => $prefix]);
}
// ── E-Mail-Adresse ändern ─────────────────────────────────────────────────
public static function handle_change_email() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$new_email = sanitize_email( $_POST['new_email'] ?? '' );
$password = $_POST['password'] ?? '';
if ( ! is_email($new_email) ) {
wp_send_json_error(['message' => 'Ungültige E-Mail-Adresse.']);
}
if ( empty($password) ) {
wp_send_json_error(['message' => 'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
}
if ( ! password_verify($password, $user->password) ) {
wp_send_json_error(['message' => 'Falsches Passwort.']);
}
if ( strtolower($new_email) === strtolower($user->email) ) {
wp_send_json_error(['message' => 'Das ist bereits deine aktuelle E-Mail-Adresse.']);
}
// Prüfen ob E-Mail bereits vergeben
$existing = WBF_DB::get_user_by('email', $new_email);
if ( $existing && (int)$existing->id !== (int)$user->id ) {
wp_send_json_error(['message' => 'Diese E-Mail-Adresse ist bereits registriert.']);
}
WBF_DB::update_user($user->id, ['email' => $new_email]);
wp_send_json_success(['message' => 'E-Mail-Adresse erfolgreich geändert.']);
}
// ── Benachrichtigungs-Einstellungen speichern ─────────────────────────────
public static function handle_save_notification_prefs() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$allowed = ['notify_reply', 'notify_mention', 'notify_message'];
foreach ( $allowed as $key ) {
// 1 wenn Checkbox aktiviert, 0 wenn deaktiviert
$val = isset($_POST[$key]) && $_POST[$key] === '1' ? '1' : '0';
WBF_DB::set_user_meta($user->id, $key, $val);
}
wp_send_json_success(['message' => 'Benachrichtigungs-Einstellungen gespeichert.']);
}
// ── User ignorieren / Ignorierung aufheben ────────────────────────────────
public static function handle_toggle_ignore() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error( ['message' => 'Nicht eingeloggt.'] );
$ignored_id = (int)( $_POST['ignored_id'] ?? 0 );
if ( ! $ignored_id ) wp_send_json_error( ['message' => 'Ungültiger Nutzer.'] );
if ( $ignored_id === (int)$user->id ) {
wp_send_json_error( ['message' => 'Du kannst dich nicht selbst ignorieren.'] );
}
$target = WBF_DB::get_user( $ignored_id );
if ( ! $target ) wp_send_json_error( ['message' => 'Nutzer nicht gefunden.'] );
// Prüfen ob diese Rolle geblockt werden darf (konfigurierbar in den Einstellungen)
if ( ! wbf_can_be_ignored( $target ) ) {
$role_label = WBF_Roles::get($target->role)['label'] ?? $target->role;
wp_send_json_error( ['message' => 'Nutzer mit der Rolle "' . $role_label . '" können nicht ignoriert werden.'] );
}
$now_ignored = WBF_DB::toggle_ignore( $user->id, $ignored_id );
wp_send_json_success( [
'ignored' => $now_ignored,
'ignored_id' => $ignored_id,
'display_name' => $target->display_name,
'message' => $now_ignored
? esc_html( $target->display_name ) . ' wird jetzt ignoriert.'
: 'Ignorierung von ' . esc_html( $target->display_name ) . ' aufgehoben.',
] );
}
}
add_action( 'init', [ 'WBF_Ajax', 'init' ] );

View File

@@ -6,8 +6,25 @@ class WBF_Auth {
const SESSION_KEY = 'wbf_forum_user';
public static function init() {
// PHP 8.3: session_start() nach gesendeten Headers erzeugt E_WARNING,
// der direkt in den HTML-Output fließt und das Layout zerstört.
// Lösung: headers_sent() prüfen + session_start() mit Cookie-Optionen aufrufen.
if ( ! session_id() ) {
session_start();
if ( headers_sent() ) {
// Headers bereits gesendet — Session kann nicht sicher gestartet werden.
// Passiert z.B. wenn WP_DEBUG=true und PHP Notices vor dem Hook ausgegeben hat.
return;
}
$session_opts = [
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
'use_strict_mode' => true,
];
// cookie_secure nur setzen wenn HTTPS aktiv — verhindert Session-Verlust bei HTTP
if ( is_ssl() || ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ) {
$session_opts['cookie_secure'] = true;
}
session_start( $session_opts );
}
// Auto-login via Remember-Me cookie if not already logged in
if ( empty( $_SESSION[ self::SESSION_KEY ] ) && isset( $_COOKIE['wbf_remember'] ) ) {
@@ -55,6 +72,7 @@ class WBF_Auth {
]);
// Frisch laden und einloggen
$user = WBF_DB::get_user( $user->id );
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
$_SESSION[ self::SESSION_KEY ] = $user->id;
WBF_DB::touch_last_active( $user->id );
return array( 'success' => true, 'user' => $user );
@@ -67,6 +85,7 @@ class WBF_Auth {
}
return array( 'success' => false, 'banned' => true, 'message' => $reason );
}
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
$_SESSION[ self::SESSION_KEY ] = $user->id;
WBF_DB::touch_last_active( $user->id );
return array( 'success' => true, 'user' => $user );
@@ -96,6 +115,7 @@ class WBF_Auth {
'avatar_url' => $avatar,
));
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
$_SESSION[ self::SESSION_KEY ] = $id;
return array('success'=>true,'user'=>WBF_DB::get_user($id));
}

View File

@@ -279,6 +279,18 @@ class WBF_DB {
) $charset;";
dbDelta( $sql_bookmarks );
// ── Ignore-Liste ──────────────────────────────────────────────────────
$sql_ignore = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_ignore_list (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
ignored_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY user_ignored (user_id, ignored_id),
KEY ignored_id (ignored_id)
) $charset;";
dbDelta( $sql_ignore );
// ── prefix_id zu threads ──────────────────────────────────────────────
self::maybe_add_column( "{$wpdb->prefix}forum_threads", 'prefix_id',
"ALTER TABLE {$wpdb->prefix}forum_threads ADD COLUMN prefix_id BIGINT UNSIGNED DEFAULT NULL" );
@@ -478,7 +490,7 @@ class WBF_DB {
}
// Move post_count contribution too
$post_count = (int)$wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id
));
if ( $post_count > 0 ) {
$wpdb->query($wpdb->prepare(
@@ -500,7 +512,7 @@ class WBF_DB {
FROM {$wpdb->prefix}forum_threads t
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id
WHERE t.id = %d", $id
WHERE t.id = %d AND t.deleted_at IS NULL", $id
));
}
@@ -560,7 +572,7 @@ class WBF_DB {
public static function count_posts( $thread_id ) {
global $wpdb;
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id));
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id));
}
public static function create_post( $data ) {
@@ -623,15 +635,16 @@ class WBF_DB {
FROM {$wpdb->prefix}forum_threads t
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
AND t.status != 'archived' ORDER BY t.created_at DESC LIMIT %d", $limit
WHERE t.status != 'archived' AND t.deleted_at IS NULL
ORDER BY t.created_at DESC LIMIT %d", $limit
));
}
public static function get_stats() {
global $wpdb;
return [
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived'"),
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts"),
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived' AND deleted_at IS NULL"),
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL"),
'members' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users"),
'newest' => $wpdb->get_var("SELECT display_name FROM {$wpdb->prefix}forum_users ORDER BY registered DESC LIMIT 1"),
];
@@ -718,9 +731,23 @@ class WBF_DB {
// ── Suche ─────────────────────────────────────────────────────────────────
public static function search( $query, $limit = 30 ) {
public static function search( $query, $limit = 30, $user = null ) {
global $wpdb;
$like = '%' . $wpdb->esc_like( $query ) . '%';
// Kategorie-Sichtbarkeit: Gäste und Member dürfen keine privaten Kategorien sehen
$user_level = $user ? WBF_Roles::level( $user->role ) : -99;
if ( $user_level >= 50 ) {
// Moderatoren+ sehen alles (inkl. soft-deleted ist extra)
$cat_filter = '';
} elseif ( $user ) {
// Eingeloggte Member/VIP: nur guest_visible oder eigene Rolle reicht
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role IN ('member','vip'))";
} else {
// Gäste: nur komplett öffentliche Kategorien
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role = 'member')";
}
return $wpdb->get_results( $wpdb->prepare(
"SELECT 'thread' AS result_type,
t.id, t.title, t.content, t.created_at, t.reply_count,
@@ -729,7 +756,9 @@ class WBF_DB {
FROM {$wpdb->prefix}forum_threads t
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
WHERE (t.title LIKE %s OR t.content LIKE %s) AND t.status != 'archived'
WHERE (t.title LIKE %s OR t.content LIKE %s)
AND t.status != 'archived' AND t.deleted_at IS NULL
$cat_filter
UNION ALL
SELECT 'post' AS result_type,
p.id, t.title, p.content, p.created_at, 0 AS reply_count,
@@ -739,7 +768,9 @@ class WBF_DB {
JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id
JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
WHERE p.content LIKE %s AND t.status != 'archived'
WHERE p.content LIKE %s
AND p.deleted_at IS NULL AND t.status != 'archived' AND t.deleted_at IS NULL
$cat_filter
ORDER BY created_at DESC
LIMIT %d",
$like, $like, $like, $limit
@@ -1207,13 +1238,18 @@ class WBF_DB {
global $wpdb;
$token = bin2hex( random_bytes(32) );
$hash = hash( 'sha256', $token );
// Alte Tokens löschen
$wpdb->delete( "{$wpdb->prefix}forum_users", [] ); // nur placeholder
// Altes Token dieses Users zurücksetzen bevor ein neues gesetzt wird
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->prefix}forum_users
SET reset_token=NULL, reset_token_expires=NULL
WHERE id=%d",
(int) $user_id
) );
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->prefix}forum_users
SET reset_token=%s, reset_token_expires=DATE_ADD(NOW(), INTERVAL 1 HOUR)
WHERE id=%d",
$hash, $user_id
$hash, (int) $user_id
) );
return $token; // Klartext-Token → per E-Mail senden
}
@@ -1458,6 +1494,25 @@ class WBF_DB {
update_option( 'wbf_profile_fields', $fields );
}
public static function get_profile_field_categories() {
$cats = get_option( 'wbf_profile_field_cats', null );
if ( $cats === null ) {
// Default-Kategorien beim ersten Aufruf
$defaults = [
[ 'id' => 'cat_allgemein', 'name' => 'Allgemein', 'icon' => '👤' ],
[ 'id' => 'cat_kontakt', 'name' => 'Kontakt', 'icon' => '✉️' ],
[ 'id' => 'cat_social', 'name' => 'Social Media', 'icon' => '🌐' ],
];
update_option( 'wbf_profile_field_cats', $defaults );
return $defaults;
}
return is_array( $cats ) ? $cats : [];
}
public static function save_profile_field_categories( $cats ) {
update_option( 'wbf_profile_field_cats', $cats );
}
public static function get_user_meta( $user_id ) {
global $wpdb;
$rows = $wpdb->get_results( $wpdb->prepare(
@@ -1689,6 +1744,128 @@ class WBF_DB {
));
}
// ── Ignore-Liste ──────────────────────────────────────────────────────────
public static function toggle_ignore( $user_id, $ignored_id ) {
global $wpdb;
$user_id = (int) $user_id;
$ignored_id = (int) $ignored_id;
if ( self::is_ignored( $user_id, $ignored_id ) ) {
$wpdb->delete( "{$wpdb->prefix}forum_ignore_list", [
'user_id' => $user_id,
'ignored_id' => $ignored_id,
] );
return false;
}
$wpdb->replace( "{$wpdb->prefix}forum_ignore_list", [
'user_id' => $user_id,
'ignored_id' => $ignored_id,
] );
return true;
}
public static function is_ignored( $user_id, $ignored_id ) {
global $wpdb;
$table = "{$wpdb->prefix}forum_ignore_list";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return false;
return (bool) $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d AND ignored_id=%d",
(int) $user_id, (int) $ignored_id
) );
}
/** Gibt alle ignorierten User-IDs als int-Array zurück */
public static function get_ignored_ids( $user_id ) {
global $wpdb;
$table = "{$wpdb->prefix}forum_ignore_list";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return [];
$ids = $wpdb->get_col( $wpdb->prepare(
"SELECT ignored_id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d",
(int) $user_id
) );
return array_map( 'intval', $ids );
}
/** Vollständige Ignore-Liste mit User-Daten */
public static function get_ignore_list( $user_id ) {
global $wpdb;
$table = "{$wpdb->prefix}forum_ignore_list";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return [];
return $wpdb->get_results( $wpdb->prepare(
"SELECT u.id, u.username, u.display_name, u.avatar_url, u.role,
il.created_at AS ignored_since
FROM {$wpdb->prefix}forum_ignore_list il
JOIN {$wpdb->prefix}forum_users u ON u.id = il.ignored_id
WHERE il.user_id = %d
ORDER BY il.created_at DESC",
(int) $user_id
) );
}
// ── DSGVO Art. 17: Konto vollständig löschen ──────────────────────────────
public static function delete_user_gdpr( $user_id ) {
global $wpdb;
$user_id = (int) $user_id;
$user = self::get_user( $user_id );
if ( ! $user ) return false;
if ( $user->role === 'superadmin' ) return false;
$wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'from_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'to_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", [ 'user_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'user_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'actor_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_subscriptions", [ 'user_id' => $user_id ] );
$table_bm = "{$wpdb->prefix}forum_bookmarks";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_bm'" ) === $table_bm ) {
$wpdb->delete( $table_bm, [ 'user_id' => $user_id ] );
}
$wpdb->delete( "{$wpdb->prefix}forum_likes", [ 'user_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_reactions", [ 'user_id' => $user_id ] );
$wpdb->delete( "{$wpdb->prefix}forum_reports", [ 'reporter_id' => $user_id ] );
$table_pv = "{$wpdb->prefix}forum_poll_votes";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_pv'" ) === $table_pv ) {
$wpdb->delete( $table_pv, [ 'user_id' => $user_id ] );
}
// Ignore-Liste beidseitig bereinigen
$table_il = "{$wpdb->prefix}forum_ignore_list";
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_il'" ) === $table_il ) {
$wpdb->delete( $table_il, [ 'user_id' => $user_id ] );
$wpdb->delete( $table_il, [ 'ignored_id' => $user_id ] );
}
delete_transient( 'wbf_flood_' . $user_id );
delete_transient( 'wbf_flood_ts_' . $user_id );
self::delete_user_meta_all( $user_id );
$anon_hash = substr( hash( 'sha256', $user_id . wp_salt() . microtime( true ) ), 0, 12 );
$wpdb->update(
"{$wpdb->prefix}forum_users",
[
'username' => 'deleted_' . $anon_hash,
'email' => 'deleted_' . $anon_hash . '@deleted.invalid',
'password' => '',
'display_name' => 'Gelöschter Nutzer',
'avatar_url' => '',
'bio' => '',
'signature' => '',
'ban_reason' => '',
'reset_token' => null,
'reset_token_expires' => null,
'pre_ban_role' => '',
'ban_until' => null,
'role' => 'banned',
],
[ 'id' => $user_id ]
);
return true;
}
// ── Wortfilter ────────────────────────────────────────────────────────────
public static function get_word_filter() {
@@ -1711,25 +1888,29 @@ class WBF_DB {
// ── Flood Control ─────────────────────────────────────────────────────────
public static function check_flood( $user_id ) {
$user_id = (int) $user_id;
if ( $user_id <= 0 ) return true; // kein eingeloggter User — kein Flood-Check
$interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 );
if ( $interval <= 0 ) return true; // deaktiviert
$key = 'wbf_flood_' . (int)$user_id;
$key = 'wbf_flood_' . (int)$user_id;
$ts_key = 'wbf_flood_ts_' . (int)$user_id;
$last = get_transient( $key );
if ( $last !== false ) {
return false; // noch gesperrt
}
set_transient( $key, time(), $interval );
set_transient( $key, 1, $interval );
set_transient( $ts_key, time(), $interval + 5 );
return true;
}
public static function flood_remaining( $user_id ) {
$interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 );
if ( $interval <= 0 ) return 0;
$key = 'wbf_flood_' . (int)$user_id;
$last = get_transient( $key );
if ( $last === false ) return 0;
// Transients speichern keine genaue Restzeit — wir schätzen über $interval
return $interval;
$ts_key = 'wbf_flood_ts_' . (int)$user_id;
$sent = get_transient( $ts_key );
if ( $sent === false ) return 0;
$remaining = $interval - ( time() - (int)$sent );
return max( 0, $remaining );
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -15,7 +15,7 @@ class WBF_Roles {
private static function default_roles() {
return [
'superadmin' => [
'label' => 'Superadmin',
'label' => 'Admin',
'level' => 100,
'color' => '#e11d48',
'bg_color' => 'rgba(225,29,72,.15)',
@@ -108,7 +108,7 @@ class WBF_Roles {
/** Nach Level sortiert (höchstes zuerst) */
public static function get_sorted() {
$all = self::get_all();
uasort($all, fn($a,$b) => $b['level'] <=> $a['level']);
uasort($all, function($a, $b) { return $b['level'] <=> $a['level']; });
return $all;
}

View File

@@ -9,6 +9,15 @@ class WBF_Shortcodes {
// ── Helpers ───────────────────────────────────────────────────────────────
/** Alter aus Geburtsdatum berechnen */
public static function calc_age( $date_str ) {
if ( ! $date_str || ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_str) ) return null;
$birth = new DateTime( $date_str );
$today = new DateTime();
if ( $birth > $today ) return null;
return (int) $birth->diff($today)->y;
}
public static function time_ago( $datetime ) {
$diff = time() - strtotime($datetime);
if ($diff < 60) return 'Gerade eben';
@@ -149,8 +158,12 @@ class WBF_Shortcodes {
// ── Router ────────────────────────────────────────────────────────────────
public static function forum_main( $atts ) {
// Server-seitiger Logout-Fallback
// Server-seitiger Logout-Fallback — Nonce-Schutz gegen CSRF
if (isset($_GET['wbf_do_logout'])) {
if ( ! isset($_GET['_wpnonce']) || ! wp_verify_nonce( sanitize_text_field($_GET['_wpnonce']), 'wbf_logout' ) ) {
wp_redirect( wbf_get_forum_url() );
exit;
}
WBF_Auth::logout();
wp_redirect( wbf_get_forum_url() );
exit;
@@ -310,7 +323,7 @@ class WBF_Shortcodes {
</div>
<div class="wbf-profile-widget__actions">
<a href="?forum_profile=<?php echo (int)$current->id; ?>" class="wbf-btn wbf-btn--sm">Profil</a>
<a href="<?php echo esc_url(wbf_get_forum_url() . '?wbf_do_logout=1'); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
<a href="<?php echo esc_url(wp_nonce_url(wbf_get_forum_url() . '?wbf_do_logout=1', 'wbf_logout')); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
</div>
</div>
<?php endif; ?>
@@ -341,9 +354,9 @@ class WBF_Shortcodes {
<?php endif; ?>
</aside>
</div>
<?php self::render_forum_footer(); ?>
</div>
<?php self::render_new_thread_modal(WBF_DB::get_categories_flat(), $current); ?>
<?php self::render_forum_footer(); ?>
<?php self::render_auth_modal(); ?>
</div>
<?php return ob_get_clean();
@@ -500,9 +513,9 @@ class WBF_Shortcodes {
</div>
</div>
<?php endif; endif; ?>
<?php self::render_forum_footer(); ?>
<?php self::render_new_thread_modal(WBF_DB::get_categories_flat(),$current,$cat->id); ?>
<?php self::render_forum_footer(); ?>
<?php self::render_auth_modal(); ?>
</div>
<?php return ob_get_clean();
@@ -731,6 +744,23 @@ class WBF_Shortcodes {
<i class="fas fa-pen"></i> Bearbeiten
</button>
<?php endif; ?>
<?php
// Ignore-Button: nur wenn der Thread-Autor nicht der eingeloggte User ist
// und die Rolle blockiert werden darf (konfigurierbar in Einstellungen)
$op_author = WBF_DB::get_user((int)$thread->user_id);
if ($current && (int)$current->id !== (int)$thread->user_id && wbf_can_be_ignored($op_author)):
$op_is_ignored = WBF_DB::is_ignored($current->id, (int)$thread->user_id);
?>
<button class="wbf-ignore-btn"
data-id="<?php echo (int)$thread->user_id; ?>"
data-name="<?php echo esc_attr($thread->display_name); ?>"
data-ignored="<?php echo $op_is_ignored ? '1' : '0'; ?>"
title="<?php echo $op_is_ignored ? 'Ignorierung aufheben' : 'Nutzer ignorieren'; ?>"
style="background:none;border:none;cursor:pointer;color:var(--c-muted,#94a3b8);padding:2px 6px;border-radius:4px;font-size:.82rem">
<i class="fas fa-<?php echo $op_is_ignored ? 'eye' : 'eye-slash'; ?>"></i>
<?php echo $op_is_ignored ? 'Entblocken' : 'Ignorieren'; ?>
</button>
<?php endif; ?>
</div>
</div>
</div>
@@ -774,8 +804,8 @@ class WBF_Shortcodes {
<?php else: ?>
<div class="wbf-notice wbf-notice--warning"><i class="fas fa-lock"></i> Dieser Thread ist geschlossen.</div>
<?php endif; ?>
</div>
<?php self::render_forum_footer(); ?>
</div>
<?php self::render_auth_modal(); ?>
<?php self::render_report_modal(); ?>
<?php if (WBF_DB::can($current,'manage_cats')): self::render_move_modal(WBF_DB::get_categories_flat(), $id); endif; ?>
@@ -843,6 +873,22 @@ class WBF_Shortcodes {
<i class="fas fa-pen"></i> Bearbeiten
</button>
<?php endif; ?>
<?php
// Ignore-Button im Post-Footer
$post_author = WBF_DB::get_user((int)$post->user_id);
if ($current && (int)$current->id !== (int)$post->user_id && wbf_can_be_ignored($post_author)):
$post_is_ignored = WBF_DB::is_ignored($current->id, (int)$post->user_id);
?>
<button class="wbf-ignore-btn"
data-id="<?php echo (int)$post->user_id; ?>"
data-name="<?php echo esc_attr($post->display_name); ?>"
data-ignored="<?php echo $post_is_ignored ? '1' : '0'; ?>"
title="<?php echo $post_is_ignored ? 'Ignorierung aufheben' : 'Nutzer ignorieren'; ?>"
style="background:none;border:none;cursor:pointer;color:var(--c-muted,#94a3b8);padding:2px 6px;border-radius:4px;font-size:.82rem">
<i class="fas fa-<?php echo $post_is_ignored ? 'eye' : 'eye-slash'; ?>"></i>
<?php echo $post_is_ignored ? 'Entblocken' : 'Ignorieren'; ?>
</button>
<?php endif; ?>
<?php echo self::mod_tools_post($post->id,$current); ?>
</div>
</div>
@@ -860,7 +906,9 @@ class WBF_Shortcodes {
$is_own = $current && $current->id == $profile->id;
$is_staff = $current && WBF_Roles::level($current->role) >= 50;
// Profil-Sichtbarkeit prüfen
if (!$is_own && !$is_staff && (int)($profile->profile_public ?? 1) === 0) {
// profile_public NULL = Spalte fehlt noch = als öffentlich (1) behandeln
$profile_public = isset($profile->profile_public) ? (int)$profile->profile_public : 1;
if (!$is_own && !$is_staff && $profile_public === 0) {
ob_start(); ?>
<div class="wbf-wrap"><?php self::render_topbar($current); ?>
<div class="wbf-container wbf-mt">
@@ -870,14 +918,29 @@ class WBF_Shortcodes {
</div></div>
<?php return ob_get_clean();
}
$user_posts = WBF_DB::get_user_posts( $profile->id, 50 );
$user_posts = WBF_DB::get_user_posts( $profile->id, 50 );
$bookmarks = $is_own ? WBF_DB::get_user_bookmarks($current->id, 50) : [];
$ignore_list = $is_own ? WBF_DB::get_ignore_list($current->id) : [];
$cf_defs = WBF_DB::get_profile_field_defs();
$cf_cats = WBF_DB::get_profile_field_categories();
$cf_cat_map = array_column( $cf_cats, null, 'id' );
$cf_vals = WBF_DB::get_user_meta( $profile->id );
// Aktiven Tab aus URL lesen (tab=1|2|3), Standard: 1 für eigenes, 2 für fremdes
// Tab-ID: numerisch (14) oder String-Slug (z.B. 'mc' von der Forum-Bridge)
$ptab_raw = $_GET['ptab'] ?? ($is_own ? 1 : 2);
$active_tab = ctype_digit( (string) $ptab_raw ) ? (int) $ptab_raw : sanitize_key( $ptab_raw );
if ( is_int($active_tab) && ! in_array($active_tab, [1,2,3,4]) ) {
$active_tab = $is_own ? 1 : 2;
}
// Tab 1, 3, 4 und String-Tabs nur für eigenes Profil (außer Tab 2 = Aktivität)
if ( ! $is_own && $active_tab !== 2 ) $active_tab = 2;
ob_start(); ?>
<div class="wbf-wrap">
<?php self::render_topbar($current); ?>
<div class="wbf-container wbf-mt">
<nav class="wbf-breadcrumb">
<a href="<?php echo esc_url(remove_query_arg('forum_profile')); ?>"><i class="fas fa-home"></i> Forum</a>
<a href="<?php echo esc_url(remove_query_arg(['forum_profile', 'ptab'])); ?>"><i class="fas fa-home"></i> Forum</a>
<span>/</span><span>Profil</span>
</nav>
@@ -885,11 +948,10 @@ class WBF_Shortcodes {
<!-- ── SIDEBAR ─────────────────────────────────────────── -->
<aside class="wbf-profile-sidebar">
<!-- Avatar -->
<div class="wbf-profile-sidebar__avatar-wrap">
<img src="<?php echo esc_url($profile->avatar_url); ?>"
alt="<?php echo esc_attr($profile->display_name); ?>"
id="wbfProfileAvatar"
class="wbf-profile-sidebar__avatar">
<?php if ($is_own): ?>
<label class="wbf-avatar-upload-btn" title="Avatar ändern">
@@ -898,15 +960,24 @@ class WBF_Shortcodes {
</label>
<?php endif; ?>
</div>
<!-- Name + Badge + Username -->
<div class="wbf-profile-sidebar__identity">
<h2><?php echo esc_html($profile->display_name); ?></h2>
<?php echo self::role_badge($profile->role); ?>
<span class="wbf-profile-sidebar__username">@<?php echo esc_html($profile->username); ?></span>
<?php
$profile_online = WBF_DB::is_online($profile->id, 15);
if ($profile_online): ?>
<span class="wbf-profile-online-badge">
<span class="wbf-profile-online-dot"></span> Online
</span>
<?php else:
$last = $profile->last_active ?? null;
if ($last && $last !== '0000-00-00 00:00:00'): ?>
<span class="wbf-profile-lastseen">
<i class="fas fa-clock"></i> Zuletzt aktiv: <?php echo self::time_ago($last); ?>
</span>
<?php endif; endif; ?>
</div>
<!-- Stats -->
<div class="wbf-profile-sidebar__stats">
<div class="wbf-profile-sidebar__stat">
<span><?php echo (int)$profile->post_count; ?></span>
@@ -917,77 +988,154 @@ class WBF_Shortcodes {
<em>Dabei seit</em>
</div>
</div>
<!-- Level-Fortschritt -->
<?php $level_bar = WBF_Levels::progress_bar((int)$profile->post_count); if ($level_bar): ?>
<div class="wbf-profile-sidebar__section">
<?php echo $level_bar; ?>
</div>
<div class="wbf-profile-sidebar__section"><?php echo $level_bar; ?></div>
<?php endif; ?>
<!-- Bio -->
<?php if (!empty($profile->bio)): ?>
<div class="wbf-profile-sidebar__section">
<span class="wbf-profile-sidebar__section-label"><i class="fas fa-align-left"></i> Bio</span>
<p><?php echo nl2br(esc_html($profile->bio)); ?></p>
</div>
<?php endif; ?>
<!-- Signatur -->
<?php if (!empty($profile->signature)): ?>
<div class="wbf-profile-sidebar__section">
<span class="wbf-profile-sidebar__section-label"><i class="fas fa-pen-nib"></i> Signatur</span>
<p class="wbf-profile-sidebar__sig"><?php echo nl2br(esc_html($profile->signature)); ?></p>
</div>
<?php endif; ?>
<!-- Benutzerdefinierte Profilfelder (öffentliche) -->
<!-- Öffentliche Custom Fields — nach Kategorie gruppiert -->
<?php
$cf_defs_pub = WBF_DB::get_profile_field_defs();
$cf_vals_pub = WBF_DB::get_user_meta( $profile->id );
foreach ( $cf_defs_pub as $def ):
if ( ! $is_own && empty($def['public']) ) continue;
$val = trim( $cf_vals_pub[ $def['key'] ] ?? '' );
if ( $val === '' ) continue;
$cf_by_cat_sb = [];
foreach ( $cf_defs as $def_sb ) {
if (!$is_own && empty($def_sb['public'])) continue;
$val_sb = trim($cf_vals[$def_sb['key']] ?? '');
if ($val_sb === '') continue;
$cid_sb = $def_sb['category_id'] ?? '';
if (!$cid_sb || !isset($cf_cat_map[$cid_sb])) $cid_sb = '__none__';
$cf_by_cat_sb[$cid_sb][] = ['def'=>$def_sb,'val'=>$val_sb];
}
$sb_sections = $cf_cats;
if (isset($cf_by_cat_sb['__none__'])) {
$sb_sections[] = ['id'=>'__none__','name'=>'Weitere Infos','icon'=>''];
}
foreach ($sb_sections as $scat_sb):
$scid_sb = $scat_sb['id'];
if (empty($cf_by_cat_sb[$scid_sb])) continue;
?>
<div class="wbf-profile-sidebar__section">
<span class="wbf-profile-sidebar__section-label">
<i class="fas fa-<?php echo $def['type']==='url'?'link':($def['type']==='number'?'hashtag':'tag'); ?>"></i>
<?php echo esc_html($def['label']); ?>
<span class="wbf-profile-sidebar__section-label" style="display:flex;align-items:center;gap:5px;margin-bottom:4px">
<?php if(!empty($scat_sb['icon'])): ?>
<span style="font-size:.9rem"><?php echo esc_html($scat_sb['icon']); ?></span>
<?php endif; ?>
<?php echo esc_html($scat_sb['name']); ?>
</span>
<?php if ( $def['type'] === 'url' ): ?>
<a href="<?php echo esc_url($val); ?>" target="_blank" rel="noopener noreferrer"
style="color:var(--c-primary);font-size:.85rem;word-break:break-all">
<?php echo esc_html( mb_strtolower( preg_replace('#^https?://#i','',$val) ) ); ?>
</a>
<?php elseif ( $def['type'] === 'textarea' ): ?>
<p style="font-size:.85rem"><?php echo nl2br(esc_html($val)); ?></p>
<?php else: ?>
<p style="font-size:.85rem"><?php echo esc_html($val); ?></p>
<?php endif; ?>
<?php foreach ($cf_by_cat_sb[$scid_sb] as $cf_entry_sb):
$def_sb = $cf_entry_sb['def'];
$val_sb = $cf_entry_sb['val'];
// Auto-Link für Telegram und Discord anhand des Feld-Keys erkennen
$key_sb = strtolower($def_sb['key']);
$is_telegram = strpos($key_sb, 'telegram') !== false;
$is_discord = strpos($key_sb, 'discord') !== false;
?>
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:6px;margin-bottom:4px;font-size:.85rem">
<span style="color:var(--c-muted,#94a3b8);flex-shrink:0"><?php echo esc_html($def_sb['label']); ?></span>
<?php if ($def_sb['type'] === 'url'): ?>
<a href="<?php echo esc_url($val_sb); ?>" target="_blank" rel="noopener noreferrer"
style="color:var(--c-primary);word-break:break-all;text-align:right">
<?php echo esc_html(mb_strtolower(preg_replace('#^https?://#i','',$val_sb))); ?>
</a>
<?php elseif ($is_telegram):
// Username bereinigen: @ und Leerzeichen entfernen
$tg_user = ltrim(trim($val_sb), '@');
$tg_url = 'https://t.me/' . rawurlencode($tg_user);
?>
<a href="<?php echo esc_url($tg_url); ?>" target="_blank" rel="noopener noreferrer"
style="color:#29b6f6;text-align:right;font-size:inherit">
@<?php echo esc_html($tg_user); ?>
</a>
<?php elseif ($is_discord): ?>
<span style="color:#7289da;text-align:right;font-size:inherit">
<?php echo esc_html($val_sb); ?>
</span>
<?php elseif ($def_sb['type'] === 'textarea'): ?>
<span style="text-align:right"><?php echo nl2br(esc_html($val_sb)); ?></span>
<?php elseif ($def_sb['type'] === 'date'):
$age_sb = self::calc_age($val_sb); ?>
<span><?php echo $age_sb !== null ? esc_html((string)$age_sb) . ' Jahre' : '—'; ?></span>
<?php else: ?>
<span style="text-align:right"><?php echo esc_html($val_sb); ?></span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</aside>
<!-- ── MAIN ────────────────────────────────────────────── -->
<div class="wbf-profile-main">
<!-- Profil bearbeiten (nur eigenes) -->
<!-- DM-Button + Ignorieren-Button (nur auf fremden Profilen) -->
<?php if ($current && !$is_own && WBF_Roles::level($profile->role) >= 0): ?>
<div style="display:flex;justify-content:flex-end;gap:.5rem;margin-bottom:.75rem;flex-wrap:wrap">
<a href="?forum_dm=inbox&with=<?php echo (int)$profile->id; ?>"
class="wbf-btn wbf-btn--sm wbf-btn--primary">
<i class="fas fa-envelope"></i> Nachricht senden
</a>
<?php if ( wbf_can_be_ignored($profile) ):
$viewer_ignores = WBF_DB::is_ignored($current->id, $profile->id); ?>
<button class="wbf-ignore-btn wbf-btn wbf-btn--sm<?php echo $viewer_ignores?' wbf-btn--primary':''; ?>"
data-id="<?php echo (int)$profile->id; ?>"
data-name="<?php echo esc_attr($profile->display_name); ?>"
data-ignored="<?php echo $viewer_ignores?'1':'0'; ?>">
<i class="fas fa-<?php echo $viewer_ignores?'eye':'eye-slash'; ?>"></i>
<?php echo $viewer_ignores?'Ignorierung aufheben':'Nutzer ignorieren'; ?>
</button>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- ── TAB-NAVIGATION ─────────────────────────────── -->
<?php if ($is_own): ?>
<div class="wbf-profile-card"> <div class="wbf-profile-card__header">
<div class="wbf-profile-tabs">
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=1"
class="wbf-profile-tab<?php echo $active_tab===1?' active':''; ?>">
<i class="fas fa-sliders"></i> Profil
</a>
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=2"
class="wbf-profile-tab<?php echo $active_tab===2?' active':''; ?>">
<i class="fas fa-comments"></i> Aktivität
</a>
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=3"
class="wbf-profile-tab<?php echo $active_tab===3?' active':''; ?>">
<i class="fas fa-shield-halved"></i> Privatsphäre
</a>
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=4"
class="wbf-profile-tab<?php echo $active_tab===4?' active':''; ?>">
<i class="fas fa-lock"></i> Sicherheit
</a>
<?php if ( class_exists('MC_Gallery_Forum_Bridge') ) : ?>
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=mc"
class="wbf-profile-tab<?php echo $active_tab==='mc'?' active':''; ?>">
<i class="fas fa-cubes"></i> Minecraft
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- ══════════════════════════════════════════════════
TAB 1 — Profil bearbeiten + Weitere Profilangaben
══════════════════════════════════════════════════ -->
<?php if ($is_own && $active_tab === 1): ?>
<!-- Profil bearbeiten -->
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-sliders"></i> Profil bearbeiten
</div>
<div class="wbf-profile-card__body">
<div class="wbf-profile-edit-grid">
<div class="wbf-form-row">
<label>Anzeigename</label>
<input type="text" id="wbfEditName" value="<?php echo esc_attr($profile->display_name); ?>">
</div>
<div class="wbf-form-row">
<label>Neues Passwort <small>(leer = nicht ändern)</small></label>
<input type="password" id="wbfNewPassword" placeholder="••••••">
</div>
<div class="wbf-form-row">
<label>Anzeigename</label>
<input type="text" id="wbfEditName" value="<?php echo esc_attr($profile->display_name); ?>">
</div>
<div class="wbf-form-row">
<label>Bio</label>
@@ -995,65 +1143,70 @@ class WBF_Shortcodes {
</div>
<div class="wbf-form-row">
<label>Signatur <small>(max. 300 Zeichen)</small></label>
<div class="wbf-form-row" style="display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem">
<label style="font-size:.82rem;color:var(--c-muted)">Profil öffentlich sichtbar</label>
<?php $pub = (int)($profile->profile_public ?? 1); ?>
<button type="button" id="wbfToggleProfileVis"
class="wbf-btn wbf-btn--sm<?php echo $pub?' wbf-btn--primary':''; ?>"
data-state="<?php echo $pub; ?>">
<i class="fas fa-<?php echo $pub?'eye':'eye-slash'; ?>"></i>
<?php echo $pub?'Öffentlich':'Privat'; ?>
</button>
</div>
<textarea id="wbfEditSignature" rows="2" maxlength="300" placeholder="Deine Signatur…"><?php echo esc_textarea($profile->signature ?? ''); ?></textarea>
<textarea id="wbfEditSignature" rows="2" maxlength="300" placeholder="Deine Signatur…"><?php echo esc_textarea($profile->signature ?? ''); ?></textarea>
<div class="wbf-sig-counter"><span id="wbfSigCount"><?php echo mb_strlen($profile->signature??''); ?></span>/300</div>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfile">
<i class="fas fa-save"></i> Speichern
</button>
<span class="wbf-msg" id="wbfProfileMsg"></span>
</div>
</div>
</div>
<!-- ── Benutzerdefinierte Profilfelder ──────────────── -->
<!-- Weitere Profilangaben — nach Kategorie gruppiert (ohne eigene Speichern-Buttons) -->
<?php
$cf_defs = WBF_DB::get_profile_field_defs();
$cf_vals = WBF_DB::get_user_meta( $profile->id );
if ( ! empty( $cf_defs ) ):
$cf_edit_by_cat = [];
foreach ( $cf_defs as $def_e ) {
$cid_e = $def_e['category_id'] ?? '';
if (!$cid_e || !isset($cf_cat_map[$cid_e])) $cid_e = '__none__';
$cf_edit_by_cat[$cid_e][] = $def_e;
}
$edit_sections = $cf_cats;
if (isset($cf_edit_by_cat['__none__'])) {
$edit_sections[] = ['id'=>'__none__','name'=>'Weitere Angaben','icon'=>'📋'];
}
if (!empty($cf_defs)):
foreach ($edit_sections as $ecat):
$ecid = $ecat['id'];
if (empty($cf_edit_by_cat[$ecid])) continue;
?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-sliders"></i> Weitere Profilangaben
<?php if(!empty($ecat['icon'])): ?>
<span style="margin-right:5px"><?php echo esc_html($ecat['icon']); ?></span>
<?php else: ?><i class="fas fa-sliders"></i><?php endif; ?>
<?php echo esc_html($ecat['name']); ?>
</div>
<div class="wbf-profile-card__body">
<div class="wbf-profile-edit-grid">
<?php foreach ( $cf_defs as $def ):
$k = esc_attr( $def['key'] );
$lbl = esc_html( $def['label'] );
$ph = esc_attr( $def['placeholder'] ?? '' );
$val = esc_attr( $cf_vals[ $def['key'] ] ?? '' );
$req = ! empty($def['required']) ? 'required' : '';
<?php foreach ($cf_edit_by_cat[$ecid] as $def):
$k = esc_attr($def['key']);
$lbl = esc_html($def['label']);
$ph = esc_attr($def['placeholder'] ?? '');
$val = esc_attr($cf_vals[$def['key']] ?? '');
$req = !empty($def['required']) ? 'required' : '';
?>
<div class="wbf-form-row">
<label><?php echo $lbl; ?><?php if($req): ?> <span style="color:var(--c-danger)">*</span><?php endif; ?></label>
<?php if ( $def['type'] === 'textarea' ): ?>
<?php if ($def['type'] === 'textarea'): ?>
<textarea class="wbf-cf-input" data-field="cf_<?php echo $k; ?>"
rows="2" placeholder="<?php echo $ph; ?>"
<?php echo $req; ?>><?php echo esc_textarea( $cf_vals[$def['key']] ?? '' ); ?></textarea>
<?php elseif ( $def['type'] === 'select' ):
$opts = array_filter( array_map('trim', explode("\n", $def['options'] ?? '')) );
<?php echo $req; ?>><?php echo esc_textarea($cf_vals[$def['key']] ?? ''); ?></textarea>
<?php elseif ($def['type'] === 'select'):
$opts = array_filter(array_map('trim', explode("\n", $def['options'] ?? '')));
?>
<select class="wbf-cf-input" data-field="cf_<?php echo $k; ?>">
<option value="">— Bitte wählen —</option>
<?php foreach ( $opts as $opt ): ?>
<?php foreach ($opts as $opt): ?>
<option value="<?php echo esc_attr($opt); ?>"
<?php selected( $cf_vals[$def['key']] ?? '', $opt ); ?>><?php echo esc_html($opt); ?></option>
<?php selected($cf_vals[$def['key']] ?? '', $opt); ?>><?php echo esc_html($opt); ?></option>
<?php endforeach; ?>
</select>
<?php elseif ($def['type'] === 'date'): ?>
<input type="date"
class="wbf-cf-input"
data-field="cf_<?php echo $k; ?>"
value="<?php echo esc_attr($cf_vals[$def['key']] ?? ''); ?>"
max="<?php echo date('Y-m-d'); ?>"
<?php echo $req; ?>>
<?php else: ?>
<input type="<?php echo $def['type'] === 'url' ? 'url' : ($def['type'] === 'number' ? 'number' : 'text'); ?>"
<input type="<?php echo $def['type']==='url'?'url':($def['type']==='number'?'number':'text'); ?>"
class="wbf-cf-input"
data-field="cf_<?php echo $k; ?>"
value="<?php echo $val; ?>"
@@ -1063,105 +1216,27 @@ class WBF_Shortcodes {
</div>
<?php endforeach; ?>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfileCf">
<i class="fas fa-save"></i> Speichern
</button>
<span class="wbf-msg" id="wbfProfileCfMsg"></span>
</div>
</div>
</div>
<?php endif; ?>
<?php endforeach; endif; ?>
<!-- ── DSGVO: Konto löschen ──────────────────────────── -->
<div class="wbf-profile-card" style="border-color:rgba(240,82,82,.25)">
<div class="wbf-profile-card__header" style="color:var(--c-danger);background:rgba(240,82,82,.06);border-bottom-color:rgba(240,82,82,.15)">
<i class="fas fa-shield-halved"></i> Datenschutz & Konto löschen
</div>
<div class="wbf-profile-card__body">
<p style="font-size:.85rem;color:var(--c-text-dim);margin-bottom:1rem;line-height:1.6">
Gemäß <strong>DSGVO Art. 17</strong> (Recht auf Vergessenwerden) kannst du die vollständige Löschung deines Kontos und aller personenbezogenen Daten beantragen.<br>
<span style="color:var(--c-muted);font-size:.8rem">Deine Beiträge bleiben anonymisiert sichtbar. Direktnachrichten, Likes, Profilinformationen und alle persönlichen Daten werden dauerhaft gelöscht.</span>
</p>
<div id="wbfGdprBox" style="background:rgba(240,82,82,.06);border:1px solid rgba(240,82,82,.2);border-radius:var(--radius-sm);padding:1.1rem;display:none">
<p style="font-size:.82rem;font-weight:700;color:var(--c-danger);margin-bottom:.9rem"><i class="fas fa-triangle-exclamation"></i> Diese Aktion ist unwiderruflich.</p>
<div class="wbf-form-row">
<label style="font-size:.72rem">Passwort zur Bestätigung</label>
<input type="password" id="wbfGdprPassword" placeholder="Dein aktuelles Passwort" autocomplete="current-password">
</div>
<label style="display:flex;align-items:center;gap:.6rem;font-size:.82rem;color:var(--c-text-dim);cursor:pointer;margin-bottom:1rem">
<input type="checkbox" id="wbfGdprConfirm" style="width:15px;height:15px;accent-color:var(--c-danger);cursor:pointer">
Ich verstehe, dass mein Konto und alle persönlichen Daten unwiderruflich gelöscht werden.
</label>
<div style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap">
<button class="wbf-btn wbf-btn--sm" id="wbfGdprCancel" onclick="document.getElementById('wbfGdprBox').style.display='none';document.getElementById('wbfGdprToggle').style.display=''">
<i class="fas fa-xmark"></i> Abbrechen
</button>
<button class="wbf-btn wbf-btn--sm" id="wbfGdprSubmit"
style="background:rgba(240,82,82,.15);color:var(--c-danger);border-color:rgba(240,82,82,.4)">
<i class="fas fa-trash-can"></i> Konto endgültig löschen
</button>
<span class="wbf-msg" id="wbfGdprMsg"></span>
</div>
</div>
<button class="wbf-btn wbf-btn--sm" id="wbfGdprToggle"
style="background:rgba(240,82,82,.08);color:var(--c-danger);border-color:rgba(240,82,82,.3)"
onclick="document.getElementById('wbfGdprBox').style.display='';document.getElementById('wbfGdprToggle').style.display='none'">
<i class="fas fa-trash-can"></i> Konto löschen (DSGVO Art. 17)
</button>
</div>
<!-- Globaler Speichern-Button für Tab 1 -->
<div style="display:flex;align-items:center;gap:1rem;padding:.25rem 0 .5rem">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfile" style="min-width:160px">
<i class="fas fa-save"></i> Alles speichern
</button>
<span class="wbf-msg" id="wbfProfileMsg"></span>
</div>
<?php endif; /* end $is_own */ ?>
<?php endif; /* end Tab 1 */ ?>
<!-- Beiträge -->
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-comments"></i> Beiträge
<span class="wbf-profile-card__count"><?php echo count($user_posts); ?></span>
</div>
<div class="wbf-profile-card__body wbf-profile-card__body--posts">
<?php if (empty($user_posts)): ?>
<p class="wbf-profile-empty">Noch keine Beiträge.</p>
<?php else:
foreach ($user_posts as $up):
$preview = esc_html( mb_substr( strip_tags($up->content), 0, 130 ) );
$more = mb_strlen( strip_tags($up->content) ) > 130 ? '…' : '';
$is_thread = isset($up->entry_type) && $up->entry_type === 'thread';
$anchor = $is_thread
? '?forum_thread=' . (int)$up->thread_id
: '?forum_thread=' . (int)$up->thread_id . '#post-' . (int)$up->id;
?>
<div class="wbf-profile-post-item">
<div class="wbf-profile-post-item__top">
<?php if ($is_thread): ?>
<span class="wbf-profile-post-item__type wbf-profile-post-item__type--thread">
<i class="fas fa-layer-group"></i> Thread
</span>
<?php else: ?>
<span class="wbf-profile-post-item__type wbf-profile-post-item__type--reply">
<i class="fas fa-reply"></i> Antwort
</span>
<?php endif; ?>
<a href="<?php echo esc_url($anchor); ?>" class="wbf-profile-post-item__title">
<?php echo esc_html( mb_substr($up->thread_title, 0, 60) ); ?>
</a>
<span class="wbf-profile-post-item__cat">
<i class="fas fa-folder"></i> <?php echo esc_html($up->cat_name); ?>
</span>
<span class="wbf-profile-post-item__time"><?php echo self::time_ago($up->created_at); ?></span>
</div>
<?php if ($preview): ?>
<p class="wbf-profile-post-item__preview"><?php echo $preview . $more; ?></p>
<?php endif; ?>
</div>
<?php endforeach; endif; ?>
</div>
</div>
<!-- ══════════════════════════════════════════════════
TAB 2 — Lesezeichen + Beiträge
══════════════════════════════════════════════════ -->
<?php if ($active_tab === 2): ?>
<!-- Lesezeichen (nur eigenes Profil) -->
<?php if ($is_own):
$bookmarks = WBF_DB::get_user_bookmarks($current->id, 50); ?>
<?php if ($is_own): ?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-bookmark"></i> Lesezeichen
@@ -1186,6 +1261,287 @@ class WBF_Shortcodes {
</div>
<?php endif; ?>
<!-- Beiträge -->
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-comments"></i> Beiträge
<span class="wbf-profile-card__count"><?php echo count($user_posts); ?></span>
</div>
<div class="wbf-profile-card__body wbf-profile-card__body--posts">
<?php if (empty($user_posts)): ?>
<p class="wbf-profile-empty">Noch keine Beiträge.</p>
<?php else:
foreach ($user_posts as $up):
$preview = esc_html(mb_substr(strip_tags($up->content), 0, 130));
$more = mb_strlen(strip_tags($up->content)) > 130 ? '…' : '';
$is_thread = isset($up->entry_type) && $up->entry_type === 'thread';
$anchor = $is_thread
? '?forum_thread=' . (int)$up->thread_id
: '?forum_thread=' . (int)$up->thread_id . '#post-' . (int)$up->id;
?>
<div class="wbf-profile-post-item">
<div class="wbf-profile-post-item__top">
<?php if ($is_thread): ?>
<span class="wbf-profile-post-item__type wbf-profile-post-item__type--thread">
<i class="fas fa-layer-group"></i> Thread
</span>
<?php else: ?>
<span class="wbf-profile-post-item__type wbf-profile-post-item__type--reply">
<i class="fas fa-reply"></i> Antwort
</span>
<?php endif; ?>
<a href="<?php echo esc_url($anchor); ?>" class="wbf-profile-post-item__title">
<?php echo esc_html(mb_substr($up->thread_title, 0, 60)); ?>
</a>
<span class="wbf-profile-post-item__cat">
<i class="fas fa-folder"></i> <?php echo esc_html($up->cat_name); ?>
</span>
<span class="wbf-profile-post-item__time"><?php echo self::time_ago($up->created_at); ?></span>
</div>
<?php if ($preview): ?>
<p class="wbf-profile-post-item__preview"><?php echo $preview . $more; ?></p>
<?php endif; ?>
</div>
<?php endforeach; endif; ?>
</div>
</div>
<?php endif; /* end Tab 2 */ ?>
<!-- ══════════════════════════════════════════════════
TAB 3 — Ignorierte Nutzer + Datenschutz
══════════════════════════════════════════════════ -->
<?php if ($is_own && $active_tab === 3): ?>
<!-- Profil-Sichtbarkeit -->
<?php $pub = (int)($profile->profile_public ?? 1); ?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-eye"></i> Profil-Sichtbarkeit
</div>
<div class="wbf-profile-card__body">
<div class="wbf-form-row" style="display:flex;align-items:center;gap:1rem">
<div>
<div style="font-size:.9rem;font-weight:600;margin-bottom:3px">Profil öffentlich sichtbar</div>
<div style="font-size:.8rem;color:var(--c-muted)">Wenn deaktiviert, können nur du selbst und Moderatoren dein Profil sehen.</div>
</div>
<button type="button" id="wbfToggleProfileVis"
class="wbf-btn wbf-btn--sm<?php echo $pub?' wbf-btn--primary':''; ?>"
data-state="<?php echo $pub; ?>"
style="margin-left:auto;white-space:nowrap">
<i class="fas fa-<?php echo $pub?'eye':'eye-slash'; ?>"></i>
<?php echo $pub?'Öffentlich':'Privat'; ?>
</button>
</div>
</div>
</div>
<!-- Ignorierte Nutzer -->
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-bell"></i> E-Mail-Benachrichtigungen
</div>
<div class="wbf-profile-card__body">
<?php
$notif_meta = WBF_DB::get_user_meta($current->id);
$n_reply = ($notif_meta['notify_reply'] ?? '1') !== '0';
$n_mention = ($notif_meta['notify_mention'] ?? '1') !== '0';
$n_message = ($notif_meta['notify_message'] ?? '1') !== '0';
?>
<p style="font-size:.82rem;color:var(--c-muted);margin-bottom:1rem">
Lege fest bei welchen Ereignissen du eine E-Mail erhältst.
</p>
<div class="wbf-notif-pref-list">
<label class="wbf-notif-pref">
<div class="wbf-notif-pref__info">
<span><i class="fas fa-reply"></i> Antworten auf meine Threads</span>
<small>Wenn jemand in einem deiner Threads antwortet</small>
</div>
<div class="wbf-toggle<?php echo $n_reply?' wbf-toggle--on':''; ?>"
id="wbfNotifReply" data-key="notify_reply" data-state="<?php echo $n_reply?'1':'0'; ?>">
<div class="wbf-toggle__knob"></div>
</div>
</label>
<label class="wbf-notif-pref">
<div class="wbf-notif-pref__info">
<span><i class="fas fa-at"></i> @Erwähnungen</span>
<small>Wenn dich jemand in einem Beitrag erwähnt</small>
</div>
<div class="wbf-toggle<?php echo $n_mention?' wbf-toggle--on':''; ?>"
id="wbfNotifMention" data-key="notify_mention" data-state="<?php echo $n_mention?'1':'0'; ?>">
<div class="wbf-toggle__knob"></div>
</div>
</label>
<label class="wbf-notif-pref">
<div class="wbf-notif-pref__info">
<span><i class="fas fa-envelope"></i> Privatnachrichten</span>
<small>Wenn du eine neue Direktnachricht erhältst</small>
</div>
<div class="wbf-toggle<?php echo $n_message?' wbf-toggle--on':''; ?>"
id="wbfNotifMessage" data-key="notify_message" data-state="<?php echo $n_message?'1':'0'; ?>">
<div class="wbf-toggle__knob"></div>
</div>
</label>
</div>
<div class="wbf-profile-card__footer" style="margin-top:1rem">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveNotifPrefs">
<i class="fas fa-save"></i> Einstellungen speichern
</button>
<span class="wbf-msg" id="wbfNotifPrefsMsg"></span>
</div>
</div>
</div>
<!-- Ignorierte Nutzer -->
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-eye-slash"></i> Ignorierte Nutzer
<span class="wbf-profile-card__count" id="wbfIgnoreCount"><?php echo count($ignore_list); ?></span>
</div>
<div class="wbf-profile-card__body" id="wbfIgnoreListWrap">
<?php if (empty($ignore_list)): ?>
<p class="wbf-profile-empty" id="wbfIgnoreEmpty">Du ignorierst niemanden.</p>
<?php else: ?>
<div class="wbf-ignore-list" id="wbfIgnoreList">
<?php foreach ($ignore_list as $ign): ?>
<div class="wbf-ignore-item" id="wbf-ignore-item-<?php echo (int)$ign->id; ?>">
<a href="?forum_profile=<?php echo (int)$ign->id; ?>" class="wbf-ignore-item__avatar">
<?php echo self::avatar($ign->avatar_url, $ign->display_name, 36); ?>
</a>
<div class="wbf-ignore-item__info">
<a href="?forum_profile=<?php echo (int)$ign->id; ?>" class="wbf-ignore-item__name">
<?php echo esc_html($ign->display_name); ?>
</a>
<span class="wbf-ignore-item__since">Ignoriert seit <?php echo self::time_ago($ign->ignored_since); ?></span>
</div>
<button class="wbf-ignore-btn wbf-btn wbf-btn--sm"
data-id="<?php echo (int)$ign->id; ?>"
data-name="<?php echo esc_attr($ign->display_name); ?>"
data-ignored="1"
style="margin-left:auto">
<i class="fas fa-eye"></i> Entblocken
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Datenschutz & Konto löschen -->
<div class="wbf-profile-card" style="border-color:rgba(240,82,82,.25)">
<div class="wbf-profile-card__header" style="color:var(--c-danger);background:rgba(240,82,82,.06);border-bottom-color:rgba(240,82,82,.15)">
<i class="fas fa-shield-halved"></i> Datenschutz & Konto löschen
</div>
<div class="wbf-profile-card__body">
<p style="font-size:.85rem;color:var(--c-text-dim);margin-bottom:1rem;line-height:1.6">
Gemäß <strong>DSGVO Art. 17</strong> (Recht auf Vergessenwerden) kannst du die vollständige Löschung deines Kontos und aller personenbezogenen Daten beantragen.<br>
<span style="color:var(--c-muted);font-size:.8rem">Deine Beiträge bleiben anonymisiert sichtbar. Direktnachrichten, Likes, Profilinformationen und alle persönlichen Daten werden dauerhaft gelöscht.</span>
</p>
<div id="wbfGdprBox" style="background:rgba(240,82,82,.06);border:1px solid rgba(240,82,82,.2);border-radius:var(--radius-sm);padding:1.1rem;display:none">
<p style="font-size:.82rem;font-weight:700;color:var(--c-danger);margin-bottom:.9rem"><i class="fas fa-triangle-exclamation"></i> Diese Aktion ist unwiderruflich.</p>
<div class="wbf-form-row">
<label style="font-size:.72rem">Passwort zur Bestätigung</label>
<input type="password" id="wbfGdprPassword" placeholder="Dein aktuelles Passwort" autocomplete="current-password">
</div>
<label style="display:flex;align-items:center;gap:.6rem;font-size:.82rem;color:var(--c-text-dim);cursor:pointer;margin-bottom:1rem">
<input type="checkbox" id="wbfGdprConfirm" style="width:15px;height:15px;accent-color:var(--c-danger);cursor:pointer">
Ich verstehe, dass mein Konto und alle persönlichen Daten unwiderruflich gelöscht werden.
</label>
<div style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap">
<button class="wbf-btn wbf-btn--sm" id="wbfGdprCancel"
onclick="document.getElementById('wbfGdprBox').style.display='none';document.getElementById('wbfGdprToggle').style.display=''">
<i class="fas fa-xmark"></i> Abbrechen
</button>
<button class="wbf-btn wbf-btn--sm" id="wbfGdprSubmit"
style="background:rgba(240,82,82,.15);color:var(--c-danger);border-color:rgba(240,82,82,.4)">
<i class="fas fa-trash-can"></i> Konto endgültig löschen
</button>
<span class="wbf-msg" id="wbfGdprMsg"></span>
</div>
</div>
<button class="wbf-btn wbf-btn--sm" id="wbfGdprToggle"
style="background:rgba(240,82,82,.08);color:var(--c-danger);border-color:rgba(240,82,82,.3)"
onclick="document.getElementById('wbfGdprBox').style.display='';document.getElementById('wbfGdprToggle').style.display='none'">
<i class="fas fa-trash-can"></i> Konto löschen (DSGVO Art. 17)
</button>
</div>
</div>
<?php endif; /* end Tab 3 */ ?>
<!-- ══════════════════════════════════════════════════
TAB 4 — Sicherheit (Passwort & E-Mail)
══════════════════════════════════════════════════ -->
<?php if ($is_own && $active_tab === 4): ?>
<!-- Passwort ändern -->
<div class="wbf-profile-card" style="border-color:rgba(0,180,216,.25)">
<div class="wbf-profile-card__header" style="background:rgba(0,180,216,.07);border-bottom-color:rgba(0,180,216,.18)">
<i class="fas fa-lock" style="color:var(--c-primary)"></i> Passwort ändern
</div>
<div class="wbf-profile-card__body">
<div class="wbf-form-row">
<label>Aktuelles Passwort</label>
<input type="password" id="wbfCurrentPassword" placeholder="Dein aktuelles Passwort" autocomplete="current-password">
</div>
<div class="wbf-profile-edit-grid">
<div class="wbf-form-row">
<label>Neues Passwort <small>(min. 6 Zeichen)</small></label>
<input type="password" id="wbfNewPassword" placeholder="Neues Passwort" autocomplete="new-password">
</div>
<div class="wbf-form-row">
<label>Neues Passwort wiederholen</label>
<input type="password" id="wbfNewPassword2" placeholder="Passwort bestätigen" autocomplete="new-password">
</div>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSavePassword">
<i class="fas fa-key"></i> Passwort ändern
</button>
<span class="wbf-msg" id="wbfPasswordMsg"></span>
</div>
</div>
</div>
<!-- E-Mail-Adresse ändern -->
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-envelope"></i> E-Mail-Adresse
</div>
<div class="wbf-profile-card__body">
<p style="font-size:.82rem;color:var(--c-muted);margin-bottom:1rem">
Aktuelle Adresse: <strong style="color:var(--c-text)"><?php echo esc_html($profile->email); ?></strong>
</p>
<div class="wbf-profile-edit-grid">
<div class="wbf-form-row">
<label>Neue E-Mail-Adresse</label>
<input type="email" id="wbfNewEmail" placeholder="neue@email.de" autocomplete="off">
</div>
<div class="wbf-form-row">
<label>Aktuelles Passwort <small>(zur Bestätigung)</small></label>
<input type="password" id="wbfEmailPassword" placeholder="••••••" autocomplete="current-password">
</div>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveEmail">
<i class="fas fa-envelope"></i> E-Mail ändern
</button>
<span class="wbf-msg" id="wbfEmailMsg"></span>
</div>
</div>
</div>
<?php endif; /* end Tab 4 */ ?>
<!-- ══════════════════════════════════════════════════
TAB MC — Minecraft-Konto verknüpfen (Bridge)
Wird nur gerendert wenn MC Gallery Forum Bridge aktiv ist.
══════════════════════════════════════════════════ -->
<?php if ( $is_own && $active_tab === 'mc' && class_exists('MC_Gallery_Forum_Bridge') ) :
echo apply_filters('wbf_profile_tab_content', '', 'minecraft', $profile);
endif; /* end Tab MC */ ?>
</div><!-- /.wbf-profile-main -->
</div><!-- /.wbf-profile-layout -->
</div>
@@ -1193,6 +1549,7 @@ class WBF_Shortcodes {
<?php return ob_get_clean();
}
// ── TAG PAGE ─────────────────────────────────────────────────────────────
private static function view_tag() {
@@ -1274,8 +1631,8 @@ class WBF_Shortcodes {
</div>
</div>
<?php endif; ?>
</div>
<?php self::render_forum_footer(); ?>
</div>
<?php self::render_auth_modal(); ?>
</div>
<?php return ob_get_clean();
@@ -1355,8 +1712,8 @@ class WBF_Shortcodes {
<?php endif; ?>
</div>
</div>
</div>
<?php self::render_forum_footer(); ?>
</div>
<?php self::render_auth_modal(); ?>
<?php self::render_dm_compose_modal(); ?>
</div>
@@ -1399,7 +1756,7 @@ class WBF_Shortcodes {
if ($maint_s === '1' && (!$cur_s || WBF_Roles::level($cur_s->role) < 50)) return self::view_maintenance();
$query = sanitize_text_field($_GET['q'] ?? '');
$current = WBF_Auth::get_current_user();
$results = mb_strlen($query) >= 2 ? WBF_DB::search($query, 40) : [];
$results = mb_strlen($query) >= 2 ? WBF_DB::search($query, 40, $current) : [];
ob_start(); ?>
<div class="wbf-wrap">
<?php self::render_topbar($current); ?>
@@ -1443,8 +1800,8 @@ class WBF_Shortcodes {
</div>
<p style="color:var(--c-muted);font-size:.82rem;margin-top:1rem"><?php echo count($results); ?> Ergebnis(se) gefunden.</p>
<?php endif; ?>
</div>
<?php self::render_forum_footer(); ?>
</div>
<?php self::render_auth_modal(); ?>
</div>
<?php return ob_get_clean();
@@ -1502,7 +1859,7 @@ class WBF_Shortcodes {
<?php echo esc_html($current->display_name); ?>
<?php echo self::role_badge($current->role); ?>
</a>
<a href="<?php echo esc_url(wbf_get_forum_url() . '?wbf_do_logout=1'); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
<a href="<?php echo esc_url(wp_nonce_url(wbf_get_forum_url() . '?wbf_do_logout=1', 'wbf_logout')); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
<?php else: ?>
<button class="wbf-btn wbf-btn--sm" id="wbfOpenLogin"><?php echo esc_html(wbf_get_settings()['btn_login']); ?></button>
<button class="wbf-btn wbf-btn--sm wbf-btn--primary" id="wbfOpenRegister"><?php echo esc_html(wbf_get_settings()['btn_register']); ?></button>
@@ -2080,8 +2437,8 @@ class WBF_Shortcodes {
</div>
<?php endif; ?>
</div>
<?php self::render_forum_footer(); ?>
</div>
<?php self::render_auth_modal(); ?>
</div>
<?php return ob_get_clean();
@@ -2120,7 +2477,7 @@ class WBF_Shortcodes {
if ( ( wbf_get_settings()['rules_enabled'] ?? '1' ) !== '1' ) return;
$rules_url = esc_url( wbf_get_forum_url() . '?forum_rules=1' );
?>
<div style="border-top:1px solid var(--c-border);margin-top:3rem;padding:1.25rem 1.5rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem;background:var(--c-bg2)">
<div style="border:1px solid var(--c-border);border-radius:var(--radius);margin-top:2rem;padding:1rem 1.25rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem;background:var(--c-surface)">
<span style="font-size:.78rem;color:var(--c-muted)">
<i class="fas fa-shield-halved" style="color:var(--c-primary);margin-right:.35rem"></i>
Durch die Nutzung des Forums stimmst du unseren Regeln zu.

View File

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

View File

@@ -3,17 +3,18 @@
* Plugin Name: WP Business Forum
* Plugin URI: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum
* Description: Professionelles Forum mit eigenem Login, Rollen, Signaturen, Hierarchie und Moderations-Tools.
* Version: 1.0.1
* Version: 1.0.3
* Author: M_Viper
* Author URI: https://m-viper.de
* Text Domain: wp-business-forum
* Requires PHP: 7.0
*/
if ( ! defined( 'ABSPATH' ) ) exit;
define( 'WBF_PATH', plugin_dir_path( __FILE__ ) );
define( 'WBF_URL', plugin_dir_url( __FILE__ ) );
define( 'WBF_VERSION', '1.0.1' );
define( 'WBF_VERSION', '1.0.2' );
require_once WBF_PATH . 'includes/class-forum-db.php';
require_once WBF_PATH . 'includes/class-forum-roles.php';
@@ -22,6 +23,7 @@ require_once WBF_PATH . 'includes/class-forum-bbcode.php';
require_once WBF_PATH . 'includes/class-forum-auth.php';
require_once WBF_PATH . 'includes/class-forum-shortcodes.php';
require_once WBF_PATH . 'includes/class-forum-ajax.php';
require_once WBF_PATH . 'includes/class-forum-export.php';
require_once WBF_PATH . 'admin/forum-admin.php';
require_once WBF_PATH . 'admin/forum-settings.php';
require_once WBF_PATH . 'admin/forum-setup.php';
@@ -33,6 +35,38 @@ register_activation_hook( __FILE__, function() {
set_transient( 'wbf_activation_redirect', true, 30 );
});
// ── Export / Import Hooks ─────────────────────────────────────────────────────
add_action( 'plugins_loaded', function() {
WBF_Export::hooks();
}, 5 );
// ── DB-Schema sicherstellen (läuft bei jedem Seitenaufruf, sehr günstig) ─────
// Stellt sicher dass neue Spalten auch auf bestehenden Installs vorhanden sind,
// ohne dass das Plugin erneut deaktiviert/aktiviert werden muss.
add_action( 'plugins_loaded', function() {
$db_ver = (int) get_option( 'wbf_db_version', 0 );
if ( $db_ver < 2 ) {
global $wpdb;
// profile_public: Sicherheits-kritisch — muss immer existieren
$cols = $wpdb->get_col( "DESCRIBE {$wpdb->prefix}forum_users" );
if ( ! in_array( 'profile_public', $cols ) ) {
$wpdb->query( "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) NOT NULL DEFAULT 1" );
// Alle bestehenden User explizit auf öffentlich setzen
$wpdb->query( "UPDATE {$wpdb->prefix}forum_users SET profile_public = 1 WHERE profile_public IS NULL" );
}
update_option( 'wbf_db_version', 2 );
}
}, 10 );
// ── Session frühzeitig starten (PHP 8.3 Fix) ────────────────────────────────
// session_start() MUSS vor jedem HTML-Output laufen.
// plugins_loaded (Prio 1) ist der früheste sichere Zeitpunkt in WordPress.
// Der 'init'-Hook (in class-forum-auth.php) läuft als Fallback weiterhin,
// aber dieser frühe Aufruf verhindert den PHP 8.3 E_WARNING "headers already sent".
add_action( 'plugins_loaded', function() {
WBF_Auth::init();
}, 1 );
// ── Superadmin-Sync ───────────────────────────────────────────────────────────
add_action( 'wp_login', function() { WBF_Roles::sync_superadmin(); } );
add_action( 'init', function() { WBF_Roles::sync_superadmin(); } );
@@ -57,6 +91,7 @@ if ( ! wp_next_scheduled( 'wbf_check_expired_bans' ) ) {
register_deactivation_hook( __FILE__, function() {
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
wp_clear_scheduled_hook( 'wbf_check_for_updates' );
} );
@@ -98,4 +133,156 @@ add_action( 'wp_enqueue_scripts', function() {
'forum_url' => wbf_get_forum_url(),
'reactions' => WBF_DB::get_allowed_reactions(),
]);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// ── Update-Checker ────────────────────────────────────────────────────────────
// Prüft täglich gegen die Gitea-Releases-API ob eine neue Version verfügbar ist.
// Releases-URL: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum/releases
// ══════════════════════════════════════════════════════════════════════════════
define( 'WBF_UPDATE_API', 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/WP-Business-Forum/releases?limit=1&page=1' );
define( 'WBF_RELEASES_PAGE', 'https://git.viper.ipv64.net/M_Viper/WP-Business-Forum/releases' );
define( 'WBF_UPDATE_TRANSIENT','wbf_update_check' );
/**
* Holt die neueste Release-Info von Gitea (gecacht per Transient, 12h).
* Gibt null zurück wenn kein Update verfügbar oder API nicht erreichbar.
*
* @return array|null ['version'=>string, 'url'=>string, 'name'=>string, 'published'=>string, 'body'=>string]
*/
function wbf_get_latest_release() {
$cached = get_transient( WBF_UPDATE_TRANSIENT );
if ( $cached !== false ) {
return $cached ?: null; // false = noch nie gecacht, '' = kein Update
}
$response = wp_remote_get( WBF_UPDATE_API, [
'timeout' => 8,
'user-agent' => 'WP-Business-Forum/' . WBF_VERSION . '; ' . get_bloginfo('url'),
'sslverify' => true,
] );
if ( is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200 ) {
// Bei Fehler 1h warten bevor erneut versucht
set_transient( WBF_UPDATE_TRANSIENT, '', HOUR_IN_SECONDS );
return null;
}
$body = wp_remote_retrieve_body( $response );
$releases = json_decode( $body, true );
if ( empty($releases) || ! is_array($releases) || empty($releases[0]) ) {
set_transient( WBF_UPDATE_TRANSIENT, '', 12 * HOUR_IN_SECONDS );
return null;
}
$latest = $releases[0];
$version = ltrim( $latest['tag_name'] ?? '', 'v' ); // "v1.2.0" → "1.2.0"
$info = [
'version' => $version,
'url' => $latest['html_url'] ?? WBF_RELEASES_PAGE,
'name' => $latest['name'] ?? $latest['tag_name'] ?? $version,
'published' => $latest['published_at'] ?? '',
'body' => wp_strip_all_tags( $latest['body'] ?? '' ),
];
// 12 Stunden cachen
set_transient( WBF_UPDATE_TRANSIENT, $info, 12 * HOUR_IN_SECONDS );
return $info;
}
/**
* Prüft ob ein Update verfügbar ist.
* Gibt die Release-Info zurück wenn Gitea-Version > installierte Version.
*/
function wbf_update_available() {
$latest = wbf_get_latest_release();
if ( ! $latest || empty($latest['version']) ) return null;
if ( version_compare( $latest['version'], WBF_VERSION, '>' ) ) {
return $latest;
}
return null;
}
// ── Cron: täglich Update prüfen (Cache warm halten) ──────────────────────────
add_action( 'wbf_check_for_updates', function() {
delete_transient( WBF_UPDATE_TRANSIENT );
wbf_get_latest_release();
} );
if ( ! wp_next_scheduled( 'wbf_check_for_updates' ) ) {
wp_schedule_event( time(), 'twicedaily', 'wbf_check_for_updates' );
}
// ── Admin-Notice wenn Update verfügbar ───────────────────────────────────────
add_action( 'admin_notices', function() {
if ( ! current_user_can('manage_options') ) return;
$update = wbf_update_available();
if ( ! $update ) return;
// Notice ausblenden wenn der User sie weggeklickt hat (per GET-Parameter)
if ( isset($_GET['wbf_dismiss_update']) && check_admin_referer('wbf_dismiss_update') ) {
set_transient( 'wbf_update_dismissed_' . WBF_VERSION, $update['version'], 7 * DAY_IN_SECONDS );
wp_safe_redirect( remove_query_arg(['wbf_dismiss_update','_wpnonce']) );
exit;
}
$dismissed = get_transient( 'wbf_update_dismissed_' . WBF_VERSION );
if ( $dismissed === $update['version'] ) return;
$dismiss_url = wp_nonce_url(
add_query_arg('wbf_dismiss_update', '1'),
'wbf_dismiss_update'
);
$changelog_url = esc_url( $update['url'] );
$new_ver = esc_html( $update['version'] );
$cur_ver = esc_html( WBF_VERSION );
echo "
<div class=\"notice notice-warning is-dismissible\" style=\"border-left-color:#f59e0b;padding:12px 15px\">
<div style=\"display:flex;align-items:center;gap:14px;flex-wrap:wrap\">
<span style=\"font-size:1.6rem\">🔔</span>
<div>
<strong style=\"font-size:.95rem\">WP Business Forum — Update verfügbar!</strong>
<p style=\"margin:.3rem 0 0;color:#374151\">
Version <strong>{$new_ver}</strong> ist verfügbar. Du verwendest <strong>{$cur_ver}</strong>.
</p>
</div>
<div style=\"display:flex;gap:8px;margin-left:auto\">
<a href=\"{$changelog_url}\" target=\"_blank\" rel=\"noopener\"
class=\"button button-primary\" style=\"background:#f59e0b;border-color:#d97706\">
📋 Changelog & Download
</a>
<a href=\"" . esc_url($dismiss_url) . "\" class=\"button\">Später erinnern</a>
</div>
</div>
</div>";
} );
// ── Update-Badge im WP-Admin-Menü ─────────────────────────────────────────────
add_action( 'admin_menu', function() {
$update = wbf_update_available();
if ( ! $update ) return;
global $menu;
if ( ! is_array($menu) ) return;
foreach ( $menu as &$item ) {
if ( isset($item[2]) && $item[2] === 'wbf-admin' ) {
$item[0] .= ' <span class="update-plugins"><span class="plugin-count">1</span></span>';
break;
}
}
}, 999 );
// ── Manuellen Cache-Reset erlauben (für die Admin-UI) ─────────────────────────
add_action( 'admin_init', function() {
if ( ! isset($_GET['wbf_refresh_update']) ) return;
if ( ! current_user_can('manage_options') ) return;
if ( ! check_admin_referer('wbf_refresh_update') ) return;
delete_transient( WBF_UPDATE_TRANSIENT );
wp_safe_redirect( remove_query_arg(['wbf_refresh_update','_wpnonce']) );
exit;
} );