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 { + +
+

+ +
+
+

+

+ 'export_plugin_data' ) ), 'kgvvm_export_plugin_data' ) ); ?>' + class='button button-secondary'> + + +
+ +
+

+

+ +

+
+ + + + +
+
+
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; + } +}