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 +929 -98 2.7.4 → 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 {
@@ -415,8 +533,10 @@
415 533 }
416 534
417 535 public function get_importer(\WP_REST_Request $request)
418 536 {
537 + Logger::setRequestType('get_importer');
538 +
419 539 $id = intval($request->get_param('id'));
420 540 $importer_data = $this->importer_manager->get_importer($id);
421 541 $response = $importer_data->data('public');
422 542 return $this->http->end_rest_success($response);
@@ -423,9 +543,11 @@
423 543 }
424 544
425 545 public function save_importer(\WP_REST_Request $request)
426 546 {
427 - $post_data = $request->get_body_params();
547 + Logger::setRequestType('save_importer');
548 +
549 + $post_data = $this->get_request_data($request);
428 550 $id = isset($post_data['id']) ? intval($post_data['id']) : null;
429 551
430 552 $temp_post_data = [
431 553 'create' => [],
@@ -433,10 +555,19 @@
433 555 ];
434 556
435 557 if (isset($post_data['permissions'])) {
436 558 // TODO: update sanitize method to keep track of hierarchy
437 - $temp_post_data['create'] = explode("\n", $post_data['permissions']['create']['fields']);
438 - $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 + }
439 570 }
440 571
441 572 $post_data = $this->sanitize($post_data);
442 573
@@ -451,12 +582,9 @@
451 582 }
452 583 }
453 584 }
454 585
455 -
456 -
457 586 $importer = new ImporterModel($id, $this->importer_manager->is_debug());
458 -
459 587 if (isset($post_data['datasource'])) {
460 588 $importer->setDatasource($post_data['datasource']);
461 589 }
462 590
@@ -464,9 +592,9 @@
464 592 $importer->setDatasourceSetting('remote_url', $post_data['remote_url']);
465 593 }
466 594
467 595 if (isset($post_data['local_url'])) {
468 - $importer->setDatasourceSetting('local_url', $post_data['local_url']);
596 + $importer->setDatasourceSetting('local_url', wp_normalize_path($post_data['local_url']));
469 597 }
470 598
471 599 if (isset($post_data['existing_id'])) {
472 600 $importer->setFileId(intval($post_data['existing_id']));
@@ -487,8 +615,14 @@
487 615 if (isset($post_data['template_options'])) {
488 616 $template_options = array_merge($template_options, $post_data['template_options']);
489 617 }
490 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 +
491 625 foreach ($template_options as $key => $val) {
492 626 $importer->setSetting($key, $val);
493 627 }
494 628 }
@@ -496,8 +630,220 @@
496 630 if (isset($post_data['name'])) {
497 631 $importer->setName($post_data['name']);
498 632 }
499 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 +
500 846 if (isset($post_data['setting_start_row'])) {
501 847 $importer->setStartRow($post_data['setting_start_row']);
502 848 }
503 849
@@ -513,8 +859,13 @@
513 859 $importer->setSetting($matches['key'], $value);
514 860 }
515 861 }
516 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 +
517 868 $parser = $importer->getParser();
518 869
519 870 if (isset($post_data['file_settings_encoding'])) {
520 871 $importer->setFileSetting('file_encoding', $post_data['file_settings_encoding']);
@@ -520,18 +871,43 @@
520 871 $importer->setFileSetting('file_encoding', $post_data['file_settings_encoding']);
521 872 }
522 873
523 874 if ($parser === 'csv') {
875 +
876 + $clear_config = false;
524 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 + }
525 883 $importer->setFileSetting('delimiter', $post_data['file_settings_delimiter']);
526 884 }
527 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 + }
528 890 $importer->setFileSetting('enclosure', $post_data['file_settings_enclosure']);
529 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 +
530 902 if (isset($post_data['file_settings_show_headings'])) {
531 - $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']));
532 904 }
533 - } 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') {
534 910 if (isset($post_data['file_settings_base_path'])) {
535 911 $importer->setFileSetting('base_path', $post_data['file_settings_base_path']);
536 912 }
537 913 }
@@ -536,25 +912,37 @@
536 912 }
537 913 }
538 914
539 915 if (isset($post_data['file_settings_setup'])) {
540 - $importer->setFileSetting('setup', $post_data['file_settings_setup'] === 'true' ? true : false);
916 + $importer->setFileSetting('setup', $this->is_truthy($post_data['file_settings_setup']));
541 917 }
542 918
543 919 if (isset($post_data['map']) && is_array($post_data['map'])) {
920 + $map = [];
544 921 foreach ($post_data['map'] as $key => $value) {
922 + if ($value === null) {
923 + continue;
924 + }
545 925
546 926 if (1 === preg_match('/\._index$/', $key)) {
547 927 $value = intval($value);
548 928 }
549 929
550 - $importer->setMap($key, $value);
930 + $map[$key] = $value;
551 931 }
932 + $importer->replaceMap($map);
552 933 }
553 934
554 935 if (isset($post_data['enabled']) && is_array($post_data['enabled'])) {
555 936 foreach ($post_data['enabled'] as $key => $value) {
556 - 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)) {
557 945 $importer->setEnabled($key);
558 946 } else {
559 947 $importer->setEnabled($key, false);
560 948 }
@@ -566,9 +954,9 @@
566 954 // create
567 955 if (isset($post_data['permissions']['create'])) {
568 956 $create = $post_data['permissions']['create'];
569 957 $importer->setPermission('create', [
570 - 'enabled' => isset($create['enabled']) && $create['enabled'] === 'true' ? true : false,
958 + 'enabled' => isset($create['enabled']) && $this->is_truthy($create['enabled']) ? true : false,
571 959 'type' => isset($create['type']) && in_array($create['type'], ['include', 'exclude'], true) ? $create['type'] : null,
572 960 'fields' => isset($create['fields']) ? $create['fields'] : [],
573 961 ]);
574 962 }
@@ -576,9 +964,9 @@
576 964 // update
577 965 if (isset($post_data['permissions']['update'])) {
578 966 $update = $post_data['permissions']['update'];
579 967 $importer->setPermission('update', [
580 - 'enabled' => isset($update['enabled']) && $update['enabled'] === 'true' ? true : false,
968 + 'enabled' => isset($update['enabled']) && $this->is_truthy($update['enabled']) ? true : false,
581 969 'type' => isset($update['type']) && in_array($update['type'], ['include', 'exclude'], true) ? $update['type'] : null,
582 970 'fields' => isset($update['fields']) ? $update['fields'] : [],
583 971 ]);
584 972 }
@@ -585,10 +973,11 @@
585 973
586 974 // remove
587 975 if (isset($post_data['permissions']['remove'])) {
588 976 $importer->setPermission('remove', [
589 - 'enabled' => $post_data['permissions']['remove']['enabled'] === 'true',
590 - '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']),
591 980 ]);
592 981 }
593 982 }
594 983
@@ -598,8 +987,88 @@
598 987 }
599 988 return $this->http->end_rest_success($importer->data());
600 989 }
601 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 +
602 1071 private function sanitize_setting($value)
603 1072 {
604 1073 if (is_array($value)) {
605 1074 foreach ($value as &$tmp) {
@@ -622,9 +1091,9 @@
622 1091 public function get_status(\WP_REST_Request $request)
623 1092 {
624 1093 $this->http->set_stream_headers();
625 1094
626 - Logger::setRequestType('rest::get_status');
1095 + Logger::disable();
627 1096
628 1097 $importer_ids = $request->get_param('ids');
629 1098
630 1099 $result = [];
@@ -641,27 +1110,29 @@
641 1110 $query = new \WP_Query($query_data);
642 1111
643 1112 foreach ($query->posts as $importer_id) {
644 1113
645 - Logger::write('-start', $importer_id);
646 -
647 1114 $importer_model = $this->importer_manager->get_importer($importer_id);
1115 + $config = $this->importer_manager->get_config($importer_model);
648 1116
649 1117 $output = ImporterState::get_state($importer_id);
650 1118 if (!$output) {
651 - // TODO: What should happen if there is no state.
1119 + $output = [
1120 + 'version' => 2,
1121 + 'message' => 'Not setup',
1122 + 'importer' => $importer_id
1123 + ];
1124 + } else {
1125 +
652 1126 $output['version'] = 2;
1127 + $output['message'] = $this->generate_status_message($output);
1128 + $output['importer'] = $importer_id;
1129 + $output['process'] = intval($config->get('process'));
653 1130 }
654 - $output['message'] = $this->generate_status_message($output);
655 - $output['importer'] = $importer_id;
656 1131
657 1132 $result[] = $this->event_handler->run('iwp/importer/status/output', [$output, $importer_model]);
658 -
659 - Logger::write('-end', $importer_id);
660 1133 }
661 1134
662 - Logger::clearRequestType();
663 -
664 1135 echo json_encode($result) . "\n";
665 1136 die();
666 1137 }
667 1138
@@ -679,12 +1150,12 @@
679 1150
680 1151 if (isset($data['status'])) {
681 1152 switch ($data['status']) {
682 1153 case 'cancelled':
683 - $output .= 'Import cancelled';
1154 + $output .= __('Import cancelled', 'jc-importer');
684 1155 break;
685 1156 case 'complete':
686 - $output .= 'Import complete';
1157 + $output .= __('Import complete', 'jc-importer');
687 1158 break;
688 1159 case 'error':
689 1160 $output = $data['message'];
690 1161 return $output;
@@ -691,15 +1162,14 @@
691 1162 break;
692 1163 }
693 1164 }
694 1165
695 - 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'])) {
696 1167 switch ($data['section']) {
697 1168 case 'import':
698 1169 case 'delete':
699 - $output = $data['section'] === 'import' ? 'Importing: ' : 'Deleting: ';
700 - $output .= $data['progress'][$data['section']]['current_row'] . ' / ' . ($data['progress'][$data['section']]['end'] - $data['progress'][$data['section']]['start']);
701 -
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);
702 1172 break;
703 1173 }
704 1174 }
705 1175
@@ -704,21 +1174,21 @@
704 1174 }
705 1175
706 1176 if (isset($data['stats'])) {
707 1177 if ($data['stats']['inserts'] > 0) {
708 - $output .= sprintf(', Inserts: %d', $data['stats']['inserts']);
1178 + $output .= sprintf(__(', Inserts: %d', 'jc-importer'), $data['stats']['inserts']);
709 1179 }
710 1180 if ($data['stats']['updates'] > 0) {
711 - $output .= sprintf(', Updates: %d', $data['stats']['updates']);
1181 + $output .= sprintf(__(', Updates: %d', 'jc-importer'), $data['stats']['updates']);
712 1182 }
713 1183 if ($data['stats']['deletes'] > 0) {
714 - $output .= sprintf(', Deletes: %d', $data['stats']['deletes']);
1184 + $output .= sprintf(__(', Deletes: %d', 'jc-importer'), $data['stats']['deletes']);
715 1185 }
716 1186 if ($data['stats']['skips'] > 0) {
717 - $output .= sprintf(', Skips: %d', $data['stats']['skips']);
1187 + $output .= sprintf(__(', Skips: %d', 'jc-importer'), $data['stats']['skips']);
718 1188 }
719 1189 if ($data['stats']['errors'] > 0) {
720 - $output .= sprintf(', Errors: %d', $data['stats']['errors']);
1190 + $output .= sprintf(__(', Errors: %d', 'jc-importer'), $data['stats']['errors']);
721 1191 }
722 1192 }
723 1193
724 1194 if (isset($data['timestamp'], $data['updated'])) {
@@ -725,15 +1195,15 @@
725 1195 /**
726 1196 * @var Util $util
727 1197 */
728 1198 $util = Container::getInstance()->get('util');
729 - $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']));
730 1200 } elseif (isset($data['duration'])) {
731 1201 /**
732 1202 * @var Util $util
733 1203 */
734 1204 $util = Container::getInstance()->get('util');
735 - $output .= ', Run Time: ' . $util->format_time($data['duration']);
1205 + $output .= sprintf(__(', Run Time: %s', 'jc-importer'), $util->format_time($data['duration']));
736 1206 }
737 1207
738 1208 return $output;
739 1209 }
@@ -739,8 +1209,10 @@
739 1209 }
740 1210
741 1211 public function delete_importer(\WP_REST_Request $request)
742 1212 {
1213 + Logger::setRequestType('delete_importer');
1214 +
743 1215 $id = intval($request->get_param('id'));
744 1216 $this->importer_manager->delete_importer($id);
745 1217 return $this->http->end_rest_success(true);
746 1218 }
@@ -755,8 +1227,10 @@
755 1227 $importer = $this->importer_manager->get_importer($id);
756 1228
757 1229 // TODO: Remove duplicate code, found in cron manager
758 1230
1231 + add_filter('upload_dir', [$this->importer_manager, 'set_custom_upload_path']);
1232 +
759 1233 switch ($action) {
760 1234 case 'file_upload':
761 1235 $attachment_id = $this->importer_manager->upload_file($importer, $_FILES['file']);
762 1236 break;
@@ -775,15 +1249,23 @@
775 1249 case 'file_local':
776 1250 $raw_source = $post_data['local_url'];
777 1251 $source = apply_filters('iwp/importer/datasource', $raw_source, $raw_source, $importer);
778 1252 $source = apply_filters('iwp/importer/datasource/local', $source, $raw_source, $importer);
779 - $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);
780 1260 break;
781 1261 default:
782 - $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'));
783 1263 break;
784 1264 }
785 1265
1266 + remove_filter('upload_dir', [$this->importer_manager, 'set_custom_upload_path']);
1267 +
786 1268 if (is_wp_error($attachment_id)) {
787 1269 return $this->http->end_rest_error($attachment_id->get_error_message());
788 1270 }
789 1271
@@ -797,9 +1279,9 @@
797 1279 $post_data = $this->sanitize($request->get_body_params());
798 1280
799 1281 $importer = $this->importer_manager->get_importer($id);
800 1282 if (!$importer) {
801 - return $this->http->end_rest_error('Invalid importer');
1283 + return $this->http->end_rest_error(__('Invalid importer', 'jc-importer'));
802 1284 }
803 1285
804 1286 $this->importer_manager->clear_config_files($id, true);
805 1287 $parser = $importer->getParser();
@@ -807,20 +1289,29 @@
807 1289 if ('xml' === $parser) {
808 1290
809 1291 $nodes = $this->importer_manager->process_xml_file($id, true);
810 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);
811 1297 } elseif ('csv' === $parser) {
812 1298
813 1299 $delimiter = isset($post_data['delimiter']) ? $post_data['delimiter'] : null;
814 1300 if (is_null($delimiter)) {
815 - $delimiter = ',';
1301 + $delimiter = $importer->getFileSetting('delimiter', ',');
816 1302 }
817 1303
818 1304 $enclosure = isset($post_data['enclosure']) ? $post_data['enclosure'] : null;
819 1305 if (is_null($enclosure)) {
820 - $enclosure = '"';
1306 + $enclosure = $importer->getFileSetting('enclosure', '"');
821 1307 }
822 1308
1309 + $escape = isset($post_data['escape']) ? $post_data['escape'] : null;
1310 + if (is_null($escape)) {
1311 + $escape = $importer->getFileSetting('escape', '\\');
1312 + }
1313 +
823 1314 $count = $this->importer_manager->process_csv_file($id, $delimiter, $enclosure, true);
824 1315
825 1316 // Need fresh importer, due to process_csv_file importer modifications might get overwritten.
826 1317 $importer = $this->importer_manager->get_importer($id);
@@ -847,9 +1338,9 @@
847 1338 $importer = $this->importer_manager->get_importer($id);
848 1339
849 1340 $import_file_exists = $this->filesystem->file_exists($importer->getFile());
850 1341 if (is_wp_error($import_file_exists)) {
851 - 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()));
852 1343 }
853 1344
854 1345 $record_index = isset($post_data['record']) ? $post_data['record'] : null;
855 1346 if (is_null($record_index)) {
@@ -860,11 +1351,21 @@
860 1351 if (isset($post_data['file_encoding'])) {
861 1352 $importer->setFileSetting('file_encoding', $post_data['file_encoding']);
862 1353 }
863 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 +
864 1366 if ($importer->getParser() === 'xml') {
865 1367
866 - $config = $this->importer_manager->get_config($importer, true);
867 1368 $file = $this->importer_manager->get_xml_file($importer, $config);
868 1369
869 1370 $base_path = $post_data['base_path'];
870 1371 if (!is_null($base_path)) {
@@ -870,20 +1371,73 @@
870 1371 if (!is_null($base_path)) {
871 1372 $file->setRecordPath($base_path);
872 1373 }
873 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 +
874 1382 $preview = new XMLPreview($file, $base_path);
875 - $result = $preview->data();
1383 + $result = $preview->data($record_index);
876 1384 if (is_wp_error($result)) {
877 1385 return $this->http->end_rest_error($result);
878 1386 }
879 1387
880 - 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 + ]);
881 1420 } elseif ($importer->getParser() === 'csv') {
882 1421
883 - $config = $this->importer_manager->get_config($importer, true);
884 1422 $file = $this->importer_manager->get_csv_file($importer, $config);
885 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 +
886 1440 $delimiter = $post_data['delimiter'];
887 1441 if (!is_null($delimiter)) {
888 1442 $file->setDelimiter($delimiter);
889 1443 }
@@ -892,8 +1446,13 @@
892 1446 if (!is_null($enclosure)) {
893 1447 $file->setEnclosure($enclosure);
894 1448 }
895 1449
1450 + $escape = $post_data['escape'];
1451 + if (!is_null($escape)) {
1452 + $file->setEscape($escape);
1453 + }
1454 +
896 1455 // parse bool param from string
897 1456 $show_headings = $post_data['show_headings'];
898 1457 if (in_array($show_headings, ['true', 'false'])) {
899 1458 $show_headings = $show_headings === 'true' ? true : false;
@@ -911,9 +1470,9 @@
911 1470
912 1471 return $this->http->end_rest_success($result);
913 1472 } else {
914 1473
915 - $result = apply_filters('iwp/file-preview/' . $importer->getParser(), null, $importer);
1474 + $result = apply_filters('iwp/file-preview/' . $importer->getParser(), null, $importer, $record_index);
916 1475 if (is_wp_error($result)) {
917 1476 return $this->http->end_rest_error($result);
918 1477 }
919 1478
@@ -931,24 +1490,31 @@
931 1490 $id = intval($request->get_param('id'));
932 1491 $importer = $this->importer_manager->get_importer($id);
933 1492 $import_file_exists = $this->filesystem->file_exists($importer->getFile());
934 1493 if (is_wp_error($import_file_exists)) {
935 - 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()));
936 1495 }
937 1496
938 1497 $parser = $importer->getParser();
939 1498
940 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 + }
941 1505 if (is_null($fields) || empty($fields)) {
942 1506 $fields = $importer->getMap();
943 1507 }
944 1508
945 1509 if ('xml' === $parser) {
946 - $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);
947 1513 } elseif ('csv' === $parser) {
948 - $row = 0;
1514 + $row = $record_index;
949 1515 if ($importer->getFileSetting('show_headings') === true) {
950 - $row = 1;
1516 + $row = $record_index + 1;
951 1517 }
952 1518 $result = $this->importer_manager->preview_csv_file($importer, $fields, $row);
953 1519 } else {
954 1520
@@ -988,8 +1554,13 @@
988 1554 // This is used for storing version on imported records
989 1555 $session_id = md5($importer_data->getId() . time());
990 1556 update_post_meta($importer_data->getId(), '_iwp_session', $session_id);
991 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 +
992 1563 Logger::clearRequestType();
993 1564
994 1565 return $this->http->end_rest_success([
995 1566 'session' => $session_id
@@ -1022,25 +1593,10 @@
1022 1593 {
1023 1594 $id = intval($request->get_param('id'));
1024 1595 $paused = $this->sanitize($request->get_param('paused'), 'paused');
1025 1596
1026 - $importer_data = $this->importer_manager->get_importer($id);
1027 - $user = uniqid('iwp', true);
1597 + $state = $this->importer_manager->pause_import($id, $paused);
1028 1598
1029 - $state = ImporterState::wait_for_lock($importer_data->getId(), $user, function () use ($importer_data, $paused) {
1030 - $state = ImporterState::get_state($importer_data->getId());
1031 -
1032 -
1033 - if ($paused === 'no') {
1034 - $state['status'] = 'running';
1035 - } else {
1036 - $state['status'] = 'paused';
1037 - }
1038 -
1039 - ImporterState::set_state($importer_data->getId(), $state);
1040 - return $state;
1041 - });
1042 -
1043 1599 return $this->http->end_rest_success($state);
1044 1600 }
1045 1601
1046 1602 public function stop_import(\WP_REST_Request $request)
@@ -1046,19 +1602,10 @@
1046 1602 public function stop_import(\WP_REST_Request $request)
1047 1603 {
1048 1604 $id = intval($request->get_param('id'));
1049 1605
1050 - $importer_data = $this->importer_manager->get_importer($id);
1606 + $state = $this->importer_manager->stop_import($id);
1051 1607
1052 - $user = uniqid('iwp');
1053 -
1054 - $state = ImporterState::wait_for_lock($importer_data->getId(), $user, function () use ($importer_data) {
1055 - $state = ImporterState::get_state($importer_data->getId());
1056 - $state['status'] = 'cancelled';
1057 - ImporterState::set_state($importer_data->getId(), $state);
1058 - return $state;
1059 - });
1060 -
1061 1608 return $this->http->end_rest_success($state);
1062 1609 }
1063 1610
1064 1611 /**
@@ -1068,15 +1615,20 @@
1068 1615 * @return void
1069 1616 */
1070 1617 public function get_template_field_options(\WP_REST_Request $request)
1071 1618 {
1072 - $id = intval($request->get_param('id'));
1073 - $field = $this->sanitize($request->get_param('field'), 'field');
1619 + try {
1074 1620
1075 - $importer_data = $this->importer_manager->get_importer($id);
1076 - $template = $this->importer_manager->get_importer_template($id);
1621 + $id = intval($request->get_param('id'));
1622 + $field = $this->sanitize($request->get_param('field'), 'field');
1077 1623
1078 - 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 + }
1079 1631 }
1080 1632
1081 1633 public function get_logs(\WP_REST_Request $request)
1082 1634 {
@@ -1109,9 +1661,19 @@
1109 1661 }
1110 1662 $importer_data = $this->importer_manager->get_importer($id);
1111 1663 $log = $this->importer_manager->get_importer_debug_log($importer_data, $page, 100);
1112 1664
1113 - $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 +
1114 1676 return $this->http->end_rest_success(['log' => $log, 'download' => $download]);
1115 1677 }
1116 1678
1117 1679 public function save_settings(\WP_REST_Request $request)
@@ -1143,9 +1705,9 @@
1143 1705 $output[$setting] = $default['value'];
1144 1706 }
1145 1707 }
1146 1708
1147 - update_site_option('iwp_settings', $output);
1709 + update_option('iwp_settings', $output);
1148 1710
1149 1711 return $this->http->end_rest_success($this->properties->get_settings());
1150 1712 }
1151 1713
@@ -1156,8 +1718,10 @@
1156 1718 }
1157 1719
1158 1720 public function get_template(\WP_REST_Request $request = null)
1159 1721 {
1722 + Logger::setRequestType('get_template');
1723 +
1160 1724 $importer_id = $request->get_param('id');
1161 1725 $importer_model = $this->importer_manager->get_importer($importer_id);
1162 1726 $importer_template = $importer_model->getTemplate();
1163 1727
@@ -1163,9 +1727,9 @@
1163 1727
1164 1728 $templates = $this->importer_manager->get_templates();
1165 1729
1166 1730 if (!isset($templates[$importer_template])) {
1167 - 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));
1168 1732 }
1169 1733
1170 1734 $template_id = $templates[$importer_template];
1171 1735 $template_class = $this->template_manager->load_template($template_id);
@@ -1175,8 +1739,9 @@
1175 1739 'label' => $template_class->get_name(),
1176 1740 'map' => $template_class->get_fields($importer_model),
1177 1741 'settings' => $template_class->register_settings(),
1178 1742 'options' => $template_class->register_options(),
1743 + 'permission_fields' => apply_filters('iwp/template/permission_fields', $template_class->get_permission_fields($importer_model), $template_class)
1179 1744 ];
1180 1745 return $this->http->end_rest_success($output);
1181 1746 }
1182 1747
@@ -1181,8 +1746,10 @@
1181 1746 }
1182 1747
1183 1748 public function export_importers(\WP_REST_Request $request = null)
1184 1749 {
1750 + Logger::setRequestType('export_importers');
1751 +
1185 1752 $importer_ids = $request->get_param('ids');
1186 1753
1187 1754 $sql_ids = array_map(function ($v) {
1188 1755 return "'" . esc_sql($v) . "'";
@@ -1229,10 +1796,147 @@
1229 1796 echo $json;
1230 1797 die();
1231 1798 }
1232 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 +
1233 1935 public function import_exporters(\WP_REST_Request $request = null)
1234 1936 {
1937 + Logger::setRequestType('import_exporters');
1938 +
1235 1939 $upload = $this->filesystem->upload_file($_FILES['file']);
1236 1940 if (is_wp_error($upload)) {
1237 1941 return $this->http->end_rest_error($upload->get_error_message());
1238 1942 }
@@ -1250,9 +1954,9 @@
1250 1954 'post_type' => IWP_POST_TYPE,
1251 1955 'post_title' => $importer['name'],
1252 1956 'post_status' => 'publish',
1253 1957 'post_author' => get_current_user_id(),
1254 - 'post_content' => serialize($importer['data'])
1958 + 'post_content' => wp_slash(serialize($importer['data']))
1255 1959 ], true);
1256 1960
1257 1961 if (is_wp_error($result)) {
1258 1962 $errors[] = $result->get_error_message();
@@ -1263,16 +1967,18 @@
1263 1967 }
1264 1968 }
1265 1969
1266 1970 if (empty($counter)) {
1267 - return $this->http->end_rest_error('No Importers found.');
1971 + return $this->http->end_rest_error(__('No Importers found.', 'jc-importer'));
1268 1972 }
1269 1973
1270 - 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));
1271 1975 }
1272 1976
1273 1977 public function save_exporter(\WP_REST_Request $request = null)
1274 1978 {
1979 + Logger::setRequestType('save_exporter');
1980 +
1275 1981 $post_data = $request->get_body_params();
1276 1982 $id = isset($post_data['id']) ? intval($post_data['id']) : null;
1277 1983
1278 1984
@@ -1289,8 +1995,12 @@
1289 1995 if (isset($post_data['file_type'])) {
1290 1996 $exporter->setFileType($post_data['file_type']);
1291 1997 }
1292 1998
1999 + if (isset($post_data['file_settings'])) {
2000 + $exporter->setFileSettings($post_data['file_settings']);
2001 + }
2002 +
1293 2003 if (isset($post_data['fields']) || $id > 0) {
1294 2004 $exporter->setFields(isset($post_data['fields']) ? $post_data['fields'] : []);
1295 2005 }
1296 2006
@@ -1301,8 +2011,20 @@
1301 2011 if (isset($post_data['unique_identifier'])) {
1302 2012 $exporter->setUniqueIdentifier($post_data['unique_identifier']);
1303 2013 }
1304 2014
2015 + if (isset($post_data['export_method'])) {
2016 + $exporter->setExportMethod($post_data['export_method']);
2017 + }
2018 +
2019 + if (isset($post_data['cron'])) {
2020 +
2021 + // TODO: Save cron settings
2022 +
2023 + $cron = $this->sanitize_setting($post_data['cron']);
2024 + $exporter->setCron($cron);
2025 + }
2026 +
1305 2027 $result = $exporter->save();
1306 2028 if (is_wp_error($result)) {
1307 2029 return $this->http->end_rest_error($result->get_error_message());
1308 2030 }
@@ -1310,8 +2032,10 @@
1310 2032 }
1311 2033
1312 2034 public function get_exporters(\WP_REST_Request $request = null)
1313 2035 {
2036 + Logger::setRequestType('get_exporters');
2037 +
1314 2038 $exporters = $this->exporter_manager->get_exporters();
1315 2039 $result = [];
1316 2040
1317 2041 foreach ($exporters as $exporter) {
@@ -1322,8 +2046,10 @@
1322 2046 }
1323 2047
1324 2048 public function get_exporter(\WP_REST_Request $request = null)
1325 2049 {
2050 + Logger::setRequestType('get_exporter');
2051 +
1326 2052 $id = intval($request->get_param('id'));
1327 2053 $exporter_data = $this->exporter_manager->get_exporter($id);
1328 2054 $response = $exporter_data->data('public');
1329 2055 return $this->http->end_rest_success($response);
@@ -1330,23 +2056,58 @@
1330 2056 }
1331 2057
1332 2058 public function delete_exporter(\WP_REST_Request $request = null)
1333 2059 {
2060 + Logger::setRequestType('delete_exporter');
2061 +
1334 2062 $id = intval($request->get_param('id'));
1335 2063 $this->exporter_manager->delete_exporter($id);
1336 2064 return $this->http->end_rest_success(true);
1337 2065 }
1338 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 +
1339 2097 public function run_exporter(\WP_REST_Request $request)
1340 2098 {
2099 + Logger::setRequestType('run_exporter');
2100 +
1341 2101 $this->http->set_stream_headers();
1342 2102
1343 2103 $id = intval($request->get_param('id'));
1344 2104 $session = $request->get_param('session');
2105 + $user = uniqid('iwp', true);
1345 2106
1346 2107 $exporter_data = $this->exporter_manager->get_exporter($id);
1347 2108
1348 - $status = $this->exporter_manager->export($exporter_data, $session);
2109 + $status = $this->exporter_manager->export($exporter_data, $user, $session);
1349 2110 echo json_encode($status);
1350 2111 // Logger::write(__CLASS__ . '::run_import -import=end -status=' . $status->get_status(), $id);
1351 2112
1352 2113 // if (!empty($this->output_cache)) {
@@ -1361,8 +2122,10 @@
1361 2122 }
1362 2123
1363 2124 public function get_exporter_status(\WP_REST_Request $request)
1364 2125 {
2126 + Logger::setRequestType('get_exporter_status');
2127 +
1365 2128 $exporter_ids = $request->get_param('ids');
1366 2129 $result = array();
1367 2130 $query_data = array(
1368 2131 'post_type' => EWP_POST_TYPE,
@@ -1379,15 +2142,83 @@
1379 2142 if (!empty($query->posts)) {
1380 2143 foreach ($query->posts as $post_id) {
1381 2144
1382 2145 $exporter = $this->exporter_manager->get_exporter($post_id);
1383 - $status = $exporter->get_status();
1384 - $status['id'] = $post_id;
2146 + $status = ExporterState::get_state($post_id);
2147 + $status['exporter'] = $post_id;
2148 + $status['version'] = 2;
1385 2149
1386 - $result[] = $status;
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 +
2159 + $output = '';
2160 +
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 + }
2175 + }
2176 +
2177 + $status['message'] = $output;
2178 +
2179 + $result[] = $this->event_handler->run('iwp/exporter/status/output', [$status, $exporter]);
1387 2180 }
1388 2181 }
1389 2182
1390 2183 echo json_encode($result) . "\n";
1391 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)]);
1392 2223 }
1393 2224 }