Files
WP-Business-Forum/includes/class-forum-export.php
2026-03-29 13:41:26 +02:00

1204 lines
63 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* WBF_Export — Vollständiger Export & Import für WP Business Forum
*
* Behebt gegenüber dem Inline-Code in forum-admin.php:
* - user_id-Remapping wird auf Threads, Posts und alle Interactions angewendet
* - Thread/Post-INSERT nutzt INSERT…ON DUPLICATE KEY UPDATE statt blindem INSERT
* - wbf_word_filter wird exportiert und importiert
* - Stat-Neuberechnung (post_count, thread_count, …) nach dem Import
* - Datei-Typ- und Größenvalidierung beim Upload
* - Force-Thread löscht nur wirklich thread-abhängige Tabellen
* - Alle Sektionen in einer DB-Transaktion (rollback bei Fehler)
* - Übersichtliches Import-Protokoll pro Sektion
*
* Einbindung in wp-business-forum.php:
* require_once WBF_PATH . 'includes/class-forum-export.php';
*
* In forum-admin.php den alten admin_init-Export-Hook und die Funktion
* wbf_admin_export() durch WBF_Export::hooks() ersetzen.
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class WBF_Export {
/** Maximale Upload-Größe für Backup-Dateien (in Bytes) */
const MAX_UPLOAD_BYTES = 52_428_800; // 50 MB
// ═══════════════════════════════════════════════════════════════
// Hooks registrieren — einmalig in wp-business-forum.php aufrufen
// ═══════════════════════════════════════════════════════════════
public static function hooks() {
// Export-Download läuft vor jedem HTML-Output
add_action( 'admin_init', [ __CLASS__, 'handle_export' ] );
}
// ═══════════════════════════════════════════════════════════════
// EXPORT — JSON-Download
// ═══════════════════════════════════════════════════════════════
public static function handle_export() {
if ( ! isset( $_POST['wbf_do_export'] ) ) return;
if ( ! check_admin_referer( 'wbf_export_nonce' ) ) return;
if ( ! current_user_can( 'manage_options' ) ) return;
$sections = isset( $_POST['export_sections'] ) && is_array( $_POST['export_sections'] )
? array_keys( array_filter( $_POST['export_sections'] ) )
: [];
if ( empty( $sections ) ) {
// Ohne Auswahl → zur Seite zurück mit Fehlermeldung
wp_safe_redirect( add_query_arg( [
'page' => '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['profile_field_cats'] = get_option( 'wbf_profile_field_cats', [] );
$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'] );
if ( isset($data['profile_field_cats']) ) update_option( 'wbf_profile_field_cats', $data['profile_field_cats'] );
$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.' );
}
?>
<div class="wrap" style="max-width:900px">
<h1 style="display:flex;align-items:center;gap:10px;margin-bottom:0">
<span class="dashicons dashicons-database-import" style="font-size:1.5rem;width:auto;color:#00b4d8"></span>
Export &amp; Import
</h1>
<p style="color:#6b7280;margin-top:4px;margin-bottom:24px">
Erstelle ein vollständiges Backup aller Forum-Daten oder stelle ein Backup wieder her.
</p>
<?php if ( $notice ) : ?>
<div class="notice notice-<?php echo esc_attr( $notice['type'] ); ?> is-dismissible"
style="border-left-width:4px;padding:12px 16px">
<p style="margin:0;font-weight:600"><?php echo esc_html( $notice['message'] ); ?></p>
<?php if ( ! empty( $notice['meta'] ) ) : ?>
<p style="margin:4px 0 0;font-size:.82rem;color:#64748b">
Backup erstellt: <?php echo esc_html( $notice['meta']['exported'] ?? '?' ); ?>
&nbsp;·&nbsp; Quelle: <?php echo esc_html( $notice['meta']['site'] ?? '?' ); ?>
</p>
<?php endif; ?>
<?php if ( ! empty( $notice['log'] ) ) : ?>
<details style="margin-top:10px">
<summary style="cursor:pointer;font-size:.83rem;color:#374151;user-select:none">
Import-Protokoll (<?php echo count( $notice['log'] ); ?> Einträge)
</summary>
<ul style="margin:8px 0 0 16px;font-size:.82rem;color:#374151;line-height:1.7">
<?php foreach ( $notice['log'] as $line ) : ?>
<li><?php echo esc_html( $line ); ?></li>
<?php endforeach; ?>
</ul>
</details>
<?php endif; ?>
</div>
<?php endif; ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;align-items:start">
<?php self::render_export_card(); ?>
<?php self::render_import_card(); ?>
</div>
<?php self::render_info_box(); ?>
</div>
<?php
}
// ── Teilansichten ──────────────────────────────────────────────
private static function render_export_card() {
?>
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.06)">
<div style="padding:14px 18px;background:#f0fdf4;border-bottom:1px solid #dcfce7;display:flex;align-items:center;gap:8px">
<span class="dashicons dashicons-download" style="color:#16a34a;font-size:1.2rem;width:auto"></span>
<strong style="color:#15803d;font-size:.9rem;text-transform:uppercase;letter-spacing:.05em">Export</strong>
</div>
<div style="padding:18px">
<p style="color:#6b7280;font-size:.85rem;margin:0 0 14px">
Wähle die Bereiche, die exportiert werden sollen. Die Datei wird als <code>.json</code> sofort heruntergeladen.
</p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin.php?page=wbf-export' ) ); ?>">
<?php wp_nonce_field( 'wbf_export_nonce' ); ?>
<!-- Alle / Keine Auswahl -->
<div style="display:flex;gap:8px;margin-bottom:10px">
<button type="button" class="button"
onclick="document.querySelectorAll('[name^=export_sections]').forEach(c=>c.checked=true)"
style="font-size:.78rem;padding:2px 10px;height:auto">
Alle wählen
</button>
<button type="button" class="button"
onclick="document.querySelectorAll('[name^=export_sections]').forEach(c=>c.checked=false)"
style="font-size:.78rem;padding:2px 10px;height:auto">
Keine
</button>
</div>
<table style="width:100%;border-collapse:collapse">
<?php
$opts = self::export_options();
foreach ( $opts as $key => [ $icon, $label, $desc ] ) : ?>
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:9px 0;width:32px;text-align:center;font-size:1.05rem"><?php echo $icon; ?></td>
<td style="padding:9px 8px">
<label style="display:block;font-weight:600;font-size:.875rem;color:#111827;cursor:pointer"
for="exp_<?php echo esc_attr( $key ); ?>">
<?php echo esc_html( $label ); ?>
</label>
<span style="font-size:.77rem;color:#9ca3af"><?php echo esc_html( $desc ); ?></span>
</td>
<td style="text-align:right;padding:9px 0;width:28px">
<input type="checkbox" id="exp_<?php echo esc_attr( $key ); ?>"
name="export_sections[<?php echo esc_attr( $key ); ?>]" value="1" checked
style="width:16px;height:16px;accent-color:#16a34a;cursor:pointer">
</td>
</tr>
<?php endforeach; ?>
</table>
<div style="margin-top:16px;padding-top:14px;border-top:1px solid #f3f4f6;display:flex;align-items:center;gap:10px">
<button type="submit" name="wbf_do_export" class="button button-primary"
style="background:#16a34a;border-color:#15803d;display:flex;align-items:center;gap:5px">
<span class="dashicons dashicons-download" style="margin-top:3px"></span>
Als JSON exportieren
</button>
<span style="font-size:.78rem;color:#9ca3af">Sofortiger Download</span>
</div>
</form>
</div>
</div>
<?php
}
private static function render_import_card() {
?>
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.06)">
<div style="padding:14px 18px;background:#eff6ff;border-bottom:1px solid #dbeafe;display:flex;align-items:center;gap:8px">
<span class="dashicons dashicons-upload" style="color:#2563eb;font-size:1.2rem;width:auto"></span>
<strong style="color:#1d4ed8;font-size:.9rem;text-transform:uppercase;letter-spacing:.05em">Import</strong>
</div>
<div style="padding:18px">
<!-- Warnbox -->
<div style="background:#fff7ed;border:1px solid #fed7aa;border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:.82rem;color:#92400e">
<strong>⚠️ Vor dem Import:</strong> Erstelle unbedingt zuerst einen Export als Backup.
Benutzer-Import enthält Passwort-Hashes — teile die Datei nicht öffentlich.
</div>
<form method="post" enctype="multipart/form-data"
action="<?php echo esc_url( admin_url( 'admin.php?page=wbf-export' ) ); ?>">
<?php wp_nonce_field( 'wbf_import_nonce' ); ?>
<!-- Datei-Upload -->
<div style="margin-bottom:14px">
<label style="display:block;font-size:.82rem;font-weight:700;color:#374151;margin-bottom:5px;text-transform:uppercase;letter-spacing:.04em">
Backup-Datei (.json)
</label>
<input type="file" name="import_file" accept=".json,application/json" required
style="width:100%;padding:6px;border:1.5px solid #d1d5db;border-radius:6px;font-size:.875rem">
<p style="font-size:.75rem;color:#94a3b8;margin:3px 0 0">
Maximum: <?php echo size_format( self::MAX_UPLOAD_BYTES ); ?>
</p>
</div>
<!-- Überschreiben-Optionen -->
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:12px 14px;margin-bottom:14px">
<p style="font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin:0 0 10px">
Überschreiben-Optionen
<span style="font-size:.72rem;font-weight:400;color:#9ca3af;text-transform:none;letter-spacing:0">
(ohne Häkchen werden Duplikate übersprungen)
</span>
</p>
<?php
$overwrite = [
'import_force_cats' => [ '📂', '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 ] ) : ?>
<label style="display:flex;align-items:flex-start;gap:8px;font-size:.82rem;color:#374151;cursor:pointer;margin-bottom:7px">
<input type="checkbox" name="<?php echo esc_attr( $name ); ?>" value="1"
style="width:15px;height:15px;accent-color:#dc2626;cursor:pointer;margin-top:2px;flex-shrink:0">
<span>
<?php echo $icon; ?> <strong><?php echo esc_html( $label ); ?></strong>
<span style="color:#9ca3af;font-size:.77rem;display:block"><?php echo esc_html( $hint ); ?></span>
</span>
</label>
<?php endforeach; ?>
</div>
<div style="display:flex;align-items:center;gap:10px">
<button type="submit" name="wbf_do_import" class="button button-primary"
style="background:#2563eb;border-color:#1d4ed8;display:flex;align-items:center;gap:5px"
onclick="return confirm('Import starten?\n\nBestehende Daten können je nach Überschreiben-Optionen überschrieben werden.')">
<span class="dashicons dashicons-upload" style="margin-top:3px"></span>
Importieren
</button>
<span style="font-size:.78rem;color:#9ca3af">Nur kompatible WBF-Backups (.json)</span>
</div>
</form>
</div>
</div>
<?php
}
private static function render_info_box() {
?>
<div style="margin-top:24px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:18px 22px">
<h3 style="margin:0 0 12px;font-size:.9rem;display:flex;align-items:center;gap:7px">
<span class="dashicons dashicons-info" style="color:#00b4d8;width:auto"></span>
Exportierte Inhalte im Überblick
</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px">
<?php
$info_items = [
[ '⚙️', 'Einstellungen & Wortfilter', 'Forum-Texte, Labels, Regeln, Auto-Logout, Wortfilter, Profilfeld-Definitionen, Reaktionen-Konfiguration' ],
[ '🛡️', 'Rollen', 'Alle Rollen mit Berechtigungen & Design (Superadmin wird nie überschrieben)' ],
[ '⭐', 'Level-System', 'Level-Namen, Schwellenwerte, Icons, Farben, An/Aus-Status' ],
[ '📂', 'Kategorien', 'Kategoriestruktur inkl. Eltern-Kind-Hierarchie, Icons, Min-Rolle' ],
[ '👥', 'Benutzer', 'Accounts inkl. Passwort-Hashes, Ban-Status, Profilfeld-Werte. ID-Remapping bei Import.' ],
[ '💬', 'Threads & Posts', 'Alle Inhalte inkl. Tag-Zuordnungen & Abonnements. User-IDs werden automatisch gemappt.' ],
[ '❤️', 'Likes & Reaktionen', 'Likes + Emoji-Reaktionen + Benachrichtigungen. User-IDs werden gemappt.' ],
[ '✉️', 'Privatnachrichten', 'Alle DM-Konversationen. from_id / to_id werden gemappt.' ],
[ '📊', 'Umfragen', 'Alle Umfragen inkl. Abstimmungen. User-IDs werden gemappt.' ],
[ '🔖', 'Lesezeichen', 'Alle gespeicherten Thread-Lesezeichen.' ],
[ '🏷️', 'Thread-Präfixe', 'Alle Präfix-Labels, Farben und Reihenfolgen.' ],
[ '🚫', 'Ignore-Liste', 'Alle gegenseitigen Nutzer-Blockierungen.' ],
[ '🚩', 'Meldungen', 'Gemeldete Beiträge inkl. Status (offen/erledigt/verworfen).' ],
[ '📨', 'Einladungen', 'Alle Einladungscodes inkl. Nutzungsanzahl & Ablaufdatum.' ],
];
foreach ( $info_items as [ $ico, $label, $desc ] ) : ?>
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px 12px">
<div style="font-weight:700;font-size:.83rem;color:#1e293b;margin-bottom:3px"><?php echo $ico; ?> <?php echo esc_html( $label ); ?></div>
<div style="font-size:.76rem;color:#94a3b8;line-height:1.4"><?php echo esc_html( $desc ); ?></div>
</div>
<?php endforeach; ?>
</div>
<!-- Hinweis zu großen Importen -->
<div style="margin-top:14px;padding:10px 14px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;font-size:.8rem;color:#64748b">
<strong style="color:#374151">💡 Hinweise:</strong>
Bei sehr großen Backups (10 MB+) das PHP-Limit <code>upload_max_filesize</code> prüfen.
Nach dem Import werden Beitrags- und Thread-Zähler automatisch neu berechnet.
Superadmin-Konten können nicht per Import überschrieben werden.
</div>
</div>
<?php
}
// ═══════════════════════════════════════════════════════════════
// HILFSFUNKTIONEN
// ═══════════════════════════════════════════════════════════════
/** Stat-Neuberechnung nach dem Import */
private static function recalculate_stats() {
global $wpdb;
// post_count pro User
$wpdb->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( $wpdb->prepare( "SHOW TABLES LIKE %s", $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' ],
];
}
}