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

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