From 4fb7c0608e3301ea99c26dc2e956a5058da62ad8 Mon Sep 17 00:00:00 2001
From: M_Viper '
+ . $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', '
', $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 '
' . $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 '
, aber nicht innerhalb von Block-Elementen
+ private static function nl_to_br( $s ) {
+ // Einfaches nl2br — ausreichend da Block-Tags (,
, etc.)
+ // bereits eigene Zeilenumbrüche erzeugen
+ return nl2br( $s );
+ }
+}
\ No newline at end of file
diff --git a/includes/class-forum-db.php b/includes/class-forum-db.php
new file mode 100644
index 0000000..00c1a9a
--- /dev/null
+++ b/includes/class-forum-db.php
@@ -0,0 +1,1137 @@
+get_charset_collate();
+
+ $sql_users = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_users (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ username VARCHAR(60) NOT NULL UNIQUE,
+ email VARCHAR(100) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ display_name VARCHAR(100) NOT NULL,
+ avatar_url VARCHAR(255) DEFAULT '',
+ bio TEXT DEFAULT '',
+ signature TEXT DEFAULT '',
+ role VARCHAR(20) DEFAULT 'member',
+ post_count INT DEFAULT 0,
+ registered DATETIME DEFAULT CURRENT_TIMESTAMP,
+ last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id)
+ ) $charset;";
+
+ $sql_cats = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_categories (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ parent_id BIGINT UNSIGNED DEFAULT 0,
+ name VARCHAR(100) NOT NULL,
+ slug VARCHAR(100) NOT NULL UNIQUE,
+ description TEXT DEFAULT '',
+ icon VARCHAR(50) DEFAULT 'fas fa-comments',
+ sort_order INT DEFAULT 0,
+ thread_count INT DEFAULT 0,
+ post_count INT DEFAULT 0,
+ min_role VARCHAR(20) DEFAULT 'member',
+ PRIMARY KEY (id),
+ KEY parent_id (parent_id)
+ ) $charset;";
+
+ $sql_threads = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_threads (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ category_id BIGINT UNSIGNED NOT NULL,
+ user_id BIGINT UNSIGNED NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ slug VARCHAR(255) NOT NULL,
+ content LONGTEXT NOT NULL,
+ status VARCHAR(20) DEFAULT 'open',
+ pinned TINYINT(1) DEFAULT 0,
+ views INT DEFAULT 0,
+ reply_count INT DEFAULT 0,
+ like_count INT DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ last_reply_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY category_id (category_id)
+ ) $charset;";
+
+ $sql_posts = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_posts (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ thread_id BIGINT UNSIGNED NOT NULL,
+ user_id BIGINT UNSIGNED NOT NULL,
+ content LONGTEXT NOT NULL,
+ like_count INT DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY thread_id (thread_id)
+ ) $charset;";
+
+ $sql_likes = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_likes (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT UNSIGNED NOT NULL,
+ object_id BIGINT UNSIGNED NOT NULL,
+ object_type VARCHAR(20) NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY unique_like (user_id, object_id, object_type)
+ ) $charset;";
+
+ $sql_reports = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_reports (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ object_id BIGINT UNSIGNED NOT NULL,
+ object_type VARCHAR(20) NOT NULL DEFAULT 'post',
+ reporter_id BIGINT UNSIGNED NOT NULL,
+ reason VARCHAR(100) NOT NULL DEFAULT '',
+ note TEXT DEFAULT '',
+ status VARCHAR(20) NOT NULL DEFAULT 'open',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY object_id (object_id),
+ KEY status (status)
+ ) $charset;";
+
+ $sql_tags = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_tags (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ name VARCHAR(60) NOT NULL,
+ slug VARCHAR(60) NOT NULL UNIQUE,
+ use_count INT DEFAULT 0,
+ PRIMARY KEY (id),
+ KEY slug (slug)
+ ) $charset;";
+
+ $sql_thread_tags = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_thread_tags (
+ thread_id BIGINT UNSIGNED NOT NULL,
+ tag_id BIGINT UNSIGNED NOT NULL,
+ PRIMARY KEY (thread_id, tag_id),
+ KEY tag_id (tag_id)
+ ) $charset;";
+
+ $sql_messages = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_messages (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ from_id BIGINT UNSIGNED NOT NULL,
+ to_id BIGINT UNSIGNED NOT NULL,
+ content TEXT NOT NULL,
+ is_read TINYINT(1) DEFAULT 0,
+ deleted_by_sender TINYINT(1) DEFAULT 0,
+ deleted_by_receiver TINYINT(1) DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY from_id (from_id),
+ KEY to_id (to_id)
+ ) $charset;";
+
+ $sql_reactions = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_reactions (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT UNSIGNED NOT NULL,
+ object_id BIGINT UNSIGNED NOT NULL,
+ object_type VARCHAR(20) NOT NULL DEFAULT 'post',
+ reaction VARCHAR(10) NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY unique_reaction (user_id, object_id, object_type),
+ KEY object_id (object_id)
+ ) $charset;";
+
+ $sql_remember = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_remember_tokens (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT UNSIGNED NOT NULL,
+ token VARCHAR(64) NOT NULL,
+ expires_at DATETIME NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY token (token),
+ KEY user_id (user_id)
+ ) $charset;";
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+ dbDelta( $sql_users );
+ dbDelta( $sql_cats );
+ dbDelta( $sql_threads );
+ dbDelta( $sql_posts );
+ dbDelta( $sql_likes );
+ dbDelta( $sql_reports );
+ dbDelta( $sql_tags );
+ dbDelta( $sql_thread_tags );
+ dbDelta( $sql_messages );
+ dbDelta( $sql_reactions );
+ dbDelta( $sql_remember );
+
+ // Live upgrades — add new columns to existing installs
+ self::maybe_add_column("{$wpdb->prefix}forum_users", 'signature', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN signature TEXT DEFAULT '' AFTER bio");
+ self::maybe_add_column("{$wpdb->prefix}forum_users", 'ban_reason', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN ban_reason TEXT DEFAULT '' AFTER role");
+ self::maybe_add_column("{$wpdb->prefix}forum_categories", 'parent_id', "ALTER TABLE {$wpdb->prefix}forum_categories ADD COLUMN parent_id BIGINT UNSIGNED DEFAULT 0 AFTER id");
+ self::maybe_add_column("{$wpdb->prefix}forum_categories", 'min_role', "ALTER TABLE {$wpdb->prefix}forum_categories ADD COLUMN min_role VARCHAR(20) DEFAULT 'member' AFTER post_count");
+ self::maybe_add_column("{$wpdb->prefix}forum_users", 'reset_token', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN reset_token VARCHAR(64) DEFAULT NULL");
+ self::maybe_add_column("{$wpdb->prefix}forum_users", 'reset_token_expires', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN reset_token_expires DATETIME DEFAULT NULL");
+ $sql_notifications = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_notifications (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT UNSIGNED NOT NULL,
+ type VARCHAR(30) NOT NULL DEFAULT 'reply',
+ object_id BIGINT UNSIGNED NOT NULL,
+ actor_id BIGINT UNSIGNED NOT NULL,
+ is_read TINYINT(1) DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY user_id (user_id),
+ KEY is_read (is_read)
+ ) $charset;";
+
+ // Ensure reports + notifications tables exist on existing installs
+ dbDelta( $sql_reports );
+ dbDelta( $sql_notifications );
+
+ // Default categories
+ $count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_categories");
+ if ( (int)$count === 0 ) {
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>0,'name'=>'Allgemein', 'slug'=>'allgemein', 'description'=>'Allgemeine Diskussionen','icon'=>'fas fa-home', 'sort_order'=>1]);
+ $p1 = $wpdb->insert_id;
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>0,'name'=>'Ankündigungen', 'slug'=>'ankuendigungen','description'=>'Wichtige Neuigkeiten', 'icon'=>'fas fa-bullhorn', 'sort_order'=>2,'min_role'=>'moderator']);
+ $p2 = $wpdb->insert_id;
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>0,'name'=>'Support', 'slug'=>'support', 'description'=>'Hilfe & Fragen', 'icon'=>'fas fa-life-ring','sort_order'=>3]);
+ $p3 = $wpdb->insert_id;
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>$p1,'name'=>'Introductions', 'slug'=>'introductions', 'description'=>'Stell dich vor!', 'icon'=>'fas fa-user', 'sort_order'=>1]);
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>$p1,'name'=>'Off-Topic', 'slug'=>'off-topic', 'description'=>'Alles außerhalb des Themas', 'icon'=>'fas fa-coffee', 'sort_order'=>2]);
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>$p3,'name'=>'Bug Reports', 'slug'=>'bug-reports', 'description'=>'Fehler melden', 'icon'=>'fas fa-bug', 'sort_order'=>1]);
+ $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>$p3,'name'=>'Feature Requests','slug'=>'feature-requests','description'=>'Neue Funktionen vorschlagen','icon'=>'fas fa-lightbulb','sort_order'=>2]);
+ }
+ }
+
+ private static function maybe_add_column( $table, $column, $sql ) {
+ global $wpdb;
+ $cols = $wpdb->get_col("DESCRIBE {$table}");
+ if ( ! in_array($column, $cols) ) {
+ $wpdb->query($sql);
+ }
+ }
+
+ // ── Rollen — delegiert an WBF_Roles ──────────────────────────────────────
+
+ public static function role_level( $role ) { return WBF_Roles::level($role); }
+ public static function all_roles() { return WBF_Roles::labels(); }
+ public static function can( $user, $action ){ return WBF_Roles::can($user, $action); }
+ public static function can_post_in( $user, $cat ) { return WBF_Roles::can_post_in($user, $cat); }
+
+ // ── Users ─────────────────────────────────────────────────────────────────
+
+ public static function get_user( $id ) {
+ global $wpdb;
+ return $wpdb->get_row( $wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_users WHERE id=%d", $id) );
+ }
+
+ public static function get_user_by( $field, $value ) {
+ global $wpdb;
+ $field = sanitize_key($field);
+ // Benutzername & E-Mail: Groß-/Kleinschreibung ignorieren (LOWER)
+ if ( in_array($field, ['username', 'email']) ) {
+ return $wpdb->get_row( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_users WHERE LOWER($field)=LOWER(%s)",
+ $value
+ ) );
+ }
+ return $wpdb->get_row( $wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_users WHERE $field=%s", $value) );
+ }
+
+ public static function create_user( $data ) {
+ global $wpdb;
+ $wpdb->insert("{$wpdb->prefix}forum_users", $data);
+ return $wpdb->insert_id;
+ }
+
+ public static function update_user( $id, $data ) {
+ global $wpdb;
+ $wpdb->update("{$wpdb->prefix}forum_users", $data, ['id' => $id]);
+ }
+
+ public static function get_all_users( $limit = 100, $offset = 0 ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_users ORDER BY registered DESC LIMIT %d OFFSET %d",
+ $limit, $offset
+ ));
+ }
+
+ // ── Categories ────────────────────────────────────────────────────────────
+
+ public static function get_categories_tree() {
+ global $wpdb;
+ $all = $wpdb->get_results(
+ "SELECT c.*,
+ (SELECT t.title FROM {$wpdb->prefix}forum_threads t WHERE t.category_id=c.id ORDER BY t.last_reply_at DESC LIMIT 1) as last_thread_title,
+ (SELECT t.id FROM {$wpdb->prefix}forum_threads t WHERE t.category_id=c.id ORDER BY t.last_reply_at DESC LIMIT 1) as last_thread_id,
+ (SELECT u.display_name FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_users u ON u.id=t.user_id
+ WHERE t.category_id=c.id ORDER BY t.last_reply_at DESC LIMIT 1) as last_post_author
+ FROM {$wpdb->prefix}forum_categories c
+ ORDER BY c.parent_id ASC, c.sort_order ASC"
+ );
+ $by_id = [];
+ foreach ($all as $cat) { $cat->children = []; $by_id[$cat->id] = $cat; }
+ $tree = [];
+ foreach ($by_id as $id => $cat) {
+ if ((int)$cat->parent_id === 0) $tree[] = &$by_id[$id];
+ elseif (isset($by_id[$cat->parent_id])) $by_id[$cat->parent_id]->children[] = &$by_id[$id];
+ }
+ return $tree;
+ }
+
+ public static function get_categories_flat() {
+ global $wpdb;
+ return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}forum_categories ORDER BY parent_id ASC, sort_order ASC");
+ }
+
+ public static function get_category( $id_or_slug ) {
+ global $wpdb;
+ if (is_numeric($id_or_slug)) return $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_categories WHERE id=%d", $id_or_slug));
+ return $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_categories WHERE slug=%s", $id_or_slug));
+ }
+
+ public static function get_category_breadcrumb( $cat ) {
+ $path = [$cat]; $max = 5;
+ while ((int)$cat->parent_id > 0 && $max--) {
+ $cat = self::get_category((int)$cat->parent_id);
+ if (!$cat) break;
+ array_unshift($path, $cat);
+ }
+ return $path;
+ }
+
+ public static function get_child_categories( $parent_id ) {
+ global $wpdb;
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_categories WHERE parent_id=%d ORDER BY sort_order ASC", $parent_id
+ ));
+ }
+
+ // ── Threads ───────────────────────────────────────────────────────────────
+
+ public static function get_threads( $category_id, $page = 1, $per_page = 20, $include_archived = false ) {
+ global $wpdb;
+ $offset = ($page - 1) * $per_page;
+ $status_sql = $include_archived ? '' : "AND t.status != 'archived'";
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT t.*, u.display_name, u.avatar_url, u.username, u.role as author_role
+ FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
+ WHERE t.category_id = %d $status_sql
+ ORDER BY t.pinned DESC, t.last_reply_at DESC
+ LIMIT %d OFFSET %d",
+ $category_id, $per_page, $offset
+ ));
+ }
+
+ public static function get_archived_threads( $category_id = 0, $page = 1, $per_page = 20 ) {
+ global $wpdb;
+ $offset = ($page - 1) * $per_page;
+ $cat_sql = $category_id ? $wpdb->prepare('AND t.category_id = %d', $category_id) : '';
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT t.*, u.display_name, u.avatar_url, u.username, u.role as author_role,
+ c.name as cat_name, c.slug as cat_slug
+ FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ WHERE t.status = 'archived' $cat_sql
+ ORDER BY t.last_reply_at DESC
+ LIMIT %d OFFSET %d",
+ $per_page, $offset
+ ));
+ }
+
+ public static function count_archived_threads( $category_id = 0 ) {
+ global $wpdb;
+ if ( $category_id ) {
+ return (int)$wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status='archived' AND category_id=%d", $category_id
+ ));
+ }
+ return (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status='archived'");
+ }
+
+ public static function count_threads( $category_id ) {
+ global $wpdb;
+ return (int)$wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE category_id=%d AND status != 'archived'",
+ $category_id
+ ));
+ }
+
+ public static function move_thread( $thread_id, $new_category_id ) {
+ global $wpdb;
+ $thread = self::get_thread($thread_id);
+ if ( ! $thread ) return false;
+ $old_cat = (int) $thread->category_id;
+ $new_cat = (int) $new_category_id;
+ if ( $old_cat === $new_cat ) return false;
+
+ $wpdb->update( "{$wpdb->prefix}forum_threads", ['category_id' => $new_cat], ['id' => $thread_id] );
+ // Adjust thread counts (don't count archived)
+ if ( $thread->status !== 'archived' ) {
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d", $old_cat
+ ));
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET thread_count=thread_count+1 WHERE id=%d", $new_cat
+ ));
+ }
+ // Move post_count contribution too
+ $post_count = (int)$wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id
+ ));
+ if ( $post_count > 0 ) {
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET post_count=GREATEST(post_count-%d,0) WHERE id=%d", $post_count, $old_cat
+ ));
+ $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_categories SET post_count=post_count+%d WHERE id=%d", $post_count, $new_cat
+ ));
+ }
+ return true;
+ }
+
+ public static function get_thread( $id ) {
+ global $wpdb;
+ return $wpdb->get_row($wpdb->prepare(
+ "SELECT t.*, u.display_name, u.avatar_url, u.username, u.signature,
+ u.post_count as author_posts, u.registered as author_registered, u.role as author_role
+ FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
+ WHERE t.id = %d", $id
+ ));
+ }
+
+ public static function create_thread( $data ) {
+ global $wpdb;
+ $wpdb->insert("{$wpdb->prefix}forum_threads", $data);
+ $id = $wpdb->insert_id;
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=thread_count+1 WHERE id=%d", $data['category_id']));
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=post_count+1 WHERE id=%d", $data['user_id']));
+ return $id;
+ }
+
+ public static function delete_thread( $id ) {
+ global $wpdb;
+ $thread = self::get_thread($id);
+ if (!$thread) return;
+ // Clean up tag associations and decrement use_counts
+ $tag_ids = $wpdb->get_col( $wpdb->prepare(
+ "SELECT tag_id FROM {$wpdb->prefix}forum_thread_tags WHERE thread_id=%d", $id
+ ) );
+ $wpdb->delete( "{$wpdb->prefix}forum_thread_tags", ['thread_id' => $id] );
+ if ( $tag_ids ) {
+ foreach ( $tag_ids as $tid ) {
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_tags SET use_count=GREATEST(use_count-1,0) WHERE id=%d", (int)$tid
+ ) );
+ }
+ }
+ $wpdb->delete("{$wpdb->prefix}forum_posts", ['thread_id' => $id]);
+ $wpdb->delete("{$wpdb->prefix}forum_threads", ['id' => $id]);
+ if ( $thread->status !== 'archived' ) {
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d", $thread->category_id));
+ }
+ }
+
+ public static function update_thread( $id, $data ) {
+ global $wpdb;
+ $wpdb->update("{$wpdb->prefix}forum_threads", $data, ['id' => $id]);
+ }
+
+ // ── Posts ─────────────────────────────────────────────────────────────────
+
+ public static function get_posts( $thread_id, $page = 1, $per_page = 15 ) {
+ global $wpdb;
+ $offset = ($page - 1) * $per_page;
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT p.*, u.display_name, u.avatar_url, u.username, u.signature,
+ u.post_count as author_posts, u.role as author_role, u.registered as author_registered
+ FROM {$wpdb->prefix}forum_posts p
+ JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
+ WHERE p.thread_id = %d
+ ORDER BY p.created_at ASC
+ LIMIT %d OFFSET %d",
+ $thread_id, $per_page, $offset
+ ));
+ }
+
+ public static function count_posts( $thread_id ) {
+ global $wpdb;
+ return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id));
+ }
+
+ public static function create_post( $data ) {
+ global $wpdb;
+ $wpdb->insert("{$wpdb->prefix}forum_posts", $data);
+ $id = $wpdb->insert_id;
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET reply_count=reply_count+1, last_reply_at=NOW() WHERE id=%d", $data['thread_id']));
+ $thread = $wpdb->get_row($wpdb->prepare("SELECT category_id FROM {$wpdb->prefix}forum_threads WHERE id=%d", $data['thread_id']));
+ if ($thread) $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET post_count=post_count+1 WHERE id=%d", $thread->category_id));
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=post_count+1 WHERE id=%d", $data['user_id']));
+ return $id;
+ }
+
+ public static function delete_post( $id ) {
+ global $wpdb;
+ $post = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}forum_posts WHERE id=%d", $id));
+ if (!$post) return;
+ $wpdb->delete("{$wpdb->prefix}forum_posts", ['id' => $id]);
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET reply_count=GREATEST(reply_count-1,0) WHERE id=%d", $post->thread_id));
+ $wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=GREATEST(post_count-1,0) WHERE id=%d", $post->user_id));
+ }
+
+ // ── Likes ─────────────────────────────────────────────────────────────────
+
+ public static function has_liked( $user_id, $object_id, $type ) {
+ global $wpdb;
+ return (bool)$wpdb->get_var($wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_likes WHERE user_id=%d AND object_id=%d AND object_type=%s",
+ $user_id, $object_id, $type
+ ));
+ }
+
+ public static function toggle_like( $user_id, $object_id, $type ) {
+ global $wpdb;
+ $map = ['thread'=>"{$wpdb->prefix}forum_threads",'post'=>"{$wpdb->prefix}forum_posts"];
+ if (self::has_liked($user_id, $object_id, $type)) {
+ $wpdb->delete("{$wpdb->prefix}forum_likes", ['user_id'=>$user_id,'object_id'=>$object_id,'object_type'=>$type]);
+ if (isset($map[$type])) $wpdb->query($wpdb->prepare("UPDATE {$map[$type]} SET like_count=GREATEST(like_count-1,0) WHERE id=%d",$object_id));
+ return 'unliked';
+ } else {
+ $wpdb->insert("{$wpdb->prefix}forum_likes", ['user_id'=>$user_id,'object_id'=>$object_id,'object_type'=>$type]);
+ if (isset($map[$type])) $wpdb->query($wpdb->prepare("UPDATE {$map[$type]} SET like_count=like_count+1 WHERE id=%d",$object_id));
+ return 'liked';
+ }
+ }
+
+ public static function get_like_count( $object_id, $type ) {
+ global $wpdb;
+ $map = ['thread'=>"{$wpdb->prefix}forum_threads",'post'=>"{$wpdb->prefix}forum_posts"];
+ if (!isset($map[$type])) return 0;
+ return (int)$wpdb->get_var($wpdb->prepare("SELECT like_count FROM {$map[$type]} WHERE id=%d",$object_id));
+ }
+
+ // ── Stats ─────────────────────────────────────────────────────────────────
+
+ public static function get_recent_threads( $limit = 5 ) {
+ global $wpdb;
+ return $wpdb->get_results($wpdb->prepare(
+ "SELECT t.id, t.title, t.created_at, u.display_name, c.name as cat_name, c.slug as cat_slug
+ FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ AND t.status != 'archived' ORDER BY t.created_at DESC LIMIT %d", $limit
+ ));
+ }
+
+ public static function get_stats() {
+ global $wpdb;
+ return [
+ 'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived'"),
+ 'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts"),
+ 'members' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users"),
+ 'newest' => $wpdb->get_var("SELECT display_name FROM {$wpdb->prefix}forum_users ORDER BY registered DESC LIMIT 1"),
+ ];
+ }
+
+ // ── Reports ───────────────────────────────────────────────────────────────
+
+ public static function create_report( $data ) {
+ global $wpdb;
+ $wpdb->insert("{$wpdb->prefix}forum_reports", $data);
+ return $wpdb->insert_id;
+ }
+
+ public static function has_reported( $reporter_id, $object_id, $type = 'post' ) {
+ global $wpdb;
+ return (bool)$wpdb->get_var($wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_reports WHERE reporter_id=%d AND object_id=%d AND object_type=%s AND status='open'",
+ $reporter_id, $object_id, $type
+ ));
+ }
+
+ public static function get_reports( $status = 'open', $limit = 50 ) {
+ global $wpdb;
+ $sql = "SELECT r.*,
+ rep.display_name AS reporter_name, rep.username AS reporter_username,
+ p.content AS post_content, p.thread_id AS thread_id,
+ t.title AS thread_title
+ FROM {$wpdb->prefix}forum_reports r
+ LEFT JOIN {$wpdb->prefix}forum_users rep ON rep.id = r.reporter_id
+ LEFT JOIN {$wpdb->prefix}forum_posts p ON p.id = r.object_id AND r.object_type = 'post'
+ LEFT JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id";
+ if ( $status !== 'all' ) {
+ $sql .= $wpdb->prepare( " WHERE r.status = %s", $status );
+ }
+ $sql .= $wpdb->prepare( " ORDER BY r.created_at DESC LIMIT %d", $limit );
+ return $wpdb->get_results( $sql );
+ }
+
+ public static function update_report( $id, $status ) {
+ global $wpdb;
+ $wpdb->update("{$wpdb->prefix}forum_reports", ['status' => $status], ['id' => $id]);
+ }
+
+ public static function get_user_posts( $user_id, $limit = 30 ) {
+ global $wpdb;
+ // UNION: Thread-Erstbeiträge (forum_threads) + Antworten (forum_posts)
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT 'thread' AS entry_type,
+ t.id AS id, t.content, t.created_at, t.like_count,
+ t.id AS thread_id, t.title AS thread_title,
+ c.name AS cat_name
+ FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ WHERE t.user_id = %d
+ UNION ALL
+ SELECT 'post' AS entry_type,
+ p.id AS id, p.content, p.created_at, p.like_count,
+ t.id AS thread_id, t.title AS thread_title,
+ c.name AS cat_name
+ FROM {$wpdb->prefix}forum_posts p
+ JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ WHERE p.user_id = %d
+ ORDER BY created_at DESC
+ LIMIT %d",
+ $user_id, $user_id, $limit
+ ) );
+ }
+
+ // ── Thread-Teilnehmer (für Benachrichtigungen) ───────────────────────────
+
+ public static function get_thread_participants( $thread_id ) {
+ global $wpdb;
+ // Thread-Ersteller + alle die geantwortet haben (ohne Duplikate)
+ return $wpdb->get_col( $wpdb->prepare(
+ "SELECT DISTINCT user_id FROM (
+ SELECT user_id FROM {$wpdb->prefix}forum_threads WHERE id = %d
+ UNION
+ SELECT user_id FROM {$wpdb->prefix}forum_posts WHERE thread_id = %d
+ ) AS participants",
+ $thread_id, $thread_id
+ ) );
+ }
+
+ // ── Suche ─────────────────────────────────────────────────────────────────
+
+ public static function search( $query, $limit = 30 ) {
+ global $wpdb;
+ $like = '%' . $wpdb->esc_like( $query ) . '%';
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT 'thread' AS result_type,
+ t.id, t.title, t.content, t.created_at, t.reply_count,
+ u.display_name, u.avatar_url, u.role AS author_role,
+ c.name AS cat_name, c.slug AS cat_slug
+ FROM {$wpdb->prefix}forum_threads t
+ JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ WHERE (t.title LIKE %s OR t.content LIKE %s) AND t.status != 'archived'
+ UNION ALL
+ SELECT 'post' AS result_type,
+ p.id, t.title, p.content, p.created_at, 0 AS reply_count,
+ u.display_name, u.avatar_url, u.role AS author_role,
+ c.name AS cat_name, c.slug AS cat_slug
+ FROM {$wpdb->prefix}forum_posts p
+ JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id
+ JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ WHERE p.content LIKE %s AND t.status != 'archived'
+ ORDER BY created_at DESC
+ LIMIT %d",
+ $like, $like, $like, $limit
+ ) );
+ }
+
+ // ── Benachrichtigungen ────────────────────────────────────────────────────
+
+ public static function create_notification( $user_id, $type, $object_id, $actor_id ) {
+ global $wpdb;
+ // Keine doppelten ungelesenen Benachrichtigungen
+ $exists = $wpdb->get_var( $wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_notifications
+ WHERE user_id=%d AND type=%s AND object_id=%d AND actor_id=%d AND is_read=0",
+ $user_id, $type, $object_id, $actor_id
+ ) );
+ if ( $exists ) return;
+ // Nicht sich selbst benachrichtigen
+ if ( (int)$user_id === (int)$actor_id ) return;
+ $wpdb->insert( "{$wpdb->prefix}forum_notifications", [
+ 'user_id' => $user_id,
+ 'type' => $type,
+ 'object_id' => $object_id,
+ 'actor_id' => $actor_id,
+ ] );
+ }
+
+ public static function get_notifications( $user_id, $limit = 20 ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT n.*,
+ u.display_name AS actor_name, u.avatar_url AS actor_avatar,
+ t.title AS thread_title, t.id AS thread_id
+ FROM {$wpdb->prefix}forum_notifications n
+ JOIN {$wpdb->prefix}forum_users u ON u.id = n.actor_id
+ LEFT JOIN {$wpdb->prefix}forum_threads t ON t.id = n.object_id
+ WHERE n.user_id = %d
+ ORDER BY n.created_at DESC
+ LIMIT %d",
+ $user_id, $limit
+ ) );
+ }
+
+ public static function count_unread_notifications( $user_id ) {
+ global $wpdb;
+ $table = "{$wpdb->prefix}forum_notifications";
+ if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return 0;
+ return (int) $wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_notifications WHERE user_id=%d AND is_read=0",
+ $user_id
+ ) );
+ }
+
+ public static function mark_notifications_read( $user_id ) {
+ global $wpdb;
+ $wpdb->update(
+ "{$wpdb->prefix}forum_notifications",
+ ['is_read' => 1],
+ ['user_id' => $user_id, 'is_read' => 0]
+ );
+ }
+
+ public static function count_open_reports() {
+ global $wpdb;
+ // Tabelle existiert evtl. noch nicht auf bestehenden Installs → erst prüfen
+ $table = "{$wpdb->prefix}forum_reports";
+ if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) {
+ return 0;
+ }
+ return (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE status='open'");
+ }
+
+ // ── Tags ──────────────────────────────────────────────────────────────────
+
+ /**
+ * Tags für einen Thread speichern.
+ * $raw_tags = komma- oder leerzeichen-getrennte Zeichenkette, z.B. "php wordpress #cms"
+ */
+ public static function sync_thread_tags( $thread_id, $raw_tags ) {
+ global $wpdb;
+ $thread_id = (int) $thread_id;
+
+ // Bestehende Verknüpfungen löschen
+ $old_ids = $wpdb->get_col( $wpdb->prepare(
+ "SELECT tag_id FROM {$wpdb->prefix}forum_thread_tags WHERE thread_id=%d", $thread_id
+ ) );
+ $wpdb->delete( "{$wpdb->prefix}forum_thread_tags", ['thread_id' => $thread_id] );
+ // use_count für entfernte Tags dekrementieren
+ if ( $old_ids ) {
+ $placeholders = implode(',', array_fill(0, count($old_ids), '%d'));
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_tags SET use_count = GREATEST(use_count-1,0) WHERE id IN ($placeholders)",
+ ...$old_ids
+ ) );
+ }
+
+ // Tags parsen
+ $names = self::parse_tag_string( $raw_tags );
+ if ( empty($names) ) return;
+
+ foreach ( $names as $name ) {
+ $slug = sanitize_title( $name );
+ if ( ! $slug ) continue;
+
+ // Upsert Tag
+ $tag = $wpdb->get_row( $wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_tags WHERE slug=%s", $slug
+ ) );
+ if ( $tag ) {
+ $tag_id = (int) $tag->id;
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_tags SET use_count=use_count+1 WHERE id=%d", $tag_id
+ ) );
+ } else {
+ $wpdb->insert( "{$wpdb->prefix}forum_tags", [
+ 'name' => $name,
+ 'slug' => $slug,
+ 'use_count' => 1,
+ ] );
+ $tag_id = $wpdb->insert_id;
+ }
+
+ // Pivot
+ $wpdb->replace( "{$wpdb->prefix}forum_thread_tags", [
+ 'thread_id' => $thread_id,
+ 'tag_id' => $tag_id,
+ ] );
+ }
+ }
+
+ public static function parse_tag_string( $raw ) {
+ // Strip # prefix, split by comma / space / semicolon
+ $raw = strip_tags( $raw );
+ $raw = str_replace('#', '', $raw);
+ $parts = preg_split('/[\s,;]+/', $raw, -1, PREG_SPLIT_NO_EMPTY);
+ $names = [];
+ foreach ( $parts as $p ) {
+ $p = mb_strtolower( trim($p) );
+ if ( mb_strlen($p) >= 2 && mb_strlen($p) <= 30 ) {
+ $names[] = $p;
+ }
+ }
+ return array_unique( array_slice($names, 0, 10) ); // max 10 Tags pro Thread
+ }
+
+ /** Tags eines Threads laden */
+ public static function get_thread_tags( $thread_id ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT t.* FROM {$wpdb->prefix}forum_tags t
+ INNER JOIN {$wpdb->prefix}forum_thread_tags tt ON tt.tag_id = t.id
+ WHERE tt.thread_id = %d
+ ORDER BY t.name ASC",
+ $thread_id
+ ) );
+ }
+
+ /** Threads nach Tag-Slug laden */
+ public static function get_threads_by_tag( $slug, $page = 1, $per_page = 20 ) {
+ global $wpdb;
+ $offset = ($page - 1) * $per_page;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT t.*, u.display_name, u.avatar_url, u.role AS author_role,
+ c.name AS cat_name, c.slug AS cat_slug
+ FROM {$wpdb->prefix}forum_threads t
+ INNER JOIN {$wpdb->prefix}forum_thread_tags tt ON tt.thread_id = t.id
+ INNER JOIN {$wpdb->prefix}forum_tags tg ON tg.id = tt.tag_id
+ JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
+ JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
+ WHERE tg.slug = %s AND t.status != 'archived'
+ ORDER BY t.last_reply_at DESC
+ LIMIT %d OFFSET %d",
+ $slug, $per_page, $offset
+ ) );
+ }
+
+ public static function count_threads_by_tag( $slug ) {
+ global $wpdb;
+ return (int) $wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads t
+ INNER JOIN {$wpdb->prefix}forum_thread_tags tt ON tt.thread_id = t.id
+ INNER JOIN {$wpdb->prefix}forum_tags tg ON tg.id = tt.tag_id
+ WHERE tg.slug = %s AND t.status != 'archived'",
+ $slug
+ ) );
+ }
+
+ /** Tag-Objekt per Slug */
+ public static function get_tag( $slug ) {
+ global $wpdb;
+ return $wpdb->get_row( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_tags WHERE slug=%s", $slug
+ ) );
+ }
+
+ /** Top-Tags nach Nutzungshäufigkeit */
+ public static function get_popular_tags( $limit = 30 ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_tags WHERE use_count > 0 ORDER BY use_count DESC LIMIT %d",
+ $limit
+ ) );
+ }
+
+ /** Autocomplete: Tags die mit $q beginnen */
+ public static function suggest_tags( $q, $limit = 8 ) {
+ global $wpdb;
+ $like = $wpdb->esc_like( strtolower($q) ) . '%';
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT name, slug, use_count FROM {$wpdb->prefix}forum_tags WHERE slug LIKE %s ORDER BY use_count DESC LIMIT %d",
+ $like, $limit
+ ) );
+ }
+
+
+ // ── Online-Status ──────────────────────────────────────────────────────────
+
+ /** Letztes Aktivitätsdatum des Users aktualisieren */
+ public static function touch_last_active( $user_id ) {
+ global $wpdb;
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_users SET last_active=NOW() WHERE id=%d", (int)$user_id
+ ) );
+ }
+
+ /** User die in den letzten $minutes Minuten aktiv waren */
+ public static function get_online_users( $minutes = 15 ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT id, username, display_name, avatar_url, role
+ FROM {$wpdb->prefix}forum_users
+ WHERE last_active >= DATE_SUB(NOW(), INTERVAL %d MINUTE)
+ ORDER BY last_active DESC
+ LIMIT 50",
+ $minutes
+ ) );
+ }
+
+ public static function is_online( $user_id, $minutes = 15 ) {
+ global $wpdb;
+ return (bool)$wpdb->get_var( $wpdb->prepare(
+ "SELECT id FROM {$wpdb->prefix}forum_users
+ WHERE id=%d AND last_active >= DATE_SUB(NOW(), INTERVAL %d MINUTE)",
+ $user_id, $minutes
+ ) );
+ }
+
+ // ── Reaktionen ────────────────────────────────────────────────────────────
+
+ public static function set_reaction( $user_id, $object_id, $object_type, $reaction ) {
+ global $wpdb;
+ $allowed = ['👍','❤️','😂','😮','😢','😡'];
+ if ( ! in_array($reaction, $allowed, true) ) return false;
+
+ $existing = $wpdb->get_row( $wpdb->prepare(
+ "SELECT reaction FROM {$wpdb->prefix}forum_reactions
+ WHERE user_id=%d AND object_id=%d AND object_type=%s",
+ $user_id, $object_id, $object_type
+ ) );
+
+ if ( $existing && $existing->reaction === $reaction ) {
+ // Same reaction → remove (toggle off)
+ $wpdb->delete( "{$wpdb->prefix}forum_reactions", [
+ 'user_id' => $user_id, 'object_id' => $object_id, 'object_type' => $object_type
+ ] );
+ return 'removed';
+ }
+ // Insert or replace
+ $wpdb->replace( "{$wpdb->prefix}forum_reactions", [
+ 'user_id' => $user_id,
+ 'object_id' => $object_id,
+ 'object_type' => $object_type,
+ 'reaction' => $reaction,
+ ] );
+ return 'added';
+ }
+
+ /** Reaktionen für ein Objekt — [emoji => count] + user's own reaction */
+ public static function get_reactions( $object_id, $object_type, $user_id = 0 ) {
+ global $wpdb;
+ $rows = $wpdb->get_results( $wpdb->prepare(
+ "SELECT reaction, COUNT(*) as cnt
+ FROM {$wpdb->prefix}forum_reactions
+ WHERE object_id=%d AND object_type=%s
+ GROUP BY reaction",
+ $object_id, $object_type
+ ) );
+ $counts = [];
+ foreach ( $rows as $r ) $counts[$r->reaction] = (int)$r->cnt;
+
+ $my = '';
+ if ( $user_id ) {
+ $row = $wpdb->get_row( $wpdb->prepare(
+ "SELECT reaction FROM {$wpdb->prefix}forum_reactions
+ WHERE user_id=%d AND object_id=%d AND object_type=%s",
+ $user_id, $object_id, $object_type
+ ) );
+ if ($row) $my = $row->reaction;
+ }
+ return ['counts' => $counts, 'mine' => $my];
+ }
+
+ // ── Private Nachrichten ───────────────────────────────────────────────────
+
+ public static function send_message( $from_id, $to_id, $content ) {
+ global $wpdb;
+ $wpdb->insert( "{$wpdb->prefix}forum_messages", [
+ 'from_id' => (int)$from_id,
+ 'to_id' => (int)$to_id,
+ 'content' => mb_substr(strip_tags($content), 0, 2000),
+ 'is_read' => 0,
+ ] );
+ return $wpdb->insert_id;
+ }
+
+ /** Alle Konversationspartner des Users */
+ public static function get_inbox( $user_id ) {
+ global $wpdb;
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT
+ partner_id,
+ MAX(last_msg_id) AS last_msg_id,
+ MAX(last_time) AS last_time,
+ SUM(unread_cnt) AS unread_cnt,
+ partner_name, partner_avatar, partner_role
+ FROM (
+ SELECT
+ m.from_id AS partner_id,
+ MAX(m.id) AS last_msg_id,
+ MAX(m.created_at) AS last_time,
+ SUM(CASE WHEN m.is_read=0 AND m.to_id=%d THEN 1 ELSE 0 END) AS unread_cnt,
+ u.display_name AS partner_name,
+ u.avatar_url AS partner_avatar,
+ u.role AS partner_role
+ FROM {$wpdb->prefix}forum_messages m
+ JOIN {$wpdb->prefix}forum_users u ON u.id = m.from_id
+ WHERE m.to_id=%d AND m.deleted_by_receiver=0
+ GROUP BY m.from_id
+
+ UNION ALL
+
+ SELECT
+ m.to_id AS partner_id,
+ MAX(m.id) AS last_msg_id,
+ MAX(m.created_at) AS last_time,
+ 0 AS unread_cnt,
+ u.display_name AS partner_name,
+ u.avatar_url AS partner_avatar,
+ u.role AS partner_role
+ FROM {$wpdb->prefix}forum_messages m
+ JOIN {$wpdb->prefix}forum_users u ON u.id = m.to_id
+ WHERE m.from_id=%d AND m.deleted_by_sender=0
+ GROUP BY m.to_id
+ ) sub
+ GROUP BY partner_id, partner_name, partner_avatar, partner_role
+ ORDER BY last_time DESC",
+ $user_id, $user_id, $user_id
+ ) );
+ }
+
+ /** Nachrichten einer Konversation zwischen zwei Usern */
+ public static function get_conversation( $user_id, $partner_id, $limit = 50, $offset = 0 ) {
+ global $wpdb;
+ // Neueste $limit Nachrichten ab $offset holen, dann aufsteigend sortieren
+ return $wpdb->get_results( $wpdb->prepare(
+ "SELECT * FROM (
+ SELECT m.*, u.display_name AS sender_name, u.avatar_url AS sender_avatar
+ FROM {$wpdb->prefix}forum_messages m
+ JOIN {$wpdb->prefix}forum_users u ON u.id = m.from_id
+ WHERE ( (m.from_id=%d AND m.to_id=%d AND m.deleted_by_sender=0)
+ OR (m.from_id=%d AND m.to_id=%d AND m.deleted_by_receiver=0) )
+ ORDER BY m.created_at DESC
+ LIMIT %d OFFSET %d
+ ) sub ORDER BY sub.created_at ASC",
+ $user_id, $partner_id, $partner_id, $user_id, $limit, $offset
+ ) );
+ }
+
+ public static function count_conversation( $user_id, $partner_id ) {
+ global $wpdb;
+ return (int) $wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_messages m
+ WHERE ( (m.from_id=%d AND m.to_id=%d AND m.deleted_by_sender=0)
+ OR (m.from_id=%d AND m.to_id=%d AND m.deleted_by_receiver=0) )",
+ $user_id, $partner_id, $partner_id, $user_id
+ ) );
+ }
+
+ public static function mark_messages_read( $user_id, $partner_id ) {
+ global $wpdb;
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_messages SET is_read=1
+ WHERE to_id=%d AND from_id=%d AND is_read=0",
+ $user_id, $partner_id
+ ) );
+ }
+
+ public static function count_unread_messages( $user_id ) {
+ global $wpdb;
+ $table = "{$wpdb->prefix}forum_messages";
+ if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return 0;
+ return (int)$wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->prefix}forum_messages WHERE to_id=%d AND is_read=0 AND deleted_by_receiver=0",
+ $user_id
+ ) );
+ }
+
+ // ── Remember-Me ───────────────────────────────────────────────────────────
+
+ public static function create_remember_token( $user_id ) {
+ global $wpdb;
+ $token = bin2hex( random_bytes(32) );
+ $expires = date('Y-m-d H:i:s', strtotime('+30 days'));
+ // Delete existing tokens for this user first
+ $wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", ['user_id' => $user_id] );
+ $wpdb->insert( "{$wpdb->prefix}forum_remember_tokens", [
+ 'user_id' => $user_id,
+ 'token' => $token,
+ 'expires_at' => $expires,
+ ] );
+ return $token;
+ }
+
+ public static function verify_remember_token( $token ) {
+ global $wpdb;
+ $table = "{$wpdb->prefix}forum_remember_tokens";
+ if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return null;
+ return $wpdb->get_row( $wpdb->prepare(
+ "SELECT user_id FROM {$wpdb->prefix}forum_remember_tokens
+ WHERE token=%s AND expires_at > NOW()",
+ sanitize_text_field($token)
+ ) );
+ }
+
+ public static function delete_remember_token( $user_id ) {
+ global $wpdb;
+ $wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", ['user_id' => (int)$user_id] );
+ }
+
+ // ── @Erwähnungen ──────────────────────────────────────────────────────────
+
+ /** Extrahiert @usernames und gibt User-Objekte zurück */
+ public static function extract_mentions( $content ) {
+ preg_match_all( '/@([a-zA-Z0-9_]{3,60})/', $content, $m );
+ $users = [];
+ foreach ( array_unique($m[1]) as $username ) {
+ $user = self::get_user_by('username', $username);
+ if ($user) $users[] = $user;
+ }
+ return $users;
+ }
+
+
+
+ // ── Passwort-Reset ────────────────────────────────────────────────────────
+
+ public static function create_reset_token( $user_id ) {
+ global $wpdb;
+ $token = bin2hex( random_bytes(32) );
+ $hash = hash( 'sha256', $token );
+ // Alte Tokens löschen
+ $wpdb->delete( "{$wpdb->prefix}forum_users", [] ); // nur placeholder
+ $wpdb->query( $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}forum_users
+ SET reset_token=%s, reset_token_expires=DATE_ADD(NOW(), INTERVAL 1 HOUR)
+ WHERE id=%d",
+ $hash, $user_id
+ ) );
+ return $token; // Klartext-Token → per E-Mail senden
+ }
+
+ public static function verify_reset_token( $token ) {
+ global $wpdb;
+ $hash = hash( 'sha256', $token );
+ return $wpdb->get_row( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}forum_users
+ WHERE reset_token=%s AND reset_token_expires > NOW()",
+ $hash
+ ) );
+ }
+
+ public static function use_reset_token( $token, $new_password ) {
+ global $wpdb;
+ $user = self::verify_reset_token( $token );
+ if ( ! $user ) return false;
+ $wpdb->update(
+ "{$wpdb->prefix}forum_users",
+ [
+ 'password' => password_hash( $new_password, PASSWORD_DEFAULT ),
+ 'reset_token' => null,
+ 'reset_token_expires' => null,
+ ],
+ ['id' => $user->id]
+ );
+ return true;
+ }
+
+
+}
\ No newline at end of file
diff --git a/includes/class-forum-levels.php b/includes/class-forum-levels.php
new file mode 100644
index 0000000..29b271d
--- /dev/null
+++ b/includes/class-forum-levels.php
@@ -0,0 +1,130 @@
+ 0, 'label' => 'Neuling', 'icon' => 'fas fa-seedling', 'color' => '#94a3b8' ],
+ [ 'min' => 10, 'label' => 'Schreiberling', 'icon' => 'fas fa-feather', 'color' => '#60a5fa' ],
+ [ 'min' => 50, 'label' => 'Erfahrener', 'icon' => 'fas fa-fire', 'color' => '#f97316' ],
+ [ 'min' => 150, 'label' => 'Veteran', 'icon' => 'fas fa-shield-halved', 'color' => '#a78bfa' ],
+ [ 'min' => 500, 'label' => 'Legende', 'icon' => 'fas fa-crown', 'color' => '#fbbf24' ],
+ ];
+ }
+
+ // ── Laden / Speichern ─────────────────────────────────────────
+
+ public static function get_all() {
+ $saved = get_option( self::OPTION_KEY, null );
+ if ( $saved === null ) {
+ $defaults = self::default_levels();
+ update_option( self::OPTION_KEY, $defaults );
+ return $defaults;
+ }
+ $levels = (array) $saved;
+ usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
+ return $levels;
+ }
+
+ public static function save( $levels ) {
+ usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
+ update_option( self::OPTION_KEY, $levels );
+ }
+
+ public static function reset_to_defaults() {
+ update_option( self::OPTION_KEY, self::default_levels() );
+ }
+
+ // ── Level für eine Beitragsanzahl ermitteln ───────────────────
+
+ public static function get_for_count( $post_count ) {
+ $levels = self::get_all();
+ // Von oben (höchster min) nach unten suchen
+ $sorted = array_reverse( $levels );
+ foreach ( $sorted as $level ) {
+ if ( (int) $post_count >= (int) $level['min'] ) {
+ return $level;
+ }
+ }
+ return $levels[0]; // Fallback: niedrigstes Level
+ }
+
+ // Nächstes Level (für Fortschrittsanzeige)
+ public static function get_next( $post_count ) {
+ $levels = self::get_all(); // bereits aufsteigend sortiert
+ foreach ( $levels as $level ) {
+ if ( (int) $level['min'] > (int) $post_count ) {
+ return $level;
+ }
+ }
+ return null; // Maxlevel erreicht
+ }
+
+ // Fortschritt in Prozent zum nächsten Level (0–100)
+ public static function progress( $post_count ) {
+ $current = self::get_for_count( $post_count );
+ $next = self::get_next( $post_count );
+ if ( ! $next ) return 100;
+ $range = (int) $next['min'] - (int) $current['min'];
+ if ( $range <= 0 ) return 100;
+ $done = (int) $post_count - (int) $current['min'];
+ return min( 100, (int) round( $done / $range * 100 ) );
+ }
+
+ // ── Badge HTML ────────────────────────────────────────────────
+
+ public static function badge( $post_count ) {
+ if ( ! self::is_enabled() ) return '';
+ $level = self::get_for_count( $post_count );
+ $label = esc_html( $level['label'] );
+ $icon = esc_attr( $level['icon'] );
+ $color = esc_attr( $level['color'] );
+ return ""
+ . " {$label}";
+ }
+
+ // ── Fortschrittsbalken HTML (für Profil-Sidebar) ──────────────
+
+ public static function progress_bar( $post_count ) {
+ if ( ! self::is_enabled() ) return '';
+ $current = self::get_for_count( $post_count );
+ $next = self::get_next( $post_count );
+ $pct = self::progress( $post_count );
+ $color = esc_attr( $current['color'] );
+ $cur_lbl = esc_html( $current['label'] );
+ $next_lbl = $next ? esc_html( $next['label'] ) : $cur_lbl;
+ $posts_to = $next ? ( (int)$next['min'] - (int)$post_count ) . ' Beiträge bis ' . $next_lbl : 'Maximales Level erreicht';
+
+ return "
+
+
+
description); ?>
Noch keine Threads. Starte die Diskussion!
Noch keine Beiträge.
+ content), 0, 130 ) ); + $more = mb_strlen( strip_tags($up->content) ) > 130 ? '…' : ''; + $is_thread = isset($up->entry_type) && $up->entry_type === 'thread'; + $anchor = $is_thread + ? '?forum_thread=' . (int)$up->thread_id + : '?forum_thread=' . (int)$up->thread_id . '#post-' . (int)$up->id; + ?> +Keine Threads mit diesem Tag.
Wähle eine Konversation aus oder starte eine neue Nachricht.
+ +Keine Ergebnisse.
Ergebnis(se) gefunden.
+ +Gib deine E-Mail ein — wir schicken dir einen Reset-Link.
+ + + + +Keine Mitglieder gefunden.
+