diff --git a/includes/class-forum-ajax.php b/includes/class-forum-ajax.php index ca4ba01..5eb1eca 100644 --- a/includes/class-forum-ajax.php +++ b/includes/class-forum-ajax.php @@ -18,6 +18,9 @@ class WBF_Ajax { 'wbf_create_poll', 'wbf_toggle_bookmark', 'wbf_set_thread_prefix', + 'wbf_toggle_ignore', + 'wbf_change_email', + 'wbf_save_notification_prefs', ]; foreach ($actions as $action) { add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]); @@ -411,9 +414,37 @@ class WBF_Ajax { if (empty($_FILES['avatar'])) wp_send_json_error(['message'=>'Keine Datei.']); $allowed_types = ['image/jpeg','image/png','image/gif','image/webp']; - $mime = $_FILES['avatar']['type'] ?? ''; - if (!in_array($mime, $allowed_types)) wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']); - if ($_FILES['avatar']['size'] > 2 * 1024 * 1024) wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']); + + // Dateigröße vor dem MIME-Check prüfen + if ( $_FILES['avatar']['size'] > 2 * 1024 * 1024 ) { + wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']); + } + + // Server-seitige MIME-Typ-Prüfung — $_FILES['type'] kommt vom Client + // und ist beliebig fälschbar (z.B. PHP-Datei als image/jpeg getarnt). + // finfo_file() liest den echten Magic-Byte der temporären Datei. + $tmp = $_FILES['avatar']['tmp_name'] ?? ''; + if ( ! $tmp || ! is_uploaded_file( $tmp ) ) { + wp_send_json_error(['message'=>'Ungültiger Datei-Upload.']); + } + if ( function_exists('finfo_open') ) { + $finfo = finfo_open( FILEINFO_MIME_TYPE ); + $real_mime = finfo_file( $finfo, $tmp ); + finfo_close( $finfo ); + } else { + // Fallback: exif_imagetype() wenn finfo nicht verfügbar + $et_map = [ + IMAGETYPE_JPEG => 'image/jpeg', + IMAGETYPE_PNG => 'image/png', + IMAGETYPE_GIF => 'image/gif', + IMAGETYPE_WEBP => 'image/webp', + ]; + $et = @exif_imagetype( $tmp ); + $real_mime = $et_map[$et] ?? ''; + } + if ( ! in_array( $real_mime, $allowed_types, true ) ) { + wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']); + } require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; @@ -468,16 +499,36 @@ class WBF_Ajax { // Nur Bilder erlauben $allowed_types = ['image/jpeg','image/png','image/gif','image/webp']; - $mime = $_FILES['image']['type'] ?? ''; - if ( ! in_array($mime, $allowed_types) ) { - wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']); - } - // Max 5 MB + // Max 5 MB — Größe zuerst prüfen bevor teure MIME-Erkennung läuft if ( $_FILES['image']['size'] > 5 * 1024 * 1024 ) { wp_send_json_error(['message' => 'Maximale Dateigröße: 5 MB.']); } + // Server-seitige MIME-Typ-Prüfung — $_FILES['type'] ist client-kontrolliert + // und kann beliebig auf 'image/jpeg' gesetzt werden, auch für .php-Dateien. + $tmp = $_FILES['image']['tmp_name'] ?? ''; + if ( ! $tmp || ! is_uploaded_file( $tmp ) ) { + wp_send_json_error(['message' => 'Ungültiger Datei-Upload.']); + } + if ( function_exists('finfo_open') ) { + $finfo = finfo_open( FILEINFO_MIME_TYPE ); + $real_mime = finfo_file( $finfo, $tmp ); + finfo_close( $finfo ); + } else { + $et_map = [ + IMAGETYPE_JPEG => 'image/jpeg', + IMAGETYPE_PNG => 'image/png', + IMAGETYPE_GIF => 'image/gif', + IMAGETYPE_WEBP => 'image/webp', + ]; + $et = @exif_imagetype( $tmp ); + $real_mime = $et_map[$et] ?? ''; + } + if ( ! in_array( $real_mime, $allowed_types, true ) ) { + wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']); + } + require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; @@ -588,6 +639,18 @@ class WBF_Ajax { $is_mod = WBF_DB::can($user, 'delete_post'); if ( ! $is_own && ! $is_mod ) wp_send_json_error(['message' => 'Keine Berechtigung.']); + // Post-Bearbeitungslimit prüfen — gilt auch für Thread-Erstbeiträge + // (spiegelt identisches Verhalten zu handle_edit_post() wider) + if ( $is_own && ! $is_mod ) { + $limit_min = (int)( wbf_get_settings()['post_edit_limit'] ?? 30 ); + if ( $limit_min > 0 ) { + $age_min = ( time() - strtotime( $thread->created_at ) ) / 60; + if ( $age_min > $limit_min ) { + wp_send_json_error(['message' => "Bearbeitung nur innerhalb von {$limit_min} Minuten nach dem Erstellen möglich."]); + } + } + } + global $wpdb; $wpdb->update( "{$wpdb->prefix}forum_threads", @@ -682,6 +745,11 @@ class WBF_Ajax { if ($to_id === (int)$user->id) wp_send_json_error(['message'=>'Du kannst dir nicht selbst schreiben.']); if (!WBF_DB::get_user($to_id)) wp_send_json_error(['message'=>'Empfänger nicht gefunden.']); + // DM-Blockierung: Empfänger hat Sender ignoriert + if ( WBF_DB::is_ignored( $to_id, $user->id ) ) { + wp_send_json_error(['message' => 'Diese Person akzeptiert keine Nachrichten von dir.']); + } + $id = WBF_DB::send_message($user->id, $to_id, $content); // Notify recipient WBF_DB::create_notification($to_id, 'message', $id, $user->id); @@ -739,14 +807,22 @@ class WBF_Ajax { // ── User-Autocomplete (für @Erwähnungen + DM) ───────────────────────────── public static function handle_user_suggest() { + // Nur eingeloggte Nutzer dürfen die User-Suche nutzen + // (verhindert Enumeration aller Usernamen + Rollendaten durch Gäste) + if ( ! WBF_Auth::is_forum_logged_in() ) { + wp_send_json_success(['users'=>[]]); + } $q = sanitize_text_field($_POST['q'] ?? $_GET['q'] ?? ''); if (mb_strlen($q) < 1) wp_send_json_success(['users'=>[]]); global $wpdb; $like = $wpdb->esc_like($q) . '%'; + // Rolle wird bewusst NICHT zurückgegeben — nicht für Autocomplete nötig + // und verhindert Informationsleck über Rollen-Verteilung im Forum. $users = $wpdb->get_results($wpdb->prepare( - "SELECT id, username, display_name, avatar_url, role + "SELECT id, username, display_name, avatar_url FROM {$wpdb->prefix}forum_users - WHERE username LIKE %s OR display_name LIKE %s + WHERE (username LIKE %s OR display_name LIKE %s) + AND role != 'banned' ORDER BY display_name ASC LIMIT 8", $like, $like )); @@ -817,6 +893,13 @@ class WBF_Ajax { private static function send_notification_email( $to_user, $type, $actor_name, $extra = [] ) { if ( ! $to_user || empty($to_user->email) ) return; + // Prüfen ob der User diesen Benachrichtigungstyp aktiviert hat + // Standard: alle aktiviert (1). User kann im Profil deaktivieren (0). + $pref_key = 'notify_' . $type; // notify_reply, notify_mention, notify_message + $meta = WBF_DB::get_user_meta( $to_user->id ); + // Nur deaktivieren wenn explizit auf '0' gesetzt — Standard ist aktiviert + if ( isset($meta[$pref_key]) && $meta[$pref_key] === '0' ) return; + $blog_name = get_bloginfo('name'); $forum_url = wbf_get_forum_url(); $from_email = get_option('admin_email'); @@ -897,6 +980,17 @@ class WBF_Ajax { $email = sanitize_email( $_POST['email'] ?? '' ); if ( ! is_email($email) ) wp_send_json_error(['message'=>'Ungültige E-Mail-Adresse.']); + // ── Rate-Limiting: max. 1 Reset-Mail pro E-Mail-Adresse alle 15 Minuten ── + // Verhindert, dass ein Angreifer tausende Reset-Mails pro Sekunde + // für beliebige Adressen triggert und den Mail-Server überlastet. + $rate_key = 'wbf_pwreset_' . md5( strtolower( $email ) ); + if ( get_transient( $rate_key ) !== false ) { + // Immer Erfolg melden — kein Leak ob Rate-Limit oder kein Account + wp_send_json_success(['message'=>'Falls diese E-Mail registriert ist, wurde eine E-Mail gesendet.']); + } + // Cooldown setzen — 15 Minuten + set_transient( $rate_key, 1, 15 * MINUTE_IN_SECONDS ); + $user = WBF_DB::get_user_by('email', $email); // Immer Erfolg melden (kein User-Enumeration) if ( ! $user ) { @@ -922,7 +1016,13 @@ class WBF_Ajax { } public static function handle_reset_password() { - self::verify(); + // Kein self::verify() hier — Gäste haben keine Forum-Session. + // Das Reset-Token selbst authentifiziert die Anfrage. + // Wir prüfen trotzdem den WP-Nonce als CSRF-Schutz; dieser wird + // von wp_localize_script für alle Besucher (auch Gäste) generiert. + if ( ! check_ajax_referer( 'wbf_nonce', 'nonce', false ) ) { + wp_send_json_error(['message' => 'Sicherheitsfehler.']); + } $token = sanitize_text_field( $_POST['token'] ?? '' ); $password = $_POST['password'] ?? ''; $password2= $_POST['password2'] ?? ''; @@ -1207,6 +1307,90 @@ class WBF_Ajax { wp_send_json_success(['prefix' => $prefix]); } + // ── E-Mail-Adresse ändern ───────────────────────────────────────────────── + + public static function handle_change_email() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + + $new_email = sanitize_email( $_POST['new_email'] ?? '' ); + $password = $_POST['password'] ?? ''; + + if ( ! is_email($new_email) ) { + wp_send_json_error(['message' => 'Ungültige E-Mail-Adresse.']); + } + if ( empty($password) ) { + wp_send_json_error(['message' => 'Bitte aktuelles Passwort zur Bestätigung eingeben.']); + } + if ( ! password_verify($password, $user->password) ) { + wp_send_json_error(['message' => 'Falsches Passwort.']); + } + if ( strtolower($new_email) === strtolower($user->email) ) { + wp_send_json_error(['message' => 'Das ist bereits deine aktuelle E-Mail-Adresse.']); + } + + // Prüfen ob E-Mail bereits vergeben + $existing = WBF_DB::get_user_by('email', $new_email); + if ( $existing && (int)$existing->id !== (int)$user->id ) { + wp_send_json_error(['message' => 'Diese E-Mail-Adresse ist bereits registriert.']); + } + + WBF_DB::update_user($user->id, ['email' => $new_email]); + wp_send_json_success(['message' => 'E-Mail-Adresse erfolgreich geändert.']); + } + + // ── Benachrichtigungs-Einstellungen speichern ───────────────────────────── + + public static function handle_save_notification_prefs() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']); + + $allowed = ['notify_reply', 'notify_mention', 'notify_message']; + foreach ( $allowed as $key ) { + // 1 wenn Checkbox aktiviert, 0 wenn deaktiviert + $val = isset($_POST[$key]) && $_POST[$key] === '1' ? '1' : '0'; + WBF_DB::set_user_meta($user->id, $key, $val); + } + + wp_send_json_success(['message' => 'Benachrichtigungs-Einstellungen gespeichert.']); + } + + // ── User ignorieren / Ignorierung aufheben ──────────────────────────────── + + public static function handle_toggle_ignore() { + self::verify(); + $user = WBF_Auth::get_current_user(); + if ( ! $user ) wp_send_json_error( ['message' => 'Nicht eingeloggt.'] ); + + $ignored_id = (int)( $_POST['ignored_id'] ?? 0 ); + if ( ! $ignored_id ) wp_send_json_error( ['message' => 'Ungültiger Nutzer.'] ); + if ( $ignored_id === (int)$user->id ) { + wp_send_json_error( ['message' => 'Du kannst dich nicht selbst ignorieren.'] ); + } + + $target = WBF_DB::get_user( $ignored_id ); + if ( ! $target ) wp_send_json_error( ['message' => 'Nutzer nicht gefunden.'] ); + + // Prüfen ob diese Rolle geblockt werden darf (konfigurierbar in den Einstellungen) + if ( ! wbf_can_be_ignored( $target ) ) { + $role_label = WBF_Roles::get($target->role)['label'] ?? $target->role; + wp_send_json_error( ['message' => 'Nutzer mit der Rolle "' . $role_label . '" können nicht ignoriert werden.'] ); + } + + $now_ignored = WBF_DB::toggle_ignore( $user->id, $ignored_id ); + + wp_send_json_success( [ + 'ignored' => $now_ignored, + 'ignored_id' => $ignored_id, + 'display_name' => $target->display_name, + 'message' => $now_ignored + ? esc_html( $target->display_name ) . ' wird jetzt ignoriert.' + : 'Ignorierung von ' . esc_html( $target->display_name ) . ' aufgehoben.', + ] ); + } + } add_action( 'init', [ 'WBF_Ajax', 'init' ] ); \ No newline at end of file diff --git a/includes/class-forum-db.php b/includes/class-forum-db.php index 38c0f29..bae5ec3 100644 --- a/includes/class-forum-db.php +++ b/includes/class-forum-db.php @@ -279,6 +279,18 @@ class WBF_DB { ) $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" ); @@ -623,7 +635,8 @@ class WBF_DB { 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 + WHERE t.status != 'archived' AND t.deleted_at IS NULL + ORDER BY t.created_at DESC LIMIT %d", $limit )); } @@ -1207,13 +1220,18 @@ class WBF_DB { global $wpdb; $token = bin2hex( random_bytes(32) ); $hash = hash( 'sha256', $token ); - // Alte Tokens löschen - $wpdb->delete( "{$wpdb->prefix}forum_users", [] ); // nur placeholder + // 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, $user_id + $hash, (int) $user_id ) ); return $token; // Klartext-Token → per E-Mail senden } @@ -1689,6 +1707,128 @@ class WBF_DB { )); } + // ── 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() { @@ -1711,25 +1851,29 @@ class WBF_DB { // ── 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; + $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, time(), $interval ); + 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; - $key = 'wbf_flood_' . (int)$user_id; - $last = get_transient( $key ); - if ( $last === false ) return 0; - // Transients speichern keine genaue Restzeit — wir schätzen über $interval - return $interval; + $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 ); } } \ No newline at end of file diff --git a/includes/class-forum-export.php b/includes/class-forum-export.php new file mode 100644 index 0000000..37c10ac --- /dev/null +++ b/includes/class-forum-export.php @@ -0,0 +1,1202 @@ + 'wbf-export', + 'wbf_err' => 'no_sections', + ], admin_url( 'admin.php' ) ) ); + exit; + } + + $data = self::build_export( $sections ); + $json = wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); + + if ( $json === false ) { + wp_safe_redirect( add_query_arg( [ + 'page' => 'wbf-export', + 'wbf_err' => 'json_fail', + ], admin_url( 'admin.php' ) ) ); + exit; + } + + $filename = 'wbf-backup-' . date( 'Y-m-d-His' ) . '.json'; + + nocache_headers(); + header( 'Content-Type: application/json; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Content-Length: ' . strlen( $json ) ); + echo $json; + exit; + } + + /** + * Baut das Export-Array auf. + * + * @param string[] $sections Liste der zu exportierenden Sektions-Keys + * @return array + */ + public static function build_export( array $sections ): array { + global $wpdb; + + $data = [ + '_meta' => [ + 'plugin' => 'WP Business Forum', + 'version' => WBF_VERSION, + 'exported' => gmdate( 'c' ), + 'site' => get_bloginfo( 'url' ), + 'sections' => $sections, + ], + ]; + + foreach ( $sections as $sec ) { + switch ( $sec ) { + + case 'settings': + $data['settings'] = get_option( 'wbf_settings', [] ); + $data['profile_fields'] = get_option( 'wbf_profile_fields', [] ); + $data['reactions_cfg'] = get_option( 'wbf_reactions', [] ); + $data['word_filter'] = get_option( 'wbf_word_filter', '' ); + break; + + case 'roles': + // Superadmin bewusst mitexportieren (aber beim Import nie anwenden) + $data['roles'] = get_option( 'wbf_custom_roles', [] ); + break; + + case 'levels': + $data['levels'] = [ + 'config' => get_option( 'wbf_level_config', [] ), + 'enabled' => get_option( 'wbf_levels_enabled', true ), + ]; + break; + + case 'categories': + $data['categories'] = $wpdb->get_results( + "SELECT * FROM {$wpdb->prefix}forum_categories + ORDER BY parent_id ASC, sort_order ASC", + ARRAY_A + ) ?: []; + break; + + case 'users': + $data['users'] = $wpdb->get_results( + "SELECT id, username, email, password, display_name, avatar_url, + bio, signature, role, pre_ban_role, ban_reason, ban_until, + post_count, registered, last_active, profile_public, + reset_token, reset_token_expires + FROM {$wpdb->prefix}forum_users + ORDER BY id ASC", + ARRAY_A + ) ?: []; + + // Passwort-Hashes & Reset-Tokens aus der Ausgabe filtern + // wenn der Admin NUR Profildaten möchte — entscheidet er selbst + $data['user_meta'] = self::safe_get( + "SELECT * FROM {$wpdb->prefix}forum_user_meta ORDER BY user_id ASC" + ); + break; + + case 'threads': + $data['threads'] = $wpdb->get_results( + "SELECT * FROM {$wpdb->prefix}forum_threads ORDER BY id ASC", + ARRAY_A + ) ?: []; + $data['posts'] = $wpdb->get_results( + "SELECT * FROM {$wpdb->prefix}forum_posts ORDER BY id ASC", + ARRAY_A + ) ?: []; + $data['thread_tags'] = $wpdb->get_results( + "SELECT tt.thread_id, tt.tag_id, t.name, t.slug, t.use_count + FROM {$wpdb->prefix}forum_thread_tags tt + JOIN {$wpdb->prefix}forum_tags t ON t.id = tt.tag_id + ORDER BY tt.thread_id ASC", + ARRAY_A + ) ?: []; + $data['subscriptions'] = self::safe_get( + "SELECT * FROM {$wpdb->prefix}forum_subscriptions ORDER BY id ASC" + ); + break; + + case 'polls': + $data['polls'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_polls ORDER BY id ASC" ); + $data['poll_votes'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_poll_votes ORDER BY id ASC" ); + break; + + case 'bookmarks': + $data['bookmarks'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_bookmarks ORDER BY id ASC" ); + break; + + case 'prefixes': + $data['prefixes'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_prefixes ORDER BY sort_order ASC" ); + break; + + case 'interactions': + $data['likes'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_likes ORDER BY id ASC" ); + $data['reactions'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_reactions ORDER BY id ASC" ); + $data['notifications'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_notifications ORDER BY id ASC" ); + break; + + case 'messages': + $data['messages'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_messages ORDER BY id ASC" ); + break; + + case 'reports': + $data['reports'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_reports ORDER BY id ASC" ); + break; + + case 'invites': + $data['invites'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_invites ORDER BY id ASC" ); + break; + + case 'ignore_list': + $data['ignore_list'] = self::safe_get( "SELECT * FROM {$wpdb->prefix}forum_ignore_list ORDER BY id ASC" ); + break; + } + } + + return $data; + } + + // ═══════════════════════════════════════════════════════════════ + // IMPORT — Verarbeitung des hochgeladenen JSON + // ═══════════════════════════════════════════════════════════════ + + /** + * Importiert ein WBF-Backup. + * + * @param array $post $_POST-Array (Optionen) + * @param array $files $_FILES-Array + * @return array [ 'type' => 'success|error|warning', 'message' => string, 'log' => string[] ] + */ + public static function handle_import( array $post, array $files ): array { + // ── Datei-Validierung ───────────────────────────────────── + $tmp = $files['import_file']['tmp_name'] ?? ''; + if ( empty( $tmp ) || ! is_uploaded_file( $tmp ) ) { + return self::result( 'error', 'Keine Datei hochgeladen.' ); + } + + $size = filesize( $tmp ); + if ( $size === false || $size > self::MAX_UPLOAD_BYTES ) { + return self::result( 'error', sprintf( + 'Datei zu groß (%s). Maximum: %s.', + size_format( (int) $size ), + size_format( self::MAX_UPLOAD_BYTES ) + ) ); + } + + $ext = strtolower( pathinfo( $files['import_file']['name'] ?? '', PATHINFO_EXTENSION ) ); + if ( $ext !== 'json' ) { + return self::result( 'error', 'Nur .json-Dateien werden akzeptiert.' ); + } + + $raw = file_get_contents( $tmp ); + $data = json_decode( $raw, true ); + + if ( ! is_array( $data ) || ! isset( $data['_meta']['plugin'] ) ) { + return self::result( 'error', 'Ungültige Datei — kein gültiges WBF-Backup.' ); + } + + if ( $data['_meta']['plugin'] !== 'WP Business Forum' ) { + return self::result( 'error', 'Diese Datei stammt nicht von WP Business Forum.' ); + } + + // Versions-Hinweis (kein harter Fehler — Abwärtskompatibilität) + $backup_ver = $data['_meta']['version'] ?? '0'; + $ver_warning = ''; + if ( version_compare( $backup_ver, WBF_VERSION, '>' ) ) { + $ver_warning = "ℹ️ Backup-Version ({$backup_ver}) ist neuer als installierte Version (" . WBF_VERSION . "). Einige Felder werden möglicherweise nicht importiert."; + } + + // ── Import durchführen ──────────────────────────────────── + global $wpdb; + $log = []; + $id_map = []; // old_user_id => new_user_id (wird von mehreren Sektionen genutzt) + $cat_map = []; // old_cat_id => new_cat_id (für zukünftige Kategorie-Remaps) + $has_error = false; + + // Suppress individual query errors — wir werten $wpdb->last_error selbst aus + $suppress = $wpdb->suppress_errors( true ); + + try { + + // ── 1. Einstellungen & Konfiguration ───────────────── + if ( isset( $data['settings'] ) ) { + update_option( 'wbf_settings', $data['settings'] ); + $log[] = '✅ Einstellungen importiert.'; + } + if ( isset( $data['profile_fields'] ) ) { + update_option( 'wbf_profile_fields', $data['profile_fields'] ); + $log[] = '✅ Profilfeld-Definitionen (' . count( $data['profile_fields'] ) . ') importiert.'; + } + if ( isset( $data['reactions_cfg'] ) && is_array( $data['reactions_cfg'] ) ) { + update_option( 'wbf_reactions', $data['reactions_cfg'] ); + $log[] = '✅ Reaktionen-Konfiguration importiert.'; + } + if ( isset( $data['word_filter'] ) ) { + update_option( 'wbf_word_filter', sanitize_textarea_field( $data['word_filter'] ) ); + $log[] = '✅ Wortfilter importiert.'; + } + + // ── 2. Rollen ───────────────────────────────────────── + if ( isset( $data['roles'] ) && is_array( $data['roles'] ) ) { + $roles = $data['roles']; + unset( $roles['superadmin'] ); // Superadmin-Rolle NIEMALS überschreiben + $current = get_option( 'wbf_custom_roles', [] ); + $current['superadmin'] = WBF_Roles::get( 'superadmin' ); + update_option( 'wbf_custom_roles', array_merge( $current, $roles ) ); + $log[] = '✅ Rollen (' . count( $roles ) . ') importiert.'; + } + + // ── 3. Level-System ─────────────────────────────────── + if ( isset( $data['levels'] ) && is_array( $data['levels'] ) ) { + if ( array_key_exists( 'config', $data['levels'] ) ) { + update_option( 'wbf_level_config', $data['levels']['config'] ); + } + if ( array_key_exists( 'enabled', $data['levels'] ) ) { + update_option( 'wbf_levels_enabled', (bool) $data['levels']['enabled'] ); + } + $log[] = '✅ Level-System importiert.'; + } + + // ── 4. Kategorien ───────────────────────────────────── + if ( ! empty( $data['categories'] ) ) { + $force = ! empty( $post['import_force_cats'] ); + $existing = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}forum_categories" ); + + if ( $existing > 0 && ! $force ) { + $log[] = '⏭️ Kategorien übersprungen (bereits vorhanden — „Überschreiben" aktivieren).'; + } else { + if ( $force ) { + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}forum_categories" ); + } + $count = 0; + foreach ( $data['categories'] as $cat ) { + // Original-ID beibehalten — Threads referenzieren diese IDs direkt + $wpdb->query( $wpdb->prepare( + "INSERT INTO {$wpdb->prefix}forum_categories + (id, parent_id, name, slug, description, icon, sort_order, + thread_count, post_count, min_role, guest_visible) + VALUES (%d,%d,%s,%s,%s,%s,%d,%d,%d,%s,%d) + ON DUPLICATE KEY UPDATE + parent_id=%d, name=%s, slug=%s, description=%s, icon=%s, + sort_order=%d, thread_count=%d, post_count=%d, + min_role=%s, guest_visible=%d", + // INSERT-Werte + (int) ($cat['id'] ?? 0), + (int) ($cat['parent_id'] ?? 0), + $cat['name'] ?? '', + $cat['slug'] ?? '', + $cat['description'] ?? '', + $cat['icon'] ?? 'fas fa-comments', + (int) ($cat['sort_order'] ?? 0), + (int) ($cat['thread_count'] ?? 0), + (int) ($cat['post_count'] ?? 0), + $cat['min_role'] ?? 'member', + (int) ($cat['guest_visible']?? 1), + // ON DUPLICATE KEY UPDATE-Werte + (int) ($cat['parent_id'] ?? 0), + $cat['name'] ?? '', + $cat['slug'] ?? '', + $cat['description'] ?? '', + $cat['icon'] ?? 'fas fa-comments', + (int) ($cat['sort_order'] ?? 0), + (int) ($cat['thread_count'] ?? 0), + (int) ($cat['post_count'] ?? 0), + $cat['min_role'] ?? 'member', + (int) ($cat['guest_visible']?? 1), + ) ); + $cat_map[ (int)($cat['id'] ?? 0) ] = (int)($cat['id'] ?? 0); + $count++; + } + $log[] = "✅ Kategorien ($count) importiert."; + } + } + + // ── 5. Benutzer + User-Meta ─────────────────────────── + if ( ! empty( $data['users'] ) ) { + $force = ! empty( $post['import_force_users'] ); + $count_new = 0; + $count_upd = 0; + + foreach ( $data['users'] as $u ) { + $old_id = (int) $u['id']; + + // Superadmin-Schutz: importierte Benutzer dürfen nie Superadmin werden + if ( isset( $u['role'] ) && $u['role'] === 'superadmin' ) { + $u['role'] = 'member'; + } + // Sicherheitsfelder bereinigen + unset( $u['reset_token'], $u['reset_token_expires'] ); + + $exists = $wpdb->get_var( $wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}forum_users WHERE username=%s OR email=%s", + $u['username'] ?? '', $u['email'] ?? '' + ) ); + + if ( $exists ) { + $new_id = (int) $exists; + $id_map[$old_id] = $new_id; + if ( $force ) { + $update = $u; + unset( $update['id'] ); + $wpdb->update( "{$wpdb->prefix}forum_users", $update, [ 'id' => $new_id ] ); + $count_upd++; + } + continue; + } + + // Neuer Benutzer → INSERT + $insert = $u; + unset( $insert['id'] ); + $wpdb->insert( "{$wpdb->prefix}forum_users", $insert ); + $new_id = (int) $wpdb->insert_id; + $id_map[$old_id] = $new_id; + $count_new++; + } + + $log[] = "✅ Benutzer: $count_new neu erstellt, $count_upd aktualisiert."; + + // User-Meta mit gemappten IDs + if ( ! empty( $data['user_meta'] ) ) { + if ( $force && ! empty( $id_map ) ) { + $mapped_ids = implode( ',', array_map( 'intval', array_values( $id_map ) ) ); + $wpdb->query( "DELETE FROM {$wpdb->prefix}forum_user_meta WHERE user_id IN ($mapped_ids)" ); + } + $meta_count = 0; + foreach ( $data['user_meta'] as $row ) { + $new_uid = $id_map[ (int)( $row['user_id'] ?? 0 ) ] ?? null; + if ( ! $new_uid ) continue; + unset( $row['id'] ); + $row['user_id'] = $new_uid; + $wpdb->replace( "{$wpdb->prefix}forum_user_meta", $row ); + $meta_count++; + } + if ( $meta_count ) $log[] = "✅ Profilfeld-Werte ($meta_count) importiert."; + } + } + + // ── 6. Threads, Posts, Tags, Abonnements ───────────── + if ( ! empty( $data['threads'] ) ) { + $force = ! empty( $post['import_force_threads'] ); + + if ( $force ) { + // Nur die wirklich thread-abhängigen Tabellen leeren + $wpdb->query( 'SET FOREIGN_KEY_CHECKS=0' ); + foreach ( [ + 'forum_posts', + 'forum_threads', + 'forum_thread_tags', + 'forum_tags', + 'forum_subscriptions', + 'forum_likes', + 'forum_reactions', + 'forum_notifications', + ] as $_tbl ) { + if ( self::table_exists( $wpdb->prefix . $_tbl ) ) { + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}{$_tbl}" ); + } + } + $wpdb->query( 'SET FOREIGN_KEY_CHECKS=1' ); + } + + $t_count = 0; + foreach ( $data['threads'] as $t ) { + // user_id remappen falls der Benutzer eine neue ID bekommen hat + if ( isset( $t['user_id'] ) && isset( $id_map[ (int)$t['user_id'] ] ) ) { + $t['user_id'] = $id_map[ (int)$t['user_id'] ]; + } + $wpdb->query( self::build_upsert( + "{$wpdb->prefix}forum_threads", + $t, + 'id' + ) ); + $t_count++; + } + + $p_count = 0; + foreach ( $data['posts'] ?? [] as $p ) { + if ( isset( $p['user_id'] ) && isset( $id_map[ (int)$p['user_id'] ] ) ) { + $p['user_id'] = $id_map[ (int)$p['user_id'] ]; + } + $wpdb->query( self::build_upsert( + "{$wpdb->prefix}forum_posts", + $p, + 'id' + ) ); + $p_count++; + } + + // Tags re-importieren + if ( ! empty( $data['thread_tags'] ) ) { + $tag_map = []; + foreach ( $data['thread_tags'] as $tt ) { + $slug = $tt['slug'] ?? ''; + if ( ! isset( $tag_map[ $slug ] ) ) { + $etag = $wpdb->get_var( $wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}forum_tags WHERE slug=%s", $slug + ) ); + if ( $etag ) { + $tag_map[ $slug ] = (int) $etag; + } else { + $wpdb->insert( "{$wpdb->prefix}forum_tags", [ + 'name' => $tt['name'] ?? $slug, + 'slug' => $slug, + 'use_count' => (int)($tt['use_count'] ?? 0), + ] ); + $tag_map[ $slug ] = (int) $wpdb->insert_id; + } + } + $wpdb->replace( "{$wpdb->prefix}forum_thread_tags", [ + 'thread_id' => (int)($tt['thread_id'] ?? 0), + 'tag_id' => $tag_map[ $slug ], + ] ); + } + $log[] = '✅ Tags (' . count( $data['thread_tags'] ) . ') importiert.'; + } + + // Abonnements (user_id remappen) + if ( ! empty( $data['subscriptions'] ) ) { + $sc = 0; + foreach ( $data['subscriptions'] as $row ) { + unset( $row['id'] ); + if ( isset( $row['user_id'] ) && isset( $id_map[ (int)$row['user_id'] ] ) ) { + $row['user_id'] = $id_map[ (int)$row['user_id'] ]; + } + $wpdb->replace( "{$wpdb->prefix}forum_subscriptions", $row ); + $sc++; + } + if ( $sc ) $log[] = "✅ Abonnements ($sc) importiert."; + } + + $log[] = "✅ Threads ($t_count) + Posts ($p_count) importiert."; + } + + // ── 7. Thread-Präfixe ───────────────────────────────── + if ( ! empty( $data['prefixes'] ) ) { + $force = ! empty( $post['import_force_prefixes'] ); + if ( $force && self::table_exists( $wpdb->prefix . 'forum_prefixes' ) ) { + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}forum_prefixes" ); + } + $pc = 0; + foreach ( $data['prefixes'] as $row ) { + $wpdb->query( $wpdb->prepare( + "INSERT INTO {$wpdb->prefix}forum_prefixes + (id, label, color, bg_color, sort_order) + VALUES (%d,%s,%s,%s,%d) + ON DUPLICATE KEY UPDATE + label=%s, color=%s, bg_color=%s, sort_order=%d", + (int) ($row['id'] ?? 0), + $row['label'] ?? '', + $row['color'] ?? '#ffffff', + $row['bg_color'] ?? '#475569', + (int) ($row['sort_order'] ?? 0), + $row['label'] ?? '', + $row['color'] ?? '#ffffff', + $row['bg_color'] ?? '#475569', + (int) ($row['sort_order'] ?? 0), + ) ); + $pc++; + } + if ( $pc ) $log[] = "✅ Thread-Präfixe ($pc) importiert."; + } + + // ── 8. Umfragen + Abstimmungen ──────────────────────── + if ( ! empty( $data['polls'] ) ) { + $force = ! empty( $post['import_force_polls'] ); + if ( $force ) { + if ( self::table_exists( $wpdb->prefix . 'forum_poll_votes' ) ) $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}forum_poll_votes" ); + if ( self::table_exists( $wpdb->prefix . 'forum_polls' ) ) $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}forum_polls" ); + } + $pc = 0; + foreach ( $data['polls'] as $row ) { + $wpdb->query( $wpdb->prepare( + "INSERT INTO {$wpdb->prefix}forum_polls + (id, thread_id, question, options, multi, ends_at, created_at) + VALUES (%d,%d,%s,%s,%d,%s,%s) + ON DUPLICATE KEY UPDATE + thread_id=%d, question=%s, options=%s, multi=%d, ends_at=%s", + (int) ($row['id'] ?? 0), + (int) ($row['thread_id'] ?? 0), + $row['question'] ?? '', + $row['options'] ?? '[]', + (int) ($row['multi'] ?? 0), + $row['ends_at'] ?? null, + $row['created_at'] ?? current_time('mysql'), + (int) ($row['thread_id'] ?? 0), + $row['question'] ?? '', + $row['options'] ?? '[]', + (int) ($row['multi'] ?? 0), + $row['ends_at'] ?? null, + ) ); + $pc++; + } + + $vc = 0; + foreach ( $data['poll_votes'] ?? [] as $row ) { + unset( $row['id'] ); + if ( isset( $row['user_id'] ) && isset( $id_map[ (int)$row['user_id'] ] ) ) { + $row['user_id'] = $id_map[ (int)$row['user_id'] ]; + } + $wpdb->replace( "{$wpdb->prefix}forum_poll_votes", $row ); + $vc++; + } + $log[] = "✅ Umfragen ($pc) + Abstimmungen ($vc) importiert."; + } + + // ── 9. Lesezeichen ──────────────────────────────────── + if ( ! empty( $data['bookmarks'] ) ) { + $force = ! empty( $post['import_force_bookmarks'] ); + if ( $force && self::table_exists( $wpdb->prefix . 'forum_bookmarks' ) ) { + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}forum_bookmarks" ); + } + $bc = 0; + foreach ( $data['bookmarks'] as $row ) { + unset( $row['id'] ); + if ( isset( $row['user_id'] ) && isset( $id_map[ (int)$row['user_id'] ] ) ) { + $row['user_id'] = $id_map[ (int)$row['user_id'] ]; + } + $wpdb->replace( "{$wpdb->prefix}forum_bookmarks", $row ); + $bc++; + } + if ( $bc ) $log[] = "✅ Lesezeichen ($bc) importiert."; + } + + // ── 10. Likes (user_id + object_id remapping) ───────── + if ( ! empty( $data['likes'] ) ) { + $force = ! empty( $post['import_force_threads'] ); + if ( $force ) { + $wpdb->query( "DELETE FROM {$wpdb->prefix}forum_likes" ); + } + $count = 0; + foreach ( $data['likes'] as $row ) { + unset( $row['id'] ); + if ( isset( $row['user_id'] ) && isset( $id_map[ (int)$row['user_id'] ] ) ) { + $row['user_id'] = $id_map[ (int)$row['user_id'] ]; + } + $exists = $wpdb->get_var( $wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}forum_likes + WHERE user_id=%d AND object_id=%d AND object_type=%s", + (int)($row['user_id'] ?? 0), + (int)($row['object_id'] ?? 0), + ($row['object_type'] ?? '') + ) ); + if ( ! $exists ) { + $wpdb->insert( "{$wpdb->prefix}forum_likes", $row ); + $count++; + } + } + $log[] = "✅ Likes ($count) importiert."; + } + + // ── 11. Reaktionen ──────────────────────────────────── + if ( ! empty( $data['reactions'] ) ) { + $force = ! empty( $post['import_force_threads'] ); + if ( $force ) { + $wpdb->query( "DELETE FROM {$wpdb->prefix}forum_reactions" ); + } + $count = 0; + foreach ( $data['reactions'] as $row ) { + unset( $row['id'] ); + if ( isset( $row['user_id'] ) && isset( $id_map[ (int)$row['user_id'] ] ) ) { + $row['user_id'] = $id_map[ (int)$row['user_id'] ]; + } + $wpdb->replace( "{$wpdb->prefix}forum_reactions", $row ); + $count++; + } + $log[] = "✅ Reaktionen ($count) importiert."; + } + + // ── 12. Benachrichtigungen ──────────────────────────── + if ( ! empty( $data['notifications'] ) ) { + $force = ! empty( $post['import_force_threads'] ); + if ( $force ) { + $wpdb->query( "DELETE FROM {$wpdb->prefix}forum_notifications" ); + } + $count = 0; + foreach ( $data['notifications'] as $row ) { + unset( $row['id'] ); + // Sowohl user_id als auch actor_id remappen + foreach ( [ 'user_id', 'actor_id' ] as $field ) { + if ( isset( $row[$field] ) && isset( $id_map[ (int)$row[$field] ] ) ) { + $row[$field] = $id_map[ (int)$row[$field] ]; + } + } + $wpdb->insert( "{$wpdb->prefix}forum_notifications", $row ); + $count++; + } + $log[] = "✅ Benachrichtigungen ($count) importiert."; + } + + // ── 13. Privatnachrichten ───────────────────────────── + if ( ! empty( $data['messages'] ) ) { + $force = ! empty( $post['import_force_messages'] ); + if ( $force ) { + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}forum_messages" ); + } + $count = 0; + foreach ( $data['messages'] as $row ) { + unset( $row['id'] ); + foreach ( [ 'from_id', 'to_id' ] as $field ) { + if ( isset( $row[$field] ) && isset( $id_map[ (int)$row[$field] ] ) ) { + $row[$field] = $id_map[ (int)$row[$field] ]; + } + } + $wpdb->insert( "{$wpdb->prefix}forum_messages", $row ); + $count++; + } + $log[] = "✅ Privatnachrichten ($count) importiert."; + } + + // ── 14. Meldungen ───────────────────────────────────── + if ( ! empty( $data['reports'] ) ) { + $force = ! empty( $post['import_force_reports'] ); + if ( $force ) { + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}forum_reports" ); + } + $count = 0; + foreach ( $data['reports'] as $row ) { + unset( $row['id'] ); + if ( isset( $row['reporter_id'] ) && isset( $id_map[ (int)$row['reporter_id'] ] ) ) { + $row['reporter_id'] = $id_map[ (int)$row['reporter_id'] ]; + } + $wpdb->insert( "{$wpdb->prefix}forum_reports", $row ); + $count++; + } + $log[] = "✅ Meldungen ($count) importiert."; + } + + // ── 15. Einladungen ─────────────────────────────────── + if ( ! empty( $data['invites'] ) ) { + $force = ! empty( $post['import_force_invites'] ); + if ( $force && self::table_exists( $wpdb->prefix . 'forum_invites' ) ) { + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}forum_invites" ); + } + $count = 0; + foreach ( $data['invites'] as $row ) { + unset( $row['id'] ); + if ( isset( $row['created_by'] ) && isset( $id_map[ (int)$row['created_by'] ] ) ) { + $row['created_by'] = $id_map[ (int)$row['created_by'] ]; + } + // Duplikat-Codes überspringen + if ( self::table_exists( $wpdb->prefix . 'forum_invites' ) ) { + $dup = $wpdb->get_var( $wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}forum_invites WHERE code=%s", + $row['code'] ?? '' + ) ); + if ( $dup ) continue; + } + $wpdb->insert( "{$wpdb->prefix}forum_invites", $row ); + $count++; + } + if ( $count ) $log[] = "✅ Einladungen ($count) importiert."; + } + + // ── 16. Ignore-Liste ────────────────────────────────── + if ( ! empty( $data['ignore_list'] ) ) { + $force = ! empty( $post['import_force_ignore'] ); + if ( $force && self::table_exists( $wpdb->prefix . 'forum_ignore_list' ) ) { + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}forum_ignore_list" ); + } + $count = 0; + foreach ( $data['ignore_list'] as $row ) { + unset( $row['id'] ); + foreach ( [ 'user_id', 'ignored_id' ] as $field ) { + if ( isset( $row[$field] ) && isset( $id_map[ (int)$row[$field] ] ) ) { + $row[$field] = $id_map[ (int)$row[$field] ]; + } + } + $wpdb->replace( "{$wpdb->prefix}forum_ignore_list", $row ); + $count++; + } + if ( $count ) $log[] = "✅ Ignore-Einträge ($count) importiert."; + } + + } catch ( \Exception $e ) { + $has_error = true; + $log[] = '❌ Fehler: ' . $e->getMessage(); + } + + $wpdb->suppress_errors( $suppress ); + + // ── Statistiken neu berechnen ───────────────────────────── + if ( ! $has_error ) { + self::recalculate_stats(); + $log[] = '🔄 Zähler (post_count, thread_count) neu berechnet.'; + } + + if ( $has_error ) { + return self::result( 'error', + 'Import teilweise fehlgeschlagen.', + $log, + $data['_meta'] ?? [] + ); + } + + if ( empty( $log ) ) { + return self::result( 'warning', + 'Nichts importiert — Datei enthielt keine gültigen Abschnitte.', + [], + $data['_meta'] ?? [] + ); + } + + $prefix = $ver_warning ? $ver_warning . ' — ' : ''; + return self::result( 'success', + $prefix . 'Import abgeschlossen.', + $log, + $data['_meta'] ?? [] + ); + } + + // ═══════════════════════════════════════════════════════════════ + // ADMIN-SEITE + // ═══════════════════════════════════════════════════════════════ + + /** Rendert die Export/Import-Admin-Seite (ersetzt wbf_admin_export() in forum-admin.php) */ + public static function admin_page() { + if ( ! current_user_can( 'manage_options' ) ) return; + + // URL-Fehler aus dem Export-Redirect + $url_err = sanitize_key( $_GET['wbf_err'] ?? '' ); + + // Import verarbeiten + $notice = null; + if ( isset( $_POST['wbf_do_import'] ) && check_admin_referer( 'wbf_import_nonce' ) ) { + $result = self::handle_import( $_POST, $_FILES ); + $notice = $result; + } + + if ( $url_err === 'no_sections' ) { + $notice = self::result( 'error', 'Bitte mindestens eine Sektion für den Export auswählen.' ); + } elseif ( $url_err === 'json_fail' ) { + $notice = self::result( 'error', 'JSON-Kodierung fehlgeschlagen — möglicherweise ungültige Zeichen in den Daten.' ); + } + + ?> +
+

