Add data export/import under settings
This commit is contained in:
@@ -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 {
|
||||
</table>
|
||||
<?php submit_button( __( 'Einstellungen speichern', KGVVM_TEXT_DOMAIN ) ); ?>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
<h2><?php echo esc_html__( 'Datensicherung', KGVVM_TEXT_DOMAIN ); ?></h2>
|
||||
|
||||
<div class='kgvvm-grid'>
|
||||
<div class='kgvvm-card'>
|
||||
<h3><?php echo esc_html__( 'Export', KGVVM_TEXT_DOMAIN ); ?></h3>
|
||||
<p><?php echo esc_html__( 'Exportiert alle Plugin-Tabellen (Sparten, Parzellen, Zähler, Verbrauch, Kosten, Arbeitsstunden usw.) als JSON-Datei.', KGVVM_TEXT_DOMAIN ); ?></p>
|
||||
<a href='<?php echo esc_url( wp_nonce_url( $this->admin_url( 'kgvvm-settings', array( 'kgvvm_action' => 'export_plugin_data' ) ), 'kgvvm_export_plugin_data' ) ); ?>'
|
||||
class='button button-secondary'>
|
||||
<?php echo esc_html__( 'JSON-Export herunterladen', KGVVM_TEXT_DOMAIN ); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class='kgvvm-card'>
|
||||
<h3><?php echo esc_html__( 'Import', KGVVM_TEXT_DOMAIN ); ?></h3>
|
||||
<p class='notice notice-warning inline' style='margin:0 0 10px;padding:6px 10px;'>
|
||||
<?php echo esc_html__( 'Achtung: Der Import überschreibt alle vorhandenen Plugin-Daten vollständig. Bitte vorher einen Export erstellen.', KGVVM_TEXT_DOMAIN ); ?>
|
||||
</p>
|
||||
<form method='post' enctype='multipart/form-data'>
|
||||
<?php wp_nonce_field( 'kgvvm_import_plugin_data' ); ?>
|
||||
<input type='hidden' name='kgvvm_action' value='import_plugin_data' />
|
||||
<input type='file' name='kgvvm_import_file' accept='.json,application/json' required style='margin-bottom:8px;display:block;' />
|
||||
<?php submit_button( __( 'Import starten', KGVVM_TEXT_DOMAIN ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
195
includes/DataTransfer.php
Normal file
195
includes/DataTransfer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user