1204 lines
63 KiB
PHP
1204 lines
63 KiB
PHP
<?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, banner_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 & 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'] ?? '?' ); ?>
|
||
· 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' ],
|
||
];
|
||
}
|
||
}
|