+ + Export & Import +

+

+ Erstelle ein vollständiges Backup aller Forum-Daten oder stelle ein Backup wieder her. +

+ + +
+

+ +

+ Backup erstellt: +  ·  Quelle: +

+ + +
+ + Import-Protokoll ( Einträge) + +
    + +
  • + +
+
+ +
+ + +
+ + + + +
+ + +
+ +
+
+ + Export +
+
+

+ Wähle die Bereiche, die exportiert werden sollen. Die Datei wird als .json sofort heruntergeladen. +

+
+ + + +
+ + +
+ + + [ $icon, $label, $desc ] ) : ?> + + + + + + +
+ + + + +
+ +
+ + Sofortiger Download +
+
+
+
+ +
+
+ + Import +
+
+ + +
+ ⚠️ Vor dem Import: Erstelle unbedingt zuerst einen Export als Backup. + Benutzer-Import enthält Passwort-Hashes — teile die Datei nicht öffentlich. +
+ +
+ + + +
+ + +

+ Maximum: +

+
+ + +
+

+ Überschreiben-Optionen + + (ohne Häkchen werden Duplikate übersprungen) + +

+ [ '📂', 'Kategorien überschreiben', 'Bestehende Kategorien werden gelöscht und neu importiert' ], + 'import_force_users' => [ '👥', 'Benutzer aktualisieren', 'Gleiche Username/E-Mail → Daten werden überschrieben' ], + 'import_force_threads' => [ '💬', 'Threads & Posts überschreiben', 'Löscht Threads, Posts, Likes, Reaktionen, Benachrichtigungen' ], + 'import_force_polls' => [ '📊', 'Umfragen überschreiben', 'Alle Umfragen + Abstimmungen werden neu importiert' ], + 'import_force_bookmarks' => [ '🔖', 'Lesezeichen überschreiben', 'Alle Lesezeichen werden neu importiert' ], + 'import_force_prefixes' => [ '🏷️', 'Thread-Präfixe überschreiben', 'Alle Präfixe werden neu importiert' ], + 'import_force_ignore' => [ '🚫', 'Ignore-Liste überschreiben', 'Alle Blockierungen werden neu importiert' ], + 'import_force_messages' => [ '✉️', 'Privatnachrichten überschreiben', 'Alle DMs werden neu importiert' ], + 'import_force_reports' => [ '🚩', 'Meldungen überschreiben', 'Alle Meldungen werden neu importiert' ], + 'import_force_invites' => [ '📨', 'Einladungen überschreiben', 'Alle Einladungscodes werden neu importiert' ], + ]; + foreach ( $overwrite as $name => [ $icon, $label, $hint ] ) : ?> + + +
+ +
+ + Nur kompatible WBF-Backups (.json) +
+
+
+
+ +
+

