6 Commits
1.0.1 ... 1.0.2

Author SHA1 Message Date
65d2371239 Update from Git Manager GUI 2026-03-22 00:40:18 +01:00
ead2f3a62a Update from Git Manager GUI 2026-03-22 00:40:17 +01:00
dfdc74bcf9 Update from Git Manager GUI 2026-03-22 00:40:15 +01:00
44672a61aa Upload file uninstall.php via GUI 2026-03-22 00:40:09 +01:00
2adba16d29 Upload file wp-business-forum.php via GUI 2026-03-22 00:40:00 +01:00
290279df1c README.md aktualisiert 2026-03-21 23:39:28 +00:00
12 changed files with 3328 additions and 320 deletions

360
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)
### 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
- 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
6. E-Mail-Versand testen
7. Backup-Export erstellen
7. Vollständigen Backup-Export erstellen
Viel Erfolg mit deinem Forum!

View File

@@ -31,6 +31,7 @@ add_action( 'admin_menu', function() {
add_submenu_page( 'wbf-admin', 'Wortfilter', 'Wortfilter', 'manage_options', 'wbf-wordfilter', 'wbf_admin_wordfilter' );
add_submenu_page( 'wbf-admin', 'Export / Import','Export / Import','manage_options', 'wbf-export', 'wbf_admin_export' );
add_submenu_page( 'wbf-admin', '⚠️ Deinstallieren', '⚠️ Deinstallieren', 'manage_options', 'wbf-uninstall', 'wbf_admin_uninstall' );
add_submenu_page( 'wbf-admin', '🔔 Updates', '🔔 Updates', 'manage_options', 'wbf-updates', 'wbf_admin_updates' );
}, 10 );
// Meldungs-Badge im Menü (separater Hook mit Priorität 999, läuft nach der Registrierung)
@@ -84,6 +85,7 @@ add_action( 'admin_init', function() {
case 'settings':
$data['settings'] = get_option('wbf_settings', []);
$data['profile_fields'] = get_option('wbf_profile_fields', []);
$data['reactions_cfg'] = get_option('wbf_reactions', []);
break;
case 'roles':
$data['roles'] = get_option('wbf_custom_roles', []);
@@ -125,6 +127,16 @@ add_action( 'admin_init', function() {
);
$data['subscriptions'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_subscriptions ORDER BY id ASC", ARRAY_A );
break;
case 'polls':
$data['polls'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_polls ORDER BY id ASC", ARRAY_A );
$data['poll_votes'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_poll_votes ORDER BY id ASC", ARRAY_A );
break;
case 'bookmarks':
$data['bookmarks'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_bookmarks ORDER BY id ASC", ARRAY_A );
break;
case 'prefixes':
$data['prefixes'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_prefixes ORDER BY sort_order ASC", ARRAY_A );
break;
case 'interactions':
$data['likes'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_likes ORDER BY id ASC", ARRAY_A );
$data['reactions'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_reactions ORDER BY id ASC", ARRAY_A );
@@ -139,6 +151,9 @@ add_action( 'admin_init', function() {
case 'invites':
$data['invites'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_invites ORDER BY id ASC", ARRAY_A );
break;
case 'ignore_list':
$data['ignore_list'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_ignore_list ORDER BY id ASC", ARRAY_A );
break;
}
}
@@ -339,6 +354,7 @@ function wbf_admin_page() {
$online_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users WHERE last_active >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)");
$invite_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_invites WHERE use_count < max_uses AND (expires_at IS NULL OR expires_at > NOW())");
$banned_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users WHERE role='banned'");
$update_info = wbf_update_available(); // null oder array mit Versionsdaten
// ── System ────────────────────────────────────────────────────────────────
$php_ver = PHP_VERSION;
@@ -537,6 +553,7 @@ function wbf_admin_page() {
['wbf-trash', 'fas fa-trash-can', 'Papierkorb', $deleted_count>0?$deleted_count:0, true],
['wbf-export', 'fas fa-database', 'Export / Import'],
['wbf-settings', 'fas fa-gear', 'Einstellungen'],
['wbf-updates', 'fas fa-arrow-up-from-bracket', '🔔 Updates', $update_info ? 1 : 0],
];
foreach ($nav as $n):
if ($n===null) { echo '<div class="wbf-nav__sep"></div>'; continue; }
@@ -1201,6 +1218,65 @@ function wbf_admin_members() {
}
}
// ── Nutzer löschen (DSGVO Art. 17 / Hard Delete) ─────────────────────────
if ( isset( $_POST['wbf_delete_member'] ) && check_admin_referer( 'wbf_delete_member_nonce' ) ) {
$uid = (int) ( $_POST['user_id'] ?? 0 );
$mode = sanitize_key( $_POST['delete_mode'] ?? 'anonymize' );
if ( $uid ) {
$target = WBF_DB::get_user( $uid );
if ( ! $target ) {
echo '<div class="notice notice-error is-dismissible"><p>&#10060; Nutzer nicht gefunden.</p></div>';
} elseif ( $target->role === WBF_Roles::SUPERADMIN ) {
echo '<div class="notice notice-error is-dismissible"><p>&#10060; Der Superadmin kann nicht gelöscht werden.</p></div>';
} else {
global $wpdb;
$name = esc_html( $target->display_name );
if ( $mode === 'hard' ) {
// Alle nutzerbezogenen Daten entfernen
$dep_tables = [
'forum_messages' => [ 'from_id', 'to_id' ],
'forum_notifications' => [ 'user_id', 'actor_id' ],
'forum_subscriptions' => [ 'user_id' ],
'forum_bookmarks' => [ 'user_id' ],
'forum_likes' => [ 'user_id' ],
'forum_reactions' => [ 'user_id' ],
'forum_reports' => [ 'reporter_id' ],
'forum_poll_votes' => [ 'user_id' ],
'forum_remember_tokens' => [ 'user_id' ],
'forum_user_meta' => [ 'user_id' ],
'forum_ignore_list' => [ 'user_id', 'ignored_id' ],
];
foreach ( $dep_tables as $tbl => $cols ) {
$full = $wpdb->prefix . $tbl;
if ( $wpdb->get_var( "SHOW TABLES LIKE '$full'" ) !== $full ) continue;
foreach ( $cols as $col ) {
$wpdb->delete( $full, [ $col => $uid ], [ '%d' ] );
}
}
// Threads/Posts auf user_id=0 setzen (Inhalt bleibt)
$wpdb->update( "{$wpdb->prefix}forum_threads", [ 'user_id' => 0 ], [ 'user_id' => $uid ], [ '%d' ], [ '%d' ] );
$wpdb->update( "{$wpdb->prefix}forum_posts", [ 'user_id' => 0 ], [ 'user_id' => $uid ], [ '%d' ], [ '%d' ] );
// Nutzer-Datensatz hart loeschen
$wpdb->delete( "{$wpdb->prefix}forum_users", [ 'id' => $uid ], [ '%d' ] );
echo "<div class='notice notice-success is-dismissible'><p>&#128465; Nutzer <strong>{$name}</strong> dauerhaft geloescht. Threads/Posts anonym gesetzt.</p></div>";
} else {
// DSGVO Art. 17 Anonymisierung (Standard)
$ok = WBF_DB::delete_user_gdpr( $uid );
if ( $ok ) {
echo "<div class='notice notice-success is-dismissible'><p>&#9989; Nutzer <strong>{$name}</strong> anonymisiert (DSGVO Art. 17). Threads und Beitraege bleiben erhalten.</p></div>";
} else {
echo "<div class='notice notice-error is-dismissible'><p>&#10060; Anonymisierung fehlgeschlagen.</p></div>";
}
}
delete_transient( 'wbf_flood_' . $uid );
delete_transient( 'wbf_flood_ts_' . $uid );
}
}
}
// ── Profil bearbeiten ─────────────────────────────────────────────────────
if ( isset( $_POST['wbf_edit_user'] ) && check_admin_referer( 'wbf_edit_user_nonce' ) ) {
$uid = (int) $_POST['user_id'];
@@ -1238,6 +1314,35 @@ function wbf_admin_members() {
}
}
// ── Admin: einzelnen Ignore-Eintrag entfernen ─────────────────────────────
if ( isset( $_POST['wbf_admin_remove_ignore'] ) && check_admin_referer( 'wbf_admin_ignore_nonce' ) ) {
if ( current_user_can('manage_options') ) {
$uid = (int) ( $_POST['user_id'] ?? 0 );
$ignored_id = (int) ( $_POST['ignored_id'] ?? 0 );
if ( $uid && $ignored_id ) {
global $wpdb;
$wpdb->delete(
"{$wpdb->prefix}forum_ignore_list",
[ 'user_id' => $uid, 'ignored_id' => $ignored_id ],
[ '%d', '%d' ]
);
echo '<div class="notice notice-success is-dismissible"><p>Ignore-Eintrag entfernt.</p></div>';
}
}
}
// ── Admin: gesamte Ignore-Liste eines Users leeren ────────────────────────
if ( isset( $_POST['wbf_admin_clear_ignores'] ) && check_admin_referer( 'wbf_admin_ignore_nonce' ) ) {
if ( current_user_can('manage_options') ) {
$uid = (int) ( $_POST['user_id'] ?? 0 );
if ( $uid ) {
global $wpdb;
$wpdb->delete( "{$wpdb->prefix}forum_ignore_list", [ 'user_id' => $uid ], [ '%d' ] );
echo '<div class="notice notice-success is-dismissible"><p>Ignore-Liste vollständig geleert.</p></div>';
}
}
}
$members = WBF_DB::get_all_users( 200 );
?>
<div class="wrap">
@@ -1350,6 +1455,11 @@ function wbf_admin_members() {
onclick="var d=document.getElementById('wbf-edit-user-<?php echo (int)$m->id; ?>');d.style.display=d.style.display==='none'?'block':'none'">
Profil bearbeiten
</button>
<button type="button" class="button button-small"
style="color:#dc2626;border-color:#dc2626"
onclick="var d=document.getElementById('wbf-delete-user-<?php echo (int)$m->id; ?>');d.style.display=d.style.display==='none'?'block':'none'">
🗑️ Löschen
</button>
</div>
<div class="wbf-ban-reason" style="display:<?php echo $m->role === 'banned' ? 'block' : 'none'; ?>;margin-top:3px">
<input type="text" name="ban_reason" value="<?php echo $ban_reason; ?>"
@@ -1472,6 +1582,128 @@ function wbf_admin_members() {
<button type="submit" name="wbf_edit_user" class="button button-primary button-small">Änderungen speichern</button>
</div>
</form>
<!-- Ignore-Liste des Nutzers -->
<?php
$admin_ignore_list = WBF_DB::get_ignore_list( $m->id );
?>
<div style="margin-top:14px;border-top:1px solid #e0e0e0;padding-top:12px">
<strong style="font-size:12px;color:#555;text-transform:uppercase;letter-spacing:.04em">
<span class="dashicons dashicons-hidden" style="font-size:14px;width:14px;height:14px;vertical-align:-2px"></span>
Ignorierte Nutzer (<?php echo count($admin_ignore_list); ?>)
</strong>
<?php if ( empty($admin_ignore_list) ): ?>
<p style="font-size:12px;color:#999;margin:6px 0 0">Dieser Nutzer ignoriert niemanden.</p>
<?php else: ?>
<table style="width:100%;font-size:12px;margin-top:8px;border-collapse:collapse">
<thead>
<tr style="background:#f0f0f0">
<th style="padding:4px 6px;text-align:left;font-weight:600">Nutzer</th>
<th style="padding:4px 6px;text-align:left;font-weight:600">Ignoriert seit</th>
<th style="padding:4px 6px;text-align:center;font-weight:600">Entfernen</th>
</tr>
</thead>
<tbody>
<?php foreach ( $admin_ignore_list as $ign ): ?>
<tr style="border-bottom:1px solid #eee">
<td style="padding:4px 6px">
<strong><?php echo esc_html($ign->display_name); ?></strong>
<span style="color:#999"> @<?php echo esc_html($ign->username); ?></span>
</td>
<td style="padding:4px 6px;color:#666">
<?php echo esc_html(date_i18n('d.m.Y H:i', strtotime($ign->ignored_since))); ?>
</td>
<td style="padding:4px 6px;text-align:center">
<form method="post" style="display:inline">
<?php wp_nonce_field('wbf_admin_ignore_nonce'); ?>
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
<input type="hidden" name="ignored_id" value="<?php echo (int)$ign->id; ?>">
<button type="submit" name="wbf_admin_remove_ignore"
class="button button-small"
style="color:#c00;border-color:#c00"
onclick="return confirm('Ignore-Eintrag für <?php echo esc_js($ign->display_name); ?> entfernen?')">
<span class="dashicons dashicons-no" style="font-size:13px;width:13px;height:13px;vertical-align:-2px"></span>
Entfernen
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<form method="post" style="margin-top:8px">
<?php wp_nonce_field('wbf_admin_ignore_nonce'); ?>
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
<button type="submit" name="wbf_admin_clear_ignores"
class="button button-small"
style="color:#c00;border-color:#c00"
onclick="return confirm('Gesamte Ignore-Liste von <?php echo esc_js($m->display_name); ?> leeren?')">
<span class="dashicons dashicons-trash" style="font-size:13px;width:13px;height:13px;vertical-align:-2px"></span>
Alle Einträge löschen
</button>
</form>
<?php endif; ?>
</div>
</div><!-- /#wbf-edit-user -->
<!-- Löschen-Dialog -->
<div id="wbf-delete-user-<?php echo (int)$m->id; ?>"
style="display:none;margin-top:8px;padding:14px 16px;background:#fff5f5;border:1px solid #fca5a5;border-radius:6px;max-width:480px">
<p style="margin:0 0 10px;font-weight:700;color:#dc2626;font-size:.9rem">
🗑️ Nutzer löschen: <?php echo esc_html($m->display_name); ?>
</p>
<p style="font-size:.82rem;color:#6b7280;margin:0 0 12px">
Wähle wie der Account behandelt werden soll:
</p>
<!-- Option A: DSGVO Anonymisierung -->
<form method="post" style="margin-bottom:8px">
<?php wp_nonce_field( 'wbf_delete_member_nonce' ); ?>
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
<input type="hidden" name="delete_mode" value="anonymize">
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:10px 12px;margin-bottom:6px">
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
<span>
<strong style="font-size:.83rem;color:#374151">📋 DSGVO Anonymisieren</strong>
<span style="display:block;font-size:.77rem;color:#9ca3af;margin-top:2px">
Account wird anonymisiert, Threads &amp; Beiträge bleiben unter „Gelöschter Nutzer" erhalten.
</span>
</span>
</label>
</div>
<button type="submit" name="wbf_delete_member" class="button"
style="font-size:.8rem;height:28px;line-height:28px;background:#fff8f0;border-color:#f97316;color:#c2410c"
onclick="return confirm('Nutzer <?php echo esc_js($m->display_name); ?> anonymisieren?\n\nDer Account wird nach DSGVO Art. 17 anonymisiert. Alle Inhalte bleiben anonym erhalten.')">
📋 Anonymisieren
</button>
</form>
<!-- Option B: Hard Delete -->
<form method="post">
<?php wp_nonce_field( 'wbf_delete_member_nonce' ); ?>
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
<input type="hidden" name="delete_mode" value="hard">
<div style="background:#fff;border:1px solid #fca5a5;border-radius:6px;padding:10px 12px;margin-bottom:6px">
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
<span>
<strong style="font-size:.83rem;color:#dc2626">⚠️ Dauerhaft löschen</strong>
<span style="display:block;font-size:.77rem;color:#9ca3af;margin-top:2px">
Account + alle nutzerbezogenen Daten werden unwiderruflich gelöscht. Threads/Posts bleiben anonym erhalten.
</span>
</span>
</label>
</div>
<button type="submit" name="wbf_delete_member" class="button"
style="font-size:.8rem;height:28px;line-height:28px;background:#fef2f2;border-color:#dc2626;color:#dc2626"
onclick="return confirm('⚠️ ACHTUNG: Nutzer <?php echo esc_js($m->display_name); ?> DAUERHAFT löschen?\n\nDieser Vorgang kann NICHT rückgängig gemacht werden!\n\nAlle persönlichen Daten, Nachrichten und Einstellungen werden unwiderruflich entfernt.')">
🗑️ Dauerhaft löschen
</button>
</form>
<button type="button" class="button" style="margin-top:8px;font-size:.78rem"
onclick="document.getElementById('wbf-delete-user-<?php echo (int)$m->id; ?>').style.display='none'">
Abbrechen
</button>
</div>
<?php endif; ?>
</td>
@@ -2353,6 +2585,11 @@ function wbf_admin_export() {
update_option('wbf_profile_fields', $data['profile_fields']);
$imported[] = 'Profilfelder-Definitionen (' . count($data['profile_fields']) . ')';
}
// Reaktionen-Konfiguration
if ( isset($data['reactions_cfg']) && is_array($data['reactions_cfg']) ) {
update_option('wbf_reactions', $data['reactions_cfg']);
$imported[] = 'Reaktionen-Konfiguration';
}
// ── Rollen ────────────────────────────────────────────────
if ( ! empty($data['roles']) ) {
@@ -2378,8 +2615,21 @@ function wbf_admin_export() {
if ( $existing === 0 || $force ) {
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_categories");
foreach ($data['categories'] as $cat) {
unset($cat['id']);
$wpdb->insert("{$wpdb->prefix}forum_categories", $cat);
// BUGFIX: Original-ID behalten! Threads zeigen direkt auf diese IDs.
$wpdb->query( $wpdb->prepare(
"INSERT INTO {$wpdb->prefix}forum_categories
(id, parent_id, name, slug, description, icon, sort_order, thread_count, post_count, min_role, guest_visible)
VALUES (%d,%d,%s,%s,%s,%s,%d,%d,%d,%s,%d)
ON DUPLICATE KEY UPDATE parent_id=%d, name=%s, slug=%s, description=%s, icon=%s, sort_order=%d, thread_count=%d, post_count=%d, min_role=%s, guest_visible=%d",
(int)($cat['id']), (int)($cat['parent_id'] ?? 0), $cat['name'] ?? '', $cat['slug'] ?? '',
$cat['description'] ?? '', $cat['icon'] ?? 'fas fa-comments',
(int)($cat['sort_order'] ?? 0), (int)($cat['thread_count'] ?? 0),
(int)($cat['post_count'] ?? 0), $cat['min_role'] ?? 'member', (int)($cat['guest_visible'] ?? 1),
(int)($cat['parent_id'] ?? 0), $cat['name'] ?? '', $cat['slug'] ?? '',
$cat['description'] ?? '', $cat['icon'] ?? 'fas fa-comments',
(int)($cat['sort_order'] ?? 0), (int)($cat['thread_count'] ?? 0),
(int)($cat['post_count'] ?? 0), $cat['min_role'] ?? 'member', (int)($cat['guest_visible'] ?? 1)
) );
}
$imported[] = 'Kategorien (' . count($data['categories']) . ')';
} else {
@@ -2439,11 +2689,17 @@ function wbf_admin_export() {
if ( ! empty($data['threads']) ) {
$force = ! empty($_POST['import_force_threads']);
if ($force) {
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_posts");
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_threads");
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_thread_tags");
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_tags");
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_subscriptions");
// Alle abhängigen Tabellen mitbereinigen (verhindert verwaiste Einträge)
$wpdb->query("SET FOREIGN_KEY_CHECKS=0");
foreach ([
'forum_posts', 'forum_threads', 'forum_thread_tags', 'forum_tags',
'forum_subscriptions', 'forum_polls', 'forum_poll_votes',
'forum_bookmarks', 'forum_prefixes', 'forum_likes', 'forum_reactions',
'forum_notifications', 'forum_ignore_list',
] as $_tbl) {
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}$_tbl");
}
$wpdb->query("SET FOREIGN_KEY_CHECKS=1");
}
foreach ($data['threads'] ?? [] as $t) { $wpdb->insert("{$wpdb->prefix}forum_threads", $t); }
foreach ($data['posts'] ?? [] as $p) { $wpdb->insert("{$wpdb->prefix}forum_posts", $p); }
@@ -2478,6 +2734,61 @@ function wbf_admin_export() {
$imported[] = 'Threads + Posts (' . count($data['threads']) . ' / ' . count($data['posts'] ?? []) . ')';
}
// ── Thread-Präfixe ────────────────────────────────────────
// ── Thread-Präfixe ────────────────────────────────────────
if ( ! empty($data['prefixes']) ) {
$force = ! empty($_POST['import_force_prefixes']);
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_prefixes");
$pc = 0;
foreach ($data['prefixes'] as $row) {
$wpdb->query( $wpdb->prepare(
"INSERT INTO {$wpdb->prefix}forum_prefixes (id, label, color, bg_color, sort_order)
VALUES (%d,%s,%s,%s,%d)
ON DUPLICATE KEY UPDATE label=%s, color=%s, bg_color=%s, sort_order=%d",
(int)$row['id'], $row['label'], $row['color'] ?? '#ffffff',
$row['bg_color'] ?? '#475569', (int)($row['sort_order'] ?? 0),
$row['label'], $row['color'] ?? '#ffffff',
$row['bg_color'] ?? '#475569', (int)($row['sort_order'] ?? 0)
) );
$pc++;
}
if ($pc) $imported[] = "Thread-Präfixe ($pc)";
}
// ── Umfragen + Abstimmungen ───────────────────────────────
if ( ! empty($data['polls']) ) {
$force = ! empty($_POST['import_force_polls']);
if ($force) {
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_poll_votes");
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_polls");
}
$pc = 0;
foreach ($data['polls'] as $row) {
$wpdb->query( $wpdb->prepare(
"INSERT INTO {$wpdb->prefix}forum_polls (id, thread_id, question, options, multi, ends_at, created_at)
VALUES (%d,%d,%s,%s,%d,%s,%s)
ON DUPLICATE KEY UPDATE thread_id=%d, question=%s, options=%s, multi=%d, ends_at=%s",
(int)$row['id'], (int)$row['thread_id'], $row['question'] ?? '',
$row['options'] ?? '[]', (int)($row['multi'] ?? 0),
$row['ends_at'] ?? null, $row['created_at'] ?? current_time('mysql'),
(int)$row['thread_id'], $row['question'] ?? '',
$row['options'] ?? '[]', (int)($row['multi'] ?? 0), $row['ends_at'] ?? null
) );
$pc++;
}
foreach ($data['poll_votes'] ?? [] as $row) { unset($row['id']); $wpdb->replace("{$wpdb->prefix}forum_poll_votes", $row); }
$imported[] = "Umfragen ($pc)";
}
// ── Lesezeichen ───────────────────────────────────────────
if ( ! empty($data['bookmarks']) ) {
$force = ! empty($_POST['import_force_bookmarks']);
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_bookmarks");
$bc = 0;
foreach ($data['bookmarks'] as $row) { unset($row['id']); $wpdb->replace("{$wpdb->prefix}forum_bookmarks", $row); $bc++; }
if ($bc) $imported[] = "Lesezeichen ($bc)";
}
// ── Likes, Reaktionen, Benachrichtigungen ─────────────────
if ( ! empty($data['likes']) ) {
$force = ! empty($_POST['import_force_threads']);
@@ -2539,6 +2850,19 @@ function wbf_admin_export() {
if ($count) $imported[] = "Einladungen ($count)";
}
// ── Ignore-Liste ──────────────────────────────────────────
if ( ! empty($data['ignore_list']) ) {
$force = ! empty($_POST['import_force_ignore']);
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_ignore_list");
$count = 0;
foreach ($data['ignore_list'] as $row) {
unset($row['id']);
$wpdb->replace("{$wpdb->prefix}forum_ignore_list", $row);
$count++;
}
if ($count) $imported[] = "Ignore-Einträge ($count)";
}
if ( empty($imported) ) {
$notice = ['warning', 'Nichts importiert — Datei enthielt keine gültigen Abschnitte.'];
} else {
@@ -2581,8 +2905,12 @@ function wbf_admin_export() {
'categories' => ['📂', 'Kategorien', 'Alle Kategorien inkl. Hierarchie'],
'users' => ['👥', 'Benutzer & Profilfelder', 'Accounts, Ban-Status, Profilfeld-Werte'],
'threads' => ['💬', 'Threads, Posts & Abos', 'Alle Inhalte, Tags & Abonnements'],
'polls' => ['📊', 'Umfragen', 'Alle Umfragen inkl. Abstimmungen'],
'bookmarks' => ['🔖', 'Lesezeichen', 'Alle gespeicherten Thread-Lesezeichen'],
'prefixes' => ['🏷️', 'Thread-Präfixe', 'Alle konfigurierten Präfix-Labels & Farben'],
'interactions' => ['❤️', 'Likes & Reaktionen', 'Likes, Reaktionen, Benachrichtigungen'],
'messages' => ['✉️', 'Privatnachrichten', 'Alle DM-Konversationen'],
'ignore_list' => ['🚫', 'Ignore-Liste', 'Alle Nutzer-Blockierungen'],
'reports' => ['🚩', 'Meldungen', 'Gemeldete Beiträge inkl. Status'],
'invites' => ['📨', 'Einladungen', 'Alle Einladungscodes inkl. Nutzungsstand'],
];
@@ -2644,6 +2972,10 @@ function wbf_admin_export() {
'import_force_cats' => 'Kategorien überschreiben (löscht bestehende)',
'import_force_users' => 'Benutzer aktualisieren (bei gleichem Username) inkl. Profilfeld-Werte',
'import_force_threads' => 'Threads, Posts, Likes, Reaktionen & Abonnements überschreiben',
'import_force_polls' => 'Umfragen & Abstimmungen überschreiben',
'import_force_bookmarks'=> 'Lesezeichen überschreiben',
'import_force_prefixes' => 'Thread-Präfixe überschreiben',
'import_force_ignore' => 'Ignore-Liste überschreiben',
'import_force_messages' => 'Privatnachrichten überschreiben',
'import_force_reports' => 'Meldungen überschreiben',
'import_force_invites' => 'Einladungen überschreiben',
@@ -2868,6 +3200,142 @@ function wbf_admin_profile_fields() {
}
// ── Deinstallations-Seite ─────────────────────────────────────────────────────
// ── Update-Seite ──────────────────────────────────────────────────────────────
function wbf_admin_updates() {
if ( ! current_user_can('manage_options') ) return;
$latest = wbf_get_latest_release();
$update = wbf_update_available();
$cur_ver = WBF_VERSION;
$admin_url = admin_url('admin.php?page=wbf-updates');
// Manuelles Refresh
$refresh_url = wp_nonce_url( add_query_arg('wbf_refresh_update', '1', $admin_url), 'wbf_refresh_update' );
?>
<div class="wrap" style="max-width:860px">
<h1 style="display:flex;align-items:center;gap:10px">
<span style="font-size:1.4rem">🔔</span> WP Business Forum — Updates
</h1>
<!-- ── Status-Karte ─────────────────────────────────── -->
<div style="margin-top:20px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.06)">
<?php if ( $update ): ?>
<!-- UPDATE VERFÜGBAR -->
<div style="background:linear-gradient(135deg,#fffbeb,#fef3c7);border-bottom:2px solid #fcd34d;padding:18px 24px;display:flex;align-items:center;gap:14px">
<span style="font-size:2.2rem">📦</span>
<div>
<strong style="font-size:1.1rem;color:#92400e">Update verfügbar!</strong>
<p style="margin:.2rem 0 0;color:#78350f">
Version <strong><?php echo esc_html($update['version']); ?></strong> wurde veröffentlicht.
Du verwendest aktuell <strong><?php echo esc_html($cur_ver); ?></strong>.
</p>
</div>
<a href="<?php echo esc_url($update['url']); ?>" target="_blank" rel="noopener"
class="button button-primary"
style="margin-left:auto;background:#f59e0b;border-color:#d97706;white-space:nowrap;font-size:.9rem;padding:6px 16px">
📥 Zum Download
</a>
</div>
<?php else: ?>
<!-- AKTUELL -->
<div style="background:linear-gradient(135deg,#f0fdf4,#dcfce7);border-bottom:2px solid #86efac;padding:18px 24px;display:flex;align-items:center;gap:14px">
<span style="font-size:2.2rem">✅</span>
<div>
<strong style="font-size:1.1rem;color:#166534">Du verwendest die neueste Version</strong>
<p style="margin:.2rem 0 0;color:#15803d">Version <?php echo esc_html($cur_ver); ?> ist aktuell.</p>
</div>
</div>
<?php endif; ?>
<div style="padding:22px 24px">
<!-- ── Versionen-Tabelle ──────────────────────── -->
<table style="width:100%;border-collapse:collapse;margin-bottom:20px">
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:10px 0;color:#6b7280;font-size:.875rem;width:220px">Installierte Version</td>
<td style="padding:10px 0;font-weight:700"><?php echo esc_html($cur_ver); ?></td>
</tr>
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:10px 0;color:#6b7280;font-size:.875rem">Neueste Version</td>
<td style="padding:10px 0;font-weight:700">
<?php if ($latest && !empty($latest['version'])): ?>
<?php echo esc_html($latest['version']); ?>
<?php if ($update): ?>
<span style="margin-left:6px;background:#fef3c7;color:#92400e;border:1px solid #fcd34d;border-radius:12px;padding:2px 8px;font-size:.75rem;font-weight:600">NEU</span>
<?php endif; ?>
<?php else: ?>
<span style="color:#9ca3af">Nicht abrufbar</span>
<?php endif; ?>
</td>
</tr>
<?php if ($latest && !empty($latest['published'])): ?>
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:10px 0;color:#6b7280;font-size:.875rem">Veröffentlicht am</td>
<td style="padding:10px 0"><?php echo esc_html(date_i18n('d.m.Y H:i', strtotime($latest['published']))); ?> Uhr</td>
</tr>
<?php endif; ?>
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:10px 0;color:#6b7280;font-size:.875rem">Update-Quelle</td>
<td style="padding:10px 0">
<a href="<?php echo esc_url(WBF_RELEASES_PAGE); ?>" target="_blank" rel="noopener"
style="color:#2563eb;text-decoration:none">
<i class="fas fa-code-branch" style="margin-right:5px"></i>
git.viper.ipv64.net — WP-Business-Forum
</a>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6b7280;font-size:.875rem">Letzter Check</td>
<td style="padding:10px 0;display:flex;align-items:center;gap:12px">
<?php
$cached = get_transient(WBF_UPDATE_TRANSIENT);
echo ($cached !== false) ? '<span style="color:#059669">✔ Cache aktiv (12h)</span>' : '<span style="color:#9ca3af">—</span>';
?>
<a href="<?php echo esc_url($refresh_url); ?>"
style="color:#6b7280;font-size:.8rem;text-decoration:none;border:1px solid #e5e7eb;border-radius:5px;padding:3px 9px">
🔄 Jetzt prüfen
</a>
</td>
</tr>
</table>
<?php if ($latest && !empty($latest['body'])): ?>
<!-- ── Changelog ──────────────────────────────── -->
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:16px 18px">
<h3 style="margin:0 0 10px;font-size:.9rem;color:#374151;display:flex;align-items:center;gap:7px">
<span>📋</span> Release Notes — v<?php echo esc_html($latest['version']); ?>
</h3>
<pre style="white-space:pre-wrap;font-family:inherit;font-size:.82rem;color:#4b5563;margin:0;line-height:1.6;max-height:300px;overflow-y:auto"><?php echo esc_html(mb_substr($latest['body'], 0, 3000)); ?></pre>
<div style="margin-top:12px;padding-top:10px;border-top:1px solid #e2e8f0">
<a href="<?php echo esc_url($latest['url']); ?>" target="_blank" rel="noopener"
class="button" style="font-size:.82rem">
Vollständige Release-Seite öffnen →
</a>
</div>
</div>
<?php endif; ?>
<!-- ── Update-Anleitung ───────────────────────── -->
<div style="margin-top:18px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:14px 16px">
<strong style="font-size:.85rem;color:#1e40af">📖 Update-Anleitung</strong>
<ol style="margin:.6rem 0 0 1.2rem;padding:0;font-size:.82rem;color:#1e3a8a;line-height:1.8">
<li>Lade die neue <code>.zip</code> von der <a href="<?php echo esc_url(WBF_RELEASES_PAGE); ?>" target="_blank" rel="noopener" style="color:#2563eb">Release-Seite</a> herunter.</li>
<li>Erstelle vorher ein Backup über <a href="<?php echo esc_url(admin_url('admin.php?page=wbf-export')); ?>" style="color:#2563eb">Export / Import</a>.</li>
<li>Lade die neue Version per FTP/SFTP hoch und überschreibe die alten Dateien — oder nutze den WP-Plugins-Bereich (Plugin deaktivieren → löschen → neu hochladen).</li>
<li>Aktiviere das Plugin. Das DB-Schema wird automatisch aktualisiert.</li>
</ol>
</div>
</div>
</div>
</div>
<?php
}
// ── Deinstallations-Seite ─────────────────────────────────────────────────────
function wbf_admin_uninstall() {
if ( ! current_user_can('manage_options') ) return;

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,18 @@ function wbf_admin_settings() {
// rules_content separat (nicht in $fields, da textarea mit eigener Behandlung)
$settings['rules_content'] = sanitize_textarea_field( $_POST['rules_content'] ?? '' );
// 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 +447,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);
@@ -2832,3 +2834,285 @@ select.wbf-cf-input option { background: var(--c-surface2); color: var(--c-text)
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;
}

View File

@@ -1944,6 +1944,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 +2008,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)]);
@@ -411,9 +414,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 +499,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';
@@ -588,6 +639,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 +745,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 +807,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 +893,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 +980,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 +1016,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'] ?? '';
@@ -1207,6 +1307,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

@@ -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" );
@@ -623,7 +635,8 @@ 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
));
}
@@ -1207,13 +1220,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
}
@@ -1689,6 +1707,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 +1851,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;
$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

@@ -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)',

View File

@@ -341,9 +341,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 +500,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 +731,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 +791,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 +860,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>
@@ -871,13 +904,22 @@ class WBF_Shortcodes {
<?php return ob_get_clean();
}
$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_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
$active_tab = (int)($_GET['ptab'] ?? ($is_own ? 1 : 2));
$active_tab = in_array($active_tab, [1,2,3]) ? $active_tab : ($is_own ? 1 : 2);
// Tab 1 + 3 nur für eigenes Profil
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,8 +927,6 @@ 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); ?>"
@@ -898,15 +938,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,39 +966,26 @@ 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) -->
<?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 ):
<!-- Öffentliche Custom Fields -->
<?php foreach ($cf_defs as $def):
if (!$is_own && empty($def['public'])) continue;
$val = trim( $cf_vals_pub[ $def['key'] ] ?? '' );
if ( $val === '' ) continue;
?>
$val = trim($cf_vals[$def['key']] ?? '');
if ($val === '') 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>
@@ -967,15 +1003,57 @@ class WBF_Shortcodes {
<?php endif; ?>
</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>
</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">
@@ -995,7 +1073,10 @@ 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">
<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-form-row" style="display:flex;align-items:center;gap:.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"
@@ -1005,9 +1086,6 @@ class WBF_Shortcodes {
<?php echo $pub?'Öffentlich':'Privat'; ?>
</button>
</div>
<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
@@ -1017,12 +1095,36 @@ class WBF_Shortcodes {
</div>
</div>
<!-- ── Benutzerdefinierte Profilfelder ──────────────── -->
<?php
$cf_defs = WBF_DB::get_profile_field_defs();
$cf_vals = WBF_DB::get_user_meta( $profile->id );
if ( ! empty( $cf_defs ) ):
?>
<!-- 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>
<!-- Weitere Profilangaben -->
<?php if (!empty($cf_defs)): ?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-sliders"></i> Weitere Profilangaben
@@ -1073,46 +1175,38 @@ class WBF_Shortcodes {
</div>
<?php 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>
</div>
<?php endif; /* end Tab 1 */ ?>
<?php endif; /* end $is_own */ ?>
<!-- ══════════════════════════════════════════════════
TAB 2 — Lesezeichen + Beiträge
══════════════════════════════════════════════════ -->
<?php if ($active_tab === 2): ?>
<!-- Lesezeichen (nur eigenes Profil) -->
<?php if ($is_own): ?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-bookmark"></i> Lesezeichen
<span class="wbf-profile-card__count"><?php echo count($bookmarks); ?></span>
</div>
<div class="wbf-profile-card__body wbf-profile-card__body--posts">
<?php if (empty($bookmarks)): ?>
<p class="wbf-profile-empty">Noch keine Lesezeichen.</p>
<?php else: foreach ($bookmarks as $bm): ?>
<div class="wbf-profile-post-item">
<div class="wbf-profile-post-item__top">
<?php echo self::render_prefix($bm); ?>
<a href="?forum_thread=<?php echo (int)$bm->id; ?>" class="wbf-profile-post-item__title">
<?php echo esc_html(mb_substr($bm->title,0,60)); ?>
</a>
<span class="wbf-profile-post-item__cat"><i class="fas fa-folder"></i> <?php echo esc_html($bm->cat_name); ?></span>
<span class="wbf-profile-post-item__time"><i class="fas fa-bookmark" style="font-size:.65rem"></i> <?php echo self::time_ago($bm->bookmarked_at); ?></span>
</div>
</div>
<?php endforeach; endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Beiträge -->
<div class="wbf-profile-card">
@@ -1159,32 +1253,146 @@ class WBF_Shortcodes {
</div>
</div>
<!-- Lesezeichen (nur eigenes Profil) -->
<?php if ($is_own):
$bookmarks = WBF_DB::get_user_bookmarks($current->id, 50); ?>
<?php endif; /* end Tab 2 */ ?>
<!-- ══════════════════════════════════════════════════
TAB 3 — Ignorierte Nutzer + Datenschutz
══════════════════════════════════════════════════ -->
<?php if ($is_own && $active_tab === 3): ?>
<!-- Ignorierte Nutzer -->
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-bookmark"></i> Lesezeichen
<span class="wbf-profile-card__count"><?php echo count($bookmarks); ?></span>
<i class="fas fa-bell"></i> E-Mail-Benachrichtigungen
</div>
<div class="wbf-profile-card__body wbf-profile-card__body--posts">
<?php if (empty($bookmarks)): ?>
<p class="wbf-profile-empty">Noch keine Lesezeichen.</p>
<?php else: foreach ($bookmarks as $bm): ?>
<div class="wbf-profile-post-item">
<div class="wbf-profile-post-item__top">
<?php echo self::render_prefix($bm); ?>
<a href="?forum_thread=<?php echo (int)$bm->id; ?>" class="wbf-profile-post-item__title">
<?php echo esc_html(mb_substr($bm->title,0,60)); ?>
<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>
<span class="wbf-profile-post-item__cat"><i class="fas fa-folder"></i> <?php echo esc_html($bm->cat_name); ?></span>
<span class="wbf-profile-post-item__time"><i class="fas fa-bookmark" style="font-size:.65rem"></i> <?php echo self::time_ago($bm->bookmarked_at); ?></span>
<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; endif; ?>
</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 */ ?>
</div><!-- /.wbf-profile-main -->
</div><!-- /.wbf-profile-layout -->
@@ -1193,6 +1401,7 @@ class WBF_Shortcodes {
<?php return ob_get_clean();
}
// ── TAG PAGE ─────────────────────────────────────────────────────────────
private static function view_tag() {
@@ -1274,8 +1483,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 +1564,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>
@@ -1443,8 +1652,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();
@@ -2080,8 +2289,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 +2329,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

@@ -20,9 +20,12 @@ $tables = [
'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',
@@ -49,6 +52,7 @@ $options = [
'wbf_forum_page_id',
'wbf_superadmin_email',
'wbf_db_version',
'wbf_word_filter',
];
foreach ( $options as $option ) {
@@ -65,8 +69,9 @@ if ( is_multisite() ) {
// ── 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
// Alle wbf_* Transients per LIKE-Query entfernen (inkl. Update-Dismissed-Transients)
$wpdb->query(
"DELETE FROM `{$wpdb->options}`
WHERE `option_name` LIKE '_transient_wbf_%'
@@ -75,6 +80,7 @@ $wpdb->query(
// ── 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' );

View File

@@ -3,7 +3,7 @@
* Plugin Name: WP Business Forum
* Plugin URI: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum
* Description: Professionelles Forum mit eigenem Login, Rollen, Signaturen, Hierarchie und Moderations-Tools.
* Version: 1.0.1
* Version: 1.0.2
* Author: M_Viper
* Author URI: https://m-viper.de
* Text Domain: wp-business-forum
@@ -13,7 +13,7 @@ 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 +22,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 +34,11 @@ register_activation_hook( __FILE__, function() {
set_transient( 'wbf_activation_redirect', true, 30 );
});
// ── Export / Import Hooks ─────────────────────────────────────────────────────
add_action( 'plugins_loaded', function() {
WBF_Export::hooks();
}, 5 );
// ── Superadmin-Sync ───────────────────────────────────────────────────────────
add_action( 'wp_login', function() { WBF_Roles::sync_superadmin(); } );
add_action( 'init', function() { WBF_Roles::sync_superadmin(); } );
@@ -57,6 +63,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' );
} );
@@ -99,3 +106,155 @@ add_action( 'wp_enqueue_scripts', function() {
'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;
} );