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 +887 -100 2.7.6 → 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']);
@@ -524,18 +871,43 @@
524 871 $importer->setFileSetting('file_encoding', $post_data['file_settings_encoding']);
525 872 }
526 873
527 874 if ($parser === 'csv') {
875 +
876 + $clear_config = false;
528 877 if (isset($post_data['file_settings_delimiter'])) {
878 +
879 + $old_delimiter = $importer->getFileSetting('delimiter');
880 + if ($old_delimiter !== $post_data['file_settings_delimiter']) {
881 + $clear_config = true;
882 + }
529 883 $importer->setFileSetting('delimiter', $post_data['file_settings_delimiter']);
530 884 }
531 885 if (isset($post_data['file_settings_enclosure'])) {
886 + $old_enclosure = $importer->getFileSetting('enclosure');
887 + if ($old_enclosure !== $post_data['file_settings_enclosure']) {
888 + $clear_config = true;
889 + }
532 890 $importer->setFileSetting('enclosure', $post_data['file_settings_enclosure']);
533 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 +
534 902 if (isset($post_data['file_settings_show_headings'])) {
535 - $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']));
536 904 }
537 - } elseif ($parser === 'xml') {
905 +
906 + if ($clear_config) {
907 + $this->importer_manager->clear_config_files($importer->getId(), true);
908 + }
909 + } elseif ($parser === 'xml' || $parser === 'json') {
538 910 if (isset($post_data['file_settings_base_path'])) {
539 911 $importer->setFileSetting('base_path', $post_data['file_settings_base_path']);
540 912 }
541 913 }
@@ -540,25 +912,37 @@
540 912 }
541 913 }
542 914
543 915 if (isset($post_data['file_settings_setup'])) {
544 - $importer->setFileSetting('setup', $post_data['file_settings_setup'] === 'true' ? true : false);
916 + $importer->setFileSetting('setup', $this->is_truthy($post_data['file_settings_setup']));
545 917 }
546 918
547 919 if (isset($post_data['map']) && is_array($post_data['map'])) {
920 + $map = [];
548 921 foreach ($post_data['map'] as $key => $value) {
922 + if ($value === null) {
923 + continue;
924 + }
549 925
550 926 if (1 === preg_match('/\._index$/', $key)) {
551 927 $value = intval($value);
552 928 }
553 929
554 - $importer->setMap($key, $value);
930 + $map[$key] = $value;
555 931 }
932 + $importer->replaceMap($map);
556 933 }
557 934
558 935 if (isset($post_data['enabled']) && is_array($post_data['enabled'])) {
559 936 foreach ($post_data['enabled'] as $key => $value) {
560 - 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)) {
561 945 $importer->setEnabled($key);
562 946 } else {
563 947 $importer->setEnabled($key, false);
564 948 }
@@ -570,9 +954,9 @@
570 954 // create
571 955 if (isset($post_data['permissions']['create'])) {
572 956 $create = $post_data['permissions']['create'];
573 957 $importer->setPermission('create', [
574 - 'enabled' => isset($create['enabled']) && $create['enabled'] === 'true' ? true : false,
958 + 'enabled' => isset($create['enabled']) && $this->is_truthy($create['enabled']) ? true : false,
575 959 'type' => isset($create['type']) && in_array($create['type'], ['include', 'exclude'], true) ? $create['type'] : null,
576 960 'fields' => isset($create['fields']) ? $create['fields'] : [],
577 961 ]);
578 962 }
@@ -580,9 +964,9 @@
580 964 // update
581 965 if (isset($post_data['permissions']['update'])) {
582 966 $update = $post_data['permissions']['update'];
583 967 $importer->setPermission('update', [
584 - 'enabled' => isset($update['enabled']) && $update['enabled'] === 'true' ? true : false,
968 + 'enabled' => isset($update['enabled']) && $this->is_truthy($update['enabled']) ? true : false,
585 969 'type' => isset($update['type']) && in_array($update['type'], ['include', 'exclude'], true) ? $update['type'] : null,
586 970 'fields' => isset($update['fields']) ? $update['fields'] : [],
587 971 ]);
588 972 }
@@ -589,10 +973,11 @@
589 973
590 974 // remove
591 975 if (isset($post_data['permissions']['remove'])) {
592 976 $importer->setPermission('remove', [
593 - 'enabled' => $post_data['permissions']['remove']['enabled'] === 'true',
594 - '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']),
595 980 ]);
596 981 }
597 982 }
598 983
@@ -602,8 +987,88 @@
602 987 }
603 988 return $this->http->end_rest_success($importer->data());
604 989 }
605 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 +
606 1071 private function sanitize_setting($value)
607 1072 {
608 1073 if (is_array($value)) {
609 1074 foreach ($value as &$tmp) {
@@ -646,14 +1111,25 @@
646 1111
647 1112 foreach ($query->posts as $importer_id) {
648 1113
649 1114 $importer_model = $this->importer_manager->get_importer($importer_id);
1115 + $config = $this->importer_manager->get_config($importer_model);
650 1116
651 1117 $output = ImporterState::get_state($importer_id);
652 - $output['version'] = 2;
653 - $output['message'] = $this->generate_status_message($output);
654 - $output['importer'] = $importer_id;
1118 + if (!$output) {
1119 + $output = [
1120 + 'version' => 2,
1121 + 'message' => 'Not setup',
1122 + 'importer' => $importer_id
1123 + ];
1124 + } else {
655 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 +
656 1132 $result[] = $this->event_handler->run('iwp/importer/status/output', [$output, $importer_model]);
657 1133 }
658 1134
659 1135 echo json_encode($result) . "\n";
@@ -674,12 +1150,12 @@
674 1150
675 1151 if (isset($data['status'])) {
676 1152 switch ($data['status']) {
677 1153 case 'cancelled':
678 - $output .= 'Import cancelled';
1154 + $output .= __('Import cancelled', 'jc-importer');
679 1155 break;
680 1156 case 'complete':
681 - $output .= 'Import complete';
1157 + $output .= __('Import complete', 'jc-importer');
682 1158 break;
683 1159 case 'error':
684 1160 $output = $data['message'];
685 1161 return $output;
@@ -686,15 +1162,14 @@
686 1162 break;
687 1163 }
688 1164 }
689 1165
690 - 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'])) {
691 1167 switch ($data['section']) {
692 1168 case 'import':
693 1169 case 'delete':
694 - $output = $data['section'] === 'import' ? 'Importing: ' : 'Deleting: ';
695 - $output .= $data['progress'][$data['section']]['current_row'] . ' / ' . ($data['progress'][$data['section']]['end'] - $data['progress'][$data['section']]['start']);
696 -
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);
697 1172 break;
698 1173 }
699 1174 }
700 1175
@@ -699,21 +1174,21 @@
699 1174 }
700 1175
701 1176 if (isset($data['stats'])) {
702 1177 if ($data['stats']['inserts'] > 0) {
703 - $output .= sprintf(', Inserts: %d', $data['stats']['inserts']);
1178 + $output .= sprintf(__(', Inserts: %d', 'jc-importer'), $data['stats']['inserts']);
704 1179 }
705 1180 if ($data['stats']['updates'] > 0) {
706 - $output .= sprintf(', Updates: %d', $data['stats']['updates']);
1181 + $output .= sprintf(__(', Updates: %d', 'jc-importer'), $data['stats']['updates']);
707 1182 }
708 1183 if ($data['stats']['deletes'] > 0) {
709 - $output .= sprintf(', Deletes: %d', $data['stats']['deletes']);
1184 + $output .= sprintf(__(', Deletes: %d', 'jc-importer'), $data['stats']['deletes']);
710 1185 }
711 1186 if ($data['stats']['skips'] > 0) {
712 - $output .= sprintf(', Skips: %d', $data['stats']['skips']);
1187 + $output .= sprintf(__(', Skips: %d', 'jc-importer'), $data['stats']['skips']);
713 1188 }
714 1189 if ($data['stats']['errors'] > 0) {
715 - $output .= sprintf(', Errors: %d', $data['stats']['errors']);
1190 + $output .= sprintf(__(', Errors: %d', 'jc-importer'), $data['stats']['errors']);
716 1191 }
717 1192 }
718 1193
719 1194 if (isset($data['timestamp'], $data['updated'])) {
@@ -720,15 +1195,15 @@
720 1195 /**
721 1196 * @var Util $util
722 1197 */
723 1198 $util = Container::getInstance()->get('util');
724 - $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']));
725 1200 } elseif (isset($data['duration'])) {
726 1201 /**
727 1202 * @var Util $util
728 1203 */
729 1204 $util = Container::getInstance()->get('util');
730 - $output .= ', Run Time: ' . $util->format_time($data['duration']);
1205 + $output .= sprintf(__(', Run Time: %s', 'jc-importer'), $util->format_time($data['duration']));
731 1206 }
732 1207
733 1208 return $output;
734 1209 }
@@ -752,8 +1227,10 @@
752 1227 $importer = $this->importer_manager->get_importer($id);
753 1228
754 1229 // TODO: Remove duplicate code, found in cron manager
755 1230
1231 + add_filter('upload_dir', [$this->importer_manager, 'set_custom_upload_path']);
1232 +
756 1233 switch ($action) {
757 1234 case 'file_upload':
758 1235 $attachment_id = $this->importer_manager->upload_file($importer, $_FILES['file']);
759 1236 break;
@@ -772,15 +1249,23 @@
772 1249 case 'file_local':
773 1250 $raw_source = $post_data['local_url'];
774 1251 $source = apply_filters('iwp/importer/datasource', $raw_source, $raw_source, $importer);
775 1252 $source = apply_filters('iwp/importer/datasource/local', $source, $raw_source, $importer);
776 - $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);
777 1260 break;
778 1261 default:
779 - $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'));
780 1263 break;
781 1264 }
782 1265
1266 + remove_filter('upload_dir', [$this->importer_manager, 'set_custom_upload_path']);
1267 +
783 1268 if (is_wp_error($attachment_id)) {
784 1269 return $this->http->end_rest_error($attachment_id->get_error_message());
785 1270 }
786 1271
@@ -794,9 +1279,9 @@
794 1279 $post_data = $this->sanitize($request->get_body_params());
795 1280
796 1281 $importer = $this->importer_manager->get_importer($id);
797 1282 if (!$importer) {
798 - return $this->http->end_rest_error('Invalid importer');
1283 + return $this->http->end_rest_error(__('Invalid importer', 'jc-importer'));
799 1284 }
800 1285
801 1286 $this->importer_manager->clear_config_files($id, true);
802 1287 $parser = $importer->getParser();
@@ -804,20 +1289,29 @@
804 1289 if ('xml' === $parser) {
805 1290
806 1291 $nodes = $this->importer_manager->process_xml_file($id, true);
807 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);
808 1297 } elseif ('csv' === $parser) {
809 1298
810 1299 $delimiter = isset($post_data['delimiter']) ? $post_data['delimiter'] : null;
811 1300 if (is_null($delimiter)) {
812 - $delimiter = ',';
1301 + $delimiter = $importer->getFileSetting('delimiter', ',');
813 1302 }
814 1303
815 1304 $enclosure = isset($post_data['enclosure']) ? $post_data['enclosure'] : null;
816 1305 if (is_null($enclosure)) {
817 - $enclosure = '"';
1306 + $enclosure = $importer->getFileSetting('enclosure', '"');
818 1307 }
819 1308
1309 + $escape = isset($post_data['escape']) ? $post_data['escape'] : null;
1310 + if (is_null($escape)) {
1311 + $escape = $importer->getFileSetting('escape', '\\');
1312 + }
1313 +
820 1314 $count = $this->importer_manager->process_csv_file($id, $delimiter, $enclosure, true);
821 1315
822 1316 // Need fresh importer, due to process_csv_file importer modifications might get overwritten.
823 1317 $importer = $this->importer_manager->get_importer($id);
@@ -844,9 +1338,9 @@
844 1338 $importer = $this->importer_manager->get_importer($id);
845 1339
846 1340 $import_file_exists = $this->filesystem->file_exists($importer->getFile());
847 1341 if (is_wp_error($import_file_exists)) {
848 - 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()));
849 1343 }
850 1344
851 1345 $record_index = isset($post_data['record']) ? $post_data['record'] : null;
852 1346 if (is_null($record_index)) {
@@ -857,11 +1351,21 @@
857 1351 if (isset($post_data['file_encoding'])) {
858 1352 $importer->setFileSetting('file_encoding', $post_data['file_encoding']);
859 1353 }
860 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 +
861 1366 if ($importer->getParser() === 'xml') {
862 1367
863 - $config = $this->importer_manager->get_config($importer, true);
864 1368 $file = $this->importer_manager->get_xml_file($importer, $config);
865 1369
866 1370 $base_path = $post_data['base_path'];
867 1371 if (!is_null($base_path)) {
@@ -867,20 +1371,73 @@
867 1371 if (!is_null($base_path)) {
868 1372 $file->setRecordPath($base_path);
869 1373 }
870 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 +
871 1382 $preview = new XMLPreview($file, $base_path);
872 - $result = $preview->data();
1383 + $result = $preview->data($record_index);
873 1384 if (is_wp_error($result)) {
874 1385 return $this->http->end_rest_error($result);
875 1386 }
876 1387
877 - 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 + ]);
878 1420 } elseif ($importer->getParser() === 'csv') {
879 1421
880 - $config = $this->importer_manager->get_config($importer, true);
881 1422 $file = $this->importer_manager->get_csv_file($importer, $config);
882 1423
1424 + $clear_config = false;
1425 + if (!is_null($post_data['delimiter']) && $post_data['delimiter'] !== $file->getDelimiter()) {
1426 + $clear_config = true;
1427 + } elseif (!is_null($post_data['enclosure']) && $post_data['enclosure'] !== $file->getEnclosure()) {
1428 + $clear_config = true;
1429 + } elseif (!is_null($post_data['escape']) && $post_data['escape'] !== $file->getEscape()) {
1430 + $clear_config = true;
1431 + }
1432 +
1433 + if ($clear_config) {
1434 + $this->importer_manager->clear_config_files($importer->getId(), true);
1435 + $config = $this->importer_manager->get_config($importer, true);
1436 + $config->set('preview_full_index', true);
1437 + $file = $this->importer_manager->get_csv_file($importer, $config);
1438 + }
1439 +
883 1440 $delimiter = $post_data['delimiter'];
884 1441 if (!is_null($delimiter)) {
885 1442 $file->setDelimiter($delimiter);
886 1443 }
@@ -889,8 +1446,13 @@
889 1446 if (!is_null($enclosure)) {
890 1447 $file->setEnclosure($enclosure);
891 1448 }
892 1449
1450 + $escape = $post_data['escape'];
1451 + if (!is_null($escape)) {
1452 + $file->setEscape($escape);
1453 + }
1454 +
893 1455 // parse bool param from string
894 1456 $show_headings = $post_data['show_headings'];
895 1457 if (in_array($show_headings, ['true', 'false'])) {
896 1458 $show_headings = $show_headings === 'true' ? true : false;
@@ -908,9 +1470,9 @@
908 1470
909 1471 return $this->http->end_rest_success($result);
910 1472 } else {
911 1473
912 - $result = apply_filters('iwp/file-preview/' . $importer->getParser(), null, $importer);
1474 + $result = apply_filters('iwp/file-preview/' . $importer->getParser(), null, $importer, $record_index);
913 1475 if (is_wp_error($result)) {
914 1476 return $this->http->end_rest_error($result);
915 1477 }
916 1478
@@ -928,24 +1490,31 @@
928 1490 $id = intval($request->get_param('id'));
929 1491 $importer = $this->importer_manager->get_importer($id);
930 1492 $import_file_exists = $this->filesystem->file_exists($importer->getFile());
931 1493 if (is_wp_error($import_file_exists)) {
932 - 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()));
933 1495 }
934 1496
935 1497 $parser = $importer->getParser();
936 1498
937 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 + }
938 1505 if (is_null($fields) || empty($fields)) {
939 1506 $fields = $importer->getMap();
940 1507 }
941 1508
942 1509 if ('xml' === $parser) {
943 - $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);
944 1513 } elseif ('csv' === $parser) {
945 - $row = 0;
1514 + $row = $record_index;
946 1515 if ($importer->getFileSetting('show_headings') === true) {
947 - $row = 1;
1516 + $row = $record_index + 1;
948 1517 }
949 1518 $result = $this->importer_manager->preview_csv_file($importer, $fields, $row);
950 1519 } else {
951 1520
@@ -985,8 +1554,13 @@
985 1554 // This is used for storing version on imported records
986 1555 $session_id = md5($importer_data->getId() . time());
987 1556 update_post_meta($importer_data->getId(), '_iwp_session', $session_id);
988 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 +
989 1563 Logger::clearRequestType();
990 1564
991 1565 return $this->http->end_rest_success([
992 1566 'session' => $session_id
@@ -1019,25 +1593,10 @@
1019 1593 {
1020 1594 $id = intval($request->get_param('id'));
1021 1595 $paused = $this->sanitize($request->get_param('paused'), 'paused');
1022 1596
1023 - $importer_data = $this->importer_manager->get_importer($id);
1024 - $user = uniqid('iwp', true);
1597 + $state = $this->importer_manager->pause_import($id, $paused);
1025 1598
1026 - $state = ImporterState::wait_for_lock($importer_data->getId(), $user, function () use ($importer_data, $paused) {
1027 - $state = ImporterState::get_state($importer_data->getId());
1028 -
1029 -
1030 - if ($paused === 'no') {
1031 - $state['status'] = 'running';
1032 - } else {
1033 - $state['status'] = 'paused';
1034 - }
1035 -
1036 - ImporterState::set_state($importer_data->getId(), $state);
1037 - return $state;
1038 - });
1039 -
1040 1599 return $this->http->end_rest_success($state);
1041 1600 }
1042 1601
1043 1602 public function stop_import(\WP_REST_Request $request)
@@ -1043,19 +1602,10 @@
1043 1602 public function stop_import(\WP_REST_Request $request)
1044 1603 {
1045 1604 $id = intval($request->get_param('id'));
1046 1605
1047 - $importer_data = $this->importer_manager->get_importer($id);
1606 + $state = $this->importer_manager->stop_import($id);
1048 1607
1049 - $user = uniqid('iwp');
1050 -
1051 - $state = ImporterState::wait_for_lock($importer_data->getId(), $user, function () use ($importer_data) {
1052 - $state = ImporterState::get_state($importer_data->getId());
1053 - $state['status'] = 'cancelled';
1054 - ImporterState::set_state($importer_data->getId(), $state);
1055 - return $state;
1056 - });
1057 -
1058 1608 return $this->http->end_rest_success($state);
1059 1609 }
1060 1610
1061 1611 /**
@@ -1065,15 +1615,20 @@
1065 1615 * @return void
1066 1616 */
1067 1617 public function get_template_field_options(\WP_REST_Request $request)
1068 1618 {
1069 - $id = intval($request->get_param('id'));
1070 - $field = $this->sanitize($request->get_param('field'), 'field');
1619 + try {
1071 1620
1072 - $importer_data = $this->importer_manager->get_importer($id);
1073 - $template = $this->importer_manager->get_importer_template($id);
1621 + $id = intval($request->get_param('id'));
1622 + $field = $this->sanitize($request->get_param('field'), 'field');
1074 1623
1075 - 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 + }
1076 1631 }
1077 1632
1078 1633 public function get_logs(\WP_REST_Request $request)
1079 1634 {
@@ -1106,9 +1661,19 @@
1106 1661 }
1107 1662 $importer_data = $this->importer_manager->get_importer($id);
1108 1663 $log = $this->importer_manager->get_importer_debug_log($importer_data, $page, 100);
1109 1664
1110 - $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 +
1111 1676 return $this->http->end_rest_success(['log' => $log, 'download' => $download]);
1112 1677 }
1113 1678
1114 1679 public function save_settings(\WP_REST_Request $request)
@@ -1140,9 +1705,9 @@
1140 1705 $output[$setting] = $default['value'];
1141 1706 }
1142 1707 }
1143 1708
1144 - update_site_option('iwp_settings', $output);
1709 + update_option('iwp_settings', $output);
1145 1710
1146 1711 return $this->http->end_rest_success($this->properties->get_settings());
1147 1712 }
1148 1713
@@ -1162,9 +1727,9 @@
1162 1727
1163 1728 $templates = $this->importer_manager->get_templates();
1164 1729
1165 1730 if (!isset($templates[$importer_template])) {
1166 - 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));
1167 1732 }
1168 1733
1169 1734 $template_id = $templates[$importer_template];
1170 1735 $template_class = $this->template_manager->load_template($template_id);
@@ -1174,8 +1739,9 @@
1174 1739 'label' => $template_class->get_name(),
1175 1740 'map' => $template_class->get_fields($importer_model),
1176 1741 'settings' => $template_class->register_settings(),
1177 1742 'options' => $template_class->register_options(),
1743 + 'permission_fields' => apply_filters('iwp/template/permission_fields', $template_class->get_permission_fields($importer_model), $template_class)
1178 1744 ];
1179 1745 return $this->http->end_rest_success($output);
1180 1746 }
1181 1747
@@ -1230,8 +1796,143 @@
1230 1796 echo $json;
1231 1797 die();
1232 1798 }
1233 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 +
1234 1935 public function import_exporters(\WP_REST_Request $request = null)
1235 1936 {
1236 1937 Logger::setRequestType('import_exporters');
1237 1938
@@ -1253,9 +1954,9 @@
1253 1954 'post_type' => IWP_POST_TYPE,
1254 1955 'post_title' => $importer['name'],
1255 1956 'post_status' => 'publish',
1256 1957 'post_author' => get_current_user_id(),
1257 - 'post_content' => serialize($importer['data'])
1958 + 'post_content' => wp_slash(serialize($importer['data']))
1258 1959 ], true);
1259 1960
1260 1961 if (is_wp_error($result)) {
1261 1962 $errors[] = $result->get_error_message();
@@ -1266,12 +1967,12 @@
1266 1967 }
1267 1968 }
1268 1969
1269 1970 if (empty($counter)) {
1270 - return $this->http->end_rest_error('No Importers found.');
1971 + return $this->http->end_rest_error(__('No Importers found.', 'jc-importer'));
1271 1972 }
1272 1973
1273 - 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));
1274 1975 }
1275 1976
1276 1977 public function save_exporter(\WP_REST_Request $request = null)
1277 1978 {
@@ -1294,8 +1995,12 @@
1294 1995 if (isset($post_data['file_type'])) {
1295 1996 $exporter->setFileType($post_data['file_type']);
1296 1997 }
1297 1998
1999 + if (isset($post_data['file_settings'])) {
2000 + $exporter->setFileSettings($post_data['file_settings']);
2001 + }
2002 +
1298 2003 if (isset($post_data['fields']) || $id > 0) {
1299 2004 $exporter->setFields(isset($post_data['fields']) ? $post_data['fields'] : []);
1300 2005 }
1301 2006
@@ -1358,8 +2063,38 @@
1358 2063 $this->exporter_manager->delete_exporter($id);
1359 2064 return $this->http->end_rest_success(true);
1360 2065 }
1361 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 +
1362 2097 public function run_exporter(\WP_REST_Request $request)
1363 2098 {
1364 2099 Logger::setRequestType('run_exporter');
1365 2100
@@ -1366,12 +2101,13 @@
1366 2101 $this->http->set_stream_headers();
1367 2102
1368 2103 $id = intval($request->get_param('id'));
1369 2104 $session = $request->get_param('session');
2105 + $user = uniqid('iwp', true);
1370 2106
1371 2107 $exporter_data = $this->exporter_manager->get_exporter($id);
1372 2108
1373 - $status = $this->exporter_manager->export($exporter_data, $session);
2109 + $status = $this->exporter_manager->export($exporter_data, $user, $session);
1374 2110 echo json_encode($status);
1375 2111 // Logger::write(__CLASS__ . '::run_import -import=end -status=' . $status->get_status(), $id);
1376 2112
1377 2113 // if (!empty($this->output_cache)) {
@@ -1406,24 +2142,37 @@
1406 2142 if (!empty($query->posts)) {
1407 2143 foreach ($query->posts as $post_id) {
1408 2144
1409 2145 $exporter = $this->exporter_manager->get_exporter($post_id);
1410 - $status = $exporter->get_status();
1411 - $status['id'] = $post_id;
2146 + $status = ExporterState::get_state($post_id);
2147 + $status['exporter'] = $post_id;
1412 2148 $status['version'] = 2;
1413 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 +
1414 2159 $output = '';
1415 2160
1416 - switch ($status['status']) {
1417 - case 'cancelled':
1418 - $output .= 'Export cancelled';
1419 - break;
1420 - case 'complete':
1421 - $output .= 'Export complete';
1422 - break;
1423 - case 'running':
1424 - $output .= 'Exporting: ' . $status['count'] . '/' . $status['total'];
1425 - 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 + }
1426 2175 }
1427 2176
1428 2177 $status['message'] = $output;
1429 2178
@@ -1432,6 +2181,44 @@
1432 2181 }
1433 2182
1434 2183 echo json_encode($result) . "\n";
1435 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)]);
1436 2223 }
1437 2224 }