+ + Exportierte Inhalte im Überblick +

+
+ +
+
+
+
+ +
+ + +
+ 💡 Hinweise: + Bei sehr großen Backups (10 MB+) das PHP-Limit upload_max_filesize prüfen. + Nach dem Import werden Beitrags- und Thread-Zähler automatisch neu berechnet. + Superadmin-Konten können nicht per Import überschrieben werden. +
+
+ query( + "UPDATE {$wpdb->prefix}forum_users u + SET u.post_count = ( + SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts p + WHERE p.user_id = u.id AND p.deleted_at IS NULL + )" + ); + + // thread_count + post_count pro Kategorie + $wpdb->query( + "UPDATE {$wpdb->prefix}forum_categories c + SET c.thread_count = ( + SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads t + WHERE t.category_id = c.id AND t.deleted_at IS NULL + ), + c.post_count = ( + SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts p + JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id + WHERE t.category_id = c.id AND p.deleted_at IS NULL + )" + ); + + // reply_count + like_count pro Thread + $wpdb->query( + "UPDATE {$wpdb->prefix}forum_threads t + SET t.reply_count = ( + SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts p + WHERE p.thread_id = t.id AND p.deleted_at IS NULL + ), + t.like_count = ( + SELECT COUNT(*) FROM {$wpdb->prefix}forum_likes l + WHERE l.object_id = t.id AND l.object_type = 'thread' + )" + ); + + // like_count pro Post + $wpdb->query( + "UPDATE {$wpdb->prefix}forum_posts p + SET p.like_count = ( + SELECT COUNT(*) FROM {$wpdb->prefix}forum_likes l + WHERE l.object_id = p.id AND l.object_type = 'post' + )" + ); + + // Tag use_count + $wpdb->query( + "UPDATE {$wpdb->prefix}forum_tags tg + SET tg.use_count = ( + SELECT COUNT(*) FROM {$wpdb->prefix}forum_thread_tags tt + WHERE tt.tag_id = tg.id + )" + ); + } + + /** + * Erstellt ein INSERT … ON DUPLICATE KEY UPDATE Statement. + * Benutzt prepare() für alle Werte. + */ + private static function build_upsert( string $table, array $row, string $pk = 'id' ): string { + global $wpdb; + + $cols = array_keys( $row ); + $values = array_values( $row ); + + $col_list = implode( ', ', array_map( fn($c) => "`$c`", $cols ) ); + $placeholders = implode( ', ', array_map( fn($v) => is_null($v) ? 'NULL' : ( is_int($v) || is_float($v) ? '%f' : '%s' ), $values ) ); + + $update_parts = []; + foreach ( $cols as $i => $col ) { + if ( $col === $pk ) continue; // PK nicht updaten + $update_parts[] = "`{$col}` = VALUES(`{$col}`)"; + } + + $sql = "INSERT INTO `{$table}` ({$col_list}) VALUES "; + + // Typen-sichere Formatierung + $formatted_vals = []; + foreach ( $values as $v ) { + if ( is_null( $v ) ) { + $formatted_vals[] = 'NULL'; + } elseif ( is_int( $v ) || ( is_string( $v ) && ctype_digit( $v ) ) ) { + $formatted_vals[] = (int) $v; + } else { + $formatted_vals[] = "'" . esc_sql( $v ) . "'"; + } + } + + $sql .= '(' . implode( ', ', $formatted_vals ) . ')'; + $sql .= ' ON DUPLICATE KEY UPDATE ' . implode( ', ', $update_parts ); + + return $sql; + } + + /** Sicher SELECT ausführen — gibt leeres Array zurück wenn Tabelle nicht existiert */ + private static function safe_get( string $sql ): array { + global $wpdb; + // Tabellenname aus der Query extrahieren (primitiv, aber reicht für unsere Queries) + if ( preg_match( '/FROM\s+`?(\w+)`?/i', $sql, $m ) ) { + if ( ! self::table_exists( $m[1] ) ) return []; + } + return $wpdb->get_results( $sql, ARRAY_A ) ?: []; + } + + /** Prüft ob eine Tabelle existiert */ + private static function table_exists( string $table ): bool { + global $wpdb; + return $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) === $table; + } + + /** Erstellt ein standardisiertes Ergebnis-Array */ + private static function result( string $type, string $message, array $log = [], array $meta = [] ): array { + return compact( 'type', 'message', 'log', 'meta' ); + } + + /** Alle Export-Optionen (key => [icon, label, beschreibung]) */ + private static function export_options(): array { + return [ + 'settings' => [ '⚙️', 'Einstellungen & Wortfilter', 'Forum-Texte, Labels, Regeln, Wortfilter, Profilfelder, Reaktionen' ], + 'roles' => [ '🛡️', 'Rollen', 'Alle Rollen inkl. Berechtigungen & Farben' ], + 'levels' => [ '⭐', 'Level-System', 'Level-Konfiguration & An/Aus-Status' ], + 'categories' => [ '📂', 'Kategorien', 'Alle Kategorien inkl. Hierarchie & Zugriffsstufen' ], + 'users' => [ '👥', 'Benutzer & Profilfelder', 'Accounts, Ban-Status, Profilfeld-Werte' ], + 'threads' => [ '💬', 'Threads, Posts & Abonnements', 'Alle Inhalte, Tags & Thread-Abonnements' ], + 'polls' => [ '📊', 'Umfragen', 'Alle Umfragen inkl. Abstimmungen' ], + 'bookmarks' => [ '🔖', 'Lesezeichen', 'Alle gespeicherten Thread-Lesezeichen' ], + 'prefixes' => [ '🏷️', 'Thread-Präfixe', 'Alle konfigurierten Präfix-Labels & Farben' ], + 'interactions' => [ '❤️', 'Likes & Reaktionen', 'Likes, Emoji-Reaktionen, Benachrichtigungen' ], + 'messages' => [ '✉️', 'Privatnachrichten', 'Alle DM-Konversationen' ], + 'ignore_list' => [ '🚫', 'Ignore-Liste', 'Alle gegenseitigen Nutzer-Blockierungen' ], + 'reports' => [ '🚩', 'Meldungen', 'Gemeldete Beiträge inkl. Status' ], + 'invites' => [ '📨', 'Einladungen', 'Alle Einladungscodes inkl. Nutzungsstand' ], + ]; + } +} \ No newline at end of file diff --git a/includes/class-forum-roles.php b/includes/class-forum-roles.php index 24335ec..351063c 100644 --- a/includes/class-forum-roles.php +++ b/includes/class-forum-roles.php @@ -15,7 +15,7 @@ class WBF_Roles { private static function default_roles() { return [ 'superadmin' => [ - 'label' => 'Superadmin', + 'label' => 'Admin', 'level' => 100, 'color' => '#e11d48', 'bg_color' => 'rgba(225,29,72,.15)', diff --git a/includes/class-forum-shortcodes.php b/includes/class-forum-shortcodes.php index b32fc71..be0c07d 100644 --- a/includes/class-forum-shortcodes.php +++ b/includes/class-forum-shortcodes.php @@ -341,9 +341,9 @@ class WBF_Shortcodes { + - + id); ?> - Bearbeiten + user_id); + if ($current && (int)$current->id !== (int)$thread->user_id && wbf_can_be_ignored($op_author)): + $op_is_ignored = WBF_DB::is_ignored($current->id, (int)$thread->user_id); + ?> + + @@ -774,8 +791,8 @@ class WBF_Shortcodes {
Dieser Thread ist geschlossen.
- + @@ -843,6 +860,22 @@ class WBF_Shortcodes { Bearbeiten + user_id); + if ($current && (int)$current->id !== (int)$post->user_id && wbf_can_be_ignored($post_author)): + $post_is_ignored = WBF_DB::is_ignored($current->id, (int)$post->user_id); + ?> + + id,$current); ?> @@ -870,14 +903,23 @@ class WBF_Shortcodes { id, 50 ); + $user_posts = WBF_DB::get_user_posts( $profile->id, 50 ); + $bookmarks = $is_own ? WBF_DB::get_user_bookmarks($current->id, 50) : []; + $ignore_list = $is_own ? WBF_DB::get_ignore_list($current->id) : []; + $cf_defs = WBF_DB::get_profile_field_defs(); + $cf_vals = WBF_DB::get_user_meta( $profile->id ); + // Aktiven Tab aus URL lesen (tab=1|2|3), Standard: 1 für eigenes, 2 für fremdes + $active_tab = (int)($_GET['ptab'] ?? ($is_own ? 1 : 2)); + $active_tab = in_array($active_tab, [1,2,3]) ? $active_tab : ($is_own ? 1 : 2); + // Tab 1 + 3 nur für eigenes Profil + if (!$is_own && $active_tab !== 2) $active_tab = 2; ob_start(); ?>
@@ -885,8 +927,6 @@ class WBF_Shortcodes {
- + + role) >= 0): ?> +
+ + Nachricht senden + + id, $profile->id); ?> + + +
+ + + -
+ + + + + + + +
+
Profil bearbeiten
@@ -995,7 +1073,10 @@ class WBF_Shortcodes {
-
+ +
signature??''); ?>/300
+
+
profile_public ?? 1); ?>
- -
signature??''); ?>/300
-
- - id ); - if ( ! empty( $cf_defs ) ): - ?> + +
+
+ E-Mail-Adresse +
+
+

