PluginProbe
Import WP – CSV & XML Import Export for WordPress / 2.15.1
Import WP – CSV & XML Import Export for WordPress v2.15.1
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 2.9.1 All 144 releases
jc-importer / class / Common / Importer / Importer.php

Importer.php in Import WP – CSV & XML Import Export for WordPress 2.15.1, at class/Common/Importer/Importer.php

745 lines 22.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ImportWP\Common\Importer;
4
5 use ImportWP\Common\Importer\ConfigInterface;
6 use ImportWP\Common\Importer\Exception\FileException;
7 use ImportWP\Common\Importer\Exception\MapperException;
8 use ImportWP\Common\Importer\Exception\ParserException;
9 use ImportWP\Common\Importer\Exception\RecordUpdatedSkippedException;
10 use ImportWP\Common\Importer\File\CSVFile;
11 use ImportWP\Common\Importer\File\JSONFile;
12 use ImportWP\Common\Importer\File\XMLFile;
13 use ImportWP\Common\Importer\MapperInterface;
14 use ImportWP\Common\Importer\Parser\CSVParser;
15 use ImportWP\Common\Importer\Parser\JSONParser;
16 use ImportWP\Common\Importer\Parser\XMLParser;
17 use ImportWP\Common\Importer\ParserInterface;
18 use ImportWP\Common\Importer\State\ImporterState;
19 use ImportWP\Common\Properties\Properties;
20 use ImportWP\Common\Runner\ImporterRunnerState;
21 use ImportWP\Common\Util\Logger;
22 use ImportWP\Common\Util\Util;
23 use ImportWP\Container;
24
25 class Importer
26 {
27 /**
28 * @var int
29 */
30 protected $memory_limit;
31 /**
32 * @var ConfigInterface $config
33 */
34 public $config;
35
36 /**
37 * @var MapperInterface $mapper
38 */
39 private $mapper;
40
41 /**
42 * @var int $start
43 */
44 private $start;
45
46 /**
47 * @var int end
48 */
49 private $end;
50
51 /**
52 * @var ParserInterface $parser
53 */
54 private $parser;
55
56 /**
57 * Flag used to determine type of shutdown
58 *
59 * @var boolean
60 */
61 private $graceful_shutdown = true;
62
63 /**
64 * List of filters that can be applied
65 *
66 * @var array
67 */
68 private $filter_data = [];
69
70 /**
71 * @param ConfigInterface $config
72 */
73 public function __construct($config)
74 {
75 $this->config = $config;
76 }
77
78 /**
79 * Set Parser
80 *
81 * @param ParserInterface $parser
82 *
83 * @return $this
84 */
85 public function parser($parser)
86 {
87 $this->parser = $parser;
88
89 return $this;
90 }
91
92 /**
93 * Set Mapper
94 *
95 * @param MapperInterface $mapper
96 *
97 * @return $this
98 */
99 public function mapper(MapperInterface $mapper)
100 {
101 $this->mapper = $mapper;
102
103 return $this;
104 }
105
106 /**
107 * Load XML File
108 *
109 * @param string $file_path
110 *
111 * @return $this
112 */
113 public function xmlFile($file_path)
114 {
115 $file = new XMLFile($file_path, $this->config);
116 $this->parser = new XMLParser($file);
117
118 return $this;
119 }
120
121 /**
122 * Load CSV File
123 *
124 * @param string $file_path
125 *
126 * @return $this
127 */
128 public function csvFile($file_path)
129 {
130 $file = new CSVFile($file_path, $this->config);
131 $this->parser = new CSVParser($file);
132
133 return $this;
134 }
135
136 /**
137 * Load JSON File
138 *
139 * @param string $file_path
140 *
141 * @return $this
142 */
143 public function jsonFile($file_path)
144 {
145 $file = new JSONFile($file_path, $this->config);
146 $this->parser = new JSONParser($file);
147
148 return $this;
149 }
150
151 /**
152 * Set record to start importing from
153 *
154 * @param int $start
155 */
156 public function from($start)
157 {
158 $this->start = $start;
159 }
160
161 /**
162 * Set record to end import at
163 *
164 * @param int $end
165 */
166 public function to($end)
167 {
168 $this->end = $end;
169 }
170
171 /**
172 * Get Record Start Index
173 *
174 * @return int
175 */
176 private function getRecordStart()
177 {
178 return isset($this->start) && $this->start >= 0 ? $this->start : 0;
179 }
180
181 /**
182 * Get Record End Index
183 *
184 * @return int
185 */
186 public function getRecordEnd()
187 {
188 return isset($this->end) && $this->end >= $this->getRecordStart() ? $this->end : $this->parser->file()->getRecordCount();
189 }
190
191 private function register_shutdown($importer_state)
192 {
193 $this->graceful_shutdown = false;
194
195 register_shutdown_function(function () use ($importer_state) {
196 if ($this->is_graceful_shutdown()) {
197 // $this->record_time();
198 return;
199 }
200
201 // TODO: Log errors
202 $error = error_get_last();
203 if (!is_null($error)) {
204
205 $importer_state->update(function ($state) use ($error) {
206 $state['status'] = 'error';
207 $state['message'] = $error['message'];
208 return $state;
209 });
210
211
212 $this->mapper->teardown();
213 echo json_encode($importer_state->get_raw()) . "\n";
214 die();
215 }
216
217 $this->mapper->teardown();
218 });
219 }
220
221 private function unregister_shutdown()
222 {
223 $this->graceful_shutdown = true;
224 }
225
226 private function is_graceful_shutdown()
227 {
228 return $this->graceful_shutdown;
229 }
230
231 /**
232 * Run Import
233 *
234 * @param int $id Importer Id
235 * @param string $user Unique user id
236 * @param ImporterRunnerState $importer_state
237 *
238 * @throws \Exception
239 */
240 public function import($id, $user, $importer_state)
241 {
242 if ($this->parser == null) {
243 throw new \Exception(__("Parser Not Loaded.", 'jc-importer'));
244 }
245
246 if ($this->mapper == null) {
247 throw new \Exception(__("Mapper Not Loaded.", 'jc-importer'));
248 }
249
250 $this->mapper->setup();
251
252 $this->register_shutdown($importer_state);
253
254 $this->disable_caching();
255
256 /**
257 * @var Util $util
258 */
259 $util = Container::getInstance()->get('util');
260 $util->set_time_limit();
261
262 // TODO:
263 // $runner = new ImporterRunner($properties, $this);
264 // $runner->process($id, $user, $importer_state);
265 $this->process_chunk($id, $user, $importer_state);
266
267 $this->mapper->teardown();
268 $this->unregister_shutdown();
269 }
270
271 protected function disable_caching()
272 {
273 if (!defined('WP_IMPORTING')) {
274 define('WP_IMPORTING', true);
275 }
276
277 // WP Rocket Integration
278 add_filter('rocket_is_importing', '__return_true');
279 }
280
281 protected function process_chunk($id, $user, $importer_state)
282 {
283 // Introduce new running state, to stop cron running duplicates
284 $importer_state->populate([
285 'status' => 'processing'
286 ]);
287 ImporterState::set_state($id, $importer_state->get_raw());
288
289 /**
290 * @var Properties $properties
291 */
292 $properties = Container::getInstance()->get('properties');
293 $time_limit = $properties->get_setting('timeout');
294 Logger::info('time_limit ' . $time_limit . 's');
295
296 $start = microtime(true);
297 $max_record_time = 0;
298 $memory_max_usage = 0;
299
300 $progress = $importer_state->get_progress();
301 $session = $importer_state->get_session();
302 $max_total = $progress['end'] - 1;
303 $i = $progress['start'] + $progress['current_row'] - 1;
304
305 // limit to max 20 rows per chunk
306 $i_max = $i + apply_filters('iwp/chunk_max_records', 20);
307
308 while (
309 $i < $max_total
310 && (!defined('REST_REQUEST') || !REST_REQUEST || $i < $i_max)
311 && (
312 $time_limit === 0 || $this->has_enough_time($start, $time_limit, $max_record_time)
313 )
314 && $this->has_enough_memory($memory_max_usage)
315 ) {
316 $i++;
317
318 $flag = ImporterState::get_flag($id);
319
320 if (ImporterState::is_paused($flag)) {
321
322 $importer_state->populate([
323 'status' => 'paused'
324 ]);
325
326 ImporterState::set_state($id, $importer_state->get_raw());
327 Util::write_status_session_to_file($id, $importer_state);
328 return;
329 }
330
331 if (ImporterState::is_cancelled($flag)) {
332 $importer_state->populate([
333 'status' => 'cancelled'
334 ]);
335
336 ImporterState::set_state($id, $importer_state->get_raw());
337 Util::write_status_session_to_file($id, $importer_state);
338 return;
339 }
340
341 $stats = [
342 'inserts' => 0,
343 'updates' => 0,
344 'deletes' => 0,
345 'skips' => 0,
346 'errors' => 0,
347 ];
348
349 $record_time = microtime(true);
350
351 if ($importer_state->get_section() === 'import') {
352
353 /**
354 * @var ParsedData $data
355 */
356 $data = null;
357
358 $data_parser = new DataParser($this->getParser(), $this->getMapper(), $this->config->getData());
359
360 try {
361
362 $data = $data_parser->get($i);
363 do_action('iwp/importer/before_row', $data);
364
365 $skip_record = $this->filterRecords();
366 $skip_record = apply_filters('iwp/importer/skip_record', $skip_record, $data, $this);
367
368 if ($skip_record) {
369
370 Logger::write('import -skip-record=' . $i);
371
372 $stats['skips']++;
373
374 // set data to null, to flag chunk as skipped
375 $message = apply_filters('iwp/status/record_skipped', "Skipped Record");
376 Util::write_status_log_file_message($id, $session, $message, 'S', $progress['current_row']);
377
378 $data = null;
379 } else {
380
381 // import
382 $data = apply_filters('iwp/importer/before_mapper', $data, $this);
383 $data->map();
384
385 $unique_identifier_str = $this->get_unique_identifier_log_text();
386
387 if ($data->isInsert()) {
388
389 Logger::write('import:' . $i . ' -success -insert');
390
391 $stats['inserts']++;
392
393 $message = apply_filters('iwp/status/record_inserted', 'Record Inserted: #' . $data->getId(), $data->getId(), $data);
394 Util::write_status_log_file_message($id, $session, $message . $unique_identifier_str, 'S', $progress['current_row']);
395 }
396
397 if ($data->isUpdate()) {
398
399 Logger::write('import:' . $i . ' -success -update');
400
401 $stats['updates']++;
402
403 $message = apply_filters('iwp/status/record_updated', 'Record Updated: #' . $data->getId(), $data->getId(), $data);
404 Util::write_status_log_file_message($id, $session, $message . $unique_identifier_str, 'S', $progress['current_row']);
405 }
406 }
407 } catch (RecordUpdatedSkippedException $e) {
408
409 Logger::write('import:' . $i . ' -success -update -skipped="hash"');
410 $stats['updates']++;
411 $message = 'Record Update Skipped: #' . $data->getId() . ' ' . $e->getMessage();
412 $unique_identifier_str = $this->get_unique_identifier_log_text();
413
414 Util::write_status_log_file_message($id, $session, $message . $unique_identifier_str, 'S', $progress['current_row']);
415 } catch (ParserException $e) {
416
417 $stats['errors']++;
418 Logger::error('import:' . $i . ' -parser-error=' . $e->getMessage());
419 Util::write_status_log_file_message($id, $session, $e->getMessage(), 'E', $progress['current_row']);
420 } catch (MapperException $e) {
421
422 $stats['errors']++;
423 Logger::error('import:' . $i . ' -mapper-error=' . $e->getMessage());
424 Util::write_status_log_file_message($id, $session, $e->getMessage(), 'E', $progress['current_row']);
425 } catch (FileException $e) {
426
427 $stats['errors']++;
428 Logger::error('import:' . $i . ' -file-error=' . $e->getMessage());
429 Util::write_status_log_file_message($id, $session, $e->getMessage(), 'E', $progress['current_row']);
430 }
431
432 do_action('iwp/importer/after_row');
433 } elseif ($importer_state->get_section() === 'delete') {
434
435 if ($this->getMapper()->permission() && $this->getMapper()->permission()->allowed_method('remove')) {
436
437 try {
438 $GLOBALS['wp_object_cache']->delete('iwp_importer_config_' . $id, 'options');
439 $config = get_option('iwp_importer_config_' . $id);
440
441 $object_ids = $config['delete_ids'];
442 if ($object_ids && count($object_ids) > $i) {
443
444 $object_id = $object_ids[$i];
445
446 if (apply_filters('iwp/importer/enable_custom_delete_action', false, $id)) {
447
448 Logger::write('custom_delete_action:' . $i . ' -object=' . $object_id);
449 do_action('iwp/importer/custom_delete_action', $id, $object_id);
450 } else {
451
452 Logger::write('delete:' . $i . ' -object=' . $object_id);
453 $this->getMapper()->delete($object_id);
454 }
455
456 $message = apply_filters('iwp/status/record_deleted', 'Record Deleted: #' . $object_id, $object_id);
457 $stats['deletes']++;
458
459 Util::write_status_log_file_message($id, $session, $message, 'D', $progress['current_row']);
460 }
461 } catch (MapperException $e) {
462
463 $stats['errors']++;
464 Logger::error('delete:' . $i . ' -mapper-error=' . $e->getMessage());
465 Util::write_status_log_file_message($id, $session, $e->getMessage(), 'E', $progress['current_row']);
466 }
467 }
468 }
469
470 $importer_state->update_importer_stats($stats);
471 Util::write_status_session_to_file($id, $importer_state);
472
473 $importer_state->increment_current_row();
474 $progress = $importer_state->get_progress();
475
476 ImporterState::set_state($id, $importer_state->get_raw());
477
478 $max_record_time = max($max_record_time, microtime(true) - $record_time);
479 }
480
481 // TODO: need a new state that will stop the running from happening more than once.
482 // if returning timeout then the cron will stop on older versions
483 if (defined('IWP_PRO_VERSION') && version_compare(IWP_PRO_VERSION, '2.8.0', '>')) {
484 // default status to idle after run
485 $importer_state->populate([
486 'status' => 'timeout'
487 ]);
488 } else {
489 $importer_state->populate([
490 'status' => 'running'
491 ]);
492 }
493
494 $state_data = $importer_state->get_raw();
495
496 $progress = $importer_state->get_progress();
497 if ($progress['end'] - $progress['start'] <= $progress['current_row']) {
498
499 switch ($importer_state->get_section()) {
500 case 'import':
501
502
503 if ($this->getMapper()->permission() && $this->getMapper()->permission()->allowed_method('remove')) {
504
505 // importer delete
506 $state_data['section'] = 'delete';
507
508 // generate list of items to be deleted
509 $object_ids = $this->getMapper()->get_objects_for_removal();
510 if (!empty($object_ids)) {
511
512 $config = get_option('iwp_importer_config_' . $id);
513 $config['delete_ids'] = $object_ids;
514 update_option('iwp_importer_config_' . $id, $config);
515
516 $state_data['progress']['delete']['start'] = 0;
517 $state_data['progress']['delete']['end'] = $object_ids ? count($object_ids) : 0;
518 } else {
519 $state_data['section'] = '';
520 $state_data['status'] = 'complete';
521 }
522 } else {
523 $state_data['section'] = '';
524 $state_data['status'] = 'complete';
525 }
526
527 break;
528 case 'delete':
529
530 // importer complete
531 $state_data['section'] = '';
532 $state_data['status'] = 'complete';
533
534 break;
535 }
536 }
537
538 ImporterState::set_state($id, $state_data);
539 $importer_state->populate($state_data);
540
541 Util::write_status_session_to_file($id, $importer_state);
542 }
543
544 function get_unique_identifier_log_text()
545 {
546 $unique_identifier_str = '';
547
548 $unqiue_identifier_settings = $this->getMapper()->get_unqiue_identifier_settings();
549 if (!empty($unqiue_identifier_settings) && isset($unqiue_identifier_settings['field'], $unqiue_identifier_settings['value'])) {
550
551 $unique_identifier_str = ' using unique identifier ';
552 if ($unqiue_identifier_settings['field'] === '_iwp_ref_uid') {
553 $unique_identifier_str .= sprintf('("_iwp_ref_uid" = "%s")', $unqiue_identifier_settings['value']);
554 } else {
555 $unique_identifier_str .= sprintf('("%s" = "%s")', $unqiue_identifier_settings['field'], $unqiue_identifier_settings['value']);
556 }
557 }
558
559 return $unique_identifier_str;
560 }
561
562 function has_enough_time($start, $time_limit, $max_record_time)
563 {
564 return (microtime(true) - $start) < $time_limit - $max_record_time;
565 }
566
567 function get_memory_usage()
568 {
569 return memory_get_usage(true);
570 }
571
572 function has_enough_memory($memory_max_usage)
573 {
574 $limit = $this->get_memory_limit();
575
576 // Has unlimited memory
577 if ($limit == '-1') {
578 return true;
579 }
580
581 $limit *= 0.9;
582 $current_usage = $this->get_memory_usage();
583
584 if ($current_usage + $memory_max_usage < $limit) {
585 return true;
586 }
587
588 Logger::error(sprintf("Not Enough Memory left to use %s, %s/%s", Logger::formatBytes($memory_max_usage, 2), Logger::formatBytes($current_usage, 2), Logger::formatBytes($limit, 2)));
589
590 return false;
591 }
592
593 function get_memory_limit($force = false)
594 {
595 if ($force || is_null($this->memory_limit)) {
596
597 $memory_limit = ini_get('memory_limit');
598 if (preg_match('/^(\d+)(.)$/', $memory_limit, $matches)) {
599 if ($matches[2] == 'G') {
600 $memory_limit = $matches[1] * 1024 * 1024 * 1024; // nnnM -> nnn MB
601 } elseif ($matches[2] == 'M') {
602 $memory_limit = $matches[1] * 1024 * 1024; // nnnM -> nnn MB
603 } else if ($matches[2] == 'K') {
604 $memory_limit = $matches[1] * 1024; // nnnK -> nnn KB
605 }
606 }
607
608 $this->memory_limit = $memory_limit;
609
610 Logger::info('memory_limit ' . $this->memory_limit . ' bytes');
611 }
612
613 return $this->memory_limit;
614 }
615
616 /**
617 * Apply any importer filters to skip records
618 *
619 * @return boolean
620 */
621 function filterRecords()
622 {
623 $result = false;
624
625 if (empty($this->filter_data)) {
626 return $result;
627 }
628
629 foreach ($this->filter_data as $group) {
630
631 $result = true;
632
633 if (empty($group)) {
634 continue;
635 }
636
637 foreach ($group as $row) {
638
639 $left = trim($this->parser->query_string($row['left']));
640 $right = $row['right'];
641 $right_parts = array_map('trim', explode(',', $right));
642
643 switch ($row['condition']) {
644 case 'equal':
645 if (strcasecmp($left, $right) !== 0) {
646 $result = false;
647 }
648 break;
649 case 'contains':
650 if (stripos($left, $right) === false) {
651 $result = false;
652 }
653 break;
654 case 'in':
655 $found = false;
656 foreach ($right_parts as $right_part) {
657 if (strcasecmp($left, $right_part) === 0) {
658 $found = true;
659 break 1;
660 }
661 }
662
663 if (!$found) {
664 $result = false;
665 }
666
667 break;
668 case 'contains-in':
669 $found = false;
670 foreach ($right_parts as $right_part) {
671 if (stripos($left, $right_part) !== false) {
672 $found = true;
673 break 1;
674 }
675 }
676
677 if (!$found) {
678 $result = false;
679 }
680 break;
681 case 'not-equal':
682 if (strcasecmp($left, $right) === 0) {
683 $result = false;
684 }
685 break;
686 case 'not-contains':
687 if (stripos($left, $right) !== false) {
688 $result = false;
689 }
690 break;
691 case 'not-in':
692 $found = false;
693 foreach ($right_parts as $right_part) {
694 if (strcasecmp($right_part, $left) === 0) {
695 $found = true;
696 break 1;
697 }
698 }
699
700 if ($found) {
701 $result = false;
702 }
703
704 break;
705 case 'not-contains-in':
706 $found = false;
707 foreach ($right_parts as $right_part) {
708 if (stripos($left, $right_part) !== false) {
709 $found = true;
710 break 1;
711 }
712 }
713
714 if ($found) {
715 $result = false;
716 }
717 break;
718 }
719 }
720
721 if ($result) {
722 return true;
723 }
724 }
725
726
727 return $result;
728 }
729
730 function filter($filter_data = [])
731 {
732 $this->filter_data = $filter_data;
733 }
734
735 public function getParser()
736 {
737 return $this->parser;
738 }
739
740 public function getMapper()
741 {
742 return $this->mapper;
743 }
744 }
745