From 6fff4f9dc2f7da1166d981bd08fa654000d200e9 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sat, 21 Mar 2026 18:47:28 +0100 Subject: [PATCH] Update from Git Manager GUI --- includes/class-forum-ajax.php | 395 ++++++++++++++- includes/class-forum-auth.php | 20 + includes/class-forum-db.php | 616 +++++++++++++++++++++++- includes/class-forum-shortcodes.php | 716 +++++++++++++++++++++++++++- 4 files changed, 1710 insertions(+), 37 deletions(-) diff --git a/includes/class-forum-ajax.php b/includes/class-forum-ajax.php index 4126b8e..ca4ba01 100644 --- a/includes/class-forum-ajax.php +++ b/includes/class-forum-ajax.php @@ -9,8 +9,15 @@ class WBF_Ajax { 'wbf_new_thread', 'wbf_new_post', 'wbf_toggle_like', 'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image', 'wbf_forgot_password', 'wbf_reset_password', 'wbf_load_more_messages', + 'wbf_create_invite', 'wbf_delete_invite', + 'wbf_toggle_subscribe', 'wbf_restore_content', 'wbf_toggle_profile_visibility', 'wbf_mod_action', 'wbf_report_post', 'wbf_edit_post', 'wbf_edit_thread', 'wbf_search', 'wbf_get_notifications', 'wbf_mark_notifications_read', 'wbf_move_thread', 'wbf_tag_suggest', 'wbf_set_reaction', 'wbf_send_message', 'wbf_get_inbox', 'wbf_get_conversation', 'wbf_mark_messages_read', 'wbf_get_online_users', 'wbf_user_suggest', 'wbf_delete_message', 'wbf_get_new_messages', + 'wbf_delete_account', + 'wbf_vote_poll', + 'wbf_create_poll', + 'wbf_toggle_bookmark', + 'wbf_set_thread_prefix', ]; foreach ($actions as $action) { add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]); @@ -50,7 +57,37 @@ class WBF_Ajax { } public static function handle_register() { - // Register braucht keinen Nonce + // Spam-Schutz: Honeypot + Zeitlimit + if ( ! empty($_POST['wbf_website']) ) { + wp_send_json_error(['message' => 'Spam erkannt.']); + } + $min_secs = (int)(wbf_get_settings()['spam_min_seconds'] ?? 30); + if ( $min_secs > 0 ) { + $form_time = (int)($_POST['wbf_form_time'] ?? 0); + if ( $form_time > 0 && (time() - $form_time) < $min_secs ) { + wp_send_json_error(['message' => 'Bitte warte noch einen Moment, bevor du das Formular absendest.']); + } + } + // Registrierungsmodus prüfen + $reg_mode = wbf_get_settings()['registration_mode'] ?? 'open'; + if ( $reg_mode === 'disabled' ) { + wp_send_json_error(['message' => 'Registrierung ist deaktiviert.']); + } + // Regel-Akzeptierung prüfen (wenn Pflicht aktiviert) + $rules_required = ( wbf_get_settings()['rules_accept_required'] ?? '1' ) === '1'; + $rules_enabled = ( wbf_get_settings()['rules_enabled'] ?? '1' ) === '1'; + if ( $rules_enabled && $rules_required && empty( $_POST['rules_accepted'] ) ) { + wp_send_json_error(['message' => 'Bitte akzeptiere die Forum-Regeln um fortzufahren.']); + } + if ( $reg_mode === 'invite' ) { + $code = strtoupper( trim( sanitize_text_field( $_POST['invite_code'] ?? '' ) ) ); + if ( ! $code ) { + wp_send_json_error(['message' => 'Einladungscode erforderlich.', 'need_invite' => true]); + } + if ( ! WBF_DB::verify_invite( $code ) ) { + wp_send_json_error(['message' => 'Einladungscode ungültig oder abgelaufen.', 'need_invite' => true]); + } + } $result = WBF_Auth::register( sanitize_text_field($_POST['username'] ?? ''), sanitize_email( $_POST['email'] ?? ''), @@ -59,6 +96,12 @@ class WBF_Ajax { ); if ($result['success']) { $u = $result['user']; + // Einladungscode einlösen + $reg_mode2 = wbf_get_settings()['registration_mode'] ?? 'open'; + if ( $reg_mode2 === 'invite' ) { + $code2 = strtoupper( trim( sanitize_text_field( $_POST['invite_code'] ?? '' ) ) ); + if ( $code2 ) WBF_DB::use_invite( $code2, $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); @@ -79,23 +122,40 @@ class WBF_Ajax { if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); if (!WBF_DB::can($user, 'create_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + // Flood Control + if ( ! WBF_DB::check_flood( $user->id ) ) { + $secs = (int)( wbf_get_settings()['flood_interval'] ?? 30 ); + wp_send_json_error(['message'=>"Bitte warte {$secs} Sekunden zwischen Beiträgen.", 'flood'=>true]); + } + $title = sanitize_text_field($_POST['title'] ?? ''); $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' ); $category_id = (int)($_POST['category_id'] ?? 0); + $prefix_id = (int)($_POST['prefix_id'] ?? 0) ?: null; 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.']); + // Inhalt nur prüfen wenn KEIN Poll mitgeschickt wird + $has_poll = ! empty( sanitize_text_field($_POST['poll_question'] ?? '') ); + if ( ! $has_poll && strlen($content) < 10 ) { + wp_send_json_error(['message'=>'Inhalt zu kurz (min. 10 Zeichen).']); + } + // Bei Umfrage ohne Inhalt: Platzhalter setzen + if ( $has_poll && strlen($content) < 1 ) { + $content = '—'; + } + $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, + 'title' => WBF_DB::apply_word_filter($title), 'slug' => sanitize_title($title) . '-' . time(), - 'content' => $content, + 'content' => WBF_DB::apply_word_filter($content), + 'prefix_id' => $prefix_id, ]); // Tags speichern @@ -104,6 +164,23 @@ class WBF_Ajax { WBF_DB::sync_thread_tags( $id, $raw_tags ); } + // Umfrage erstellen (optional) + $poll_question = sanitize_text_field( $_POST['poll_question'] ?? '' ); + $poll_opts_raw = $_POST['poll_options'] ?? []; + if ( $poll_question && is_array($poll_opts_raw) ) { + $poll_options = array_values( array_filter( array_map( 'sanitize_text_field', $poll_opts_raw ) ) ); + if ( count($poll_options) >= 2 ) { + $poll_multi = ! empty($_POST['poll_multi']) ? true : false; + $poll_ends = sanitize_text_field( $_POST['poll_ends_at'] ?? '' ); + $poll_ends_dt = null; + if ( $poll_ends ) { + $ts = strtotime($poll_ends); + if ( $ts && $ts > time() ) $poll_ends_dt = date('Y-m-d H:i:s', $ts); + } + WBF_DB::create_poll( $id, $poll_question, $poll_options, $poll_multi, $poll_ends_dt ); + } + } + wp_send_json_success(['thread_id'=>$id,'message'=>'Thread erstellt!']); } @@ -115,8 +192,15 @@ class WBF_Ajax { if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); if (!WBF_DB::can($user, 'post')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + // Flood Control + if ( ! WBF_DB::check_flood( $user->id ) ) { + $secs = (int)( wbf_get_settings()['flood_interval'] ?? 30 ); + wp_send_json_error(['message'=>"Bitte warte {$secs} Sekunden zwischen Beiträgen.", 'flood'=>true]); + } + $thread_id = (int)($_POST['thread_id'] ?? 0); $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' ); + $content = WBF_DB::apply_word_filter( $content ); if (strlen($content) < 3) wp_send_json_error(['message'=>'Antwort zu kurz.']); if (!$thread_id) wp_send_json_error(['message'=>'Ungültiger Thread.']); @@ -131,13 +215,24 @@ class WBF_Ajax { $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, ]); } + // Thread-Abonnenten benachrichtigen + $subscribers = WBF_DB::get_thread_subscribers($thread_id); + foreach ($subscribers as $sub) { + if ((int)$sub->id === (int)$user->id) continue; // nicht sich selbst + if (in_array($sub->id, array_column($notif_users, 'id') ?: [])) continue; // schon benachrichtigt + self::send_notification_email($sub, 'reply', $user->display_name, [ + 'thread_id' => $thread_id, + 'thread_title' => $thread->title, + ]); + } + // Ersteller auto-abonniert + WBF_DB::subscribe($thread->user_id, $thread_id); // @Erwähnungen $mentioned = WBF_DB::extract_mentions($content); foreach ($mentioned as $m_user) { @@ -198,13 +293,13 @@ class WBF_Ajax { 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); + WBF_DB::soft_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); + WBF_DB::soft_delete_post($object_id); wp_send_json_success(['action'=>'post_deleted']); break; @@ -278,6 +373,34 @@ class WBF_Ajax { } WBF_DB::update_user($user->id, $update); + + // Benutzerdefinierte Profilfelder speichern + $field_defs = WBF_DB::get_profile_field_defs(); + foreach ( $field_defs as $def ) { + $key = sanitize_key( $def['key'] ); + if ( ! $key ) continue; + $raw = $_POST[ 'cf_' . $key ] ?? null; + if ( $raw === null ) continue; // nicht übermittelt — nicht anfassen + + // Pflichtfeld-Prüfung + if ( ! empty($def['required']) && trim($raw) === '' ) { + wp_send_json_error(['message' => sprintf('Das Feld "%s" ist ein Pflichtfeld.', $def['label'])]); + } + + // Sanitisierung je nach Typ + if ( $def['type'] === 'url' ) { + $value = esc_url_raw( trim($raw) ); + } elseif ( $def['type'] === 'textarea' ) { + $value = sanitize_textarea_field( $raw ); + } elseif ( $def['type'] === 'number' ) { + $value = is_numeric($raw) ? (string)(float)$raw : ''; + } else { + $value = sanitize_text_field( $raw ); + } + + WBF_DB::set_user_meta( $user->id, $key, $value ); + } + wp_send_json_success(['message'=>'Profil gespeichert!']); } @@ -394,6 +517,17 @@ class WBF_Ajax { wp_send_json_error(['message' => 'Keine Berechtigung.']); } + // Post-Bearbeitungslimit prüfen + if ($is_own && !$is_mod) { + $limit_min = (int)(wbf_get_settings()['post_edit_limit'] ?? 30); + if ($limit_min > 0) { + $age_min = (time() - strtotime($db_post->created_at)) / 60; + if ($age_min > $limit_min) { + wp_send_json_error(['message' => "Bearbeitung nur innerhalb von {$limit_min} Minuten nach dem Posten möglich."]); + } + } + } + $wpdb->update( "{$wpdb->prefix}forum_posts", ['content' => $content, 'updated_at' => current_time('mysql')], @@ -828,6 +962,251 @@ class WBF_Ajax { } + + // ── Einladungen ─────────────────────────────────────────────────────────── + + public static function handle_create_invite() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user || ! WBF_Roles::can($user, 'manage_users') ) { + wp_send_json_error(['message' => 'Keine Berechtigung.']); + } + $max_uses = max(1, (int)($_POST['max_uses'] ?? 1)); + $note = sanitize_text_field($_POST['note'] ?? ''); + $expires = sanitize_text_field($_POST['expires'] ?? ''); + $expires_at = null; + if ($expires) { + $ts = strtotime($expires); + if ($ts > time()) { + $expires_at = date('Y-m-d H:i:s', $ts); + } + } + $code = WBF_DB::create_invite($user->id, $max_uses, $note, $expires_at); + $url = wbf_get_forum_url() . '?wbf_invite=' . $code; + wp_send_json_success(['code' => $code, 'url' => $url]); + } + + public static function handle_delete_invite() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user || ! WBF_Roles::can($user, 'manage_users') ) { + wp_send_json_error(['message' => 'Keine Berechtigung.']); + } + $id = (int)($_POST['invite_id'] ?? 0); + if ($id) WBF_DB::delete_invite($id); + wp_send_json_success(); + } + + + + // ── Thread-Abonnement ───────────────────────────────────────────────────── + + public static function handle_toggle_subscribe() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + $thread_id = (int)($_POST['thread_id'] ?? 0); + if (!$thread_id) wp_send_json_error(['message'=>'Ungültig.']); + + if (WBF_DB::is_subscribed($user->id, $thread_id)) { + WBF_DB::unsubscribe($user->id, $thread_id); + wp_send_json_success(['subscribed'=>false,'msg'=>'Abonnement entfernt.']); + } else { + WBF_DB::subscribe($user->id, $thread_id); + wp_send_json_success(['subscribed'=>true,'msg'=>'Thread abonniert! Du erhältst E-Mails bei neuen Antworten.']); + } + } + + // ── Wiederherstellen (Soft-Delete) ──────────────────────────────────────── + + public static function handle_restore_content() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user || !WBF_Roles::can($user,'delete_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + $type = sanitize_key($_POST['content_type'] ?? ''); + $id = (int)($_POST['content_id'] ?? 0); + if ($type === 'thread') { + WBF_DB::restore_thread($id); + } elseif ($type === 'post') { + WBF_DB::restore_post($id); + } else { + wp_send_json_error(['message'=>'Ungültig.']); + } + wp_send_json_success(['message'=>'Wiederhergestellt.']); + } + + // ── Profil-Sichtbarkeit umschalten ──────────────────────────────────────── + + public static function handle_toggle_profile_visibility() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + $current = (int)($user->profile_public ?? 1); + $new = $current ? 0 : 1; + WBF_DB::update_user($user->id, ['profile_public'=>$new]); + wp_send_json_success(['public'=>$new,'msg'=> $new ? 'Profil ist jetzt öffentlich.' : 'Profil ist jetzt privat.']); + } + + // ── DSGVO: Konto löschen ───────────────────────────────────────────────── + + public static function handle_delete_account() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] ); + + // Superadmin darf sich nicht selbst löschen + if ( $user->role === 'superadmin' ) { + wp_send_json_error( [ 'message' => 'Der Superadmin-Account kann nicht gelöscht werden.' ] ); + } + + // Passwort-Bestätigung prüfen + $password = $_POST['password'] ?? ''; + if ( empty( $password ) ) { + wp_send_json_error( [ 'message' => 'Bitte Passwort zur Bestätigung eingeben.' ] ); + } + if ( ! password_verify( $password, $user->password ) ) { + wp_send_json_error( [ 'message' => 'Falsches Passwort.' ] ); + } + + // Bestätigungs-Checkbox + if ( empty( $_POST['confirm'] ) ) { + wp_send_json_error( [ 'message' => 'Bitte Löschung ausdrücklich bestätigen.' ] ); + } + + // Ausloggen bevor gelöscht wird + WBF_Auth::logout(); + + // DSGVO-Löschung durchführen + $ok = WBF_DB::delete_user_gdpr( $user->id ); + // Custom Profile Meta ebenfalls löschen + WBF_DB::delete_user_meta_all( $user->id ); + if ( ! $ok ) { + wp_send_json_error( [ 'message' => 'Fehler bei der Kontolöschung. Bitte Admin kontaktieren.' ] ); + } + + // Admin benachrichtigen + $blog_name = get_bloginfo( 'name' ); + $admin_email = get_option( 'admin_email' ); + wp_mail( + $admin_email, + "[{$blog_name}] DSGVO: Konto gelöscht", + "Nutzer #{$user->id} ({$user->username}) hat sein Konto gemäß DSGVO Art. 17 gelöscht.\n\n" + . "Zeitpunkt: " . date('d.m.Y H:i:s') . "\n" + . "Alle personenbezogenen Daten wurden anonymisiert.", + [ 'Content-Type: text/plain; charset=UTF-8' ] + ); + + wp_send_json_success( [ + 'message' => 'Dein Konto wurde vollständig gelöscht. Alle personenbezogenen Daten wurden entfernt.', + 'redirect' => wbf_get_forum_url(), + ] ); + } + + // ── Umfrage: Erstellen (aus Thread-View) ────────────────────────────────── + + public static function handle_create_poll() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + + $thread_id = (int)( $_POST['thread_id'] ?? 0 ); + if ( ! $thread_id ) wp_send_json_error(['message' => 'Ungültiger Thread.']); + + $thread = WBF_DB::get_thread( $thread_id ); + if ( ! $thread ) wp_send_json_error(['message' => 'Thread nicht gefunden.']); + + // Nur der Thread-Ersteller darf eine Umfrage hinzufügen + if ( (int)$thread->user_id !== (int)$user->id && $user->role !== 'superadmin' ) { + wp_send_json_error(['message' => 'Keine Berechtigung.']); + } + + // Bereits eine Umfrage vorhanden? + if ( WBF_DB::get_poll( $thread_id ) ) { + wp_send_json_error(['message' => 'Dieser Thread hat bereits eine Umfrage.']); + } + + $question = sanitize_text_field( $_POST['poll_question'] ?? '' ); + $opts_raw = $_POST['poll_options'] ?? []; + $multi = ! empty($_POST['poll_multi']); + $ends_raw = sanitize_text_field( $_POST['poll_ends_at'] ?? '' ); + + if ( ! $question ) wp_send_json_error(['message' => 'Bitte eine Frage eingeben.']); + + $options = array_values( array_filter( array_map( 'sanitize_text_field', (array)$opts_raw ) ) ); + if ( count($options) < 2 ) wp_send_json_error(['message' => 'Mindestens 2 Antwortmöglichkeiten erforderlich.']); + if ( count($options) > 10 ) wp_send_json_error(['message' => 'Maximal 10 Antwortmöglichkeiten erlaubt.']); + + $ends_at = null; + if ( $ends_raw ) { + $ts = strtotime( $ends_raw ); + if ( $ts && $ts > time() ) $ends_at = date('Y-m-d H:i:s', $ts); + } + + WBF_DB::create_poll( $thread_id, $question, $options, $multi, $ends_at ); + wp_send_json_success(['message' => 'Umfrage erstellt! Seite wird neu geladen…']); + } + + // ── Umfrage: Abstimmen ──────────────────────────────────────────────────── + + public static function handle_vote_poll() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Bitte einloggen um abzustimmen.']); + + $poll_id = (int)( $_POST['poll_id'] ?? 0 ); + $option_idxs = array_map( 'intval', (array)( $_POST['options'] ?? [] ) ); + + if ( ! $poll_id || empty($option_idxs) ) { + wp_send_json_error(['message' => 'Ungültige Abstimmung.']); + } + + $ok = WBF_DB::vote_poll( $poll_id, $user->id, $option_idxs ); + if ( ! $ok ) { + wp_send_json_error(['message' => 'Bereits abgestimmt oder Umfrage beendet.']); + } + + $results = WBF_DB::get_poll_results( $poll_id ); + $my_votes = WBF_DB::get_user_votes( $poll_id, $user->id ); + wp_send_json_success([ + 'results' => $results, + 'my_votes' => $my_votes, + 'total' => array_sum( $results ), + ]); + } + + // ── Lesezeichen ─────────────────────────────────────────────────────────── + + public static function handle_toggle_bookmark() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + $thread_id = (int)($_POST['thread_id'] ?? 0); + if (!$thread_id) wp_send_json_error(['message'=>'Ungültiger Thread.']); + $added = WBF_DB::toggle_bookmark( $user->id, $thread_id ); + wp_send_json_success(['bookmarked' => $added]); + } + + // ── Thread-Präfix setzen ────────────────────────────────────────────────── + + public static function handle_set_thread_prefix() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + $thread_id = (int)($_POST['thread_id'] ?? 0); + $prefix_id = (int)($_POST['prefix_id'] ?? 0) ?: null; + if (!$thread_id) wp_send_json_error(['message'=>'Ungültiger Thread.']); + $thread = WBF_DB::get_thread($thread_id); + if (!$thread) wp_send_json_error(['message'=>'Thread nicht gefunden.']); + // Nur Thread-Ersteller oder Mods + if ( (int)$thread->user_id !== (int)$user->id && !WBF_DB::can($user,'pin_thread') ) { + wp_send_json_error(['message'=>'Keine Berechtigung.']); + } + global $wpdb; + $wpdb->update( "{$wpdb->prefix}forum_threads", ['prefix_id'=>$prefix_id], ['id'=>$thread_id] ); + $prefix = $prefix_id ? WBF_DB::get_prefix($prefix_id) : null; + wp_send_json_success(['prefix' => $prefix]); + } + } -WBF_Ajax::init(); \ No newline at end of file +add_action( 'init', [ 'WBF_Ajax', 'init' ] ); \ No newline at end of file diff --git a/includes/class-forum-auth.php b/includes/class-forum-auth.php index 2eb65df..5cdc32a 100644 --- a/includes/class-forum-auth.php +++ b/includes/class-forum-auth.php @@ -44,7 +44,27 @@ class WBF_Auth { return array( 'success' => false, 'message' => 'Falsches Passwort.' ); } if ( WBF_Roles::level($user->role) < 0 ) { + // Zeitlich begrenzte Sperre prüfen — automatisch aufheben wenn abgelaufen + if ( ! empty($user->ban_until) && strtotime($user->ban_until) <= time() ) { + $restore = ! empty($user->pre_ban_role) ? $user->pre_ban_role : 'member'; + WBF_DB::update_user( $user->id, [ + 'role' => $restore, + 'ban_reason' => '', + 'ban_until' => null, + 'pre_ban_role' => '', + ]); + // Frisch laden und einloggen + $user = WBF_DB::get_user( $user->id ); + $_SESSION[ self::SESSION_KEY ] = $user->id; + WBF_DB::touch_last_active( $user->id ); + return array( 'success' => true, 'user' => $user ); + } $reason = !empty($user->ban_reason) ? $user->ban_reason : 'Dein Konto wurde gesperrt.'; + // Zeitstempel anhängen wenn temporäre Sperre + if ( ! empty($user->ban_until) ) { + $until_fmt = date_i18n( 'd.m.Y \u\m H:i \U\h\r', strtotime($user->ban_until) ); + $reason .= ' (Gesperrt bis: ' . $until_fmt . ')'; + } return array( 'success' => false, 'banned' => true, 'message' => $reason ); } $_SESSION[ self::SESSION_KEY ] = $user->id; diff --git a/includes/class-forum-db.php b/includes/class-forum-db.php index 00c1a9a..38c0f29 100644 --- a/includes/class-forum-db.php +++ b/includes/class-forum-db.php @@ -96,10 +96,10 @@ class WBF_DB { $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, + slug VARCHAR(60) NOT NULL, use_count INT DEFAULT 0, PRIMARY KEY (id), - KEY slug (slug) + UNIQUE KEY slug (slug) ) $charset;"; $sql_thread_tags = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_thread_tags ( @@ -163,8 +163,28 @@ class WBF_DB { 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_categories", 'guest_visible', "ALTER TABLE {$wpdb->prefix}forum_categories ADD COLUMN guest_visible TINYINT(1) DEFAULT 1 AFTER min_role"); 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"); + // Soft-Delete + self::maybe_add_column("{$wpdb->prefix}forum_threads", 'deleted_at', "ALTER TABLE {$wpdb->prefix}forum_threads ADD COLUMN deleted_at DATETIME DEFAULT NULL"); + self::maybe_add_column("{$wpdb->prefix}forum_posts", 'deleted_at', "ALTER TABLE {$wpdb->prefix}forum_posts ADD COLUMN deleted_at DATETIME DEFAULT NULL"); + // Profil-Sichtbarkeit + self::maybe_add_column("{$wpdb->prefix}forum_users", 'profile_public', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) DEFAULT 1"); + // Zeitlich begrenzte Sperren + self::maybe_add_column("{$wpdb->prefix}forum_users", 'ban_until', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN ban_until DATETIME DEFAULT NULL"); + self::maybe_add_column("{$wpdb->prefix}forum_users", 'pre_ban_role', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN pre_ban_role VARCHAR(20) DEFAULT 'member'"); + // Thread-Abonnements + $sql_subscriptions = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_subscriptions ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + thread_id BIGINT UNSIGNED NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY user_thread (user_id, thread_id), + KEY thread_id (thread_id) + ) $charset;"; + dbDelta( $sql_subscriptions ); $sql_notifications = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_notifications ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, @@ -182,7 +202,87 @@ class WBF_DB { dbDelta( $sql_reports ); dbDelta( $sql_notifications ); - // Default categories + // Einladungs-Tabelle + $sql_invites = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_invites ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + code VARCHAR(64) NOT NULL UNIQUE, + created_by BIGINT UNSIGNED NOT NULL, + used_by BIGINT UNSIGNED DEFAULT NULL, + max_uses SMALLINT UNSIGNED DEFAULT 1, + use_count SMALLINT UNSIGNED DEFAULT 0, + note VARCHAR(255) DEFAULT '', + expires_at DATETIME DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY code (code), + KEY created_by (created_by) + ) $charset;"; + dbDelta( $sql_invites ); + + // Benutzerdefinierte Profilfelder — Meta-Tabelle + $sql_user_meta = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_user_meta ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + meta_key VARCHAR(60) NOT NULL, + meta_value TEXT DEFAULT '', + PRIMARY KEY (id), + UNIQUE KEY user_key (user_id, meta_key), + KEY user_id (user_id) + ) $charset;"; + dbDelta( $sql_user_meta ); + + // Umfragen (Polls) + $sql_polls = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_polls ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + thread_id BIGINT UNSIGNED NOT NULL, + question VARCHAR(255) NOT NULL DEFAULT '', + options TEXT NOT NULL DEFAULT '', + multi TINYINT(1) DEFAULT 0, + ends_at DATETIME DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY thread_id (thread_id) + ) $charset;"; + dbDelta( $sql_polls ); + + $sql_poll_votes = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_poll_votes ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + poll_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + option_idx TINYINT UNSIGNED NOT NULL, + voted_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY poll_user_option (poll_id, user_id, option_idx), + KEY poll_id (poll_id) + ) $charset;"; + dbDelta( $sql_poll_votes ); + + // ── Thread-Präfixe ──────────────────────────────────────────────────── + $sql_prefixes = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_prefixes ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + label VARCHAR(60) NOT NULL, + color VARCHAR(30) DEFAULT '#ffffff', + bg_color VARCHAR(30) DEFAULT '#475569', + sort_order INT DEFAULT 0, + PRIMARY KEY (id) + ) $charset;"; + dbDelta( $sql_prefixes ); + + // ── Lesezeichen ─────────────────────────────────────────────────────── + $sql_bookmarks = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_bookmarks ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + thread_id BIGINT UNSIGNED NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY user_thread (user_id, thread_id) + ) $charset;"; + dbDelta( $sql_bookmarks ); + + // ── prefix_id zu threads ────────────────────────────────────────────── + self::maybe_add_column( "{$wpdb->prefix}forum_threads", 'prefix_id', + "ALTER TABLE {$wpdb->prefix}forum_threads ADD COLUMN prefix_id BIGINT UNSIGNED DEFAULT NULL" ); + $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]); @@ -311,10 +411,12 @@ class WBF_DB { $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 + "SELECT t.*, u.display_name, u.avatar_url, u.username, u.role as author_role, + p.label as prefix_label, p.color as prefix_color, p.bg_color as prefix_bg 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 + LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id + WHERE t.category_id = %d AND t.deleted_at IS NULL $status_sql ORDER BY t.pinned DESC, t.last_reply_at DESC LIMIT %d OFFSET %d", $category_id, $per_page, $offset @@ -351,7 +453,7 @@ class WBF_DB { 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'", + "SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE category_id=%d AND status != 'archived' AND deleted_at IS NULL", $category_id )); } @@ -393,9 +495,11 @@ class WBF_DB { 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 + u.post_count as author_posts, u.registered as author_registered, u.role as author_role, + p.label as prefix_label, p.color as prefix_color, p.bg_color as prefix_bg FROM {$wpdb->prefix}forum_threads t JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id + LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id WHERE t.id = %d", $id )); } @@ -447,7 +551,7 @@ class WBF_DB { 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 + WHERE p.thread_id = %d AND p.deleted_at IS NULL ORDER BY p.created_at ASC LIMIT %d OFFSET %d", $thread_id, $per_page, $offset @@ -885,9 +989,16 @@ class WBF_DB { // ── Reaktionen ──────────────────────────────────────────────────────────── + /** Erlaubte Reaktionen aus den Einstellungen holen */ + public static function get_allowed_reactions() { + $saved = get_option('wbf_reactions', null); + if ( $saved !== null && is_array($saved) && count($saved) > 0 ) return $saved; + return ['👍','❤️','😂','😮','😢','😡']; // Defaults + } + public static function set_reaction( $user_id, $object_id, $object_type, $reaction ) { global $wpdb; - $allowed = ['👍','❤️','😂','😮','😢','😡']; + $allowed = self::get_allowed_reactions(); if ( ! in_array($reaction, $allowed, true) ) return false; $existing = $wpdb->get_row( $wpdb->prepare( @@ -1134,4 +1245,491 @@ class WBF_DB { } + + // ── Einladungen ─────────────────────────────────────────────────────────── + + public static function create_invite( $created_by, $max_uses = 1, $note = '', $expires_at = null ) { + global $wpdb; + $code = strtoupper( substr( bin2hex( random_bytes(6) ), 0, 10 ) ); + $wpdb->insert( "{$wpdb->prefix}forum_invites", [ + 'code' => $code, + 'created_by' => (int) $created_by, + 'max_uses' => (int) $max_uses, + 'note' => sanitize_text_field( $note ), + 'expires_at' => $expires_at, + ] ); + return $code; + } + + public static function get_invite( $code ) { + global $wpdb; + return $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}forum_invites WHERE code = %s", + strtoupper( trim($code) ) + ) ); + } + + public static function verify_invite( $code ) { + $inv = self::get_invite( $code ); + if ( ! $inv ) return false; + if ( $inv->use_count >= $inv->max_uses ) return false; + if ( $inv->expires_at && strtotime($inv->expires_at) < time() ) return false; + return $inv; + } + + public static function use_invite( $code, $user_id ) { + global $wpdb; + $inv = self::verify_invite( $code ); + if ( ! $inv ) return false; + $wpdb->query( $wpdb->prepare( + "UPDATE {$wpdb->prefix}forum_invites + SET use_count = use_count + 1, used_by = %d + WHERE code = %s", + (int) $user_id, strtoupper($code) + ) ); + return true; + } + + public static function get_all_invites( $limit = 100 ) { + global $wpdb; + return $wpdb->get_results( $wpdb->prepare( + "SELECT i.*, u.display_name AS creator_name, + uu.display_name AS used_name + FROM {$wpdb->prefix}forum_invites i + LEFT JOIN {$wpdb->prefix}forum_users u ON u.id = i.created_by + LEFT JOIN {$wpdb->prefix}forum_users uu ON uu.id = i.used_by + ORDER BY i.created_at DESC + LIMIT %d", + $limit + ) ); + } + + public static function delete_invite( $id ) { + global $wpdb; + $wpdb->delete( "{$wpdb->prefix}forum_invites", ['id' => (int)$id] ); + } + + + + // ── Thread-Abonnements ──────────────────────────────────────────────────── + + public static function subscribe( $user_id, $thread_id ) { + global $wpdb; + $wpdb->replace("{$wpdb->prefix}forum_subscriptions", [ + 'user_id' => (int)$user_id, + 'thread_id' => (int)$thread_id, + ]); + } + + public static function unsubscribe( $user_id, $thread_id ) { + global $wpdb; + $wpdb->delete("{$wpdb->prefix}forum_subscriptions", [ + 'user_id' => (int)$user_id, + 'thread_id' => (int)$thread_id, + ]); + } + + public static function is_subscribed( $user_id, $thread_id ) { + global $wpdb; + return (bool)$wpdb->get_var($wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}forum_subscriptions WHERE user_id=%d AND thread_id=%d", + (int)$user_id, (int)$thread_id + )); + } + + public static function get_thread_subscribers( $thread_id ) { + global $wpdb; + return $wpdb->get_results($wpdb->prepare( + "SELECT u.id, u.email, u.display_name + FROM {$wpdb->prefix}forum_subscriptions s + JOIN {$wpdb->prefix}forum_users u ON u.id = s.user_id + WHERE s.thread_id = %d", + (int)$thread_id + )); + } + + // ── Soft-Delete ─────────────────────────────────────────────────────────── + + public static function soft_delete_thread( $thread_id ) { + global $wpdb; + $wpdb->update( + "{$wpdb->prefix}forum_threads", + ['deleted_at' => current_time('mysql')], + ['id' => (int)$thread_id] + ); + } + + public static function soft_delete_post( $post_id ) { + global $wpdb; + $wpdb->update( + "{$wpdb->prefix}forum_posts", + ['deleted_at' => current_time('mysql')], + ['id' => (int)$post_id] + ); + } + + public static function restore_thread( $thread_id ) { + global $wpdb; + $wpdb->update("{$wpdb->prefix}forum_threads", ['deleted_at'=>null], ['id'=>(int)$thread_id]); + } + + public static function restore_post( $post_id ) { + global $wpdb; + $wpdb->update("{$wpdb->prefix}forum_posts", ['deleted_at'=>null], ['id'=>(int)$post_id]); + } + + public static function get_deleted_content( $limit = 50 ) { + global $wpdb; + $threads = $wpdb->get_results($wpdb->prepare( + "SELECT 'thread' as type, t.id, t.title as content_preview, t.deleted_at, + u.display_name, c.name as cat_name + FROM {$wpdb->prefix}forum_threads t + JOIN {$wpdb->prefix}forum_users u ON u.id=t.user_id + LEFT JOIN {$wpdb->prefix}forum_categories c ON c.id=t.category_id + WHERE t.deleted_at IS NOT NULL + ORDER BY t.deleted_at DESC LIMIT %d", $limit + )); + $posts = $wpdb->get_results($wpdb->prepare( + "SELECT 'post' as type, p.id, LEFT(p.content,80) as content_preview, p.deleted_at, + u.display_name, t.title as cat_name + FROM {$wpdb->prefix}forum_posts p + JOIN {$wpdb->prefix}forum_users u ON u.id=p.user_id + LEFT JOIN {$wpdb->prefix}forum_threads t ON t.id=p.thread_id + WHERE p.deleted_at IS NOT NULL + ORDER BY p.deleted_at DESC LIMIT %d", $limit + )); + return array_merge($threads, $posts); + } + + // ── Nutzungs-Statistiken ────────────────────────────────────────────────── + + public static function get_activity_stats( $days = 30 ) { + global $wpdb; + $since = date('Y-m-d', strtotime("-{$days} days")); + + $posts_per_day = $wpdb->get_results($wpdb->prepare( + "SELECT DATE(created_at) as day, COUNT(*) as count + FROM {$wpdb->prefix}forum_posts + WHERE created_at >= %s AND deleted_at IS NULL + GROUP BY DATE(created_at) ORDER BY day ASC", + $since + )); + $threads_per_day = $wpdb->get_results($wpdb->prepare( + "SELECT DATE(created_at) as day, COUNT(*) as count + FROM {$wpdb->prefix}forum_threads + WHERE created_at >= %s AND deleted_at IS NULL + GROUP BY DATE(created_at) ORDER BY day ASC", + $since + )); + $registrations = $wpdb->get_results($wpdb->prepare( + "SELECT DATE(registered) as day, COUNT(*) as count + FROM {$wpdb->prefix}forum_users + WHERE registered >= %s + GROUP BY DATE(registered) ORDER BY day ASC", + $since + )); + $top_posters = $wpdb->get_results($wpdb->prepare( + "SELECT u.display_name, u.role, COUNT(p.id) as post_count + FROM {$wpdb->prefix}forum_posts p + JOIN {$wpdb->prefix}forum_users u ON u.id=p.user_id + WHERE p.created_at >= %s AND p.deleted_at IS NULL + GROUP BY u.id ORDER BY post_count DESC LIMIT 10", + $since + )); + $active_hours = $wpdb->get_results($wpdb->prepare( + "SELECT HOUR(created_at) as hour, COUNT(*) as count + FROM {$wpdb->prefix}forum_posts + WHERE created_at >= %s AND deleted_at IS NULL + GROUP BY HOUR(created_at) ORDER BY hour ASC", + $since + )); + + return compact('posts_per_day','threads_per_day','registrations','top_posters','active_hours'); + } + + // ── Benutzerdefinierte Profilfelder ─────────────────────────────────────── + + public static function get_profile_field_defs() { + $fields = get_option( 'wbf_profile_fields', [] ); + return is_array( $fields ) ? $fields : []; + } + + public static function save_profile_field_defs( $fields ) { + update_option( 'wbf_profile_fields', $fields ); + } + + public static function get_user_meta( $user_id ) { + global $wpdb; + $rows = $wpdb->get_results( $wpdb->prepare( + "SELECT meta_key, meta_value FROM {$wpdb->prefix}forum_user_meta WHERE user_id = %d", + (int) $user_id + ) ); + $out = []; + foreach ( $rows as $r ) $out[ $r->meta_key ] = $r->meta_value; + return $out; + } + + public static function set_user_meta( $user_id, $key, $value ) { + global $wpdb; + $wpdb->replace( + "{$wpdb->prefix}forum_user_meta", + [ 'user_id' => (int) $user_id, 'meta_key' => $key, 'meta_value' => $value ], + [ '%d', '%s', '%s' ] + ); + } + + public static function delete_user_meta_all( $user_id ) { + global $wpdb; + $wpdb->delete( "{$wpdb->prefix}forum_user_meta", [ 'user_id' => (int) $user_id ] ); + } + + // ── Zeitlich begrenzte Sperren ──────────────────────────────────────────── + + /** + * Setzt eine zeitlich begrenzte Sperre für einen User. + * Speichert die vorherige Rolle in pre_ban_role. + * + * @param int $user_id + * @param string $until MySQL DATETIME z.B. '2025-12-31 23:59:00' + * @param string $reason Sperrgrund + */ + public static function temp_ban( $user_id, $until, $reason = '' ) { + global $wpdb; + $user = self::get_user( (int) $user_id ); + if ( ! $user || $user->role === 'superadmin' ) return false; + + $wpdb->update( + "{$wpdb->prefix}forum_users", + [ + 'pre_ban_role' => $user->role !== 'banned' ? $user->role : ( $user->pre_ban_role ?: 'member' ), + 'role' => 'banned', + 'ban_reason' => $reason, + 'ban_until' => $until, + ], + [ 'id' => (int) $user_id ], + [ '%s', '%s', '%s', '%s' ], + [ '%d' ] + ); + return true; + } + + /** + * Hebt abgelaufene Sperren auf — läuft per WP-Cron täglich. + * Gibt Anzahl entsperrter User zurück. + */ + public static function check_expired_bans() { + global $wpdb; + $expired = $wpdb->get_results( + "SELECT id, pre_ban_role FROM {$wpdb->prefix}forum_users + WHERE role = 'banned' + AND ban_until IS NOT NULL + AND ban_until <= NOW()" + ); + $count = 0; + foreach ( $expired as $u ) { + $restore = ! empty( $u->pre_ban_role ) ? $u->pre_ban_role : 'member'; + $wpdb->update( + "{$wpdb->prefix}forum_users", + [ 'role' => $restore, 'ban_reason' => '', 'ban_until' => null, 'pre_ban_role' => '' ], + [ 'id' => (int) $u->id ], + [ '%s', '%s', null, '%s' ], + [ '%d' ] + ); + $count++; + } + return $count; + } + + // ── Umfragen (Polls) ────────────────────────────────────────────────────── + + public static function create_poll( $thread_id, $question, $options, $multi = false, $ends_at = null ) { + global $wpdb; + $wpdb->insert( "{$wpdb->prefix}forum_polls", [ + 'thread_id' => (int) $thread_id, + 'question' => sanitize_text_field( $question ), + 'options' => wp_json_encode( array_values( $options ) ), + 'multi' => $multi ? 1 : 0, + 'ends_at' => $ends_at, + ]); + return $wpdb->insert_id; + } + + public static function get_poll( $thread_id ) { + global $wpdb; + $row = $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}forum_polls WHERE thread_id = %d", (int) $thread_id + ) ); + if ( ! $row ) return null; + $row->options = json_decode( $row->options, true ) ?: []; + return $row; + } + + public static function get_poll_results( $poll_id ) { + global $wpdb; + $rows = $wpdb->get_results( $wpdb->prepare( + "SELECT option_idx, COUNT(*) AS votes FROM {$wpdb->prefix}forum_poll_votes + WHERE poll_id = %d GROUP BY option_idx", (int) $poll_id + ) ); + $out = []; + foreach ( $rows as $r ) $out[(int)$r->option_idx] = (int)$r->votes; + return $out; + } + + public static function get_user_votes( $poll_id, $user_id ) { + global $wpdb; + return array_map( fn($r) => (int)$r->option_idx, + $wpdb->get_results( $wpdb->prepare( + "SELECT option_idx FROM {$wpdb->prefix}forum_poll_votes WHERE poll_id=%d AND user_id=%d", + (int) $poll_id, (int) $user_id + ) ) + ); + } + + public static function vote_poll( $poll_id, $user_id, $option_idxs ) { + global $wpdb; + $poll = $wpdb->get_row( $wpdb->prepare( + "SELECT ends_at, multi FROM {$wpdb->prefix}forum_polls WHERE id=%d", (int) $poll_id + ) ); + if ( ! $poll ) return false; + if ( $poll->ends_at && strtotime( $poll->ends_at ) < time() ) return false; + if ( ! empty( self::get_user_votes( $poll_id, $user_id ) ) ) return false; + if ( ! $poll->multi ) $option_idxs = [ (int)$option_idxs[0] ]; + foreach ( $option_idxs as $idx ) { + $wpdb->insert( "{$wpdb->prefix}forum_poll_votes", [ + 'poll_id' => (int)$poll_id, 'user_id' => (int)$user_id, 'option_idx' => (int)$idx, + ]); + } + return true; + } + + public static function delete_poll( $thread_id ) { + global $wpdb; + $poll = self::get_poll( $thread_id ); + if ( ! $poll ) return; + $wpdb->delete( "{$wpdb->prefix}forum_poll_votes", [ 'poll_id' => $poll->id ] ); + $wpdb->delete( "{$wpdb->prefix}forum_polls", [ 'id' => $poll->id ] ); + } + + // ── Thread-Präfixe ──────────────────────────────────────────────────────── + + public static function get_prefixes() { + global $wpdb; + return $wpdb->get_results( + "SELECT * FROM {$wpdb->prefix}forum_prefixes ORDER BY sort_order ASC, id ASC" + ); + } + + public static function get_prefix( $id ) { + global $wpdb; + return $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}forum_prefixes WHERE id=%d", (int)$id + )); + } + + public static function create_prefix( $data ) { + global $wpdb; + $wpdb->insert( "{$wpdb->prefix}forum_prefixes", $data ); + return $wpdb->insert_id; + } + + public static function update_prefix( $id, $data ) { + global $wpdb; + $wpdb->update( "{$wpdb->prefix}forum_prefixes", $data, ['id' => (int)$id] ); + } + + public static function delete_prefix( $id ) { + global $wpdb; + // Präfix bei betroffenen Threads entfernen + $wpdb->update( "{$wpdb->prefix}forum_threads", ['prefix_id' => null], ['prefix_id' => (int)$id] ); + $wpdb->delete( "{$wpdb->prefix}forum_prefixes", ['id' => (int)$id] ); + } + + // ── Lesezeichen ─────────────────────────────────────────────────────────── + + public static function is_bookmarked( $user_id, $thread_id ) { + global $wpdb; + return (bool)$wpdb->get_var( $wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}forum_bookmarks WHERE user_id=%d AND thread_id=%d", + (int)$user_id, (int)$thread_id + )); + } + + public static function toggle_bookmark( $user_id, $thread_id ) { + global $wpdb; + if ( self::is_bookmarked( $user_id, $thread_id ) ) { + $wpdb->delete( "{$wpdb->prefix}forum_bookmarks", [ + 'user_id' => (int)$user_id, + 'thread_id' => (int)$thread_id, + ]); + return false; // removed + } + $wpdb->insert( "{$wpdb->prefix}forum_bookmarks", [ + 'user_id' => (int)$user_id, + 'thread_id' => (int)$thread_id, + ]); + return true; // added + } + + public static function get_user_bookmarks( $user_id, $limit = 50 ) { + global $wpdb; + return $wpdb->get_results( $wpdb->prepare( + "SELECT t.id, t.title, t.reply_count, t.views, t.created_at, t.last_reply_at, + t.prefix_id, t.status, t.pinned, + u.display_name, u.avatar_url, u.role as author_role, + c.name as cat_name, c.slug as cat_slug, + b.created_at as bookmarked_at + FROM {$wpdb->prefix}forum_bookmarks b + JOIN {$wpdb->prefix}forum_threads t ON t.id = b.thread_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 b.user_id = %d AND t.deleted_at IS NULL + ORDER BY b.created_at DESC + LIMIT %d", + (int)$user_id, $limit + )); + } + + // ── Wortfilter ──────────────────────────────────────────────────────────── + + public static function get_word_filter() { + $raw = get_option( 'wbf_word_filter', '' ); + if ( empty( $raw ) ) return []; + return array_values( array_filter( array_map( 'trim', explode( "\n", $raw ) ) ) ); + } + + public static function apply_word_filter( $text ) { + $words = self::get_word_filter(); + if ( empty( $words ) ) return $text; + foreach ( $words as $word ) { + if ( empty($word) ) continue; + $replacement = str_repeat( '*', mb_strlen($word) ); + $text = preg_replace( '/\b' . preg_quote($word, '/') . '\b/iu', $replacement, $text ); + } + return $text; + } + + // ── Flood Control ───────────────────────────────────────────────────────── + + public static function check_flood( $user_id ) { + $interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 ); + if ( $interval <= 0 ) return true; // deaktiviert + $key = 'wbf_flood_' . (int)$user_id; + $last = get_transient( $key ); + if ( $last !== false ) { + return false; // noch gesperrt + } + set_transient( $key, time(), $interval ); + return true; + } + + public static function flood_remaining( $user_id ) { + $interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 ); + if ( $interval <= 0 ) return 0; + $key = 'wbf_flood_' . (int)$user_id; + $last = get_transient( $key ); + if ( $last === false ) return 0; + // Transients speichern keine genaue Restzeit — wir schätzen über $interval + return $interval; + } + } \ No newline at end of file diff --git a/includes/class-forum-shortcodes.php b/includes/class-forum-shortcodes.php index 536f14b..b32fc71 100644 --- a/includes/class-forum-shortcodes.php +++ b/includes/class-forum-shortcodes.php @@ -30,6 +30,14 @@ class WBF_Shortcodes { return WBF_Roles::badge( $role ); } + public static function render_prefix( $thread ) { + if ( empty($thread->prefix_label) ) return ''; + $label = esc_html($thread->prefix_label); + $color = esc_attr($thread->prefix_color ?? '#fff'); + $bg = esc_attr($thread->prefix_bg ?? '#475569'); + return "{$label}"; + } + public static function render_tags( $tags, $small = false ) { if ( empty($tags) ) return ''; $cls = $small ? 'wbf-tag wbf-tag--sm' : 'wbf-tag'; @@ -54,7 +62,7 @@ class WBF_Shortcodes { } private static function reaction_bar( $object_id, $object_type, $current_user ) { - $emojis = ['👍','❤️','😂','😮','😢','😡']; + $emojis = WBF_DB::get_allowed_reactions(); $user_id = $current_user ? (int)$current_user->id : 0; $data = WBF_DB::get_reactions($object_id, $object_type, $user_id); $counts = $data['counts']; @@ -147,8 +155,39 @@ class WBF_Shortcodes { wp_redirect( wbf_get_forum_url() ); exit; } + + // ── Wartungsmodus — zentraler Check vor allem anderen ──────────────── + $wbf_current_user = WBF_Auth::get_current_user(); + $wbf_maint = wbf_get_settings()['maintenance_mode'] ?? '0'; + if ( $wbf_maint === '1' ) { + $is_staff = $wbf_current_user && WBF_Roles::level($wbf_current_user->role) >= 50; + if ( ! $is_staff ) { + return self::view_maintenance(); + } + } + if (isset($_GET['forum_members'])) return self::view_members(); + // Einladungscode aus URL vorausfüllen + if (isset($_GET['wbf_invite'])) { + $inv_code = strtoupper(sanitize_text_field($_GET['wbf_invite'])); + if (!WBF_DB::verify_invite($inv_code)) { + // Ungültiger Code — zeige Meldung + ob_start(); ?> +
+
+
+ + Dieser Einladungslink ist ungültig oder bereits abgelaufen. +
+
+ ← Zurück zum Forum +
+
+ guest_visible ?? 1) === 0 ) return false; + // Min-Rolle: Nutzer muss mindestens diese Rolle haben um die Kategorie zu sehen + if ( $cat->min_role && $cat->min_role !== 'member' ) { + if ( ! $user ) return false; // nicht eingeloggt → versteckt wenn min_role > member + if ( WBF_Roles::level($user->role) < WBF_Roles::level($cat->min_role) ) return false; + } + return true; + } + // ── HOME ────────────────────────────────────────────────────────────────── private static function view_home() { @@ -195,11 +247,15 @@ class WBF_Shortcodes {

