PluginProbe
Import WP – CSV & XML Import Export for WordPress / 2.14.23
Import WP – CSV & XML Import Export for WordPress v2.14.23
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.14.23, at class/Common/Importer/Importer.php

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