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

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