| @@ -8,8 +8,9 @@ | ||
| 8 | 8 | use ImportWP\Common\Filesystem\ZipArchive; |
| 9 | 9 | use ImportWP\Common\Http\Http; |
| 10 | 10 | use ImportWP\Common\Importer\ImporterManager; |
| 11 | 11 | use ImportWP\Common\Importer\Preview\CSVPreview; |
| 12 | +use ImportWP\Common\Importer\Preview\JSONPreview; | |
| 12 | 13 | use ImportWP\Common\Importer\Preview\XMLPreview; |
| 13 | 14 | use ImportWP\Common\Importer\State\ImporterState; |
| 14 | 15 | use ImportWP\Common\Importer\Template\Template; |
| 15 | 16 | use ImportWP\Common\Importer\Template\TemplateManager; |
| @@ -432,8 +433,11 @@ | ||
| 432 | 433 | |
| 433 | 434 | /** |
| 434 | 435 | * Decode map/enabled when sent as a single JSON string (avoids max_input_vars). |
| 435 | 436 | * |
| 437 | + * Do not wp_unslash() first: REST body params are already unslashed, and a | |
| 438 | + * second pass strips JSON escapes inside quoted modifier args. | |
| 439 | + * | |
| 436 | 440 | * @param array $post_data |
| 437 | 441 | * @return array |
| 438 | 442 | */ |
| 439 | 443 | protected function decode_packed_fields($post_data) |
| @@ -442,9 +446,16 @@ | ||
| 442 | 446 | if (!isset($post_data[$field]) || !is_string($post_data[$field])) { |
| 443 | 447 | continue; |
| 444 | 448 | } |
| 445 | 449 | |
| 446 | - $decoded = json_decode(wp_unslash($post_data[$field]), true); | |
| 450 | + $raw = $post_data[$field]; | |
| 451 | + $decoded = json_decode($raw, true); | |
| 452 | + | |
| 453 | + // Fallback for callers that still pass slashed form data. | |
| 454 | + if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) { | |
| 455 | + $decoded = json_decode(wp_unslash($raw), true); | |
| 456 | + } | |
| 457 | + | |
| 447 | 458 | if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { |
| 448 | 459 | $post_data[$field] = $decoded; |
| 449 | 460 | } |
| 450 | 461 | } |
| @@ -626,8 +637,9 @@ | ||
| 626 | 637 | if (isset($post_data['setup_type']) && in_array($post_data['setup_type'], ['upload', 'generate'])) { |
| 627 | 638 | |
| 628 | 639 | $setup_type = $post_data['setup_type'] === 'upload' ? 'upload' : 'generate'; |
| 629 | 640 | $file_type = null; |
| 641 | + $exporter_unique_identifier = ''; | |
| 630 | 642 | |
| 631 | 643 | if ($setup_type === 'upload') { |
| 632 | 644 | $config = json_decode($post_data['exporter_config_file'], true); |
| 633 | 645 | $file_type = $config['data']['file_type']; |
| @@ -633,8 +645,9 @@ | ||
| 633 | 645 | $file_type = $config['data']['file_type']; |
| 634 | 646 | $fields = $config['fields']; |
| 635 | 647 | $formatted_fields = $config['formatted_fields']; |
| 636 | 648 | $file_settings = $config['data']['file_settings']; |
| 649 | + $exporter_unique_identifier = isset($config['data']['unique_identifier']) ? $config['data']['unique_identifier'] : ''; | |
| 637 | 650 | } else { |
| 638 | 651 | |
| 639 | 652 | /** |
| 640 | 653 | * @var \ImportWP\Common\Model\ExporterModel $exporter_data |
| @@ -644,11 +657,12 @@ | ||
| 644 | 657 | $mapper = $this->exporter_manager->get_exporter_mapper($exporter_data); |
| 645 | 658 | $formatted_fields = $mapper->get_fields(); |
| 646 | 659 | $file_type = $exporter_data->getFileType(); |
| 647 | 660 | $file_settings = $exporter_data->getFileSettings(); |
| 661 | + $exporter_unique_identifier = $exporter_data->getUniqueIdentifier(); | |
| 648 | 662 | } |
| 649 | 663 | |
| 650 | - if (is_null($file_type) || !in_array($file_type, ['xml', 'csv'])) { | |
| 664 | + if (is_null($file_type) || !in_array($file_type, ['xml', 'csv', 'json'])) { | |
| 651 | 665 | return $this->http->end_rest_error('Invalid exporter file type'); |
| 652 | 666 | } |
| 653 | 667 | |
| 654 | 668 | $importer->setParser($file_type); |
| @@ -736,8 +750,71 @@ | ||
| 736 | 750 | } |
| 737 | 751 | |
| 738 | 752 | $headings = $tmp; |
| 739 | 753 | break; |
| 754 | + case 'json': | |
| 755 | + | |
| 756 | + $main = false; | |
| 757 | + foreach ($fields as $field) { | |
| 758 | + if ($field['selection'] === 'main' && ($field['loop'] === true || $field['loop'] === "true")) { | |
| 759 | + $main = $field; | |
| 760 | + break; | |
| 761 | + } | |
| 762 | + } | |
| 763 | + | |
| 764 | + if (!$main) { | |
| 765 | + return $this->http->end_rest_error("JSON exporter is missing main loop."); | |
| 766 | + } | |
| 767 | + | |
| 768 | + // generate base_path from nested wrappers + main loop label | |
| 769 | + $post_data['file_settings_base_path'] = implode('/', array_reverse(array_filter($this->generate_base_path($main, $fields)))); | |
| 770 | + | |
| 771 | + $allowed = [$main['id']]; | |
| 772 | + | |
| 773 | + $current_section = null; | |
| 774 | + $current_section_ancestors = []; | |
| 775 | + $current_section_map = []; | |
| 776 | + | |
| 777 | + foreach ($fields as $field) { | |
| 778 | + | |
| 779 | + if (in_array($field['parent'], $allowed)) { | |
| 780 | + | |
| 781 | + $field_map = '/' . implode('/', array_reverse(array_filter($this->generate_base_path($field, $fields, [], $main['id'])))); | |
| 782 | + $headings[$field_map] = $field['selection']; | |
| 783 | + | |
| 784 | + if ($field['loop'] === true || $field['loop'] === 'true') { | |
| 785 | + | |
| 786 | + $current_section = $field['selection']; | |
| 787 | + $current_section_ancestors = [$field['id']]; | |
| 788 | + } else { | |
| 789 | + | |
| 790 | + if (!is_null($current_section) && in_array($field['parent'], $current_section_ancestors)) { | |
| 791 | + | |
| 792 | + $current_section_ancestors[] = $field['id']; | |
| 793 | + $current_section_map[$field_map] = $current_section . '.' . $field['selection']; | |
| 794 | + } else { | |
| 795 | + $headings = $this->complete_section_map($headings, $current_section_map, $current_section, $formatted_fields); | |
| 796 | + $current_section = null; | |
| 797 | + $current_section_ancestors = []; | |
| 798 | + $current_section_map = []; | |
| 799 | + } | |
| 800 | + } | |
| 801 | + | |
| 802 | + if (!in_array($field['id'], $allowed)) { | |
| 803 | + $allowed[] = $field['id']; | |
| 804 | + } | |
| 805 | + } | |
| 806 | + } | |
| 807 | + | |
| 808 | + $headings = $this->complete_section_map($headings, $current_section_map, $current_section, $formatted_fields); | |
| 809 | + | |
| 810 | + $tmp = []; | |
| 811 | + foreach ($headings as $map => $heading) { | |
| 812 | + $tmp[$map] = $heading; | |
| 813 | + } | |
| 814 | + | |
| 815 | + $headings = $tmp; | |
| 816 | + break; | |
| 740 | 817 | } |
| 741 | 818 | |
| 742 | 819 | if (!empty($headings)) { |
| 743 | 820 | $template = $this->importer_manager->get_importer_template($importer); |
| @@ -746,8 +823,24 @@ | ||
| 746 | 823 | |
| 747 | 824 | $post_data['map'] = $field_map['map']; |
| 748 | 825 | $post_data['enabled'] = $field_map['enabled']; |
| 749 | 826 | } |
| 827 | + | |
| 828 | + // Prefill Permissions unique identifier from the exporter when available. | |
| 829 | + if (!empty($exporter_unique_identifier)) { | |
| 830 | + $importer_unique_identifier = apply_filters( | |
| 831 | + 'iwp/importer/from_exporter/unique_identifier', | |
| 832 | + $exporter_unique_identifier, | |
| 833 | + $exporter_unique_identifier, | |
| 834 | + $importer, | |
| 835 | + $setup_type === 'upload' ? $config : $exporter_data | |
| 836 | + ); | |
| 837 | + | |
| 838 | + if (!empty($importer_unique_identifier)) { | |
| 839 | + $post_data['setting_unique_identifier_type'] = 'field'; | |
| 840 | + $post_data['setting_unique_identifier'] = $importer_unique_identifier; | |
| 841 | + } | |
| 842 | + } | |
| 750 | 843 | } |
| 751 | 844 | } |
| 752 | 845 | |
| 753 | 846 | if (isset($post_data['setting_start_row'])) { |
| @@ -812,9 +905,9 @@ | ||
| 812 | 905 | |
| 813 | 906 | if ($clear_config) { |
| 814 | 907 | $this->importer_manager->clear_config_files($importer->getId(), true); |
| 815 | 908 | } |
| 816 | - } elseif ($parser === 'xml') { | |
| 909 | + } elseif ($parser === 'xml' || $parser === 'json') { | |
| 817 | 910 | if (isset($post_data['file_settings_base_path'])) { |
| 818 | 911 | $importer->setFileSetting('base_path', $post_data['file_settings_base_path']); |
| 819 | 912 | } |
| 820 | 913 | } |
| @@ -840,8 +933,15 @@ | ||
| 840 | 933 | } |
| 841 | 934 | |
| 842 | 935 | if (isset($post_data['enabled']) && is_array($post_data['enabled'])) { |
| 843 | 936 | foreach ($post_data['enabled'] as $key => $value) { |
| 937 | + // generate_field_map() returns a list of field ids: [0 => 'billing.first_name', ...] | |
| 938 | + // The UI saves an object map: ['billing.first_name' => true, ...] | |
| 939 | + if (is_int($key) && is_string($value) && $value !== '') { | |
| 940 | + $importer->setEnabled($value); | |
| 941 | + continue; | |
| 942 | + } | |
| 943 | + | |
| 844 | 944 | if ($this->is_truthy($value)) { |
| 845 | 945 | $importer->setEnabled($key); |
| 846 | 946 | } else { |
| 847 | 947 | $importer->setEnabled($key, false); |
| @@ -1189,8 +1289,12 @@ | ||
| 1189 | 1289 | if ('xml' === $parser) { |
| 1190 | 1290 | |
| 1191 | 1291 | $nodes = $this->importer_manager->process_xml_file($id, true); |
| 1192 | 1292 | $importer->setFileSetting('nodes', $nodes); |
| 1293 | + } elseif ('json' === $parser) { | |
| 1294 | + | |
| 1295 | + $nodes = $this->importer_manager->process_json_file($id, true); | |
| 1296 | + $importer->setFileSetting('nodes', $nodes); | |
| 1193 | 1297 | } elseif ('csv' === $parser) { |
| 1194 | 1298 | |
| 1195 | 1299 | $delimiter = isset($post_data['delimiter']) ? $post_data['delimiter'] : null; |
| 1196 | 1300 | if (is_null($delimiter)) { |
| @@ -1247,11 +1351,21 @@ | ||
| 1247 | 1351 | if (isset($post_data['file_encoding'])) { |
| 1248 | 1352 | $importer->setFileSetting('file_encoding', $post_data['file_encoding']); |
| 1249 | 1353 | } |
| 1250 | 1354 | |
| 1355 | + $record_index = intval($record_index); | |
| 1356 | + | |
| 1357 | + // Rebuild a complete temp index once for preview navigation. | |
| 1358 | + // file-process may have stored a partial sample index (e.g. header + 1 row). | |
| 1359 | + $config = $this->importer_manager->get_config($importer, true); | |
| 1360 | + if (!$config->get('preview_full_index')) { | |
| 1361 | + $this->importer_manager->clear_config_files($id, true); | |
| 1362 | + $config = $this->importer_manager->get_config($importer, true); | |
| 1363 | + $config->set('preview_full_index', true); | |
| 1364 | + } | |
| 1365 | + | |
| 1251 | 1366 | if ($importer->getParser() === 'xml') { |
| 1252 | 1367 | |
| 1253 | - $config = $this->importer_manager->get_config($importer, true); | |
| 1254 | 1368 | $file = $this->importer_manager->get_xml_file($importer, $config); |
| 1255 | 1369 | |
| 1256 | 1370 | $base_path = $post_data['base_path']; |
| 1257 | 1371 | if (!is_null($base_path)) { |
| @@ -1257,18 +1371,55 @@ | ||
| 1257 | 1371 | if (!is_null($base_path)) { |
| 1258 | 1372 | $file->setRecordPath($base_path); |
| 1259 | 1373 | } |
| 1260 | 1374 | |
| 1375 | + $total = $file->getRecordCount(); | |
| 1376 | + if ($total > 0) { | |
| 1377 | + $record_index = max(0, min($record_index, $total - 1)); | |
| 1378 | + } else { | |
| 1379 | + $record_index = 0; | |
| 1380 | + } | |
| 1381 | + | |
| 1261 | 1382 | $preview = new XMLPreview($file, $base_path); |
| 1262 | - $result = $preview->data(); | |
| 1383 | + $result = $preview->data($record_index); | |
| 1263 | 1384 | if (is_wp_error($result)) { |
| 1264 | 1385 | return $this->http->end_rest_error($result); |
| 1265 | 1386 | } |
| 1266 | 1387 | |
| 1267 | - return $this->http->end_rest_success($result[0]); | |
| 1388 | + return $this->http->end_rest_success([ | |
| 1389 | + 'data' => $result[0], | |
| 1390 | + 'record' => $record_index, | |
| 1391 | + 'total' => $total, | |
| 1392 | + ]); | |
| 1393 | + } elseif ($importer->getParser() === 'json') { | |
| 1394 | + | |
| 1395 | + $file = $this->importer_manager->get_json_file($importer, $config); | |
| 1396 | + | |
| 1397 | + $base_path = isset($post_data['base_path']) ? $post_data['base_path'] : $importer->getFileSetting('base_path'); | |
| 1398 | + if (!is_null($base_path)) { | |
| 1399 | + $file->setRecordPath($base_path); | |
| 1400 | + } | |
| 1401 | + | |
| 1402 | + $total = $file->getRecordCount(); | |
| 1403 | + if ($total > 0) { | |
| 1404 | + $record_index = max(0, min($record_index, $total - 1)); | |
| 1405 | + } else { | |
| 1406 | + $record_index = 0; | |
| 1407 | + } | |
| 1408 | + | |
| 1409 | + $preview = new JSONPreview($file, $base_path); | |
| 1410 | + $result = $preview->data($record_index); | |
| 1411 | + if (is_wp_error($result)) { | |
| 1412 | + return $this->http->end_rest_error($result); | |
| 1413 | + } | |
| 1414 | + | |
| 1415 | + return $this->http->end_rest_success([ | |
| 1416 | + 'data' => $result[0], | |
| 1417 | + 'record' => $record_index, | |
| 1418 | + 'total' => $total, | |
| 1419 | + ]); | |
| 1268 | 1420 | } elseif ($importer->getParser() === 'csv') { |
| 1269 | 1421 | |
| 1270 | - $config = $this->importer_manager->get_config($importer, true); | |
| 1271 | 1422 | $file = $this->importer_manager->get_csv_file($importer, $config); |
| 1272 | 1423 | |
| 1273 | 1424 | $clear_config = false; |
| 1274 | 1425 | if (!is_null($post_data['delimiter']) && $post_data['delimiter'] !== $file->getDelimiter()) { |
| @@ -1281,8 +1432,9 @@ | ||
| 1281 | 1432 | |
| 1282 | 1433 | if ($clear_config) { |
| 1283 | 1434 | $this->importer_manager->clear_config_files($importer->getId(), true); |
| 1284 | 1435 | $config = $this->importer_manager->get_config($importer, true); |
| 1436 | + $config->set('preview_full_index', true); | |
| 1285 | 1437 | $file = $this->importer_manager->get_csv_file($importer, $config); |
| 1286 | 1438 | } |
| 1287 | 1439 | |
| 1288 | 1440 | $delimiter = $post_data['delimiter']; |
| @@ -1344,18 +1496,25 @@ | ||
| 1344 | 1496 | |
| 1345 | 1497 | $parser = $importer->getParser(); |
| 1346 | 1498 | |
| 1347 | 1499 | $fields = $this->sanitize($request->get_body_params()); |
| 1500 | + $record_index = 0; | |
| 1501 | + if (isset($fields['record'])) { | |
| 1502 | + $record_index = intval($fields['record']); | |
| 1503 | + unset($fields['record']); | |
| 1504 | + } | |
| 1348 | 1505 | if (is_null($fields) || empty($fields)) { |
| 1349 | 1506 | $fields = $importer->getMap(); |
| 1350 | 1507 | } |
| 1351 | 1508 | |
| 1352 | 1509 | if ('xml' === $parser) { |
| 1353 | - $result = $this->importer_manager->preview_xml_file($importer, $fields); | |
| 1510 | + $result = $this->importer_manager->preview_xml_file($importer, $fields, $record_index); | |
| 1511 | + } elseif ('json' === $parser) { | |
| 1512 | + $result = $this->importer_manager->preview_json_file($importer, $fields, $record_index); | |
| 1354 | 1513 | } elseif ('csv' === $parser) { |
| 1355 | - $row = 0; | |
| 1514 | + $row = $record_index; | |
| 1356 | 1515 | if ($importer->getFileSetting('show_headings') === true) { |
| 1357 | - $row = 1; | |
| 1516 | + $row = $record_index + 1; | |
| 1358 | 1517 | } |
| 1359 | 1518 | $result = $this->importer_manager->preview_csv_file($importer, $fields, $row); |
| 1360 | 1519 | } else { |
| 1361 | 1520 | |
| @@ -1502,9 +1661,19 @@ | ||
| 1502 | 1661 | } |
| 1503 | 1662 | $importer_data = $this->importer_manager->get_importer($id); |
| 1504 | 1663 | $log = $this->importer_manager->get_importer_debug_log($importer_data, $page, 100); |
| 1505 | 1664 | |
| 1506 | - $download = Logger::getLogFile($importer_data->getId(), true); | |
| 1665 | + // Authenticated admin download URL — direct uploads/importwp URLs are blocked by .htaccess. | |
| 1666 | + $download = add_query_arg( | |
| 1667 | + array( | |
| 1668 | + 'page' => 'importwp', | |
| 1669 | + 'import' => $importer_data->getId(), | |
| 1670 | + 'download_debug' => 1, | |
| 1671 | + '_wpnonce' => wp_create_nonce('iwp_debug_log_download'), | |
| 1672 | + ), | |
| 1673 | + admin_url('tools.php') | |
| 1674 | + ); | |
| 1675 | + | |
| 1507 | 1676 | return $this->http->end_rest_success(['log' => $log, 'download' => $download]); |
| 1508 | 1677 | } |
| 1509 | 1678 | |
| 1510 | 1679 | public function save_settings(\WP_REST_Request $request) |
| @@ -1686,9 +1855,9 @@ | ||
| 1686 | 1855 | if (!isset($contents['name'], $contents['fields'], $contents['data'], $contents['data']['file_type'])) { |
| 1687 | 1856 | return $this->http->end_rest_error("Invalid exporter config file."); |
| 1688 | 1857 | } |
| 1689 | 1858 | |
| 1690 | - if (!in_array($contents['data']['file_type'], ['xml', 'csv'])) { | |
| 1859 | + if (!in_array($contents['data']['file_type'], ['xml', 'csv', 'json'])) { | |
| 1691 | 1860 | return $this->http->end_rest_error("Exporter file type is not supported."); |
| 1692 | 1861 | } |
| 1693 | 1862 | |
| 1694 | 1863 | return $this->http->end_rest_success(['exporter' => $contents]); |