diff --git a/includes/class-forum-ajax.php b/includes/class-forum-ajax.php new file mode 100644 index 0000000..4126b8e --- /dev/null +++ b/includes/class-forum-ajax.php @@ -0,0 +1,833 @@ +'Sicherheitsfehler.']); + exit; + } + // Update last_active for online status tracking + if ( WBF_Auth::is_forum_logged_in() ) { + $u = WBF_Auth::get_current_user(); + if ($u) WBF_DB::touch_last_active($u->id); + } + } + + // ── Auth ────────────────────────────────────────────────────────────────── + + public static function handle_login() { + // Login braucht keinen Nonce — Credentials sind die Authentifizierung + $result = WBF_Auth::login( + sanitize_text_field($_POST['username'] ?? ''), + $_POST['password'] ?? '' + ); + if ($result['success']) { + $u = $result['user']; + if ( ! empty($_POST['remember_me']) ) { + WBF_Auth::set_remember_cookie($u->id); + } + wp_send_json_success(['display_name'=>$u->display_name,'avatar_url'=>$u->avatar_url,'user_id'=>$u->id]); + } else { + wp_send_json_error($result); + } + } + + public static function handle_register() { + // Register braucht keinen Nonce + $result = WBF_Auth::register( + sanitize_text_field($_POST['username'] ?? ''), + sanitize_email( $_POST['email'] ?? ''), + $_POST['password'] ?? '', + sanitize_text_field($_POST['display_name'] ?? '') + ); + if ($result['success']) { + $u = $result['user']; + wp_send_json_success(['display_name'=>$u->display_name,'avatar_url'=>$u->avatar_url,'user_id'=>$u->id]); + } else { + wp_send_json_error($result); + } + } + + public static function handle_logout() { + // Kein Nonce-Check für Logout nötig — Session-Clearing ist sicher + WBF_Auth::logout(); + wp_send_json_success(['message' => 'logged_out']); + } + + // ── Threads ─────────────────────────────────────────────────────────────── + + public static function handle_new_thread() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + if (!WBF_DB::can($user, 'create_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + + $title = sanitize_text_field($_POST['title'] ?? ''); + $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' ); + $category_id = (int)($_POST['category_id'] ?? 0); + + if (strlen($title) < 5) wp_send_json_error(['message'=>'Titel zu kurz (min. 5 Zeichen).']); + if (strlen($content) < 10) wp_send_json_error(['message'=>'Inhalt zu kurz (min. 10 Zeichen).']); + if (!$category_id) wp_send_json_error(['message'=>'Keine Kategorie gewählt.']); + + $cat = WBF_DB::get_category($category_id); + if (!$cat || !WBF_DB::can_post_in($user, $cat)) wp_send_json_error(['message'=>'Keine Berechtigung für diese Kategorie.']); + + $id = WBF_DB::create_thread([ + 'category_id' => $category_id, + 'user_id' => $user->id, + 'title' => $title, + 'slug' => sanitize_title($title) . '-' . time(), + 'content' => $content, + ]); + + // Tags speichern + $raw_tags = sanitize_text_field( $_POST['tags'] ?? '' ); + if ( $raw_tags ) { + WBF_DB::sync_thread_tags( $id, $raw_tags ); + } + + wp_send_json_success(['thread_id'=>$id,'message'=>'Thread erstellt!']); + } + + // ── Posts ───────────────────────────────────────────────────────────────── + + public static function handle_new_post() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + if (!WBF_DB::can($user, 'post')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + + $thread_id = (int)($_POST['thread_id'] ?? 0); + $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' ); + + if (strlen($content) < 3) wp_send_json_error(['message'=>'Antwort zu kurz.']); + if (!$thread_id) wp_send_json_error(['message'=>'Ungültiger Thread.']); + + $thread = WBF_DB::get_thread($thread_id); + if (!$thread || $thread->status === 'closed') wp_send_json_error(['message'=>'Thread ist geschlossen.']); + if ($thread->status === 'archived') wp_send_json_error(['message'=>'Thread ist archiviert.']); + + $id = WBF_DB::create_post(['thread_id'=>$thread_id,'user_id'=>$user->id,'content'=>$content]); + + // Benachrichtigungen: Thread-Ersteller + alle bisherigen Antwortenden + $notif_users = WBF_DB::get_thread_participants($thread_id); + foreach ($notif_users as $participant_id) { + WBF_DB::create_notification($participant_id, 'reply', $thread_id, $user->id); + // E-Mail + $notif_user = WBF_DB::get_user($participant_id); + self::send_notification_email($notif_user, 'reply', $user->display_name, [ + 'thread_id' => $thread_id, + 'thread_title' => $thread->title, + ]); + } + // @Erwähnungen + $mentioned = WBF_DB::extract_mentions($content); + foreach ($mentioned as $m_user) { + if ((int)$m_user->id !== (int)$user->id) { + WBF_DB::create_notification($m_user->id, 'mention', $thread_id, $user->id); + // E-Mail + self::send_notification_email($m_user, 'mention', $user->display_name, [ + 'thread_id' => $thread_id, + 'thread_title' => $thread->title, + ]); + } + } + + // Direkt den neuen Post laden — nicht alle Posts fetchen + global $wpdb; + $new_post = $wpdb->get_row( $wpdb->prepare( + "SELECT p.*, u.display_name, u.avatar_url, u.username, u.signature, + u.post_count as author_posts, u.role as author_role, u.registered as author_registered + FROM {$wpdb->prefix}forum_posts p + JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id + WHERE p.id = %d", $id + ) ); + + ob_start(); + WBF_Shortcodes::render_single_post($new_post, $user); + $html = ob_get_clean(); + + wp_send_json_success(['html'=>$html,'post_id'=>$id]); + } + + // ── Mod Actions ─────────────────────────────────────────────────────────── + + public static function handle_mod_action() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + + $action = sanitize_key($_POST['mod_action'] ?? ''); + $object_id = (int)($_POST['object_id'] ?? 0); + + switch ($action) { + + case 'pin_thread': + case 'unpin_thread': + if (!WBF_DB::can($user,'pin_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + WBF_DB::update_thread($object_id, ['pinned' => $action === 'pin_thread' ? 1 : 0]); + wp_send_json_success(['action'=>$action,'message'=>$action==='pin_thread'?'Gepinnt!':'Entpinnt!']); + break; + + case 'close_thread': + case 'open_thread': + if (!WBF_DB::can($user,'close_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + WBF_DB::update_thread($object_id, ['status' => $action === 'close_thread' ? 'closed' : 'open']); + wp_send_json_success(['action'=>$action,'message'=>$action==='close_thread'?'Thread geschlossen.':'Thread geöffnet.']); + break; + + case 'delete_thread': + if (!WBF_DB::can($user,'delete_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + $thread = WBF_DB::get_thread($object_id); + if (!$thread) wp_send_json_error(['message'=>'Thread nicht gefunden.']); + WBF_DB::delete_thread($object_id); + wp_send_json_success(['action'=>'deleted','redirect'=>'?forum_cat='.urlencode('')]); + break; + + case 'delete_post': + if (!WBF_DB::can($user,'delete_post')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + WBF_DB::delete_post($object_id); + wp_send_json_success(['action'=>'post_deleted']); + break; + + case 'archive_thread': + case 'unarchive_thread': + if (!WBF_DB::can($user,'close_thread')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + $thread = WBF_DB::get_thread($object_id); + if (!$thread) wp_send_json_error(['message'=>'Thread nicht gefunden.']); + if ($action === 'archive_thread') { + if ($thread->status !== 'archived') { + global $wpdb; + $wpdb->query($wpdb->prepare( + "UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d", + $thread->category_id + )); + } + WBF_DB::update_thread($object_id, ['status' => 'archived']); + wp_send_json_success(['action'=>'archived','message'=>'Thread archiviert.']); + } else { + WBF_DB::update_thread($object_id, ['status' => 'open']); + global $wpdb; + $wpdb->query($wpdb->prepare( + "UPDATE {$wpdb->prefix}forum_categories SET thread_count=thread_count+1 WHERE id=%d", + $thread->category_id + )); + wp_send_json_success(['action'=>'unarchived','message'=>'Thread wiederhergestellt.']); + } + break; + + default: + wp_send_json_error(['message'=>'Unbekannte Aktion.']); + } + } + + // ── Likes ───────────────────────────────────────────────────────────────── + + public static function handle_toggle_like() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Bitte einloggen um zu liken.']); + if (!WBF_DB::can($user,'like')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + + $object_id = (int)($_POST['object_id'] ?? 0); + $type = sanitize_key($_POST['object_type'] ?? 'post'); + if (!in_array($type, ['thread','post'])) wp_send_json_error(['message'=>'Ungültiger Typ.']); + + $action = WBF_DB::toggle_like($user->id, $object_id, $type); + $count = WBF_DB::get_like_count($object_id, $type); + wp_send_json_success(['action'=>$action,'count'=>$count]); + } + + // ── Profile ─────────────────────────────────────────────────────────────── + + public static function handle_update_profile() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + + $update = []; + $display_name = sanitize_text_field($_POST['display_name'] ?? ''); + $bio = sanitize_textarea_field($_POST['bio'] ?? ''); + $signature = sanitize_textarea_field($_POST['signature'] ?? ''); + + if (!empty($display_name)) $update['display_name'] = $display_name; + $update['bio'] = $bio; + $update['signature'] = mb_substr($signature, 0, 300); // max 300 Zeichen + + if (!empty($_POST['new_password'])) { + if (strlen($_POST['new_password']) < 6) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']); + $update['password'] = password_hash($_POST['new_password'], PASSWORD_DEFAULT); + } + + WBF_DB::update_user($user->id, $update); + wp_send_json_success(['message'=>'Profil gespeichert!']); + } + + public static function handle_upload_avatar() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + if (empty($_FILES['avatar'])) wp_send_json_error(['message'=>'Keine Datei.']); + + $allowed_types = ['image/jpeg','image/png','image/gif','image/webp']; + $mime = $_FILES['avatar']['type'] ?? ''; + if (!in_array($mime, $allowed_types)) wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']); + if ($_FILES['avatar']['size'] > 2 * 1024 * 1024) wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']); + + require_once ABSPATH . 'wp-admin/includes/image.php'; + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/media.php'; + + $id = media_handle_upload('avatar', 0); + if (is_wp_error($id)) wp_send_json_error(['message'=>$id->get_error_message()]); + + $url = wp_get_attachment_url($id); + WBF_DB::update_user($user->id, ['avatar_url'=>$url]); + wp_send_json_success(['avatar_url'=>$url]); + } + + // ── Report ──────────────────────────────────────────────────────────────── + + public static function handle_report_post() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Bitte einloggen um zu melden.']); + if (!WBF_Roles::can($user, 'post')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + + $object_id = (int)($_POST['object_id'] ?? 0); + $type = sanitize_key($_POST['object_type'] ?? 'post'); + $reason = sanitize_text_field($_POST['reason'] ?? ''); + $note = sanitize_textarea_field($_POST['note'] ?? ''); + + if (!$object_id) wp_send_json_error(['message'=>'Ungültiges Objekt.']); + if (!in_array($type, ['post','thread'])) wp_send_json_error(['message'=>'Ungültiger Typ.']); + if (empty($reason)) wp_send_json_error(['message'=>'Bitte einen Grund angeben.']); + if (WBF_DB::has_reported($user->id, $object_id, $type)) + wp_send_json_error(['message'=>'Du hast diesen Beitrag bereits gemeldet.']); + + WBF_DB::create_report([ + 'object_id' => $object_id, + 'object_type' => $type, + 'reporter_id' => $user->id, + 'reason' => $reason, + 'note' => mb_substr($note, 0, 500), + 'status' => 'open', + ]); + + wp_send_json_success(['message'=>'Beitrag wurde gemeldet. Danke!']); + } + // ── Post-Bild Upload ────────────────────────────────────────────────────── + + public static function handle_upload_post_image() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + if ( ! WBF_Roles::can($user, 'post') ) wp_send_json_error(['message' => 'Keine Berechtigung.']); + if ( empty($_FILES['image']) ) wp_send_json_error(['message' => 'Keine Datei empfangen.']); + + // Nur Bilder erlauben + $allowed_types = ['image/jpeg','image/png','image/gif','image/webp']; + $mime = $_FILES['image']['type'] ?? ''; + if ( ! in_array($mime, $allowed_types) ) { + wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']); + } + + // Max 5 MB + if ( $_FILES['image']['size'] > 5 * 1024 * 1024 ) { + wp_send_json_error(['message' => 'Maximale Dateigröße: 5 MB.']); + } + + require_once ABSPATH . 'wp-admin/includes/image.php'; + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/media.php'; + + // $_FILES-Schlüssel auf 'image' umbenennen → media_handle_upload erwartet den Feldnamen + $attachment_id = media_handle_upload('image', 0); + if ( is_wp_error($attachment_id) ) { + wp_send_json_error(['message' => $attachment_id->get_error_message()]); + } + + $url = wp_get_attachment_url($attachment_id); + wp_send_json_success(['url' => $url]); + } + // ── Post bearbeiten ─────────────────────────────────────────────────────── + + public static function handle_edit_post() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + + $post_id = (int)( $_POST['post_id'] ?? 0 ); + $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' ); + + if ( ! $post_id ) wp_send_json_error(['message' => 'Ungültiger Beitrag.']); + if ( strlen($content) < 3 ) wp_send_json_error(['message' => 'Inhalt zu kurz.']); + + global $wpdb; + $db_post = $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}forum_posts WHERE id=%d", $post_id + ) ); + if ( ! $db_post ) wp_send_json_error(['message' => 'Beitrag nicht gefunden.']); + + $is_own = (int)$db_post->user_id === (int)$user->id; + $is_mod = WBF_DB::can( $user, 'delete_post' ); + + if ( ! $is_own && ! $is_mod ) { + wp_send_json_error(['message' => 'Keine Berechtigung.']); + } + + $wpdb->update( + "{$wpdb->prefix}forum_posts", + ['content' => $content, 'updated_at' => current_time('mysql')], + ['id' => $post_id] + ); + + wp_send_json_success(['message' => 'Beitrag aktualisiert!', 'content' => WBF_BBCode::render($content)]); + } + + // ── Suche ───────────────────────────────────────────────────────────────── + + public static function handle_search() { + self::verify(); + $query = sanitize_text_field( $_POST['query'] ?? '' ); + if ( mb_strlen( $query ) < 2 ) wp_send_json_error(['message' => 'Suchbegriff zu kurz.']); + $results = WBF_DB::search( $query, 40 ); + wp_send_json_success(['results' => $results, 'query' => $query]); + } + + // ── Benachrichtigungen ──────────────────────────────────────────────────── + + public static function handle_get_notifications() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + $notifs = WBF_DB::get_notifications( $user->id, 20 ); + $unread = WBF_DB::count_unread_notifications( $user->id ); + wp_send_json_success(['notifications' => $notifs, 'unread' => $unread]); + } + + public static function handle_mark_notifications_read() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + WBF_DB::mark_notifications_read( $user->id ); + wp_send_json_success(['message' => 'Gelesen.']); + } + + // ── Thread-Inhalt bearbeiten (OP) ──────────────────────────────────────── + + public static function handle_edit_thread() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + + $thread_id = (int)( $_POST['thread_id'] ?? 0 ); + $content = WBF_BBCode::sanitize( $_POST['content'] ?? '' ); + $title = sanitize_text_field( $_POST['title'] ?? '' ); + + if ( ! $thread_id ) wp_send_json_error(['message' => 'Ungültiger Thread.']); + if ( strlen($content) < 5 ) wp_send_json_error(['message' => 'Inhalt zu kurz.']); + if ( strlen($title) < 3 ) wp_send_json_error(['message' => 'Titel zu kurz (min. 3 Zeichen).']); + + $thread = WBF_DB::get_thread($thread_id); + if ( ! $thread ) wp_send_json_error(['message' => 'Thread nicht gefunden.']); + + $is_own = (int)$thread->user_id === (int)$user->id; + $is_mod = WBF_DB::can($user, 'delete_post'); + if ( ! $is_own && ! $is_mod ) wp_send_json_error(['message' => 'Keine Berechtigung.']); + + global $wpdb; + $wpdb->update( + "{$wpdb->prefix}forum_threads", + [ + 'title' => $title, + 'content' => $content, + 'updated_at' => current_time('mysql'), + ], + ['id' => $thread_id] + ); + + wp_send_json_success([ + 'message' => 'Thread aktualisiert!', + 'title' => esc_html($title), + 'content' => WBF_BBCode::render($content), + ]); + } + + // ── Thread verschieben ──────────────────────────────────────────────────── + + public static function handle_move_thread() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + if ( ! WBF_DB::can($user, 'manage_cats') ) wp_send_json_error(['message' => 'Keine Berechtigung.']); + + $thread_id = (int)( $_POST['thread_id'] ?? 0 ); + $category_id = (int)( $_POST['category_id'] ?? 0 ); + + if ( ! $thread_id || ! $category_id ) wp_send_json_error(['message' => 'Ungültige Daten.']); + + $thread = WBF_DB::get_thread($thread_id); + $new_cat = WBF_DB::get_category($category_id); + if ( ! $thread ) wp_send_json_error(['message' => 'Thread nicht gefunden.']); + if ( ! $new_cat ) wp_send_json_error(['message' => 'Kategorie nicht gefunden.']); + if ( (int)$thread->category_id === $category_id ) wp_send_json_error(['message' => 'Thread ist bereits in dieser Kategorie.']); + + WBF_DB::move_thread($thread_id, $category_id); + wp_send_json_success([ + 'message' => 'Thread verschoben nach: ' . $new_cat->name, + 'cat_name' => $new_cat->name, + 'cat_slug' => $new_cat->slug, + ]); + } + + // ── Tag-Autocomplete ────────────────────────────────────────────────────── + + public static function handle_tag_suggest() { + // Kein Nonce-Check nötig — nur lesend, öffentlich + $q = sanitize_text_field( $_POST['q'] ?? $_GET['q'] ?? '' ); + if ( mb_strlen($q) < 1 ) wp_send_json_success(['tags' => []]); + $tags = WBF_DB::suggest_tags( $q, 8 ); + wp_send_json_success(['tags' => $tags]); + } + + // ── Reaktionen ──────────────────────────────────────────────────────────── + + public static function handle_set_reaction() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + if (!WBF_DB::can($user,'like')) wp_send_json_error(['message'=>'Keine Berechtigung.']); + + $object_id = (int)($_POST['object_id'] ?? 0); + $object_type = sanitize_key($_POST['object_type'] ?? 'post'); + $reaction = $_POST['reaction'] ?? ''; + + if (!$object_id || !in_array($object_type, ['post','thread'])) { + wp_send_json_error(['message'=>'Ungültige Daten.']); + } + + $result = WBF_DB::set_reaction($user->id, $object_id, $object_type, $reaction); + if ($result === false) wp_send_json_error(['message'=>'Ungültige Reaktion.']); + + $data = WBF_DB::get_reactions($object_id, $object_type, $user->id); + wp_send_json_success(['action'=>$result,'counts'=>$data['counts'],'mine'=>$data['mine']]); + } + + // ── Private Nachrichten ─────────────────────────────────────────────────── + + public static function handle_send_message() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + if (WBF_Roles::level($user->role) < 0) wp_send_json_error(['message'=>'Kein Zugriff.']); + + $to_id = (int)($_POST['to_id'] ?? 0); + $content = sanitize_textarea_field($_POST['content'] ?? ''); + + if (!$to_id) wp_send_json_error(['message'=>'Empfänger fehlt.']); + if (mb_strlen($content) < 1) wp_send_json_error(['message'=>'Nachricht leer.']); + if ($to_id === (int)$user->id) wp_send_json_error(['message'=>'Du kannst dir nicht selbst schreiben.']); + if (!WBF_DB::get_user($to_id)) wp_send_json_error(['message'=>'Empfänger nicht gefunden.']); + + $id = WBF_DB::send_message($user->id, $to_id, $content); + // Notify recipient + WBF_DB::create_notification($to_id, 'message', $id, $user->id); + // E-Mail + $to_user = WBF_DB::get_user($to_id); + self::send_notification_email($to_user, 'message', $user->display_name); + + wp_send_json_success(['message_id'=>$id,'message'=>'Gesendet!', + 'content'=>esc_html($content), + 'created_at'=>current_time('mysql'), + 'sender_name'=>$user->display_name, + 'sender_avatar'=>$user->avatar_url, + ]); + } + + public static function handle_get_inbox() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + $inbox = WBF_DB::get_inbox($user->id); + $unread = WBF_DB::count_unread_messages($user->id); + wp_send_json_success(['inbox'=>$inbox,'unread'=>$unread]); + } + + public static function handle_get_conversation() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + $partner_id = (int)($_POST['partner_id'] ?? 0); + if (!$partner_id) wp_send_json_error(['message'=>'Ungültig.']); + WBF_DB::mark_messages_read($user->id, $partner_id); + $total = WBF_DB::count_conversation($user->id, $partner_id); + $msgs = WBF_DB::get_conversation($user->id, $partner_id, 50, max(0, $total - 50)); + $partner = WBF_DB::get_user($partner_id); + wp_send_json_success(['messages'=>$msgs,'partner'=>$partner,'my_id'=>$user->id,'total'=>$total]); + } + + public static function handle_mark_messages_read() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + $partner_id = (int)($_POST['partner_id'] ?? 0); + if ($partner_id) WBF_DB::mark_messages_read($user->id, $partner_id); + wp_send_json_success(); + } + + // ── Online-Status ───────────────────────────────────────────────────────── + + public static function handle_get_online_users() { + // Public — kein Login nötig + $users = WBF_DB::get_online_users(15); + wp_send_json_success(['users' => $users]); + } + + // ── User-Autocomplete (für @Erwähnungen + DM) ───────────────────────────── + + public static function handle_user_suggest() { + $q = sanitize_text_field($_POST['q'] ?? $_GET['q'] ?? ''); + if (mb_strlen($q) < 1) wp_send_json_success(['users'=>[]]); + global $wpdb; + $like = $wpdb->esc_like($q) . '%'; + $users = $wpdb->get_results($wpdb->prepare( + "SELECT id, username, display_name, avatar_url, role + FROM {$wpdb->prefix}forum_users + WHERE username LIKE %s OR display_name LIKE %s + ORDER BY display_name ASC LIMIT 8", + $like, $like + )); + wp_send_json_success(['users'=>$users]); + } + + // ── Nachricht löschen ───────────────────────────────────────────────────── + + public static function handle_delete_message() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + + $msg_id = (int)( $_POST['message_id'] ?? 0 ); + if ( ! $msg_id ) wp_send_json_error(['message' => 'Ungültige Nachricht.']); + + global $wpdb; + $msg = $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}forum_messages WHERE id = %d", $msg_id + )); + if ( ! $msg ) wp_send_json_error(['message' => 'Nachricht nicht gefunden.']); + + $uid = (int) $user->id; + if ( (int)$msg->from_id === $uid ) { + $wpdb->update( "{$wpdb->prefix}forum_messages", ['deleted_by_sender' => 1], ['id' => $msg_id] ); + } elseif ( (int)$msg->to_id === $uid ) { + $wpdb->update( "{$wpdb->prefix}forum_messages", ['deleted_by_receiver' => 1], ['id' => $msg_id] ); + } else { + wp_send_json_error(['message' => 'Keine Berechtigung.']); + } + + wp_send_json_success(['message_id' => $msg_id]); + } + + // ── Neue Nachrichten seit ID (Live-Polling) ─────────────────────────────── + + public static function handle_get_new_messages() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + + $partner_id = (int)( $_POST['partner_id'] ?? 0 ); + $since_id = (int)( $_POST['since_id'] ?? 0 ); + + if ( ! $partner_id ) wp_send_json_error(['message' => 'Ungueltig.']); + + global $wpdb; + $msgs = $wpdb->get_results( $wpdb->prepare( + "SELECT m.*, u.display_name AS sender_name, u.avatar_url AS sender_avatar + FROM {$wpdb->prefix}forum_messages m + JOIN {$wpdb->prefix}forum_users u ON u.id = m.from_id + WHERE m.id > %d + AND ( (m.from_id=%d AND m.to_id=%d AND m.deleted_by_sender=0) + OR (m.from_id=%d AND m.to_id=%d AND m.deleted_by_receiver=0) ) + ORDER BY m.created_at ASC + LIMIT 50", + $since_id, + (int)$user->id, $partner_id, + $partner_id, (int)$user->id + )); + + wp_send_json_success(['messages' => $msgs]); + } + + + // ── E-Mail Benachrichtigungen ───────────────────────────────────────────── + + private static function send_notification_email( $to_user, $type, $actor_name, $extra = [] ) { + if ( ! $to_user || empty($to_user->email) ) return; + + $blog_name = get_bloginfo('name'); + $forum_url = wbf_get_forum_url(); + $from_email = get_option('admin_email'); + $from_name = $blog_name . ' Forum'; + + $headers = [ + 'Content-Type: text/html; charset=UTF-8', + "From: {$from_name} <{$from_email}>", + ]; + + switch ( $type ) { + case 'reply': + $thread_id = $extra['thread_id'] ?? 0; + $thread_title = $extra['thread_title'] ?? ''; + $subject = "[{$blog_name}] {$actor_name} hat auf deinen Thread geantwortet"; + $body = self::email_template( + $to_user->display_name, + "{$actor_name} hat in deinem Thread " . esc_html($thread_title) . " geantwortet.", + $forum_url . '?forum_thread=' . (int)$thread_id, + 'Thread ansehen', + $blog_name + ); + break; + + case 'mention': + $thread_id = $extra['thread_id'] ?? 0; + $thread_title = $extra['thread_title'] ?? ''; + $subject = "[{$blog_name}] {$actor_name} hat dich erwähnt"; + $body = self::email_template( + $to_user->display_name, + "{$actor_name} hat dich in einem Beitrag erwähnt: " . esc_html($thread_title) . "", + $forum_url . '?forum_thread=' . (int)$thread_id, + 'Beitrag ansehen', + $blog_name + ); + break; + + case 'message': + $subject = "[{$blog_name}] Neue Privatnachricht von {$actor_name}"; + $body = self::email_template( + $to_user->display_name, + "{$actor_name} hat dir eine Privatnachricht gesendet.", + $forum_url . '?forum_dm=1', + 'Nachricht lesen', + $blog_name + ); + break; + + default: + return; + } + + wp_mail( $to_user->email, $subject, $body, $headers ); + } + + private static function email_template( $name, $text, $url, $btn_label, $blog_name ) { + return " +
+
+ 💬 {$blog_name} +
+
+

