PluginProbe
Maps Plugin using Google Maps for WordPress – WP Google Map / trunk
Maps Plugin using Google Maps for WordPress – WP Google Map vtrunk
1.9.7 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 All 96 releases
gmap-embed / includes / traits / ImportExport.php

ImportExport.php in Maps Plugin using Google Maps for WordPress – WP Google Map trunk, at includes/traits/ImportExport.php

761 lines 29.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WGMSRM\Traits;
3
4 defined( 'ABSPATH' ) || exit;
5
6 trait ImportExport
7 {
8 /**
9 * Register import/export hooks
10 */
11 public function register_import_export_hooks()
12 {
13 add_action('admin_post_wgm_export', array($this, 'wgm_export_data'));
14 add_action('admin_post_wgm_import', array($this, 'wgm_handle_import'));
15
16 // Preview import (AJAX)
17 add_action('wp_ajax_wgm_import_preview', array($this, 'wgm_import_preview'));
18 add_action('wp_ajax_wgm_import', array($this, 'wgm_handle_import'));
19 }
20
21 /**
22 * Export data handler
23 */
24 public function wgm_export_data()
25 {
26 // Nonce validation
27 check_admin_referer('wgm_export_nonce', '_wgm_export_nonce');
28
29 if (!current_user_can($this->capability)) {
30 wp_die(esc_html__('You do not have permission to export data.', 'gmap-embed'));
31 }
32
33 $export_type = isset($_POST['wgm_export_type']) ? sanitize_text_field(wp_unslash($_POST['wgm_export_type'])) : 'json';
34 $map_ids = [];
35 if (isset($_POST['wgm_maps'])) {
36 $raw_maps = map_deep(wp_unslash($_POST['wgm_maps']), 'sanitize_text_field');
37 if (is_array($raw_maps)) {
38 $map_ids = array_map('intval', $raw_maps);
39 } else {
40 $map_ids[] = intval($raw_maps);
41 }
42 }
43
44 // Initial export structure
45 $export_data = [
46 'creator' => 'WPGoogleMap',
47 'plugin_version' => WGM_PLUGIN_VERSION,
48 'json_version' => '1.1',
49 'maps' => [],
50 'markers' => [],
51 'categories' => [],
52 ];
53
54 // 1. Always fetch Map data for selected IDs
55 $maps = $this->get_all_maps($map_ids);
56 $export_data['maps'] = $maps ? $maps : [];
57
58 // 2. Fetch ALL markers for these maps (needed for category calculation even if not exporting)
59 $map_ids_for_query = array_filter(array_column($export_data['maps'], 'id'));
60 $related_markers = [];
61 if (!empty($map_ids_for_query)) {
62 global $wpdb;
63 $ids_placeholders = implode(',', array_fill(0, count($map_ids_for_query), '%d'));
64 // Building query string with placeholders for IN clause
65 $markers_query = "SELECT id, map_id, marker_name, marker_desc, icon, address, lat_lng,
66 have_marker_link, marker_link, marker_link_new_tab, animation, category_id, show_desc_by_default
67 FROM {$wpdb->prefix}wgm_markers WHERE map_id IN ($ids_placeholders)";
68
69 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
70 $related_markers = $wpdb->get_results(
71 $wpdb->prepare(
72 $markers_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
73 $map_ids_for_query
74 ),
75 ARRAY_A
76 );
77 }
78
79
80 // Determine selected types from UI based on format
81 $selected_types = [];
82 if ($export_type === 'json') {
83 $selected_types = isset($_POST['wgm_export_data_types']) ? array_map('sanitize_text_field', wp_unslash($_POST['wgm_export_data_types'])) : ['markers', 'categories'];
84 } elseif ($export_type === 'csv') {
85 // For CSV, we only allow one type, but we map it to our internal array structure
86 $csv_type = isset($_POST['wgm_export_data_type']) ? sanitize_text_field(wp_unslash($_POST['wgm_export_data_type'])) : 'markers';
87 $selected_types = [$csv_type];
88 }
89
90 // 3. Add Markers to export if selected
91 if (in_array('markers', $selected_types)) {
92 $export_data['markers'] = $related_markers ? $related_markers : [];
93 }
94
95 // 4. Calculate and Add Categories only if selected
96 if (in_array('categories', $selected_types) && !empty($related_markers)) {
97 // Collect used category IDs
98 $raw_cat_ids = array_column($related_markers, 'category_id');
99 $used_cat_ids = [];
100 foreach ($raw_cat_ids as $val) {
101 if (empty($val)) continue;
102 // Handle multiple categories (comma separated)
103 $parts = explode(',', $val);
104 foreach ($parts as $p) {
105 $p = intval(trim($p));
106 if ($p > 0) {
107 $used_cat_ids[] = $p;
108 }
109 }
110 }
111 $used_cat_ids = array_unique($used_cat_ids);
112
113 if (!empty($used_cat_ids)) {
114 global $wpdb;
115 $categories_table = $wpdb->prefix . 'wgm_categories';
116
117 // Recursive fetch for parents
118 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
119 $all_cats_pool = $wpdb->get_results("SELECT * FROM {$categories_table}", ARRAY_A);
120
121 // Index by ID for easier lookup
122 $cat_lookup = [];
123 foreach ($all_cats_pool as $c) {
124 $cat_lookup[intval($c['id'])] = $c;
125 }
126
127 $final_export_cats = [];
128 $processed_ids = [];
129 $queue = $used_cat_ids;
130
131 while (!empty($queue)) {
132 $cid = array_pop($queue);
133 $cid = intval($cid);
134
135 if (isset($processed_ids[$cid])) continue;
136 if (!isset($cat_lookup[$cid])) continue;
137
138 $processed_ids[$cid] = true;
139 $cat_obj = $cat_lookup[$cid];
140 $final_export_cats[] = $cat_obj;
141
142 // Add parent to queue
143 if (!empty($cat_obj['parent_id'])) {
144 $queue[] = intval($cat_obj['parent_id']);
145 }
146 }
147
148 $export_data['categories'] = $final_export_cats;
149 }
150 }
151
152 if ($export_type === 'json') {
153 header('Content-Disposition: attachment; filename="gmap-export.json"');
154 header('Content-Type: application/json; charset=utf-8');
155 echo wp_json_encode($export_data, JSON_PRETTY_PRINT);
156 exit;
157 } elseif ($export_type === 'csv') {
158 $export_data_type_csv = isset($_POST['wgm_export_data_type']) ? sanitize_text_field(wp_unslash($_POST['wgm_export_data_type'])) : '';
159 // CSV logic remains largely similar, just pulling from our new filtered dataset
160 // Note: CSV export in this plugin generally implies flat single-type export.
161 // Maps or Markers are the options in UI.
162
163 $data_csv = [];
164 if ($export_data_type_csv === 'maps') {
165 $data_csv = $export_data['maps'];
166 } elseif ($export_data_type_csv === 'markers') {
167 $data_csv = $export_data['markers'];
168 } elseif ($export_data_type_csv === 'categories') {
169 // Use the processed categories (which includes dependencies)
170 $data_csv = $export_data['categories'];
171 }
172
173 header('Content-Type: text/csv');
174 header('Content-Disposition: attachment; filename="gmap-export.csv"');
175
176 if (!empty($data_csv)) {
177 // Get headers from first row
178 $all_headers = array_keys(reset($data_csv));
179 // Filter out timestamps
180 $headers = array_filter($all_headers, function($h) {
181 return !in_array($h, ['created_at', 'updated_at']);
182 });
183
184 $csv_content = '';
185
186 // Helper for manual CSV generation
187 $to_csv_row = function($data) {
188 foreach ($data as &$val) {
189 $val = '"' . str_replace('"', '""', $val) . '"';
190 }
191 return implode(',', $data) . "\n";
192 };
193
194 $csv_content .= $to_csv_row($headers);
195 foreach ($data_csv as $row) {
196 $filtered_row = [];
197 foreach($headers as $h) {
198 $filtered_row[] = isset($row[$h]) ? $row[$h] : '';
199 }
200 $csv_content .= $to_csv_row($filtered_row);
201 }
202 echo $csv_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
203 }
204 exit;
205 }
206 }
207
208 /**
209 * Import data handler
210 */
211 public function wgm_handle_import()
212 {
213 $redirect_with_error = function ($code, $message = '') {
214 $args = array('wgm_import' => 'error', 'error_code' => $code);
215 if (!empty($message)) {
216 $args['error_msg'] = rawurlencode(sanitize_text_field(wp_strip_all_tags($message)));
217 }
218
219 if (defined('DOING_AJAX') && DOING_AJAX) {
220 wp_send_json_error(array('message' => $message ?: $code));
221 }
222
223 wp_safe_redirect(add_query_arg($args, wp_get_referer() ?: admin_url('admin.php?page=wpgmapembed-settings')));
224 exit;
225 };
226
227 $nonce = isset($_POST['_wgm_import_nonce']) ? sanitize_text_field(wp_unslash($_POST['_wgm_import_nonce'])) : '';
228 if (empty($nonce) || !wp_verify_nonce($nonce, 'wgm_import_nonce')) {
229 $redirect_with_error('invalid_nonce', 'Invalid import request.');
230 }
231
232 if (!current_user_can($this->capability)) {
233 $redirect_with_error('no_permission', 'You do not have permission to import data.');
234 }
235
236 if (
237 empty($_FILES['wgm_import_file']) ||
238 !isset($_FILES['wgm_import_file']['error']) ||
239 $_FILES['wgm_import_file']['error'] !== UPLOAD_ERR_OK ||
240 !isset($_FILES['wgm_import_file']['tmp_name']) ||
241 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Path sanitized below/validated.
242 !is_uploaded_file($_FILES['wgm_import_file']['tmp_name'])
243 ) {
244 $redirect_with_error('no_file', 'No import file uploaded or upload error.');
245 }
246
247 if (isset($_FILES['wgm_import_file']['size']) && $_FILES['wgm_import_file']['size'] > $this->max_import_size) {
248 $redirect_with_error('file_too_large', 'Uploaded file exceeds maximum allowed size.');
249 }
250
251 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
252 $uploaded = $_FILES['wgm_import_file'];
253 $ext = strtolower(pathinfo($uploaded['name'], PATHINFO_EXTENSION));
254
255 // Fallback for extension check if needed
256 if ($ext !== 'json' && $ext !== 'csv') {
257 $redirect_with_error('invalid_file_type', 'Unsupported file type. Only JSON and CSV are allowed.');
258 }
259
260 $import_type_ui = isset($_POST['wgm_import_type']) ? sanitize_text_field(wp_unslash($_POST['wgm_import_type'])) : 'json';
261 // If UI says json but file is csv (or vice versa), trust file extension or UI? Usually trust extension for processing logic.
262 $process_type = ($ext === 'json') ? 'json' : 'csv';
263
264 $import_mode = isset($_POST['wgm_import_mode']) ? sanitize_text_field(wp_unslash($_POST['wgm_import_mode'])) : 'merge';
265 $import_target = isset($_POST['wgm_import_target']) ? sanitize_text_field(wp_unslash($_POST['wgm_import_target'])) : 'new';
266 $target_map_id = isset($_POST['wgm_target_map_id']) ? intval(wp_unslash($_POST['wgm_target_map_id'])) : 0;
267 $dry_run = isset($_POST['wgm_import_dry_run']) && sanitize_text_field(wp_unslash($_POST['wgm_import_dry_run'])) === '1';
268
269 require_once ABSPATH . 'wp-admin/includes/file.php';
270 WP_Filesystem();
271 global $wp_filesystem;
272
273 $file_contents = $wp_filesystem->get_contents($uploaded['tmp_name']);
274 if ($file_contents === false) {
275 $redirect_with_error('read_error', 'Failed to read uploaded file.');
276 }
277
278 global $wpdb;
279 $markers_table = "{$wpdb->prefix}wgm_markers";
280 $categories_table = "{$wpdb->prefix}wgm_categories";
281
282 // Helper to sanitize meta
283 $sanitize_meta = function ($key, $value) {
284 if (is_array($value) || is_object($value)) {
285 return wp_json_encode($value);
286 }
287 if ($key === 'marker_desc' || $key === 'wgm_marker_desc') {
288 return wp_kses_post((string) $value);
289 }
290 if ($key === 'wgm_theme_json') {
291 return (string) $value;
292 }
293 return sanitize_text_field((string) $value);
294 };
295
296 // ID Mappings
297 $map_id_map = []; // old_id => new_id
298 $cat_id_map = []; // old_id => new_id
299
300 // --------------------- DELETE LOGIC ---------------------
301 if (!$dry_run && $import_mode === 'replace') {
302 $data_type = isset($_POST['wgm_import_data_type']) ? sanitize_text_field(wp_unslash($_POST['wgm_import_data_type'])) : 'markers';
303
304 if ($process_type === 'csv' && $data_type === 'categories') {
305 // selective category wipe
306 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
307 $wpdb->query("TRUNCATE TABLE {$categories_table}");
308 } elseif ($import_target === 'new') {
309 // Wipe EVERYTHING (Standard for JSON or Marker->New Map batch)
310 $existing_maps = get_posts(array('post_type' => 'wpgmapembed', 'numberposts' => -1, 'post_status' => 'any'));
311 foreach ($existing_maps as $p) wp_delete_post($p->ID, true);
312 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
313 $wpdb->query("TRUNCATE TABLE {$markers_table}");
314 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
315 $wpdb->query("TRUNCATE TABLE {$categories_table}");
316 } elseif ($import_target === 'existing' && $target_map_id) {
317 // Just wipe markers for this map
318 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
319 $wpdb->query($wpdb->prepare("DELETE FROM {$markers_table} WHERE map_id = %d", intval($target_map_id)));
320 }
321 }
322
323 // Existing category lookup (by name) - MUST BE AFTER WIPE
324 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
325 $existing_cats = $wpdb->get_results("SELECT id, name FROM {$categories_table}", ARRAY_A);
326 $cat_name_to_id = [];
327 foreach ($existing_cats as $ecat) {
328 $cat_name_to_id[strtolower($ecat['name'])] = intval($ecat['id']);
329 }
330
331 // --------------------- JSON PROCESSING ---------------------
332 if ($process_type === 'json') {
333 // Remove UTF-8 BOM if present
334 $bom = pack('H*', 'EFBBBF');
335 if (substr($file_contents, 0, 3) === $bom) {
336 $file_contents = substr($file_contents, 3);
337 }
338 $payload = json_decode($file_contents, true);
339 if (!is_array($payload)) $redirect_with_error('invalid_json', 'Invalid JSON file.');
340
341 $maps = isset($payload['maps']) ? $payload['maps'] : [];
342 $markers = isset($payload['markers']) ? $payload['markers'] : [];
343 $categories = isset($payload['categories']) ? $payload['categories'] : [];
344
345 // 1. Import Categories
346 $created_cats = 0;
347 $matched_cats = 0;
348 if (!empty($categories)) {
349 foreach ($categories as $cat) {
350 $old_id = isset($cat['id']) ? intval($cat['id']) : 0;
351 // remove id to auto-increment, remove dates to use current
352 unset($cat['id'], $cat['created_at'], $cat['updated_at']);
353
354 // Sanitize
355 $cat['name'] = sanitize_text_field($cat['name']);
356 $cat['icon'] = sanitize_text_field($cat['icon']);
357
358 // Check for existing category by name
359 $lower_name = strtolower($cat['name']);
360 if (isset($cat_name_to_id[$lower_name])) {
361 if ($old_id) $cat_id_map[$old_id] = $cat_name_to_id[$lower_name];
362 $matched_cats++;
363 continue;
364 }
365
366 // Parent ID logic is tricky if IDs change.
367 // Simplified: Reset parent to 0 for now to avoid broken trees, or try to map if parent imported first.
368 // Ideally we should do a second pass potential parents, but for now lets default 0 if mapping not found.
369 $cat['parent_id'] = 0;
370
371 if (!$dry_run) {
372 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
373 $wpdb->insert($categories_table, $cat);
374 $new_id = $wpdb->insert_id;
375 if ($old_id) $cat_id_map[$old_id] = $new_id;
376 // Add to local cache to prevent duplicates in same file
377 $cat_name_to_id[strtolower($cat['name'])] = $new_id;
378 }
379 $created_cats++;
380 }
381
382 // Fix parents if possible (if we had improved logic we'd do a second pass update here using $cat_id_map)
383 }
384
385 // 2. Import Maps
386 $created_maps = 0;
387 foreach ($maps as $map) {
388 $orig_id = isset($map['id']) ? intval($map['id']) : 0;
389 $title = isset($map['wpgmap_title']) ? sanitize_text_field($map['wpgmap_title']) : __('Imported Map', 'gmap-embed');
390
391 // Skip map creation if importing into existing target
392 if ($import_target === 'existing' && $target_map_id) {
393 if ($orig_id) $map_id_map[$orig_id] = $target_map_id;
394 // Optionally update options of the existing map? For now, assume we just want markers.
395 continue;
396 }
397
398 if ($dry_run) {
399 $map_id_map[$orig_id] = -($orig_id ?: ++$created_maps);
400 $created_maps++;
401 continue;
402 }
403
404 $new_id = $this->initiate_new_map($title);
405
406 if ($new_id && !is_wp_error($new_id)) {
407 $map_id_map[$orig_id] = $new_id;
408 // Import Meta
409 foreach ($map as $key => $val) {
410 if ($key === 'id') continue;
411 update_post_meta($new_id, $key, $sanitize_meta($key, $val));
412 }
413 $created_maps++;
414 }
415 }
416
417 // 2b. Map Fallback for Markers
418 // If we are merging into an existing map but valid JSON didn't have map objects, map 0 => target
419 if ($import_target === 'existing' && $target_map_id && empty($maps)) {
420 $map_id_map[0] = $target_map_id;
421 }
422
423 // 3. Import Markers
424 $inserted_markers = 0;
425 foreach ($markers as $marker) {
426 // Resolve Map ID
427 $orig_map = isset($marker['map_id']) ? intval($marker['map_id']) : 0;
428 $new_map = isset($map_id_map[$orig_map]) ? $map_id_map[$orig_map] : 0;
429
430 if ($import_target === 'existing' && $target_map_id) {
431 $new_map = $target_map_id;
432 }
433
434 // If we have no valid map destination, skip or create default?
435 // If new_map is 0 and we are in new mode, we need a map.
436 // Logic: create one catch-all map if needed? For now, skip orphans.
437 if (!$new_map && !$dry_run) continue;
438
439 // Resolve Category ID
440 $orig_cat = isset($marker['category_id']) ? intval($marker['category_id']) : 0;
441 $new_cat = isset($cat_id_map[$orig_cat]) ? $cat_id_map[$orig_cat] : 0;
442
443 $insert = array(
444 'map_id' => $new_map,
445 'marker_name' => isset($marker['marker_name']) ? sanitize_text_field($marker['marker_name']) : '',
446 'marker_desc' => isset($marker['marker_desc']) ? $sanitize_meta('marker_desc', $marker['marker_desc']) : '',
447 'icon' => isset($marker['icon']) ? sanitize_text_field($marker['icon']) : '',
448 'address' => isset($marker['address']) ? sanitize_text_field($marker['address']) : '',
449 'lat_lng' => isset($marker['lat_lng']) ? sanitize_text_field($marker['lat_lng']) : '',
450 'have_marker_link' => isset($marker['have_marker_link']) ? sanitize_text_field($marker['have_marker_link']) : '',
451 'marker_link' => isset($marker['marker_link']) ? sanitize_text_field($marker['marker_link']) : '',
452 'marker_link_new_tab' => isset($marker['marker_link_new_tab']) ? intval($marker['marker_link_new_tab']) : 0,
453 'show_desc_by_default' => isset($marker['show_desc_by_default']) ? intval($marker['show_desc_by_default']) : 0,
454 'category_id' => $new_cat
455 );
456
457 if (!$dry_run) {
458 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
459 $wpdb->insert($markers_table, $insert);
460 }
461 $inserted_markers++;
462 }
463
464 // Success Redirect
465 if ($dry_run) {
466 if (defined('DOING_AJAX') && DOING_AJAX) {
467 wp_send_json_success(array(
468 'type' => 'json',
469 'maps' => $created_maps,
470 'markers' => $inserted_markers,
471 'categories' => $created_cats + $matched_cats
472 ));
473 }
474 $query_args = array('wgm_import' => 'dryrun', 'maps_simulated' => $created_maps, 'markers_simulated' => $inserted_markers);
475 } else {
476 $query_args = array('wgm_import' => 'success');
477 }
478 wp_safe_redirect(add_query_arg($query_args, wp_get_referer() ?: admin_url('admin.php')));
479 exit;
480 }
481
482 // --------------------- CSV PROCESSING ---------------------
483 if ($process_type === 'csv') {
484 $data_type = isset($_POST['wgm_import_data_type']) ? sanitize_text_field(wp_unslash($_POST['wgm_import_data_type'])) : 'markers';
485
486 global $wp_filesystem;
487 WP_Filesystem();
488 $csv_content = $wp_filesystem->get_contents($uploaded['tmp_name']);
489
490 if (empty($csv_content)) {
491 wp_safe_redirect(add_query_arg(array('wgm_import' => 'error', 'error_code' => 'file_open_failed'), wp_get_referer()));
492 exit;
493 }
494
495 $csv_rows = str_getcsv($csv_content, "\n");
496 $header_row = array_shift($csv_rows);
497 $header = $header_row ? str_getcsv($header_row) : [];
498
499 if (!$header) {
500 wp_safe_redirect(add_query_arg(array('wgm_import' => 'error', 'error_code' => 'empty_csv'), wp_get_referer()));
501 exit;
502 }
503
504 // Map Mapping Logic
505 $mapping = [];
506 if (!empty($_POST['wgm_import_mapping'])) {
507 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized recursively using map_deep below.
508 $mapping = json_decode(wp_unslash($_POST['wgm_import_mapping']), true);
509 $mapping = map_deep($mapping, 'sanitize_text_field');
510 }
511
512 if ($data_type === 'categories') {
513 // Pass 1: Insert Categories (ignore parents initially)
514 $created_cats = 0;
515 $temp_parent_map = []; // new_id => old_parent_id
516
517 foreach ($csv_rows as $row_str) {
518 if (empty(trim($row_str))) continue;
519 $row = str_getcsv($row_str);
520
521 if (count($row) !== count($header)) continue;
522 $item = array_combine($header, $row);
523
524 // Apply mapping
525 if (!empty($mapping)) {
526 $mapped_item = [];
527 foreach ($item as $k => $v) {
528 $key = isset($mapping[$k]) && $mapping[$k] ? $mapping[$k] : $k;
529 $mapped_item[$key] = $v;
530 }
531 $item = $mapped_item;
532 }
533
534 $old_id = isset($item['id']) ? intval($item['id']) : 0;
535 $old_parent_id = isset($item['parent_id']) ? intval($item['parent_id']) : 0;
536 $cat_name = isset($item['name']) ? sanitize_text_field($item['name']) : 'Imported Category';
537
538 // Check for existing category by name
539 $lower_name = strtolower($cat_name);
540 if (isset($cat_name_to_id[$lower_name])) {
541 if ($old_id) $cat_id_map[$old_id] = $cat_name_to_id[$lower_name];
542 continue;
543 }
544
545 if (!$dry_run) {
546 $insert = [
547 'name' => $cat_name,
548 'icon' => isset($item['icon']) ? sanitize_text_field($item['icon']) : '',
549 'parent_id' => 0, // Set to 0 initially
550 'created_at' => current_time('mysql'),
551 'updated_at' => current_time('mysql'),
552 ];
553
554 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
555 $wpdb->insert($categories_table, $insert);
556 $new_id = $wpdb->insert_id;
557
558 if ($old_id) {
559 $cat_id_map[$old_id] = $new_id;
560 }
561
562 // Add to local cache
563 $cat_name_to_id[$lower_name] = $new_id;
564
565 // Store parent ref for Pass 2 if it exists
566 if ($old_parent_id) {
567 $temp_parent_map[$new_id] = $old_parent_id;
568 }
569 }
570 $created_cats++;
571 }
572
573 // Pass 2: Update Parents
574 if (!$dry_run && !empty($temp_parent_map)) {
575 foreach ($temp_parent_map as $child_new_id => $old_parent_id) {
576 if (isset($cat_id_map[$old_parent_id])) {
577 $new_parent_id = $cat_id_map[$old_parent_id];
578 // Update DB
579 // Update DB
580 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
581 $wpdb->update(
582 $categories_table,
583 ['parent_id' => $new_parent_id],
584 ['id' => $child_new_id],
585 ['%d'],
586 ['%d']
587 );
588 }
589 }
590 }
591
592 $status = $dry_run ? 'dryrun' : 'success';
593 if ($dry_run && defined('DOING_AJAX') && DOING_AJAX) {
594 wp_send_json_success(array(
595 'type' => 'categories',
596 'count' => $created_cats
597 ));
598 }
599 wp_safe_redirect(add_query_arg(array('wgm_import' => $status, 'type' => 'categories', 'count' => $created_cats), wp_get_referer()));
600 exit;
601 } elseif ($data_type === 'markers') {
602 $inserted = 0;
603 foreach ($csv_rows as $row_str) {
604 if (empty(trim($row_str))) continue;
605 $row = str_getcsv($row_str);
606
607 if (count($row) !== count($header)) continue;
608 $item = array_combine($header, $row);
609
610 // Apply mapping
611 if ($mapping) {
612 $newItem = [];
613 foreach ($item as $k => $v) {
614 $key = isset($mapping[$k]) && $mapping[$k] ? $mapping[$k] : $k;
615 $newItem[$key] = $v;
616 }
617 $item = $newItem;
618 }
619
620 // Resolve Map
621 $target = $target_map_id; // Default to selected
622 if ($import_target === 'new') {
623 static $new_csv_map_id = 0;
624 if (!$new_csv_map_id && !$dry_run) {
625 $new_csv_map_id = $this->initiate_new_map(__('Imported Map (CSV)', 'gmap-embed'));
626 }
627 $target = $new_csv_map_id;
628 }
629
630 $cat_id = isset($item['category_id']) ? intval($item['category_id']) : 0;
631
632 if (!$dry_run && $target) {
633 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
634 $wpdb->insert($markers_table, array(
635 'map_id' => $target,
636 'marker_name' => isset($item['marker_name']) ? sanitize_text_field($item['marker_name']) : '',
637 'lat_lng' => isset($item['lat_lng']) ? sanitize_text_field($item['lat_lng']) : '',
638 'address' => isset($item['address']) ? sanitize_text_field($item['address']) : '',
639 'category_id' => $cat_id,
640 'icon' => isset($item['icon']) ? sanitize_text_field($item['icon']) : '',
641 'animation' => isset($item['animation']) ? sanitize_text_field($item['animation']) : '',
642 'have_marker_link' => isset($item['have_marker_link']) ? sanitize_text_field($item['have_marker_link']) : '0',
643 'marker_link' => isset($item['marker_link']) ? sanitize_text_field($item['marker_link']) : '',
644 'marker_link_new_tab' => isset($item['marker_link_new_tab']) ? intval($item['marker_link_new_tab']) : 0,
645 'show_desc_by_default' => isset($item['show_desc_by_default']) ? intval($item['show_desc_by_default']) : 0,
646 'marker_desc' => isset($item['marker_desc']) ? $sanitize_meta('marker_desc', $item['marker_desc']) : ''
647 ));
648 }
649 $inserted++;
650 }
651 // Redirect...
652 $status = $dry_run ? 'dryrun' : 'success';
653 if ($dry_run && defined('DOING_AJAX') && DOING_AJAX) {
654 wp_send_json_success(array(
655 'type' => 'markers',
656 'count' => $inserted
657 ));
658 }
659 wp_safe_redirect(add_query_arg(array('wgm_import' => $status, 'type' => 'markers', 'count' => $inserted), wp_get_referer()));
660 exit;
661 }
662 }
663 }
664
665 /**
666 * Preview Import Handler
667 * Returns a small sample preview of uploaded content (CSV columns or JSON counts)
668 */
669 public function wgm_import_preview() {
670 // Nonce validation
671 $nonce = isset($_POST['_wgm_import_nonce']) ? sanitize_text_field(wp_unslash($_POST['_wgm_import_nonce'])) : '';
672 /* phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- verified via wp_verify_nonce below */
673 if (empty($nonce) || !wp_verify_nonce($nonce, 'wgm_import_nonce')) {
674 wp_send_json_error(['message' => __('Invalid security nonce. Please reload the page.', 'gmap-embed')]);
675 }
676
677 if (!current_user_can($this->capability)) {
678 wp_send_json_error(['message' => __('You do not have permission to preview imports.', 'gmap-embed')]);
679 }
680
681 $type = isset($_POST['preview_type']) ? sanitize_text_field(wp_unslash($_POST['preview_type'])) : 'json';
682 $content = isset($_POST['content']) ? wp_unslash($_POST['content']) : '';
683 $max_rows = isset($_POST['max_rows']) ? intval($_POST['max_rows']) : 5;
684
685 if (empty($content)) {
686 wp_send_json_error(['message' => __('No content detected in the file.', 'gmap-embed')]);
687 }
688
689 if ($type === 'csv') {
690 $rows_all = str_getcsv($content, "\n");
691 $header_row = array_shift($rows_all);
692 $all_columns = $header_row ? str_getcsv($header_row) : [];
693
694 if (!$all_columns) {
695 wp_send_json_error(['message' => __('Failed to parse CSV header.', 'gmap-embed')]);
696 }
697
698 // Filter out columns we don't want to show (timestamps)
699 $columns = array_values(array_filter($all_columns, function($c) {
700 return !in_array($c, ['created_at', 'updated_at']);
701 }));
702
703 if (empty($columns)) {
704 wp_send_json_error(['message' => __('Could not find valid columns in the CSV header.', 'gmap-embed')]);
705 }
706
707 $rows = [];
708 $total_rows = 0;
709 foreach ($rows_all as $row_str) {
710 if (empty(trim($row_str))) continue;
711 $row_data = str_getcsv($row_str);
712
713 if (count($row_data) === count($all_columns)) {
714 $total_rows++;
715 // Only add to rows array if we haven't reached max_rows
716 if (count($rows) < $max_rows) {
717 $combined = array_combine($all_columns, $row_data);
718 $filtered_row = [];
719 foreach($columns as $c) {
720 $filtered_row[$c] = isset($combined[$c]) ? $combined[$c] : '';
721 }
722 $rows[] = $filtered_row;
723 }
724 }
725 }
726
727 wp_send_json_success([
728 'type' => 'csv',
729 'columns' => $columns,
730 'rows' => $rows,
731 'total_rows' => $total_rows
732 ]);
733 } elseif ($type === 'json') {
734 // Remove UTF-8 BOM if present
735 $bom = pack('H*', 'EFBBBF');
736 if (substr($content, 0, 3) === $bom) {
737 $content = substr($content, 3);
738 }
739
740 // JSON preview might fail if content is truncated (chunked)
741 $data = json_decode($content, true);
742 if (json_last_error() !== JSON_ERROR_NONE) {
743 // If it failed and content is large, it's likely truncated
744 if (strlen($content) >= 190000) {
745 wp_send_json_error(['message' => __('The JSON file is too large to preview (truncated), but you can still attempt the import.', 'gmap-embed')]);
746 }
747 wp_send_json_error(['message' => __('Invalid JSON structure: ', 'gmap-embed') . json_last_error_msg()]);
748 }
749
750 wp_send_json_success([
751 'type' => 'json',
752 'maps' => isset($data['maps']) ? $data['maps'] : [],
753 'markers' => isset($data['markers']) ? $data['markers'] : [],
754 'categories' => isset($data['categories']) ? $data['categories'] : [],
755 ]);
756 }
757
758 wp_send_json_error(['message' => __('Unsupported preview format.', 'gmap-embed')]);
759 }
760 }
761