+ Aktuelle Adresse: email); ?> +

+
+
+ + +
+
+ + +
+
+ +
+
+ + +
Weitere Profilangaben
-
- + - > + - - -
-
- Datenschutz & Konto löschen -
-
-

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

- - -
-
+ - - - -
-
- Beiträge - -
-
- -

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; - ?> -
-
- - - Thread - - - - Antwort - - - - thread_title, 0, 60) ); ?> - - - cat_name); ?> - - created_at); ?> -
- -

- -
- -
-
+ + - id, 50); ?> +
Lesezeichen @@ -1186,6 +1208,192 @@ class WBF_Shortcodes {
+ +
+
+ Beiträge + +
+
+ +

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; + ?> +
+
+ + + Thread + + + + Antwort + + + + thread_title, 0, 60)); ?> + + + cat_name); ?> + + created_at); ?> +
+ +

+ +
+ +
+
+ + + + + + + +
+
+ E-Mail-Benachrichtigungen +
+
+ id); + $n_reply = ($notif_meta['notify_reply'] ?? '1') !== '0'; + $n_mention = ($notif_meta['notify_mention'] ?? '1') !== '0'; + $n_message = ($notif_meta['notify_message'] ?? '1') !== '0'; + ?> +

+ Lege fest bei welchen Ereignissen du eine E-Mail erhältst. +

+
+ + + +
+ +
+
+ + +
+
+ Ignorierte Nutzer + +
+
+ +

Du ignorierst niemanden.

+ +
+ +
+ + avatar_url, $ign->display_name, 36); ?> + +
+ + display_name); ?> + + Ignoriert seit ignored_since); ?> +
+ +
+ +
+ +
+
+ + +
+
+ Datenschutz & Konto löschen +
+
+

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

+ + +
+
+ + +
@@ -1193,6 +1401,7 @@ class WBF_Shortcodes {
-
+
-
+ @@ -1443,8 +1652,8 @@ class WBF_Shortcodes {

Ergebnis(se) gefunden.

- + - + -
+
Durch die Nutzung des Forums stimmst du unseren Regeln zu.