- +
+ + +
- +
@@ -217,7 +273,8 @@ class WBF_Shortcodes {
children)): ?>
- children as $child): ?> + children as $child): + if (!self::can_see_category($current, $child)) continue; ?>
@@ -286,6 +343,7 @@ class WBF_Shortcodes {
+
Kategorie nicht gefunden.

'; + $current = WBF_Auth::get_current_user(); + + // Zugang prüfen — Gäste + Min-Rolle + if (!self::can_see_category($current, $cat)) { + ob_start(); ?> +
+
+
+ + + Diese Kategorie ist nur für eingeloggte Mitglieder sichtbar. + + + Du hast keine Berechtigung um diese Kategorie zu sehen. + +
+
+ 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); @@ -322,12 +399,16 @@ class WBF_Shortcodes {

name); ?>

description); ?>

- +
+ + +
- +
@@ -347,12 +428,14 @@ class WBF_Shortcodes { $liked = $current ? WBF_DB::has_liked($current->id,$t->id,'thread') : false; ?>
+ data-last-reply="last_reply_at); ?>" + data-preview="content)),0,160)); ?>">
avatar_url,$t->display_name); ?>
@@ -419,6 +502,7 @@ class WBF_Shortcodes { id); ?> +
role) < 50 ) ) { + return self::view_maintenance(); + } $id = (int)($_GET['forum_thread'] ?? 0); $thread = WBF_DB::get_thread($id); if (!$thread) return '

