Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43fcc6cb95 | |||
| 1c229ab72b | |||
| 781dbf9f41 | |||
| 3b7fd16301 | |||
| 3ea89e9841 | |||
| 65d2371239 | |||
| ead2f3a62a | |||
| dfdc74bcf9 | |||
| 44672a61aa | |||
| 2adba16d29 | |||
| 290279df1c |
360
README.md
360
README.md
@@ -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!
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,8 @@ if ( ! function_exists('wbf_get_settings') ) {
|
||||
'rules_accept_required' => '1',
|
||||
'rules_title' => 'Forum-Regeln & Nutzungsbedingungen',
|
||||
'rules_content' => "**1. Respektvoller Umgang**\nBehandle alle Mitglieder freundlich und respektvoll. Beleidigungen, Mobbing und Diskriminierung sind nicht toleriert.\n\n**2. Keine Spam-Inhalte**\nWerbung, Spam und irrelevante Links sind verboten.\n\n**3. Keine illegalen Inhalte**\nJegliche Inhalte, die gegen geltendes Recht verstoßen, sind streng verboten.\n\n**4. Themenrelevanz**\nBeiträge sollten zur jeweiligen Kategorie passen.\n\n**5. Urheberrecht**\nVeröffentliche keine Inhalte, an denen du keine Rechte besitzt.\n\n**6. Datenschutz**\nTeile keine persönlichen Daten anderer Personen ohne deren Zustimmung.\n\n**7. Moderations-Entscheidungen**\nEntscheidungen der Moderatoren sind zu respektieren. Bei Fragen wende dich direkt ans Team.\n\nVerstöße können zur Verwarnung oder dauerhaften Sperrung führen.",
|
||||
// Ignore/Block-System: Rollen die nicht geblockt werden können (kommagetrennte Schlüssel)
|
||||
'ignore_blocked_roles' => 'superadmin,admin,moderator',
|
||||
];
|
||||
|
||||
$saved = get_option( 'wbf_settings', [] );
|
||||
@@ -63,6 +65,38 @@ if ( ! function_exists('wbf_get_settings') ) {
|
||||
|
||||
// ── Admin-Seite ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt ein Array der Rollen-Keys zurück die nicht geblockt/ignoriert werden können.
|
||||
* Superadmin ist immer enthalten — unabhängig von der Einstellung.
|
||||
*
|
||||
* @return string[] z.B. ['superadmin', 'admin', 'moderator']
|
||||
*/
|
||||
if ( ! function_exists('wbf_get_ignore_blocked_roles') ) {
|
||||
function wbf_get_ignore_blocked_roles() {
|
||||
$raw = wbf_get_settings()['ignore_blocked_roles'] ?? 'superadmin,admin,moderator';
|
||||
$keys = array_filter( array_map( 'trim', explode( ',', $raw ) ) );
|
||||
// superadmin immer schützen
|
||||
if ( ! in_array('superadmin', $keys, true) ) {
|
||||
$keys[] = 'superadmin';
|
||||
}
|
||||
return array_values( $keys );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein User ignoriert/geblockt werden darf.
|
||||
*
|
||||
* @param object $target Forum-User-Objekt
|
||||
* @return bool true = darf ignoriert werden, false = nicht erlaubt
|
||||
*/
|
||||
if ( ! function_exists('wbf_can_be_ignored') ) {
|
||||
function wbf_can_be_ignored( $target ) {
|
||||
if ( ! $target ) return false;
|
||||
$blocked_roles = wbf_get_ignore_blocked_roles();
|
||||
return ! in_array( $target->role, $blocked_roles, true );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists('wbf_admin_settings') ) {
|
||||
function wbf_admin_settings() {
|
||||
|
||||
@@ -96,6 +130,25 @@ function wbf_admin_settings() {
|
||||
// rules_content separat (nicht in $fields, da textarea mit eigener Behandlung)
|
||||
$settings['rules_content'] = sanitize_textarea_field( $_POST['rules_content'] ?? '' );
|
||||
|
||||
// Checkbox-Felder explizit als '0' speichern wenn nicht angehakt,
|
||||
// damit array_filter(...,'strlen') sie nicht wegwirft und der Default '1' greift.
|
||||
$checkbox_fields = ['maintenance_mode', 'rules_enabled', 'rules_accept_required'];
|
||||
foreach ( $checkbox_fields as $cb ) {
|
||||
$settings[$cb] = isset($_POST[$cb]) && $_POST[$cb] === '1' ? '1' : '0';
|
||||
}
|
||||
|
||||
// ignore_blocked_roles: kommagetrennte Liste der gewählten Rollen-Keys
|
||||
$all_role_keys = array_keys( WBF_Roles::get_all() );
|
||||
$checked_roles = array_intersect(
|
||||
array_map( 'sanitize_key', (array)( $_POST['ignore_blocked_roles'] ?? [] ) ),
|
||||
$all_role_keys
|
||||
);
|
||||
// superadmin ist immer blockiert — kann nicht entfernt werden
|
||||
if ( ! in_array('superadmin', $checked_roles, true) ) {
|
||||
$checked_roles[] = 'superadmin';
|
||||
}
|
||||
$settings['ignore_blocked_roles'] = implode( ',', $checked_roles );
|
||||
|
||||
update_option( 'wbf_settings', $settings );
|
||||
echo '<div class="notice notice-success is-dismissible"><p>✅ Einstellungen gespeichert!</p></div>';
|
||||
}
|
||||
@@ -401,6 +454,51 @@ function wbf_admin_settings() {
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ── Ignore / Block-System ────────────────────────── -->
|
||||
<h2 style="border-bottom:1px solid #ddd;padding-bottom:.4rem;margin-top:1.5rem">
|
||||
🚫 Ignore / Block-System
|
||||
</h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><label>Nicht blockierbare Rollen</label></th>
|
||||
<td>
|
||||
<?php
|
||||
$blocked_roles = array_filter( array_map('trim', explode(',', $s['ignore_blocked_roles'] ?? 'superadmin,admin,moderator')) );
|
||||
$all_roles = WBF_Roles::get_sorted();
|
||||
foreach ( $all_roles as $key => $role ):
|
||||
$is_superadmin = ($key === 'superadmin');
|
||||
$is_checked = in_array($key, $blocked_roles, true);
|
||||
$rc = esc_attr($role['color']);
|
||||
$rb = esc_attr($role['bg_color']);
|
||||
?>
|
||||
<label style="display:flex;align-items:center;gap:8px;margin-bottom:7px;cursor:<?php echo $is_superadmin?'not-allowed':'pointer'; ?>">
|
||||
<input type="checkbox"
|
||||
name="ignore_blocked_roles[]"
|
||||
value="<?php echo esc_attr($key); ?>"
|
||||
<?php checked($is_checked, true); ?>
|
||||
<?php echo $is_superadmin ? 'disabled' : ''; ?>
|
||||
style="width:16px;height:16px;accent-color:<?php echo $rc; ?>">
|
||||
<?php if ($is_superadmin): ?>
|
||||
<!-- superadmin immer als hidden mitschicken da disabled nicht übermittelt wird -->
|
||||
<input type="hidden" name="ignore_blocked_roles[]" value="superadmin">
|
||||
<?php endif; ?>
|
||||
<span style="display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:20px;font-size:.78rem;font-weight:700;color:<?php echo $rc; ?>;background:<?php echo $rb; ?>;border:1px solid <?php echo $rc; ?>">
|
||||
<i class="<?php echo esc_attr($role['icon'] ?? 'fas fa-user'); ?>"></i>
|
||||
<?php echo esc_html($role['label']); ?>
|
||||
</span>
|
||||
<?php if ($is_superadmin): ?>
|
||||
<span style="font-size:.72rem;color:#999">(immer geschützt)</span>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<p class="description" style="margin-top:8px">
|
||||
Nutzer mit diesen Rollen können von anderen Mitgliedern <strong>nicht</strong> geblockt oder ignoriert werden.
|
||||
Superadmin ist permanent geschützt und kann nicht abgewählt werden.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(
|
||||
'💾 Einstellungen speichern',
|
||||
'primary',
|
||||
|
||||
@@ -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,887 @@ 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;
|
||||
}
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
PROFIL v3 — Sidebar-Links Layout (wbf-prof)
|
||||
════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.wbf-prof {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* ── SIDEBAR ─────────────────────────────────────────────────────── */
|
||||
.wbf-prof__sidebar {
|
||||
background: var(--c-surface);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.75rem 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: .75rem;
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
/* Avatar */
|
||||
.wbf-prof__av-wrap {
|
||||
position: relative;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
.wbf-prof__av-ring {
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(from 0deg, var(--c-primary), #6366f1, var(--c-primary));
|
||||
opacity: .6;
|
||||
animation: wbf-spin 8s linear infinite;
|
||||
}
|
||||
@keyframes wbf-spin { to { transform: rotate(360deg); } }
|
||||
.wbf-prof__av {
|
||||
position: relative;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--c-surface);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
background: var(--c-bg2);
|
||||
box-shadow: 0 0 28px rgba(0,180,216,.25);
|
||||
}
|
||||
.wbf-prof__av-online {
|
||||
position: absolute;
|
||||
bottom: 5px; right: 5px;
|
||||
width: 16px; height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
border: 3px solid var(--c-surface);
|
||||
box-shadow: 0 0 8px rgba(34,197,94,.7);
|
||||
}
|
||||
.wbf-prof__av-camera {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(0,0,0,.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity .18s;
|
||||
}
|
||||
.wbf-prof__av-wrap:hover .wbf-prof__av-camera { opacity: 1; }
|
||||
|
||||
/* Identity */
|
||||
.wbf-prof__sb-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: var(--c-text);
|
||||
letter-spacing: -.01em;
|
||||
}
|
||||
.wbf-prof__sb-role { margin-top: -.15rem; }
|
||||
.wbf-prof__sb-status {
|
||||
font-size: .75rem;
|
||||
color: var(--c-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
justify-content: center;
|
||||
}
|
||||
.wbf-prof__sb-status--online { color: #22c55e; font-weight: 600; }
|
||||
.wbf-prof__dot {
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 6px rgba(34,197,94,.8);
|
||||
animation: wbf-pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* Stats grid */
|
||||
.wbf-prof__sb-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3,1fr);
|
||||
gap: .5rem;
|
||||
width: 100%;
|
||||
margin: .25rem 0;
|
||||
border-top: 1px solid var(--c-border);
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
padding: .9rem 0;
|
||||
}
|
||||
.wbf-prof__sb-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: .2rem;
|
||||
}
|
||||
.wbf-prof__sb-stat span {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--c-text);
|
||||
line-height: 1;
|
||||
}
|
||||
.wbf-prof__sb-stat em {
|
||||
font-style: normal;
|
||||
font-size: .65rem;
|
||||
color: var(--c-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
}
|
||||
|
||||
/* Level progress */
|
||||
.wbf-prof__sb-level {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .35rem;
|
||||
}
|
||||
.wbf-prof__sb-level-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wbf-prof__sb-level-xp {
|
||||
color: var(--c-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
.wbf-prof__sb-level-bar {
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,.07);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wbf-prof__sb-level-bar > div {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width .6s ease;
|
||||
}
|
||||
.wbf-prof__sb-level-next {
|
||||
font-size: .72rem;
|
||||
color: var(--c-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Meta */
|
||||
.wbf-prof__sb-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .45rem;
|
||||
font-size: .78rem;
|
||||
color: var(--c-muted);
|
||||
}
|
||||
.wbf-prof__sb-meta i { color: var(--c-primary); }
|
||||
|
||||
/* Sidebar custom field blocks */
|
||||
.wbf-prof__sb-block {
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--c-border);
|
||||
padding-top: .85rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
text-align: left;
|
||||
}
|
||||
.wbf-prof__sb-block-title {
|
||||
font-size: .65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .09em;
|
||||
color: var(--c-primary);
|
||||
margin-bottom: .15rem;
|
||||
}
|
||||
.wbf-prof__sb-field { display: flex; flex-direction: column; gap: .1rem; }
|
||||
.wbf-prof__sb-field-lbl {
|
||||
font-size: .72rem;
|
||||
color: var(--c-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
.wbf-prof__sb-field-lbl i { margin-right: .25rem; }
|
||||
.wbf-prof__sb-field-val {
|
||||
font-size: .82rem;
|
||||
color: var(--c-text-dim);
|
||||
word-break: break-all;
|
||||
}
|
||||
.wbf-prof__sb-field-val--link {
|
||||
color: var(--c-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.wbf-prof__sb-field-val--link:hover { text-decoration: underline; }
|
||||
|
||||
/* ── MAIN ────────────────────────────────────────────────────────── */
|
||||
.wbf-prof__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Header card */
|
||||
.wbf-prof__header-card {
|
||||
position: relative;
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--c-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
.wbf-prof__header-card-bg {
|
||||
position: absolute; inset: 0;
|
||||
background: linear-gradient(135deg, #0a1628 0%, #0d1f3c 50%, #061218 100%);
|
||||
}
|
||||
.wbf-prof__header-card-bg::after {
|
||||
content: '';
|
||||
position: absolute; inset: 0;
|
||||
background: radial-gradient(ellipse 70% 120% at 85% 50%, rgba(0,180,216,.2) 0%, transparent 65%);
|
||||
}
|
||||
.wbf-prof__header-card-inner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem 1.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.wbf-prof__header-name {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
margin: 0 0 .35rem;
|
||||
letter-spacing: -.02em;
|
||||
}
|
||||
.wbf-prof__header-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .6rem;
|
||||
margin-bottom: .5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.wbf-prof__header-online {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
color: #22c55e;
|
||||
background: rgba(34,197,94,.12);
|
||||
border: 1px solid rgba(34,197,94,.25);
|
||||
border-radius: 20px;
|
||||
padding: .15rem .65rem;
|
||||
}
|
||||
.wbf-prof__header-online span {
|
||||
width: 6px; height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 5px rgba(34,197,94,.8);
|
||||
animation: wbf-pulse 2s infinite;
|
||||
}
|
||||
.wbf-prof__header-bio {
|
||||
font-size: .85rem;
|
||||
color: rgba(255,255,255,.6);
|
||||
margin: 0;
|
||||
max-width: 420px;
|
||||
}
|
||||
.wbf-prof__header-btns {
|
||||
display: flex;
|
||||
gap: .6rem;
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.wbf-prof__tabs {
|
||||
display: flex;
|
||||
background: var(--c-surface);
|
||||
border: 1px solid var(--c-border);
|
||||
border-top: none;
|
||||
padding: 0 .75rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.wbf-prof__tabs::-webkit-scrollbar { display: none; }
|
||||
.wbf-prof__tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
padding: .85rem 1rem;
|
||||
font-size: .82rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-muted);
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
white-space: nowrap;
|
||||
transition: color .15s, border-color .15s;
|
||||
}
|
||||
.wbf-prof__tab:hover { color: var(--c-text); }
|
||||
.wbf-prof__tab.active { color: var(--c-primary); border-bottom-color: var(--c-primary); }
|
||||
.wbf-prof__tab i { font-size: .8rem; }
|
||||
|
||||
/* Tab body */
|
||||
.wbf-prof__tab-body {
|
||||
background: var(--c-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-top: none;
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Section headers */
|
||||
.wbf-prof__section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.wbf-prof__section-header > span {
|
||||
font-size: .82rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
color: var(--c-text-dim);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .45rem;
|
||||
}
|
||||
.wbf-prof__section-header > span i { color: var(--c-primary); }
|
||||
.wbf-prof__section-more {
|
||||
font-size: .75rem;
|
||||
color: var(--c-primary);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .3rem;
|
||||
opacity: .8;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.wbf-prof__section-more:hover { opacity: 1; }
|
||||
|
||||
/* Post cards */
|
||||
.wbf-prof__posts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .6rem;
|
||||
}
|
||||
.wbf-prof__post-card {
|
||||
display: block;
|
||||
background: var(--c-surface);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-sm);
|
||||
text-decoration: none;
|
||||
transition: border-color .15s, transform .12s;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wbf-prof__post-card:hover {
|
||||
border-color: rgba(0,180,216,.4);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
.wbf-prof__post-card-inner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: .9rem;
|
||||
padding: .9rem 1.1rem;
|
||||
}
|
||||
.wbf-prof__post-card-icon { flex-shrink: 0; }
|
||||
.wbf-prof__post-card-type-icon {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.wbf-prof__post-card-type-icon--thread { background: rgba(0,180,216,.15); color: var(--c-primary); }
|
||||
.wbf-prof__post-card-type-icon--reply { background: rgba(99,102,241,.15); color: #818cf8; }
|
||||
.wbf-prof__post-card-body { flex: 1; min-width: 0; }
|
||||
.wbf-prof__post-card-title {
|
||||
font-size: .88rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: .2rem;
|
||||
}
|
||||
.wbf-prof__post-card-preview {
|
||||
font-size: .78rem;
|
||||
color: var(--c-muted);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: .35rem;
|
||||
}
|
||||
.wbf-prof__post-card-meta {
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
font-size: .72rem;
|
||||
color: var(--c-muted);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.wbf-prof__post-card-meta i { margin-right: .2rem; }
|
||||
.wbf-prof__post-card-type { font-weight: 600; }
|
||||
.wbf-prof__post-card-type--thread { color: var(--c-primary); }
|
||||
.wbf-prof__post-card-type--reply { color: #818cf8; }
|
||||
.wbf-prof__post-card-time {
|
||||
font-size: .72rem;
|
||||
color: var(--c-muted);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
margin-top: .2rem;
|
||||
}
|
||||
|
||||
/* Overview 2-col grid */
|
||||
.wbf-prof__overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Stat cards */
|
||||
.wbf-prof__stat-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .65rem;
|
||||
}
|
||||
.wbf-prof__stat-card {
|
||||
background: var(--c-surface);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: .9rem 1rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wbf-prof__stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
width: 3px;
|
||||
}
|
||||
.wbf-prof__stat-card--blue::before { background: var(--c-primary); }
|
||||
.wbf-prof__stat-card--pink::before { background: #ec4899; }
|
||||
.wbf-prof__stat-card--gold::before { background: #fbbf24; }
|
||||
.wbf-prof__stat-card-icon {
|
||||
font-size: 1rem;
|
||||
margin-bottom: .3rem;
|
||||
}
|
||||
.wbf-prof__stat-card--blue .wbf-prof__stat-card-icon { color: var(--c-primary); }
|
||||
.wbf-prof__stat-card--pink .wbf-prof__stat-card-icon { color: #ec4899; }
|
||||
.wbf-prof__stat-card--gold .wbf-prof__stat-card-icon { color: #fbbf24; }
|
||||
.wbf-prof__stat-card-val {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 800;
|
||||
color: var(--c-text);
|
||||
line-height: 1.1;
|
||||
}
|
||||
.wbf-prof__stat-card-lbl {
|
||||
font-size: .72rem;
|
||||
color: var(--c-muted);
|
||||
margin-top: .15rem;
|
||||
}
|
||||
.wbf-prof__stat-card-sub {
|
||||
font-size: .7rem;
|
||||
color: var(--c-muted);
|
||||
margin-top: .3rem;
|
||||
}
|
||||
.wbf-prof__stat-card-bar {
|
||||
height: 3px;
|
||||
background: rgba(255,255,255,.07);
|
||||
border-radius: 3px;
|
||||
margin-top: .6rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wbf-prof__stat-card--blue .wbf-prof__stat-card-bar > div { background: var(--c-primary); height: 100%; border-radius: 3px; }
|
||||
.wbf-prof__stat-card--pink .wbf-prof__stat-card-bar > div { background: #ec4899; height: 100%; border-radius: 3px; }
|
||||
|
||||
/* Badges */
|
||||
.wbf-prof__badges {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
||||
gap: .65rem;
|
||||
}
|
||||
.wbf-prof__badge {
|
||||
background: var(--c-surface);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: .75rem .5rem;
|
||||
text-align: center;
|
||||
opacity: .45;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.wbf-prof__badge--active { opacity: 1; }
|
||||
.wbf-prof__badge-icon {
|
||||
width: 40px; height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1rem;
|
||||
margin: 0 auto .5rem;
|
||||
}
|
||||
.wbf-prof__badge-name {
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
color: var(--c-text);
|
||||
}
|
||||
.wbf-prof__badge-sub {
|
||||
font-size: .62rem;
|
||||
color: var(--c-muted);
|
||||
margin-top: .1rem;
|
||||
}
|
||||
|
||||
/* Activity list */
|
||||
.wbf-prof__activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
.wbf-prof__activity-item {
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
align-items: flex-start;
|
||||
padding: .75rem 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,.04);
|
||||
}
|
||||
.wbf-prof__activity-item:last-child { border-bottom: none; }
|
||||
.wbf-prof__activity-av { flex-shrink: 0; }
|
||||
.wbf-prof__activity-body { flex: 1; min-width: 0; }
|
||||
.wbf-prof__activity-title {
|
||||
display: block;
|
||||
font-size: .82rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-text);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: .2rem;
|
||||
}
|
||||
.wbf-prof__activity-title:hover { color: var(--c-primary); }
|
||||
.wbf-prof__activity-meta {
|
||||
font-size: .7rem;
|
||||
color: var(--c-muted);
|
||||
}
|
||||
.wbf-prof__activity-meta i { margin-right: .2rem; }
|
||||
.wbf-prof__activity-time {
|
||||
font-size: .7rem;
|
||||
color: var(--c-muted);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Settings grid */
|
||||
.wbf-prof__settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.25rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 900px) {
|
||||
.wbf-prof { grid-template-columns: 1fr; }
|
||||
.wbf-prof__sidebar { position: static; }
|
||||
.wbf-prof__overview-grid,
|
||||
.wbf-prof__settings-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.wbf-prof__tab-body { padding: 1rem; }
|
||||
.wbf-prof__header-card-inner { padding: 1.1rem; }
|
||||
.wbf-prof__stat-cards { flex-direction: column; }
|
||||
.wbf-prof__badges { grid-template-columns: repeat(3,1fr); }
|
||||
}
|
||||
@@ -510,17 +510,16 @@
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Profil speichern ───────────────────────────────────────── */
|
||||
$(document).on('click', '#wbfSaveProfile, #wbfSaveProfileCf', function () {
|
||||
/* ── Profil speichern (alles auf einmal) ───────────────────── */
|
||||
$(document).on('click', '#wbfSaveProfile', function () {
|
||||
var $btn = $(this).prop('disabled', true);
|
||||
var $msg = $(this).siblings('.wbf-msg').length ? $(this).siblings('.wbf-msg') : $('#wbfProfileMsg');
|
||||
var $msg = $('#wbfProfileMsg');
|
||||
var data = {
|
||||
display_name: $('#wbfEditName').val(),
|
||||
bio: $('#wbfEditBio').val(),
|
||||
signature: $('#wbfEditSignature').val(),
|
||||
new_password: $('#wbfNewPassword').val()
|
||||
signature: $('#wbfEditSignature').val()
|
||||
};
|
||||
// Benutzerdefinierte Profilfelder einsammeln
|
||||
// Alle benutzerdefinierten Felder (alle Kategorien) einsammeln
|
||||
$('.wbf-cf-input').each(function () {
|
||||
data[$(this).data('field')] = $(this).val();
|
||||
});
|
||||
@@ -533,6 +532,40 @@
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Passwort ändern ────────────────────────────────────────── */
|
||||
$(document).on('click', '#wbfSavePassword', function () {
|
||||
var $btn = $(this).prop('disabled', true);
|
||||
var $msg = $('#wbfPasswordMsg');
|
||||
var cur = $('#wbfCurrentPassword').val();
|
||||
var pw1 = $('#wbfNewPassword').val();
|
||||
var pw2 = $('#wbfNewPassword2').val();
|
||||
|
||||
if (!cur) {
|
||||
showMsg($msg, 'Bitte aktuelles Passwort eingeben.', false);
|
||||
return $btn.prop('disabled', false);
|
||||
}
|
||||
if (pw1.length < 6) {
|
||||
showMsg($msg, 'Neues Passwort mindestens 6 Zeichen.', false);
|
||||
return $btn.prop('disabled', false);
|
||||
}
|
||||
if (pw1 !== pw2) {
|
||||
showMsg($msg, 'Die Passwörter stimmen nicht überein.', false);
|
||||
return $btn.prop('disabled', false);
|
||||
}
|
||||
|
||||
wbfPost('wbf_update_profile', {
|
||||
current_password: cur,
|
||||
new_password: pw1
|
||||
}, function (d) {
|
||||
showMsg($msg, d.message, true);
|
||||
$('#wbfCurrentPassword, #wbfNewPassword, #wbfNewPassword2').val('');
|
||||
$btn.prop('disabled', false);
|
||||
}, function (d) {
|
||||
showMsg($msg, d.message || 'Fehler', false);
|
||||
$btn.prop('disabled', false);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Signatur Zeichenzähler ─────────────────────────────────── */
|
||||
$(document).on('input', '#wbfEditSignature', function () {
|
||||
$('#wbfSigCount').text($(this).val().length);
|
||||
@@ -542,10 +575,16 @@
|
||||
$(document).on('change', '#wbfAvatarFile', function () {
|
||||
var file = this.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Sofort-Vorschau — synchron, kein Callback, kein Warten
|
||||
var objectUrl = URL.createObjectURL(file);
|
||||
$('#wbfProfileAvatar').attr('src', objectUrl).css('opacity', '.6');
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('action', 'wbf_upload_avatar');
|
||||
fd.append('nonce', WBF.nonce);
|
||||
fd.append('avatar', file);
|
||||
|
||||
$.ajax({
|
||||
url: WBF.ajax_url,
|
||||
type: 'POST',
|
||||
@@ -553,9 +592,19 @@
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (res) {
|
||||
$('#wbfProfileAvatar').css('opacity', '1');
|
||||
if (res.success) {
|
||||
$('.wbf-profile-page__avatar').attr('src', res.data.avatar_url);
|
||||
// Object-URL freigeben, endgültige Server-URL setzen
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
var finalUrl = res.data.avatar_url + '?v=' + Date.now();
|
||||
$('#wbfProfileAvatar').attr('src', finalUrl);
|
||||
// Topbar-Avatar ebenfalls aktualisieren
|
||||
$('.wbf-topbar__user img').attr('src', finalUrl);
|
||||
$('.wbf-profile-widget__avatar img').attr('src', finalUrl);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
$('#wbfProfileAvatar').css('opacity', '1');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1091,7 +1140,7 @@
|
||||
var html = '';
|
||||
d.notifications.forEach(function (n) {
|
||||
var isUnread = n.is_read == 0;
|
||||
var avatar = n.actor_avatar || '';
|
||||
var avatar = $('<img>').attr('src', n.actor_avatar || '').attr('alt', '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
|
||||
var base = WBF.forum_url || window.location.href.split('?')[0];
|
||||
var sep = base.indexOf('?') !== -1 ? '&' : '?';
|
||||
var actor = '<strong>' + $('<span>').text(n.actor_name).html() + '</strong>';
|
||||
@@ -1118,7 +1167,7 @@
|
||||
}
|
||||
|
||||
html += '<a class="wbf-notif-item' + (isUnread ? ' wbf-notif-item--unread' : '') + '" href="' + url + '">' +
|
||||
'<div class="wbf-notif-item__avatar"><img src="' + avatar + '" alt=""></div>' +
|
||||
'<div class="wbf-notif-item__avatar">' + avatar + '</div>' +
|
||||
'<div class="wbf-notif-item__body">' +
|
||||
'<div class="wbf-notif-item__text">' + text +
|
||||
(sub ? '<br><span style="color:var(--c-muted);font-size:.78rem">' + $('<span>').text(sub).html() + '</span>' : '') +
|
||||
@@ -1218,7 +1267,7 @@
|
||||
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Loeschen"><i class="fas fa-trash-can"></i></button>';
|
||||
var html = '<div class="' + cls + '" data-msg-id="' + m.id + '">';
|
||||
if (!isMine) {
|
||||
html += '<img src="' + (m.sender_avatar||'') + '" class="wbf-dm-inbox-item__avatar">';
|
||||
html += $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
|
||||
}
|
||||
html += '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
|
||||
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
|
||||
@@ -1452,7 +1501,7 @@
|
||||
var html = '';
|
||||
d.users.forEach(function(u) {
|
||||
html += '<div class="wbf-mention-item" data-username="' + $('<span>').text(u.username).html() + '">'
|
||||
+ '<img src="' + (u.avatar_url || '') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
|
||||
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
|
||||
+ '<span>' + $('<span>').text(u.display_name).html() + '</span>'
|
||||
+ '<small>@' + $('<span>').text(u.username).html() + '</small>'
|
||||
+ '</div>';
|
||||
@@ -1500,7 +1549,7 @@
|
||||
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Löschen"><i class="fas fa-trash-can"></i></button>';
|
||||
if (!isMine) {
|
||||
html += '<div class="' + cls + '" data-msg-id="' + m.id + '">'
|
||||
+ '<img src="' + (m.sender_avatar || '') + '" class="wbf-dm-msg__avatar">'
|
||||
+ $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-msg__avatar')[0].outerHTML
|
||||
+ '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
|
||||
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
|
||||
+ '</div><div class="wbf-dm-msg__time">' + time + delBtn + '</div></div></div>';
|
||||
@@ -1524,7 +1573,7 @@
|
||||
var href = window.location.pathname + '?forum_dm=inbox&with=' + conv.partner_id;
|
||||
var unread = parseInt(conv.unread_cnt) > 0;
|
||||
html += '<a class="wbf-dm-inbox-item' + (unread ? ' wbf-dm-inbox-item--unread' : '') + '" href="' + href + '">'
|
||||
+ '<img src="' + (conv.partner_avatar || '') + '" class="wbf-dm-inbox-item__avatar">'
|
||||
+ $('<img>').attr('src', conv.partner_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
|
||||
+ '<div class="wbf-dm-inbox-item__body">'
|
||||
+ '<span class="wbf-dm-inbox-item__name">' + $('<span>').text(conv.partner_name).html() + '</span>'
|
||||
+ (unread ? '<span class="wbf-dm-inbox-item__badge">' + conv.unread_cnt + '</span>' : '')
|
||||
@@ -1550,7 +1599,7 @@
|
||||
var backUrl = window.location.pathname + '?forum_dm=inbox';
|
||||
$('#wbfDmHeader').html(
|
||||
'<a href="' + backUrl + '" class="wbf-dm-back-btn" title="Zurück zur Inbox"><i class="fas fa-arrow-left"></i></a>'
|
||||
+ '<img src="' + (p.avatar_url||'') + '" class="wbf-dm-inbox-item__avatar">'
|
||||
+ $('<img>').attr('src', p.avatar_url || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
|
||||
+ '<strong>' + $('<span>').text(p.display_name).html() + '</strong>'
|
||||
+ '<a href="?forum_profile=' + p.id + '" style="font-size:.78rem;color:var(--c-muted);text-decoration:none">@' + $('<span>').text(p.username).html() + '</a>'
|
||||
);
|
||||
@@ -1634,7 +1683,7 @@
|
||||
var html = '';
|
||||
d.users.forEach(function(u) {
|
||||
html += '<div class="wbf-tag-suggest-item" data-id="' + u.id + '" data-name="' + $('<span>').text(u.display_name).html() + '">'
|
||||
+ '<img src="' + (u.avatar_url||'') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
|
||||
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
|
||||
+ $('<span>').text(u.display_name).html()
|
||||
+ '<span style="color:var(--c-muted);font-size:.75rem">@' + $('<span>').text(u.username).html() + '</span>'
|
||||
+ '</div>';
|
||||
@@ -1944,6 +1993,55 @@
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/* ── E-Mail-Adresse ändern ──────────────────────────────────────────── */
|
||||
$(document).on('click', '#wbfSaveEmail', function () {
|
||||
var $btn = $(this);
|
||||
var email = $('#wbfNewEmail').val().trim();
|
||||
var password = $('#wbfEmailPassword').val();
|
||||
var $msg = $('#wbfEmailMsg');
|
||||
if (!email) { $msg.removeClass('wbf-ok').addClass('wbf-err').text('Bitte E-Mail eingeben.'); return; }
|
||||
if (!password) { $msg.removeClass('wbf-ok').addClass('wbf-err').text('Bitte Passwort eingeben.'); return; }
|
||||
$btn.prop('disabled', true);
|
||||
wbfPost('wbf_change_email', { new_email: email, password: password }, function (d) {
|
||||
$msg.removeClass('wbf-err').addClass('wbf-ok').text(d.message || 'E-Mail geaendert.');
|
||||
$('#wbfNewEmail').val('');
|
||||
$('#wbfEmailPassword').val('');
|
||||
$btn.prop('disabled', false);
|
||||
}, function (d) {
|
||||
$msg.removeClass('wbf-ok').addClass('wbf-err').text(d.message || 'Fehler.');
|
||||
$btn.prop('disabled', false);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Toggle-Switch (Notification Prefs) ─────────────────────────────── */
|
||||
$(document).on('click', '.wbf-toggle', function () {
|
||||
var $t = $(this);
|
||||
var on = String($t.data('state')) === '1';
|
||||
var val = on ? '0' : '1';
|
||||
$t.data('state', val).attr('data-state', val);
|
||||
if (val === '1') { $t.addClass('wbf-toggle--on'); }
|
||||
else { $t.removeClass('wbf-toggle--on'); }
|
||||
});
|
||||
|
||||
/* ── Benachrichtigungs-Einstellungen speichern ───────────────────────── */
|
||||
$(document).on('click', '#wbfSaveNotifPrefs', function () {
|
||||
var $btn = $(this);
|
||||
var $msg = $('#wbfNotifPrefsMsg');
|
||||
$btn.prop('disabled', true);
|
||||
wbfPost('wbf_save_notification_prefs', {
|
||||
notify_reply: String($('#wbfNotifReply').data('state')) === '1' ? '1' : '0',
|
||||
notify_mention: String($('#wbfNotifMention').data('state')) === '1' ? '1' : '0',
|
||||
notify_message: String($('#wbfNotifMessage').data('state')) === '1' ? '1' : '0'
|
||||
}, function (d) {
|
||||
$msg.removeClass('wbf-err').addClass('wbf-ok').text(d.message || 'Gespeichert!');
|
||||
$btn.prop('disabled', false);
|
||||
}, function (d) {
|
||||
$msg.removeClass('wbf-ok').addClass('wbf-err').text(d.message || 'Fehler.');
|
||||
$btn.prop('disabled', false);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Lesezeichen ────────────────────────────────────────────────────── */
|
||||
$(document).on('click', '.wbf-bookmark-btn', function () {
|
||||
var $btn = $(this);
|
||||
@@ -1959,4 +2057,110 @@
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Nutzer ignorieren / Ignorierung aufheben ────────────────────────── */
|
||||
$(document).on('click', '.wbf-ignore-btn', function () {
|
||||
var $btn = $(this);
|
||||
var ignoredId = parseInt($btn.data('id'), 10);
|
||||
var name = $btn.data('name') || 'diesen Nutzer';
|
||||
var isIgnored = String($btn.data('ignored')) === '1';
|
||||
|
||||
// Bestätigung nur beim Ignorieren, nicht beim Entblocken
|
||||
if (!isIgnored) {
|
||||
if (!confirm(name + ' ignorieren?\n\nDessen Beiträge werden in Threads ausgeblendet und DMs werden blockiert.')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$btn.prop('disabled', true);
|
||||
|
||||
wbfPost('wbf_toggle_ignore', { ignored_id: ignoredId }, function (d) {
|
||||
var nowIgnored = d.ignored;
|
||||
|
||||
// Alle Buttons mit dieser User-ID auf der Seite aktualisieren
|
||||
$('.wbf-ignore-btn[data-id="' + ignoredId + '"]').each(function () {
|
||||
var $b = $(this);
|
||||
$b.data('ignored', nowIgnored ? '1' : '0');
|
||||
$b.attr('data-ignored', nowIgnored ? '1' : '0');
|
||||
|
||||
// Icon + Label aktualisieren
|
||||
$b.find('i').attr('class', 'fas fa-' + (nowIgnored ? 'eye' : 'eye-slash'));
|
||||
|
||||
// Button-Variante (Post-Footer, klein ohne wbf-btn)
|
||||
if (!$b.hasClass('wbf-btn')) {
|
||||
$b.text('');
|
||||
$b.append('<i class="fas fa-' + (nowIgnored ? 'eye' : 'eye-slash') + '"></i> ' + (nowIgnored ? 'Entblocken' : 'Ignorieren'));
|
||||
} else {
|
||||
// Profil-Variante mit wbf-btn
|
||||
$b.html('<i class="fas fa-' + (nowIgnored ? 'eye' : 'eye-slash') + '"></i> ' + (nowIgnored ? 'Ignorierung aufheben' : 'Nutzer ignorieren'));
|
||||
}
|
||||
$b.prop('disabled', false);
|
||||
});
|
||||
|
||||
// Posts des Users auf der aktuellen Seite ein-/ausblenden
|
||||
$('.wbf-post, .wbf-post--op').each(function () {
|
||||
var $post = $(this);
|
||||
// Buttons innerhalb dieses Posts mit der User-ID suchen
|
||||
var $ib = $post.find('.wbf-ignore-btn[data-id="' + ignoredId + '"]');
|
||||
if (!$ib.length) return;
|
||||
|
||||
if (nowIgnored) {
|
||||
// Ignoriert → Overlay zeigen wenn noch nicht vorhanden
|
||||
if (!$post.find('.wbf-ignored-bar').length) {
|
||||
var barHtml = '<div class="wbf-ignored-bar">' +
|
||||
'<span><i class="fas fa-eye-slash"></i> Beitrag von ignoriertem Nutzer: <strong>' +
|
||||
$('<span>').text(name).html() + '</strong></span>' +
|
||||
'<button class="wbf-show-ignored-btn" type="button">Trotzdem anzeigen</button>' +
|
||||
'</div>' +
|
||||
'<div class="wbf-ignored-content" style="display:none">';
|
||||
$post.addClass('wbf-post--ignored');
|
||||
$post.prepend(barHtml);
|
||||
// Restlichen Inhalt in ignored-content verschieben
|
||||
$post.children(':not(.wbf-ignored-bar):not(.wbf-ignored-content)').wrapAll('<div class="wbf-ignored-content-inner">');
|
||||
$post.find('.wbf-ignored-content').append($post.find('.wbf-ignored-content-inner').children());
|
||||
$post.find('.wbf-ignored-content-inner').remove();
|
||||
}
|
||||
} else {
|
||||
// Entblockt → Overlay entfernen
|
||||
var $bar = $post.find('.wbf-ignored-bar');
|
||||
var $content = $post.find('.wbf-ignored-content');
|
||||
if ($bar.length) {
|
||||
// Inhalt wieder nach oben holen
|
||||
$content.children().unwrap();
|
||||
$bar.remove();
|
||||
$post.removeClass('wbf-post--ignored');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Ignore-Liste im Profil aktualisieren (falls Nutzer auf eigener Profil-Seite)
|
||||
if (!nowIgnored) {
|
||||
// Eintrag aus der Liste entfernen
|
||||
$('#wbf-ignore-item-' + ignoredId).fadeOut(300, function () {
|
||||
$(this).remove();
|
||||
var remaining = $('#wbfIgnoreList .wbf-ignore-item').length;
|
||||
$('#wbfIgnoreCount').text(remaining);
|
||||
if (remaining === 0) {
|
||||
$('#wbfIgnoreList').replaceWith('<p class="wbf-profile-empty" id="wbfIgnoreEmpty">Du ignorierst niemanden.</p>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toast-Meldung
|
||||
var $t = $('<div class="wbf-toast">' + (d.message || (nowIgnored ? name + ' ignoriert.' : 'Ignorierung aufgehoben.')) + '</div>').appendTo('body');
|
||||
setTimeout(function () { $t.remove(); }, 3000);
|
||||
|
||||
}, function () {
|
||||
// Fehler-Callback
|
||||
$('.wbf-ignore-btn[data-id="' + ignoredId + '"]').prop('disabled', false);
|
||||
});
|
||||
});
|
||||
|
||||
/* "Trotzdem anzeigen" — eingeklappten ignorierten Post aufdecken */
|
||||
$(document).on('click', '.wbf-show-ignored-btn', function () {
|
||||
var $bar = $(this).closest('.wbf-ignored-bar');
|
||||
var $content = $bar.next('.wbf-ignored-content');
|
||||
$content.slideDown(200);
|
||||
$bar.hide();
|
||||
});
|
||||
|
||||
}(jQuery));
|
||||
@@ -18,6 +18,9 @@ class WBF_Ajax {
|
||||
'wbf_create_poll',
|
||||
'wbf_toggle_bookmark',
|
||||
'wbf_set_thread_prefix',
|
||||
'wbf_toggle_ignore',
|
||||
'wbf_change_email',
|
||||
'wbf_save_notification_prefs',
|
||||
];
|
||||
foreach ($actions as $action) {
|
||||
add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]);
|
||||
@@ -40,23 +43,46 @@ class WBF_Ajax {
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public static function handle_login() {
|
||||
// Brute-Force-Schutz: max. 10 Versuche pro IP in 15 Minuten
|
||||
$ip_key = 'wbf_login_fail_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
|
||||
$fails = (int) get_transient( $ip_key );
|
||||
if ( $fails >= 10 ) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Zu viele fehlgeschlagene Loginversuche. Bitte warte 15 Minuten.',
|
||||
'locked' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// Login braucht keinen Nonce — Credentials sind die Authentifizierung
|
||||
$result = WBF_Auth::login(
|
||||
sanitize_text_field($_POST['username'] ?? ''),
|
||||
$_POST['password'] ?? ''
|
||||
);
|
||||
if ($result['success']) {
|
||||
// Erfolgreicher Login: Fehlzähler löschen
|
||||
delete_transient( $ip_key );
|
||||
$u = $result['user'];
|
||||
if ( ! empty($_POST['remember_me']) ) {
|
||||
WBF_Auth::set_remember_cookie($u->id);
|
||||
}
|
||||
wp_send_json_success(['display_name'=>$u->display_name,'avatar_url'=>$u->avatar_url,'user_id'=>$u->id]);
|
||||
} else {
|
||||
// Fehlversuch zählen — außer bei gesperrten Konten (kein Passwortfehler)
|
||||
if ( empty($result['banned']) ) {
|
||||
set_transient( $ip_key, $fails + 1, 15 * MINUTE_IN_SECONDS );
|
||||
}
|
||||
wp_send_json_error($result);
|
||||
}
|
||||
}
|
||||
|
||||
public static function handle_register() {
|
||||
// Brute-Force/Spam-Schutz: max. 5 Registrierungen pro IP pro Stunde
|
||||
$reg_ip_key = 'wbf_reg_ip_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
|
||||
$reg_fails = (int) get_transient( $reg_ip_key );
|
||||
if ( $reg_fails >= 5 ) {
|
||||
wp_send_json_error(['message' => 'Zu viele Registrierungsversuche von dieser IP. Bitte warte eine Stunde.']);
|
||||
}
|
||||
|
||||
// Spam-Schutz: Honeypot + Zeitlimit
|
||||
if ( ! empty($_POST['wbf_website']) ) {
|
||||
wp_send_json_error(['message' => 'Spam erkannt.']);
|
||||
@@ -95,6 +121,8 @@ class WBF_Ajax {
|
||||
sanitize_text_field($_POST['display_name'] ?? '')
|
||||
);
|
||||
if ($result['success']) {
|
||||
// Registrierungs-Zähler für IP erhöhen
|
||||
set_transient( $reg_ip_key, $reg_fails + 1, HOUR_IN_SECONDS );
|
||||
$u = $result['user'];
|
||||
// Einladungscode einlösen
|
||||
$reg_mode2 = wbf_get_settings()['registration_mode'] ?? 'open';
|
||||
@@ -223,9 +251,11 @@ class WBF_Ajax {
|
||||
}
|
||||
// Thread-Abonnenten benachrichtigen
|
||||
$subscribers = WBF_DB::get_thread_subscribers($thread_id);
|
||||
// $notif_users is a flat array of IDs (from get_col) — cast to int for comparison
|
||||
$notif_ids = array_map('intval', $notif_users);
|
||||
foreach ($subscribers as $sub) {
|
||||
if ((int)$sub->id === (int)$user->id) continue; // nicht sich selbst
|
||||
if (in_array($sub->id, array_column($notif_users, 'id') ?: [])) continue; // schon benachrichtigt
|
||||
if (in_array((int)$sub->id, $notif_ids, true)) continue; // schon benachrichtigt
|
||||
self::send_notification_email($sub, 'reply', $user->display_name, [
|
||||
'thread_id' => $thread_id,
|
||||
'thread_title' => $thread->title,
|
||||
@@ -369,6 +399,19 @@ class WBF_Ajax {
|
||||
|
||||
if (!empty($_POST['new_password'])) {
|
||||
if (strlen($_POST['new_password']) < 6) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']);
|
||||
// Sicherheit: aktuelles Passwort muss zur Bestätigung angegeben werden
|
||||
$current_pw = $_POST['current_password'] ?? '';
|
||||
if ( empty($current_pw) ) {
|
||||
wp_send_json_error(['message'=>'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
|
||||
}
|
||||
if ( ! password_verify($current_pw, $user->password) ) {
|
||||
wp_send_json_error(['message'=>'Aktuelles Passwort ist falsch.']);
|
||||
}
|
||||
// Bestätigungsfeld server-seitig prüfen
|
||||
$new_pw2 = $_POST['new_password2'] ?? '';
|
||||
if ( ! empty($new_pw2) && $new_pw2 !== $_POST['new_password'] ) {
|
||||
wp_send_json_error(['message'=>'Die Passwörter stimmen nicht überein.']);
|
||||
}
|
||||
$update['password'] = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
@@ -394,6 +437,15 @@ class WBF_Ajax {
|
||||
$value = sanitize_textarea_field( $raw );
|
||||
} elseif ( $def['type'] === 'number' ) {
|
||||
$value = is_numeric($raw) ? (string)(float)$raw : '';
|
||||
} elseif ( $def['type'] === 'date' ) {
|
||||
// Datum validieren — nur YYYY-MM-DD, nicht in der Zukunft
|
||||
$raw_date = sanitize_text_field( trim($raw) );
|
||||
if ( preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw_date) ) {
|
||||
$ts = strtotime($raw_date);
|
||||
$value = ($ts && $ts <= time()) ? $raw_date : '';
|
||||
} else {
|
||||
$value = '';
|
||||
}
|
||||
} else {
|
||||
$value = sanitize_text_field( $raw );
|
||||
}
|
||||
@@ -411,9 +463,37 @@ class WBF_Ajax {
|
||||
if (empty($_FILES['avatar'])) wp_send_json_error(['message'=>'Keine Datei.']);
|
||||
|
||||
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
|
||||
$mime = $_FILES['avatar']['type'] ?? '';
|
||||
if (!in_array($mime, $allowed_types)) wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
||||
if ($_FILES['avatar']['size'] > 2 * 1024 * 1024) wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
|
||||
|
||||
// Dateigröße vor dem MIME-Check prüfen
|
||||
if ( $_FILES['avatar']['size'] > 2 * 1024 * 1024 ) {
|
||||
wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
|
||||
}
|
||||
|
||||
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] kommt vom Client
|
||||
// und ist beliebig fälschbar (z.B. PHP-Datei als image/jpeg getarnt).
|
||||
// finfo_file() liest den echten Magic-Byte der temporären Datei.
|
||||
$tmp = $_FILES['avatar']['tmp_name'] ?? '';
|
||||
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
|
||||
wp_send_json_error(['message'=>'Ungültiger Datei-Upload.']);
|
||||
}
|
||||
if ( function_exists('finfo_open') ) {
|
||||
$finfo = finfo_open( FILEINFO_MIME_TYPE );
|
||||
$real_mime = finfo_file( $finfo, $tmp );
|
||||
finfo_close( $finfo );
|
||||
} else {
|
||||
// Fallback: exif_imagetype() wenn finfo nicht verfügbar
|
||||
$et_map = [
|
||||
IMAGETYPE_JPEG => 'image/jpeg',
|
||||
IMAGETYPE_PNG => 'image/png',
|
||||
IMAGETYPE_GIF => 'image/gif',
|
||||
IMAGETYPE_WEBP => 'image/webp',
|
||||
];
|
||||
$et = @exif_imagetype( $tmp );
|
||||
$real_mime = $et_map[$et] ?? '';
|
||||
}
|
||||
if ( ! in_array( $real_mime, $allowed_types, true ) ) {
|
||||
wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
@@ -468,16 +548,36 @@ class WBF_Ajax {
|
||||
|
||||
// Nur Bilder erlauben
|
||||
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
|
||||
$mime = $_FILES['image']['type'] ?? '';
|
||||
if ( ! in_array($mime, $allowed_types) ) {
|
||||
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
||||
}
|
||||
|
||||
// Max 5 MB
|
||||
// Max 5 MB — Größe zuerst prüfen bevor teure MIME-Erkennung läuft
|
||||
if ( $_FILES['image']['size'] > 5 * 1024 * 1024 ) {
|
||||
wp_send_json_error(['message' => 'Maximale Dateigröße: 5 MB.']);
|
||||
}
|
||||
|
||||
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] ist client-kontrolliert
|
||||
// und kann beliebig auf 'image/jpeg' gesetzt werden, auch für .php-Dateien.
|
||||
$tmp = $_FILES['image']['tmp_name'] ?? '';
|
||||
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
|
||||
wp_send_json_error(['message' => 'Ungültiger Datei-Upload.']);
|
||||
}
|
||||
if ( function_exists('finfo_open') ) {
|
||||
$finfo = finfo_open( FILEINFO_MIME_TYPE );
|
||||
$real_mime = finfo_file( $finfo, $tmp );
|
||||
finfo_close( $finfo );
|
||||
} else {
|
||||
$et_map = [
|
||||
IMAGETYPE_JPEG => 'image/jpeg',
|
||||
IMAGETYPE_PNG => 'image/png',
|
||||
IMAGETYPE_GIF => 'image/gif',
|
||||
IMAGETYPE_WEBP => 'image/webp',
|
||||
];
|
||||
$et = @exif_imagetype( $tmp );
|
||||
$real_mime = $et_map[$et] ?? '';
|
||||
}
|
||||
if ( ! in_array( $real_mime, $allowed_types, true ) ) {
|
||||
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
@@ -543,7 +643,8 @@ class WBF_Ajax {
|
||||
self::verify();
|
||||
$query = sanitize_text_field( $_POST['query'] ?? '' );
|
||||
if ( mb_strlen( $query ) < 2 ) wp_send_json_error(['message' => 'Suchbegriff zu kurz.']);
|
||||
$results = WBF_DB::search( $query, 40 );
|
||||
$current_search = WBF_Auth::get_current_user();
|
||||
$results = WBF_DB::search( $query, 40, $current_search );
|
||||
wp_send_json_success(['results' => $results, 'query' => $query]);
|
||||
}
|
||||
|
||||
@@ -588,6 +689,18 @@ class WBF_Ajax {
|
||||
$is_mod = WBF_DB::can($user, 'delete_post');
|
||||
if ( ! $is_own && ! $is_mod ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
|
||||
|
||||
// Post-Bearbeitungslimit prüfen — gilt auch für Thread-Erstbeiträge
|
||||
// (spiegelt identisches Verhalten zu handle_edit_post() wider)
|
||||
if ( $is_own && ! $is_mod ) {
|
||||
$limit_min = (int)( wbf_get_settings()['post_edit_limit'] ?? 30 );
|
||||
if ( $limit_min > 0 ) {
|
||||
$age_min = ( time() - strtotime( $thread->created_at ) ) / 60;
|
||||
if ( $age_min > $limit_min ) {
|
||||
wp_send_json_error(['message' => "Bearbeitung nur innerhalb von {$limit_min} Minuten nach dem Erstellen möglich."]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$wpdb->update(
|
||||
"{$wpdb->prefix}forum_threads",
|
||||
@@ -682,6 +795,11 @@ class WBF_Ajax {
|
||||
if ($to_id === (int)$user->id) wp_send_json_error(['message'=>'Du kannst dir nicht selbst schreiben.']);
|
||||
if (!WBF_DB::get_user($to_id)) wp_send_json_error(['message'=>'Empfänger nicht gefunden.']);
|
||||
|
||||
// DM-Blockierung: Empfänger hat Sender ignoriert
|
||||
if ( WBF_DB::is_ignored( $to_id, $user->id ) ) {
|
||||
wp_send_json_error(['message' => 'Diese Person akzeptiert keine Nachrichten von dir.']);
|
||||
}
|
||||
|
||||
$id = WBF_DB::send_message($user->id, $to_id, $content);
|
||||
// Notify recipient
|
||||
WBF_DB::create_notification($to_id, 'message', $id, $user->id);
|
||||
@@ -739,14 +857,22 @@ class WBF_Ajax {
|
||||
// ── User-Autocomplete (für @Erwähnungen + DM) ─────────────────────────────
|
||||
|
||||
public static function handle_user_suggest() {
|
||||
// Nur eingeloggte Nutzer dürfen die User-Suche nutzen
|
||||
// (verhindert Enumeration aller Usernamen + Rollendaten durch Gäste)
|
||||
if ( ! WBF_Auth::is_forum_logged_in() ) {
|
||||
wp_send_json_success(['users'=>[]]);
|
||||
}
|
||||
$q = sanitize_text_field($_POST['q'] ?? $_GET['q'] ?? '');
|
||||
if (mb_strlen($q) < 1) wp_send_json_success(['users'=>[]]);
|
||||
global $wpdb;
|
||||
$like = $wpdb->esc_like($q) . '%';
|
||||
// Rolle wird bewusst NICHT zurückgegeben — nicht für Autocomplete nötig
|
||||
// und verhindert Informationsleck über Rollen-Verteilung im Forum.
|
||||
$users = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id, username, display_name, avatar_url, role
|
||||
"SELECT id, username, display_name, avatar_url
|
||||
FROM {$wpdb->prefix}forum_users
|
||||
WHERE username LIKE %s OR display_name LIKE %s
|
||||
WHERE (username LIKE %s OR display_name LIKE %s)
|
||||
AND role != 'banned'
|
||||
ORDER BY display_name ASC LIMIT 8",
|
||||
$like, $like
|
||||
));
|
||||
@@ -817,6 +943,13 @@ class WBF_Ajax {
|
||||
private static function send_notification_email( $to_user, $type, $actor_name, $extra = [] ) {
|
||||
if ( ! $to_user || empty($to_user->email) ) return;
|
||||
|
||||
// Prüfen ob der User diesen Benachrichtigungstyp aktiviert hat
|
||||
// Standard: alle aktiviert (1). User kann im Profil deaktivieren (0).
|
||||
$pref_key = 'notify_' . $type; // notify_reply, notify_mention, notify_message
|
||||
$meta = WBF_DB::get_user_meta( $to_user->id );
|
||||
// Nur deaktivieren wenn explizit auf '0' gesetzt — Standard ist aktiviert
|
||||
if ( isset($meta[$pref_key]) && $meta[$pref_key] === '0' ) return;
|
||||
|
||||
$blog_name = get_bloginfo('name');
|
||||
$forum_url = wbf_get_forum_url();
|
||||
$from_email = get_option('admin_email');
|
||||
@@ -897,6 +1030,17 @@ class WBF_Ajax {
|
||||
$email = sanitize_email( $_POST['email'] ?? '' );
|
||||
if ( ! is_email($email) ) wp_send_json_error(['message'=>'Ungültige E-Mail-Adresse.']);
|
||||
|
||||
// ── Rate-Limiting: max. 1 Reset-Mail pro E-Mail-Adresse alle 15 Minuten ──
|
||||
// Verhindert, dass ein Angreifer tausende Reset-Mails pro Sekunde
|
||||
// für beliebige Adressen triggert und den Mail-Server überlastet.
|
||||
$rate_key = 'wbf_pwreset_' . md5( strtolower( $email ) );
|
||||
if ( get_transient( $rate_key ) !== false ) {
|
||||
// Immer Erfolg melden — kein Leak ob Rate-Limit oder kein Account
|
||||
wp_send_json_success(['message'=>'Falls diese E-Mail registriert ist, wurde eine E-Mail gesendet.']);
|
||||
}
|
||||
// Cooldown setzen — 15 Minuten
|
||||
set_transient( $rate_key, 1, 15 * MINUTE_IN_SECONDS );
|
||||
|
||||
$user = WBF_DB::get_user_by('email', $email);
|
||||
// Immer Erfolg melden (kein User-Enumeration)
|
||||
if ( ! $user ) {
|
||||
@@ -922,7 +1066,13 @@ class WBF_Ajax {
|
||||
}
|
||||
|
||||
public static function handle_reset_password() {
|
||||
self::verify();
|
||||
// Kein self::verify() hier — Gäste haben keine Forum-Session.
|
||||
// Das Reset-Token selbst authentifiziert die Anfrage.
|
||||
// Wir prüfen trotzdem den WP-Nonce als CSRF-Schutz; dieser wird
|
||||
// von wp_localize_script für alle Besucher (auch Gäste) generiert.
|
||||
if ( ! check_ajax_referer( 'wbf_nonce', 'nonce', false ) ) {
|
||||
wp_send_json_error(['message' => 'Sicherheitsfehler.']);
|
||||
}
|
||||
$token = sanitize_text_field( $_POST['token'] ?? '' );
|
||||
$password = $_POST['password'] ?? '';
|
||||
$password2= $_POST['password2'] ?? '';
|
||||
@@ -1041,6 +1191,12 @@ class WBF_Ajax {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
|
||||
// Sicherstellen dass Spalte existiert (Schutz für bestehende Installs)
|
||||
global $wpdb;
|
||||
$cols = $wpdb->get_col( "DESCRIBE {$wpdb->prefix}forum_users" );
|
||||
if ( ! in_array( 'profile_public', $cols ) ) {
|
||||
$wpdb->query( "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) NOT NULL DEFAULT 1" );
|
||||
}
|
||||
$current = (int)($user->profile_public ?? 1);
|
||||
$new = $current ? 0 : 1;
|
||||
WBF_DB::update_user($user->id, ['profile_public'=>$new]);
|
||||
@@ -1207,6 +1363,90 @@ class WBF_Ajax {
|
||||
wp_send_json_success(['prefix' => $prefix]);
|
||||
}
|
||||
|
||||
// ── E-Mail-Adresse ändern ─────────────────────────────────────────────────
|
||||
|
||||
public static function handle_change_email() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||
|
||||
$new_email = sanitize_email( $_POST['new_email'] ?? '' );
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ( ! is_email($new_email) ) {
|
||||
wp_send_json_error(['message' => 'Ungültige E-Mail-Adresse.']);
|
||||
}
|
||||
if ( empty($password) ) {
|
||||
wp_send_json_error(['message' => 'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
|
||||
}
|
||||
if ( ! password_verify($password, $user->password) ) {
|
||||
wp_send_json_error(['message' => 'Falsches Passwort.']);
|
||||
}
|
||||
if ( strtolower($new_email) === strtolower($user->email) ) {
|
||||
wp_send_json_error(['message' => 'Das ist bereits deine aktuelle E-Mail-Adresse.']);
|
||||
}
|
||||
|
||||
// Prüfen ob E-Mail bereits vergeben
|
||||
$existing = WBF_DB::get_user_by('email', $new_email);
|
||||
if ( $existing && (int)$existing->id !== (int)$user->id ) {
|
||||
wp_send_json_error(['message' => 'Diese E-Mail-Adresse ist bereits registriert.']);
|
||||
}
|
||||
|
||||
WBF_DB::update_user($user->id, ['email' => $new_email]);
|
||||
wp_send_json_success(['message' => 'E-Mail-Adresse erfolgreich geändert.']);
|
||||
}
|
||||
|
||||
// ── Benachrichtigungs-Einstellungen speichern ─────────────────────────────
|
||||
|
||||
public static function handle_save_notification_prefs() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||
|
||||
$allowed = ['notify_reply', 'notify_mention', 'notify_message'];
|
||||
foreach ( $allowed as $key ) {
|
||||
// 1 wenn Checkbox aktiviert, 0 wenn deaktiviert
|
||||
$val = isset($_POST[$key]) && $_POST[$key] === '1' ? '1' : '0';
|
||||
WBF_DB::set_user_meta($user->id, $key, $val);
|
||||
}
|
||||
|
||||
wp_send_json_success(['message' => 'Benachrichtigungs-Einstellungen gespeichert.']);
|
||||
}
|
||||
|
||||
// ── User ignorieren / Ignorierung aufheben ────────────────────────────────
|
||||
|
||||
public static function handle_toggle_ignore() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error( ['message' => 'Nicht eingeloggt.'] );
|
||||
|
||||
$ignored_id = (int)( $_POST['ignored_id'] ?? 0 );
|
||||
if ( ! $ignored_id ) wp_send_json_error( ['message' => 'Ungültiger Nutzer.'] );
|
||||
if ( $ignored_id === (int)$user->id ) {
|
||||
wp_send_json_error( ['message' => 'Du kannst dich nicht selbst ignorieren.'] );
|
||||
}
|
||||
|
||||
$target = WBF_DB::get_user( $ignored_id );
|
||||
if ( ! $target ) wp_send_json_error( ['message' => 'Nutzer nicht gefunden.'] );
|
||||
|
||||
// Prüfen ob diese Rolle geblockt werden darf (konfigurierbar in den Einstellungen)
|
||||
if ( ! wbf_can_be_ignored( $target ) ) {
|
||||
$role_label = WBF_Roles::get($target->role)['label'] ?? $target->role;
|
||||
wp_send_json_error( ['message' => 'Nutzer mit der Rolle "' . $role_label . '" können nicht ignoriert werden.'] );
|
||||
}
|
||||
|
||||
$now_ignored = WBF_DB::toggle_ignore( $user->id, $ignored_id );
|
||||
|
||||
wp_send_json_success( [
|
||||
'ignored' => $now_ignored,
|
||||
'ignored_id' => $ignored_id,
|
||||
'display_name' => $target->display_name,
|
||||
'message' => $now_ignored
|
||||
? esc_html( $target->display_name ) . ' wird jetzt ignoriert.'
|
||||
: 'Ignorierung von ' . esc_html( $target->display_name ) . ' aufgehoben.',
|
||||
] );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
add_action( 'init', [ 'WBF_Ajax', 'init' ] );
|
||||
@@ -6,8 +6,25 @@ class WBF_Auth {
|
||||
const SESSION_KEY = 'wbf_forum_user';
|
||||
|
||||
public static function init() {
|
||||
// PHP 8.3: session_start() nach gesendeten Headers erzeugt E_WARNING,
|
||||
// der direkt in den HTML-Output fließt und das Layout zerstört.
|
||||
// Lösung: headers_sent() prüfen + session_start() mit Cookie-Optionen aufrufen.
|
||||
if ( ! session_id() ) {
|
||||
session_start();
|
||||
if ( headers_sent() ) {
|
||||
// Headers bereits gesendet — Session kann nicht sicher gestartet werden.
|
||||
// Passiert z.B. wenn WP_DEBUG=true und PHP Notices vor dem Hook ausgegeben hat.
|
||||
return;
|
||||
}
|
||||
$session_opts = [
|
||||
'cookie_httponly' => true,
|
||||
'cookie_samesite' => 'Lax',
|
||||
'use_strict_mode' => true,
|
||||
];
|
||||
// cookie_secure nur setzen wenn HTTPS aktiv — verhindert Session-Verlust bei HTTP
|
||||
if ( is_ssl() || ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ) {
|
||||
$session_opts['cookie_secure'] = true;
|
||||
}
|
||||
session_start( $session_opts );
|
||||
}
|
||||
// Auto-login via Remember-Me cookie if not already logged in
|
||||
if ( empty( $_SESSION[ self::SESSION_KEY ] ) && isset( $_COOKIE['wbf_remember'] ) ) {
|
||||
@@ -55,6 +72,7 @@ class WBF_Auth {
|
||||
]);
|
||||
// Frisch laden und einloggen
|
||||
$user = WBF_DB::get_user( $user->id );
|
||||
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
|
||||
$_SESSION[ self::SESSION_KEY ] = $user->id;
|
||||
WBF_DB::touch_last_active( $user->id );
|
||||
return array( 'success' => true, 'user' => $user );
|
||||
@@ -67,6 +85,7 @@ class WBF_Auth {
|
||||
}
|
||||
return array( 'success' => false, 'banned' => true, 'message' => $reason );
|
||||
}
|
||||
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
|
||||
$_SESSION[ self::SESSION_KEY ] = $user->id;
|
||||
WBF_DB::touch_last_active( $user->id );
|
||||
return array( 'success' => true, 'user' => $user );
|
||||
@@ -96,6 +115,7 @@ class WBF_Auth {
|
||||
'avatar_url' => $avatar,
|
||||
));
|
||||
|
||||
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
|
||||
$_SESSION[ self::SESSION_KEY ] = $id;
|
||||
return array('success'=>true,'user'=>WBF_DB::get_user($id));
|
||||
}
|
||||
|
||||
@@ -279,6 +279,18 @@ class WBF_DB {
|
||||
) $charset;";
|
||||
dbDelta( $sql_bookmarks );
|
||||
|
||||
// ── Ignore-Liste ──────────────────────────────────────────────────────
|
||||
$sql_ignore = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_ignore_list (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
ignored_id BIGINT UNSIGNED NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_ignored (user_id, ignored_id),
|
||||
KEY ignored_id (ignored_id)
|
||||
) $charset;";
|
||||
dbDelta( $sql_ignore );
|
||||
|
||||
// ── prefix_id zu threads ──────────────────────────────────────────────
|
||||
self::maybe_add_column( "{$wpdb->prefix}forum_threads", 'prefix_id',
|
||||
"ALTER TABLE {$wpdb->prefix}forum_threads ADD COLUMN prefix_id BIGINT UNSIGNED DEFAULT NULL" );
|
||||
@@ -478,7 +490,7 @@ class WBF_DB {
|
||||
}
|
||||
// Move post_count contribution too
|
||||
$post_count = (int)$wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id
|
||||
));
|
||||
if ( $post_count > 0 ) {
|
||||
$wpdb->query($wpdb->prepare(
|
||||
@@ -500,7 +512,7 @@ class WBF_DB {
|
||||
FROM {$wpdb->prefix}forum_threads t
|
||||
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
|
||||
LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id
|
||||
WHERE t.id = %d", $id
|
||||
WHERE t.id = %d AND t.deleted_at IS NULL", $id
|
||||
));
|
||||
}
|
||||
|
||||
@@ -560,7 +572,7 @@ class WBF_DB {
|
||||
|
||||
public static function count_posts( $thread_id ) {
|
||||
global $wpdb;
|
||||
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id));
|
||||
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id));
|
||||
}
|
||||
|
||||
public static function create_post( $data ) {
|
||||
@@ -623,15 +635,16 @@ class WBF_DB {
|
||||
FROM {$wpdb->prefix}forum_threads t
|
||||
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
|
||||
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
|
||||
AND t.status != 'archived' ORDER BY t.created_at DESC LIMIT %d", $limit
|
||||
WHERE t.status != 'archived' AND t.deleted_at IS NULL
|
||||
ORDER BY t.created_at DESC LIMIT %d", $limit
|
||||
));
|
||||
}
|
||||
|
||||
public static function get_stats() {
|
||||
global $wpdb;
|
||||
return [
|
||||
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived'"),
|
||||
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts"),
|
||||
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived' AND deleted_at IS NULL"),
|
||||
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL"),
|
||||
'members' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users"),
|
||||
'newest' => $wpdb->get_var("SELECT display_name FROM {$wpdb->prefix}forum_users ORDER BY registered DESC LIMIT 1"),
|
||||
];
|
||||
@@ -718,9 +731,23 @@ class WBF_DB {
|
||||
|
||||
// ── Suche ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public static function search( $query, $limit = 30 ) {
|
||||
public static function search( $query, $limit = 30, $user = null ) {
|
||||
global $wpdb;
|
||||
$like = '%' . $wpdb->esc_like( $query ) . '%';
|
||||
|
||||
// Kategorie-Sichtbarkeit: Gäste und Member dürfen keine privaten Kategorien sehen
|
||||
$user_level = $user ? WBF_Roles::level( $user->role ) : -99;
|
||||
if ( $user_level >= 50 ) {
|
||||
// Moderatoren+ sehen alles (inkl. soft-deleted ist extra)
|
||||
$cat_filter = '';
|
||||
} elseif ( $user ) {
|
||||
// Eingeloggte Member/VIP: nur guest_visible oder eigene Rolle reicht
|
||||
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role IN ('member','vip'))";
|
||||
} else {
|
||||
// Gäste: nur komplett öffentliche Kategorien
|
||||
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role = 'member')";
|
||||
}
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT 'thread' AS result_type,
|
||||
t.id, t.title, t.content, t.created_at, t.reply_count,
|
||||
@@ -729,7 +756,9 @@ class WBF_DB {
|
||||
FROM {$wpdb->prefix}forum_threads t
|
||||
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
|
||||
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
|
||||
WHERE (t.title LIKE %s OR t.content LIKE %s) AND t.status != 'archived'
|
||||
WHERE (t.title LIKE %s OR t.content LIKE %s)
|
||||
AND t.status != 'archived' AND t.deleted_at IS NULL
|
||||
$cat_filter
|
||||
UNION ALL
|
||||
SELECT 'post' AS result_type,
|
||||
p.id, t.title, p.content, p.created_at, 0 AS reply_count,
|
||||
@@ -739,7 +768,9 @@ class WBF_DB {
|
||||
JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id
|
||||
JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
|
||||
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
|
||||
WHERE p.content LIKE %s AND t.status != 'archived'
|
||||
WHERE p.content LIKE %s
|
||||
AND p.deleted_at IS NULL AND t.status != 'archived' AND t.deleted_at IS NULL
|
||||
$cat_filter
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %d",
|
||||
$like, $like, $like, $limit
|
||||
@@ -1207,13 +1238,18 @@ class WBF_DB {
|
||||
global $wpdb;
|
||||
$token = bin2hex( random_bytes(32) );
|
||||
$hash = hash( 'sha256', $token );
|
||||
// Alte Tokens löschen
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_users", [] ); // nur placeholder
|
||||
// Altes Token dieses Users zurücksetzen bevor ein neues gesetzt wird
|
||||
$wpdb->query( $wpdb->prepare(
|
||||
"UPDATE {$wpdb->prefix}forum_users
|
||||
SET reset_token=NULL, reset_token_expires=NULL
|
||||
WHERE id=%d",
|
||||
(int) $user_id
|
||||
) );
|
||||
$wpdb->query( $wpdb->prepare(
|
||||
"UPDATE {$wpdb->prefix}forum_users
|
||||
SET reset_token=%s, reset_token_expires=DATE_ADD(NOW(), INTERVAL 1 HOUR)
|
||||
WHERE id=%d",
|
||||
$hash, $user_id
|
||||
$hash, (int) $user_id
|
||||
) );
|
||||
return $token; // Klartext-Token → per E-Mail senden
|
||||
}
|
||||
@@ -1458,6 +1494,25 @@ class WBF_DB {
|
||||
update_option( 'wbf_profile_fields', $fields );
|
||||
}
|
||||
|
||||
public static function get_profile_field_categories() {
|
||||
$cats = get_option( 'wbf_profile_field_cats', null );
|
||||
if ( $cats === null ) {
|
||||
// Default-Kategorien beim ersten Aufruf
|
||||
$defaults = [
|
||||
[ 'id' => 'cat_allgemein', 'name' => 'Allgemein', 'icon' => '👤' ],
|
||||
[ 'id' => 'cat_kontakt', 'name' => 'Kontakt', 'icon' => '✉️' ],
|
||||
[ 'id' => 'cat_social', 'name' => 'Social Media', 'icon' => '🌐' ],
|
||||
];
|
||||
update_option( 'wbf_profile_field_cats', $defaults );
|
||||
return $defaults;
|
||||
}
|
||||
return is_array( $cats ) ? $cats : [];
|
||||
}
|
||||
|
||||
public static function save_profile_field_categories( $cats ) {
|
||||
update_option( 'wbf_profile_field_cats', $cats );
|
||||
}
|
||||
|
||||
public static function get_user_meta( $user_id ) {
|
||||
global $wpdb;
|
||||
$rows = $wpdb->get_results( $wpdb->prepare(
|
||||
@@ -1689,6 +1744,128 @@ class WBF_DB {
|
||||
));
|
||||
}
|
||||
|
||||
// ── Ignore-Liste ──────────────────────────────────────────────────────────
|
||||
|
||||
public static function toggle_ignore( $user_id, $ignored_id ) {
|
||||
global $wpdb;
|
||||
$user_id = (int) $user_id;
|
||||
$ignored_id = (int) $ignored_id;
|
||||
if ( self::is_ignored( $user_id, $ignored_id ) ) {
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_ignore_list", [
|
||||
'user_id' => $user_id,
|
||||
'ignored_id' => $ignored_id,
|
||||
] );
|
||||
return false;
|
||||
}
|
||||
$wpdb->replace( "{$wpdb->prefix}forum_ignore_list", [
|
||||
'user_id' => $user_id,
|
||||
'ignored_id' => $ignored_id,
|
||||
] );
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function is_ignored( $user_id, $ignored_id ) {
|
||||
global $wpdb;
|
||||
$table = "{$wpdb->prefix}forum_ignore_list";
|
||||
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return false;
|
||||
return (bool) $wpdb->get_var( $wpdb->prepare(
|
||||
"SELECT id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d AND ignored_id=%d",
|
||||
(int) $user_id, (int) $ignored_id
|
||||
) );
|
||||
}
|
||||
|
||||
/** Gibt alle ignorierten User-IDs als int-Array zurück */
|
||||
public static function get_ignored_ids( $user_id ) {
|
||||
global $wpdb;
|
||||
$table = "{$wpdb->prefix}forum_ignore_list";
|
||||
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return [];
|
||||
$ids = $wpdb->get_col( $wpdb->prepare(
|
||||
"SELECT ignored_id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d",
|
||||
(int) $user_id
|
||||
) );
|
||||
return array_map( 'intval', $ids );
|
||||
}
|
||||
|
||||
/** Vollständige Ignore-Liste mit User-Daten */
|
||||
public static function get_ignore_list( $user_id ) {
|
||||
global $wpdb;
|
||||
$table = "{$wpdb->prefix}forum_ignore_list";
|
||||
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return [];
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT u.id, u.username, u.display_name, u.avatar_url, u.role,
|
||||
il.created_at AS ignored_since
|
||||
FROM {$wpdb->prefix}forum_ignore_list il
|
||||
JOIN {$wpdb->prefix}forum_users u ON u.id = il.ignored_id
|
||||
WHERE il.user_id = %d
|
||||
ORDER BY il.created_at DESC",
|
||||
(int) $user_id
|
||||
) );
|
||||
}
|
||||
|
||||
// ── DSGVO Art. 17: Konto vollständig löschen ──────────────────────────────
|
||||
|
||||
public static function delete_user_gdpr( $user_id ) {
|
||||
global $wpdb;
|
||||
$user_id = (int) $user_id;
|
||||
$user = self::get_user( $user_id );
|
||||
if ( ! $user ) return false;
|
||||
if ( $user->role === 'superadmin' ) return false;
|
||||
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'from_id' => $user_id ] );
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'to_id' => $user_id ] );
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", [ 'user_id' => $user_id ] );
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'user_id' => $user_id ] );
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'actor_id' => $user_id ] );
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_subscriptions", [ 'user_id' => $user_id ] );
|
||||
|
||||
$table_bm = "{$wpdb->prefix}forum_bookmarks";
|
||||
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_bm'" ) === $table_bm ) {
|
||||
$wpdb->delete( $table_bm, [ 'user_id' => $user_id ] );
|
||||
}
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_likes", [ 'user_id' => $user_id ] );
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_reactions", [ 'user_id' => $user_id ] );
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_reports", [ 'reporter_id' => $user_id ] );
|
||||
|
||||
$table_pv = "{$wpdb->prefix}forum_poll_votes";
|
||||
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_pv'" ) === $table_pv ) {
|
||||
$wpdb->delete( $table_pv, [ 'user_id' => $user_id ] );
|
||||
}
|
||||
|
||||
// Ignore-Liste beidseitig bereinigen
|
||||
$table_il = "{$wpdb->prefix}forum_ignore_list";
|
||||
if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_il'" ) === $table_il ) {
|
||||
$wpdb->delete( $table_il, [ 'user_id' => $user_id ] );
|
||||
$wpdb->delete( $table_il, [ 'ignored_id' => $user_id ] );
|
||||
}
|
||||
|
||||
delete_transient( 'wbf_flood_' . $user_id );
|
||||
delete_transient( 'wbf_flood_ts_' . $user_id );
|
||||
|
||||
self::delete_user_meta_all( $user_id );
|
||||
|
||||
$anon_hash = substr( hash( 'sha256', $user_id . wp_salt() . microtime( true ) ), 0, 12 );
|
||||
$wpdb->update(
|
||||
"{$wpdb->prefix}forum_users",
|
||||
[
|
||||
'username' => 'deleted_' . $anon_hash,
|
||||
'email' => 'deleted_' . $anon_hash . '@deleted.invalid',
|
||||
'password' => '',
|
||||
'display_name' => 'Gelöschter Nutzer',
|
||||
'avatar_url' => '',
|
||||
'bio' => '',
|
||||
'signature' => '',
|
||||
'ban_reason' => '',
|
||||
'reset_token' => null,
|
||||
'reset_token_expires' => null,
|
||||
'pre_ban_role' => '',
|
||||
'ban_until' => null,
|
||||
'role' => 'banned',
|
||||
],
|
||||
[ 'id' => $user_id ]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Wortfilter ────────────────────────────────────────────────────────────
|
||||
|
||||
public static function get_word_filter() {
|
||||
@@ -1711,25 +1888,29 @@ class WBF_DB {
|
||||
// ── Flood Control ─────────────────────────────────────────────────────────
|
||||
|
||||
public static function check_flood( $user_id ) {
|
||||
$user_id = (int) $user_id;
|
||||
if ( $user_id <= 0 ) return true; // kein eingeloggter User — kein Flood-Check
|
||||
$interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 );
|
||||
if ( $interval <= 0 ) return true; // deaktiviert
|
||||
$key = 'wbf_flood_' . (int)$user_id;
|
||||
$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 );
|
||||
}
|
||||
|
||||
}
|
||||
1204
includes/class-forum-export.php
Normal file
1204
includes/class-forum-export.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,12 +43,12 @@ class WBF_Levels {
|
||||
return $defaults;
|
||||
}
|
||||
$levels = (array) $saved;
|
||||
usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
|
||||
usort( $levels, function($a, $b) { return (int)$a['min'] <=> (int)$b['min']; } );
|
||||
return $levels;
|
||||
}
|
||||
|
||||
public static function save( $levels ) {
|
||||
usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
|
||||
usort( $levels, function($a, $b) { return (int)$a['min'] <=> (int)$b['min']; } );
|
||||
update_option( self::OPTION_KEY, $levels );
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class WBF_Roles {
|
||||
private static function default_roles() {
|
||||
return [
|
||||
'superadmin' => [
|
||||
'label' => 'Superadmin',
|
||||
'label' => 'Admin',
|
||||
'level' => 100,
|
||||
'color' => '#e11d48',
|
||||
'bg_color' => 'rgba(225,29,72,.15)',
|
||||
@@ -108,7 +108,7 @@ class WBF_Roles {
|
||||
/** Nach Level sortiert (höchstes zuerst) */
|
||||
public static function get_sorted() {
|
||||
$all = self::get_all();
|
||||
uasort($all, fn($a,$b) => $b['level'] <=> $a['level']);
|
||||
uasort($all, function($a, $b) { return $b['level'] <=> $a['level']; });
|
||||
return $all;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,15 @@ class WBF_Shortcodes {
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Alter aus Geburtsdatum berechnen */
|
||||
public static function calc_age( $date_str ) {
|
||||
if ( ! $date_str || ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_str) ) return null;
|
||||
$birth = new DateTime( $date_str );
|
||||
$today = new DateTime();
|
||||
if ( $birth > $today ) return null;
|
||||
return (int) $birth->diff($today)->y;
|
||||
}
|
||||
|
||||
public static function time_ago( $datetime ) {
|
||||
$diff = time() - strtotime($datetime);
|
||||
if ($diff < 60) return 'Gerade eben';
|
||||
@@ -149,8 +158,12 @@ class WBF_Shortcodes {
|
||||
// ── Router ────────────────────────────────────────────────────────────────
|
||||
|
||||
public static function forum_main( $atts ) {
|
||||
// Server-seitiger Logout-Fallback
|
||||
// Server-seitiger Logout-Fallback — Nonce-Schutz gegen CSRF
|
||||
if (isset($_GET['wbf_do_logout'])) {
|
||||
if ( ! isset($_GET['_wpnonce']) || ! wp_verify_nonce( sanitize_text_field($_GET['_wpnonce']), 'wbf_logout' ) ) {
|
||||
wp_redirect( wbf_get_forum_url() );
|
||||
exit;
|
||||
}
|
||||
WBF_Auth::logout();
|
||||
wp_redirect( wbf_get_forum_url() );
|
||||
exit;
|
||||
@@ -310,7 +323,7 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
<div class="wbf-profile-widget__actions">
|
||||
<a href="?forum_profile=<?php echo (int)$current->id; ?>" class="wbf-btn wbf-btn--sm">Profil</a>
|
||||
<a href="<?php echo esc_url(wbf_get_forum_url() . '?wbf_do_logout=1'); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
|
||||
<a href="<?php echo esc_url(wp_nonce_url(wbf_get_forum_url() . '?wbf_do_logout=1', 'wbf_logout')); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -341,9 +354,9 @@ class WBF_Shortcodes {
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
</div>
|
||||
<?php self::render_forum_footer(); ?>
|
||||
</div>
|
||||
<?php self::render_new_thread_modal(WBF_DB::get_categories_flat(), $current); ?>
|
||||
<?php self::render_forum_footer(); ?>
|
||||
<?php self::render_auth_modal(); ?>
|
||||
</div>
|
||||
<?php return ob_get_clean();
|
||||
@@ -500,9 +513,9 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; endif; ?>
|
||||
<?php self::render_forum_footer(); ?>
|
||||
|
||||
<?php self::render_new_thread_modal(WBF_DB::get_categories_flat(),$current,$cat->id); ?>
|
||||
<?php self::render_forum_footer(); ?>
|
||||
<?php self::render_auth_modal(); ?>
|
||||
</div>
|
||||
<?php return ob_get_clean();
|
||||
@@ -731,6 +744,23 @@ class WBF_Shortcodes {
|
||||
<i class="fas fa-pen"></i> Bearbeiten
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// Ignore-Button: nur wenn der Thread-Autor nicht der eingeloggte User ist
|
||||
// und die Rolle blockiert werden darf (konfigurierbar in Einstellungen)
|
||||
$op_author = WBF_DB::get_user((int)$thread->user_id);
|
||||
if ($current && (int)$current->id !== (int)$thread->user_id && wbf_can_be_ignored($op_author)):
|
||||
$op_is_ignored = WBF_DB::is_ignored($current->id, (int)$thread->user_id);
|
||||
?>
|
||||
<button class="wbf-ignore-btn"
|
||||
data-id="<?php echo (int)$thread->user_id; ?>"
|
||||
data-name="<?php echo esc_attr($thread->display_name); ?>"
|
||||
data-ignored="<?php echo $op_is_ignored ? '1' : '0'; ?>"
|
||||
title="<?php echo $op_is_ignored ? 'Ignorierung aufheben' : 'Nutzer ignorieren'; ?>"
|
||||
style="background:none;border:none;cursor:pointer;color:var(--c-muted,#94a3b8);padding:2px 6px;border-radius:4px;font-size:.82rem">
|
||||
<i class="fas fa-<?php echo $op_is_ignored ? 'eye' : 'eye-slash'; ?>"></i>
|
||||
<?php echo $op_is_ignored ? 'Entblocken' : 'Ignorieren'; ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -774,8 +804,8 @@ class WBF_Shortcodes {
|
||||
<?php else: ?>
|
||||
<div class="wbf-notice wbf-notice--warning"><i class="fas fa-lock"></i> Dieser Thread ist geschlossen.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php self::render_forum_footer(); ?>
|
||||
</div>
|
||||
<?php self::render_auth_modal(); ?>
|
||||
<?php self::render_report_modal(); ?>
|
||||
<?php if (WBF_DB::can($current,'manage_cats')): self::render_move_modal(WBF_DB::get_categories_flat(), $id); endif; ?>
|
||||
@@ -843,6 +873,22 @@ class WBF_Shortcodes {
|
||||
<i class="fas fa-pen"></i> Bearbeiten
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// Ignore-Button im Post-Footer
|
||||
$post_author = WBF_DB::get_user((int)$post->user_id);
|
||||
if ($current && (int)$current->id !== (int)$post->user_id && wbf_can_be_ignored($post_author)):
|
||||
$post_is_ignored = WBF_DB::is_ignored($current->id, (int)$post->user_id);
|
||||
?>
|
||||
<button class="wbf-ignore-btn"
|
||||
data-id="<?php echo (int)$post->user_id; ?>"
|
||||
data-name="<?php echo esc_attr($post->display_name); ?>"
|
||||
data-ignored="<?php echo $post_is_ignored ? '1' : '0'; ?>"
|
||||
title="<?php echo $post_is_ignored ? 'Ignorierung aufheben' : 'Nutzer ignorieren'; ?>"
|
||||
style="background:none;border:none;cursor:pointer;color:var(--c-muted,#94a3b8);padding:2px 6px;border-radius:4px;font-size:.82rem">
|
||||
<i class="fas fa-<?php echo $post_is_ignored ? 'eye' : 'eye-slash'; ?>"></i>
|
||||
<?php echo $post_is_ignored ? 'Entblocken' : 'Ignorieren'; ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php echo self::mod_tools_post($post->id,$current); ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -860,7 +906,9 @@ class WBF_Shortcodes {
|
||||
$is_own = $current && $current->id == $profile->id;
|
||||
$is_staff = $current && WBF_Roles::level($current->role) >= 50;
|
||||
// Profil-Sichtbarkeit prüfen
|
||||
if (!$is_own && !$is_staff && (int)($profile->profile_public ?? 1) === 0) {
|
||||
// profile_public NULL = Spalte fehlt noch = als öffentlich (1) behandeln
|
||||
$profile_public = isset($profile->profile_public) ? (int)$profile->profile_public : 1;
|
||||
if (!$is_own && !$is_staff && $profile_public === 0) {
|
||||
ob_start(); ?>
|
||||
<div class="wbf-wrap"><?php self::render_topbar($current); ?>
|
||||
<div class="wbf-container wbf-mt">
|
||||
@@ -871,13 +919,28 @@ 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_cats = WBF_DB::get_profile_field_categories();
|
||||
$cf_cat_map = array_column( $cf_cats, null, 'id' );
|
||||
$cf_vals = WBF_DB::get_user_meta( $profile->id );
|
||||
// Aktiven Tab aus URL lesen (tab=1|2|3), Standard: 1 für eigenes, 2 für fremdes
|
||||
// Tab-ID: numerisch (1–4) oder String-Slug (z.B. 'mc' von der Forum-Bridge)
|
||||
$ptab_raw = $_GET['ptab'] ?? ($is_own ? 1 : 2);
|
||||
$active_tab = ctype_digit( (string) $ptab_raw ) ? (int) $ptab_raw : sanitize_key( $ptab_raw );
|
||||
if ( is_int($active_tab) && ! in_array($active_tab, [1,2,3,4]) ) {
|
||||
$active_tab = $is_own ? 1 : 2;
|
||||
}
|
||||
// Tab 1, 3, 4 und String-Tabs nur für eigenes Profil (außer Tab 2 = Aktivität)
|
||||
if ( ! $is_own && $active_tab !== 2 ) $active_tab = 2;
|
||||
|
||||
ob_start(); ?>
|
||||
<div class="wbf-wrap">
|
||||
<?php self::render_topbar($current); ?>
|
||||
<div class="wbf-container wbf-mt">
|
||||
<nav class="wbf-breadcrumb">
|
||||
<a href="<?php echo esc_url(remove_query_arg('forum_profile')); ?>"><i class="fas fa-home"></i> Forum</a>
|
||||
<a href="<?php echo esc_url(remove_query_arg(['forum_profile', 'ptab'])); ?>"><i class="fas fa-home"></i> Forum</a>
|
||||
<span>/</span><span>Profil</span>
|
||||
</nav>
|
||||
|
||||
@@ -885,11 +948,10 @@ class WBF_Shortcodes {
|
||||
|
||||
<!-- ── SIDEBAR ─────────────────────────────────────────── -->
|
||||
<aside class="wbf-profile-sidebar">
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="wbf-profile-sidebar__avatar-wrap">
|
||||
<img src="<?php echo esc_url($profile->avatar_url); ?>"
|
||||
alt="<?php echo esc_attr($profile->display_name); ?>"
|
||||
id="wbfProfileAvatar"
|
||||
class="wbf-profile-sidebar__avatar">
|
||||
<?php if ($is_own): ?>
|
||||
<label class="wbf-avatar-upload-btn" title="Avatar ändern">
|
||||
@@ -898,15 +960,24 @@ class WBF_Shortcodes {
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Name + Badge + Username -->
|
||||
<div class="wbf-profile-sidebar__identity">
|
||||
<h2><?php echo esc_html($profile->display_name); ?></h2>
|
||||
<?php echo self::role_badge($profile->role); ?>
|
||||
<span class="wbf-profile-sidebar__username">@<?php echo esc_html($profile->username); ?></span>
|
||||
<?php
|
||||
$profile_online = WBF_DB::is_online($profile->id, 15);
|
||||
if ($profile_online): ?>
|
||||
<span class="wbf-profile-online-badge">
|
||||
<span class="wbf-profile-online-dot"></span> Online
|
||||
</span>
|
||||
<?php else:
|
||||
$last = $profile->last_active ?? null;
|
||||
if ($last && $last !== '0000-00-00 00:00:00'): ?>
|
||||
<span class="wbf-profile-lastseen">
|
||||
<i class="fas fa-clock"></i> Zuletzt aktiv: <?php echo self::time_ago($last); ?>
|
||||
</span>
|
||||
<?php endif; endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="wbf-profile-sidebar__stats">
|
||||
<div class="wbf-profile-sidebar__stat">
|
||||
<span><?php echo (int)$profile->post_count; ?></span>
|
||||
@@ -917,119 +988,194 @@ class WBF_Shortcodes {
|
||||
<em>Dabei seit</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Level-Fortschritt -->
|
||||
<?php $level_bar = WBF_Levels::progress_bar((int)$profile->post_count); if ($level_bar): ?>
|
||||
<div class="wbf-profile-sidebar__section">
|
||||
<?php echo $level_bar; ?>
|
||||
</div>
|
||||
<div class="wbf-profile-sidebar__section"><?php echo $level_bar; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Bio -->
|
||||
<?php if (!empty($profile->bio)): ?>
|
||||
<div class="wbf-profile-sidebar__section">
|
||||
<span class="wbf-profile-sidebar__section-label"><i class="fas fa-align-left"></i> Bio</span>
|
||||
<p><?php echo nl2br(esc_html($profile->bio)); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Signatur -->
|
||||
<?php if (!empty($profile->signature)): ?>
|
||||
<div class="wbf-profile-sidebar__section">
|
||||
<span class="wbf-profile-sidebar__section-label"><i class="fas fa-pen-nib"></i> Signatur</span>
|
||||
<p class="wbf-profile-sidebar__sig"><?php echo nl2br(esc_html($profile->signature)); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Benutzerdefinierte Profilfelder (öffentliche) -->
|
||||
<!-- Öffentliche Custom Fields — nach Kategorie gruppiert -->
|
||||
<?php
|
||||
$cf_defs_pub = WBF_DB::get_profile_field_defs();
|
||||
$cf_vals_pub = WBF_DB::get_user_meta( $profile->id );
|
||||
foreach ( $cf_defs_pub as $def ):
|
||||
if ( ! $is_own && empty($def['public']) ) continue;
|
||||
$val = trim( $cf_vals_pub[ $def['key'] ] ?? '' );
|
||||
if ( $val === '' ) continue;
|
||||
$cf_by_cat_sb = [];
|
||||
foreach ( $cf_defs as $def_sb ) {
|
||||
if (!$is_own && empty($def_sb['public'])) continue;
|
||||
$val_sb = trim($cf_vals[$def_sb['key']] ?? '');
|
||||
if ($val_sb === '') continue;
|
||||
$cid_sb = $def_sb['category_id'] ?? '';
|
||||
if (!$cid_sb || !isset($cf_cat_map[$cid_sb])) $cid_sb = '__none__';
|
||||
$cf_by_cat_sb[$cid_sb][] = ['def'=>$def_sb,'val'=>$val_sb];
|
||||
}
|
||||
$sb_sections = $cf_cats;
|
||||
if (isset($cf_by_cat_sb['__none__'])) {
|
||||
$sb_sections[] = ['id'=>'__none__','name'=>'Weitere Infos','icon'=>''];
|
||||
}
|
||||
foreach ($sb_sections as $scat_sb):
|
||||
$scid_sb = $scat_sb['id'];
|
||||
if (empty($cf_by_cat_sb[$scid_sb])) continue;
|
||||
?>
|
||||
<div class="wbf-profile-sidebar__section">
|
||||
<span class="wbf-profile-sidebar__section-label">
|
||||
<i class="fas fa-<?php echo $def['type']==='url'?'link':($def['type']==='number'?'hashtag':'tag'); ?>"></i>
|
||||
<?php echo esc_html($def['label']); ?>
|
||||
<span class="wbf-profile-sidebar__section-label" style="display:flex;align-items:center;gap:5px;margin-bottom:4px">
|
||||
<?php if(!empty($scat_sb['icon'])): ?>
|
||||
<span style="font-size:.9rem"><?php echo esc_html($scat_sb['icon']); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php echo esc_html($scat_sb['name']); ?>
|
||||
</span>
|
||||
<?php if ( $def['type'] === 'url' ): ?>
|
||||
<a href="<?php echo esc_url($val); ?>" target="_blank" rel="noopener noreferrer"
|
||||
style="color:var(--c-primary);font-size:.85rem;word-break:break-all">
|
||||
<?php echo esc_html( mb_strtolower( preg_replace('#^https?://#i','',$val) ) ); ?>
|
||||
<?php foreach ($cf_by_cat_sb[$scid_sb] as $cf_entry_sb):
|
||||
$def_sb = $cf_entry_sb['def'];
|
||||
$val_sb = $cf_entry_sb['val'];
|
||||
// Auto-Link für Telegram und Discord anhand des Feld-Keys erkennen
|
||||
$key_sb = strtolower($def_sb['key']);
|
||||
$is_telegram = strpos($key_sb, 'telegram') !== false;
|
||||
$is_discord = strpos($key_sb, 'discord') !== false;
|
||||
?>
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:6px;margin-bottom:4px;font-size:.85rem">
|
||||
<span style="color:var(--c-muted,#94a3b8);flex-shrink:0"><?php echo esc_html($def_sb['label']); ?></span>
|
||||
<?php if ($def_sb['type'] === 'url'): ?>
|
||||
<a href="<?php echo esc_url($val_sb); ?>" target="_blank" rel="noopener noreferrer"
|
||||
style="color:var(--c-primary);word-break:break-all;text-align:right">
|
||||
<?php echo esc_html(mb_strtolower(preg_replace('#^https?://#i','',$val_sb))); ?>
|
||||
</a>
|
||||
<?php elseif ( $def['type'] === 'textarea' ): ?>
|
||||
<p style="font-size:.85rem"><?php echo nl2br(esc_html($val)); ?></p>
|
||||
<?php elseif ($is_telegram):
|
||||
// Username bereinigen: @ und Leerzeichen entfernen
|
||||
$tg_user = ltrim(trim($val_sb), '@');
|
||||
$tg_url = 'https://t.me/' . rawurlencode($tg_user);
|
||||
?>
|
||||
<a href="<?php echo esc_url($tg_url); ?>" target="_blank" rel="noopener noreferrer"
|
||||
style="color:#29b6f6;text-align:right;font-size:inherit">
|
||||
@<?php echo esc_html($tg_user); ?>
|
||||
</a>
|
||||
<?php elseif ($is_discord): ?>
|
||||
<span style="color:#7289da;text-align:right;font-size:inherit">
|
||||
<?php echo esc_html($val_sb); ?>
|
||||
</span>
|
||||
<?php elseif ($def_sb['type'] === 'textarea'): ?>
|
||||
<span style="text-align:right"><?php echo nl2br(esc_html($val_sb)); ?></span>
|
||||
<?php elseif ($def_sb['type'] === 'date'):
|
||||
$age_sb = self::calc_age($val_sb); ?>
|
||||
<span><?php echo $age_sb !== null ? esc_html((string)$age_sb) . ' Jahre' : '—'; ?></span>
|
||||
<?php else: ?>
|
||||
<p style="font-size:.85rem"><?php echo esc_html($val); ?></p>
|
||||
<span style="text-align:right"><?php echo esc_html($val_sb); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</aside>
|
||||
|
||||
<!-- ── MAIN ────────────────────────────────────────────── -->
|
||||
<div class="wbf-profile-main">
|
||||
|
||||
<!-- Profil bearbeiten (nur eigenes) -->
|
||||
<!-- DM-Button + Ignorieren-Button (nur auf fremden Profilen) -->
|
||||
<?php if ($current && !$is_own && WBF_Roles::level($profile->role) >= 0): ?>
|
||||
<div style="display:flex;justify-content:flex-end;gap:.5rem;margin-bottom:.75rem;flex-wrap:wrap">
|
||||
<a href="?forum_dm=inbox&with=<?php echo (int)$profile->id; ?>"
|
||||
class="wbf-btn wbf-btn--sm wbf-btn--primary">
|
||||
<i class="fas fa-envelope"></i> Nachricht senden
|
||||
</a>
|
||||
<?php if ( wbf_can_be_ignored($profile) ):
|
||||
$viewer_ignores = WBF_DB::is_ignored($current->id, $profile->id); ?>
|
||||
<button class="wbf-ignore-btn wbf-btn wbf-btn--sm<?php echo $viewer_ignores?' wbf-btn--primary':''; ?>"
|
||||
data-id="<?php echo (int)$profile->id; ?>"
|
||||
data-name="<?php echo esc_attr($profile->display_name); ?>"
|
||||
data-ignored="<?php echo $viewer_ignores?'1':'0'; ?>">
|
||||
<i class="fas fa-<?php echo $viewer_ignores?'eye':'eye-slash'; ?>"></i>
|
||||
<?php echo $viewer_ignores?'Ignorierung aufheben':'Nutzer ignorieren'; ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ── TAB-NAVIGATION ─────────────────────────────── -->
|
||||
<?php if ($is_own): ?>
|
||||
<div class="wbf-profile-card"> <div class="wbf-profile-card__header">
|
||||
<div class="wbf-profile-tabs">
|
||||
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=1"
|
||||
class="wbf-profile-tab<?php echo $active_tab===1?' active':''; ?>">
|
||||
<i class="fas fa-sliders"></i> Profil
|
||||
</a>
|
||||
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=2"
|
||||
class="wbf-profile-tab<?php echo $active_tab===2?' active':''; ?>">
|
||||
<i class="fas fa-comments"></i> Aktivität
|
||||
</a>
|
||||
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=3"
|
||||
class="wbf-profile-tab<?php echo $active_tab===3?' active':''; ?>">
|
||||
<i class="fas fa-shield-halved"></i> Privatsphäre
|
||||
</a>
|
||||
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=4"
|
||||
class="wbf-profile-tab<?php echo $active_tab===4?' active':''; ?>">
|
||||
<i class="fas fa-lock"></i> Sicherheit
|
||||
</a>
|
||||
<?php if ( class_exists('MC_Gallery_Forum_Bridge') ) : ?>
|
||||
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=mc"
|
||||
class="wbf-profile-tab<?php echo $active_tab==='mc'?' active':''; ?>">
|
||||
<i class="fas fa-cubes"></i> Minecraft
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
TAB 1 — Profil bearbeiten + Weitere Profilangaben
|
||||
══════════════════════════════════════════════════ -->
|
||||
<?php if ($is_own && $active_tab === 1): ?>
|
||||
|
||||
<!-- Profil bearbeiten -->
|
||||
<div class="wbf-profile-card">
|
||||
<div class="wbf-profile-card__header">
|
||||
<i class="fas fa-sliders"></i> Profil bearbeiten
|
||||
</div>
|
||||
<div class="wbf-profile-card__body">
|
||||
<div class="wbf-profile-edit-grid">
|
||||
<div class="wbf-form-row">
|
||||
<label>Anzeigename</label>
|
||||
<input type="text" id="wbfEditName" value="<?php echo esc_attr($profile->display_name); ?>">
|
||||
</div>
|
||||
<div class="wbf-form-row">
|
||||
<label>Neues Passwort <small>(leer = nicht ändern)</small></label>
|
||||
<input type="password" id="wbfNewPassword" placeholder="••••••">
|
||||
</div>
|
||||
</div>
|
||||
<div class="wbf-form-row">
|
||||
<label>Bio</label>
|
||||
<textarea id="wbfEditBio" rows="2"><?php echo esc_textarea($profile->bio); ?></textarea>
|
||||
</div>
|
||||
<div class="wbf-form-row">
|
||||
<label>Signatur <small>(max. 300 Zeichen)</small></label>
|
||||
<div class="wbf-form-row" style="display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem">
|
||||
<label style="font-size:.82rem;color:var(--c-muted)">Profil öffentlich sichtbar</label>
|
||||
<?php $pub = (int)($profile->profile_public ?? 1); ?>
|
||||
<button type="button" id="wbfToggleProfileVis"
|
||||
class="wbf-btn wbf-btn--sm<?php echo $pub?' wbf-btn--primary':''; ?>"
|
||||
data-state="<?php echo $pub; ?>">
|
||||
<i class="fas fa-<?php echo $pub?'eye':'eye-slash'; ?>"></i>
|
||||
<?php echo $pub?'Öffentlich':'Privat'; ?>
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="wbfEditSignature" rows="2" maxlength="300" placeholder="Deine Signatur…"><?php echo esc_textarea($profile->signature ?? ''); ?></textarea>
|
||||
<div class="wbf-sig-counter"><span id="wbfSigCount"><?php echo mb_strlen($profile->signature??''); ?></span>/300</div>
|
||||
</div>
|
||||
<div class="wbf-profile-card__footer">
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfile">
|
||||
<i class="fas fa-save"></i> Speichern
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbfProfileMsg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Benutzerdefinierte Profilfelder ──────────────── -->
|
||||
<!-- Weitere Profilangaben — nach Kategorie gruppiert (ohne eigene Speichern-Buttons) -->
|
||||
<?php
|
||||
$cf_defs = WBF_DB::get_profile_field_defs();
|
||||
$cf_vals = WBF_DB::get_user_meta( $profile->id );
|
||||
$cf_edit_by_cat = [];
|
||||
foreach ( $cf_defs as $def_e ) {
|
||||
$cid_e = $def_e['category_id'] ?? '';
|
||||
if (!$cid_e || !isset($cf_cat_map[$cid_e])) $cid_e = '__none__';
|
||||
$cf_edit_by_cat[$cid_e][] = $def_e;
|
||||
}
|
||||
$edit_sections = $cf_cats;
|
||||
if (isset($cf_edit_by_cat['__none__'])) {
|
||||
$edit_sections[] = ['id'=>'__none__','name'=>'Weitere Angaben','icon'=>'📋'];
|
||||
}
|
||||
if (!empty($cf_defs)):
|
||||
foreach ($edit_sections as $ecat):
|
||||
$ecid = $ecat['id'];
|
||||
if (empty($cf_edit_by_cat[$ecid])) continue;
|
||||
?>
|
||||
<div class="wbf-profile-card">
|
||||
<div class="wbf-profile-card__header">
|
||||
<i class="fas fa-sliders"></i> Weitere Profilangaben
|
||||
<?php if(!empty($ecat['icon'])): ?>
|
||||
<span style="margin-right:5px"><?php echo esc_html($ecat['icon']); ?></span>
|
||||
<?php else: ?><i class="fas fa-sliders"></i><?php endif; ?>
|
||||
<?php echo esc_html($ecat['name']); ?>
|
||||
</div>
|
||||
<div class="wbf-profile-card__body">
|
||||
<div class="wbf-profile-edit-grid">
|
||||
<?php foreach ( $cf_defs as $def ):
|
||||
<?php foreach ($cf_edit_by_cat[$ecid] as $def):
|
||||
$k = esc_attr($def['key']);
|
||||
$lbl = esc_html($def['label']);
|
||||
$ph = esc_attr($def['placeholder'] ?? '');
|
||||
@@ -1052,6 +1198,13 @@ class WBF_Shortcodes {
|
||||
<?php selected($cf_vals[$def['key']] ?? '', $opt); ?>><?php echo esc_html($opt); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php elseif ($def['type'] === 'date'): ?>
|
||||
<input type="date"
|
||||
class="wbf-cf-input"
|
||||
data-field="cf_<?php echo $k; ?>"
|
||||
value="<?php echo esc_attr($cf_vals[$def['key']] ?? ''); ?>"
|
||||
max="<?php echo date('Y-m-d'); ?>"
|
||||
<?php echo $req; ?>>
|
||||
<?php else: ?>
|
||||
<input type="<?php echo $def['type']==='url'?'url':($def['type']==='number'?'number':'text'); ?>"
|
||||
class="wbf-cf-input"
|
||||
@@ -1063,57 +1216,51 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="wbf-profile-card__footer">
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfileCf">
|
||||
<i class="fas fa-save"></i> Speichern
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbfProfileCfMsg"></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; endif; ?>
|
||||
|
||||
<!-- Globaler Speichern-Button für Tab 1 -->
|
||||
<div style="display:flex;align-items:center;gap:1rem;padding:.25rem 0 .5rem">
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfile" style="min-width:160px">
|
||||
<i class="fas fa-save"></i> Alles speichern
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbfProfileMsg"></span>
|
||||
</div>
|
||||
|
||||
<?php endif; /* end Tab 1 */ ?>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
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; ?>
|
||||
|
||||
<!-- ── 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 $is_own */ ?>
|
||||
|
||||
<!-- Beiträge -->
|
||||
<div class="wbf-profile-card">
|
||||
<div class="wbf-profile-card__header">
|
||||
@@ -1159,32 +1306,241 @@ 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): ?>
|
||||
|
||||
<!-- Profil-Sichtbarkeit -->
|
||||
<?php $pub = (int)($profile->profile_public ?? 1); ?>
|
||||
<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-eye"></i> Profil-Sichtbarkeit
|
||||
</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">
|
||||
<div class="wbf-form-row" style="display:flex;align-items:center;gap:1rem">
|
||||
<div>
|
||||
<div style="font-size:.9rem;font-weight:600;margin-bottom:3px">Profil öffentlich sichtbar</div>
|
||||
<div style="font-size:.8rem;color:var(--c-muted)">Wenn deaktiviert, können nur du selbst und Moderatoren dein Profil sehen.</div>
|
||||
</div>
|
||||
<button type="button" id="wbfToggleProfileVis"
|
||||
class="wbf-btn wbf-btn--sm<?php echo $pub?' wbf-btn--primary':''; ?>"
|
||||
data-state="<?php echo $pub; ?>"
|
||||
style="margin-left:auto;white-space:nowrap">
|
||||
<i class="fas fa-<?php echo $pub?'eye':'eye-slash'; ?>"></i>
|
||||
<?php echo $pub?'Öffentlich':'Privat'; ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ignorierte Nutzer -->
|
||||
<div class="wbf-profile-card">
|
||||
<div class="wbf-profile-card__header">
|
||||
<i class="fas fa-bell"></i> E-Mail-Benachrichtigungen
|
||||
</div>
|
||||
<div class="wbf-profile-card__body">
|
||||
<?php
|
||||
$notif_meta = WBF_DB::get_user_meta($current->id);
|
||||
$n_reply = ($notif_meta['notify_reply'] ?? '1') !== '0';
|
||||
$n_mention = ($notif_meta['notify_mention'] ?? '1') !== '0';
|
||||
$n_message = ($notif_meta['notify_message'] ?? '1') !== '0';
|
||||
?>
|
||||
<p style="font-size:.82rem;color:var(--c-muted);margin-bottom:1rem">
|
||||
Lege fest bei welchen Ereignissen du eine E-Mail erhältst.
|
||||
</p>
|
||||
<div class="wbf-notif-pref-list">
|
||||
<label class="wbf-notif-pref">
|
||||
<div class="wbf-notif-pref__info">
|
||||
<span><i class="fas fa-reply"></i> Antworten auf meine Threads</span>
|
||||
<small>Wenn jemand in einem deiner Threads antwortet</small>
|
||||
</div>
|
||||
<div class="wbf-toggle<?php echo $n_reply?' wbf-toggle--on':''; ?>"
|
||||
id="wbfNotifReply" data-key="notify_reply" data-state="<?php echo $n_reply?'1':'0'; ?>">
|
||||
<div class="wbf-toggle__knob"></div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="wbf-notif-pref">
|
||||
<div class="wbf-notif-pref__info">
|
||||
<span><i class="fas fa-at"></i> @Erwähnungen</span>
|
||||
<small>Wenn dich jemand in einem Beitrag erwähnt</small>
|
||||
</div>
|
||||
<div class="wbf-toggle<?php echo $n_mention?' wbf-toggle--on':''; ?>"
|
||||
id="wbfNotifMention" data-key="notify_mention" data-state="<?php echo $n_mention?'1':'0'; ?>">
|
||||
<div class="wbf-toggle__knob"></div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="wbf-notif-pref">
|
||||
<div class="wbf-notif-pref__info">
|
||||
<span><i class="fas fa-envelope"></i> Privatnachrichten</span>
|
||||
<small>Wenn du eine neue Direktnachricht erhältst</small>
|
||||
</div>
|
||||
<div class="wbf-toggle<?php echo $n_message?' wbf-toggle--on':''; ?>"
|
||||
id="wbfNotifMessage" data-key="notify_message" data-state="<?php echo $n_message?'1':'0'; ?>">
|
||||
<div class="wbf-toggle__knob"></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="wbf-profile-card__footer" style="margin-top:1rem">
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbfSaveNotifPrefs">
|
||||
<i class="fas fa-save"></i> Einstellungen speichern
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbfNotifPrefsMsg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ignorierte Nutzer -->
|
||||
<div class="wbf-profile-card">
|
||||
<div class="wbf-profile-card__header">
|
||||
<i class="fas fa-eye-slash"></i> Ignorierte Nutzer
|
||||
<span class="wbf-profile-card__count" id="wbfIgnoreCount"><?php echo count($ignore_list); ?></span>
|
||||
</div>
|
||||
<div class="wbf-profile-card__body" id="wbfIgnoreListWrap">
|
||||
<?php if (empty($ignore_list)): ?>
|
||||
<p class="wbf-profile-empty" id="wbfIgnoreEmpty">Du ignorierst niemanden.</p>
|
||||
<?php else: ?>
|
||||
<div class="wbf-ignore-list" id="wbfIgnoreList">
|
||||
<?php foreach ($ignore_list as $ign): ?>
|
||||
<div class="wbf-ignore-item" id="wbf-ignore-item-<?php echo (int)$ign->id; ?>">
|
||||
<a href="?forum_profile=<?php echo (int)$ign->id; ?>" class="wbf-ignore-item__avatar">
|
||||
<?php echo self::avatar($ign->avatar_url, $ign->display_name, 36); ?>
|
||||
</a>
|
||||
<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 */ ?>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
TAB 4 — Sicherheit (Passwort & E-Mail)
|
||||
══════════════════════════════════════════════════ -->
|
||||
<?php if ($is_own && $active_tab === 4): ?>
|
||||
|
||||
<!-- Passwort ändern -->
|
||||
<div class="wbf-profile-card" style="border-color:rgba(0,180,216,.25)">
|
||||
<div class="wbf-profile-card__header" style="background:rgba(0,180,216,.07);border-bottom-color:rgba(0,180,216,.18)">
|
||||
<i class="fas fa-lock" style="color:var(--c-primary)"></i> Passwort ändern
|
||||
</div>
|
||||
<div class="wbf-profile-card__body">
|
||||
<div class="wbf-form-row">
|
||||
<label>Aktuelles Passwort</label>
|
||||
<input type="password" id="wbfCurrentPassword" placeholder="Dein aktuelles Passwort" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="wbf-profile-edit-grid">
|
||||
<div class="wbf-form-row">
|
||||
<label>Neues Passwort <small>(min. 6 Zeichen)</small></label>
|
||||
<input type="password" id="wbfNewPassword" placeholder="Neues Passwort" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="wbf-form-row">
|
||||
<label>Neues Passwort wiederholen</label>
|
||||
<input type="password" id="wbfNewPassword2" placeholder="Passwort bestätigen" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="wbf-profile-card__footer">
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbfSavePassword">
|
||||
<i class="fas fa-key"></i> Passwort ändern
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbfPasswordMsg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail-Adresse ändern -->
|
||||
<div class="wbf-profile-card">
|
||||
<div class="wbf-profile-card__header">
|
||||
<i class="fas fa-envelope"></i> E-Mail-Adresse
|
||||
</div>
|
||||
<div class="wbf-profile-card__body">
|
||||
<p style="font-size:.82rem;color:var(--c-muted);margin-bottom:1rem">
|
||||
Aktuelle Adresse: <strong style="color:var(--c-text)"><?php echo esc_html($profile->email); ?></strong>
|
||||
</p>
|
||||
<div class="wbf-profile-edit-grid">
|
||||
<div class="wbf-form-row">
|
||||
<label>Neue E-Mail-Adresse</label>
|
||||
<input type="email" id="wbfNewEmail" placeholder="neue@email.de" autocomplete="off">
|
||||
</div>
|
||||
<div class="wbf-form-row">
|
||||
<label>Aktuelles Passwort <small>(zur Bestätigung)</small></label>
|
||||
<input type="password" id="wbfEmailPassword" placeholder="••••••" autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="wbf-profile-card__footer">
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbfSaveEmail">
|
||||
<i class="fas fa-envelope"></i> E-Mail ändern
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbfEmailMsg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; /* end Tab 4 */ ?>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
TAB MC — Minecraft-Konto verknüpfen (Bridge)
|
||||
Wird nur gerendert wenn MC Gallery Forum Bridge aktiv ist.
|
||||
══════════════════════════════════════════════════ -->
|
||||
<?php if ( $is_own && $active_tab === 'mc' && class_exists('MC_Gallery_Forum_Bridge') ) :
|
||||
echo apply_filters('wbf_profile_tab_content', '', 'minecraft', $profile);
|
||||
endif; /* end Tab MC */ ?>
|
||||
|
||||
</div><!-- /.wbf-profile-main -->
|
||||
</div><!-- /.wbf-profile-layout -->
|
||||
@@ -1193,6 +1549,7 @@ class WBF_Shortcodes {
|
||||
<?php return ob_get_clean();
|
||||
}
|
||||
|
||||
|
||||
// ── TAG PAGE ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static function view_tag() {
|
||||
@@ -1274,8 +1631,8 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php self::render_forum_footer(); ?>
|
||||
</div>
|
||||
<?php self::render_auth_modal(); ?>
|
||||
</div>
|
||||
<?php return ob_get_clean();
|
||||
@@ -1355,8 +1712,8 @@ class WBF_Shortcodes {
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php self::render_forum_footer(); ?>
|
||||
</div>
|
||||
<?php self::render_auth_modal(); ?>
|
||||
<?php self::render_dm_compose_modal(); ?>
|
||||
</div>
|
||||
@@ -1399,7 +1756,7 @@ class WBF_Shortcodes {
|
||||
if ($maint_s === '1' && (!$cur_s || WBF_Roles::level($cur_s->role) < 50)) return self::view_maintenance();
|
||||
$query = sanitize_text_field($_GET['q'] ?? '');
|
||||
$current = WBF_Auth::get_current_user();
|
||||
$results = mb_strlen($query) >= 2 ? WBF_DB::search($query, 40) : [];
|
||||
$results = mb_strlen($query) >= 2 ? WBF_DB::search($query, 40, $current) : [];
|
||||
ob_start(); ?>
|
||||
<div class="wbf-wrap">
|
||||
<?php self::render_topbar($current); ?>
|
||||
@@ -1443,8 +1800,8 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
<p style="color:var(--c-muted);font-size:.82rem;margin-top:1rem"><?php echo count($results); ?> Ergebnis(se) gefunden.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php self::render_forum_footer(); ?>
|
||||
</div>
|
||||
<?php self::render_auth_modal(); ?>
|
||||
</div>
|
||||
<?php return ob_get_clean();
|
||||
@@ -1502,7 +1859,7 @@ class WBF_Shortcodes {
|
||||
<?php echo esc_html($current->display_name); ?>
|
||||
<?php echo self::role_badge($current->role); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url(wbf_get_forum_url() . '?wbf_do_logout=1'); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
|
||||
<a href="<?php echo esc_url(wp_nonce_url(wbf_get_forum_url() . '?wbf_do_logout=1', 'wbf_logout')); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
|
||||
<?php else: ?>
|
||||
<button class="wbf-btn wbf-btn--sm" id="wbfOpenLogin"><?php echo esc_html(wbf_get_settings()['btn_login']); ?></button>
|
||||
<button class="wbf-btn wbf-btn--sm wbf-btn--primary" id="wbfOpenRegister"><?php echo esc_html(wbf_get_settings()['btn_register']); ?></button>
|
||||
@@ -2080,8 +2437,8 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
<?php self::render_forum_footer(); ?>
|
||||
</div>
|
||||
<?php self::render_auth_modal(); ?>
|
||||
</div>
|
||||
<?php return ob_get_clean();
|
||||
@@ -2120,7 +2477,7 @@ class WBF_Shortcodes {
|
||||
if ( ( wbf_get_settings()['rules_enabled'] ?? '1' ) !== '1' ) return;
|
||||
$rules_url = esc_url( wbf_get_forum_url() . '?forum_rules=1' );
|
||||
?>
|
||||
<div style="border-top:1px solid var(--c-border);margin-top:3rem;padding:1.25rem 1.5rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem;background:var(--c-bg2)">
|
||||
<div style="border:1px solid var(--c-border);border-radius:var(--radius);margin-top:2rem;padding:1rem 1.25rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem;background:var(--c-surface)">
|
||||
<span style="font-size:.78rem;color:var(--c-muted)">
|
||||
<i class="fas fa-shield-halved" style="color:var(--c-primary);margin-right:.35rem"></i>
|
||||
Durch die Nutzung des Forums stimmst du unseren Regeln zu.
|
||||
|
||||
@@ -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',
|
||||
@@ -45,10 +48,12 @@ $options = [
|
||||
'wbf_level_config',
|
||||
'wbf_levels_enabled',
|
||||
'wbf_profile_fields',
|
||||
'wbf_profile_field_cats',
|
||||
'wbf_reactions',
|
||||
'wbf_forum_page_id',
|
||||
'wbf_superadmin_email',
|
||||
'wbf_db_version',
|
||||
'wbf_word_filter',
|
||||
];
|
||||
|
||||
foreach ( $options as $option ) {
|
||||
@@ -65,8 +70,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 +81,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' );
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
* Plugin Name: WP Business Forum
|
||||
* Plugin URI: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum
|
||||
* Description: Professionelles Forum mit eigenem Login, Rollen, Signaturen, Hierarchie und Moderations-Tools.
|
||||
* Version: 1.0.1
|
||||
* Version: 1.0.3
|
||||
* Author: M_Viper
|
||||
* Author URI: https://m-viper.de
|
||||
* Text Domain: wp-business-forum
|
||||
* Requires PHP: 7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
define( 'WBF_PATH', plugin_dir_path( __FILE__ ) );
|
||||
define( 'WBF_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'WBF_VERSION', '1.0.1' );
|
||||
define( 'WBF_VERSION', '1.0.2' );
|
||||
|
||||
require_once WBF_PATH . 'includes/class-forum-db.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-roles.php';
|
||||
@@ -22,6 +23,7 @@ require_once WBF_PATH . 'includes/class-forum-bbcode.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-auth.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-shortcodes.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-ajax.php';
|
||||
require_once WBF_PATH . 'includes/class-forum-export.php';
|
||||
require_once WBF_PATH . 'admin/forum-admin.php';
|
||||
require_once WBF_PATH . 'admin/forum-settings.php';
|
||||
require_once WBF_PATH . 'admin/forum-setup.php';
|
||||
@@ -33,6 +35,38 @@ register_activation_hook( __FILE__, function() {
|
||||
set_transient( 'wbf_activation_redirect', true, 30 );
|
||||
});
|
||||
|
||||
// ── Export / Import Hooks ─────────────────────────────────────────────────────
|
||||
add_action( 'plugins_loaded', function() {
|
||||
WBF_Export::hooks();
|
||||
}, 5 );
|
||||
|
||||
// ── DB-Schema sicherstellen (läuft bei jedem Seitenaufruf, sehr günstig) ─────
|
||||
// Stellt sicher dass neue Spalten auch auf bestehenden Installs vorhanden sind,
|
||||
// ohne dass das Plugin erneut deaktiviert/aktiviert werden muss.
|
||||
add_action( 'plugins_loaded', function() {
|
||||
$db_ver = (int) get_option( 'wbf_db_version', 0 );
|
||||
if ( $db_ver < 2 ) {
|
||||
global $wpdb;
|
||||
// profile_public: Sicherheits-kritisch — muss immer existieren
|
||||
$cols = $wpdb->get_col( "DESCRIBE {$wpdb->prefix}forum_users" );
|
||||
if ( ! in_array( 'profile_public', $cols ) ) {
|
||||
$wpdb->query( "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) NOT NULL DEFAULT 1" );
|
||||
// Alle bestehenden User explizit auf öffentlich setzen
|
||||
$wpdb->query( "UPDATE {$wpdb->prefix}forum_users SET profile_public = 1 WHERE profile_public IS NULL" );
|
||||
}
|
||||
update_option( 'wbf_db_version', 2 );
|
||||
}
|
||||
}, 10 );
|
||||
|
||||
// ── Session frühzeitig starten (PHP 8.3 Fix) ────────────────────────────────
|
||||
// session_start() MUSS vor jedem HTML-Output laufen.
|
||||
// plugins_loaded (Prio 1) ist der früheste sichere Zeitpunkt in WordPress.
|
||||
// Der 'init'-Hook (in class-forum-auth.php) läuft als Fallback weiterhin,
|
||||
// aber dieser frühe Aufruf verhindert den PHP 8.3 E_WARNING "headers already sent".
|
||||
add_action( 'plugins_loaded', function() {
|
||||
WBF_Auth::init();
|
||||
}, 1 );
|
||||
|
||||
// ── Superadmin-Sync ───────────────────────────────────────────────────────────
|
||||
add_action( 'wp_login', function() { WBF_Roles::sync_superadmin(); } );
|
||||
add_action( 'init', function() { WBF_Roles::sync_superadmin(); } );
|
||||
@@ -57,6 +91,7 @@ if ( ! wp_next_scheduled( 'wbf_check_expired_bans' ) ) {
|
||||
|
||||
register_deactivation_hook( __FILE__, function() {
|
||||
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
|
||||
wp_clear_scheduled_hook( 'wbf_check_for_updates' );
|
||||
} );
|
||||
|
||||
|
||||
@@ -99,3 +134,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;
|
||||
} );
|
||||
Reference in New Issue
Block a user