Hallo {$name},

+

{$text}

+ {$btn_label} +
+
+

Du erhältst diese E-Mail weil du im {$blog_name} Forum registriert bist.
+ Forum öffnen

+
+
"; + } + + // ── Passwort vergessen ──────────────────────────────────────────────────── + + public static function handle_forgot_password() { + // Kein Nonce nötig — nur E-Mail wird verarbeitet + $email = sanitize_email( $_POST['email'] ?? '' ); + if ( ! is_email($email) ) wp_send_json_error(['message'=>'Ungültige E-Mail-Adresse.']); + + $user = WBF_DB::get_user_by('email', $email); + // Immer Erfolg melden (kein User-Enumeration) + if ( ! $user ) { + wp_send_json_success(['message'=>'Falls diese E-Mail registriert ist, wurde eine E-Mail gesendet.']); + } + + $token = WBF_DB::create_reset_token($user->id); + $forum_url = wbf_get_forum_url(); + $reset_url = $forum_url . '?wbf_reset_token=' . urlencode($token); + $blog_name = get_bloginfo('name'); + + $headers = ['Content-Type: text/html; charset=UTF-8', 'From: ' . $blog_name . ' <' . get_option('admin_email') . '>']; + $body = self::email_template( + $user->display_name, + 'Du hast ein neues Passwort angefordert. Klicke den Button um dein Passwort zurückzusetzen. Der Link ist 1 Stunde gültig.', + $reset_url, + 'Passwort zurücksetzen', + $blog_name + ); + wp_mail( $user->email, "[{$blog_name}] Passwort zurücksetzen", $body, $headers ); + + wp_send_json_success(['message'=>'E-Mail gesendet! Bitte prüfe deinen Posteingang.']); + } + + public static function handle_reset_password() { + self::verify(); + $token = sanitize_text_field( $_POST['token'] ?? '' ); + $password = $_POST['password'] ?? ''; + $password2= $_POST['password2'] ?? ''; + + if ( strlen($password) < 6 ) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']); + if ( $password !== $password2 ) wp_send_json_error(['message'=>'Passwörter stimmen nicht überein.']); + + $ok = WBF_DB::use_reset_token($token, $password); + if ( ! $ok ) wp_send_json_error(['message'=>'Link ungültig oder abgelaufen.']); + + wp_send_json_success(['message'=>'Passwort geändert! Du kannst dich jetzt einloggen.']); + } + + // ── Ältere Nachrichten laden ────────────────────────────────────────────── + + public static function handle_load_more_messages() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']); + + $partner_id = (int)($_POST['partner_id'] ?? 0); + $offset = (int)($_POST['offset'] ?? 0); + $per_page = 30; + + if (!$partner_id) wp_send_json_error(['message'=>'Ungültig.']); + + $total = WBF_DB::count_conversation($user->id, $partner_id); + $msgs = WBF_DB::get_conversation($user->id, $partner_id, $per_page, $offset); + + wp_send_json_success([ + 'messages' => $msgs, + 'my_id' => $user->id, + 'total' => $total, + 'offset' => $offset, + 'has_more' => ($offset + $per_page) < $total, + ]); + } + + +} + +WBF_Ajax::init(); \ No newline at end of file diff --git a/includes/class-forum-auth.php b/includes/class-forum-auth.php new file mode 100644 index 0000000..2eb65df --- /dev/null +++ b/includes/class-forum-auth.php @@ -0,0 +1,103 @@ +user_id ); + if ( $user && WBF_Roles::level($user->role) >= 0 ) { + $_SESSION[ self::SESSION_KEY ] = $user->id; + WBF_DB::touch_last_active( $user->id ); + } + } + } + } + + public static function is_forum_logged_in() { + self::init(); + return ! empty( $_SESSION[ self::SESSION_KEY ] ); + } + + public static function get_current_user() { + self::init(); + if ( empty( $_SESSION[ self::SESSION_KEY ] ) ) return null; + return WBF_DB::get_user( (int) $_SESSION[ self::SESSION_KEY ] ); + } + + public static function login( $username_or_email, $password ) { + self::init(); + $user = WBF_DB::get_user_by( 'username', $username_or_email ); + if ( ! $user ) { + $user = WBF_DB::get_user_by( 'email', $username_or_email ); + } + if ( ! $user ) return array( 'success' => false, 'message' => 'Benutzer nicht gefunden.' ); + if ( ! password_verify( $password, $user->password ) ) { + return array( 'success' => false, 'message' => 'Falsches Passwort.' ); + } + if ( WBF_Roles::level($user->role) < 0 ) { + $reason = !empty($user->ban_reason) ? $user->ban_reason : 'Dein Konto wurde gesperrt.'; + return array( 'success' => false, 'banned' => true, 'message' => $reason ); + } + $_SESSION[ self::SESSION_KEY ] = $user->id; + WBF_DB::touch_last_active( $user->id ); + return array( 'success' => true, 'user' => $user ); + } + + public static function register( $username, $email, $password, $display_name ) { + self::init(); + $username = sanitize_user( $username ); + $email = sanitize_email( $email ); + $display_name = sanitize_text_field( $display_name ); + + if ( strlen($username) < 3 ) return array('success'=>false,'message'=>'Benutzername mindestens 3 Zeichen.'); + if ( ! is_email($email) ) return array('success'=>false,'message'=>'Ungültige E-Mail-Adresse.'); + if ( strlen($password) < 6 ) return array('success'=>false,'message'=>'Passwort mindestens 6 Zeichen.'); + if ( empty($display_name) ) return array('success'=>false,'message'=>'Anzeigename darf nicht leer sein.'); + + if ( WBF_DB::get_user_by('username', $username) ) return array('success'=>false,'message'=>'Benutzername bereits vergeben.'); + if ( WBF_DB::get_user_by('email', $email) ) return array('success'=>false,'message'=>'E-Mail bereits registriert.'); + + $avatar = 'https://www.gravatar.com/avatar/' . md5( strtolower($email) ) . '?d=identicon&s=80'; + + $id = WBF_DB::create_user( array( + 'username' => $username, + 'email' => $email, + 'password' => password_hash( $password, PASSWORD_DEFAULT ), + 'display_name' => $display_name, + 'avatar_url' => $avatar, + )); + + $_SESSION[ self::SESSION_KEY ] = $id; + return array('success'=>true,'user'=>WBF_DB::get_user($id)); + } + + public static function logout() { + self::init(); + $user_id = $_SESSION[ self::SESSION_KEY ] ?? 0; + unset( $_SESSION[ self::SESSION_KEY ] ); + if ( $user_id ) { + WBF_DB::delete_remember_token( (int)$user_id ); + } + // Remove cookie + if ( isset($_COOKIE['wbf_remember']) ) { + setcookie( 'wbf_remember', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + } + } + + /** Remember-Me Token setzen und Cookie senden */ + public static function set_remember_cookie( $user_id ) { + $token = WBF_DB::create_remember_token( (int)$user_id ); + setcookie( 'wbf_remember', $token, time() + 30 * DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + } +} + +add_action('init', array('WBF_Auth','init'), 1); \ No newline at end of file diff --git a/includes/class-forum-bbcode.php b/includes/class-forum-bbcode.php new file mode 100644 index 0000000..acdaa9b --- /dev/null +++ b/includes/class-forum-bbcode.php @@ -0,0 +1,249 @@ +' + . $m[1] // bereits html-escaped durch htmlspecialchars oben + . ''; + return $key; + }, + $out + ); + + // Inline-Code + $out = preg_replace_callback( + '/\[icode\](.*?)\[\/icode\]/is', + function ( $m ) use ( &$placeholders ) { + $key = '%%ICODE_' . count($placeholders) . '%%'; + $placeholders[$key] = '' . $m[1] . ''; + return $key; + }, + $out + ); + + // 4. Alle anderen Tags parsen + $out = self::parse( $out ); + + // 5. Zeilenumbrüche zu
(nur außerhalb von Block-Tags) + $out = self::nl_to_br( $out ); + + // 6. Code-Blöcke wieder einsetzen + foreach ( $placeholders as $key => $html ) { + $out = str_replace( $key, $html, $out ); + } + + return $out; + } + + /** + * Sanitize bei Speicherung: nur HTML streifen, BBCode-Tags bleiben + */ + public static function sanitize( $raw ) { + // Alle echten HTML-Tags entfernen, BBCode-Tags [xxx] bleiben erhalten + return strip_tags( $raw ); + } + + // ── Interner Parser ────────────────────────────────────────────────────── + + private static function parse( $s ) { + + // [b] [i] [u] [s] + $s = preg_replace( '/\[b\](.*?)\[\/b\]/is', '$1', $s ); + $s = preg_replace( '/\[i\](.*?)\[\/i\]/is', '$1', $s ); + $s = preg_replace( '/\[u\](.*?)\[\/u\]/is', '$1', $s ); + $s = preg_replace( '/\[s\](.*?)\[\/s\]/is', '$1', $s ); + + // [h2] [h3] + $s = preg_replace( '/\[h2\](.*?)\[\/h2\]/is', '

