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' ), 'members' => array(), 'tables' => array(), ); $payload['members'] = $this->export_members(); 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; } /** * Export all member user accounts relevant to the plugin. * * @return array */ private function export_members() { $ids = $this->collect_member_user_ids(); if ( empty( $ids ) ) { return array( 'ids' => array(), 'users' => array(), 'usermeta' => array(), ); } $placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) ); $users_sql = "SELECT ID, user_login, user_pass, user_nicename, user_email, user_url, user_registered, user_activation_key, user_status, display_name FROM {$this->wpdb->users} WHERE ID IN ({$placeholders}) ORDER BY ID ASC"; $meta_sql = "SELECT user_id, meta_key, meta_value FROM {$this->wpdb->usermeta} WHERE user_id IN ({$placeholders}) ORDER BY user_id ASC, umeta_id ASC"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $users = $this->wpdb->get_results( $this->wpdb->prepare( $users_sql, $ids ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $meta = $this->wpdb->get_results( $this->wpdb->prepare( $meta_sql, $ids ), ARRAY_A ); return array( 'ids' => $ids, 'users' => is_array( $users ) ? $users : array(), 'usermeta' => is_array( $meta ) ? $meta : array(), ); } /** * Collect all user IDs relevant for member data export. * * @return int[] */ private function collect_member_user_ids() { $ids = array(); $member_query = new \WP_User_Query( array( 'role' => Roles::MEMBER_ROLE, 'fields' => 'ID', 'number' => 2000, 'orderby' => 'ID', 'order' => 'ASC', ) ); $ids = array_merge( $ids, (array) $member_query->get_results() ); $parcel_members_table = Schema::table( 'parcel_members' ); if ( '' !== $parcel_members_table ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery $ids = array_merge( $ids, (array) $this->wpdb->get_col( "SELECT DISTINCT user_id FROM {$parcel_members_table}" ) ); } $work_log_members_table = Schema::table( 'work_log_members' ); if ( '' !== $work_log_members_table ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery $ids = array_merge( $ids, (array) $this->wpdb->get_col( "SELECT DISTINCT user_id FROM {$work_log_members_table}" ) ); } $ids = array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) ); sort( $ids, SORT_NUMERIC ); return $ids; } /** * 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 if ( ! empty( $payload['members'] ) && is_array( $payload['members'] ) ) { $member_result = $this->import_members( $payload['members'] ); if ( ! empty( $member_result['errors'] ) ) { $result['errors'] = array_merge( $result['errors'], $member_result['errors'] ); $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; } $result['imported']['wp_members'] = isset( $member_result['users'] ) ? (int) $member_result['users'] : 0; $result['imported']['wp_member_meta'] = isset( $member_result['meta'] ) ? (int) $member_result['meta'] : 0; } // 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; } /** * Import member user accounts and their usermeta. * * @param array $members Member payload from export. * @return array */ private function import_members( array $members ) { $result = array( 'users' => 0, 'meta' => 0, 'errors' => array(), ); $users = isset( $members['users'] ) && is_array( $members['users'] ) ? $members['users'] : array(); $meta = isset( $members['usermeta'] ) && is_array( $members['usermeta'] ) ? $members['usermeta'] : array(); if ( empty( $users ) ) { return $result; } $imported_user_ids = array(); foreach ( $users as $row ) { if ( ! is_array( $row ) ) { continue; } $user_id = isset( $row['ID'] ) ? absint( $row['ID'] ) : 0; $login = isset( $row['user_login'] ) ? sanitize_user( (string) $row['user_login'], true ) : ''; $email = isset( $row['user_email'] ) ? sanitize_email( (string) $row['user_email'] ) : ''; if ( $user_id < 1 || '' === $login || '' === $email ) { continue; } $data = array( 'user_login' => $login, 'user_pass' => isset( $row['user_pass'] ) ? (string) $row['user_pass'] : '', 'user_nicename' => isset( $row['user_nicename'] ) ? sanitize_title( (string) $row['user_nicename'] ) : sanitize_title( $login ), 'user_email' => $email, 'user_url' => isset( $row['user_url'] ) ? esc_url_raw( (string) $row['user_url'] ) : '', 'user_registered' => isset( $row['user_registered'] ) ? (string) $row['user_registered'] : current_time( 'mysql', true ), 'user_activation_key' => isset( $row['user_activation_key'] ) ? (string) $row['user_activation_key'] : '', 'user_status' => isset( $row['user_status'] ) ? (int) $row['user_status'] : 0, 'display_name' => isset( $row['display_name'] ) ? sanitize_text_field( (string) $row['display_name'] ) : $login, ); $exists = (int) $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT(*) FROM {$this->wpdb->users} WHERE ID = %d", $user_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( $exists > 0 ) { $updated = $this->wpdb->update( $this->wpdb->users, $data, array( 'ID' => $user_id ) ); if ( false === $updated ) { $result['errors'][] = sprintf( /* translators: %d: user id */ __( 'Mitglied mit ID %d konnte nicht aktualisiert werden.', KGVVM_TEXT_DOMAIN ), $user_id ); continue; } } else { $insert_data = $data; $insert_data['ID'] = $user_id; $inserted = $this->wpdb->insert( $this->wpdb->users, $insert_data ); if ( false === $inserted ) { $result['errors'][] = sprintf( /* translators: 1: user id 2: DB error */ __( 'Mitglied mit ID %1$d konnte nicht angelegt werden: %2$s', KGVVM_TEXT_DOMAIN ), $user_id, (string) $this->wpdb->last_error ); continue; } } $imported_user_ids[] = $user_id; $result['users']++; } $imported_user_ids = array_values( array_unique( array_filter( array_map( 'absint', $imported_user_ids ) ) ) ); if ( empty( $imported_user_ids ) || empty( $meta ) ) { return $result; } $placeholders = implode( ', ', array_fill( 0, count( $imported_user_ids ), '%d' ) ); $delete_sql = "DELETE FROM {$this->wpdb->usermeta} WHERE user_id IN ({$placeholders})"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->wpdb->query( $this->wpdb->prepare( $delete_sql, $imported_user_ids ) ); $allowed_ids = array_flip( $imported_user_ids ); foreach ( $meta as $row ) { if ( ! is_array( $row ) ) { continue; } $user_id = isset( $row['user_id'] ) ? absint( $row['user_id'] ) : 0; if ( $user_id < 1 || ! isset( $allowed_ids[ $user_id ] ) ) { continue; } $meta_key = isset( $row['meta_key'] ) ? (string) $row['meta_key'] : ''; if ( '' === $meta_key ) { continue; } $meta_value = isset( $row['meta_value'] ) ? maybe_serialize( maybe_unserialize( $row['meta_value'] ) ) : ''; $inserted = $this->wpdb->insert( $this->wpdb->usermeta, array( 'user_id' => $user_id, 'meta_key' => $meta_key, 'meta_value' => $meta_value, ) ); if ( false !== $inserted ) { $result['meta']++; } } return $result; } }