Update from Git Manager GUI

This commit is contained in:
2026-03-21 18:47:28 +01:00
parent 5d7a4743d7
commit 6fff4f9dc2
4 changed files with 1710 additions and 37 deletions

View File

@@ -9,8 +9,15 @@ class WBF_Ajax {
'wbf_new_thread', 'wbf_new_post', 'wbf_toggle_like', 'wbf_new_thread', 'wbf_new_post', 'wbf_toggle_like',
'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image', 'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image',
'wbf_forgot_password', 'wbf_reset_password', 'wbf_load_more_messages', '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_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_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) { foreach ($actions as $action) {
add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$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() { 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( $result = WBF_Auth::register(
sanitize_text_field($_POST['username'] ?? ''), sanitize_text_field($_POST['username'] ?? ''),
sanitize_email( $_POST['email'] ?? ''), sanitize_email( $_POST['email'] ?? ''),
@@ -59,6 +96,12 @@ class WBF_Ajax {
); );
if ($result['success']) { if ($result['success']) {
$u = $result['user']; $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]); wp_send_json_success(['display_name'=>$u->display_name,'avatar_url'=>$u->avatar_url,'user_id'=>$u->id]);
} else { } else {
wp_send_json_error($result); wp_send_json_error($result);
@@ -79,23 +122,40 @@ class WBF_Ajax {
if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
if (!WBF_DB::can($user, 'create_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']); 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'] ?? ''); $title = sanitize_text_field($_POST['title'] ?? '');
$content = WBF_BBCode::sanitize( $_POST['content'] ?? '' ); $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' );
$category_id = (int)($_POST['category_id'] ?? 0); $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($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.']); 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); $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.']); 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([ $id = WBF_DB::create_thread([
'category_id' => $category_id, 'category_id' => $category_id,
'user_id' => $user->id, 'user_id' => $user->id,
'title' => $title, 'title' => WBF_DB::apply_word_filter($title),
'slug' => sanitize_title($title) . '-' . time(), 'slug' => sanitize_title($title) . '-' . time(),
'content' => $content, 'content' => WBF_DB::apply_word_filter($content),
'prefix_id' => $prefix_id,
]); ]);
// Tags speichern // Tags speichern
@@ -104,6 +164,23 @@ class WBF_Ajax {
WBF_DB::sync_thread_tags( $id, $raw_tags ); 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!']); 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 (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
if (!WBF_DB::can($user, 'post')) wp_send_json_error(['message'=>'Keine Berechtigung.']); 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); $thread_id = (int)($_POST['thread_id'] ?? 0);
$content = WBF_BBCode::sanitize( $_POST['content'] ?? '' ); $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 (strlen($content) < 3) wp_send_json_error(['message'=>'Antwort zu kurz.']);
if (!$thread_id) wp_send_json_error(['message'=>'Ungültiger Thread.']); 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); $notif_users = WBF_DB::get_thread_participants($thread_id);
foreach ($notif_users as $participant_id) { foreach ($notif_users as $participant_id) {
WBF_DB::create_notification($participant_id, 'reply', $thread_id, $user->id); WBF_DB::create_notification($participant_id, 'reply', $thread_id, $user->id);
// E-Mail
$notif_user = WBF_DB::get_user($participant_id); $notif_user = WBF_DB::get_user($participant_id);
self::send_notification_email($notif_user, 'reply', $user->display_name, [ self::send_notification_email($notif_user, 'reply', $user->display_name, [
'thread_id' => $thread_id, 'thread_id' => $thread_id,
'thread_title' => $thread->title, '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 // @Erwähnungen
$mentioned = WBF_DB::extract_mentions($content); $mentioned = WBF_DB::extract_mentions($content);
foreach ($mentioned as $m_user) { 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.']); if (!WBF_DB::can($user,'delete_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']);
$thread = WBF_DB::get_thread($object_id); $thread = WBF_DB::get_thread($object_id);
if (!$thread) wp_send_json_error(['message'=>'Thread nicht gefunden.']); 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('')]); wp_send_json_success(['action'=>'deleted','redirect'=>'?forum_cat='.urlencode('')]);
break; break;
case 'delete_post': case 'delete_post':
if (!WBF_DB::can($user,'delete_post')) wp_send_json_error(['message'=>'Keine Berechtigung.']); 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']); wp_send_json_success(['action'=>'post_deleted']);
break; break;
@@ -278,6 +373,34 @@ class WBF_Ajax {
} }
WBF_DB::update_user($user->id, $update); 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!']); wp_send_json_success(['message'=>'Profil gespeichert!']);
} }
@@ -394,6 +517,17 @@ class WBF_Ajax {
wp_send_json_error(['message' => 'Keine Berechtigung.']); 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->update(
"{$wpdb->prefix}forum_posts", "{$wpdb->prefix}forum_posts",
['content' => $content, 'updated_at' => current_time('mysql')], ['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(); add_action( 'init', [ 'WBF_Ajax', 'init' ] );

View File

@@ -44,7 +44,27 @@ class WBF_Auth {
return array( 'success' => false, 'message' => 'Falsches Passwort.' ); return array( 'success' => false, 'message' => 'Falsches Passwort.' );
} }
if ( WBF_Roles::level($user->role) < 0 ) { 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.'; $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 ); return array( 'success' => false, 'banned' => true, 'message' => $reason );
} }
$_SESSION[ self::SESSION_KEY ] = $user->id; $_SESSION[ self::SESSION_KEY ] = $user->id;

View File

@@ -96,10 +96,10 @@ class WBF_DB {
$sql_tags = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_tags ( $sql_tags = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_tags (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(60) NOT NULL, name VARCHAR(60) NOT NULL,
slug VARCHAR(60) NOT NULL UNIQUE, slug VARCHAR(60) NOT NULL,
use_count INT DEFAULT 0, use_count INT DEFAULT 0,
PRIMARY KEY (id), PRIMARY KEY (id),
KEY slug (slug) UNIQUE KEY slug (slug)
) $charset;"; ) $charset;";
$sql_thread_tags = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_thread_tags ( $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_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", '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", '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', "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"); 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 ( $sql_notifications = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_notifications (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL,
@@ -182,7 +202,87 @@ class WBF_DB {
dbDelta( $sql_reports ); dbDelta( $sql_reports );
dbDelta( $sql_notifications ); 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"); $count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_categories");
if ( (int)$count === 0 ) { 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]); $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; $offset = ($page - 1) * $per_page;
$status_sql = $include_archived ? '' : "AND t.status != 'archived'"; $status_sql = $include_archived ? '' : "AND t.status != 'archived'";
return $wpdb->get_results($wpdb->prepare( 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 FROM {$wpdb->prefix}forum_threads t
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id 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 ORDER BY t.pinned DESC, t.last_reply_at DESC
LIMIT %d OFFSET %d", LIMIT %d OFFSET %d",
$category_id, $per_page, $offset $category_id, $per_page, $offset
@@ -351,7 +453,7 @@ class WBF_DB {
public static function count_threads( $category_id ) { public static function count_threads( $category_id ) {
global $wpdb; global $wpdb;
return (int)$wpdb->get_var($wpdb->prepare( 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 $category_id
)); ));
} }
@@ -393,9 +495,11 @@ class WBF_DB {
global $wpdb; global $wpdb;
return $wpdb->get_row($wpdb->prepare( return $wpdb->get_row($wpdb->prepare(
"SELECT t.*, u.display_name, u.avatar_url, u.username, u.signature, "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 FROM {$wpdb->prefix}forum_threads t
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id
WHERE t.id = %d", $id WHERE t.id = %d", $id
)); ));
} }
@@ -447,7 +551,7 @@ class WBF_DB {
u.post_count as author_posts, u.role as author_role, u.registered as author_registered u.post_count as author_posts, u.role as author_role, u.registered as author_registered
FROM {$wpdb->prefix}forum_posts p FROM {$wpdb->prefix}forum_posts p
JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id 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 ORDER BY p.created_at ASC
LIMIT %d OFFSET %d", LIMIT %d OFFSET %d",
$thread_id, $per_page, $offset $thread_id, $per_page, $offset
@@ -885,9 +989,16 @@ class WBF_DB {
// ── Reaktionen ──────────────────────────────────────────────────────────── // ── 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 ) { public static function set_reaction( $user_id, $object_id, $object_type, $reaction ) {
global $wpdb; global $wpdb;
$allowed = ['👍','❤️','😂','😮','😢','😡']; $allowed = self::get_allowed_reactions();
if ( ! in_array($reaction, $allowed, true) ) return false; if ( ! in_array($reaction, $allowed, true) ) return false;
$existing = $wpdb->get_row( $wpdb->prepare( $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;
}
} }

View File

@@ -30,6 +30,14 @@ class WBF_Shortcodes {
return WBF_Roles::badge( $role ); 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 "<span class=\"wbf-prefix-badge\" style=\"color:{$color};background:{$bg}\">{$label}</span>";
}
public static function render_tags( $tags, $small = false ) { public static function render_tags( $tags, $small = false ) {
if ( empty($tags) ) return ''; if ( empty($tags) ) return '';
$cls = $small ? 'wbf-tag wbf-tag--sm' : 'wbf-tag'; $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 ) { 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; $user_id = $current_user ? (int)$current_user->id : 0;
$data = WBF_DB::get_reactions($object_id, $object_type, $user_id); $data = WBF_DB::get_reactions($object_id, $object_type, $user_id);
$counts = $data['counts']; $counts = $data['counts'];
@@ -147,8 +155,39 @@ class WBF_Shortcodes {
wp_redirect( wbf_get_forum_url() ); wp_redirect( wbf_get_forum_url() );
exit; 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(); 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(); ?>
<div class="wbf-wrap"><?php self::render_topbar(null); ?>
<div class="wbf-container wbf-mt">
<div class="wbf-notice wbf-notice--warning">
<i class="fas fa-exclamation-triangle"></i>
Dieser Einladungslink ist ungültig oder bereits abgelaufen.
</div>
<div style="margin-top:1rem">
<a href="<?php echo esc_url(wbf_get_forum_url()); ?>" class="wbf-btn wbf-btn--sm">← Zurück zum Forum</a>
</div>
</div></div>
<?php return ob_get_clean();
}
}
if (isset($_GET['wbf_reset_token'])) return self::view_reset_password(); if (isset($_GET['wbf_reset_token'])) return self::view_reset_password();
if (isset($_GET['forum_rules'])) return self::view_rules();
if (isset($_GET['forum_thread'])) return self::view_thread(); if (isset($_GET['forum_thread'])) return self::view_thread();
if (isset($_GET['forum_cat'])) return self::view_category(); if (isset($_GET['forum_cat'])) return self::view_category();
if (isset($_GET['forum_profile'])) return self::view_profile(); if (isset($_GET['forum_profile'])) return self::view_profile();
@@ -158,6 +197,19 @@ class WBF_Shortcodes {
return self::view_home(); return self::view_home();
} }
/** Darf der Nutzer diese Kategorie sehen? */
private static function can_see_category( $user, $cat ) {
// Gäste: guest_visible prüfen
if ( ! $user && (int)($cat->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 ────────────────────────────────────────────────────────────────── // ── HOME ──────────────────────────────────────────────────────────────────
private static function view_home() { private static function view_home() {
@@ -195,11 +247,15 @@ class WBF_Shortcodes {
<div class="wbf-section-header"> <div class="wbf-section-header">
<h2><?php echo esc_html(wbf_get_settings()['section_cats']); ?></h2> <h2><?php echo esc_html(wbf_get_settings()['section_cats']); ?></h2>
<?php if (WBF_DB::can($current,'create_thread')): ?> <?php if (WBF_DB::can($current,'create_thread')): ?>
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<button class="wbf-btn wbf-btn--outline-poll" onclick="wbfShowNewPoll()"><i class="fas fa-chart-bar"></i> Neue Umfrage</button>
<button class="wbf-btn wbf-btn--primary" onclick="wbfShowNewThread()"><i class="fas fa-plus"></i> <?php echo esc_html(wbf_get_settings()['btn_new_thread']); ?></button> <button class="wbf-btn wbf-btn--primary" onclick="wbfShowNewThread()"><i class="fas fa-plus"></i> <?php echo esc_html(wbf_get_settings()['btn_new_thread']); ?></button>
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="wbf-cat-group-list"> <div class="wbf-cat-group-list">
<?php foreach ($tree as $parent): ?> <?php foreach ($tree as $parent):
if (!self::can_see_category($current, $parent)) continue; ?>
<div class="wbf-cat-group"> <div class="wbf-cat-group">
<div class="wbf-cat-parent"> <div class="wbf-cat-parent">
<div class="wbf-cat-parent__icon"><i class="<?php echo esc_attr($parent->icon); ?>"></i></div> <div class="wbf-cat-parent__icon"><i class="<?php echo esc_attr($parent->icon); ?>"></i></div>
@@ -217,7 +273,8 @@ class WBF_Shortcodes {
</div> </div>
<?php if (!empty($parent->children)): ?> <?php if (!empty($parent->children)): ?>
<div class="wbf-cat-children"> <div class="wbf-cat-children">
<?php foreach ($parent->children as $child): ?> <?php foreach ($parent->children as $child):
if (!self::can_see_category($current, $child)) continue; ?>
<div class="wbf-cat-child"> <div class="wbf-cat-child">
<div class="wbf-cat-child__icon"><i class="<?php echo esc_attr($child->icon); ?>"></i></div> <div class="wbf-cat-child__icon"><i class="<?php echo esc_attr($child->icon); ?>"></i></div>
<div class="wbf-cat-child__body"> <div class="wbf-cat-child__body">
@@ -286,6 +343,7 @@ class WBF_Shortcodes {
</div> </div>
</div> </div>
<?php self::render_new_thread_modal(WBF_DB::get_categories_flat(), $current); ?> <?php self::render_new_thread_modal(WBF_DB::get_categories_flat(), $current); ?>
<?php self::render_forum_footer(); ?>
<?php self::render_auth_modal(); ?> <?php self::render_auth_modal(); ?>
</div> </div>
<?php return ob_get_clean(); <?php return ob_get_clean();
@@ -298,11 +356,30 @@ class WBF_Shortcodes {
$cat = WBF_DB::get_category($slug); $cat = WBF_DB::get_category($slug);
if (!$cat) return '<p class="wbf-notice">Kategorie nicht gefunden.</p>'; if (!$cat) return '<p class="wbf-notice">Kategorie nicht gefunden.</p>';
$current = WBF_Auth::get_current_user();
// Zugang prüfen — Gäste + Min-Rolle
if (!self::can_see_category($current, $cat)) {
ob_start(); ?>
<div class="wbf-wrap"><?php self::render_topbar($current); ?>
<div class="wbf-container wbf-mt">
<div class="wbf-notice wbf-notice--warning">
<i class="fas fa-lock"></i>
<?php if (!$current): ?>
Diese Kategorie ist nur für eingeloggte Mitglieder sichtbar.
<a href="#" class="wbf-login-link" style="margin-left:.5rem;font-weight:700">Jetzt einloggen</a>
<?php else: ?>
Du hast keine Berechtigung um diese Kategorie zu sehen.
<?php endif; ?>
</div>
</div></div>
<?php return ob_get_clean();
}
$page = max(1,(int)($_GET['fp']??1)); $page = max(1,(int)($_GET['fp']??1));
$threads = WBF_DB::get_threads($cat->id, $page); $threads = WBF_DB::get_threads($cat->id, $page);
$total = WBF_DB::count_threads($cat->id); $total = WBF_DB::count_threads($cat->id);
$pages = ceil($total / 20) ?: 1; $pages = ceil($total / 20) ?: 1;
$current = WBF_Auth::get_current_user();
$children = WBF_DB::get_child_categories($cat->id); $children = WBF_DB::get_child_categories($cat->id);
$crumbs = WBF_DB::get_category_breadcrumb($cat); $crumbs = WBF_DB::get_category_breadcrumb($cat);
@@ -322,12 +399,16 @@ class WBF_Shortcodes {
<div><h2><i class="<?php echo esc_attr($cat->icon); ?>"></i> <?php echo esc_html($cat->name); ?></h2> <div><h2><i class="<?php echo esc_attr($cat->icon); ?>"></i> <?php echo esc_html($cat->name); ?></h2>
<p class="wbf-muted"><?php echo esc_html($cat->description); ?></p></div> <p class="wbf-muted"><?php echo esc_html($cat->description); ?></p></div>
<?php if (WBF_DB::can($current,'create_thread') && WBF_DB::can_post_in($current,$cat)): ?> <?php if (WBF_DB::can($current,'create_thread') && WBF_DB::can_post_in($current,$cat)): ?>
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<button class="wbf-btn wbf-btn--outline-poll" onclick="wbfShowNewPoll(<?php echo $cat->id; ?>)"><i class="fas fa-chart-bar"></i> Neue Umfrage</button>
<button class="wbf-btn wbf-btn--primary" onclick="wbfShowNewThread(<?php echo $cat->id; ?>)"><i class="fas fa-plus"></i> <?php echo esc_html(wbf_get_settings()['btn_new_thread']); ?></button> <button class="wbf-btn wbf-btn--primary" onclick="wbfShowNewThread(<?php echo $cat->id; ?>)"><i class="fas fa-plus"></i> <?php echo esc_html(wbf_get_settings()['btn_new_thread']); ?></button>
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php if (!empty($children)): ?> <?php if (!empty($children)): ?>
<div class="wbf-subcat-list"> <div class="wbf-subcat-list">
<?php foreach ($children as $child): ?> <?php foreach ($children as $child):
if (!self::can_see_category($current, $child)) continue; ?>
<a href="?forum_cat=<?php echo esc_attr($child->slug); ?>" class="wbf-subcat-card"> <a href="?forum_cat=<?php echo esc_attr($child->slug); ?>" class="wbf-subcat-card">
<div class="wbf-subcat-card__icon"><i class="<?php echo esc_attr($child->icon); ?>"></i></div> <div class="wbf-subcat-card__icon"><i class="<?php echo esc_attr($child->icon); ?>"></i></div>
<div class="wbf-subcat-card__body"> <div class="wbf-subcat-card__body">
@@ -347,12 +428,14 @@ class WBF_Shortcodes {
$liked = $current ? WBF_DB::has_liked($current->id,$t->id,'thread') : false; ?> $liked = $current ? WBF_DB::has_liked($current->id,$t->id,'thread') : false; ?>
<div class="wbf-thread-row<?php echo $t->pinned?' wbf-thread-row--pinned':''; ?>" <div class="wbf-thread-row<?php echo $t->pinned?' wbf-thread-row--pinned':''; ?>"
data-thread-id="<?php echo (int)$t->id; ?>" data-thread-id="<?php echo (int)$t->id; ?>"
data-last-reply="<?php echo esc_attr($t->last_reply_at); ?>"> data-last-reply="<?php echo esc_attr($t->last_reply_at); ?>"
data-preview="<?php echo esc_attr(mb_substr(strip_tags(WBF_BBCode::render($t->content)),0,160)); ?>">
<div class="wbf-thread-row__avatar"><?php echo self::avatar($t->avatar_url,$t->display_name); ?></div> <div class="wbf-thread-row__avatar"><?php echo self::avatar($t->avatar_url,$t->display_name); ?></div>
<div class="wbf-thread-row__body"> <div class="wbf-thread-row__body">
<div class="wbf-thread-row__top"> <div class="wbf-thread-row__top">
<?php if ($t->pinned): ?><span class="wbf-pin"><i class="fas fa-thumbtack"></i></span><?php endif; ?> <?php if ($t->pinned): ?><span class="wbf-pin"><i class="fas fa-thumbtack"></i></span><?php endif; ?>
<?php if ($t->status==='closed'): ?><span class="wbf-badge wbf-badge--closed"><i class="fas fa-lock"></i> Geschlossen</span><?php endif; ?> <?php if ($t->status==='closed'): ?><span class="wbf-badge wbf-badge--closed"><i class="fas fa-lock"></i> Geschlossen</span><?php endif; ?>
<?php echo self::render_prefix($t); ?>
<a href="?forum_thread=<?php echo (int)$t->id; ?>" class="wbf-thread-row__title"><?php echo esc_html($t->title); ?></a> <a href="?forum_thread=<?php echo (int)$t->id; ?>" class="wbf-thread-row__title"><?php echo esc_html($t->title); ?></a>
<span class="wbf-new-badge" style="display:none"><i class="fas fa-circle-dot"></i> Neu</span> <span class="wbf-new-badge" style="display:none"><i class="fas fa-circle-dot"></i> Neu</span>
</div> </div>
@@ -419,6 +502,7 @@ class WBF_Shortcodes {
<?php endif; endif; ?> <?php endif; endif; ?>
<?php self::render_new_thread_modal(WBF_DB::get_categories_flat(),$current,$cat->id); ?> <?php self::render_new_thread_modal(WBF_DB::get_categories_flat(),$current,$cat->id); ?>
<?php self::render_forum_footer(); ?>
<?php self::render_auth_modal(); ?> <?php self::render_auth_modal(); ?>
</div> </div>
<?php return ob_get_clean(); <?php return ob_get_clean();
@@ -427,9 +511,33 @@ class WBF_Shortcodes {
// ── THREAD ──────────────────────────────────────────────────────────────── // ── THREAD ────────────────────────────────────────────────────────────────
private static function view_thread() { private static function view_thread() {
// Wartungsmodus
$maint6 = wbf_get_settings()['maintenance_mode'] ?? '0';
$cur6 = WBF_Auth::get_current_user();
if ( $maint6 === '1' && ( !$cur6 || WBF_Roles::level($cur6->role) < 50 ) ) {
return self::view_maintenance();
}
$id = (int)($_GET['forum_thread'] ?? 0); $id = (int)($_GET['forum_thread'] ?? 0);
$thread = WBF_DB::get_thread($id); $thread = WBF_DB::get_thread($id);
if (!$thread) return '<p class="wbf-notice">Thread nicht gefunden.</p>'; if (!$thread) return '<p class="wbf-notice">Thread nicht gefunden.</p>';
// 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(); ?>
<div class="wbf-wrap"><?php self::render_topbar($cur6); ?>
<div class="wbf-container wbf-mt">
<div class="wbf-notice wbf-notice--warning">
<i class="fas fa-lock"></i>
<?php if (!$cur6): ?>
Dieser Thread ist nur für eingeloggte Mitglieder sichtbar.
<a href="#" class="wbf-login-link" style="margin-left:.5rem;font-weight:700">Jetzt einloggen</a>
<?php else: ?>
Du hast keine Berechtigung um diesen Thread zu sehen.
<?php endif; ?>
</div>
</div></div>
<?php return ob_get_clean();
}
global $wpdb; global $wpdb;
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET views=views+1 WHERE id=%d",$id)); $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET views=views+1 WHERE id=%d",$id));
@@ -462,18 +570,101 @@ class WBF_Shortcodes {
<?php if ($thread->pinned): ?><i class="fas fa-thumbtack wbf-pin-icon"></i><?php endif; ?> <?php if ($thread->pinned): ?><i class="fas fa-thumbtack wbf-pin-icon"></i><?php endif; ?>
<?php if ($thread->status==='closed'): ?><i class="fas fa-lock" style="color:var(--c-muted);font-size:1rem;margin-right:.35rem"></i><?php endif; ?> <?php if ($thread->status==='closed'): ?><i class="fas fa-lock" style="color:var(--c-muted);font-size:1rem;margin-right:.35rem"></i><?php endif; ?>
<?php if ($thread->status==='archived'): ?><span class="wbf-badge wbf-badge--archived"><i class="fas fa-box-archive"></i> Archiviert</span><?php endif; ?> <?php if ($thread->status==='archived'): ?><span class="wbf-badge wbf-badge--archived"><i class="fas fa-box-archive"></i> Archiviert</span><?php endif; ?>
<?php echo self::render_prefix($thread); ?>
<?php echo esc_html($thread->title); ?> <?php echo esc_html($thread->title); ?>
</h1> </h1>
<div class="wbf-thread-header-meta"> <div class="wbf-thread-header-meta">
<?php echo self::like_btn($id,'thread',$thread->like_count,$t_liked); ?> <?php echo self::like_btn($id,'thread',$thread->like_count,$t_liked); ?>
<?php if ($current): $is_bm = WBF_DB::is_bookmarked($current->id,$id); ?>
<button class="wbf-bookmark-btn<?php echo $is_bm?' wbf-bookmarked':''; ?>" data-thread="<?php echo $id; ?>" title="<?php echo $is_bm?'Lesezeichen entfernen':'Lesezeichen hinzufügen'; ?>">
<i class="fa<?php echo $is_bm?'s':'r'; ?> fa-bookmark"></i>
</button>
<?php endif; ?>
<span><i class="fas fa-comment-dots"></i> <?php echo (int)$thread->reply_count; ?> Antworten</span> <span><i class="fas fa-comment-dots"></i> <?php echo (int)$thread->reply_count; ?> Antworten</span>
<span><i class="fas fa-eye"></i> <?php echo (int)$thread->views; ?> Views</span> <span><i class="fas fa-eye"></i> <?php echo (int)$thread->views; ?> Views</span>
</div> </div>
<?php $thread_tags = WBF_DB::get_thread_tags($id); echo self::render_tags($thread_tags); ?> <?php $thread_tags = WBF_DB::get_thread_tags($id); echo self::render_tags($thread_tags); ?>
</div> </div>
</div>
<div class="wbf-posts" id="wbfPosts"> <div class="wbf-posts" id="wbfPosts">
<!-- OP --> <!-- Poll (optional) -->
<?php
$poll = WBF_DB::get_poll($id);
$can_add_poll = $current && (int)$current->id === (int)$thread->user_id && !$poll && $thread->status === 'open';
if ($can_add_poll): ?>
<div style="margin-bottom:1rem">
<button class="wbf-btn wbf-btn--sm wbf-btn--outline" id="wbfOpenPollModal">
<i class="fas fa-chart-bar"></i> Umfrage hinzufügen
</button>
</div>
<?php endif;
if ($poll):
$results = WBF_DB::get_poll_results($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;
?>
<div class="wbf-poll" id="wbfPoll-<?php echo (int)$poll->id; ?>" data-poll-id="<?php echo (int)$poll->id; ?>" data-multi="<?php echo (int)$poll->multi; ?>">
<div class="wbf-poll__header">
<i class="fas fa-chart-bar"></i>
<span class="wbf-poll__title"><?php echo esc_html($poll->question); ?></span>
<?php if ($poll->multi): ?><span class="wbf-poll__badge">Mehrfachauswahl</span><?php endif; ?>
<?php if ($expired): ?><span class="wbf-poll__badge wbf-poll__badge--ended">Beendet</span><?php endif; ?>
</div>
<div class="wbf-poll__body">
<?php if ($show_results): ?>
<!-- Ergebnisse -->
<?php foreach ($poll->options as $i => $opt):
$votes = $results[$i] ?? 0;
$pct = $total > 0 ? round($votes / $total * 100) : 0;
$mine = in_array($i, $my_votes);
?>
<div class="wbf-poll__result<?php echo $mine?' wbf-poll__result--mine':''; ?>">
<div class="wbf-poll__result-bar" style="width:<?php echo $pct; ?>%"></div>
<div class="wbf-poll__result-content">
<span class="wbf-poll__result-label"><?php if($mine): ?><i class="fas fa-check-circle" style="color:var(--c-primary)"></i> <?php endif; ?><?php echo esc_html($opt); ?></span>
<span class="wbf-poll__result-pct"><?php echo $pct; ?>% <span style="color:var(--c-muted);font-size:.75em">(<?php echo $votes; ?>)</span></span>
</div>
</div>
<?php endforeach; ?>
<div class="wbf-poll__footer">
<i class="fas fa-users"></i> <?php echo $total; ?> Stimme<?php echo $total!=1?'n':''; ?>
<?php if ($poll->ends_at && !$expired): ?>
· <i class="fas fa-clock"></i> Endet <?php echo esc_html(date_i18n('d.m.Y \u\m H:i \U\h\r', strtotime($poll->ends_at))); ?>
<?php elseif ($expired): ?>
· <i class="fas fa-flag-checkered"></i> Abgestimmt
<?php endif; ?>
</div>
<?php else: ?>
<!-- Abstimmung -->
<form class="wbf-poll__form" data-poll-id="<?php echo (int)$poll->id; ?>">
<?php foreach ($poll->options as $i => $opt): ?>
<label class="wbf-poll__option">
<input type="<?php echo $poll->multi?'checkbox':'radio'; ?>"
name="wbf_poll_option" value="<?php echo $i; ?>"
style="accent-color:var(--c-primary)">
<?php echo esc_html($opt); ?>
</label>
<?php endforeach; ?>
<div style="display:flex;align-items:center;gap:.75rem;margin-top:.75rem">
<button type="submit" class="wbf-btn wbf-btn--sm wbf-btn--primary">
<i class="fas fa-vote-yea"></i> Abstimmen
</button>
<span class="wbf-poll__msg wbf-msg"></span>
</div>
</form>
<?php if ($poll->ends_at): ?>
<div class="wbf-poll__footer">
<i class="fas fa-clock"></i> Endet <?php echo esc_html(date_i18n('d.m.Y \u\m H:i \U\h\r', strtotime($poll->ends_at))); ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php <?php
$op_reported = $current ? WBF_DB::has_reported($current->id, $id, 'thread') : false; $op_reported = $current ? WBF_DB::has_reported($current->id, $id, 'thread') : false;
$op_can_edit = $current && ((int)$current->id === (int)$thread->user_id || WBF_DB::can($current,'delete_post')); $op_can_edit = $current && ((int)$current->id === (int)$thread->user_id || WBF_DB::can($current,'delete_post'));
@@ -508,6 +699,15 @@ class WBF_Shortcodes {
<div class="wbf-post__footer"> <div class="wbf-post__footer">
<span class="wbf-post__date"><?php echo self::time_ago($thread->created_at); ?></span> <span class="wbf-post__date"><?php echo self::time_ago($thread->created_at); ?></span>
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap"> <div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap">
<?php if ($current): ?>
<?php $is_subbed = WBF_DB::is_subscribed($current->id, $id); ?>
<button class="wbf-subscribe-btn wbf-btn wbf-btn--sm<?php echo $is_subbed?' wbf-btn--primary':''; ?>"
data-thread="<?php echo (int)$id; ?>"
title="<?php echo $is_subbed?'Abonnement entfernen':'Thread abonnieren'; ?>">
<i class="fas fa-bell<?php echo $is_subbed?'':'-slash'; ?>"></i>
<?php echo $is_subbed?'Abonniert':'Abonnieren'; ?>
</button>
<?php endif; ?>
<?php if ($current && WBF_DB::can($current,'post') && $thread->status !== 'closed'): ?> <?php if ($current && WBF_DB::can($current,'post') && $thread->status !== 'closed'): ?>
<button class="wbf-quote-btn" <button class="wbf-quote-btn"
data-source="wbf-thread-content-<?php echo (int)$id; ?>" data-source="wbf-thread-content-<?php echo (int)$id; ?>"
@@ -575,9 +775,11 @@ class WBF_Shortcodes {
<div class="wbf-notice wbf-notice--warning"><i class="fas fa-lock"></i> Dieser Thread ist geschlossen.</div> <div class="wbf-notice wbf-notice--warning"><i class="fas fa-lock"></i> Dieser Thread ist geschlossen.</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php self::render_forum_footer(); ?>
<?php self::render_auth_modal(); ?> <?php self::render_auth_modal(); ?>
<?php self::render_report_modal(); ?> <?php self::render_report_modal(); ?>
<?php if (WBF_DB::can($current,'manage_cats')): self::render_move_modal(WBF_DB::get_categories_flat(), $id); endif; ?> <?php if (WBF_DB::can($current,'manage_cats')): self::render_move_modal(WBF_DB::get_categories_flat(), $id); endif; ?>
<?php if ($can_add_poll): self::render_poll_modal($id); endif; ?>
</div> </div>
<?php return ob_get_clean(); <?php return ob_get_clean();
} }
@@ -656,6 +858,18 @@ class WBF_Shortcodes {
$profile = $profile_id ? WBF_DB::get_user($profile_id) : $current; $profile = $profile_id ? WBF_DB::get_user($profile_id) : $current;
if (!$profile) return '<p class="wbf-notice">Profil nicht gefunden.</p>'; if (!$profile) return '<p class="wbf-notice">Profil nicht gefunden.</p>';
$is_own = $current && $current->id == $profile->id; $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(); ?>
<div class="wbf-wrap"><?php self::render_topbar($current); ?>
<div class="wbf-container wbf-mt">
<div class="wbf-notice wbf-notice--warning">
<i class="fas fa-user-lock"></i> Dieses Profil ist nicht öffentlich.
</div>
</div></div>
<?php return ob_get_clean();
}
$user_posts = WBF_DB::get_user_posts( $profile->id, 50 ); $user_posts = WBF_DB::get_user_posts( $profile->id, 50 );
ob_start(); ?> ob_start(); ?>
@@ -727,6 +941,33 @@ class WBF_Shortcodes {
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Benutzerdefinierte Profilfelder (öffentliche) -->
<?php
$cf_defs_pub = WBF_DB::get_profile_field_defs();
$cf_vals_pub = WBF_DB::get_user_meta( $profile->id );
foreach ( $cf_defs_pub as $def ):
if ( ! $is_own && empty($def['public']) ) continue;
$val = trim( $cf_vals_pub[ $def['key'] ] ?? '' );
if ( $val === '' ) continue;
?>
<div class="wbf-profile-sidebar__section">
<span class="wbf-profile-sidebar__section-label">
<i class="fas fa-<?php echo $def['type']==='url'?'link':($def['type']==='number'?'hashtag':'tag'); ?>"></i>
<?php echo esc_html($def['label']); ?>
</span>
<?php if ( $def['type'] === 'url' ): ?>
<a href="<?php echo esc_url($val); ?>" target="_blank" rel="noopener noreferrer"
style="color:var(--c-primary);font-size:.85rem;word-break:break-all">
<?php echo esc_html( mb_strtolower( preg_replace('#^https?://#i','',$val) ) ); ?>
</a>
<?php elseif ( $def['type'] === 'textarea' ): ?>
<p style="font-size:.85rem"><?php echo nl2br(esc_html($val)); ?></p>
<?php else: ?>
<p style="font-size:.85rem"><?php echo esc_html($val); ?></p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</aside> </aside>
<!-- ── MAIN ────────────────────────────────────────────── --> <!-- ── MAIN ────────────────────────────────────────────── -->
@@ -754,6 +995,16 @@ class WBF_Shortcodes {
</div> </div>
<div class="wbf-form-row"> <div class="wbf-form-row">
<label>Signatur <small>(max. 300 Zeichen)</small></label> <label>Signatur <small>(max. 300 Zeichen)</small></label>
<div class="wbf-form-row" style="display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem">
<label style="font-size:.82rem;color:var(--c-muted)">Profil öffentlich sichtbar</label>
<?php $pub = (int)($profile->profile_public ?? 1); ?>
<button type="button" id="wbfToggleProfileVis"
class="wbf-btn wbf-btn--sm<?php echo $pub?' wbf-btn--primary':''; ?>"
data-state="<?php echo $pub; ?>">
<i class="fas fa-<?php echo $pub?'eye':'eye-slash'; ?>"></i>
<?php echo $pub?'Öffentlich':'Privat'; ?>
</button>
</div>
<textarea id="wbfEditSignature" rows="2" maxlength="300" placeholder="Deine Signatur…"><?php echo esc_textarea($profile->signature ?? ''); ?></textarea> <textarea id="wbfEditSignature" rows="2" maxlength="300" placeholder="Deine Signatur…"><?php echo esc_textarea($profile->signature ?? ''); ?></textarea>
<div class="wbf-sig-counter"><span id="wbfSigCount"><?php echo mb_strlen($profile->signature??''); ?></span>/300</div> <div class="wbf-sig-counter"><span id="wbfSigCount"><?php echo mb_strlen($profile->signature??''); ?></span>/300</div>
</div> </div>
@@ -765,7 +1016,103 @@ class WBF_Shortcodes {
</div> </div>
</div> </div>
</div> </div>
<!-- ── Benutzerdefinierte Profilfelder ──────────────── -->
<?php
$cf_defs = WBF_DB::get_profile_field_defs();
$cf_vals = WBF_DB::get_user_meta( $profile->id );
if ( ! empty( $cf_defs ) ):
?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-sliders"></i> Weitere Profilangaben
</div>
<div class="wbf-profile-card__body">
<div class="wbf-profile-edit-grid">
<?php foreach ( $cf_defs as $def ):
$k = esc_attr( $def['key'] );
$lbl = esc_html( $def['label'] );
$ph = esc_attr( $def['placeholder'] ?? '' );
$val = esc_attr( $cf_vals[ $def['key'] ] ?? '' );
$req = ! empty($def['required']) ? 'required' : '';
?>
<div class="wbf-form-row">
<label><?php echo $lbl; ?><?php if($req): ?> <span style="color:var(--c-danger)">*</span><?php endif; ?></label>
<?php if ( $def['type'] === 'textarea' ): ?>
<textarea class="wbf-cf-input" data-field="cf_<?php echo $k; ?>"
rows="2" placeholder="<?php echo $ph; ?>"
<?php echo $req; ?>><?php echo esc_textarea( $cf_vals[$def['key']] ?? '' ); ?></textarea>
<?php elseif ( $def['type'] === 'select' ):
$opts = array_filter( array_map('trim', explode("\n", $def['options'] ?? '')) );
?>
<select class="wbf-cf-input" data-field="cf_<?php echo $k; ?>">
<option value="">— Bitte wählen —</option>
<?php foreach ( $opts as $opt ): ?>
<option value="<?php echo esc_attr($opt); ?>"
<?php selected( $cf_vals[$def['key']] ?? '', $opt ); ?>><?php echo esc_html($opt); ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<input type="<?php echo $def['type'] === 'url' ? 'url' : ($def['type'] === 'number' ? 'number' : 'text'); ?>"
class="wbf-cf-input"
data-field="cf_<?php echo $k; ?>"
value="<?php echo $val; ?>"
placeholder="<?php echo $ph; ?>"
<?php echo $req; ?>>
<?php endif; ?> <?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfileCf">
<i class="fas fa-save"></i> Speichern
</button>
<span class="wbf-msg" id="wbfProfileCfMsg"></span>
</div>
</div>
</div>
<?php endif; ?>
<!-- ── DSGVO: Konto löschen ──────────────────────────── -->
<div class="wbf-profile-card" style="border-color:rgba(240,82,82,.25)">
<div class="wbf-profile-card__header" style="color:var(--c-danger);background:rgba(240,82,82,.06);border-bottom-color:rgba(240,82,82,.15)">
<i class="fas fa-shield-halved"></i> Datenschutz & Konto löschen
</div>
<div class="wbf-profile-card__body">
<p style="font-size:.85rem;color:var(--c-text-dim);margin-bottom:1rem;line-height:1.6">
Gemäß <strong>DSGVO Art. 17</strong> (Recht auf Vergessenwerden) kannst du die vollständige Löschung deines Kontos und aller personenbezogenen Daten beantragen.<br>
<span style="color:var(--c-muted);font-size:.8rem">Deine Beiträge bleiben anonymisiert sichtbar. Direktnachrichten, Likes, Profilinformationen und alle persönlichen Daten werden dauerhaft gelöscht.</span>
</p>
<div id="wbfGdprBox" style="background:rgba(240,82,82,.06);border:1px solid rgba(240,82,82,.2);border-radius:var(--radius-sm);padding:1.1rem;display:none">
<p style="font-size:.82rem;font-weight:700;color:var(--c-danger);margin-bottom:.9rem"><i class="fas fa-triangle-exclamation"></i> Diese Aktion ist unwiderruflich.</p>
<div class="wbf-form-row">
<label style="font-size:.72rem">Passwort zur Bestätigung</label>
<input type="password" id="wbfGdprPassword" placeholder="Dein aktuelles Passwort" autocomplete="current-password">
</div>
<label style="display:flex;align-items:center;gap:.6rem;font-size:.82rem;color:var(--c-text-dim);cursor:pointer;margin-bottom:1rem">
<input type="checkbox" id="wbfGdprConfirm" style="width:15px;height:15px;accent-color:var(--c-danger);cursor:pointer">
Ich verstehe, dass mein Konto und alle persönlichen Daten unwiderruflich gelöscht werden.
</label>
<div style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap">
<button class="wbf-btn wbf-btn--sm" id="wbfGdprCancel" onclick="document.getElementById('wbfGdprBox').style.display='none';document.getElementById('wbfGdprToggle').style.display=''">
<i class="fas fa-xmark"></i> Abbrechen
</button>
<button class="wbf-btn wbf-btn--sm" id="wbfGdprSubmit"
style="background:rgba(240,82,82,.15);color:var(--c-danger);border-color:rgba(240,82,82,.4)">
<i class="fas fa-trash-can"></i> Konto endgültig löschen
</button>
<span class="wbf-msg" id="wbfGdprMsg"></span>
</div>
</div>
<button class="wbf-btn wbf-btn--sm" id="wbfGdprToggle"
style="background:rgba(240,82,82,.08);color:var(--c-danger);border-color:rgba(240,82,82,.3)"
onclick="document.getElementById('wbfGdprBox').style.display='';document.getElementById('wbfGdprToggle').style.display='none'">
<i class="fas fa-trash-can"></i> Konto löschen (DSGVO Art. 17)
</button>
</div>
</div>
<?php endif; /* end $is_own */ ?>
<!-- Beiträge --> <!-- Beiträge -->
<div class="wbf-profile-card"> <div class="wbf-profile-card">
@@ -812,6 +1159,33 @@ class WBF_Shortcodes {
</div> </div>
</div> </div>
<!-- Lesezeichen (nur eigenes Profil) -->
<?php if ($is_own):
$bookmarks = WBF_DB::get_user_bookmarks($current->id, 50); ?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-bookmark"></i> Lesezeichen
<span class="wbf-profile-card__count"><?php echo count($bookmarks); ?></span>
</div>
<div class="wbf-profile-card__body wbf-profile-card__body--posts">
<?php if (empty($bookmarks)): ?>
<p class="wbf-profile-empty">Noch keine Lesezeichen.</p>
<?php else: foreach ($bookmarks as $bm): ?>
<div class="wbf-profile-post-item">
<div class="wbf-profile-post-item__top">
<?php echo self::render_prefix($bm); ?>
<a href="?forum_thread=<?php echo (int)$bm->id; ?>" class="wbf-profile-post-item__title">
<?php echo esc_html(mb_substr($bm->title,0,60)); ?>
</a>
<span class="wbf-profile-post-item__cat"><i class="fas fa-folder"></i> <?php echo esc_html($bm->cat_name); ?></span>
<span class="wbf-profile-post-item__time"><i class="fas fa-bookmark" style="font-size:.65rem"></i> <?php echo self::time_ago($bm->bookmarked_at); ?></span>
</div>
</div>
<?php endforeach; endif; ?>
</div>
</div>
<?php endif; ?>
</div><!-- /.wbf-profile-main --> </div><!-- /.wbf-profile-main -->
</div><!-- /.wbf-profile-layout --> </div><!-- /.wbf-profile-layout -->
</div> </div>
@@ -901,6 +1275,7 @@ class WBF_Shortcodes {
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php self::render_forum_footer(); ?>
<?php self::render_auth_modal(); ?> <?php self::render_auth_modal(); ?>
</div> </div>
<?php return ob_get_clean(); <?php return ob_get_clean();
@@ -912,8 +1287,20 @@ class WBF_Shortcodes {
$current = WBF_Auth::get_current_user(); $current = WBF_Auth::get_current_user();
if (!$current) { if (!$current) {
ob_start(); ?> ob_start(); ?>
<div class="wbf-wrap"><?php self::render_topbar(null); ?> <div class="wbf-wrap">
<div class="wbf-container wbf-mt"><div class="wbf-notice"><i class="fas fa-lock"></i> Bitte <a href="#" class="wbf-login-link">einloggen</a> um Nachrichten zu lesen.</div></div></div> <?php self::render_topbar(null); ?>
<div class="wbf-container wbf-mt" style="display:flex;justify-content:center;padding:3rem 1rem">
<div style="width:100%;max-width:460px;background:var(--c-surface);border:1px solid rgba(0,180,216,.25);border-radius:var(--radius);padding:2rem;position:relative;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,.6)">
<div style="position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,#00b4d8,transparent)"></div>
<div style="text-align:center;margin-bottom:1.5rem">
<div style="font-size:2.2rem;margin-bottom:.6rem">🔒</div>
<h2 style="font-size:1.1rem;font-weight:700;color:var(--c-text);margin-bottom:.3rem">Bitte einloggen</h2>
<p style="font-size:.85rem;color:var(--c-muted)">Um Nachrichten lesen zu können, musst du eingeloggt sein.</p>
</div>
<?php self::render_auth_forms(); ?>
</div>
</div>
</div>
<?php return ob_get_clean(); <?php return ob_get_clean();
} }
$partner_id = (int)($_GET['with'] ?? 0); $partner_id = (int)($_GET['with'] ?? 0);
@@ -969,6 +1356,7 @@ class WBF_Shortcodes {
</div> </div>
</div> </div>
</div> </div>
<?php self::render_forum_footer(); ?>
<?php self::render_auth_modal(); ?> <?php self::render_auth_modal(); ?>
<?php self::render_dm_compose_modal(); ?> <?php self::render_dm_compose_modal(); ?>
</div> </div>
@@ -1006,6 +1394,9 @@ class WBF_Shortcodes {
// ── SEARCH ──────────────────────────────────────────────────────────────── // ── SEARCH ────────────────────────────────────────────────────────────────
private static function view_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'] ?? ''); $query = sanitize_text_field($_GET['q'] ?? '');
$current = WBF_Auth::get_current_user(); $current = WBF_Auth::get_current_user();
$results = mb_strlen($query) >= 2 ? WBF_DB::search($query, 40) : []; $results = mb_strlen($query) >= 2 ? WBF_DB::search($query, 40) : [];
@@ -1053,6 +1444,7 @@ class WBF_Shortcodes {
<p style="color:var(--c-muted);font-size:.82rem;margin-top:1rem"><?php echo count($results); ?> Ergebnis(se) gefunden.</p> <p style="color:var(--c-muted);font-size:.82rem;margin-top:1rem"><?php echo count($results); ?> Ergebnis(se) gefunden.</p>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php self::render_forum_footer(); ?>
<?php self::render_auth_modal(); ?> <?php self::render_auth_modal(); ?>
</div> </div>
<?php return ob_get_clean(); <?php return ob_get_clean();
@@ -1063,7 +1455,7 @@ class WBF_Shortcodes {
?> ?>
<div class="wbf-topbar"> <div class="wbf-topbar">
<div class="wbf-topbar__inner"> <div class="wbf-topbar__inner">
<a href="<?php echo esc_url(remove_query_arg(['forum_cat','forum_thread','forum_profile','forum_search','forum_tag','fp','tp'])); ?>" class="wbf-topbar__brand"> <a href="<?php echo esc_url(wbf_get_forum_url()); ?>" class="wbf-topbar__brand">
<i class="fas fa-comments"></i> <?php echo esc_html(wbf_get_settings()['topbar_brand']); ?> <i class="fas fa-comments"></i> <?php echo esc_html(wbf_get_settings()['topbar_brand']); ?>
</a> </a>
<!-- Suchfeld --> <!-- Suchfeld -->
@@ -1120,11 +1512,18 @@ class WBF_Shortcodes {
</div> </div>
<?php } <?php }
private static function render_auth_forms() { ?> 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.';
?>
<div class="wbf-auth-box"> <div class="wbf-auth-box">
<div class="wbf-auth-tabs"> <div class="wbf-auth-tabs">
<button class="wbf-auth-tab active" data-tab="login">Login</button> <button class="wbf-auth-tab active" data-tab="login">Login</button>
<?php if ($reg_mode === 'open'): ?>
<button class="wbf-auth-tab" data-tab="register">Registrieren</button> <button class="wbf-auth-tab" data-tab="register">Registrieren</button>
<?php elseif ($reg_mode === 'invite'): ?>
<button class="wbf-auth-tab wbf-auth-tab--muted" disabled title="<?php echo esc_attr($invite_msg); ?>">Registrieren <i class="fas fa-lock" style="font-size:.7em"></i></button>
<?php endif; ?>
</div> </div>
<div class="wbf-auth-panel active" data-panel="login"> <div class="wbf-auth-panel active" data-panel="login">
<div class="wbf-form-row"><input type="text" class="wbf-field-username" placeholder="Benutzername oder E-Mail"></div> <div class="wbf-form-row"><input type="text" class="wbf-field-username" placeholder="Benutzername oder E-Mail"></div>
@@ -1137,6 +1536,20 @@ class WBF_Shortcodes {
<div style="text-align:right;margin-top:.4rem"><a href="#" class="wbf-forgot-link" style="font-size:.78rem;color:var(--c-muted)">Passwort vergessen?</a></div> <div style="text-align:right;margin-top:.4rem"><a href="#" class="wbf-forgot-link" style="font-size:.78rem;color:var(--c-muted)">Passwort vergessen?</a></div>
<span class="wbf-login-msg wbf-msg"></span> <span class="wbf-login-msg wbf-msg"></span>
</div> </div>
<!-- Registrierung gesperrt/invite -->
<?php if ($reg_mode === 'invite'): ?>
<div class="wbf-auth-panel" data-panel="register">
<div class="wbf-notice" style="margin:.5rem 0;font-size:.85rem;text-align:center">
<i class="fas fa-lock"></i> <?php echo esc_html($invite_msg); ?>
</div>
</div>
<?php elseif ($reg_mode === 'disabled'): ?>
<div class="wbf-auth-panel" data-panel="register">
<div class="wbf-notice wbf-notice--warning" style="margin:.5rem 0;font-size:.85rem;text-align:center">
<i class="fas fa-ban"></i> Registrierung ist deaktiviert.
</div>
</div>
<?php endif; ?>
<!-- Passwort vergessen --> <!-- Passwort vergessen -->
<div class="wbf-auth-panel" data-panel="forgot"> <div class="wbf-auth-panel" data-panel="forgot">
<p style="font-size:.82rem;color:var(--c-text-dim);margin-bottom:.75rem">Gib deine E-Mail ein — wir schicken dir einen Reset-Link.</p> <p style="font-size:.82rem;color:var(--c-text-dim);margin-bottom:.75rem">Gib deine E-Mail ein — wir schicken dir einen Reset-Link.</p>
@@ -1146,10 +1559,37 @@ class WBF_Shortcodes {
<span class="wbf-forgot-msg wbf-msg"></span> <span class="wbf-forgot-msg wbf-msg"></span>
</div> </div>
<div class="wbf-auth-panel" data-panel="register"> <div class="wbf-auth-panel" data-panel="register">
<?php
$inv_code_prefill = '';
if (isset($_GET['wbf_invite'])) $inv_code_prefill = strtoupper(sanitize_text_field($_GET['wbf_invite']));
$reg_mode_now = wbf_get_settings()['registration_mode'] ?? 'open';
if ($reg_mode_now === 'invite'): ?>
<div class="wbf-form-row">
<input type="text" class="wbf-field-invite-code"
placeholder="Einladungscode"
value="<?php echo esc_attr($inv_code_prefill); ?>"
style="text-transform:uppercase;letter-spacing:.1em;font-weight:700">
</div>
<?php endif; ?>
<!-- Spam: Honeypot (versteckt) + Zeitstempel -->
<input type="text" name="wbf_website" class="wbf-hp-field" tabindex="-1" autocomplete="off" style="display:none!important;visibility:hidden;position:absolute;left:-9999px">
<input type="hidden" class="wbf-field-form-time" value="<?php echo time(); ?>">
<div class="wbf-form-row"><input type="text" class="wbf-field-reg-user" placeholder="Benutzername"></div> <div class="wbf-form-row"><input type="text" class="wbf-field-reg-user" placeholder="Benutzername"></div>
<div class="wbf-form-row"><input type="text" class="wbf-field-reg-name" placeholder="Anzeigename"></div> <div class="wbf-form-row"><input type="text" class="wbf-field-reg-name" placeholder="Anzeigename"></div>
<div class="wbf-form-row"><input type="email" class="wbf-field-reg-email" placeholder="E-Mail"></div> <div class="wbf-form-row"><input type="email" class="wbf-field-reg-email" placeholder="E-Mail"></div>
<div class="wbf-form-row"><input type="password" class="wbf-field-reg-pass" placeholder="Passwort (min. 6 Zeichen)"></div> <div class="wbf-form-row"><input type="password" class="wbf-field-reg-pass" placeholder="Passwort (min. 6 Zeichen)"></div>
<?php
$rules_required = ( wbf_get_settings()['rules_accept_required'] ?? '1' ) === '1';
$rules_enabled = ( wbf_get_settings()['rules_enabled'] ?? '1' ) === '1';
if ( $rules_enabled ):
?>
<label style="display:flex;align-items:center;gap:.5rem;font-size:.82rem;color:var(--c-text-dim);cursor:pointer;margin-bottom:.75rem;line-height:1.4;flex-wrap:wrap">
<input type="checkbox" class="wbf-field-rules-accept"
style="width:15px;height:15px;accent-color:var(--c-primary);cursor:pointer;flex-shrink:0"
<?php echo $rules_required ? 'required' : ''; ?>>
<span>Ich akzeptiere die <a href="<?php echo esc_url(wbf_get_forum_url().'?forum_rules=1'); ?>" target="_blank" style="color:var(--c-primary);font-weight:600;white-space:nowrap">Forum-Regeln</a><?php echo $rules_required ? ' <span style="color:var(--c-danger)">*</span>' : ''; ?></span>
</label>
<?php endif; ?>
<button class="wbf-btn wbf-btn--primary wbf-btn--full wbf-reg-submit-btn"><i class="fas fa-user-plus"></i> Konto erstellen</button> <button class="wbf-btn wbf-btn--primary wbf-btn--full wbf-reg-submit-btn"><i class="fas fa-user-plus"></i> Konto erstellen</button>
<span class="wbf-reg-msg wbf-msg"></span> <span class="wbf-reg-msg wbf-msg"></span>
</div> </div>
@@ -1322,6 +1762,58 @@ class WBF_Shortcodes {
<?php <?php
} }
// ── UMFRAGE-MODAL ─────────────────────────────────────────────────────────
private static function render_poll_modal( $thread_id ) { ?>
<div class="wbf-modal" id="wbfPollModal">
<div class="wbf-modal__box">
<button class="wbf-modal__close" onclick="document.getElementById('wbfPollModal').classList.remove('active')">&times;</button>
<h3 style="margin-bottom:1.25rem"><i class="fas fa-chart-bar" style="color:var(--c-primary)"></i> Umfrage erstellen</h3>
<input type="hidden" id="wbfPollThreadId" value="<?php echo (int)$thread_id; ?>">
<div class="wbf-form-row">
<label>Frage <span style="color:var(--c-danger)">*</span></label>
<input type="text" id="wbfPollQuestion" placeholder="Was ist deine Meinung zu…?" maxlength="200">
</div>
<div class="wbf-form-row">
<label>Antwortmöglichkeiten <small>(min. 2, max. 10)</small></label>
<div id="wbfPollOptions">
<div class="wbf-poll-opt-row">
<input type="text" class="wbf-poll-opt" placeholder="Option 1" maxlength="100">
<button type="button" class="wbf-btn wbf-btn--sm" style="background:rgba(240,82,82,.1);color:var(--c-danger);border-color:rgba(240,82,82,.3);min-width:32px;flex-shrink:0" onclick="wbfRemovePollOpt(this)">✕</button>
</div>
<div class="wbf-poll-opt-row">
<input type="text" class="wbf-poll-opt" placeholder="Option 2" maxlength="100">
<button type="button" class="wbf-btn wbf-btn--sm" style="background:rgba(240,82,82,.1);color:var(--c-danger);border-color:rgba(240,82,82,.3);min-width:32px;flex-shrink:0" onclick="wbfRemovePollOpt(this)">✕</button>
</div>
</div>
<button type="button" id="wbfPollAddOpt" class="wbf-btn wbf-btn--sm" style="margin-top:.5rem">
<i class="fas fa-plus"></i> Option hinzufügen
</button>
</div>
<div style="display:flex;gap:1.25rem;flex-wrap:wrap;align-items:flex-end;margin-bottom:1rem">
<label style="display:flex;align-items:center;gap:.4rem;font-size:.82rem;color:var(--c-text-dim);cursor:pointer">
<input type="checkbox" id="wbfPollMulti" style="accent-color:var(--c-primary);width:15px;height:15px">
Mehrfachauswahl erlauben
</label>
<div class="wbf-form-row" style="margin:0;flex:1;min-width:180px">
<label style="font-size:.72rem">Endet am <small style="color:var(--c-muted)">(optional)</small></label>
<input type="datetime-local" id="wbfPollEndsAt" min="<?php echo date('Y-m-d\TH:i'); ?>">
</div>
</div>
<div style="display:flex;gap:.75rem;align-items:center">
<button class="wbf-btn wbf-btn--primary" id="wbfSubmitPoll">
<i class="fas fa-chart-bar"></i> Umfrage erstellen
</button>
<span class="wbf-msg" id="wbfPollMsg"></span>
</div>
</div>
</div>
<?php }
private static function render_new_thread_modal( $categories, $current, $preselect = 0 ) { private static function render_new_thread_modal( $categories, $current, $preselect = 0 ) {
if (!$current) return; if (!$current) return;
$parents = array_filter($categories, fn($c) => (int)$c->parent_id === 0); $parents = array_filter($categories, fn($c) => (int)$c->parent_id === 0);
@@ -1331,7 +1823,7 @@ class WBF_Shortcodes {
<div class="wbf-modal" id="wbfNewThreadModal"> <div class="wbf-modal" id="wbfNewThreadModal">
<div class="wbf-modal__box wbf-modal__box--lg"> <div class="wbf-modal__box wbf-modal__box--lg">
<button class="wbf-modal__close" onclick="document.getElementById('wbfNewThreadModal').classList.remove('active')">&times;</button> <button class="wbf-modal__close" onclick="document.getElementById('wbfNewThreadModal').classList.remove('active')">&times;</button>
<h3 style="margin-bottom:1.2rem"><i class="fas fa-plus-circle"></i> Neuen Thread erstellen</h3> <h3 id="wbfModalTitle" style="margin-bottom:1.2rem"><i class="fas fa-plus-circle"></i> Neuen Thread erstellen</h3>
<div class="wbf-form-row"><label>Kategorie</label> <div class="wbf-form-row"><label>Kategorie</label>
<select id="wbfThreadCat"> <select id="wbfThreadCat">
<?php foreach ($parents as $p): ?> <?php foreach ($parents as $p): ?>
@@ -1344,13 +1836,28 @@ class WBF_Shortcodes {
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div class="wbf-form-row"><label>Titel</label><input type="text" id="wbfThreadTitle" placeholder="Titel deines Threads"></div> <div class="wbf-form-row"><label id="wbfTitleLabel">Titel</label><input type="text" id="wbfThreadTitle" placeholder="Titel deines Threads"></div>
<div class="wbf-form-row"> <?php $prefixes = WBF_DB::get_prefixes(); if (!empty($prefixes)): ?>
<div class="wbf-form-row" id="wbfPrefixRow">
<label>Präfix <small>(optional)</small></label>
<select id="wbfThreadPrefix">
<option value="">— Kein Präfix —</option>
<?php foreach ($prefixes as $px): ?>
<option value="<?php echo (int)$px->id; ?>"
data-color="<?php echo esc_attr($px->color); ?>"
data-bg="<?php echo esc_attr($px->bg_color); ?>">
<?php echo esc_html($px->label); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="wbf-form-row" id="wbfContentRow">
<label>Inhalt</label> <label>Inhalt</label>
<?php self::render_editor_toolbar('wbfThreadContent'); ?> <?php self::render_editor_toolbar('wbfThreadContent'); ?>
<textarea id="wbfThreadContent" rows="7" placeholder="Was möchtest du besprechen?"></textarea> <textarea id="wbfThreadContent" rows="7" placeholder="Was möchtest du besprechen?"></textarea>
</div> </div>
<div class="wbf-form-row"> <div class="wbf-form-row" id="wbfTagsRow">
<label>Tags <small>(kommagetrennt, max. 10 — z.B. php, wordpress, tipps)</small></label> <label>Tags <small>(kommagetrennt, max. 10 — z.B. php, wordpress, tipps)</small></label>
<div class="wbf-tag-input-wrap" id="wbfTagInputWrap"> <div class="wbf-tag-input-wrap" id="wbfTagInputWrap">
<div class="wbf-tag-pills" id="wbfTagPills"></div> <div class="wbf-tag-pills" id="wbfTagPills"></div>
@@ -1359,10 +1866,55 @@ class WBF_Shortcodes {
<div class="wbf-tag-suggest" id="wbfTagSuggest" style="display:none"></div> <div class="wbf-tag-suggest" id="wbfTagSuggest" style="display:none"></div>
</div> </div>
</div> </div>
<div style="display:flex;gap:1rem;align-items:center">
<!-- Thread Submit Row -->
<div id="wbfThreadSubmitRow" style="display:flex;gap:1rem;align-items:center;margin-top:.25rem">
<button class="wbf-btn wbf-btn--primary" id="wbfSubmitThread"><i class="fas fa-paper-plane"></i> Thread erstellen</button> <button class="wbf-btn wbf-btn--primary" id="wbfSubmitThread"><i class="fas fa-paper-plane"></i> Thread erstellen</button>
<button type="button" id="wbfShowPollSection" class="wbf-btn wbf-btn--sm wbf-btn--outline-poll">
<i class="fas fa-chart-bar"></i> Umfrage hinzufügen
</button>
<span class="wbf-msg" id="wbfThreadMsg"></span> <span class="wbf-msg" id="wbfThreadMsg"></span>
</div> </div>
<!-- Poll Section -->
<div id="wbfPollSection" style="margin-top:1.25rem;border-top:1px solid var(--c-border);padding-top:1.1rem;display:none">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.9rem">
<span style="font-size:.85rem;font-weight:700;color:#fbbf24"><i class="fas fa-chart-bar"></i> Umfrage</span>
<button type="button" id="wbfRemovePollSection" class="wbf-btn wbf-btn--sm" style="background:rgba(240,82,82,.1);color:var(--c-danger);border-color:rgba(240,82,82,.3);padding:.25rem .6rem;font-size:.72rem"><i class="fas fa-xmark"></i> Entfernen</button>
</div>
<div class="wbf-form-row">
<label>Frage <span style="color:var(--c-danger)">*</span></label>
<input type="text" id="wbfNewThreadPollQuestion" placeholder="Was ist deine Meinung zu…?" maxlength="200">
</div>
<div class="wbf-form-row">
<label>Antwortmöglichkeiten <small>(min. 2, max. 10)</small></label>
<div id="wbfNewThreadPollOptions">
<div class="wbf-poll-opt-row" style="display:flex;gap:.5rem;margin-bottom:.4rem">
<input type="text" class="wbf-nt-poll-opt" placeholder="Option 1" maxlength="100">
<button type="button" class="wbf-btn wbf-btn--sm" style="background:rgba(240,82,82,.1);color:var(--c-danger);border-color:rgba(240,82,82,.3);min-width:32px;flex-shrink:0" onclick="wbfRemoveNTPollOpt(this)">✕</button>
</div>
<div class="wbf-poll-opt-row" style="display:flex;gap:.5rem;margin-bottom:.4rem">
<input type="text" class="wbf-nt-poll-opt" placeholder="Option 2" maxlength="100">
<button type="button" class="wbf-btn wbf-btn--sm" style="background:rgba(240,82,82,.1);color:var(--c-danger);border-color:rgba(240,82,82,.3);min-width:32px;flex-shrink:0" onclick="wbfRemoveNTPollOpt(this)">✕</button>
</div>
</div>
<button type="button" id="wbfNTPollAddOpt" class="wbf-btn wbf-btn--sm" style="margin-top:.5rem">
<i class="fas fa-plus"></i> Option hinzufügen
</button>
</div>
<label style="display:flex;align-items:center;gap:.4rem;font-size:.82rem;color:var(--c-text-dim);cursor:pointer;margin-bottom:.75rem">
<input type="checkbox" id="wbfNTPollMulti" style="accent-color:var(--c-primary);width:15px;height:15px">
Mehrfachauswahl erlauben
</label>
<div class="wbf-form-row" style="margin-bottom:1rem">
<label style="font-size:.78rem;font-weight:600;color:var(--c-text-dim)">Endet am <small style="font-weight:400;color:var(--c-muted)">(optional)</small></label>
<input type="datetime-local" id="wbfNTPollEndsAt" min="<?php echo date('Y-m-d\TH:i'); ?>" style="width:100%;box-sizing:border-box">
</div>
<div style="display:flex;gap:1rem;align-items:center">
<button class="wbf-btn wbf-btn--outline-poll" id="wbfSubmitPollThread"><i class="fas fa-chart-bar"></i> Umfrage erstellen</button>
<span class="wbf-msg" id="wbfPollThreadMsg"></span>
</div>
</div>
</div> </div>
</div> </div>
<?php } <?php }
@@ -1415,6 +1967,8 @@ class WBF_Shortcodes {
private static function view_members() { private static function view_members() {
$current = WBF_Auth::get_current_user(); $current = WBF_Auth::get_current_user();
$maint_m = wbf_get_settings()['maintenance_mode'] ?? '0';
if ($maint_m === '1' && (!$current || WBF_Roles::level($current->role) < 50)) return self::view_maintenance();
if ( ! $current ) { if ( ! $current ) {
ob_start(); ?> ob_start(); ?>
<div class="wbf-wrap"><?php self::render_topbar(null); ?> <div class="wbf-wrap"><?php self::render_topbar(null); ?>
@@ -1527,11 +2081,133 @@ class WBF_Shortcodes {
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php self::render_forum_footer(); ?>
<?php self::render_auth_modal(); ?> <?php self::render_auth_modal(); ?>
</div> </div>
<?php return ob_get_clean(); <?php return ob_get_clean();
} }
// ── Wartungsmodus ─────────────────────────────────────────────────────────
private static function view_maintenance() {
$s = wbf_get_settings();
$title = esc_html($s['maintenance_title'] ?? 'Wartungsarbeiten');
$msg = esc_html($s['maintenance_message'] ?? 'Das Forum wird gerade gewartet. Bitte versuche es später erneut.');
ob_start(); ?>
<div class="wbf-wrap">
<div style="min-height:60vh;display:flex;align-items:center;justify-content:center;text-align:center;padding:2rem">
<div style="max-width:480px">
<div style="font-size:4rem;margin-bottom:1.5rem">🔧</div>
<h1 style="font-size:1.6rem;font-weight:800;color:var(--c-text);margin-bottom:1rem"><?php echo $title; ?></h1>
<p style="color:var(--c-text-dim);font-size:1rem;line-height:1.7"><?php echo $msg; ?></p>
<p style="margin-top:2rem">
<a href="#" class="wbf-login-link wbf-btn wbf-btn--sm wbf-btn--outline" style="font-size:.85rem">
<i class="fas fa-lock"></i> Als Admin einloggen
</a>
</p>
</div>
</div>
<?php self::render_auth_modal(); ?>
</div>
<?php return ob_get_clean();
}
// ── FORUM-FOOTER ──────────────────────────────────────────────────────────
private static function render_forum_footer() {
if ( ( wbf_get_settings()['rules_enabled'] ?? '1' ) !== '1' ) return;
$rules_url = esc_url( wbf_get_forum_url() . '?forum_rules=1' );
?>
<div style="border-top:1px solid var(--c-border);margin-top:3rem;padding:1.25rem 1.5rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem;background:var(--c-bg2)">
<span style="font-size:.78rem;color:var(--c-muted)">
<i class="fas fa-shield-halved" style="color:var(--c-primary);margin-right:.35rem"></i>
Durch die Nutzung des Forums stimmst du unseren Regeln zu.
</span>
<a href="<?php echo $rules_url; ?>"
style="display:inline-flex;align-items:center;gap:.4rem;font-size:.78rem;font-weight:700;color:#0d1117;background:var(--c-primary);padding:.35rem .9rem;border-radius:6px;text-decoration:none;transition:opacity .15s"
onmouseover="this.style.opacity='.85'" onmouseout="this.style.opacity='1'">
<i class="fas fa-scroll"></i> Forum-Regeln lesen
</a>
</div>
<?php
}
// ── FORUM-REGELN ──────────────────────────────────────────────────────────
private static function view_rules() {
$current = WBF_Auth::get_current_user();
$s = wbf_get_settings();
if ( ( $s['rules_enabled'] ?? '1' ) !== '1' ) {
return '<p class="wbf-notice">Diese Seite ist nicht verfügbar.</p>';
}
$title = esc_html( $s['rules_title'] ?? 'Forum-Regeln & Nutzungsbedingungen' );
$raw = $s['rules_content'] ?? '';
ob_start(); ?>
<div class="wbf-wrap">
<?php self::render_topbar($current); ?>
<div class="wbf-container wbf-mt" style="max-width:820px">
<nav class="wbf-breadcrumb">
<a href="<?php echo esc_url(wbf_get_forum_url()); ?>"><i class="fas fa-home"></i> Forum</a>
<span>/</span><span><i class="fas fa-shield-halved"></i> Regeln</span>
</nav>
<div style="background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--radius);overflow:hidden;margin-bottom:2rem">
<div style="background:linear-gradient(135deg,rgba(0,180,216,.12),rgba(99,102,241,.08));border-bottom:1px solid var(--c-border);padding:1.75rem 2rem;display:flex;align-items:center;gap:1rem">
<div style="width:48px;height:48px;border-radius:12px;background:rgba(0,180,216,.15);border:1px solid rgba(0,180,216,.25);display:flex;align-items:center;justify-content:center;font-size:1.3rem;flex-shrink:0">📜</div>
<div>
<h1 style="font-size:1.3rem;font-weight:800;color:var(--c-text);margin:0 0 .2rem"><?php echo $title; ?></h1>
<p style="font-size:.8rem;color:var(--c-muted);margin:0">
<i class="fas fa-clock"></i> Zuletzt aktualisiert: <?php echo date_i18n('d. F Y'); ?>
&nbsp;·&nbsp;<i class="fas fa-eye"></i> Bitte sorgfältig lesen
</p>
</div>
</div>
<div class="wbf-rules-body" style="padding:2rem">
<?php echo self::render_rules_content( $raw ); ?>
</div>
<div style="border-top:1px solid var(--c-border);padding:1.25rem 2rem;background:rgba(0,0,0,.12);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem">
<span style="font-size:.82rem;color:var(--c-muted)">
<i class="fas fa-info-circle"></i> Mit der Nutzung des Forums stimmst du diesen Regeln zu.
</span>
<a href="<?php echo esc_url(wbf_get_forum_url()); ?>" class="wbf-btn wbf-btn--sm wbf-btn--primary" style="color:#0d1117;font-weight:700">
<i class="fas fa-arrow-left"></i> Zurück zum Forum
</a>
</div>
</div>
</div>
</div>
<?php return ob_get_clean();
}
private static function render_rules_content( $raw ) {
if ( empty( $raw ) ) {
return '<p style="color:var(--c-muted)">Noch keine Regeln hinterlegt.</p>';
}
$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 .= '<div class="wbf-rules-section">'
. '<span class="wbf-rules-num">' . $num . '</span>'
. '<h2 class="wbf-rules-heading">' . $text . '</h2>'
. '</div>';
} else {
$para = htmlspecialchars( $para, ENT_QUOTES, 'UTF-8' );
$para = preg_replace( '/\*\*(.+?)\*\*/s', '<strong>$1</strong>', $para );
$para = nl2br( $para );
$out .= '<p class="wbf-rules-para">' . $para . '</p>';
}
}
return $out;
}
} }
WBF_Shortcodes::init(); WBF_Shortcodes::init();