$1

', $s ); + $s = preg_replace( '/\[h3\](.*?)\[\/h3\]/is', '

$1

', $s ); + + // [center] [right] + $s = preg_replace( '/\[center\](.*?)\[\/center\]/is', '
$1
', $s ); + $s = preg_replace( '/\[right\](.*?)\[\/right\]/is', '
$1
', $s ); + + // [hr] + $s = str_replace( '[hr]', '
', $s ); + + // [color=...] + $s = preg_replace_callback( + '/\[color=([a-zA-Z0-9#]{1,20})\](.*?)\[\/color\]/is', + function ( $m ) { + $color = $m[1]; + // Hex-Farben direkt erlauben, benannte aus Whitelist + if ( preg_match( '/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $color ) ) { + $safe = esc_attr( $color ); + } elseif ( in_array( strtolower($color), self::$allowed_colors ) ) { + $safe = esc_attr( strtolower($color) ); + } else { + return $m[2]; // Unbekannte Farbe → nur Text + } + return '' . $m[2] . ''; + }, + $s + ); + + // [size=small|large|xlarge] + $s = preg_replace_callback( + '/\[size=(small|large|xlarge)\](.*?)\[\/size\]/is', + function ( $m ) { + $map = [ 'small' => '.8em', 'large' => '1.2em', 'xlarge' => '1.5em' ]; + return '' . $m[2] . ''; + }, + $s + ); + + // [url=...] und [url]...[/url] + $s = preg_replace_callback( + '/\[url=([^\]]{1,500})\](.*?)\[\/url\]/is', + function ( $m ) { + $href = esc_url( $m[1] ); + if ( ! $href ) return $m[2]; + return '' . $m[2] . ''; + }, + $s + ); + $s = preg_replace_callback( + '/\[url\](https?:\/\/[^\[]{1,500})\[\/url\]/is', + function ( $m ) { + $href = esc_url( $m[1] ); + if ( ! $href ) return $m[1]; + return '' . $href . ''; + }, + $s + ); + + // [img] + $s = preg_replace_callback( + '/\[img\](https?:\/\/[^\[]{1,1000})\[\/img\]/is', + function ( $m ) { + $src = esc_url( $m[1] ); + if ( ! $src ) return ''; + return ''; + }, + $s + ); + + // [quote] und [quote=Name] + $s = preg_replace_callback( + '/\[quote=([^\]]{1,80})\](.*?)\[\/quote\]/is', + function ( $m ) { + $author = '' . htmlspecialchars( $m[1], ENT_QUOTES ) . ' schrieb:'; + return '
' + . $author . '' . $m[2] . '
'; + }, + $s + ); + $s = preg_replace( + '/\[quote\](.*?)\[\/quote\]/is', + '
$1
', + $s + ); + + // [spoiler] und [spoiler=Titel] + static $spoiler_id = 0; + $s = preg_replace_callback( + '/\[spoiler=([^\]]{0,80})\](.*?)\[\/spoiler\]/is', + function ( $m ) use ( &$spoiler_id ) { + $spoiler_id++; + $title = htmlspecialchars( $m[1] ?: 'Spoiler', ENT_QUOTES ); + return '
' + . '' + . '' + . '
'; + }, + $s + ); + $s = preg_replace_callback( + '/\[spoiler\](.*?)\[\/spoiler\]/is', + function ( $m ) use ( &$spoiler_id ) { + $spoiler_id++; + return '
' + . '' + . '' + . '
'; + }, + $s + ); + + // [list] [list=1] [*] + $s = preg_replace_callback( + '/\[list(=1)?\](.*?)\[\/list\]/is', + function ( $m ) { + $tag = $m[1] ? 'ol' : 'ul'; + $items = preg_replace( '/\[\*\]\s*/s', '
  • ', $m[2] ); + // Auto-close li tags + $items = preg_replace( '/(
  • )(.*?)(?=
  • |$)/s', '$1$2
  • ', $items ); + return '<' . $tag . ' class="wbf-bb-list">' . trim($items) . ''; + }, + $s + ); + + // @Erwähnungen → klickbare Profil-Links + $s = preg_replace_callback( + '/@([a-zA-Z0-9_]{3,60})\b/', + function( $m ) { + global $wpdb; + $user = $wpdb->get_row( $wpdb->prepare( + "SELECT id, username FROM {$wpdb->prefix}forum_users WHERE username=%s", $m[1] + ) ); + if ( ! $user ) return esc_html($m[0]); + return '@' . esc_html($user->username) . ''; + }, + $s + ); + + return $s; + } + + // Zeilenumbrüche →
    , aber nicht innerhalb von Block-Elementen + private static function nl_to_br( $s ) { + // Einfaches nl2br — ausreichend da Block-Tags (

    ,