Thread nicht gefunden.

'; + // Kategorie-Zugang prüfen (Gäste + Min-Rolle) + $cat6 = WBF_DB::get_category($thread->category_id); + if ($cat6 && !self::can_see_category($cur6, $cat6)) { + ob_start(); ?> +
+
+
+ + + Dieser Thread ist nur für eingeloggte Mitglieder sichtbar. + + + Du hast keine Berechtigung um diesen Thread zu sehen. + +
+
+ query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET views=views+1 WHERE id=%d",$id)); @@ -462,18 +570,101 @@ class WBF_Shortcodes { pinned): ?> status==='closed'): ?> status==='archived'): ?> Archiviert + title); ?>
like_count,$t_liked); ?> + id,$id); ?> + + reply_count; ?> Antworten views; ?> Views
+
- + + id === (int)$thread->user_id && !$poll && $thread->status === 'open'; + if ($can_add_poll): ?> +
+ +
+ id); + $my_votes = $current ? WBF_DB::get_user_votes($poll->id, $current->id) : []; + $total = array_sum($results); + $voted = !empty($my_votes); + $expired = $poll->ends_at && strtotime($poll->ends_at) < time(); + $show_results = $voted || $expired || !$current; + ?> +
+
+ + question); ?> + multi): ?>Mehrfachauswahl + Beendet +
+
+ + + options as $i => $opt): + $votes = $results[$i] ?? 0; + $pct = $total > 0 ? round($votes / $total * 100) : 0; + $mine = in_array($i, $my_votes); + ?> +
+
+
+ + % () +
+
+ + + + +
+ options as $i => $opt): ?> + + +
+ + +
+
+ ends_at): ?> + + + +
+
+ id, $id, 'thread') : false; $op_can_edit = $current && ((int)$current->id === (int)$thread->user_id || WBF_DB::can($current,'delete_post')); @@ -508,7 +699,16 @@ class WBF_Shortcodes { + +
Profil nicht gefunden.

'; $is_own = $current && $current->id == $profile->id; + $is_staff = $current && WBF_Roles::level($current->role) >= 50; + // Profil-Sichtbarkeit prüfen + if (!$is_own && !$is_staff && (int)($profile->profile_public ?? 1) === 0) { + ob_start(); ?> +
+
+
+ Dieses Profil ist nicht öffentlich. +
+
+ id, 50 ); ob_start(); ?> @@ -727,6 +941,33 @@ class WBF_Shortcodes {
+ + id ); + foreach ( $cf_defs_pub as $def ): + if ( ! $is_own && empty($def['public']) ) continue; + $val = trim( $cf_vals_pub[ $def['key'] ] ?? '' ); + if ( $val === '' ) continue; + ?> +
+ + + + + + +

