PluginProbe
Import WP – CSV & XML Import Export for WordPress / 2.15.2
Import WP – CSV & XML Import Export for WordPress v2.15.2
2.15.2 2.15.1 2.15.0 2.14.24 2.14.23 2.7.0 2.7.1 2.7.10 2.7.11 2.7.12 2.7.13 2.7.14 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.9.0 All 145 releases
← All changes | class/Common/Rest/RestManager.php +859 -100 2.7.9 → 2.15.2 View file →
@@ -2,12 +2,15 @@
2 2
3 3 namespace ImportWP\Common\Rest;
4 4
5 5 use ImportWP\Common\Exporter\ExporterManager;
6 +use ImportWP\Common\Exporter\State\ExporterState;
6 7 use ImportWP\Common\Filesystem\Filesystem;
8 +use ImportWP\Common\Filesystem\ZipArchive;
7 9 use ImportWP\Common\Http\Http;
8 10 use ImportWP\Common\Importer\ImporterManager;
9 11 use ImportWP\Common\Importer\Preview\CSVPreview;
12 +use ImportWP\Common\Importer\Preview\JSONPreview;
10 13 use ImportWP\Common\Importer\Preview\XMLPreview;
11 14 use ImportWP\Common\Importer\State\ImporterState;
12 15 use ImportWP\Common\Importer\Template\Template;
13 16 use ImportWP\Common\Importer\Template\TemplateManager;
@@ -234,8 +237,21 @@
234 237 'permission_callback' => array($this, 'get_permission')
235 238 ),
236 239 ));
237 240
241 + register_rest_route($namespace, '/compatibility', array(
242 + array(
243 + 'methods' => \WP_REST_Server::CREATABLE,
244 + 'callback' => array($this, 'save_compatibility'),
245 + 'permission_callback' => array($this, 'get_permission')
246 + ),
247 + array(
248 + 'methods' => \WP_REST_Server::READABLE,
249 + 'callback' => array($this, 'get_compatibility'),
250 + 'permission_callback' => array($this, 'get_permission')
251 + ),
252 + ));
253 +
238 254 register_rest_route($namespace, '/import-export', array(
239 255 array(
240 256 'methods' => \WP_REST_Server::CREATABLE,
241 257 'callback' => array($this, 'import_exporters'),
@@ -263,8 +279,16 @@
263 279 'permission_callback' => array($this, 'get_permission')
264 280 )
265 281 ));
266 282
283 + register_rest_route($namespace, '/importer/(?P<id>\d+)/template_unique_identifiers', array(
284 + array(
285 + 'methods' => \WP_REST_Server::READABLE,
286 + 'callback' => array($this, 'get_importer_unique_identifier_options'),
287 + 'permission_callback' => array($this, 'get_permission')
288 + )
289 + ));
290 +
267 291 // Exporter
268 292
269 293 register_rest_route($namespace, '/exporter', array(
270 294 array(
@@ -299,8 +323,32 @@
299 323 'permission_callback' => array($this, 'get_permission')
300 324 ),
301 325 ));
302 326
327 + register_rest_route($namespace, '/exporter/(?P<id>\d+)/init', array(
328 + array(
329 + 'methods' => \WP_REST_Server::READABLE,
330 + 'callback' => array($this, 'init_export'),
331 + 'permission_callback' => array($this, 'get_permission')
332 + )
333 + ));
334 +
335 + register_rest_route($namespace, '/exporter/(?P<id>\d+)/download-config', array(
336 + array(
337 + 'methods' => \WP_REST_Server::READABLE,
338 + 'callback' => array($this, 'download_exporter_config'),
339 + 'permission_callback' => array($this, 'get_permission')
340 + )
341 + ));
342 +
343 + register_rest_route($namespace, '/importer/read-config', array(
344 + array(
345 + 'methods' => \WP_REST_Server::CREATABLE,
346 + 'callback' => array($this, 'read_exporter_config'),
347 + 'permission_callback' => array($this, 'get_permission')
348 + )
349 + ));
350 +
303 351 register_rest_route($namespace, '/exporter/(?P<id>\d+)/run', array(
304 352 array(
305 353 'methods' => \WP_REST_Server::CREATABLE,
306 354 'callback' => array($this, 'run_exporter'),
@@ -320,9 +368,9 @@
320 368 public function get_permission()
321 369 {
322 370
323 371 if (!current_user_can('manage_options')) {
324 - return new \WP_Error('rest_forbidden', esc_html__('You do not have permissions.', $this->properties->plugin_domain), array('status' => 401));
372 + return new \WP_Error('rest_forbidden', __('You do not have permissions.', 'jc-importer'), array('status' => 401));
325 373 }
326 374
327 375 return true;
328 376 }
@@ -338,8 +386,13 @@
338 386
339 387 return $data;
340 388 }
341 389
390 + // Preserve booleans/numbers from JSON request bodies
391 + if (is_bool($data) || is_int($data) || is_float($data) || $data === null) {
392 + return $data;
393 + }
394 +
342 395 $full_path = implode('.', array_filter($path));
343 396 if (!is_null($full_path)) {
344 397
345 398 if (preg_match('/^map\.\S+/', $full_path) !== false) {
@@ -356,8 +409,70 @@
356 409
357 410 return sanitize_text_field($data);
358 411 }
359 412
413 + /**
414 + * Read request body from form-urlencoded or JSON.
415 + *
416 + * @param \WP_REST_Request $request
417 + * @return array
418 + */
419 + protected function get_request_data(\WP_REST_Request $request)
420 + {
421 + $post_data = $request->get_body_params();
422 + if (!is_array($post_data)) {
423 + $post_data = [];
424 + }
425 +
426 + $json_params = $request->get_json_params();
427 + if (is_array($json_params) && !empty($json_params)) {
428 + $post_data = array_replace_recursive($post_data, $json_params);
429 + }
430 +
431 + return $this->decode_packed_fields($post_data);
432 + }
433 +
434 + /**
435 + * Decode map/enabled when sent as a single JSON string (avoids max_input_vars).
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 + *
440 + * @param array $post_data
441 + * @return array
442 + */
443 + protected function decode_packed_fields($post_data)
444 + {
445 + foreach (['map', 'enabled'] as $field) {
446 + if (!isset($post_data[$field]) || !is_string($post_data[$field])) {
447 + continue;
448 + }
449 +
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 +
458 + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
459 + $post_data[$field] = $decoded;
460 + }
461 + }
462 +
463 + return $post_data;
464 + }
465 +
466 + /**
467 + * @param mixed $value
468 + * @return bool
469 + */
470 + protected function is_truthy($value)
471 + {
472 + return $value === true || $value === 1 || $value === '1' || $value === 'true';
473 + }
474 +
360 475 public function check_rest_status(\WP_REST_Request $request)
361 476 {
362 477 $result = [];
363 478 // find required php ext: https://github.com/llaville/php-compat-info
@@ -367,9 +482,9 @@
367 482
368 483 $result = [
369 484 'tmp_writable' => [
370 485 'status' => $tmp_writable ? 'yes' : 'no',
371 - 'message' => $tmp_writable ? '' : 'WordPress is unable to write to directory: ' . $this->filesystem->get_temp_directory() . ', make sure this directory is writable.',
486 + 'message' => $tmp_writable ? '' : sprintf(__('WordPress is unable to write to directory: %s, make sure this directory is writable.', 'jc-importer'), $this->filesystem->get_temp_directory()),
372 487 ],
373 488 'rest_enabled' => [
374 489 'status' => 'yes',
375 490 'message' => '',
@@ -389,8 +504,11 @@
389 504 'php_version' => [
390 505 'status' => version_compare(phpversion(), '5.5.0') ? 'yes' : 'no',
391 506 'message' => '',
392 507 ],
508 + 'zip_archive' => [
509 + 'status' => ZipArchive::has_requirements_met() ? 'yes' : 'no'
510 + ]
393 511 ];
394 512 return $this->http->end_rest_success($result);
395 513 }
396 514
@@ -398,9 +516,9 @@
398 516 {
399 517 $migration = new Migrations();
400 518 $result = $migration->migrate();
401 519
402 - return $this->http->end_rest_success("Migration Complete.");
520 + return $this->http->end_rest_success(__("Migration Complete.", 'jc-importer'));
403 521 }
404 522
405 523 public function get_importers(\WP_REST_Request $request)
406 524 {
@@ -427,9 +545,9 @@
427 545 public function save_importer(\WP_REST_Request $request)
428 546 {
429 547 Logger::setRequestType('save_importer');
430 548
431 - $post_data = $request->get_body_params();
549 + $post_data = $this->get_request_data($request);
432 550 $id = isset($post_data['id']) ? intval($post_data['id']) : null;
433 551
434 552 $temp_post_data = [
435 553 'create' => [],
@@ -437,10 +555,19 @@
437 555 ];
438 556
439 557 if (isset($post_data['permissions'])) {
440 558 // TODO: update sanitize method to keep track of hierarchy
441 - $temp_post_data['create'] = explode("\n", $post_data['permissions']['create']['fields']);
442 - $temp_post_data['update'] = explode("\n", $post_data['permissions']['update']['fields']);
559 + foreach (['create', 'update'] as $permission_method) {
560 + if (!isset($post_data['permissions'][$permission_method]['fields'])) {
561 + continue;
562 + }
563 + $permission_fields = $post_data['permissions'][$permission_method]['fields'];
564 + if (is_array($permission_fields)) {
565 + $temp_post_data[$permission_method] = $permission_fields;
566 + } else {
567 + $temp_post_data[$permission_method] = explode("\n", (string) $permission_fields);
568 + }
569 + }
443 570 }
444 571
445 572 $post_data = $this->sanitize($post_data);
446 573
@@ -455,12 +582,9 @@
455 582 }
456 583 }
457 584 }
458 585
459 -
460 -
461 586 $importer = new ImporterModel($id, $this->importer_manager->is_debug());
462 -
463 587 if (isset($post_data['datasource'])) {
464 588 $importer->setDatasource($post_data['datasource']);
465 589 }
466 590
@@ -468,9 +592,9 @@
468 592 $importer->setDatasourceSetting('remote_url', $post_data['remote_url']);
469 593 }
470 594
471 595 if (isset($post_data['local_url'])) {
472 - $importer->setDatasourceSetting('local_url', $post_data['local_url']);
596 + $importer->setDatasourceSetting('local_url', wp_normalize_path($post_data['local_url']));
473 597 }
474 598
475 599 if (isset($post_data['existing_id'])) {
476 600 $importer->setFileId(intval($post_data['existing_id']));
@@ -491,8 +615,14 @@
491 615 if (isset($post_data['template_options'])) {
492 616 $template_options = array_merge($template_options, $post_data['template_options']);
493 617 }
494 618
619 + // setup default enabled fields
620 + $default_enabled_fields = $template->get_default_enabled_fields();
621 + foreach ($default_enabled_fields as $default_enabled_field) {
622 + $importer->setEnabled($default_enabled_field);
623 + }
624 +
495 625 foreach ($template_options as $key => $val) {
496 626 $importer->setSetting($key, $val);
497 627 }
498 628 }
@@ -500,8 +630,220 @@
500 630 if (isset($post_data['name'])) {
501 631 $importer->setName($post_data['name']);
502 632 }
503 633
634 + if (is_null($id)) {
635 +
636 + // NOTE: New importer, should we generate field map from exporter.
637 + if (isset($post_data['setup_type']) && in_array($post_data['setup_type'], ['upload', 'generate'])) {
638 +
639 + $setup_type = $post_data['setup_type'] === 'upload' ? 'upload' : 'generate';
640 + $file_type = null;
641 + $exporter_unique_identifier = '';
642 +
643 + if ($setup_type === 'upload') {
644 + $config = json_decode($post_data['exporter_config_file'], true);
645 + $file_type = $config['data']['file_type'];
646 + $fields = $config['fields'];
647 + $formatted_fields = $config['formatted_fields'];
648 + $file_settings = $config['data']['file_settings'];
649 + $exporter_unique_identifier = isset($config['data']['unique_identifier']) ? $config['data']['unique_identifier'] : '';
650 + } else {
651 +
652 + /**
653 + * @var \ImportWP\Common\Model\ExporterModel $exporter_data
654 + */
655 + $exporter_data = $this->exporter_manager->get_exporter($post_data['exporter']);
656 + $fields = $this->exporter_manager->get_importer_map_fields($exporter_data);
657 + $mapper = $this->exporter_manager->get_exporter_mapper($exporter_data);
658 + $formatted_fields = $mapper->get_fields();
659 + $file_type = $exporter_data->getFileType();
660 + $file_settings = $exporter_data->getFileSettings();
661 + $exporter_unique_identifier = $exporter_data->getUniqueIdentifier();
662 + }
663 +
664 + if (is_null($file_type) || !in_array($file_type, ['xml', 'csv', 'json'])) {
665 + return $this->http->end_rest_error('Invalid exporter file type');
666 + }
667 +
668 + $importer->setParser($file_type);
669 +
670 + $headings = [];
671 + switch ($file_type) {
672 + case 'csv':
673 + $headings = array_reduce($fields, function ($carry, $item) {
674 + $carry[] = $item['selection'];
675 + return $carry;
676 + }, []);
677 + $headings = array_map('trim', $headings);
678 +
679 + if (isset($file_settings['delimiter']) && !empty($file_settings['delimiter']) && strlen($file_settings['delimiter']) === 1) {
680 + $importer->setFileSetting('delimiter', $file_settings['delimiter']);
681 + }
682 + if (isset($file_settings['enclosure']) && !empty($file_settings['enclosure']) && strlen($file_settings['enclosure']) === 1) {
683 + $importer->setFileSetting('enclosure', $file_settings['enclosure']);
684 + }
685 + if (isset($file_settings['escape']) && !empty($file_settings['escape']) && strlen($file_settings['escape']) === 1) {
686 + $importer->setFileSetting('escape', $file_settings['escape']);
687 + }
688 +
689 + break;
690 + case 'xml':
691 +
692 + $main = false;
693 + foreach ($fields as $field) {
694 + if ($field['selection'] === 'main' && ($field['loop'] === true || $field['loop'] === "true")) {
695 + $main = $field;
696 + break;
697 + }
698 + }
699 +
700 + if (!$main) {
701 + return $this->http->end_rest_error("XML exporter is missing main loop.");
702 + }
703 +
704 + // generate base_path
705 + $post_data['file_settings_base_path'] = '/' . implode('/', array_reverse(array_filter($this->generate_base_path($main, $fields))));
706 +
707 + $allowed = [$main['id']];
708 +
709 + $current_section = null;
710 + $current_section_ancestors = [];
711 + $current_section_map = [];
712 +
713 + foreach ($fields as $field) {
714 +
715 + if (in_array($field['parent'], $allowed)) {
716 +
717 + $field_map = '/' . implode('/', array_reverse(array_filter($this->generate_base_path($field, $fields, [], $main['id']))));
718 + $headings[$field_map] = $field['selection'];
719 +
720 + if ($field['loop'] === true || $field['loop'] === 'true') {
721 +
722 + // we need to convert all sub fields to tax_category.id ....
723 + $current_section = $field['selection'];
724 + $current_section_ancestors = [$field['id']];
725 + } else {
726 +
727 + if (!is_null($current_section) && in_array($field['parent'], $current_section_ancestors)) {
728 +
729 + $current_section_ancestors[] = $field['id'];
730 + $current_section_map[$field_map] = $current_section . '.' . $field['selection'];
731 + } else {
732 + $headings = $this->complete_section_map($headings, $current_section_map, $current_section, $formatted_fields);
733 + $current_section = null;
734 + $current_section_ancestors = [];
735 + $current_section_map = [];
736 + }
737 + }
738 +
739 + if (!in_array($field['id'], $allowed)) {
740 + $allowed[] = $field['id'];
741 + }
742 + }
743 + }
744 +
745 + $headings = $this->complete_section_map($headings, $current_section_map, $current_section, $formatted_fields);
746 +
747 + $tmp = [];
748 + foreach ($headings as $map => $heading) {
749 + $tmp[$map] = $heading;
750 + }
751 +
752 + $headings = $tmp;
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;
817 + }
818 +
819 + if (!empty($headings)) {
820 + $template = $this->importer_manager->get_importer_template($importer);
821 + $field_map = $template->generate_field_map($headings, $importer);
822 + $field_map = apply_filters('iwp/importer/generate_field_map', $field_map, $headings, $importer);
823 +
824 + $post_data['map'] = $field_map['map'];
825 + $post_data['enabled'] = $field_map['enabled'];
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 + }
843 + }
844 + }
845 +
504 846 if (isset($post_data['setting_start_row'])) {
505 847 $importer->setStartRow($post_data['setting_start_row']);
506 848 }
507 849
@@ -517,8 +859,13 @@
517 859 $importer->setSetting($matches['key'], $value);
518 860 }
519 861 }
520 862
863 + if (isset($post_data['setting_import_method']) && $post_data['setting_import_method'] === 'background') {
864 + delete_post_meta($importer->getId(), '_iwp_cron_scheduled');
865 + delete_post_meta($importer->getId(), '_iwp_cron_last_ran');
866 + }
867 +
521 868 $parser = $importer->getParser();
522 869
523 870 if (isset($post_data['file_settings_encoding'])) {
524 871 $importer->setFileSetting('file_encoding', $post_data['file_settings_encoding']);
@@ -541,16 +888,26 @@
541 888 $clear_config = true;
542 889 }
543 890 $importer->setFileSetting('enclosure', $post_data['file_settings_enclosure']);
544 891 }
892 +
893 + if (isset($post_data['file_settings_escape'])) {
894 + $old_escape = $importer->getFileSetting('escape');
895 + if ($old_escape !== $post_data['file_settings_escape']) {
896 + $clear_config = true;
897 + }
898 + $importer->setFileSetting('escape', $post_data['file_settings_escape']);
899 + }
900 +
901 +
545 902 if (isset($post_data['file_settings_show_headings'])) {
546 - $importer->setFileSetting('show_headings', $post_data['file_settings_show_headings'] === 'true' || $post_data['file_settings_show_headings'] === true ? true : false);
903 + $importer->setFileSetting('show_headings', $this->is_truthy($post_data['file_settings_show_headings']));
547 904 }
548 905
549 906 if ($clear_config) {
550 907 $this->importer_manager->clear_config_files($importer->getId(), true);
551 908 }
552 - } elseif ($parser === 'xml') {
909 + } elseif ($parser === 'xml' || $parser === 'json') {
553 910 if (isset($post_data['file_settings_base_path'])) {
554 911 $importer->setFileSetting('base_path', $post_data['file_settings_base_path']);
555 912 }
556 913 }
@@ -555,25 +912,37 @@
555 912 }
556 913 }
557 914
558 915 if (isset($post_data['file_settings_setup'])) {
559 - $importer->setFileSetting('setup', $post_data['file_settings_setup'] === 'true' ? true : false);
916 + $importer->setFileSetting('setup', $this->is_truthy($post_data['file_settings_setup']));
560 917 }
561 918
562 919 if (isset($post_data['map']) && is_array($post_data['map'])) {
920 + $map = [];
563 921 foreach ($post_data['map'] as $key => $value) {
922 + if ($value === null) {
923 + continue;
924 + }
564 925
565 926 if (1 === preg_match('/\._index$/', $key)) {
566 927 $value = intval($value);
567 928 }
568 929
569 - $importer->setMap($key, $value);
930 + $map[$key] = $value;
570 931 }
932 + $importer->replaceMap($map);
571 933 }
572 934
573 935 if (isset($post_data['enabled']) && is_array($post_data['enabled'])) {
574 936 foreach ($post_data['enabled'] as $key => $value) {
575 - if ($value === 'true') {
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 +
944 + if ($this->is_truthy($value)) {
576 945 $importer->setEnabled($key);
577 946 } else {
578 947 $importer->setEnabled($key, false);
579 948 }
@@ -585,9 +954,9 @@
585 954 // create
586 955 if (isset($post_data['permissions']['create'])) {
587 956 $create = $post_data['permissions']['create'];
588 957 $importer->setPermission('create', [
589 - 'enabled' => isset($create['enabled']) && $create['enabled'] === 'true' ? true : false,
958 + 'enabled' => isset($create['enabled']) && $this->is_truthy($create['enabled']) ? true : false,
590 959 'type' => isset($create['type']) && in_array($create['type'], ['include', 'exclude'], true) ? $create['type'] : null,
591 960 'fields' => isset($create['fields']) ? $create['fields'] : [],
592 961 ]);
593 962 }
@@ -595,9 +964,9 @@
595 964 // update
596 965 if (isset($post_data['permissions']['update'])) {
597 966 $update = $post_data['permissions']['update'];
598 967 $importer->setPermission('update', [
599 - 'enabled' => isset($update['enabled']) && $update['enabled'] === 'true' ? true : false,
968 + 'enabled' => isset($update['enabled']) && $this->is_truthy($update['enabled']) ? true : false,
600 969 'type' => isset($update['type']) && in_array($update['type'], ['include', 'exclude'], true) ? $update['type'] : null,
601 970 'fields' => isset($update['fields']) ? $update['fields'] : [],
602 971 ]);
603 972 }
@@ -604,10 +973,11 @@
604 973
605 974 // remove
606 975 if (isset($post_data['permissions']['remove'])) {
607 976 $importer->setPermission('remove', [
608 - 'enabled' => $post_data['permissions']['remove']['enabled'] === 'true',
609 - 'trash' => $post_data['permissions']['remove']['trash'] === 'true',
977 + 'enabled' => $this->is_truthy($post_data['permissions']['remove']['enabled']),
978 + 'trash' => $this->is_truthy($post_data['permissions']['remove']['trash']),
979 + 'media' => $this->is_truthy($post_data['permissions']['remove']['media']),
610 980 ]);
611 981 }
612 982 }
613 983
@@ -617,8 +987,88 @@
617 987 }
618 988 return $this->http->end_rest_success($importer->data());
619 989 }
620 990
991 + public function generate_base_path($field, $fields, $output = [], $stop = 0)
992 + {
993 + $output[] = isset($field['label']) && !empty($field['label']) ? $field['label'] : $field['selection'];
994 +
995 + if ($field['parent'] !== $stop) {
996 +
997 + foreach ($fields as $sub_field) {
998 + if ($sub_field['id'] == $field['parent']) {
999 +
1000 + return $this->generate_base_path($sub_field, $fields, $output, $stop);
1001 + }
1002 + }
1003 + }
1004 + return $output;
1005 + }
1006 +
1007 + public function complete_section_map($headings, $current_section_map, $current_section, $fields)
1008 + {
1009 + if (is_null($current_section)) {
1010 + return $headings;
1011 + }
1012 +
1013 + if ($current_section === 'custom_fields') {
1014 + // $headings['/test/@value'] = 'custom_fields.{/test/@key}';
1015 +
1016 + $meta_key = false;
1017 + $meta_val = false;
1018 +
1019 + if (count($current_section_map) == 2) {
1020 + foreach ($current_section_map as $row_map => $row_value) {
1021 + if ($row_value == 'custom_fields.meta_key') {
1022 + $meta_key = $row_map;
1023 + } elseif ($row_value == 'custom_fields.meta_value') {
1024 + $meta_val = $row_map;
1025 + }
1026 + }
1027 +
1028 + if ($meta_key && $meta_val) {
1029 +
1030 + // Get full list of custom fields, that can be then be used to generate a full list
1031 + // /custom_fields_wrapper/custom_fields[meta_key="_edit_lock"]/meta_key
1032 + // /custom_fields_wrapper/custom_fields[meta_key="_edit_lock"]/meta_value
1033 +
1034 + // Compare the two strings and get the matching parts
1035 + $meta_key_parts = array_values(array_filter(explode('/', $meta_key)));
1036 + $meta_val_parts = array_values(array_filter(explode('/', $meta_val)));
1037 +
1038 + for ($i = 0; $i < count($meta_key_parts); $i++) {
1039 + if ($meta_key_parts[$i] !== $meta_val_parts[$i]) {
1040 + break;
1041 + }
1042 + }
1043 +
1044 + $start = array_slice($meta_key_parts, 0, $i);
1045 + $end_key = array_slice($meta_key_parts, $i);
1046 + $end_val = array_slice($meta_val_parts, $i);
1047 +
1048 + $start_path = implode('/', $start);
1049 + $end_key_path = implode('/', $end_key);
1050 + $end_val_path = implode('/', $end_val);
1051 +
1052 + $custom_field_key_list = $fields['children']['custom_fields']['fields'];
1053 + foreach ($custom_field_key_list as $custom_field_key) {
1054 +
1055 + // $tmp_field_key = '{/' . $start_path . sprintf('[%s="%s"]', $end_key_path, $custom_field_key) . '/' . $end_key_path . '}';
1056 + $tmp_field_val = '/' . $start_path . sprintf('[%s="%s"]', $end_key_path, $custom_field_key) . '/' . $end_val_path;
1057 +
1058 + $headings[$tmp_field_val] = sprintf('custom_fields.%s', $custom_field_key);
1059 + }
1060 +
1061 + return $headings;
1062 + }
1063 + }
1064 +
1065 + return array_merge($headings, $current_section_map);
1066 + }
1067 +
1068 + return array_merge($headings, $current_section_map);
1069 + }
1070 +
621 1071 private function sanitize_setting($value)
622 1072 {
623 1073 if (is_array($value)) {
624 1074 foreach ($value as &$tmp) {
@@ -661,14 +1111,25 @@
661 1111
662 1112 foreach ($query->posts as $importer_id) {
663 1113
664 1114 $importer_model = $this->importer_manager->get_importer($importer_id);
1115 + $config = $this->importer_manager->get_config($importer_model);
665 1116
666 1117 $output = ImporterState::get_state($importer_id);
667 - $output['version'] = 2;
668 - $output['message'] = $this->generate_status_message($output);
669 - $output['importer'] = $importer_id;
1118 + if (!$output) {
1119 + $output = [
1120 + 'version' => 2,
1121 + 'message' => 'Not setup',
1122 + 'importer' => $importer_id
1123 + ];
1124 + } else {
670 1125
1126 + $output['version'] = 2;
1127 + $output['message'] = $this->generate_status_message($output);
1128 + $output['importer'] = $importer_id;
1129 + $output['process'] = intval($config->get('process'));
1130 + }
1131 +
671 1132 $result[] = $this->event_handler->run('iwp/importer/status/output', [$output, $importer_model]);
672 1133 }
673 1134
674 1135 echo json_encode($result) . "\n";
@@ -689,12 +1150,12 @@
689 1150
690 1151 if (isset($data['status'])) {
691 1152 switch ($data['status']) {
692 1153 case 'cancelled':
693 - $output .= 'Import cancelled';
1154 + $output .= __('Import cancelled', 'jc-importer');
694 1155 break;
695 1156 case 'complete':
696 - $output .= 'Import complete';
1157 + $output .= __('Import complete', 'jc-importer');
697 1158 break;
698 1159 case 'error':
699 1160 $output = $data['message'];
700 1161 return $output;
@@ -701,15 +1162,14 @@
701 1162 break;
702 1163 }
703 1164 }
704 1165
705 - if (isset($data['status']) && $data['status'] === 'running' && isset($data['section'])) {
1166 + if (isset($data['status']) && ($data['status'] === 'running' || $data['status'] === 'processing' || $data['status'] === 'timeout') && isset($data['section'])) {
706 1167 switch ($data['section']) {
707 1168 case 'import':
708 1169 case 'delete':
709 - $output = $data['section'] === 'import' ? 'Importing: ' : 'Deleting: ';
710 - $output .= $data['progress'][$data['section']]['current_row'] . ' / ' . ($data['progress'][$data['section']]['end'] - $data['progress'][$data['section']]['start']);
711 -
1170 + $progress = $data['progress'][$data['section']]['current_row'] . ' / ' . ($data['progress'][$data['section']]['end'] - $data['progress'][$data['section']]['start']);
1171 + $output = sprintf($data['section'] === 'import' ? __('Importing: %s', 'jc-importer') : __('Deleting: %s', 'jc-importer'), $progress);
712 1172 break;
713 1173 }
714 1174 }
715 1175
@@ -714,21 +1174,21 @@
714 1174 }
715 1175
716 1176 if (isset($data['stats'])) {
717 1177 if ($data['stats']['inserts'] > 0) {
718 - $output .= sprintf(', Inserts: %d', $data['stats']['inserts']);
1178 + $output .= sprintf(__(', Inserts: %d', 'jc-importer'), $data['stats']['inserts']);
719 1179 }
720 1180 if ($data['stats']['updates'] > 0) {
721 - $output .= sprintf(', Updates: %d', $data['stats']['updates']);
1181 + $output .= sprintf(__(', Updates: %d', 'jc-importer'), $data['stats']['updates']);
722 1182 }
723 1183 if ($data['stats']['deletes'] > 0) {
724 - $output .= sprintf(', Deletes: %d', $data['stats']['deletes']);
1184 + $output .= sprintf(__(', Deletes: %d', 'jc-importer'), $data['stats']['deletes']);
725 1185 }
726 1186 if ($data['stats']['skips'] > 0) {
727 - $output .= sprintf(', Skips: %d', $data['stats']['skips']);
1187 + $output .= sprintf(__(', Skips: %d', 'jc-importer'), $data['stats']['skips']);
728 1188 }
729 1189 if ($data['stats']['errors'] > 0) {
730 - $output .= sprintf(', Errors: %d', $data['stats']['errors']);
1190 + $output .= sprintf(__(', Errors: %d', 'jc-importer'), $data['stats']['errors']);
731 1191 }
732 1192 }
733 1193
734 1194 if (isset($data['timestamp'], $data['updated'])) {
@@ -735,15 +1195,15 @@
735 1195 /**
736 1196 * @var Util $util
737 1197 */
738 1198 $util = Container::getInstance()->get('util');
739 - $output .= ', Run Time: ' . $util->format_time($data['updated'] - $data['timestamp']);
1199 + $output .= sprintf(__(', Run Time: %s', 'jc-importer'), $util->format_time($data['updated'] - $data['timestamp']));
740 1200 } elseif (isset($data['duration'])) {
741 1201 /**
742 1202 * @var Util $util
743 1203 */
744 1204 $util = Container::getInstance()->get('util');
745 - $output .= ', Run Time: ' . $util->format_time($data['duration']);
1205 + $output .= sprintf(__(', Run Time: %s', 'jc-importer'), $util->format_time($data['duration']));
746 1206 }
747 1207
748 1208 return $output;
749 1209 }
@@ -767,8 +1227,10 @@
767 1227 $importer = $this->importer_manager->get_importer($id);
768 1228
769 1229 // TODO: Remove duplicate code, found in cron manager
770 1230
1231 + add_filter('upload_dir', [$this->importer_manager, 'set_custom_upload_path']);
1232 +
771 1233 switch ($action) {
772 1234 case 'file_upload':
773 1235 $attachment_id = $this->importer_manager->upload_file($importer, $_FILES['file']);
774 1236 break;
@@ -787,15 +1249,23 @@
787 1249 case 'file_local':
788 1250 $raw_source = $post_data['local_url'];
789 1251 $source = apply_filters('iwp/importer/datasource', $raw_source, $raw_source, $importer);
790 1252 $source = apply_filters('iwp/importer/datasource/local', $source, $raw_source, $importer);
791 - $attachment_id = $this->importer_manager->local_file($importer, $source);
1253 +
1254 + $filetype = $importer->getParser();
1255 + if (is_null($filetype)) {
1256 + $filetype = $post_data['filetype'];
1257 + }
1258 +
1259 + $attachment_id = $this->importer_manager->local_file($importer, $source, $filetype);
792 1260 break;
793 1261 default:
794 - $attachment_id = new \WP_Error('IWP_RM_1', 'Invalid form action');
1262 + $attachment_id = new \WP_Error('IWP_RM_1', __('Invalid form action', 'jc-importer'));
795 1263 break;
796 1264 }
797 1265
1266 + remove_filter('upload_dir', [$this->importer_manager, 'set_custom_upload_path']);
1267 +
798 1268 if (is_wp_error($attachment_id)) {
799 1269 return $this->http->end_rest_error($attachment_id->get_error_message());
800 1270 }
801 1271
@@ -809,9 +1279,9 @@
809 1279 $post_data = $this->sanitize($request->get_body_params());
810 1280
811 1281 $importer = $this->importer_manager->get_importer($id);
812 1282 if (!$importer) {
813 - return $this->http->end_rest_error('Invalid importer');
1283 + return $this->http->end_rest_error(__('Invalid importer', 'jc-importer'));
814 1284 }
815 1285
816 1286 $this->importer_manager->clear_config_files($id, true);
817 1287 $parser = $importer->getParser();
@@ -819,20 +1289,29 @@
819 1289 if ('xml' === $parser) {
820 1290
821 1291 $nodes = $this->importer_manager->process_xml_file($id, true);
822 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);
823 1297 } elseif ('csv' === $parser) {
824 1298
825 1299 $delimiter = isset($post_data['delimiter']) ? $post_data['delimiter'] : null;
826 1300 if (is_null($delimiter)) {
827 - $delimiter = ',';
1301 + $delimiter = $importer->getFileSetting('delimiter', ',');
828 1302 }
829 1303
830 1304 $enclosure = isset($post_data['enclosure']) ? $post_data['enclosure'] : null;
831 1305 if (is_null($enclosure)) {
832 - $enclosure = '"';
1306 + $enclosure = $importer->getFileSetting('enclosure', '"');
833 1307 }
834 1308
1309 + $escape = isset($post_data['escape']) ? $post_data['escape'] : null;
1310 + if (is_null($escape)) {
1311 + $escape = $importer->getFileSetting('escape', '\\');
1312 + }
1313 +
835 1314 $count = $this->importer_manager->process_csv_file($id, $delimiter, $enclosure, true);
836 1315
837 1316 // Need fresh importer, due to process_csv_file importer modifications might get overwritten.
838 1317 $importer = $this->importer_manager->get_importer($id);
@@ -859,9 +1338,9 @@
859 1338 $importer = $this->importer_manager->get_importer($id);
860 1339
861 1340 $import_file_exists = $this->filesystem->file_exists($importer->getFile());
862 1341 if (is_wp_error($import_file_exists)) {
863 - throw new \Exception("Unable to load preview, " . $import_file_exists->get_error_message());
1342 + throw new \Exception(sprintf(__("Unable to load preview, %s", 'jc-importer'), $import_file_exists->get_error_message()));
864 1343 }
865 1344
866 1345 $record_index = isset($post_data['record']) ? $post_data['record'] : null;
867 1346 if (is_null($record_index)) {
@@ -872,11 +1351,21 @@
872 1351 if (isset($post_data['file_encoding'])) {
873 1352 $importer->setFileSetting('file_encoding', $post_data['file_encoding']);
874 1353 }
875 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 +
876 1366 if ($importer->getParser() === 'xml') {
877 1367
878 - $config = $this->importer_manager->get_config($importer, true);
879 1368 $file = $this->importer_manager->get_xml_file($importer, $config);
880 1369
881 1370 $base_path = $post_data['base_path'];
882 1371 if (!is_null($base_path)) {
@@ -882,18 +1371,55 @@
882 1371 if (!is_null($base_path)) {
883 1372 $file->setRecordPath($base_path);
884 1373 }
885 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 +
886 1382 $preview = new XMLPreview($file, $base_path);
887 - $result = $preview->data();
1383 + $result = $preview->data($record_index);
888 1384 if (is_wp_error($result)) {
889 1385 return $this->http->end_rest_error($result);
890 1386 }
891 1387
892 - 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 + ]);
893 1420 } elseif ($importer->getParser() === 'csv') {
894 1421
895 - $config = $this->importer_manager->get_config($importer, true);
896 1422 $file = $this->importer_manager->get_csv_file($importer, $config);
897 1423
898 1424 $clear_config = false;
899 1425 if (!is_null($post_data['delimiter']) && $post_data['delimiter'] !== $file->getDelimiter()) {
@@ -899,13 +1425,16 @@
899 1425 if (!is_null($post_data['delimiter']) && $post_data['delimiter'] !== $file->getDelimiter()) {
900 1426 $clear_config = true;
901 1427 } elseif (!is_null($post_data['enclosure']) && $post_data['enclosure'] !== $file->getEnclosure()) {
902 1428 $clear_config = true;
1429 + } elseif (!is_null($post_data['escape']) && $post_data['escape'] !== $file->getEscape()) {
1430 + $clear_config = true;
903 1431 }
904 1432
905 1433 if ($clear_config) {
906 1434 $this->importer_manager->clear_config_files($importer->getId(), true);
907 1435 $config = $this->importer_manager->get_config($importer, true);
1436 + $config->set('preview_full_index', true);
908 1437 $file = $this->importer_manager->get_csv_file($importer, $config);
909 1438 }
910 1439
911 1440 $delimiter = $post_data['delimiter'];
@@ -917,8 +1446,13 @@
917 1446 if (!is_null($enclosure)) {
918 1447 $file->setEnclosure($enclosure);
919 1448 }
920 1449
1450 + $escape = $post_data['escape'];
1451 + if (!is_null($escape)) {
1452 + $file->setEscape($escape);
1453 + }
1454 +
921 1455 // parse bool param from string
922 1456 $show_headings = $post_data['show_headings'];
923 1457 if (in_array($show_headings, ['true', 'false'])) {
924 1458 $show_headings = $show_headings === 'true' ? true : false;
@@ -936,9 +1470,9 @@
936 1470
937 1471 return $this->http->end_rest_success($result);
938 1472 } else {
939 1473
940 - $result = apply_filters('iwp/file-preview/' . $importer->getParser(), null, $importer);
1474 + $result = apply_filters('iwp/file-preview/' . $importer->getParser(), null, $importer, $record_index);
941 1475 if (is_wp_error($result)) {
942 1476 return $this->http->end_rest_error($result);
943 1477 }
944 1478
@@ -956,24 +1490,31 @@
956 1490 $id = intval($request->get_param('id'));
957 1491 $importer = $this->importer_manager->get_importer($id);
958 1492 $import_file_exists = $this->filesystem->file_exists($importer->getFile());
959 1493 if (is_wp_error($import_file_exists)) {
960 - throw new \Exception("Unable to load preview, " . $import_file_exists->get_error_message());
1494 + throw new \Exception(sprintf(__("Unable to load preview, %s", 'jc-importer'), $import_file_exists->get_error_message()));
961 1495 }
962 1496
963 1497 $parser = $importer->getParser();
964 1498
965 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 + }
966 1505 if (is_null($fields) || empty($fields)) {
967 1506 $fields = $importer->getMap();
968 1507 }
969 1508
970 1509 if ('xml' === $parser) {
971 - $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);
972 1513 } elseif ('csv' === $parser) {
973 - $row = 0;
1514 + $row = $record_index;
974 1515 if ($importer->getFileSetting('show_headings') === true) {
975 - $row = 1;
1516 + $row = $record_index + 1;
976 1517 }
977 1518 $result = $this->importer_manager->preview_csv_file($importer, $fields, $row);
978 1519 } else {
979 1520
@@ -1013,8 +1554,13 @@
1013 1554 // This is used for storing version on imported records
1014 1555 $session_id = md5($importer_data->getId() . time());
1015 1556 update_post_meta($importer_data->getId(), '_iwp_session', $session_id);
1016 1557
1558 + if ('background' === $importer_data->getSetting('import_method')) {
1559 + update_post_meta($importer_data->getId(), '_iwp_cron_scheduled', current_time('timestamp'));
1560 + delete_post_meta($importer_data->getId(), '_iwp_cron_last_ran');
1561 + }
1562 +
1017 1563 Logger::clearRequestType();
1018 1564
1019 1565 return $this->http->end_rest_success([
1020 1566 'session' => $session_id
@@ -1047,25 +1593,10 @@
1047 1593 {
1048 1594 $id = intval($request->get_param('id'));
1049 1595 $paused = $this->sanitize($request->get_param('paused'), 'paused');
1050 1596
1051 - $importer_data = $this->importer_manager->get_importer($id);
1052 - $user = uniqid('iwp', true);
1597 + $state = $this->importer_manager->pause_import($id, $paused);
1053 1598
1054 - $state = ImporterState::wait_for_lock($importer_data->getId(), $user, function () use ($importer_data, $paused) {
1055 - $state = ImporterState::get_state($importer_data->getId());
1056 -
1057 -
1058 - if ($paused === 'no') {
1059 - $state['status'] = 'running';
1060 - } else {
1061 - $state['status'] = 'paused';
1062 - }
1063 -
1064 - ImporterState::set_state($importer_data->getId(), $state);
1065 - return $state;
1066 - });
1067 -
1068 1599 return $this->http->end_rest_success($state);
1069 1600 }
1070 1601
1071 1602 public function stop_import(\WP_REST_Request $request)
@@ -1071,19 +1602,10 @@
1071 1602 public function stop_import(\WP_REST_Request $request)
1072 1603 {
1073 1604 $id = intval($request->get_param('id'));
1074 1605
1075 - $importer_data = $this->importer_manager->get_importer($id);
1606 + $state = $this->importer_manager->stop_import($id);
1076 1607
1077 - $user = uniqid('iwp');
1078 -
1079 - $state = ImporterState::wait_for_lock($importer_data->getId(), $user, function () use ($importer_data) {
1080 - $state = ImporterState::get_state($importer_data->getId());
1081 - $state['status'] = 'cancelled';
1082 - ImporterState::set_state($importer_data->getId(), $state);
1083 - return $state;
1084 - });
1085 -
1086 1608 return $this->http->end_rest_success($state);
1087 1609 }
1088 1610
1089 1611 /**
@@ -1093,15 +1615,20 @@
1093 1615 * @return void
1094 1616 */
1095 1617 public function get_template_field_options(\WP_REST_Request $request)
1096 1618 {
1097 - $id = intval($request->get_param('id'));
1098 - $field = $this->sanitize($request->get_param('field'), 'field');
1619 + try {
1099 1620
1100 - $importer_data = $this->importer_manager->get_importer($id);
1101 - $template = $this->importer_manager->get_importer_template($id);
1621 + $id = intval($request->get_param('id'));
1622 + $field = $this->sanitize($request->get_param('field'), 'field');
1102 1623
1103 - return $this->http->end_rest_success($template->get_field_options($field, $importer_data));
1624 + $importer_data = $this->importer_manager->get_importer($id);
1625 + $template = $this->importer_manager->get_importer_template($id);
1626 +
1627 + return $this->http->end_rest_success($template->get_field_options($field, $importer_data));
1628 + } catch (\Exception $e) {
1629 + return $this->http->end_rest_error($e->getMessage());
1630 + }
1104 1631 }
1105 1632
1106 1633 public function get_logs(\WP_REST_Request $request)
1107 1634 {
@@ -1134,9 +1661,19 @@
1134 1661 }
1135 1662 $importer_data = $this->importer_manager->get_importer($id);
1136 1663 $log = $this->importer_manager->get_importer_debug_log($importer_data, $page, 100);
1137 1664
1138 - $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 +
1139 1676 return $this->http->end_rest_success(['log' => $log, 'download' => $download]);
1140 1677 }
1141 1678
1142 1679 public function save_settings(\WP_REST_Request $request)
@@ -1168,9 +1705,9 @@
1168 1705 $output[$setting] = $default['value'];
1169 1706 }
1170 1707 }
1171 1708
1172 - update_site_option('iwp_settings', $output);
1709 + update_option('iwp_settings', $output);
1173 1710
1174 1711 return $this->http->end_rest_success($this->properties->get_settings());
1175 1712 }
1176 1713
@@ -1190,9 +1727,9 @@
1190 1727
1191 1728 $templates = $this->importer_manager->get_templates();
1192 1729
1193 1730 if (!isset($templates[$importer_template])) {
1194 - return $this->http->end_rest_error("Invalid template \"" . $importer_template . "\"");
1731 + return $this->http->end_rest_error(sprintf(__("Invalid template \"%s\"", 'jc-importer'), $importer_template));
1195 1732 }
1196 1733
1197 1734 $template_id = $templates[$importer_template];
1198 1735 $template_class = $this->template_manager->load_template($template_id);
@@ -1202,8 +1739,9 @@
1202 1739 'label' => $template_class->get_name(),
1203 1740 'map' => $template_class->get_fields($importer_model),
1204 1741 'settings' => $template_class->register_settings(),
1205 1742 'options' => $template_class->register_options(),
1743 + 'permission_fields' => apply_filters('iwp/template/permission_fields', $template_class->get_permission_fields($importer_model), $template_class)
1206 1744 ];
1207 1745 return $this->http->end_rest_success($output);
1208 1746 }
1209 1747
@@ -1258,8 +1796,143 @@
1258 1796 echo $json;
1259 1797 die();
1260 1798 }
1261 1799
1800 + /**
1801 + * Download exporter config json file.
1802 + *
1803 + * File is used when creating an importer to auto generate field mapping.
1804 + *
1805 + * @param WP_REST_Request|null $request
1806 + * @return never
1807 + */
1808 + public function download_exporter_config(\WP_REST_Request $request = null)
1809 + {
1810 + $exporter_id = $request->get_param('id');
1811 + $exporter_data = $this->exporter_manager->get_exporter($exporter_id);
1812 + $mapper = $this->exporter_manager->get_exporter_mapper($exporter_data);
1813 + $fields = $mapper->get_fields();
1814 +
1815 +
1816 +
1817 + /**
1818 + * @var \WPDB $wpdb
1819 + */
1820 + global $wpdb;
1821 + $result = $wpdb->get_row("SELECT post_title, post_content from {$wpdb->posts} WHERE post_type='" . EWP_POST_TYPE . "' AND ID=" . intval($exporter_id), ARRAY_A);
1822 +
1823 + $output = [
1824 + 'name' => $result['post_title'],
1825 + 'data' => unserialize($result['post_content']),
1826 + 'fields' => $this->exporter_manager->get_importer_map_fields($exporter_data),
1827 + 'formatted_fields' => $fields
1828 + ];
1829 +
1830 + $json = json_encode($output);
1831 +
1832 + header('Content-disposition: attachment; filename="ImportWP-exporter-' . $exporter_id . '-' . date('Y-m-d') . '.json"');
1833 + header('Content-type: "application/json"; charset="utf8"');
1834 + header('Expires: 0');
1835 + header('Cache-Control: must-revalidate');
1836 + header('Pragma: public');
1837 + header('Content-Length: ' . strlen($json));
1838 +
1839 + echo $json;
1840 + die();
1841 + }
1842 +
1843 + public function read_exporter_config(\WP_REST_Request $request = null)
1844 + {
1845 + $upload = $this->filesystem->upload_file($_FILES['file']);
1846 + if (is_wp_error($upload)) {
1847 + return $this->http->end_rest_error($upload->get_error_message());
1848 + }
1849 +
1850 + $json_contents = file_get_contents($upload['dest']);
1851 + @unlink($upload['dest']);
1852 +
1853 + $contents = json_decode($json_contents, true);
1854 +
1855 + if (!isset($contents['name'], $contents['fields'], $contents['data'], $contents['data']['file_type'])) {
1856 + return $this->http->end_rest_error("Invalid exporter config file.");
1857 + }
1858 +
1859 + if (!in_array($contents['data']['file_type'], ['xml', 'csv', 'json'])) {
1860 + return $this->http->end_rest_error("Exporter file type is not supported.");
1861 + }
1862 +
1863 + return $this->http->end_rest_success(['exporter' => $contents]);
1864 + }
1865 +
1866 + public function get_compatibility_settings()
1867 + {
1868 + $output = [];
1869 +
1870 + if (!function_exists('get_plugins')) {
1871 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
1872 + }
1873 +
1874 + $blacklisted = (array)get_option('iwp_compat_blacklist', []);
1875 + $whitelisted = apply_filters('iwp/compat/whitelist', [
1876 + 'jc-importer/jc-importer.php'
1877 + ]);
1878 +
1879 + $all_plugins = get_plugins();
1880 + foreach ($all_plugins as $plugin_id => $plugin_data) {
1881 +
1882 +
1883 +
1884 + if (preg_match('/^importwp-/', $plugin_id, $matches) === 1 || in_array($plugin_id, $whitelisted)) {
1885 + continue;
1886 + }
1887 +
1888 + $output[$plugin_id] = [
1889 + 'id' => $plugin_id,
1890 + 'name' => $plugin_data['Name'],
1891 + 'enabled' => in_array($plugin_id, $blacklisted) ? 'yes' : 'no'
1892 + ];
1893 + }
1894 +
1895 + return $output;
1896 + }
1897 +
1898 + public function get_compatibility(\WP_REST_Request $request = null)
1899 + {
1900 + $output = $this->get_compatibility_settings();
1901 +
1902 + return $this->http->end_rest_success($output);
1903 + }
1904 +
1905 + public function save_compatibility(\WP_REST_Request $request)
1906 + {
1907 + $post_data = $request->get_body_params();
1908 + $post_data = $this->sanitize($post_data);
1909 +
1910 + if (!function_exists('get_plugins')) {
1911 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
1912 + }
1913 + $plugin_keys = array_keys(get_plugins());
1914 +
1915 +
1916 + $tmp = [];
1917 +
1918 + if (isset($post_data['plugins'])) {
1919 +
1920 + foreach ($post_data['plugins'] as $plugin_id) {
1921 + if (!in_array($plugin_id, $plugin_keys)) {
1922 + continue;
1923 + }
1924 +
1925 + $tmp[] = "" . $plugin_id;
1926 + }
1927 + }
1928 +
1929 + update_option('iwp_compat_blacklist', $tmp);
1930 +
1931 + $output = $this->get_compatibility_settings();
1932 + return $this->http->end_rest_success($output);
1933 + }
1934 +
1262 1935 public function import_exporters(\WP_REST_Request $request = null)
1263 1936 {
1264 1937 Logger::setRequestType('import_exporters');
1265 1938
@@ -1281,9 +1954,9 @@
1281 1954 'post_type' => IWP_POST_TYPE,
1282 1955 'post_title' => $importer['name'],
1283 1956 'post_status' => 'publish',
1284 1957 'post_author' => get_current_user_id(),
1285 - 'post_content' => serialize($importer['data'])
1958 + 'post_content' => wp_slash(serialize($importer['data']))
1286 1959 ], true);
1287 1960
1288 1961 if (is_wp_error($result)) {
1289 1962 $errors[] = $result->get_error_message();
@@ -1294,12 +1967,12 @@
1294 1967 }
1295 1968 }
1296 1969
1297 1970 if (empty($counter)) {
1298 - return $this->http->end_rest_error('No Importers found.');
1971 + return $this->http->end_rest_error(__('No Importers found.', 'jc-importer'));
1299 1972 }
1300 1973
1301 - return $this->http->end_rest_success("Import Complete, {$counter} Importer" . ($counter > 1 ? 's' : '') . '.');
1974 + return $this->http->end_rest_success(sprintf(_n("Import Complete, %d Importer", "Import Complete, %d Importers", $counter, 'jc-importer'), $counter));
1302 1975 }
1303 1976
1304 1977 public function save_exporter(\WP_REST_Request $request = null)
1305 1978 {
@@ -1322,8 +1995,12 @@
1322 1995 if (isset($post_data['file_type'])) {
1323 1996 $exporter->setFileType($post_data['file_type']);
1324 1997 }
1325 1998
1999 + if (isset($post_data['file_settings'])) {
2000 + $exporter->setFileSettings($post_data['file_settings']);
2001 + }
2002 +
1326 2003 if (isset($post_data['fields']) || $id > 0) {
1327 2004 $exporter->setFields(isset($post_data['fields']) ? $post_data['fields'] : []);
1328 2005 }
1329 2006
@@ -1386,8 +2063,38 @@
1386 2063 $this->exporter_manager->delete_exporter($id);
1387 2064 return $this->http->end_rest_success(true);
1388 2065 }
1389 2066
2067 + /**
2068 + * Trigger a new export.
2069 + *
2070 + * @param \WP_REST_Request $request
2071 + * @return array
2072 + */
2073 + public function init_export(\WP_REST_Request $request)
2074 + {
2075 + $id = intval($request->get_param('id'));
2076 +
2077 + Logger::setRequestType('rest::init_export');
2078 +
2079 + Logger::write('init_import -start', $id);
2080 +
2081 + $exporter_data = $this->exporter_manager->get_exporter($id);
2082 +
2083 + // clear existing
2084 + ExporterState::clear_options($exporter_data->getId());
2085 +
2086 + // This is used for storing version on imported records
2087 + $session_id = md5($exporter_data->getId() . time());
2088 + update_post_meta($exporter_data->getId(), '_iwp_session', $session_id);
2089 +
2090 + Logger::clearRequestType();
2091 +
2092 + return $this->http->end_rest_success([
2093 + 'session' => $session_id
2094 + ]);
2095 + }
2096 +
1390 2097 public function run_exporter(\WP_REST_Request $request)
1391 2098 {
1392 2099 Logger::setRequestType('run_exporter');
1393 2100
@@ -1394,12 +2101,13 @@
1394 2101 $this->http->set_stream_headers();
1395 2102
1396 2103 $id = intval($request->get_param('id'));
1397 2104 $session = $request->get_param('session');
2105 + $user = uniqid('iwp', true);
1398 2106
1399 2107 $exporter_data = $this->exporter_manager->get_exporter($id);
1400 2108
1401 - $status = $this->exporter_manager->export($exporter_data, $session);
2109 + $status = $this->exporter_manager->export($exporter_data, $user, $session);
1402 2110 echo json_encode($status);
1403 2111 // Logger::write(__CLASS__ . '::run_import -import=end -status=' . $status->get_status(), $id);
1404 2112
1405 2113 // if (!empty($this->output_cache)) {
@@ -1434,24 +2142,37 @@
1434 2142 if (!empty($query->posts)) {
1435 2143 foreach ($query->posts as $post_id) {
1436 2144
1437 2145 $exporter = $this->exporter_manager->get_exporter($post_id);
1438 - $status = $exporter->get_status();
1439 - $status['id'] = $post_id;
2146 + $status = ExporterState::get_state($post_id);
2147 + $status['exporter'] = $post_id;
1440 2148 $status['version'] = 2;
1441 2149
2150 + if (isset($status['progress'])) {
2151 + $current = $status['progress']['export']['current_row'];
2152 + $end = $status['progress']['export']['end'] - $status['progress']['export']['start'];
2153 + $status['progress'] = $end > 0 ? ($current / $end) * 100 : 0;
2154 + } else {
2155 + $status['progress'] = 0;
2156 + }
2157 +
2158 +
1442 2159 $output = '';
1443 2160
1444 - switch ($status['status']) {
1445 - case 'cancelled':
1446 - $output .= 'Export cancelled';
1447 - break;
1448 - case 'complete':
1449 - $output .= 'Export complete';
1450 - break;
1451 - case 'running':
1452 - $output .= 'Exporting: ' . $status['count'] . '/' . $status['total'];
1453 - break;
2161 + if (isset($status['status'])) {
2162 + switch ($status['status']) {
2163 + case 'cancelled':
2164 + $output .= 'Export cancelled';
2165 + break;
2166 + case 'complete':
2167 + $output .= 'Export complete';
2168 + break;
2169 + case 'processing':
2170 + case 'timeout':
2171 + case 'running':
2172 + $output .= 'Exporting: ' . $current . '/' . $end;
2173 + break;
2174 + }
1454 2175 }
1455 2176
1456 2177 $status['message'] = $output;
1457 2178
@@ -1460,6 +2181,44 @@
1460 2181 }
1461 2182
1462 2183 echo json_encode($result) . "\n";
1463 2184 die();
2185 + }
2186 +
2187 + public function get_importer_unique_identifier_options(\WP_REST_Request $request)
2188 + {
2189 + $id = intval($request->get_param('id'));
2190 + $importer_model = $this->importer_manager->get_importer($id);
2191 + if (!$importer_model) {
2192 + return $this->http->end_rest_error(['msg' => 'Invalid importer']);
2193 + }
2194 +
2195 + // Get default template options
2196 + $template = $this->importer_manager->get_template($importer_model->getTemplate());
2197 + if (is_wp_error($template)) {
2198 + return $this->http->end_rest_error($template->get_error_message());
2199 + }
2200 +
2201 + $template_class = $this->template_manager->load_template($template);
2202 + $unique_fields = $this->template_manager->get_template_unique_fields($template_class);
2203 + $options = $template_class->get_unique_identifier_options($importer_model, $unique_fields);
2204 +
2205 + // Only add in unqiue fields if they have not been found.
2206 + // Allowing for old templates to continue to list unique identifiers.
2207 + foreach ($unique_fields as $field_id) {
2208 + if (!isset($options[$field_id])) {
2209 + $options[$field_id] = [
2210 + 'value' => $field_id,
2211 + 'label' => $field_id,
2212 + 'uid' => true,
2213 + 'active' => true,
2214 + ];
2215 + }
2216 + }
2217 +
2218 + $options = array_filter($options, function ($item) {
2219 + return $item['active'];
2220 + });
2221 +
2222 + return $this->http->end_rest_success(['options' => array_values($options), 'unique_fields' => array_values($unique_fields)]);
1464 2223 }
1465 2224 }