diff --git a/includes/Admin/Admin.php b/includes/Admin/Admin.php
index baf87fd..2d3db50 100644
--- a/includes/Admin/Admin.php
+++ b/includes/Admin/Admin.php
@@ -19,6 +19,7 @@ use KGV\VereinManager\Repositories\SectionRepository;
use KGV\VereinManager\Repositories\TenantRepository;
use KGV\VereinManager\Repositories\WorkRepository;
use KGV\VereinManager\Services\ParcelService;
+use KGV\VereinManager\DataTransfer;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -46,6 +47,7 @@ class Admin {
private $costs;
private $work;
private $parcel_service;
+ private $data_transfer;
/**
* Construct admin controller.
@@ -61,7 +63,9 @@ class Admin {
$this->chat = new ChatRepository();
$this->costs = new CostRepository();
$this->work = new WorkRepository();
- $this->parcel_service = new ParcelService();
+ $this->parcel_service = new ParcelService();
+ global $wpdb;
+ $this->data_transfer = new DataTransfer( $wpdb );
}
/**
@@ -242,6 +246,9 @@ class Admin {
case 'save_work_log':
$this->save_work_log();
break;
+ case 'import_plugin_data':
+ $this->import_plugin_data();
+ break;
}
}
@@ -312,6 +319,13 @@ class Admin {
case 'export_readings_csv':
$this->export_readings_csv();
break;
+ case 'export_plugin_data':
+ $this->require_cap( Roles::SETTINGS_CAP );
+ if ( ! wp_verify_nonce( $nonce, 'kgvvm_export_plugin_data' ) ) {
+ $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Export wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ) );
+ }
+ $this->data_transfer->send_download();
+ break;
case 'delete_work_job':
$this->require_cap( 'manage_kleingarten' );
if ( ! wp_verify_nonce( $nonce, 'kgvvm_delete_work_job_' . $id ) ) {
@@ -684,6 +698,59 @@ class Admin {
$this->redirect_with_notice( 'kgvvm-settings', 'success', __( 'Einstellungen wurden gespeichert.', KGVVM_TEXT_DOMAIN ) );
}
+ /**
+ * Handle plugin data import from an uploaded JSON file.
+ *
+ * @return void
+ */
+ private function import_plugin_data() {
+ $this->require_cap( Roles::SETTINGS_CAP );
+ check_admin_referer( 'kgvvm_import_plugin_data' );
+
+ if ( empty( $_FILES['kgvvm_import_file']['tmp_name'] ) ) {
+ $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Bitte eine JSON-Datei auswählen.', KGVVM_TEXT_DOMAIN ) );
+ }
+
+ $file = $_FILES['kgvvm_import_file'];
+
+ // Validate MIME type.
+ $finfo = new \finfo( FILEINFO_MIME_TYPE );
+ $mimetype = $finfo->file( $file['tmp_name'] );
+ if ( ! in_array( $mimetype, array( 'application/json', 'text/plain' ), true ) ) {
+ $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Die hochgeladene Datei ist keine gültige JSON-Datei.', KGVVM_TEXT_DOMAIN ) );
+ }
+
+ $raw = file_get_contents( $file['tmp_name'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+ if ( false === $raw ) {
+ $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Die Datei konnte nicht gelesen werden.', KGVVM_TEXT_DOMAIN ) );
+ }
+
+ $payload = json_decode( $raw, true );
+ if ( ! is_array( $payload ) ) {
+ $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Die JSON-Datei konnte nicht verarbeitet werden. Bitte eine gültige Exportdatei hochladen.', KGVVM_TEXT_DOMAIN ) );
+ }
+
+ $result = $this->data_transfer->import( $payload );
+
+ if ( ! empty( $result['errors'] ) ) {
+ $this->redirect_with_notice( 'kgvvm-settings', 'error', implode( ' | ', $result['errors'] ) );
+ }
+
+ $summary = array();
+ foreach ( $result['imported'] as $key => $count ) {
+ $summary[] = $key . ': ' . $count;
+ }
+ $this->redirect_with_notice(
+ 'kgvvm-settings',
+ 'success',
+ sprintf(
+ /* translators: %s: summary of imported rows per table */
+ __( 'Import erfolgreich. Importierte Datensätze – %s', KGVVM_TEXT_DOMAIN ),
+ implode( ', ', $summary )
+ )
+ );
+ }
+
/**
* Persist one year for the cost dropdown.
*
@@ -3324,6 +3391,33 @@ class Admin {
+
+
+
+
+
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;
+ }
+}