+ +

+ +
+ + @@ -754,7 +995,17 @@ class WBF_Shortcodes {
- +
+ + profile_public ?? 1); ?> + +
+
signature??''); ?>/300
+ + + id ); + if ( ! empty( $cf_defs ) ): + ?> +
+
+ Weitere Profilangaben +
+
+
+ +
+ + + + + + + > + +
+ +
+ +
+
+ +
+
+ Datenschutz & Konto löschen +
+
+

+ Gemäß DSGVO Art. 17 (Recht auf Vergessenwerden) kannst du die vollständige Löschung deines Kontos und aller personenbezogenen Daten beantragen.
+ Deine Beiträge bleiben anonymisiert sichtbar. Direktnachrichten, Likes, Profilinformationen und alle persönlichen Daten werden dauerhaft gelöscht. +

+ + +
+
+ + +
@@ -812,6 +1159,33 @@ class WBF_Shortcodes {
+ + id, 50); ?> +
+
+ Lesezeichen + +
+
+ +

Noch keine Lesezeichen.

+ +
+
+ + + title,0,60)); ?> + + cat_name); ?> + bookmarked_at); ?> +
+
+ +
+
+ + @@ -901,6 +1275,7 @@ class WBF_Shortcodes { + -
-
Bitte um Nachrichten zu lesen.
+
+ +
+
+
+
+
🔒
+

Bitte einloggen

+

Um Nachrichten lesen zu können, musst du eingeloggt sein.

+
+ +
+
+
+ @@ -1006,6 +1394,9 @@ class WBF_Shortcodes { // ── SEARCH ──────────────────────────────────────────────────────────────── private static function view_search() { + $cur_s = WBF_Auth::get_current_user(); + $maint_s = wbf_get_settings()['maintenance_mode'] ?? '0'; + if ($maint_s === '1' && (!$cur_s || WBF_Roles::level($cur_s->role) < 50)) return self::view_maintenance(); $query = sanitize_text_field($_GET['q'] ?? ''); $current = WBF_Auth::get_current_user(); $results = mb_strlen($query) >= 2 ? WBF_DB::search($query, 40) : []; @@ -1053,6 +1444,7 @@ class WBF_Shortcodes {

Ergebnis(se) gefunden.

+
- + @@ -1120,11 +1512,18 @@ class WBF_Shortcodes {
+ private static function render_auth_forms() { + $reg_mode = wbf_get_settings()['registration_mode'] ?? 'open'; + $invite_msg = wbf_get_settings()['invite_message'] ?? 'Registrierung ist aktuell nur auf Einladung möglich.'; + ?>
+ + + +
@@ -1137,6 +1536,20 @@ class WBF_Shortcodes {
Passwort vergessen?
+ + +
+
+ +
+
+ +
+
+ Registrierung ist deaktiviert. +
+
+

Gib deine E-Mail ein — wir schicken dir einen Reset-Link.

@@ -1146,10 +1559,37 @@ class WBF_Shortcodes {
+ +
+ +
+ + + +
+ + +
@@ -1322,6 +1762,58 @@ class WBF_Shortcodes { +
+
+ +

Umfrage erstellen

+ + +
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+ + +
+
+ +
+ + +
+
+
+ (int)$c->parent_id === 0); @@ -1331,7 +1823,7 @@ class WBF_Shortcodes {
-

Neuen Thread erstellen

+

Neuen Thread erstellen

-
-
+
+ +
+ + +
+ +
-
+
@@ -1359,10 +1866,55 @@ class WBF_Shortcodes {
-
+ + +
+
+ + +
role) < 50)) return self::view_maintenance(); if ( ! $current ) { ob_start(); ?>
@@ -1527,11 +2081,133 @@ class WBF_Shortcodes {
+
+
+
+
+
🔧
+

+

+

+ +

+
+
+ +
+ +
+ + + Durch die Nutzung des Forums stimmst du unseren Regeln zu. + + + Forum-Regeln lesen + +
+ Diese Seite ist nicht verfügbar.

'; + } + $title = esc_html( $s['rules_title'] ?? 'Forum-Regeln & Nutzungsbedingungen' ); + $raw = $s['rules_content'] ?? ''; + ob_start(); ?> +
+ +
+ + +
+
+
📜
+
+

+

+ Zuletzt aktualisiert: +  ·  Bitte sorgfältig lesen +

+
+
+
+ +
+
+ + Mit der Nutzung des Forums stimmst du diesen Regeln zu. + + + Zurück zum Forum + +
+
+
+
+ Noch keine Regeln hinterlegt.

'; + } + $paragraphs = preg_split( '/\n{2,}/', trim( $raw ) ); + $out = ''; + foreach ( $paragraphs as $para ) { + $para = trim( $para ); + if ( $para === '' ) continue; + if ( preg_match( '/^\*\*(\d+\.\s*.+?)\*\*$/', $para, $m ) ) { + preg_match('/^(\d+)\.\s*(.+)$/', $m[1], $parts); + $num = esc_html($parts[1] ?? ''); + $text = esc_html($parts[2] ?? $m[1]); + $out .= '
' + . '' . $num . '' + . '

' . $text . '

' + . '
'; + } else { + $para = htmlspecialchars( $para, ENT_QUOTES, 'UTF-8' ); + $para = preg_replace( '/\*\*(.+?)\*\*/s', '$1', $para ); + $para = nl2br( $para ); + $out .= '

' . $para . '

'; + } + } + return $out; + } + } WBF_Shortcodes::init(); \ No newline at end of file