diff --git a/includes/class-forum-ajax.php b/includes/class-forum-ajax.php
new file mode 100644
index 0000000..4126b8e
--- /dev/null
+++ b/includes/class-forum-ajax.php
@@ -0,0 +1,833 @@
+'Sicherheitsfehler.']);
+ exit;
+ }
+ // Update last_active for online status tracking
+ if ( WBF_Auth::is_forum_logged_in() ) {
+ $u = WBF_Auth::get_current_user();
+ if ($u) WBF_DB::touch_last_active($u->id);
+ }
+ }
+
+ // ── Auth ──────────────────────────────────────────────────────────────────
+
+ public static function handle_login() {
+ // Login braucht keinen Nonce — Credentials sind die Authentifizierung
+ $result = WBF_Auth::login(
+ sanitize_text_field($_POST['username'] ?? ''),
+ $_POST['password'] ?? ''
+ );
+ if ($result['success']) {
+ $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 {
+ wp_send_json_error($result);
+ }
+ }
+
+ public static function handle_register() {
+ // Register braucht keinen Nonce
+ $result = WBF_Auth::register(
+ sanitize_text_field($_POST['username'] ?? ''),
+ sanitize_email( $_POST['email'] ?? ''),
+ $_POST['password'] ?? '',
+ sanitize_text_field($_POST['display_name'] ?? '')
+ );
+ if ($result['success']) {
+ $u = $result['user'];
+ wp_send_json_success(['display_name'=>$u->display_name,'avatar_url'=>$u->avatar_url,'user_id'=>$u->id]);
+ } else {
+ wp_send_json_error($result);
+ }
+ }
+
+ public static function handle_logout() {
+ // Kein Nonce-Check für Logout nötig — Session-Clearing ist sicher
+ WBF_Auth::logout();
+ wp_send_json_success(['message' => 'logged_out']);
+ }
+
+ // ── Threads ───────────────────────────────────────────────────────────────
+
+ public static function handle_new_thread() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+ if (!WBF_DB::can($user, 'create_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+
+ $title = sanitize_text_field($_POST['title'] ?? '');
+ $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' );
+ $category_id = (int)($_POST['category_id'] ?? 0);
+
+ if (strlen($title) < 5) wp_send_json_error(['message'=>'Titel zu kurz (min. 5 Zeichen).']);
+ if (strlen($content) < 10) wp_send_json_error(['message'=>'Inhalt zu kurz (min. 10 Zeichen).']);
+ if (!$category_id) wp_send_json_error(['message'=>'Keine Kategorie gewählt.']);
+
+ $cat = WBF_DB::get_category($category_id);
+ if (!$cat || !WBF_DB::can_post_in($user, $cat)) wp_send_json_error(['message'=>'Keine Berechtigung für diese Kategorie.']);
+
+ $id = WBF_DB::create_thread([
+ 'category_id' => $category_id,
+ 'user_id' => $user->id,
+ 'title' => $title,
+ 'slug' => sanitize_title($title) . '-' . time(),
+ 'content' => $content,
+ ]);
+
+ // Tags speichern
+ $raw_tags = sanitize_text_field( $_POST['tags'] ?? '' );
+ if ( $raw_tags ) {
+ WBF_DB::sync_thread_tags( $id, $raw_tags );
+ }
+
+ wp_send_json_success(['thread_id'=>$id,'message'=>'Thread erstellt!']);
+ }
+
+ // ── Posts ─────────────────────────────────────────────────────────────────
+
+ public static function handle_new_post() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+ if (!WBF_DB::can($user, 'post')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+
+ $thread_id = (int)($_POST['thread_id'] ?? 0);
+ $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' );
+
+ if (strlen($content) < 3) wp_send_json_error(['message'=>'Antwort zu kurz.']);
+ if (!$thread_id) wp_send_json_error(['message'=>'Ungültiger Thread.']);
+
+ $thread = WBF_DB::get_thread($thread_id);
+ if (!$thread || $thread->status === 'closed') wp_send_json_error(['message'=>'Thread ist geschlossen.']);
+ if ($thread->status === 'archived') wp_send_json_error(['message'=>'Thread ist archiviert.']);
+
+ $id = WBF_DB::create_post(['thread_id'=>$thread_id,'user_id'=>$user->id,'content'=>$content]);
+
+ // Benachrichtigungen: Thread-Ersteller + alle bisherigen Antwortenden
+ $notif_users = WBF_DB::get_thread_participants($thread_id);
+ foreach ($notif_users as $participant_id) {
+ WBF_DB::create_notification($participant_id, 'reply', $thread_id, $user->id);
+ // E-Mail
+ $notif_user = WBF_DB::get_user($participant_id);
+ self::send_notification_email($notif_user, 'reply', $user->display_name, [
+ 'thread_id' => $thread_id,
+ 'thread_title' => $thread->title,
+ ]);
+ }
+ // @Erwähnungen
+ $mentioned = WBF_DB::extract_mentions($content);
+ foreach ($mentioned as $m_user) {
+ if ((int)$m_user->id !== (int)$user->id) {
+ WBF_DB::create_notification($m_user->id, 'mention', $thread_id, $user->id);
+ // E-Mail
+ self::send_notification_email($m_user, 'mention', $user->display_name, [
+ 'thread_id' => $thread_id,
+ 'thread_title' => $thread->title,
+ ]);
+ }
+ }
+
+ // Direkt den neuen Post laden — nicht alle Posts fetchen
+ global $wpdb;
+ $new_post = $wpdb->get_row( $wpdb->prepare(
+ "SELECT p.*, u.display_name, u.avatar_url, u.username, u.signature,
+ u.post_count as author_posts, u.role as author_role, u.registered as author_registered
+ FROM {$wpdb->prefix}forum_posts p
+ JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
+ WHERE p.id = %d", $id
+ ) );
+
+ ob_start();
+ WBF_Shortcodes::render_single_post($new_post, $user);
+ $html = ob_get_clean();
+
+ wp_send_json_success(['html'=>$html,'post_id'=>$id]);
+ }
+
+ // ── Mod Actions ───────────────────────────────────────────────────────────
+
+ public static function handle_mod_action() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+
+ $action = sanitize_key($_POST['mod_action'] ?? '');
+ $object_id = (int)($_POST['object_id'] ?? 0);
+
+ switch ($action) {
+
+ case 'pin_thread':
+ case 'unpin_thread':
+ if (!WBF_DB::can($user,'pin_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+ WBF_DB::update_thread($object_id, ['pinned' => $action === 'pin_thread' ? 1 : 0]);
+ wp_send_json_success(['action'=>$action,'message'=>$action==='pin_thread'?'Gepinnt!':'Entpinnt!']);
+ break;
+
+ case 'close_thread':
+ case 'open_thread':
+ if (!WBF_DB::can($user,'close_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+ WBF_DB::update_thread($object_id, ['status' => $action === 'close_thread' ? 'closed' : 'open']);
+ wp_send_json_success(['action'=>$action,'message'=>$action==='close_thread'?'Thread geschlossen.':'Thread geöffnet.']);
+ break;
+
+ case 'delete_thread':
+ if (!WBF_DB::can($user,'delete_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+ $thread = WBF_DB::get_thread($object_id);
+ if (!$thread) wp_send_json_error(['message'=>'Thread nicht gefunden.']);
+ WBF_DB::delete_thread($object_id);
+ wp_send_json_success(['action'=>'deleted','redirect'=>'?forum_cat='.urlencode('')]);
+ break;
+
+ case 'delete_post':
+ if (!WBF_DB::can($user,'delete_post')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+ WBF_DB::delete_post($object_id);
+ wp_send_json_success(['action'=>'post_deleted']);
+ break;
+
+ case 'archive_thread':
+ case 'unarchive_thread':
+ if (!WBF_DB::can($user,'close_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+ $thread = WBF_DB::get_thread($object_id);
+ if (!$thread) wp_send_json_error(['message'=>'Thread nicht gefunden.']);
+ if ($action === 'archive_thread') {
+ if ($thread->status !== 'archived') {
+ global $wpdb;
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d",
+ $thread->category_id
+ ));
+ }
+ WBF_DB::update_thread($object_id, ['status' => 'archived']);
+ wp_send_json_success(['action'=>'archived','message'=>'Thread archiviert.']);
+ } else {
+ WBF_DB::update_thread($object_id, ['status' => 'open']);
+ global $wpdb;
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET thread_count=thread_count+1 WHERE id=%d",
+ $thread->category_id
+ ));
+ wp_send_json_success(['action'=>'unarchived','message'=>'Thread wiederhergestellt.']);
+ }
+ break;
+
+ default:
+ wp_send_json_error(['message'=>'Unbekannte Aktion.']);
+ }
+ }
+
+ // ── Likes ─────────────────────────────────────────────────────────────────
+
+ public static function handle_toggle_like() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Bitte einloggen um zu liken.']);
+ if (!WBF_DB::can($user,'like')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+
+ $object_id = (int)($_POST['object_id'] ?? 0);
+ $type = sanitize_key($_POST['object_type'] ?? 'post');
+ if (!in_array($type, ['thread','post'])) wp_send_json_error(['message'=>'Ungültiger Typ.']);
+
+ $action = WBF_DB::toggle_like($user->id, $object_id, $type);
+ $count = WBF_DB::get_like_count($object_id, $type);
+ wp_send_json_success(['action'=>$action,'count'=>$count]);
+ }
+
+ // ── Profile ───────────────────────────────────────────────────────────────
+
+ public static function handle_update_profile() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+
+ $update = [];
+ $display_name = sanitize_text_field($_POST['display_name'] ?? '');
+ $bio = sanitize_textarea_field($_POST['bio'] ?? '');
+ $signature = sanitize_textarea_field($_POST['signature'] ?? '');
+
+ if (!empty($display_name)) $update['display_name'] = $display_name;
+ $update['bio'] = $bio;
+ $update['signature'] = mb_substr($signature, 0, 300); // max 300 Zeichen
+
+ if (!empty($_POST['new_password'])) {
+ if (strlen($_POST['new_password']) < 6) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']);
+ $update['password'] = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
+ }
+
+ WBF_DB::update_user($user->id, $update);
+ wp_send_json_success(['message'=>'Profil gespeichert!']);
+ }
+
+ public static function handle_upload_avatar() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+ 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.']);
+
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+
+ $id = media_handle_upload('avatar', 0);
+ if (is_wp_error($id)) wp_send_json_error(['message'=>$id->get_error_message()]);
+
+ $url = wp_get_attachment_url($id);
+ WBF_DB::update_user($user->id, ['avatar_url'=>$url]);
+ wp_send_json_success(['avatar_url'=>$url]);
+ }
+
+ // ── Report ────────────────────────────────────────────────────────────────
+
+ public static function handle_report_post() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Bitte einloggen um zu melden.']);
+ if (!WBF_Roles::can($user, 'post')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+
+ $object_id = (int)($_POST['object_id'] ?? 0);
+ $type = sanitize_key($_POST['object_type'] ?? 'post');
+ $reason = sanitize_text_field($_POST['reason'] ?? '');
+ $note = sanitize_textarea_field($_POST['note'] ?? '');
+
+ if (!$object_id) wp_send_json_error(['message'=>'Ungültiges Objekt.']);
+ if (!in_array($type, ['post','thread'])) wp_send_json_error(['message'=>'Ungültiger Typ.']);
+ if (empty($reason)) wp_send_json_error(['message'=>'Bitte einen Grund angeben.']);
+ if (WBF_DB::has_reported($user->id, $object_id, $type))
+ wp_send_json_error(['message'=>'Du hast diesen Beitrag bereits gemeldet.']);
+
+ WBF_DB::create_report([
+ 'object_id' => $object_id,
+ 'object_type' => $type,
+ 'reporter_id' => $user->id,
+ 'reason' => $reason,
+ 'note' => mb_substr($note, 0, 500),
+ 'status' => 'open',
+ ]);
+
+ wp_send_json_success(['message'=>'Beitrag wurde gemeldet. Danke!']);
+ }
+ // ── Post-Bild Upload ──────────────────────────────────────────────────────
+
+ public static function handle_upload_post_image() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
+ if ( ! WBF_Roles::can($user, 'post') ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
+ if ( empty($_FILES['image']) ) wp_send_json_error(['message' => 'Keine Datei empfangen.']);
+
+ // 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
+ if ( $_FILES['image']['size'] > 5 * 1024 * 1024 ) {
+ wp_send_json_error(['message' => 'Maximale Dateigröße: 5 MB.']);
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+
+ // $_FILES-Schlüssel auf 'image' umbenennen → media_handle_upload erwartet den Feldnamen
+ $attachment_id = media_handle_upload('image', 0);
+ if ( is_wp_error($attachment_id) ) {
+ wp_send_json_error(['message' => $attachment_id->get_error_message()]);
+ }
+
+ $url = wp_get_attachment_url($attachment_id);
+ wp_send_json_success(['url' => $url]);
+ }
+ // ── Post bearbeiten ───────────────────────────────────────────────────────
+
+ public static function handle_edit_post() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
+
+ $post_id = (int)( $_POST['post_id'] ?? 0 );
+ $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' );
+
+ if ( ! $post_id ) wp_send_json_error(['message' => 'Ungültiger Beitrag.']);
+ if ( strlen($content) < 3 ) wp_send_json_error(['message' => 'Inhalt zu kurz.']);
+
+ global $wpdb;
+ $db_post = $wpdb->get_row( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_posts WHERE id=%d", $post_id
+ ) );
+ if ( ! $db_post ) wp_send_json_error(['message' => 'Beitrag nicht gefunden.']);
+
+ $is_own = (int)$db_post->user_id === (int)$user->id;
+ $is_mod = WBF_DB::can( $user, 'delete_post' );
+
+ if ( ! $is_own && ! $is_mod ) {
+ wp_send_json_error(['message' => 'Keine Berechtigung.']);
+ }
+
+ $wpdb->update(
+ "{$wpdb->prefix}forum_posts",
+ ['content' => $content, 'updated_at' => current_time('mysql')],
+ ['id' => $post_id]
+ );
+
+ wp_send_json_success(['message' => 'Beitrag aktualisiert!', 'content' => WBF_BBCode::render($content)]);
+ }
+
+ // ── Suche ─────────────────────────────────────────────────────────────────
+
+ public static function handle_search() {
+ 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 );
+ wp_send_json_success(['results' => $results, 'query' => $query]);
+ }
+
+ // ── Benachrichtigungen ────────────────────────────────────────────────────
+
+ public static function handle_get_notifications() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
+ $notifs = WBF_DB::get_notifications( $user->id, 20 );
+ $unread = WBF_DB::count_unread_notifications( $user->id );
+ wp_send_json_success(['notifications' => $notifs, 'unread' => $unread]);
+ }
+
+ public static function handle_mark_notifications_read() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
+ WBF_DB::mark_notifications_read( $user->id );
+ wp_send_json_success(['message' => 'Gelesen.']);
+ }
+
+ // ── Thread-Inhalt bearbeiten (OP) ────────────────────────────────────────
+
+ public static function handle_edit_thread() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
+
+ $thread_id = (int)( $_POST['thread_id'] ?? 0 );
+ $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' );
+ $title = sanitize_text_field( $_POST['title'] ?? '' );
+
+ if ( ! $thread_id ) wp_send_json_error(['message' => 'Ungültiger Thread.']);
+ if ( strlen($content) < 5 ) wp_send_json_error(['message' => 'Inhalt zu kurz.']);
+ if ( strlen($title) < 3 ) wp_send_json_error(['message' => 'Titel zu kurz (min. 3 Zeichen).']);
+
+ $thread = WBF_DB::get_thread($thread_id);
+ if ( ! $thread ) wp_send_json_error(['message' => 'Thread nicht gefunden.']);
+
+ $is_own = (int)$thread->user_id === (int)$user->id;
+ $is_mod = WBF_DB::can($user, 'delete_post');
+ if ( ! $is_own && ! $is_mod ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
+
+ global $wpdb;
+ $wpdb->update(
+ "{$wpdb->prefix}forum_threads",
+ [
+ 'title' => $title,
+ 'content' => $content,
+ 'updated_at' => current_time('mysql'),
+ ],
+ ['id' => $thread_id]
+ );
+
+ wp_send_json_success([
+ 'message' => 'Thread aktualisiert!',
+ 'title' => esc_html($title),
+ 'content' => WBF_BBCode::render($content),
+ ]);
+ }
+
+ // ── Thread verschieben ────────────────────────────────────────────────────
+
+ public static function handle_move_thread() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
+ if ( ! WBF_DB::can($user, 'manage_cats') ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
+
+ $thread_id = (int)( $_POST['thread_id'] ?? 0 );
+ $category_id = (int)( $_POST['category_id'] ?? 0 );
+
+ if ( ! $thread_id || ! $category_id ) wp_send_json_error(['message' => 'Ungültige Daten.']);
+
+ $thread = WBF_DB::get_thread($thread_id);
+ $new_cat = WBF_DB::get_category($category_id);
+ if ( ! $thread ) wp_send_json_error(['message' => 'Thread nicht gefunden.']);
+ if ( ! $new_cat ) wp_send_json_error(['message' => 'Kategorie nicht gefunden.']);
+ if ( (int)$thread->category_id === $category_id ) wp_send_json_error(['message' => 'Thread ist bereits in dieser Kategorie.']);
+
+ WBF_DB::move_thread($thread_id, $category_id);
+ wp_send_json_success([
+ 'message' => 'Thread verschoben nach: ' . $new_cat->name,
+ 'cat_name' => $new_cat->name,
+ 'cat_slug' => $new_cat->slug,
+ ]);
+ }
+
+ // ── Tag-Autocomplete ──────────────────────────────────────────────────────
+
+ public static function handle_tag_suggest() {
+ // Kein Nonce-Check nötig — nur lesend, öffentlich
+ $q = sanitize_text_field( $_POST['q'] ?? $_GET['q'] ?? '' );
+ if ( mb_strlen($q) < 1 ) wp_send_json_success(['tags' => []]);
+ $tags = WBF_DB::suggest_tags( $q, 8 );
+ wp_send_json_success(['tags' => $tags]);
+ }
+
+ // ── Reaktionen ────────────────────────────────────────────────────────────
+
+ public static function handle_set_reaction() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+ if (!WBF_DB::can($user,'like')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
+
+ $object_id = (int)($_POST['object_id'] ?? 0);
+ $object_type = sanitize_key($_POST['object_type'] ?? 'post');
+ $reaction = $_POST['reaction'] ?? '';
+
+ if (!$object_id || !in_array($object_type, ['post','thread'])) {
+ wp_send_json_error(['message'=>'Ungültige Daten.']);
+ }
+
+ $result = WBF_DB::set_reaction($user->id, $object_id, $object_type, $reaction);
+ if ($result === false) wp_send_json_error(['message'=>'Ungültige Reaktion.']);
+
+ $data = WBF_DB::get_reactions($object_id, $object_type, $user->id);
+ wp_send_json_success(['action'=>$result,'counts'=>$data['counts'],'mine'=>$data['mine']]);
+ }
+
+ // ── Private Nachrichten ───────────────────────────────────────────────────
+
+ public static function handle_send_message() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+ if (WBF_Roles::level($user->role) < 0) wp_send_json_error(['message'=>'Kein Zugriff.']);
+
+ $to_id = (int)($_POST['to_id'] ?? 0);
+ $content = sanitize_textarea_field($_POST['content'] ?? '');
+
+ if (!$to_id) wp_send_json_error(['message'=>'Empfänger fehlt.']);
+ if (mb_strlen($content) < 1) wp_send_json_error(['message'=>'Nachricht leer.']);
+ 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.']);
+
+ $id = WBF_DB::send_message($user->id, $to_id, $content);
+ // Notify recipient
+ WBF_DB::create_notification($to_id, 'message', $id, $user->id);
+ // E-Mail
+ $to_user = WBF_DB::get_user($to_id);
+ self::send_notification_email($to_user, 'message', $user->display_name);
+
+ wp_send_json_success(['message_id'=>$id,'message'=>'Gesendet!',
+ 'content'=>esc_html($content),
+ 'created_at'=>current_time('mysql'),
+ 'sender_name'=>$user->display_name,
+ 'sender_avatar'=>$user->avatar_url,
+ ]);
+ }
+
+ public static function handle_get_inbox() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+ $inbox = WBF_DB::get_inbox($user->id);
+ $unread = WBF_DB::count_unread_messages($user->id);
+ wp_send_json_success(['inbox'=>$inbox,'unread'=>$unread]);
+ }
+
+ public static function handle_get_conversation() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+ $partner_id = (int)($_POST['partner_id'] ?? 0);
+ if (!$partner_id) wp_send_json_error(['message'=>'Ungültig.']);
+ WBF_DB::mark_messages_read($user->id, $partner_id);
+ $total = WBF_DB::count_conversation($user->id, $partner_id);
+ $msgs = WBF_DB::get_conversation($user->id, $partner_id, 50, max(0, $total - 50));
+ $partner = WBF_DB::get_user($partner_id);
+ wp_send_json_success(['messages'=>$msgs,'partner'=>$partner,'my_id'=>$user->id,'total'=>$total]);
+ }
+
+ public static function handle_mark_messages_read() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+ $partner_id = (int)($_POST['partner_id'] ?? 0);
+ if ($partner_id) WBF_DB::mark_messages_read($user->id, $partner_id);
+ wp_send_json_success();
+ }
+
+ // ── Online-Status ─────────────────────────────────────────────────────────
+
+ public static function handle_get_online_users() {
+ // Public — kein Login nötig
+ $users = WBF_DB::get_online_users(15);
+ wp_send_json_success(['users' => $users]);
+ }
+
+ // ── User-Autocomplete (für @Erwähnungen + DM) ─────────────────────────────
+
+ public static function handle_user_suggest() {
+ $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) . '%';
+ $users = $wpdb->get_results($wpdb->prepare(
+ "SELECT id, username, display_name, avatar_url, role
+ FROM {$wpdb->prefix}forum_users
+ WHERE username LIKE %s OR display_name LIKE %s
+ ORDER BY display_name ASC LIMIT 8",
+ $like, $like
+ ));
+ wp_send_json_success(['users'=>$users]);
+ }
+
+ // ── Nachricht löschen ─────────────────────────────────────────────────────
+
+ public static function handle_delete_message() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
+
+ $msg_id = (int)( $_POST['message_id'] ?? 0 );
+ if ( ! $msg_id ) wp_send_json_error(['message' => 'Ungültige Nachricht.']);
+
+ global $wpdb;
+ $msg = $wpdb->get_row( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_messages WHERE id = %d", $msg_id
+ ));
+ if ( ! $msg ) wp_send_json_error(['message' => 'Nachricht nicht gefunden.']);
+
+ $uid = (int) $user->id;
+ if ( (int)$msg->from_id === $uid ) {
+ $wpdb->update( "{$wpdb->prefix}forum_messages", ['deleted_by_sender' => 1], ['id' => $msg_id] );
+ } elseif ( (int)$msg->to_id === $uid ) {
+ $wpdb->update( "{$wpdb->prefix}forum_messages", ['deleted_by_receiver' => 1], ['id' => $msg_id] );
+ } else {
+ wp_send_json_error(['message' => 'Keine Berechtigung.']);
+ }
+
+ wp_send_json_success(['message_id' => $msg_id]);
+ }
+
+ // ── Neue Nachrichten seit ID (Live-Polling) ───────────────────────────────
+
+ public static function handle_get_new_messages() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
+
+ $partner_id = (int)( $_POST['partner_id'] ?? 0 );
+ $since_id = (int)( $_POST['since_id'] ?? 0 );
+
+ if ( ! $partner_id ) wp_send_json_error(['message' => 'Ungueltig.']);
+
+ global $wpdb;
+ $msgs = $wpdb->get_results( $wpdb->prepare(
+ "SELECT m.*, u.display_name AS sender_name, u.avatar_url AS sender_avatar
+ FROM {$wpdb->prefix}forum_messages m
+ JOIN {$wpdb->prefix}forum_users u ON u.id = m.from_id
+ WHERE m.id > %d
+ AND ( (m.from_id=%d AND m.to_id=%d AND m.deleted_by_sender=0)
+ OR (m.from_id=%d AND m.to_id=%d AND m.deleted_by_receiver=0) )
+ ORDER BY m.created_at ASC
+ LIMIT 50",
+ $since_id,
+ (int)$user->id, $partner_id,
+ $partner_id, (int)$user->id
+ ));
+
+ wp_send_json_success(['messages' => $msgs]);
+ }
+
+
+ // ── E-Mail Benachrichtigungen ─────────────────────────────────────────────
+
+ private static function send_notification_email( $to_user, $type, $actor_name, $extra = [] ) {
+ if ( ! $to_user || empty($to_user->email) ) return;
+
+ $blog_name = get_bloginfo('name');
+ $forum_url = wbf_get_forum_url();
+ $from_email = get_option('admin_email');
+ $from_name = $blog_name . ' Forum';
+
+ $headers = [
+ 'Content-Type: text/html; charset=UTF-8',
+ "From: {$from_name} <{$from_email}>",
+ ];
+
+ switch ( $type ) {
+ case 'reply':
+ $thread_id = $extra['thread_id'] ?? 0;
+ $thread_title = $extra['thread_title'] ?? '';
+ $subject = "[{$blog_name}] {$actor_name} hat auf deinen Thread geantwortet";
+ $body = self::email_template(
+ $to_user->display_name,
+ "{$actor_name} hat in deinem Thread " . esc_html($thread_title) . " geantwortet.",
+ $forum_url . '?forum_thread=' . (int)$thread_id,
+ 'Thread ansehen',
+ $blog_name
+ );
+ break;
+
+ case 'mention':
+ $thread_id = $extra['thread_id'] ?? 0;
+ $thread_title = $extra['thread_title'] ?? '';
+ $subject = "[{$blog_name}] {$actor_name} hat dich erwähnt";
+ $body = self::email_template(
+ $to_user->display_name,
+ "{$actor_name} hat dich in einem Beitrag erwähnt: " . esc_html($thread_title) . "",
+ $forum_url . '?forum_thread=' . (int)$thread_id,
+ 'Beitrag ansehen',
+ $blog_name
+ );
+ break;
+
+ case 'message':
+ $subject = "[{$blog_name}] Neue Privatnachricht von {$actor_name}";
+ $body = self::email_template(
+ $to_user->display_name,
+ "{$actor_name} hat dir eine Privatnachricht gesendet.",
+ $forum_url . '?forum_dm=1',
+ 'Nachricht lesen',
+ $blog_name
+ );
+ break;
+
+ default:
+ return;
+ }
+
+ wp_mail( $to_user->email, $subject, $body, $headers );
+ }
+
+ private static function email_template( $name, $text, $url, $btn_label, $blog_name ) {
+ return "
+
+
+ 💬 {$blog_name}
+
+
+
+
Du erhältst diese E-Mail weil du im {$blog_name} Forum registriert bist.
+ Forum öffnen
+
+
";
+ }
+
+ // ── Passwort vergessen ────────────────────────────────────────────────────
+
+ public static function handle_forgot_password() {
+ // Kein Nonce nötig — nur E-Mail wird verarbeitet
+ $email = sanitize_email( $_POST['email'] ?? '' );
+ if ( ! is_email($email) ) wp_send_json_error(['message'=>'Ungültige E-Mail-Adresse.']);
+
+ $user = WBF_DB::get_user_by('email', $email);
+ // Immer Erfolg melden (kein User-Enumeration)
+ if ( ! $user ) {
+ wp_send_json_success(['message'=>'Falls diese E-Mail registriert ist, wurde eine E-Mail gesendet.']);
+ }
+
+ $token = WBF_DB::create_reset_token($user->id);
+ $forum_url = wbf_get_forum_url();
+ $reset_url = $forum_url . '?wbf_reset_token=' . urlencode($token);
+ $blog_name = get_bloginfo('name');
+
+ $headers = ['Content-Type: text/html; charset=UTF-8', 'From: ' . $blog_name . ' <' . get_option('admin_email') . '>'];
+ $body = self::email_template(
+ $user->display_name,
+ 'Du hast ein neues Passwort angefordert. Klicke den Button um dein Passwort zurückzusetzen. Der Link ist 1 Stunde gültig.',
+ $reset_url,
+ 'Passwort zurücksetzen',
+ $blog_name
+ );
+ wp_mail( $user->email, "[{$blog_name}] Passwort zurücksetzen", $body, $headers );
+
+ wp_send_json_success(['message'=>'E-Mail gesendet! Bitte prüfe deinen Posteingang.']);
+ }
+
+ public static function handle_reset_password() {
+ self::verify();
+ $token = sanitize_text_field( $_POST['token'] ?? '' );
+ $password = $_POST['password'] ?? '';
+ $password2= $_POST['password2'] ?? '';
+
+ if ( strlen($password) < 6 ) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']);
+ if ( $password !== $password2 ) wp_send_json_error(['message'=>'Passwörter stimmen nicht überein.']);
+
+ $ok = WBF_DB::use_reset_token($token, $password);
+ if ( ! $ok ) wp_send_json_error(['message'=>'Link ungültig oder abgelaufen.']);
+
+ wp_send_json_success(['message'=>'Passwort geändert! Du kannst dich jetzt einloggen.']);
+ }
+
+ // ── Ältere Nachrichten laden ──────────────────────────────────────────────
+
+ public static function handle_load_more_messages() {
+ self::verify();
+ $user = WBF_Auth::get_current_user();
+ if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
+
+ $partner_id = (int)($_POST['partner_id'] ?? 0);
+ $offset = (int)($_POST['offset'] ?? 0);
+ $per_page = 30;
+
+ if (!$partner_id) wp_send_json_error(['message'=>'Ungültig.']);
+
+ $total = WBF_DB::count_conversation($user->id, $partner_id);
+ $msgs = WBF_DB::get_conversation($user->id, $partner_id, $per_page, $offset);
+
+ wp_send_json_success([
+ 'messages' => $msgs,
+ 'my_id' => $user->id,
+ 'total' => $total,
+ 'offset' => $offset,
+ 'has_more' => ($offset + $per_page) < $total,
+ ]);
+ }
+
+
+}
+
+WBF_Ajax::init();
\ No newline at end of file
diff --git a/includes/class-forum-auth.php b/includes/class-forum-auth.php
new file mode 100644
index 0000000..2eb65df
--- /dev/null
+++ b/includes/class-forum-auth.php
@@ -0,0 +1,103 @@
+user_id );
+ if ( $user && WBF_Roles::level($user->role) >= 0 ) {
+ $_SESSION[ self::SESSION_KEY ] = $user->id;
+ WBF_DB::touch_last_active( $user->id );
+ }
+ }
+ }
+ }
+
+ public static function is_forum_logged_in() {
+ self::init();
+ return ! empty( $_SESSION[ self::SESSION_KEY ] );
+ }
+
+ public static function get_current_user() {
+ self::init();
+ if ( empty( $_SESSION[ self::SESSION_KEY ] ) ) return null;
+ return WBF_DB::get_user( (int) $_SESSION[ self::SESSION_KEY ] );
+ }
+
+ public static function login( $username_or_email, $password ) {
+ self::init();
+ $user = WBF_DB::get_user_by( 'username', $username_or_email );
+ if ( ! $user ) {
+ $user = WBF_DB::get_user_by( 'email', $username_or_email );
+ }
+ if ( ! $user ) return array( 'success' => false, 'message' => 'Benutzer nicht gefunden.' );
+ if ( ! password_verify( $password, $user->password ) ) {
+ return array( 'success' => false, 'message' => 'Falsches Passwort.' );
+ }
+ if ( WBF_Roles::level($user->role) < 0 ) {
+ $reason = !empty($user->ban_reason) ? $user->ban_reason : 'Dein Konto wurde gesperrt.';
+ return array( 'success' => false, 'banned' => true, 'message' => $reason );
+ }
+ $_SESSION[ self::SESSION_KEY ] = $user->id;
+ WBF_DB::touch_last_active( $user->id );
+ return array( 'success' => true, 'user' => $user );
+ }
+
+ public static function register( $username, $email, $password, $display_name ) {
+ self::init();
+ $username = sanitize_user( $username );
+ $email = sanitize_email( $email );
+ $display_name = sanitize_text_field( $display_name );
+
+ if ( strlen($username) < 3 ) return array('success'=>false,'message'=>'Benutzername mindestens 3 Zeichen.');
+ if ( ! is_email($email) ) return array('success'=>false,'message'=>'Ungültige E-Mail-Adresse.');
+ if ( strlen($password) < 6 ) return array('success'=>false,'message'=>'Passwort mindestens 6 Zeichen.');
+ if ( empty($display_name) ) return array('success'=>false,'message'=>'Anzeigename darf nicht leer sein.');
+
+ if ( WBF_DB::get_user_by('username', $username) ) return array('success'=>false,'message'=>'Benutzername bereits vergeben.');
+ if ( WBF_DB::get_user_by('email', $email) ) return array('success'=>false,'message'=>'E-Mail bereits registriert.');
+
+ $avatar = 'https://www.gravatar.com/avatar/' . md5( strtolower($email) ) . '?d=identicon&s=80';
+
+ $id = WBF_DB::create_user( array(
+ 'username' => $username,
+ 'email' => $email,
+ 'password' => password_hash( $password, PASSWORD_DEFAULT ),
+ 'display_name' => $display_name,
+ 'avatar_url' => $avatar,
+ ));
+
+ $_SESSION[ self::SESSION_KEY ] = $id;
+ return array('success'=>true,'user'=>WBF_DB::get_user($id));
+ }
+
+ public static function logout() {
+ self::init();
+ $user_id = $_SESSION[ self::SESSION_KEY ] ?? 0;
+ unset( $_SESSION[ self::SESSION_KEY ] );
+ if ( $user_id ) {
+ WBF_DB::delete_remember_token( (int)$user_id );
+ }
+ // Remove cookie
+ if ( isset($_COOKIE['wbf_remember']) ) {
+ setcookie( 'wbf_remember', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
+ }
+ }
+
+ /** Remember-Me Token setzen und Cookie senden */
+ public static function set_remember_cookie( $user_id ) {
+ $token = WBF_DB::create_remember_token( (int)$user_id );
+ setcookie( 'wbf_remember', $token, time() + 30 * DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
+ }
+}
+
+add_action('init', array('WBF_Auth','init'), 1);
\ No newline at end of file
diff --git a/includes/class-forum-bbcode.php b/includes/class-forum-bbcode.php
new file mode 100644
index 0000000..acdaa9b
--- /dev/null
+++ b/includes/class-forum-bbcode.php
@@ -0,0 +1,249 @@
+'
+ . $m[1] // bereits html-escaped durch htmlspecialchars oben
+ . '';
+ return $key;
+ },
+ $out
+ );
+
+ // Inline-Code
+ $out = preg_replace_callback(
+ '/\[icode\](.*?)\[\/icode\]/is',
+ function ( $m ) use ( &$placeholders ) {
+ $key = '%%ICODE_' . count($placeholders) . '%%';
+ $placeholders[$key] = '' . $m[1] . '';
+ return $key;
+ },
+ $out
+ );
+
+ // 4. Alle anderen Tags parsen
+ $out = self::parse( $out );
+
+ // 5. Zeilenumbrüche zu
(nur außerhalb von Block-Tags)
+ $out = self::nl_to_br( $out );
+
+ // 6. Code-Blöcke wieder einsetzen
+ foreach ( $placeholders as $key => $html ) {
+ $out = str_replace( $key, $html, $out );
+ }
+
+ return $out;
+ }
+
+ /**
+ * Sanitize bei Speicherung: nur HTML streifen, BBCode-Tags bleiben
+ */
+ public static function sanitize( $raw ) {
+ // Alle echten HTML-Tags entfernen, BBCode-Tags [xxx] bleiben erhalten
+ return strip_tags( $raw );
+ }
+
+ // ── Interner Parser ──────────────────────────────────────────────────────
+
+ private static function parse( $s ) {
+
+ // [b] [i] [u] [s]
+ $s = preg_replace( '/\[b\](.*?)\[\/b\]/is', '$1', $s );
+ $s = preg_replace( '/\[i\](.*?)\[\/i\]/is', '$1', $s );
+ $s = preg_replace( '/\[u\](.*?)\[\/u\]/is', '$1', $s );
+ $s = preg_replace( '/\[s\](.*?)\[\/s\]/is', '$1', $s );
+
+ // [h2] [h3]
+ $s = preg_replace( '/\[h2\](.*?)\[\/h2\]/is', '$1
', $s );
+ $s = preg_replace( '/\[h3\](.*?)\[\/h3\]/is', '$1
', $s );
+
+ // [center] [right]
+ $s = preg_replace( '/\[center\](.*?)\[\/center\]/is', '$1
', $s );
+ $s = preg_replace( '/\[right\](.*?)\[\/right\]/is', '$1
', $s );
+
+ // [hr]
+ $s = str_replace( '[hr]', '
', $s );
+
+ // [color=...]
+ $s = preg_replace_callback(
+ '/\[color=([a-zA-Z0-9#]{1,20})\](.*?)\[\/color\]/is',
+ function ( $m ) {
+ $color = $m[1];
+ // Hex-Farben direkt erlauben, benannte aus Whitelist
+ if ( preg_match( '/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $color ) ) {
+ $safe = esc_attr( $color );
+ } elseif ( in_array( strtolower($color), self::$allowed_colors ) ) {
+ $safe = esc_attr( strtolower($color) );
+ } else {
+ return $m[2]; // Unbekannte Farbe → nur Text
+ }
+ return '' . $m[2] . '';
+ },
+ $s
+ );
+
+ // [size=small|large|xlarge]
+ $s = preg_replace_callback(
+ '/\[size=(small|large|xlarge)\](.*?)\[\/size\]/is',
+ function ( $m ) {
+ $map = [ 'small' => '.8em', 'large' => '1.2em', 'xlarge' => '1.5em' ];
+ return '' . $m[2] . '';
+ },
+ $s
+ );
+
+ // [url=...] und [url]...[/url]
+ $s = preg_replace_callback(
+ '/\[url=([^\]]{1,500})\](.*?)\[\/url\]/is',
+ function ( $m ) {
+ $href = esc_url( $m[1] );
+ if ( ! $href ) return $m[2];
+ return '' . $m[2] . '';
+ },
+ $s
+ );
+ $s = preg_replace_callback(
+ '/\[url\](https?:\/\/[^\[]{1,500})\[\/url\]/is',
+ function ( $m ) {
+ $href = esc_url( $m[1] );
+ if ( ! $href ) return $m[1];
+ return '' . $href . '';
+ },
+ $s
+ );
+
+ // [img]
+ $s = preg_replace_callback(
+ '/\[img\](https?:\/\/[^\[]{1,1000})\[\/img\]/is',
+ function ( $m ) {
+ $src = esc_url( $m[1] );
+ if ( ! $src ) return '';
+ return '
';
+ },
+ $s
+ );
+
+ // [quote] und [quote=Name]
+ $s = preg_replace_callback(
+ '/\[quote=([^\]]{1,80})\](.*?)\[\/quote\]/is',
+ function ( $m ) {
+ $author = '' . htmlspecialchars( $m[1], ENT_QUOTES ) . ' schrieb:';
+ return ''
+ . $author . '' . $m[2] . '
';
+ },
+ $s
+ );
+ $s = preg_replace(
+ '/\[quote\](.*?)\[\/quote\]/is',
+ '$1
',
+ $s
+ );
+
+ // [spoiler] und [spoiler=Titel]
+ static $spoiler_id = 0;
+ $s = preg_replace_callback(
+ '/\[spoiler=([^\]]{0,80})\](.*?)\[\/spoiler\]/is',
+ function ( $m ) use ( &$spoiler_id ) {
+ $spoiler_id++;
+ $title = htmlspecialchars( $m[1] ?: 'Spoiler', ENT_QUOTES );
+ return ''
+ . '
'
+ . '
' . $m[2] . '
'
+ . '
';
+ },
+ $s
+ );
+ $s = preg_replace_callback(
+ '/\[spoiler\](.*?)\[\/spoiler\]/is',
+ function ( $m ) use ( &$spoiler_id ) {
+ $spoiler_id++;
+ return ''
+ . '
'
+ . '
' . $m[1] . '
'
+ . '
';
+ },
+ $s
+ );
+
+ // [list] [list=1] [*]
+ $s = preg_replace_callback(
+ '/\[list(=1)?\](.*?)\[\/list\]/is',
+ function ( $m ) {
+ $tag = $m[1] ? 'ol' : 'ul';
+ $items = preg_replace( '/\[\*\]\s*/s', '', $m[2] );
+ // Auto-close li tags
+ $items = preg_replace( '/()(.*?)(?=|$)/s', '$1$2', $items );
+ return '<' . $tag . ' class="wbf-bb-list">' . trim($items) . '' . $tag . '>';
+ },
+ $s
+ );
+
+ // @Erwähnungen → klickbare Profil-Links
+ $s = preg_replace_callback(
+ '/@([a-zA-Z0-9_]{3,60})\b/',
+ function( $m ) {
+ global $wpdb;
+ $user = $wpdb->get_row( $wpdb->prepare(
+ "SELECT id, username FROM {$wpdb->prefix}forum_users WHERE username=%s", $m[1]
+ ) );
+ if ( ! $user ) return esc_html($m[0]);
+ return '@' . esc_html($user->username) . '';
+ },
+ $s
+ );
+
+ return $s;
+ }
+
+ // Zeilenumbrüche →
, aber nicht innerhalb von Block-Elementen
+ private static function nl_to_br( $s ) {
+ // Einfaches nl2br — ausreichend da Block-Tags (, , etc.)
+ // bereits eigene Zeilenumbrüche erzeugen
+ return nl2br( $s );
+ }
+}
\ No newline at end of file
diff --git a/includes/class-forum-db.php b/includes/class-forum-db.php
new file mode 100644
index 0000000..00c1a9a
--- /dev/null
+++ b/includes/class-forum-db.php
@@ -0,0 +1,1137 @@
+get_charset_collate();
+
+ $sql_users = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_users (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ username VARCHAR(60) NOT NULL UNIQUE,
+ email VARCHAR(100) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ display_name VARCHAR(100) NOT NULL,
+ avatar_url VARCHAR(255) DEFAULT '',
+ bio TEXT DEFAULT '',
+ signature TEXT DEFAULT '',
+ role VARCHAR(20) DEFAULT 'member',
+ post_count INT DEFAULT 0,
+ registered DATETIME DEFAULT CURRENT_TIMESTAMP,
+ last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id)
+ ) $charset;";
+
+ $sql_cats = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_categories (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ parent_id BIGINT UNSIGNED DEFAULT 0,
+ name VARCHAR(100) NOT NULL,
+ slug VARCHAR(100) NOT NULL UNIQUE,
+ description TEXT DEFAULT '',
+ icon VARCHAR(50) DEFAULT 'fas fa-comments',
+ sort_order INT DEFAULT 0,
+ thread_count INT DEFAULT 0,
+ post_count INT DEFAULT 0,
+ min_role VARCHAR(20) DEFAULT 'member',
+ PRIMARY KEY (id),
+ KEY parent_id (parent_id)
+ ) $charset;";
+
+ $sql_threads = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_threads (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ category_id BIGINT UNSIGNED NOT NULL,
+ user_id BIGINT UNSIGNED NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ slug VARCHAR(255) NOT NULL,
+ content LONGTEXT NOT NULL,
+ status VARCHAR(20) DEFAULT 'open',
+ pinned TINYINT(1) DEFAULT 0,
+ views INT DEFAULT 0,
+ reply_count INT DEFAULT 0,
+ like_count INT DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ last_reply_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY category_id (category_id)
+ ) $charset;";
+
+ $sql_posts = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_posts (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ thread_id BIGINT UNSIGNED NOT NULL,
+ user_id BIGINT UNSIGNED NOT NULL,
+ content LONGTEXT NOT NULL,
+ like_count INT DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY thread_id (thread_id)
+ ) $charset;";
+
+ $sql_likes = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_likes (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT UNSIGNED NOT NULL,
+ object_id BIGINT UNSIGNED NOT NULL,
+ object_type VARCHAR(20) NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY unique_like (user_id, object_id, object_type)
+ ) $charset;";
+
+ $sql_reports = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_reports (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ object_id BIGINT UNSIGNED NOT NULL,
+ object_type VARCHAR(20) NOT NULL DEFAULT 'post',
+ reporter_id BIGINT UNSIGNED NOT NULL,
+ reason VARCHAR(100) NOT NULL DEFAULT '',
+ note TEXT DEFAULT '',
+ status VARCHAR(20) NOT NULL DEFAULT 'open',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY object_id (object_id),
+ KEY status (status)
+ ) $charset;";
+
+ $sql_tags = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_tags (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ name VARCHAR(60) NOT NULL,
+ slug VARCHAR(60) NOT NULL UNIQUE,
+ use_count INT DEFAULT 0,
+ PRIMARY KEY (id),
+ KEY slug (slug)
+ ) $charset;";
+
+ $sql_thread_tags = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_thread_tags (
+ thread_id BIGINT UNSIGNED NOT NULL,
+ tag_id BIGINT UNSIGNED NOT NULL,
+ PRIMARY KEY (thread_id, tag_id),
+ KEY tag_id (tag_id)
+ ) $charset;";
+
+ $sql_messages = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_messages (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ from_id BIGINT UNSIGNED NOT NULL,
+ to_id BIGINT UNSIGNED NOT NULL,
+ content TEXT NOT NULL,
+ is_read TINYINT(1) DEFAULT 0,
+ deleted_by_sender TINYINT(1) DEFAULT 0,
+ deleted_by_receiver TINYINT(1) DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY from_id (from_id),
+ KEY to_id (to_id)
+ ) $charset;";
+
+ $sql_reactions = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_reactions (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT UNSIGNED NOT NULL,
+ object_id BIGINT UNSIGNED NOT NULL,
+ object_type VARCHAR(20) NOT NULL DEFAULT 'post',
+ reaction VARCHAR(10) NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY unique_reaction (user_id, object_id, object_type),
+ KEY object_id (object_id)
+ ) $charset;";
+
+ $sql_remember = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_remember_tokens (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT UNSIGNED NOT NULL,
+ token VARCHAR(64) NOT NULL,
+ expires_at DATETIME NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY token (token),
+ KEY user_id (user_id)
+ ) $charset;";
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+ dbDelta( $sql_users );
+ dbDelta( $sql_cats );
+ dbDelta( $sql_threads );
+ dbDelta( $sql_posts );
+ dbDelta( $sql_likes );
+ dbDelta( $sql_reports );
+ dbDelta( $sql_tags );
+ dbDelta( $sql_thread_tags );
+ dbDelta( $sql_messages );
+ dbDelta( $sql_reactions );
+ dbDelta( $sql_remember );
+
+ // Live upgrades — add new columns to existing installs
+ self::maybe_add_column("{$wpdb->prefix}forum_users", 'signature', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN signature TEXT DEFAULT '' AFTER bio");
+ self::maybe_add_column("{$wpdb->prefix}forum_users", 'ban_reason', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN ban_reason TEXT DEFAULT '' AFTER role");
+ self::maybe_add_column("{$wpdb->prefix}forum_categories", 'parent_id', "ALTER TABLE {$wpdb->prefix}forum_categories ADD COLUMN parent_id BIGINT UNSIGNED DEFAULT 0 AFTER id");
+ self::maybe_add_column("{$wpdb->prefix}forum_categories", 'min_role', "ALTER TABLE {$wpdb->prefix}forum_categories ADD COLUMN min_role VARCHAR(20) DEFAULT 'member' AFTER post_count");
+ self::maybe_add_column("{$wpdb->prefix}forum_users", 'reset_token', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN reset_token VARCHAR(64) DEFAULT NULL");
+ self::maybe_add_column("{$wpdb->prefix}forum_users", 'reset_token_expires', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN reset_token_expires DATETIME DEFAULT NULL");
+ $sql_notifications = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_notifications (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT UNSIGNED NOT NULL,
+ type VARCHAR(30) NOT NULL DEFAULT 'reply',
+ object_id BIGINT UNSIGNED NOT NULL,
+ actor_id BIGINT UNSIGNED NOT NULL,
+ is_read TINYINT(1) DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY user_id (user_id),
+ KEY is_read (is_read)
+ ) $charset;";
+
+ // Ensure reports + notifications tables exist on existing installs
+ dbDelta( $sql_reports );
+ dbDelta( $sql_notifications );
+
+ // Default categories
+ $count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_categories");
+ if ( (int)$count === 0 ) {
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>0,'name'=>'Allgemein', 'slug'=>'allgemein', 'description'=>'Allgemeine Diskussionen','icon'=>'fas fa-home', 'sort_order'=>1]);
+ $p1 = $wpdb->insert_id;
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>0,'name'=>'Ankündigungen', 'slug'=>'ankuendigungen','description'=>'Wichtige Neuigkeiten', 'icon'=>'fas fa-bullhorn', 'sort_order'=>2,'min_role'=>'moderator']);
+ $p2 = $wpdb->insert_id;
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>0,'name'=>'Support', 'slug'=>'support', 'description'=>'Hilfe & Fragen', 'icon'=>'fas fa-life-ring','sort_order'=>3]);
+ $p3 = $wpdb->insert_id;
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>$p1,'name'=>'Introductions', 'slug'=>'introductions', 'description'=>'Stell dich vor!', 'icon'=>'fas fa-user', 'sort_order'=>1]);
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>$p1,'name'=>'Off-Topic', 'slug'=>'off-topic', 'description'=>'Alles außerhalb des Themas', 'icon'=>'fas fa-coffee', 'sort_order'=>2]);
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>$p3,'name'=>'Bug Reports', 'slug'=>'bug-reports', 'description'=>'Fehler melden', 'icon'=>'fas fa-bug', 'sort_order'=>1]);
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>$p3,'name'=>'Feature Requests','slug'=>'feature-requests','description'=>'Neue Funktionen vorschlagen','icon'=>'fas fa-lightbulb','sort_order'=>2]);
+ }
+ }
+
+ private static function maybe_add_column( $table, $column, $sql ) {
+ global $wpdb;
+ $cols = $wpdb->get_col("DESCRIBE {$table}");
+ if ( ! in_array($column, $cols) ) {
+ $wpdb->query($sql);
+ }
+ }
+
+ // ── Rollen — delegiert an WBF_Roles ──────────────────────────────────────
+
+ public static function role_level( $role ) { return WBF_Roles::level($role); }
+ public static function all_roles() { return WBF_Roles::labels(); }
+ public static function can( $user, $action ){ return WBF_Roles::can($user, $action); }
+ public static function can_post_in( $user, $cat ) { return WBF_Roles::can_post_in($user, $cat); }
+
+ // ── Users ─────────────────────────────────────────────────────────────────
+
+ public static function get_user( $id ) {
+ global $wpdb;
+ return $wpdb->get_row( $wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_users WHERE id=%d", $id) );
+ }
+
+ public static function get_user_by( $field, $value ) {
+ global $wpdb;
+ $field = sanitize_key($field);
+ // Benutzername & E-Mail: Groß-/Kleinschreibung ignorieren (LOWER)
+ if ( in_array($field, ['username', 'email']) ) {
+ return $wpdb->get_row( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_users WHERE LOWER($field)=LOWER(%s)",
+ $value
+ ) );
+ }
+ return $wpdb->get_row( $wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_users WHERE $field=%s", $value) );
+ }
+
+ public static function create_user( $data ) {
+ global $wpdb;
+ $wpdb->insert("{$wpdb->prefix}forum_users", $data);
+ return $wpdb->insert_id;
+ }
+
+ public static function update_user( $id, $data ) {
+ global $wpdb;
+ $wpdb->update("{$wpdb->prefix}forum_users", $data, ['id' => $id]);
+ }
+
+ public static function get_all_users( $limit = 100, $offset = 0 ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_users ORDER BY registered DESC LIMIT %d OFFSET %d",
+ $limit, $offset
+ ));
+ }
+
+ // ── Categories ────────────────────────────────────────────────────────────
+
+ public static function get_categories_tree() {
+ global $wpdb;
+ $all = $wpdb->get_results(
+ "SELECT c.*,
+ (SELECT t.title FROM {$wpdb->prefix}forum_threads t WHERE t.category_id=c.id ORDER BY t.last_reply_at DESC LIMIT 1) as last_thread_title,
+ (SELECT t.id FROM {$wpdb->prefix}forum_threads t WHERE t.category_id=c.id ORDER BY t.last_reply_at DESC LIMIT 1) as last_thread_id,
+ (SELECT u.display_name FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_users u ON u.id=t.user_id
+ WHERE t.category_id=c.id ORDER BY t.last_reply_at DESC LIMIT 1) as last_post_author
+ FROM {$wpdb->prefix}forum_categories c
+ ORDER BY c.parent_id ASC, c.sort_order ASC"
+ );
+ $by_id = [];
+ foreach ($all as $cat) { $cat->children = []; $by_id[$cat->id] = $cat; }
+ $tree = [];
+ foreach ($by_id as $id => $cat) {
+ if ((int)$cat->parent_id === 0) $tree[] = &$by_id[$id];
+ elseif (isset($by_id[$cat->parent_id])) $by_id[$cat->parent_id]->children[] = &$by_id[$id];
+ }
+ return $tree;
+ }
+
+ public static function get_categories_flat() {
+ global $wpdb;
+ return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}forum_categories ORDER BY parent_id ASC, sort_order ASC");
+ }
+
+ public static function get_category( $id_or_slug ) {
+ global $wpdb;
+ if (is_numeric($id_or_slug)) return $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_categories WHERE id=%d", $id_or_slug));
+ return $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_categories WHERE slug=%s", $id_or_slug));
+ }
+
+ public static function get_category_breadcrumb( $cat ) {
+ $path = [$cat]; $max = 5;
+ while ((int)$cat->parent_id > 0 && $max--) {
+ $cat = self::get_category((int)$cat->parent_id);
+ if (!$cat) break;
+ array_unshift($path, $cat);
+ }
+ return $path;
+ }
+
+ public static function get_child_categories( $parent_id ) {
+ global $wpdb;
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_categories WHERE parent_id=%d ORDER BY sort_order ASC", $parent_id
+ ));
+ }
+
+ // ── Threads ───────────────────────────────────────────────────────────────
+
+ public static function get_threads( $category_id, $page = 1, $per_page = 20, $include_archived = false ) {
+ global $wpdb;
+ $offset = ($page - 1) * $per_page;
+ $status_sql = $include_archived ? '' : "AND t.status != 'archived'";
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT t.*, u.display_name, u.avatar_url, u.username, u.role as author_role
+ FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
+ WHERE t.category_id = %d $status_sql
+ ORDER BY t.pinned DESC, t.last_reply_at DESC
+ LIMIT %d OFFSET %d",
+ $category_id, $per_page, $offset
+ ));
+ }
+
+ public static function get_archived_threads( $category_id = 0, $page = 1, $per_page = 20 ) {
+ global $wpdb;
+ $offset = ($page - 1) * $per_page;
+ $cat_sql = $category_id ? $wpdb->prepare('AND t.category_id = %d', $category_id) : '';
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT t.*, u.display_name, u.avatar_url, u.username, u.role as author_role,
+ c.name as cat_name, c.slug as cat_slug
+ 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.status = 'archived' $cat_sql
+ ORDER BY t.last_reply_at DESC
+ LIMIT %d OFFSET %d",
+ $per_page, $offset
+ ));
+ }
+
+ public static function count_archived_threads( $category_id = 0 ) {
+ global $wpdb;
+ if ( $category_id ) {
+ return (int)$wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status='archived' AND category_id=%d", $category_id
+ ));
+ }
+ return (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status='archived'");
+ }
+
+ public static function count_threads( $category_id ) {
+ global $wpdb;
+ return (int)$wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE category_id=%d AND status != 'archived'",
+ $category_id
+ ));
+ }
+
+ public static function move_thread( $thread_id, $new_category_id ) {
+ global $wpdb;
+ $thread = self::get_thread($thread_id);
+ if ( ! $thread ) return false;
+ $old_cat = (int) $thread->category_id;
+ $new_cat = (int) $new_category_id;
+ if ( $old_cat === $new_cat ) return false;
+
+ $wpdb->update( "{$wpdb->prefix}forum_threads", ['category_id' => $new_cat], ['id' => $thread_id] );
+ // Adjust thread counts (don't count archived)
+ if ( $thread->status !== 'archived' ) {
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d", $old_cat
+ ));
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET thread_count=thread_count+1 WHERE id=%d", $new_cat
+ ));
+ }
+ // 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
+ ));
+ if ( $post_count > 0 ) {
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET post_count=GREATEST(post_count-%d,0) WHERE id=%d", $post_count, $old_cat
+ ));
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET post_count=post_count+%d WHERE id=%d", $post_count, $new_cat
+ ));
+ }
+ return true;
+ }
+
+ public static function get_thread( $id ) {
+ global $wpdb;
+ return $wpdb->get_row($wpdb->prepare(
+ "SELECT t.*, u.display_name, u.avatar_url, u.username, u.signature,
+ u.post_count as author_posts, u.registered as author_registered, u.role as author_role
+ FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
+ WHERE t.id = %d", $id
+ ));
+ }
+
+ public static function create_thread( $data ) {
+ global $wpdb;
+ $wpdb->insert("{$wpdb->prefix}forum_threads", $data);
+ $id = $wpdb->insert_id;
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=thread_count+1 WHERE id=%d", $data['category_id']));
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=post_count+1 WHERE id=%d", $data['user_id']));
+ return $id;
+ }
+
+ public static function delete_thread( $id ) {
+ global $wpdb;
+ $thread = self::get_thread($id);
+ if (!$thread) return;
+ // Clean up tag associations and decrement use_counts
+ $tag_ids = $wpdb->get_col( $wpdb->prepare(
+ "SELECT tag_id FROM {$wpdb->prefix}forum_thread_tags WHERE thread_id=%d", $id
+ ) );
+ $wpdb->delete( "{$wpdb->prefix}forum_thread_tags", ['thread_id' => $id] );
+ if ( $tag_ids ) {
+ foreach ( $tag_ids as $tid ) {
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_tags SET use_count=GREATEST(use_count-1,0) WHERE id=%d", (int)$tid
+ ) );
+ }
+ }
+ $wpdb->delete("{$wpdb->prefix}forum_posts", ['thread_id' => $id]);
+ $wpdb->delete("{$wpdb->prefix}forum_threads", ['id' => $id]);
+ if ( $thread->status !== 'archived' ) {
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d", $thread->category_id));
+ }
+ }
+
+ public static function update_thread( $id, $data ) {
+ global $wpdb;
+ $wpdb->update("{$wpdb->prefix}forum_threads", $data, ['id' => $id]);
+ }
+
+ // ── Posts ─────────────────────────────────────────────────────────────────
+
+ public static function get_posts( $thread_id, $page = 1, $per_page = 15 ) {
+ global $wpdb;
+ $offset = ($page - 1) * $per_page;
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT p.*, u.display_name, u.avatar_url, u.username, u.signature,
+ u.post_count as author_posts, u.role as author_role, u.registered as author_registered
+ FROM {$wpdb->prefix}forum_posts p
+ JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
+ WHERE p.thread_id = %d
+ ORDER BY p.created_at ASC
+ LIMIT %d OFFSET %d",
+ $thread_id, $per_page, $offset
+ ));
+ }
+
+ 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));
+ }
+
+ public static function create_post( $data ) {
+ global $wpdb;
+ $wpdb->insert("{$wpdb->prefix}forum_posts", $data);
+ $id = $wpdb->insert_id;
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET reply_count=reply_count+1, last_reply_at=NOW() WHERE id=%d", $data['thread_id']));
+ $thread = $wpdb->get_row($wpdb->prepare("SELECT category_id FROM {$wpdb->prefix}forum_threads WHERE id=%d", $data['thread_id']));
+ if ($thread) $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET post_count=post_count+1 WHERE id=%d", $thread->category_id));
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=post_count+1 WHERE id=%d", $data['user_id']));
+ return $id;
+ }
+
+ public static function delete_post( $id ) {
+ global $wpdb;
+ $post = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_posts WHERE id=%d", $id));
+ if (!$post) return;
+ $wpdb->delete("{$wpdb->prefix}forum_posts", ['id' => $id]);
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET reply_count=GREATEST(reply_count-1,0) WHERE id=%d", $post->thread_id));
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=GREATEST(post_count-1,0) WHERE id=%d", $post->user_id));
+ }
+
+ // ── Likes ─────────────────────────────────────────────────────────────────
+
+ public static function has_liked( $user_id, $object_id, $type ) {
+ global $wpdb;
+ return (bool)$wpdb->get_var($wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_likes WHERE user_id=%d AND object_id=%d AND object_type=%s",
+ $user_id, $object_id, $type
+ ));
+ }
+
+ public static function toggle_like( $user_id, $object_id, $type ) {
+ global $wpdb;
+ $map = ['thread'=>"{$wpdb->prefix}forum_threads",'post'=>"{$wpdb->prefix}forum_posts"];
+ if (self::has_liked($user_id, $object_id, $type)) {
+ $wpdb->delete("{$wpdb->prefix}forum_likes", ['user_id'=>$user_id,'object_id'=>$object_id,'object_type'=>$type]);
+ if (isset($map[$type])) $wpdb->query($wpdb->prepare("UPDATE {$map[$type]} SET like_count=GREATEST(like_count-1,0) WHERE id=%d",$object_id));
+ return 'unliked';
+ } else {
+ $wpdb->insert("{$wpdb->prefix}forum_likes", ['user_id'=>$user_id,'object_id'=>$object_id,'object_type'=>$type]);
+ if (isset($map[$type])) $wpdb->query($wpdb->prepare("UPDATE {$map[$type]} SET like_count=like_count+1 WHERE id=%d",$object_id));
+ return 'liked';
+ }
+ }
+
+ public static function get_like_count( $object_id, $type ) {
+ global $wpdb;
+ $map = ['thread'=>"{$wpdb->prefix}forum_threads",'post'=>"{$wpdb->prefix}forum_posts"];
+ if (!isset($map[$type])) return 0;
+ return (int)$wpdb->get_var($wpdb->prepare("SELECT like_count FROM {$map[$type]} WHERE id=%d",$object_id));
+ }
+
+ // ── Stats ─────────────────────────────────────────────────────────────────
+
+ public static function get_recent_threads( $limit = 5 ) {
+ global $wpdb;
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT t.id, t.title, t.created_at, u.display_name, c.name as cat_name, c.slug as cat_slug
+ 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
+ ));
+ }
+
+ 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"),
+ '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"),
+ ];
+ }
+
+ // ── Reports ───────────────────────────────────────────────────────────────
+
+ public static function create_report( $data ) {
+ global $wpdb;
+ $wpdb->insert("{$wpdb->prefix}forum_reports", $data);
+ return $wpdb->insert_id;
+ }
+
+ public static function has_reported( $reporter_id, $object_id, $type = 'post' ) {
+ global $wpdb;
+ return (bool)$wpdb->get_var($wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_reports WHERE reporter_id=%d AND object_id=%d AND object_type=%s AND status='open'",
+ $reporter_id, $object_id, $type
+ ));
+ }
+
+ public static function get_reports( $status = 'open', $limit = 50 ) {
+ global $wpdb;
+ $sql = "SELECT r.*,
+ rep.display_name AS reporter_name, rep.username AS reporter_username,
+ p.content AS post_content, p.thread_id AS thread_id,
+ t.title AS thread_title
+ FROM {$wpdb->prefix}forum_reports r
+ LEFT JOIN {$wpdb->prefix}forum_users rep ON rep.id = r.reporter_id
+ LEFT JOIN {$wpdb->prefix}forum_posts p ON p.id = r.object_id AND r.object_type = 'post'
+ LEFT JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id";
+ if ( $status !== 'all' ) {
+ $sql .= $wpdb->prepare( " WHERE r.status = %s", $status );
+ }
+ $sql .= $wpdb->prepare( " ORDER BY r.created_at DESC LIMIT %d", $limit );
+ return $wpdb->get_results( $sql );
+ }
+
+ public static function update_report( $id, $status ) {
+ global $wpdb;
+ $wpdb->update("{$wpdb->prefix}forum_reports", ['status' => $status], ['id' => $id]);
+ }
+
+ public static function get_user_posts( $user_id, $limit = 30 ) {
+ global $wpdb;
+ // UNION: Thread-Erstbeiträge (forum_threads) + Antworten (forum_posts)
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT 'thread' AS entry_type,
+ t.id AS id, t.content, t.created_at, t.like_count,
+ t.id AS thread_id, t.title AS thread_title,
+ c.name AS cat_name
+ FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ WHERE t.user_id = %d
+ UNION ALL
+ SELECT 'post' AS entry_type,
+ p.id AS id, p.content, p.created_at, p.like_count,
+ t.id AS thread_id, t.title AS thread_title,
+ c.name AS cat_name
+ FROM {$wpdb->prefix}forum_posts p
+ JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ WHERE p.user_id = %d
+ ORDER BY created_at DESC
+ LIMIT %d",
+ $user_id, $user_id, $limit
+ ) );
+ }
+
+ // ── Thread-Teilnehmer (für Benachrichtigungen) ───────────────────────────
+
+ public static function get_thread_participants( $thread_id ) {
+ global $wpdb;
+ // Thread-Ersteller + alle die geantwortet haben (ohne Duplikate)
+ return $wpdb->get_col( $wpdb->prepare(
+ "SELECT DISTINCT user_id FROM (
+ SELECT user_id FROM {$wpdb->prefix}forum_threads WHERE id = %d
+ UNION
+ SELECT user_id FROM {$wpdb->prefix}forum_posts WHERE thread_id = %d
+ ) AS participants",
+ $thread_id, $thread_id
+ ) );
+ }
+
+ // ── Suche ─────────────────────────────────────────────────────────────────
+
+ public static function search( $query, $limit = 30 ) {
+ global $wpdb;
+ $like = '%' . $wpdb->esc_like( $query ) . '%';
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT 'thread' AS result_type,
+ t.id, t.title, t.content, t.created_at, t.reply_count,
+ u.display_name, u.avatar_url, u.role AS author_role,
+ c.name AS cat_name, c.slug AS cat_slug
+ 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'
+ UNION ALL
+ SELECT 'post' AS result_type,
+ p.id, t.title, p.content, p.created_at, 0 AS reply_count,
+ u.display_name, u.avatar_url, u.role AS author_role,
+ c.name AS cat_name, c.slug AS cat_slug
+ FROM {$wpdb->prefix}forum_posts p
+ 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'
+ ORDER BY created_at DESC
+ LIMIT %d",
+ $like, $like, $like, $limit
+ ) );
+ }
+
+ // ── Benachrichtigungen ────────────────────────────────────────────────────
+
+ public static function create_notification( $user_id, $type, $object_id, $actor_id ) {
+ global $wpdb;
+ // Keine doppelten ungelesenen Benachrichtigungen
+ $exists = $wpdb->get_var( $wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_notifications
+ WHERE user_id=%d AND type=%s AND object_id=%d AND actor_id=%d AND is_read=0",
+ $user_id, $type, $object_id, $actor_id
+ ) );
+ if ( $exists ) return;
+ // Nicht sich selbst benachrichtigen
+ if ( (int)$user_id === (int)$actor_id ) return;
+ $wpdb->insert( "{$wpdb->prefix}forum_notifications", [
+ 'user_id' => $user_id,
+ 'type' => $type,
+ 'object_id' => $object_id,
+ 'actor_id' => $actor_id,
+ ] );
+ }
+
+ public static function get_notifications( $user_id, $limit = 20 ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT n.*,
+ u.display_name AS actor_name, u.avatar_url AS actor_avatar,
+ t.title AS thread_title, t.id AS thread_id
+ FROM {$wpdb->prefix}forum_notifications n
+ JOIN {$wpdb->prefix}forum_users u ON u.id = n.actor_id
+ LEFT JOIN {$wpdb->prefix}forum_threads t ON t.id = n.object_id
+ WHERE n.user_id = %d
+ ORDER BY n.created_at DESC
+ LIMIT %d",
+ $user_id, $limit
+ ) );
+ }
+
+ public static function count_unread_notifications( $user_id ) {
+ global $wpdb;
+ $table = "{$wpdb->prefix}forum_notifications";
+ if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return 0;
+ return (int) $wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_notifications WHERE user_id=%d AND is_read=0",
+ $user_id
+ ) );
+ }
+
+ public static function mark_notifications_read( $user_id ) {
+ global $wpdb;
+ $wpdb->update(
+ "{$wpdb->prefix}forum_notifications",
+ ['is_read' => 1],
+ ['user_id' => $user_id, 'is_read' => 0]
+ );
+ }
+
+ public static function count_open_reports() {
+ global $wpdb;
+ // Tabelle existiert evtl. noch nicht auf bestehenden Installs → erst prüfen
+ $table = "{$wpdb->prefix}forum_reports";
+ if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) {
+ return 0;
+ }
+ return (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status='open'");
+ }
+
+ // ── Tags ──────────────────────────────────────────────────────────────────
+
+ /**
+ * Tags für einen Thread speichern.
+ * $raw_tags = komma- oder leerzeichen-getrennte Zeichenkette, z.B. "php wordpress #cms"
+ */
+ public static function sync_thread_tags( $thread_id, $raw_tags ) {
+ global $wpdb;
+ $thread_id = (int) $thread_id;
+
+ // Bestehende Verknüpfungen löschen
+ $old_ids = $wpdb->get_col( $wpdb->prepare(
+ "SELECT tag_id FROM {$wpdb->prefix}forum_thread_tags WHERE thread_id=%d", $thread_id
+ ) );
+ $wpdb->delete( "{$wpdb->prefix}forum_thread_tags", ['thread_id' => $thread_id] );
+ // use_count für entfernte Tags dekrementieren
+ if ( $old_ids ) {
+ $placeholders = implode(',', array_fill(0, count($old_ids), '%d'));
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_tags SET use_count = GREATEST(use_count-1,0) WHERE id IN ($placeholders)",
+ ...$old_ids
+ ) );
+ }
+
+ // Tags parsen
+ $names = self::parse_tag_string( $raw_tags );
+ if ( empty($names) ) return;
+
+ foreach ( $names as $name ) {
+ $slug = sanitize_title( $name );
+ if ( ! $slug ) continue;
+
+ // Upsert Tag
+ $tag = $wpdb->get_row( $wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_tags WHERE slug=%s", $slug
+ ) );
+ if ( $tag ) {
+ $tag_id = (int) $tag->id;
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_tags SET use_count=use_count+1 WHERE id=%d", $tag_id
+ ) );
+ } else {
+ $wpdb->insert( "{$wpdb->prefix}forum_tags", [
+ 'name' => $name,
+ 'slug' => $slug,
+ 'use_count' => 1,
+ ] );
+ $tag_id = $wpdb->insert_id;
+ }
+
+ // Pivot
+ $wpdb->replace( "{$wpdb->prefix}forum_thread_tags", [
+ 'thread_id' => $thread_id,
+ 'tag_id' => $tag_id,
+ ] );
+ }
+ }
+
+ public static function parse_tag_string( $raw ) {
+ // Strip # prefix, split by comma / space / semicolon
+ $raw = strip_tags( $raw );
+ $raw = str_replace('#', '', $raw);
+ $parts = preg_split('/[\s,;]+/', $raw, -1, PREG_SPLIT_NO_EMPTY);
+ $names = [];
+ foreach ( $parts as $p ) {
+ $p = mb_strtolower( trim($p) );
+ if ( mb_strlen($p) >= 2 && mb_strlen($p) <= 30 ) {
+ $names[] = $p;
+ }
+ }
+ return array_unique( array_slice($names, 0, 10) ); // max 10 Tags pro Thread
+ }
+
+ /** Tags eines Threads laden */
+ public static function get_thread_tags( $thread_id ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT t.* FROM {$wpdb->prefix}forum_tags t
+ INNER JOIN {$wpdb->prefix}forum_thread_tags tt ON tt.tag_id = t.id
+ WHERE tt.thread_id = %d
+ ORDER BY t.name ASC",
+ $thread_id
+ ) );
+ }
+
+ /** Threads nach Tag-Slug laden */
+ public static function get_threads_by_tag( $slug, $page = 1, $per_page = 20 ) {
+ global $wpdb;
+ $offset = ($page - 1) * $per_page;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT t.*, u.display_name, u.avatar_url, u.role AS author_role,
+ c.name AS cat_name, c.slug AS cat_slug
+ FROM {$wpdb->prefix}forum_threads t
+ INNER JOIN {$wpdb->prefix}forum_thread_tags tt ON tt.thread_id = t.id
+ INNER JOIN {$wpdb->prefix}forum_tags tg ON tg.id = tt.tag_id
+ JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ WHERE tg.slug = %s AND t.status != 'archived'
+ ORDER BY t.last_reply_at DESC
+ LIMIT %d OFFSET %d",
+ $slug, $per_page, $offset
+ ) );
+ }
+
+ public static function count_threads_by_tag( $slug ) {
+ global $wpdb;
+ return (int) $wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads t
+ INNER JOIN {$wpdb->prefix}forum_thread_tags tt ON tt.thread_id = t.id
+ INNER JOIN {$wpdb->prefix}forum_tags tg ON tg.id = tt.tag_id
+ WHERE tg.slug = %s AND t.status != 'archived'",
+ $slug
+ ) );
+ }
+
+ /** Tag-Objekt per Slug */
+ public static function get_tag( $slug ) {
+ global $wpdb;
+ return $wpdb->get_row( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_tags WHERE slug=%s", $slug
+ ) );
+ }
+
+ /** Top-Tags nach Nutzungshäufigkeit */
+ public static function get_popular_tags( $limit = 30 ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_tags WHERE use_count > 0 ORDER BY use_count DESC LIMIT %d",
+ $limit
+ ) );
+ }
+
+ /** Autocomplete: Tags die mit $q beginnen */
+ public static function suggest_tags( $q, $limit = 8 ) {
+ global $wpdb;
+ $like = $wpdb->esc_like( strtolower($q) ) . '%';
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT name, slug, use_count FROM {$wpdb->prefix}forum_tags WHERE slug LIKE %s ORDER BY use_count DESC LIMIT %d",
+ $like, $limit
+ ) );
+ }
+
+
+ // ── Online-Status ──────────────────────────────────────────────────────────
+
+ /** Letztes Aktivitätsdatum des Users aktualisieren */
+ public static function touch_last_active( $user_id ) {
+ global $wpdb;
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_users SET last_active=NOW() WHERE id=%d", (int)$user_id
+ ) );
+ }
+
+ /** User die in den letzten $minutes Minuten aktiv waren */
+ public static function get_online_users( $minutes = 15 ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT id, username, display_name, avatar_url, role
+ FROM {$wpdb->prefix}forum_users
+ WHERE last_active >= DATE_SUB(NOW(), INTERVAL %d MINUTE)
+ ORDER BY last_active DESC
+ LIMIT 50",
+ $minutes
+ ) );
+ }
+
+ public static function is_online( $user_id, $minutes = 15 ) {
+ global $wpdb;
+ return (bool)$wpdb->get_var( $wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_users
+ WHERE id=%d AND last_active >= DATE_SUB(NOW(), INTERVAL %d MINUTE)",
+ $user_id, $minutes
+ ) );
+ }
+
+ // ── Reaktionen ────────────────────────────────────────────────────────────
+
+ public static function set_reaction( $user_id, $object_id, $object_type, $reaction ) {
+ global $wpdb;
+ $allowed = ['👍','❤️','😂','😮','😢','😡'];
+ if ( ! in_array($reaction, $allowed, true) ) return false;
+
+ $existing = $wpdb->get_row( $wpdb->prepare(
+ "SELECT reaction FROM {$wpdb->prefix}forum_reactions
+ WHERE user_id=%d AND object_id=%d AND object_type=%s",
+ $user_id, $object_id, $object_type
+ ) );
+
+ if ( $existing && $existing->reaction === $reaction ) {
+ // Same reaction → remove (toggle off)
+ $wpdb->delete( "{$wpdb->prefix}forum_reactions", [
+ 'user_id' => $user_id, 'object_id' => $object_id, 'object_type' => $object_type
+ ] );
+ return 'removed';
+ }
+ // Insert or replace
+ $wpdb->replace( "{$wpdb->prefix}forum_reactions", [
+ 'user_id' => $user_id,
+ 'object_id' => $object_id,
+ 'object_type' => $object_type,
+ 'reaction' => $reaction,
+ ] );
+ return 'added';
+ }
+
+ /** Reaktionen für ein Objekt — [emoji => count] + user's own reaction */
+ public static function get_reactions( $object_id, $object_type, $user_id = 0 ) {
+ global $wpdb;
+ $rows = $wpdb->get_results( $wpdb->prepare(
+ "SELECT reaction, COUNT(*) as cnt
+ FROM {$wpdb->prefix}forum_reactions
+ WHERE object_id=%d AND object_type=%s
+ GROUP BY reaction",
+ $object_id, $object_type
+ ) );
+ $counts = [];
+ foreach ( $rows as $r ) $counts[$r->reaction] = (int)$r->cnt;
+
+ $my = '';
+ if ( $user_id ) {
+ $row = $wpdb->get_row( $wpdb->prepare(
+ "SELECT reaction FROM {$wpdb->prefix}forum_reactions
+ WHERE user_id=%d AND object_id=%d AND object_type=%s",
+ $user_id, $object_id, $object_type
+ ) );
+ if ($row) $my = $row->reaction;
+ }
+ return ['counts' => $counts, 'mine' => $my];
+ }
+
+ // ── Private Nachrichten ───────────────────────────────────────────────────
+
+ public static function send_message( $from_id, $to_id, $content ) {
+ global $wpdb;
+ $wpdb->insert( "{$wpdb->prefix}forum_messages", [
+ 'from_id' => (int)$from_id,
+ 'to_id' => (int)$to_id,
+ 'content' => mb_substr(strip_tags($content), 0, 2000),
+ 'is_read' => 0,
+ ] );
+ return $wpdb->insert_id;
+ }
+
+ /** Alle Konversationspartner des Users */
+ public static function get_inbox( $user_id ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT
+ partner_id,
+ MAX(last_msg_id) AS last_msg_id,
+ MAX(last_time) AS last_time,
+ SUM(unread_cnt) AS unread_cnt,
+ partner_name, partner_avatar, partner_role
+ FROM (
+ SELECT
+ m.from_id AS partner_id,
+ MAX(m.id) AS last_msg_id,
+ MAX(m.created_at) AS last_time,
+ SUM(CASE WHEN m.is_read=0 AND m.to_id=%d THEN 1 ELSE 0 END) AS unread_cnt,
+ u.display_name AS partner_name,
+ u.avatar_url AS partner_avatar,
+ u.role AS partner_role
+ FROM {$wpdb->prefix}forum_messages m
+ JOIN {$wpdb->prefix}forum_users u ON u.id = m.from_id
+ WHERE m.to_id=%d AND m.deleted_by_receiver=0
+ GROUP BY m.from_id
+
+ UNION ALL
+
+ SELECT
+ m.to_id AS partner_id,
+ MAX(m.id) AS last_msg_id,
+ MAX(m.created_at) AS last_time,
+ 0 AS unread_cnt,
+ u.display_name AS partner_name,
+ u.avatar_url AS partner_avatar,
+ u.role AS partner_role
+ FROM {$wpdb->prefix}forum_messages m
+ JOIN {$wpdb->prefix}forum_users u ON u.id = m.to_id
+ WHERE m.from_id=%d AND m.deleted_by_sender=0
+ GROUP BY m.to_id
+ ) sub
+ GROUP BY partner_id, partner_name, partner_avatar, partner_role
+ ORDER BY last_time DESC",
+ $user_id, $user_id, $user_id
+ ) );
+ }
+
+ /** Nachrichten einer Konversation zwischen zwei Usern */
+ public static function get_conversation( $user_id, $partner_id, $limit = 50, $offset = 0 ) {
+ global $wpdb;
+ // Neueste $limit Nachrichten ab $offset holen, dann aufsteigend sortieren
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT * FROM (
+ SELECT m.*, u.display_name AS sender_name, u.avatar_url AS sender_avatar
+ FROM {$wpdb->prefix}forum_messages m
+ JOIN {$wpdb->prefix}forum_users u ON u.id = m.from_id
+ WHERE ( (m.from_id=%d AND m.to_id=%d AND m.deleted_by_sender=0)
+ OR (m.from_id=%d AND m.to_id=%d AND m.deleted_by_receiver=0) )
+ ORDER BY m.created_at DESC
+ LIMIT %d OFFSET %d
+ ) sub ORDER BY sub.created_at ASC",
+ $user_id, $partner_id, $partner_id, $user_id, $limit, $offset
+ ) );
+ }
+
+ public static function count_conversation( $user_id, $partner_id ) {
+ global $wpdb;
+ return (int) $wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_messages m
+ WHERE ( (m.from_id=%d AND m.to_id=%d AND m.deleted_by_sender=0)
+ OR (m.from_id=%d AND m.to_id=%d AND m.deleted_by_receiver=0) )",
+ $user_id, $partner_id, $partner_id, $user_id
+ ) );
+ }
+
+ public static function mark_messages_read( $user_id, $partner_id ) {
+ global $wpdb;
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_messages SET is_read=1
+ WHERE to_id=%d AND from_id=%d AND is_read=0",
+ $user_id, $partner_id
+ ) );
+ }
+
+ public static function count_unread_messages( $user_id ) {
+ global $wpdb;
+ $table = "{$wpdb->prefix}forum_messages";
+ if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return 0;
+ return (int)$wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_messages WHERE to_id=%d AND is_read=0 AND deleted_by_receiver=0",
+ $user_id
+ ) );
+ }
+
+ // ── Remember-Me ───────────────────────────────────────────────────────────
+
+ public static function create_remember_token( $user_id ) {
+ global $wpdb;
+ $token = bin2hex( random_bytes(32) );
+ $expires = date('Y-m-d H:i:s', strtotime('+30 days'));
+ // Delete existing tokens for this user first
+ $wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", ['user_id' => $user_id] );
+ $wpdb->insert( "{$wpdb->prefix}forum_remember_tokens", [
+ 'user_id' => $user_id,
+ 'token' => $token,
+ 'expires_at' => $expires,
+ ] );
+ return $token;
+ }
+
+ public static function verify_remember_token( $token ) {
+ global $wpdb;
+ $table = "{$wpdb->prefix}forum_remember_tokens";
+ if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return null;
+ return $wpdb->get_row( $wpdb->prepare(
+ "SELECT user_id FROM {$wpdb->prefix}forum_remember_tokens
+ WHERE token=%s AND expires_at > NOW()",
+ sanitize_text_field($token)
+ ) );
+ }
+
+ public static function delete_remember_token( $user_id ) {
+ global $wpdb;
+ $wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", ['user_id' => (int)$user_id] );
+ }
+
+ // ── @Erwähnungen ──────────────────────────────────────────────────────────
+
+ /** Extrahiert @usernames und gibt User-Objekte zurück */
+ public static function extract_mentions( $content ) {
+ preg_match_all( '/@([a-zA-Z0-9_]{3,60})/', $content, $m );
+ $users = [];
+ foreach ( array_unique($m[1]) as $username ) {
+ $user = self::get_user_by('username', $username);
+ if ($user) $users[] = $user;
+ }
+ return $users;
+ }
+
+
+
+ // ── Passwort-Reset ────────────────────────────────────────────────────────
+
+ public static function create_reset_token( $user_id ) {
+ global $wpdb;
+ $token = bin2hex( random_bytes(32) );
+ $hash = hash( 'sha256', $token );
+ // Alte Tokens löschen
+ $wpdb->delete( "{$wpdb->prefix}forum_users", [] ); // nur placeholder
+ $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
+ ) );
+ return $token; // Klartext-Token → per E-Mail senden
+ }
+
+ public static function verify_reset_token( $token ) {
+ global $wpdb;
+ $hash = hash( 'sha256', $token );
+ return $wpdb->get_row( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_users
+ WHERE reset_token=%s AND reset_token_expires > NOW()",
+ $hash
+ ) );
+ }
+
+ public static function use_reset_token( $token, $new_password ) {
+ global $wpdb;
+ $user = self::verify_reset_token( $token );
+ if ( ! $user ) return false;
+ $wpdb->update(
+ "{$wpdb->prefix}forum_users",
+ [
+ 'password' => password_hash( $new_password, PASSWORD_DEFAULT ),
+ 'reset_token' => null,
+ 'reset_token_expires' => null,
+ ],
+ ['id' => $user->id]
+ );
+ return true;
+ }
+
+
+}
\ No newline at end of file
diff --git a/includes/class-forum-levels.php b/includes/class-forum-levels.php
new file mode 100644
index 0000000..29b271d
--- /dev/null
+++ b/includes/class-forum-levels.php
@@ -0,0 +1,130 @@
+ 0, 'label' => 'Neuling', 'icon' => 'fas fa-seedling', 'color' => '#94a3b8' ],
+ [ 'min' => 10, 'label' => 'Schreiberling', 'icon' => 'fas fa-feather', 'color' => '#60a5fa' ],
+ [ 'min' => 50, 'label' => 'Erfahrener', 'icon' => 'fas fa-fire', 'color' => '#f97316' ],
+ [ 'min' => 150, 'label' => 'Veteran', 'icon' => 'fas fa-shield-halved', 'color' => '#a78bfa' ],
+ [ 'min' => 500, 'label' => 'Legende', 'icon' => 'fas fa-crown', 'color' => '#fbbf24' ],
+ ];
+ }
+
+ // ── Laden / Speichern ─────────────────────────────────────────
+
+ public static function get_all() {
+ $saved = get_option( self::OPTION_KEY, null );
+ if ( $saved === null ) {
+ $defaults = self::default_levels();
+ update_option( self::OPTION_KEY, $defaults );
+ return $defaults;
+ }
+ $levels = (array) $saved;
+ usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
+ return $levels;
+ }
+
+ public static function save( $levels ) {
+ usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
+ update_option( self::OPTION_KEY, $levels );
+ }
+
+ public static function reset_to_defaults() {
+ update_option( self::OPTION_KEY, self::default_levels() );
+ }
+
+ // ── Level für eine Beitragsanzahl ermitteln ───────────────────
+
+ public static function get_for_count( $post_count ) {
+ $levels = self::get_all();
+ // Von oben (höchster min) nach unten suchen
+ $sorted = array_reverse( $levels );
+ foreach ( $sorted as $level ) {
+ if ( (int) $post_count >= (int) $level['min'] ) {
+ return $level;
+ }
+ }
+ return $levels[0]; // Fallback: niedrigstes Level
+ }
+
+ // Nächstes Level (für Fortschrittsanzeige)
+ public static function get_next( $post_count ) {
+ $levels = self::get_all(); // bereits aufsteigend sortiert
+ foreach ( $levels as $level ) {
+ if ( (int) $level['min'] > (int) $post_count ) {
+ return $level;
+ }
+ }
+ return null; // Maxlevel erreicht
+ }
+
+ // Fortschritt in Prozent zum nächsten Level (0–100)
+ public static function progress( $post_count ) {
+ $current = self::get_for_count( $post_count );
+ $next = self::get_next( $post_count );
+ if ( ! $next ) return 100;
+ $range = (int) $next['min'] - (int) $current['min'];
+ if ( $range <= 0 ) return 100;
+ $done = (int) $post_count - (int) $current['min'];
+ return min( 100, (int) round( $done / $range * 100 ) );
+ }
+
+ // ── Badge HTML ────────────────────────────────────────────────
+
+ public static function badge( $post_count ) {
+ if ( ! self::is_enabled() ) return '';
+ $level = self::get_for_count( $post_count );
+ $label = esc_html( $level['label'] );
+ $icon = esc_attr( $level['icon'] );
+ $color = esc_attr( $level['color'] );
+ return ""
+ . " {$label}";
+ }
+
+ // ── Fortschrittsbalken HTML (für Profil-Sidebar) ──────────────
+
+ public static function progress_bar( $post_count ) {
+ if ( ! self::is_enabled() ) return '';
+ $current = self::get_for_count( $post_count );
+ $next = self::get_next( $post_count );
+ $pct = self::progress( $post_count );
+ $color = esc_attr( $current['color'] );
+ $cur_lbl = esc_html( $current['label'] );
+ $next_lbl = $next ? esc_html( $next['label'] ) : $cur_lbl;
+ $posts_to = $next ? ( (int)$next['min'] - (int)$post_count ) . ' Beiträge bis ' . $next_lbl : 'Maximales Level erreicht';
+
+ return "
+
+
+ " . self::badge( $post_count ) . "
+ {$posts_to}
+
+
+
";
+ }
+}
\ No newline at end of file
diff --git a/includes/class-forum-roles.php b/includes/class-forum-roles.php
new file mode 100644
index 0000000..24335ec
--- /dev/null
+++ b/includes/class-forum-roles.php
@@ -0,0 +1,211 @@
+ [
+ 'label' => 'Superadmin',
+ 'level' => 100,
+ 'color' => '#e11d48',
+ 'bg_color' => 'rgba(225,29,72,.15)',
+ 'icon' => 'fas fa-crown',
+ 'permissions' => ['all'],
+ 'locked' => true, // unveränderlich
+ 'description' => 'Vollständige Kontrolle — immer an den WordPress-Admin gebunden.',
+ ],
+ 'admin' => [
+ 'label' => 'Admin',
+ 'level' => 80,
+ 'color' => '#f87171',
+ 'bg_color' => 'rgba(248,113,113,.13)',
+ 'icon' => 'fas fa-shield-halved',
+ 'permissions' => ['post','create_thread','like','pin_thread','close_thread','delete_post','delete_thread','manage_users','manage_cats','post_announcement'],
+ 'locked' => false,
+ 'description' => 'Volle Moderations- und Verwaltungsrechte.',
+ ],
+ 'moderator' => [
+ 'label' => 'Moderator',
+ 'level' => 50,
+ 'color' => '#fbbf24',
+ 'bg_color' => 'rgba(251,191,36,.12)',
+ 'icon' => 'fas fa-shield',
+ 'permissions' => ['post','create_thread','like','pin_thread','close_thread','delete_post','delete_thread','post_announcement'],
+ 'locked' => false,
+ 'description' => 'Kann Threads & Posts moderieren.',
+ ],
+ 'vip' => [
+ 'label' => 'VIP',
+ 'level' => 20,
+ 'color' => '#38bdf8',
+ 'bg_color' => 'rgba(56,189,248,.12)',
+ 'icon' => 'fas fa-star',
+ 'permissions' => ['post','create_thread','like'],
+ 'locked' => false,
+ 'description' => 'VIP-Mitglied mit besonderem Badge.',
+ ],
+ 'member' => [
+ 'label' => 'Member',
+ 'level' => 10,
+ 'color' => '#94a3b8',
+ 'bg_color' => 'rgba(148,163,184,.1)',
+ 'icon' => 'fas fa-user',
+ 'permissions' => ['post','create_thread','like'],
+ 'locked' => false,
+ 'description' => 'Standard-Mitglied.',
+ ],
+ 'banned' => [
+ 'label' => 'Gesperrt',
+ 'level' => -1,
+ 'color' => '#475569',
+ 'bg_color' => 'rgba(71,85,105,.2)',
+ 'icon' => 'fas fa-ban',
+ 'permissions' => [],
+ 'locked' => false,
+ 'description' => 'Kein Forum-Zugang.',
+ ],
+ ];
+ }
+
+ /** Alle Rollen laden (aus DB oder Defaults) */
+ public static function get_all() {
+ $saved = get_option(self::OPTION_KEY, null);
+ if ( $saved === null ) {
+ $defaults = self::default_roles();
+ update_option(self::OPTION_KEY, $defaults);
+ return $defaults;
+ }
+ // Superadmin immer aus defaults übernehmen (kann nicht manipuliert werden)
+ $saved[self::SUPERADMIN] = self::default_roles()[self::SUPERADMIN];
+ return $saved;
+ }
+
+ /** Einzelne Rolle */
+ public static function get( $key ) {
+ $all = self::get_all();
+ return $all[$key] ?? $all['member'];
+ }
+
+ /** Alle verfügbaren Rollen als key=>label Array */
+ public static function labels() {
+ $out = [];
+ foreach ( self::get_sorted() as $key => $role ) {
+ $out[$key] = $role['label'];
+ }
+ return $out;
+ }
+
+ /** Nach Level sortiert (höchstes zuerst) */
+ public static function get_sorted() {
+ $all = self::get_all();
+ uasort($all, fn($a,$b) => $b['level'] <=> $a['level']);
+ return $all;
+ }
+
+ /** Rolle speichern / erstellen */
+ public static function save( $key, $data ) {
+ if ( $key === self::SUPERADMIN ) return false; // nie überschreiben
+ $all = self::get_all();
+ $all[$key] = $data;
+ update_option(self::OPTION_KEY, $all);
+ return true;
+ }
+
+ /** Rolle löschen */
+ public static function delete( $key ) {
+ if ( $key === self::SUPERADMIN ) return false;
+ if ( $key === 'member' ) return false; // member darf nicht gelöscht werden
+ $all = self::get_all();
+ unset($all[$key]);
+ update_option(self::OPTION_KEY, $all);
+ // Alle Nutzer dieser Rolle zu 'member' degradieren
+ global $wpdb;
+ $wpdb->update("{$wpdb->prefix}forum_users", ['role'=>'member'], ['role'=>$key]);
+ return true;
+ }
+
+ /** Level einer Rolle */
+ public static function level( $key ) {
+ return (int)( self::get($key)['level'] ?? 10 );
+ }
+
+ /** Hat Rolle eine bestimmte Permission? */
+ public static function has_permission( $role_key, $permission ) {
+ $role = self::get($role_key);
+ $perms = $role['permissions'] ?? [];
+ return in_array('all', $perms) || in_array($permission, $perms);
+ }
+
+ /** Darf User eine Aktion ausführen? */
+ public static function can( $user, $action ) {
+ if ( ! $user ) return false;
+ // Superadmin — immer alles erlaubt
+ if ( $user->role === self::SUPERADMIN ) return true;
+ if ( self::level($user->role) < 0 ) return false; // banned
+ return self::has_permission($user->role, $action);
+ }
+
+ /** Darf User in Kategorie posten? */
+ public static function can_post_in( $user, $cat ) {
+ if ( ! $user ) return false;
+ if ( $user->role === self::SUPERADMIN ) return true;
+ $min = $cat->min_role ?? 'member';
+ return self::level($user->role) >= self::level($min);
+ }
+
+ /** Badge HTML */
+ public static function badge( $role_key ) {
+ $role = self::get($role_key);
+ $label = esc_html($role['label']);
+ $color = esc_attr($role['color']);
+ $bg = esc_attr($role['bg_color']);
+ $icon = esc_attr($role['icon'] ?? 'fas fa-user');
+ $border = esc_attr($role['color']);
+ return "
+ {$label}
+ ";
+ }
+
+ /** Alle erlaubten Permissions (für Checkboxen im Admin) */
+ public static function all_permissions() {
+ return [
+ 'post' => 'Beiträge schreiben',
+ 'create_thread' => 'Threads erstellen',
+ 'like' => 'Beiträge liken',
+ 'pin_thread' => 'Threads pinnen',
+ 'close_thread' => 'Threads schließen',
+ 'delete_post' => 'Posts löschen',
+ 'delete_thread' => 'Threads löschen',
+ 'manage_users' => 'Nutzer verwalten',
+ 'manage_cats' => 'Kategorien verwalten',
+ 'post_announcement' => 'Ankündigungen posten',
+ ];
+ }
+
+ /** Ist der aktuelle WP-User der Seiteninhaber (Superadmin)? */
+ public static function is_wp_superadmin() {
+ return current_user_can('administrator') || (is_multisite() && is_super_admin());
+ }
+
+ /** Superadmin-Status erzwingen: Forum-User des WP-Admins immer auf superadmin setzen */
+ public static function sync_superadmin() {
+ if ( ! is_user_logged_in() ) return;
+ if ( ! self::is_wp_superadmin() ) return;
+
+ $wp_user = wp_get_current_user();
+ $forum_user = WBF_DB::get_user_by('email', $wp_user->user_email);
+ if ( $forum_user && $forum_user->role !== self::SUPERADMIN ) {
+ WBF_DB::update_user($forum_user->id, ['role' => self::SUPERADMIN]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/includes/class-forum-shortcodes.php b/includes/class-forum-shortcodes.php
new file mode 100644
index 0000000..536f14b
--- /dev/null
+++ b/includes/class-forum-shortcodes.php
@@ -0,0 +1,1537 @@
+";
+ if (!$online) return $img;
+ return "$img";
+ }
+
+ public static function role_badge( $role ) {
+ // Dynamisch aus WBF_Roles — deckt alle Rollen inkl. superadmin ab
+ return WBF_Roles::badge( $role );
+ }
+
+ public static function render_tags( $tags, $small = false ) {
+ if ( empty($tags) ) return '';
+ $cls = $small ? 'wbf-tag wbf-tag--sm' : 'wbf-tag';
+ $out = '';
+ }
+
+ private static function like_btn( $object_id, $type, $count, $liked = false ) {
+ // Keep for thread likes (header area)
+ $cls = $liked ? ' wbf-liked' : '';
+ return sprintf(
+ '',
+ $cls, $object_id, $type, $count
+ );
+ }
+
+ private static function reaction_bar( $object_id, $object_type, $current_user ) {
+ $emojis = ['👍','❤️','😂','😮','😢','😡'];
+ $user_id = $current_user ? (int)$current_user->id : 0;
+ $data = WBF_DB::get_reactions($object_id, $object_type, $user_id);
+ $counts = $data['counts'];
+ $mine = $data['mine'];
+ $total = array_sum($counts);
+
+ $out = '';
+
+ // Summary of existing reactions (clickable)
+ if ($total > 0) {
+ $out .= '
';
+ foreach ($emojis as $e) {
+ if (!empty($counts[$e])) {
+ $active = ($mine === $e) ? ' wbf-reaction-active' : '';
+ $out .= '';
+ }
+ }
+ $out .= '
';
+ }
+
+ // Picker trigger (only for logged-in users with 'like' perm)
+ if ($current_user && WBF_DB::can($current_user, 'like')) {
+ $out .= '
'
+ . '
'
+ . '
';
+ foreach ($emojis as $e) {
+ $active = ($mine === $e) ? ' wbf-reaction-active' : '';
+ $out .= '';
+ }
+ $out .= '
';
+ }
+
+ $out .= '
';
+ return $out;
+ }
+
+ private static function mod_tools_thread( $thread, $current ) {
+ if (!$current) return '';
+ // Only show mod bar to users who have at least one mod-level permission
+ $can_pin = WBF_DB::can($current, 'pin_thread');
+ $can_close = WBF_DB::can($current, 'close_thread');
+ $can_delete = WBF_DB::can($current, 'delete_thread');
+ $can_move = WBF_DB::can($current, 'manage_cats');
+ if ( !$can_pin && !$can_close && !$can_delete && !$can_move ) return '';
+
+ $is_archived = $thread->status === 'archived';
+ $pin_label = $thread->pinned ? ' Entpinnen' : ' Pinnen';
+ $pin_action = $thread->pinned ? 'unpin_thread' : 'pin_thread';
+ $arch_action = $is_archived ? 'unarchive_thread' : 'archive_thread';
+ $arch_label = $is_archived
+ ? ' Wiederherstellen'
+ : ' Archivieren';
+
+ // Close/Open only makes sense on non-archived threads
+ $cls_btn = '';
+ if ( !$is_archived && $can_close ) {
+ $cls_action = $thread->status==='closed' ? 'open_thread' : 'close_thread';
+ $cls_label = $thread->status==='closed' ? ' Öffnen' : ' Schließen';
+ $cls_btn = '';
+ }
+ $move_btn = $can_move
+ ? ''
+ : '';
+ $del_btn = $can_delete
+ ? ''
+ : '';
+
+ return '
+ Mod
+ '.( $can_pin ? '' : '').'
+ '.$cls_btn.'
+ '.( $can_close ? '' : '').'
+ '.$move_btn.'
+ '.$del_btn.'
+
';
+ }
+
+ private static function mod_tools_post( $post_id, $current ) {
+ if (!$current || !WBF_DB::can($current,'delete_post')) return '';
+ return '';
+ }
+
+ // ── Router ────────────────────────────────────────────────────────────────
+
+ public static function forum_main( $atts ) {
+ // Server-seitiger Logout-Fallback
+ if (isset($_GET['wbf_do_logout'])) {
+ WBF_Auth::logout();
+ wp_redirect( wbf_get_forum_url() );
+ exit;
+ }
+ if (isset($_GET['forum_members'])) return self::view_members();
+ if (isset($_GET['wbf_reset_token'])) return self::view_reset_password();
+ if (isset($_GET['forum_thread'])) return self::view_thread();
+ if (isset($_GET['forum_cat'])) return self::view_category();
+ if (isset($_GET['forum_profile'])) return self::view_profile();
+ if (isset($_GET['forum_search'])) return self::view_search();
+ if (isset($_GET['forum_tag'])) return self::view_tag();
+ if (isset($_GET['forum_dm'])) return self::view_dm();
+ return self::view_home();
+ }
+
+ // ── HOME ──────────────────────────────────────────────────────────────────
+
+ private static function view_home() {
+ $current = WBF_Auth::get_current_user();
+ $tree = WBF_DB::get_categories_tree();
+ $stats = WBF_DB::get_stats();
+ $recent = WBF_DB::get_recent_threads(5);
+ ob_start(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
name); ?>
+
description); ?>
+
+
+ thread_count; ?>
+ post_count; ?>
+ min_role??'member') !== 'member'): ?>
+ min_role)); ?>+
+
+
+
+ children)): ?>
+
+ children as $child): ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Kategorie nicht gefunden.';
+
+ $page = max(1,(int)($_GET['fp']??1));
+ $threads = WBF_DB::get_threads($cat->id, $page);
+ $total = WBF_DB::count_threads($cat->id);
+ $pages = ceil($total / 20) ?: 1;
+ $current = WBF_Auth::get_current_user();
+ $children = WBF_DB::get_child_categories($cat->id);
+ $crumbs = WBF_DB::get_category_breadcrumb($cat);
+
+ ob_start(); ?>
+
+
+
+
+
+
+
+
+
+
Noch keine Threads. Starte die Diskussion!
+
+
+ id,$t->id,'thread') : false; ?>
+
+
avatar_url,$t->display_name); ?>
+
+
+ pinned): ?>
+ status==='closed'): ?>
Geschlossen
+
title); ?>
+
Neu
+
+
+ von display_name); ?>
+ author_role); ?>
+ created_at); ?>
+
+ id); echo self::render_tags($row_tags, true); ?>
+
+
+ reply_count; ?>
+ views; ?>
+ id,'thread',$t->like_count,$liked); ?>
+
+
+
+
+ 1): ?>
+
+
+
+
+ id, 1, 50);
+ $arch_count = WBF_DB::count_archived_threads($cat->id);
+ if ($arch_count > 0):
+ ?>
+
+
+
+ Archivierte Threads
+
+
+
+
+
+
+
avatar_url,$at->display_name); ?>
+
+
+
+ von display_name); ?>
+ author_role); ?>
+ created_at); ?>
+
+
+
+ reply_count; ?>
+
+
+
+
+
+
+
+ id); ?>
+
+
+ Thread nicht gefunden.';
+
+ global $wpdb;
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET views=views+1 WHERE id=%d",$id));
+
+ $page = max(1,(int)($_GET['tp']??1));
+ $posts = WBF_DB::get_posts($id,$page,15);
+ $total = WBF_DB::count_posts($id);
+ $pages = ceil($total/15) ?: 1;
+ $current = WBF_Auth::get_current_user();
+ $cat = WBF_DB::get_category($thread->category_id);
+ $crumbs = $cat ? WBF_DB::get_category_breadcrumb($cat) : [];
+ $t_liked = $current ? WBF_DB::has_liked($current->id,$id,'thread') : false;
+
+ ob_start(); ?>
+
+
+
+
+
+
+
+
+
+
+
+ id, $id, 'thread') : false;
+ $op_can_edit = $current && ((int)$current->id === (int)$thread->user_id || WBF_DB::can($current,'delete_post'));
+ ?>
+
+
+
+
content); ?>
+
+
+
+
+
+
+
+
+
+ signature)): ?>
+
+
+
+
+
+
+ id,$post->id,'post') : false;
+ self::render_single_post($post,$current,$p_liked);
+ endforeach; ?>
+
+
+ 1): ?>
+
+
+
+ status==='open'): ?>
+
+
+
Dieser Thread ist geschlossen.
+
+
+
+
+
+
+ id, $post->id, 'post') : false;
+ ?>
+
+
+
+
Neu
+
content); ?>
+
+
+
+
+
+
+
+
+ signature)): ?>
+
+
+
+
+
+ Profil nicht gefunden.';
+ $is_own = $current && $current->id == $profile->id;
+ $user_posts = WBF_DB::get_user_posts( $profile->id, 50 );
+
+ ob_start(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Noch keine Beiträge.
+ content), 0, 130 ) );
+ $more = mb_strlen( strip_tags($up->content) ) > 130 ? '…' : '';
+ $is_thread = isset($up->entry_type) && $up->entry_type === 'thread';
+ $anchor = $is_thread
+ ? '?forum_thread=' . (int)$up->thread_id
+ : '?forum_thread=' . (int)$up->thread_id . '#post-' . (int)$up->id;
+ ?>
+
+
+
+
+ Thread
+
+
+
+ Antwort
+
+
+
+ thread_title, 0, 60) ); ?>
+
+
+ cat_name); ?>
+
+
created_at); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tag nicht gefunden.';
+
+ $current = WBF_Auth::get_current_user();
+ $page = max(1,(int)($_GET['fp']??1));
+ $threads = WBF_DB::get_threads_by_tag($slug, $page, 20);
+ $total = WBF_DB::count_threads_by_tag($slug);
+ $pages = ceil($total/20) ?: 1;
+
+ ob_start(); ?>
+
+
+
+
+
+
+
Keine Threads mit diesem Tag.
+
+
+ id,$t->id,'thread') : false;
+ $t_tags = WBF_DB::get_thread_tags($t->id); ?>
+
+
avatar_url,$t->display_name); ?>
+
+
+
+ von display_name); ?>
+ author_role); ?>
+ created_at); ?>
+
+
+
+
+ reply_count; ?>
+ views; ?>
+ id,'thread',$t->like_count,$liked); ?>
+
+
+
+
+ 1): ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Wähle eine Konversation aus oder starte eine neue Nachricht.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Neue Nachricht
+
+
+
+
+
+
+
+
+
+
+
+ = 2 ? WBF_DB::search($query, 40) : [];
+ ob_start(); ?>
+
+
+
+
+
+
+
+
+
+ result_type === 'thread';
+ $url = $is_thread ? '?forum_thread='.(int)$r->id : '?forum_thread='.(int)$r->id;
+ $preview = esc_html(mb_substr(strip_tags($r->content), 0, 200));
+ $more = mb_strlen(strip_tags($r->content)) > 200 ? '…' : '';
+ ?>
+
+
+
+ Thread
+
+ Antwort
+
+ cat_name); ?>
+ avatar_url, $r->display_name, 18); ?> display_name); ?>
+ created_at); ?>
+
+
title, 0, 80)); ?>
+
+
+
+
+
Ergebnis(se) gefunden.
+
+
+
+
+ id) : 0;
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Gib deine E-Mail ein — wir schicken dir einen Reset-Link.
+
+
+
+
+
+
+
+
+
+
+
+
Forum Zugang
+
+
+
+ (int)$c->parent_id === 0);
+ $children_map = [];
+ foreach ($categories as $c) { if ((int)$c->parent_id > 0) $children_map[$c->parent_id][] = $c; }
+ ?>
+
+
+
+
Thread verschieben
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Beitrag melden
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (int)$c->parent_id === 0);
+ $children_map = [];
+ foreach ($categories as $c) { if ((int)$c->parent_id > 0) $children_map[$c->parent_id][] = $c; }
+ ?>
+
+
+
+
Neuen Thread erstellen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bitte
einloggen um die Mitgliederliste zu sehen.
+
+
+ prepare("WHERE (display_name LIKE %s OR username LIKE %s)", '%'.$search.'%', '%'.$search.'%')
+ : '';
+ $order = $sort === 'display_name' ? 'ASC' : 'DESC';
+
+ $total = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users $where");
+ $members = $wpdb->get_results($wpdb->prepare(
+ "SELECT id, username, display_name, avatar_url, role, post_count, registered, last_active
+ FROM {$wpdb->prefix}forum_users $where
+ ORDER BY $sort $order
+ LIMIT %d OFFSET %d",
+ $per_page, $offset
+ ));
+ $pages = ceil($total / $per_page) ?: 1;
+
+ ob_start(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1): ?>
+
+
+
+
+
+
+