Add data export/import under settings

This commit is contained in:
2026-04-17 13:29:11 +02:00
parent 6c793ad41a
commit 96969bb0a2
2 changed files with 290 additions and 1 deletions

195
includes/DataTransfer.php Normal file
View File

@@ -0,0 +1,195 @@
<?php
/**
* Export and import all kgvvm plugin data as JSON.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class DataTransfer {
/** @var \wpdb */
private $wpdb;
/**
* Table keys in dependency order (parent before child).
*
* @var string[]
*/
private static $table_keys = array(
'sections',
'parcels',
'tenants',
'parcel_members',
'parcel_tenants',
'meters',
'meter_readings',
'chat_messages',
'cost_years',
'cost_rates',
'cost_entries',
'work_jobs',
'work_year_config',
'work_logs',
'work_log_members',
);
public function __construct( \wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
// -------------------------------------------------------------------------
// Export
// -------------------------------------------------------------------------
/**
* Build the export payload as an associative array.
*
* @return array
*/
public function build_export() {
$payload = array(
'plugin' => 'kgv-verein-manager',
'version' => KGVVM_VERSION,
'exported' => gmdate( 'Y-m-d\TH:i:s\Z' ),
'tables' => array(),
);
foreach ( self::$table_keys as $key ) {
$table = Schema::table( $key );
if ( '' === $table ) {
continue;
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$rows = $this->wpdb->get_results( "SELECT * FROM {$table}", ARRAY_A );
$payload['tables'][ $key ] = is_array( $rows ) ? $rows : array();
}
return $payload;
}
/**
* Send the export as a JSON file download and exit.
*
* @return void
*/
public function send_download() {
$payload = $this->build_export();
$json = wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
$filename = 'kgvvm-export-' . gmdate( 'Y-m-d' ) . '.json';
nocache_headers();
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Content-Length: ' . strlen( $json ) );
echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit;
}
// -------------------------------------------------------------------------
// Import
// -------------------------------------------------------------------------
/**
* Validate and import data from a decoded JSON array.
*
* Returns an array with keys 'imported' (int[]), 'skipped' (string[]), 'errors' (string[]).
*
* @param array $payload Decoded JSON payload.
* @return array
*/
public function import( array $payload ) {
$result = array(
'imported' => array(),
'skipped' => array(),
'errors' => array(),
);
if ( empty( $payload['plugin'] ) || 'kgv-verein-manager' !== $payload['plugin'] ) {
$result['errors'][] = __( 'Die Datei stammt nicht vom KGV Verein Manager Plugin.', KGVVM_TEXT_DOMAIN );
return $result;
}
if ( empty( $payload['tables'] ) || ! is_array( $payload['tables'] ) ) {
$result['errors'][] = __( 'Die Exportdatei enthält keine Tabellendaten.', KGVVM_TEXT_DOMAIN );
return $result;
}
// Wrap everything in a transaction so a partial failure can be rolled back.
$this->wpdb->query( 'SET FOREIGN_KEY_CHECKS = 0' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$this->wpdb->query( 'START TRANSACTION' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
// Truncate in reverse dependency order.
foreach ( array_reverse( self::$table_keys ) as $key ) {
if ( ! isset( $payload['tables'][ $key ] ) ) {
continue;
}
$table = Schema::table( $key );
if ( '' === $table ) {
continue;
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
$this->wpdb->query( "TRUNCATE TABLE {$table}" );
}
// Re-insert in dependency order.
foreach ( self::$table_keys as $key ) {
if ( ! isset( $payload['tables'][ $key ] ) ) {
$result['skipped'][] = $key;
continue;
}
$table = Schema::table( $key );
if ( '' === $table ) {
$result['skipped'][] = $key;
continue;
}
$rows = (array) $payload['tables'][ $key ];
$count = 0;
foreach ( $rows as $row ) {
if ( ! is_array( $row ) || empty( $row ) ) {
continue;
}
// Sanitize keys only allow simple column names.
$clean = array();
foreach ( $row as $col => $val ) {
$col_clean = preg_replace( '/[^a-zA-Z0-9_]/', '', (string) $col );
if ( '' !== $col_clean ) {
$clean[ $col_clean ] = $val;
}
}
$inserted = $this->wpdb->insert( $table, $clean ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
if ( false !== $inserted ) {
$count++;
} else {
$result['errors'][] = sprintf(
/* translators: 1: table key 2: DB error */
__( 'Fehler beim Einfügen in %1$s: %2$s', KGVVM_TEXT_DOMAIN ),
$key,
(string) $this->wpdb->last_error
);
}
}
$result['imported'][ $key ] = $count;
}
if ( empty( $result['errors'] ) ) {
$this->wpdb->query( 'COMMIT' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
} else {
$this->wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
}
$this->wpdb->query( 'SET FOREIGN_KEY_CHECKS = 1' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
return $result;
}
}