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

750 lines 22.4 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 $start = microtime(true);
257 $max_record_time = 0;
258 $memory_max_usage = 0;
259 $i = 0;
260
261 // Does this current user have any dangling jobs?
262 $this->try_import_dangling_rows($id, $user, $importer_state, $user);
263
264 $config = get_site_option('iwp_importer_config_' . $id, []);
265
266 while (
267 ($i = 0 || (
268 ($time_limit === 0 || $this->has_enough_time($start, $time_limit, $max_record_time))
269 && $this->has_enough_memory($memory_max_usage))
270 )
271 && $importer_state
272 && $importer_state->has_section(['import', 'delete', 'timeout'])
273 ) {
274
275 $memory_usage = $this->get_memory_usage();
276
277 $this->is_timeout = false;
278
279 $importer_state = $importer_state->update(function ($state) use ($importer_state, $config, $user, $id) {
280 return $this->setup_importer_state($importer_state, $state, $config, $user, $id);
281 });
282
283 if (!$importer_state || $this->is_timeout || !$importer_state->has_status('running')) {
284 break;
285 }
286
287 $record_time = microtime(true);
288 $this->import_row($id, $user, $importer_state, $importer_state->get_session(), $importer_state->get_section(), $importer_state->get_progress());
289 $max_record_time = max($max_record_time, microtime(true) - $record_time);
290
291 if (!wp_using_ext_object_cache()) {
292 wp_cache_flush();
293 }
294
295 do_action('iwp/importer/shutdown');
296
297 // keep track of largest memory change
298 $memory_delta = $this->get_memory_usage() - $memory_usage;
299 if ($memory_delta > $memory_max_usage) {
300 $memory_max_usage = $memory_delta;
301 }
302
303 $i++;
304 }
305
306 Util::write_status_session_to_file($id, $importer_state);
307
308 $this->mapper->teardown();
309 $this->unregister_shutdown();
310 }
311
312 function has_enough_time($start, $time_limit, $max_record_time)
313 {
314 return (microtime(true) - $start) < $time_limit - $max_record_time;
315 }
316
317 function get_memory_usage()
318 {
319 return memory_get_usage(true);
320 }
321
322 function has_enough_memory($memory_max_usage)
323 {
324 $limit = $this->get_memory_limit() * 0.9;
325 $current_usage = $this->get_memory_usage();
326
327 if ($current_usage + $memory_max_usage < $limit) {
328 return true;
329 }
330
331 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)));
332
333 return false;
334 }
335
336 function get_memory_limit($force = false)
337 {
338 if ($force || is_null($this->memory_limit)) {
339
340 $memory_limit = ini_get('memory_limit');
341 if (preg_match('/^(\d+)(.)$/', $memory_limit, $matches)) {
342 if ($matches[2] == 'G') {
343 $memory_limit = $matches[1] * 1024 * 1024 * 1024; // nnnM -> nnn MB
344 } elseif ($matches[2] == 'M') {
345 $memory_limit = $matches[1] * 1024 * 1024; // nnnM -> nnn MB
346 } else if ($matches[2] == 'K') {
347 $memory_limit = $matches[1] * 1024; // nnnK -> nnn KB
348 }
349 }
350
351 $this->memory_limit = $memory_limit;
352 }
353
354 return $this->memory_limit;
355 }
356
357 function setup_importer_state($importer_state, $state, $config, $user, $id)
358 {
359 $importer_state->populate($state);
360
361 if (!$importer_state->validate($config['id'])) {
362 throw new \Exception("Importer session has changed");
363 }
364
365 if ($importer_state->has_status('running')) {
366
367 $section = $importer_state->get_section();
368 if (isset($state['progress'][$section]) && $state['progress'][$section]['end'] - $state['progress'][$section]['start'] <= $state['progress'][$section]['current_row']) {
369
370 // Does this user or any user have any dangling jobs?
371 $dangling = $this->try_import_dangling_rows($id, $user, $importer_state);
372 if (!$dangling) {
373 $this->is_timeout = true;
374 $state['duration'] = floatval($state['duration']) + Logger::timer();
375 return $state;
376 }
377
378 switch ($importer_state->get_section()) {
379 case 'import':
380
381 if ($this->mapper->permission() && $this->mapper->permission()->allowed_method('remove')) {
382
383 // importer delete
384 $state['section'] = 'delete';
385
386 // generate list of items to be deleted
387 $object_ids = $this->mapper->get_objects_for_removal();
388
389 $config = get_site_option('iwp_importer_config_' . $id);
390 $config['delete_ids'] = $object_ids;
391 update_site_option('iwp_importer_config_' . $id, $config);
392
393 $state['progress']['delete']['start'] = 0;
394 $state['progress']['delete']['end'] = $object_ids ? count($object_ids) : 0;
395 } else {
396 $state['section'] = '';
397 $state['status'] = 'complete';
398 }
399
400 break;
401 case 'delete':
402
403 // importer complete
404 $state['section'] = '';
405 $state['status'] = 'complete';
406
407 break;
408 }
409 }
410
411 // Get increase index, locking record, and saving to user importer state
412 if (!empty($state['section'])) {
413 $state['progress'][$state['section']]['current_row']++;
414 update_site_option('iwp_importer_state_' . $id . '_' . $user, array_merge($state, ['last_modified' => current_time('timestamp')]));
415 }
416 }
417
418 $state['duration'] = floatval($state['duration']) + Logger::timer();
419
420 return $state;
421 }
422
423 function try_import_dangling_rows($id, $user, $importer_state, $user_to_check = null)
424 {
425 $dangling = $this->has_dangling_state($id, $user_to_check);
426 if (!empty($dangling)) {
427 $fixed = 0;
428 foreach ($dangling as $dangling_id) {
429
430 // TODO: Should the option be renamed to the current user first? to make sure its not ran multiple times.
431 // TODO: status should not be overwritten like this.
432 $GLOBALS['wp_object_cache']->delete($dangling_id, 'options');
433 $status = get_site_option($dangling_id);
434 if ($status && $status['last_modified'] < current_time('timestamp') - 30) {
435
436 Logger::write('try_import_dangling_rows -id=' . $id . ' -user=' . $user . ' -dangling=' . $dangling_id);
437
438 $GLOBALS['wp_object_cache']->delete($dangling_id, 'options');
439 $status['last_modified'] = current_time('timestamp');
440 update_site_option($dangling_id, $status);
441
442
443 $this->import_row($id, $user, $importer_state, $importer_state->get_session(), $status['section'], $status['progress'][$status['section']]);
444 delete_site_option($dangling_id);
445 $fixed++;
446 }
447 }
448
449 if ($fixed !== count($dangling)) {
450 // escape due to dangling records that have not timed out
451 return false;
452 }
453 }
454
455 return true;
456 }
457
458 function import_row($id, $user, $importer_state, $session, $section, $progress)
459 {
460 $stats = [
461 'inserts' => 0,
462 'updates' => 0,
463 'deletes' => 0,
464 'skips' => 0,
465 'errors' => 0,
466 ];
467
468 if ($section === 'import') {
469
470
471 // TODO: Run through field map from config (xml or csv)
472 $data_parser = new DataParser($this->parser, $this->mapper, $this->config->getData());
473
474 $i = $progress['start'] + $progress['current_row'] - 1;
475
476 /**
477 * @var ParsedData $data
478 */
479 $data = null;
480
481 try {
482
483 $data = $data_parser->get($i);
484
485 $skip_record = $this->filterRecords();
486 $skip_record = apply_filters('iwp/importer/skip_record', $skip_record, $data, $this);
487
488 if ($skip_record) {
489
490 Logger::write('import -skip-record=' . $i);
491
492 $stats['skips']++;
493
494 // set data to null, to flag chunk as skipped
495 Util::write_status_log_file_message($id, $session, "Skipped Record", 'S', $progress['current_row']);
496
497 $data = null;
498 } else {
499
500 // import
501 $data = apply_filters('iwp/importer/before_mapper', $data, $this);
502 $data->map();
503
504 if ($data->isInsert()) {
505
506 Logger::write('import:' . $i . ' -success -insert');
507
508 $stats['inserts']++;
509
510 $message = apply_filters('iwp/status/record_inserted', 'Record Inserted: #' . $data->getId(), $data->getId(), $data);
511 Util::write_status_log_file_message($id, $session, $message, 'S', $progress['current_row']);
512 }
513
514 if ($data->isUpdate()) {
515
516 Logger::write('import:' . $i . ' -success -update');
517
518 $stats['updates']++;
519
520 $message = apply_filters('iwp/status/record_updated', 'Record Updated: #' . $data->getId(), $data->getId(), $data);
521 Util::write_status_log_file_message($id, $session, $message, 'S', $progress['current_row']);
522 }
523 }
524 } catch (ParserException $e) {
525
526 $stats['errors']++;
527 Logger::error('import:' . $i . ' -parser-error=' . $e->getMessage());
528 Util::write_status_log_file_message($id, $session, $e->getMessage(), 'E', $progress['current_row']);
529 } catch (MapperException $e) {
530
531 $stats['errors']++;
532 Logger::error('import:' . $i . ' -mapper-error=' . $e->getMessage());
533 Util::write_status_log_file_message($id, $session, $e->getMessage(), 'E', $progress['current_row']);
534 } catch (FileException $e) {
535
536 $stats['errors']++;
537 Logger::error('import:' . $i . ' -file-error=' . $e->getMessage());
538 Util::write_status_log_file_message($id, $session, $e->getMessage(), 'E', $progress['current_row']);
539 }
540
541 $this->update_importer_stats($importer_state, $stats);
542 Util::write_status_session_to_file($id, $importer_state);
543
544 delete_site_option('iwp_importer_state_' . $id . '_' . $user);
545 return;
546 }
547
548 if ($section === 'delete') {
549 if ($this->mapper->permission() && $this->mapper->permission()->allowed_method('remove')) {
550
551 $GLOBALS['wp_object_cache']->delete('iwp_importer_config_' . $id, 'options');
552 $config = get_site_option('iwp_importer_config_' . $id);
553 $i = $progress['current_row'] - 1;
554
555 $object_ids = $config['delete_ids'];
556 if ($object_ids && count($object_ids) > $i) {
557 $object_id = $object_ids[$i];
558 $this->mapper->delete($object_id);
559 $stats['deletes']++;
560
561 Logger::write('delete:' . $i . ' -object=' . $object_id);
562
563 $message = apply_filters('iwp/status/record_deleted', 'Record Deleted: #' . $object_id, $object_id);
564 Util::write_status_log_file_message($id, $session, $message, 'D', $progress['current_row']);
565 }
566 }
567
568 $this->update_importer_stats($importer_state, $stats);
569 Util::write_status_session_to_file($id, $importer_state);
570
571 delete_site_option('iwp_importer_state_' . $id . '_' . $user);
572 return;
573 }
574 }
575
576 function update_importer_stats($importer_state, $stats)
577 {
578 $importer_state->update(function ($state) use ($stats) {
579 if (!isset($state['stats'])) {
580 $state['stats'] = [
581 'inserts' => 0,
582 'updates' => 0,
583 'deletes' => 0,
584 'skips' => 0,
585 'errors' => 0,
586 ];
587 }
588
589 $state['stats']['inserts'] += $stats['inserts'];
590 $state['stats']['updates'] += $stats['updates'];
591 $state['stats']['deletes'] += $stats['deletes'];
592 $state['stats']['skips'] += $stats['skips'];
593 $state['stats']['errors'] += $stats['errors'];
594
595 return $state;
596 });
597 }
598
599 function has_dangling_state($id, $user = null, $key_prefix = 'iwp_importer_state')
600 {
601 /**
602 * @var \WPDB $wpdb
603 */
604 global $wpdb;
605
606 $key_prefix = str_replace('_', '\_', $key_prefix);
607 $query = "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '{$key_prefix}\_{$id}\_";
608
609 if (!empty($user)) {
610 $query .= $user;
611 } else {
612 $query .= '%';
613 }
614
615 $query .= "'";
616
617 $option_names = $wpdb->get_col($query);
618 return $option_names;
619 }
620
621 /**
622 * Apply any importer filters to skip records
623 *
624 * @return boolean
625 */
626 function filterRecords()
627 {
628 $result = false;
629
630 if (empty($this->filter_data)) {
631 return $result;
632 }
633
634 foreach ($this->filter_data as $group) {
635
636 $result = true;
637
638 if (empty($group)) {
639 continue;
640 }
641
642 foreach ($group as $row) {
643
644 $left = trim($this->parser->query_string($row['left']));
645 $right = $row['right'];
646 $right_parts = array_map('trim', explode(',', $right));
647
648 switch ($row['condition']) {
649 case 'equal':
650 if (strcasecmp($left, $right) !== 0) {
651 $result = false;
652 }
653 break;
654 case 'contains':
655 if (stripos($left, $right) === false) {
656 $result = false;
657 }
658 break;
659 case 'in':
660 $found = false;
661 foreach ($right_parts as $right_part) {
662 if (strcasecmp($left, $right_part) === 0) {
663 $found = true;
664 break 1;
665 }
666 }
667
668 if (!$found) {
669 $result = false;
670 }
671
672 break;
673 case 'contains-in':
674 $found = false;
675 foreach ($right_parts as $right_part) {
676 if (stripos($left, $right_part) !== false) {
677 $found = true;
678 break 1;
679 }
680 }
681
682 if (!$found) {
683 $result = false;
684 }
685 break;
686 case 'not-equal':
687 if (strcasecmp($left, $right) === 0) {
688 $result = false;
689 }
690 break;
691 case 'not-contains':
692 if (stripos($left, $right) !== false) {
693 $result = false;
694 }
695 break;
696 case 'not-in':
697 $found = false;
698 foreach ($right_parts as $right_part) {
699 if (strcasecmp($right_part, $left) === 0) {
700 $found = true;
701 break 1;
702 }
703 }
704
705 if ($found) {
706 $result = false;
707 }
708
709 break;
710 case 'not-contains-in':
711 $found = false;
712 foreach ($right_parts as $right_part) {
713 if (stripos($left, $right_part) !== false) {
714 $found = true;
715 break 1;
716 }
717 }
718
719 if ($found) {
720 $result = false;
721 }
722 break;
723 }
724 }
725
726 if ($result) {
727 return true;
728 }
729 }
730
731
732 return $result;
733 }
734
735 function filter($filter_data = [])
736 {
737 $this->filter_data = $filter_data;
738 }
739
740 public function getParser()
741 {
742 return $this->parser;
743 }
744
745 public function getMapper()
746 {
747 return $this->mapper;
748 }
749 }
750