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, use_count INT DEFAULT 0, PRIMARY KEY (id), UNIQUE 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_categories", 'guest_visible', "ALTER TABLE {$wpdb->prefix}forum_categories ADD COLUMN guest_visible TINYINT(1) DEFAULT 1 AFTER min_role"); self::maybe_add_column("{$wpdb->prefix}forum_users", 'reset_token', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN reset_token VARCHAR(64) DEFAULT NULL"); self::maybe_add_column("{$wpdb->prefix}forum_users", 'reset_token_expires', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN reset_token_expires DATETIME DEFAULT NULL"); // Soft-Delete self::maybe_add_column("{$wpdb->prefix}forum_threads", 'deleted_at', "ALTER TABLE {$wpdb->prefix}forum_threads ADD COLUMN deleted_at DATETIME DEFAULT NULL"); self::maybe_add_column("{$wpdb->prefix}forum_posts", 'deleted_at', "ALTER TABLE {$wpdb->prefix}forum_posts ADD COLUMN deleted_at DATETIME DEFAULT NULL"); // Profil-Sichtbarkeit self::maybe_add_column("{$wpdb->prefix}forum_users", 'profile_public', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) DEFAULT 1"); // Zeitlich begrenzte Sperren self::maybe_add_column("{$wpdb->prefix}forum_users", 'ban_until', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN ban_until DATETIME DEFAULT NULL"); self::maybe_add_column("{$wpdb->prefix}forum_users", 'pre_ban_role', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN pre_ban_role VARCHAR(20) DEFAULT 'member'"); // Profilbanner self::maybe_add_column("{$wpdb->prefix}forum_users", 'banner_url', "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN banner_url VARCHAR(255) DEFAULT ''"); // Thread-Abonnements $sql_subscriptions = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_subscriptions ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, thread_id BIGINT UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY user_thread (user_id, thread_id), KEY thread_id (thread_id) ) $charset;"; dbDelta( $sql_subscriptions ); $sql_notifications = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_notifications ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, 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 ); // 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 ); // ── Ignore-Liste ────────────────────────────────────────────────────── $sql_ignore = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}forum_ignore_list ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, ignored_id BIGINT UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY user_ignored (user_id, ignored_id), KEY ignored_id (ignored_id) ) $charset;"; dbDelta( $sql_ignore ); // ── prefix_id zu threads ────────────────────────────────────────────── self::maybe_add_column( "{$wpdb->prefix}forum_threads", 'prefix_id', "ALTER TABLE {$wpdb->prefix}forum_threads ADD COLUMN prefix_id BIGINT UNSIGNED DEFAULT NULL" ); $count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_categories"); if ( (int)$count === 0 ) { $wpdb->insert("{$wpdb->prefix}forum_categories", ['parent_id'=>0,'name'=>'Allgemein', 'slug'=>'allgemein', 'description'=>'Allgemeine Diskussionen','icon'=>'fas fa-home', 'sort_order'=>1]); $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]); // --- Discord-Rollen-Sync nach Rollenänderung --- if (isset($data['role'])) { // Discord-User-ID holen $discord_user_id = $wpdb->get_var($wpdb->prepare( "SELECT meta_value FROM {$wpdb->prefix}forum_user_meta WHERE user_id = %d AND meta_key = 'discord_user_id'", $id )); if ($discord_user_id) { // Einstellungen laden $s = function_exists('wbf_get_settings') ? wbf_get_settings() : []; $token = trim($s['discord_bot_token'] ?? ''); $guild = trim($s['discord_guild_id'] ?? ''); $role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: []; if ($token && $guild && !empty($role_map)) { // Ziel-Discord-Rolle anhand Mapping finden $target_discord_role = null; foreach ($role_map as $dc_role_id => $forum_role) { if ($forum_role === $data['role']) { $target_discord_role = (string)$dc_role_id; break; } } if ($target_discord_role) { // Aktuelle Rollen des Users abrufen $res = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}/members/{$discord_user_id}", [ 'timeout' => 6, 'headers' => ['Authorization' => 'Bot ' . $token], ]); if (!is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200) { $member = json_decode(wp_remote_retrieve_body($res), true); $user_roles = $member['roles'] ?? []; // Alle gemappten Discord-Rollen entfernen, außer Zielrolle $remove_roles = []; foreach ($role_map as $dc_role_id => $forum_role) { if ((string)$dc_role_id !== $target_discord_role && in_array((string)$dc_role_id, $user_roles, true)) { $remove_roles[] = (string)$dc_role_id; } } // Zielrolle hinzufügen, falls nicht vorhanden if (!in_array($target_discord_role, $user_roles, true)) { $user_roles[] = $target_discord_role; } // Entfernte Rollen rausnehmen $user_roles = array_values(array_diff($user_roles, $remove_roles)); // PATCH an Discord senden $body = json_encode(['roles' => array_values($user_roles)]); wp_remote_request("https://discord.com/api/v10/guilds/{$guild}/members/{$discord_user_id}", [ 'method' => 'PATCH', 'timeout' => 6, 'headers' => [ 'Authorization' => 'Bot ' . $token, 'Content-Type' => 'application/json', ], 'body' => $body, ]); } } } } } } 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, p.label as prefix_label, p.color as prefix_color, p.bg_color as prefix_bg FROM {$wpdb->prefix}forum_threads t JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id WHERE t.category_id = %d AND t.deleted_at IS NULL $status_sql ORDER BY t.pinned DESC, t.last_reply_at DESC LIMIT %d OFFSET %d", $category_id, $per_page, $offset )); } 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' AND deleted_at IS NULL", $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 AND deleted_at IS NULL", $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, p.label as prefix_label, p.color as prefix_color, p.bg_color as prefix_bg FROM {$wpdb->prefix}forum_threads t JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id WHERE t.id = %d AND t.deleted_at IS NULL", $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 AND p.deleted_at IS NULL 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 AND deleted_at IS NULL", $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 WHERE t.status != 'archived' AND t.deleted_at IS NULL 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' AND deleted_at IS NULL"), 'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL"), '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, $user = null ) { global $wpdb; $like = '%' . $wpdb->esc_like( $query ) . '%'; // Kategorie-Sichtbarkeit: Gäste und Member dürfen keine privaten Kategorien sehen $user_level = $user ? WBF_Roles::level( $user->role ) : -99; if ( $user_level >= 50 ) { // Moderatoren+ sehen alles (inkl. soft-deleted ist extra) $cat_filter = ''; } elseif ( $user ) { // Eingeloggte Member/VIP: nur guest_visible oder eigene Rolle reicht $cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role IN ('member','vip'))"; } else { // Gäste: nur komplett öffentliche Kategorien $cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role = 'member')"; } 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' AND t.deleted_at IS NULL $cat_filter 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 p.deleted_at IS NULL AND t.status != 'archived' AND t.deleted_at IS NULL $cat_filter 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 ──────────────────────────────────────────────────────────── /** Erlaubte Reaktionen aus den Einstellungen holen */ public static function get_allowed_reactions() { $saved = get_option('wbf_reactions', null); if ( $saved !== null && is_array($saved) && count($saved) > 0 ) return $saved; return ['👍','❤️','😂','😮','😢','😡']; // Defaults } public static function set_reaction( $user_id, $object_id, $object_type, $reaction ) { global $wpdb; $allowed = self::get_allowed_reactions(); 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 ); // Altes Token dieses Users zurücksetzen bevor ein neues gesetzt wird $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}forum_users SET reset_token=NULL, reset_token_expires=NULL WHERE id=%d", (int) $user_id ) ); $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, (int) $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; } // ── 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_profile_field_categories() { $cats = get_option( 'wbf_profile_field_cats', null ); if ( $cats === null ) { // Default-Kategorien beim ersten Aufruf $defaults = [ [ 'id' => 'cat_allgemein', 'name' => 'Allgemein', 'icon' => '👤' ], [ 'id' => 'cat_kontakt', 'name' => 'Kontakt', 'icon' => '✉️' ], [ 'id' => 'cat_social', 'name' => 'Social Media', 'icon' => '🌐' ], ]; update_option( 'wbf_profile_field_cats', $defaults ); return $defaults; } return is_array( $cats ) ? $cats : []; } public static function save_profile_field_categories( $cats ) { update_option( 'wbf_profile_field_cats', $cats ); } 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 )); } // ── Ignore-Liste ────────────────────────────────────────────────────────── public static function toggle_ignore( $user_id, $ignored_id ) { global $wpdb; $user_id = (int) $user_id; $ignored_id = (int) $ignored_id; if ( self::is_ignored( $user_id, $ignored_id ) ) { $wpdb->delete( "{$wpdb->prefix}forum_ignore_list", [ 'user_id' => $user_id, 'ignored_id' => $ignored_id, ] ); return false; } $wpdb->replace( "{$wpdb->prefix}forum_ignore_list", [ 'user_id' => $user_id, 'ignored_id' => $ignored_id, ] ); return true; } public static function is_ignored( $user_id, $ignored_id ) { global $wpdb; $table = "{$wpdb->prefix}forum_ignore_list"; if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return false; return (bool) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d AND ignored_id=%d", (int) $user_id, (int) $ignored_id ) ); } /** Gibt alle ignorierten User-IDs als int-Array zurück */ public static function get_ignored_ids( $user_id ) { global $wpdb; $table = "{$wpdb->prefix}forum_ignore_list"; if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return []; $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ignored_id FROM {$wpdb->prefix}forum_ignore_list WHERE user_id=%d", (int) $user_id ) ); return array_map( 'intval', $ids ); } /** Vollständige Ignore-Liste mit User-Daten */ public static function get_ignore_list( $user_id ) { global $wpdb; $table = "{$wpdb->prefix}forum_ignore_list"; if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) !== $table ) return []; return $wpdb->get_results( $wpdb->prepare( "SELECT u.id, u.username, u.display_name, u.avatar_url, u.role, il.created_at AS ignored_since FROM {$wpdb->prefix}forum_ignore_list il JOIN {$wpdb->prefix}forum_users u ON u.id = il.ignored_id WHERE il.user_id = %d ORDER BY il.created_at DESC", (int) $user_id ) ); } // ── DSGVO Art. 17: Konto vollständig löschen ────────────────────────────── public static function delete_user_gdpr( $user_id ) { global $wpdb; $user_id = (int) $user_id; $user = self::get_user( $user_id ); if ( ! $user ) return false; if ( $user->role === 'superadmin' ) return false; $wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'from_id' => $user_id ] ); $wpdb->delete( "{$wpdb->prefix}forum_messages", [ 'to_id' => $user_id ] ); $wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", [ 'user_id' => $user_id ] ); $wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'user_id' => $user_id ] ); $wpdb->delete( "{$wpdb->prefix}forum_notifications", [ 'actor_id' => $user_id ] ); $wpdb->delete( "{$wpdb->prefix}forum_subscriptions", [ 'user_id' => $user_id ] ); $table_bm = "{$wpdb->prefix}forum_bookmarks"; if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_bm'" ) === $table_bm ) { $wpdb->delete( $table_bm, [ 'user_id' => $user_id ] ); } $wpdb->delete( "{$wpdb->prefix}forum_likes", [ 'user_id' => $user_id ] ); $wpdb->delete( "{$wpdb->prefix}forum_reactions", [ 'user_id' => $user_id ] ); $wpdb->delete( "{$wpdb->prefix}forum_reports", [ 'reporter_id' => $user_id ] ); $table_pv = "{$wpdb->prefix}forum_poll_votes"; if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_pv'" ) === $table_pv ) { $wpdb->delete( $table_pv, [ 'user_id' => $user_id ] ); } // Ignore-Liste beidseitig bereinigen $table_il = "{$wpdb->prefix}forum_ignore_list"; if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_il'" ) === $table_il ) { $wpdb->delete( $table_il, [ 'user_id' => $user_id ] ); $wpdb->delete( $table_il, [ 'ignored_id' => $user_id ] ); } delete_transient( 'wbf_flood_' . $user_id ); delete_transient( 'wbf_flood_ts_' . $user_id ); self::delete_user_meta_all( $user_id ); $anon_hash = substr( hash( 'sha256', $user_id . wp_salt() . microtime( true ) ), 0, 12 ); $wpdb->update( "{$wpdb->prefix}forum_users", [ 'username' => 'deleted_' . $anon_hash, 'email' => 'deleted_' . $anon_hash . '@deleted.invalid', 'password' => '', 'display_name' => 'Gelöschter Nutzer', 'avatar_url' => '', 'bio' => '', 'signature' => '', 'ban_reason' => '', 'reset_token' => null, 'reset_token_expires' => null, 'pre_ban_role' => '', 'ban_until' => null, 'role' => 'banned', ], [ 'id' => $user_id ] ); return true; } // ── 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 ) { $user_id = (int) $user_id; if ( $user_id <= 0 ) return true; // kein eingeloggter User — kein Flood-Check $interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 ); if ( $interval <= 0 ) return true; // deaktiviert $key = 'wbf_flood_' . (int)$user_id; $ts_key = 'wbf_flood_ts_' . (int)$user_id; $last = get_transient( $key ); if ( $last !== false ) { return false; // noch gesperrt } set_transient( $key, 1, $interval ); set_transient( $ts_key, time(), $interval + 5 ); return true; } public static function flood_remaining( $user_id ) { $interval = (int)( wbf_get_settings()['flood_interval'] ?? 0 ); if ( $interval <= 0 ) return 0; $ts_key = 'wbf_flood_ts_' . (int)$user_id; $sent = get_transient( $ts_key ); if ( $sent === false ) return 0; $remaining = $interval - ( time() - (int)$sent ); return max( 0, $remaining ); } }