PluginProbe
Import WP – CSV & XML Import Export for WordPress / trunk
Import WP – CSV & XML Import Export for WordPress vtrunk
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 trunk 0.1.6 All 142 releases
jc-importer / class / Common / Importer / File / XMLFile.php

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

553 lines 16.8 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\File;
4
5 use ImportWP\Common\Importer\FileInterface;
6 use ImportWP\Common\Importer\State\ImporterState;
7
8 class XMLFile extends AbstractIndexedFile implements FileInterface
9 {
10
11 /**
12 * Base path
13 *
14 * Name of the files base path node, added onto the end of base_path_segments.
15 * e.g. if record path is 'records/record', base_path would be 'record'.
16 *
17 * @var string
18 */
19 protected $base_path;
20
21 /**
22 * Base path segments
23 *
24 * Stores a list of nodes up to and not including the final base_path
25 * e.g. if base_path is 'records/record' , base_path_node would be ['records']
26 *
27 * @var string[]
28 */
29 protected $base_path_segments = array();
30
31 /**
32 * Open nodes
33 *
34 * List of nodes that have not been opened so far while parsing the file.
35 *
36 * @var string[]
37 */
38 protected $open_nodes = array();
39
40 protected $record_offset = 0;
41 protected $chunk_size = 8192;
42 protected $depth = 0;
43 protected $node_list = array();
44 protected $namespace_list = null;
45 protected $chunk;
46 protected $chunk_offset = 0;
47
48 protected $record_counter = 0;
49 protected $record_start_index = 0;
50
51 /**
52 * Record Opened
53 *
54 * Flag to say we are parsing data within the chosen record path.
55 *
56 * @var boolean
57 */
58 protected $record_opened = false;
59 protected $last_record_incomplete = false;
60 protected $record_patch_cache_key = '';
61
62 /**
63 * Set base path for records
64 *
65 * @param string $base_path
66 */
67 public function setRecordPath($base_path = '/')
68 {
69 // strip slash from start
70 if (strpos($base_path, '/') === 0) {
71 $base_path = substr($base_path, 1);
72 }
73
74 // split basepath in segments
75 $this->base_path_segments = explode('/', $base_path);
76 array_filter($this->base_path_segments, function ($value) {
77 return $value !== "";
78 });
79
80 if (!empty($this->base_path_segments)) {
81 $this->record_patch_cache_key = implode('_', $this->base_path_segments);
82 $base_path = array_pop($this->base_path_segments);
83 }
84
85 $this->base_path = $base_path;
86 }
87
88 /**
89 * Get record
90 *
91 * @param int $index
92 *
93 * @return string
94 */
95 public function getRecord($index = 0)
96 {
97 $record = parent::getRecord($index);
98
99 if ($index === $this->getRecordCount() - 1 && true === $this->last_record_incomplete) {
100 $record = $this->fixRecord($record);
101 }
102
103 return $this->wrapWithRecordTag($record);
104 }
105
106 /**
107 * Fix a broken xml segment
108 *
109 * @param $record
110 *
111 * @return bool|string
112 */
113 public function fixRecord($record)
114 {
115 $open = "/<[^\/][^>]+[^\/]>/";
116 $close = "/<\/[^>]+[^\/]>/";
117 $depth = 0;
118 $started = false;
119 $open_nodes = array();
120
121 $chunk = $record;
122
123 while (preg_match(
124 "/<[^?|!\-\-][^>]*>/",
125 $chunk,
126 $matches,
127 PREG_OFFSET_CAPTURE
128 ) !== 0) {
129 if (isset($matches[0]) && isset($matches[0][0])) {
130 list($captured, $offset) = $matches[0];
131
132 if (preg_match($open, $captured) === 1) {
133 $started = true;
134 $depth++;
135 $node_name = $this->getNodeName($captured);
136 $open_nodes[] = $node_name;
137 } else if (preg_match($close, $captured) === 1) {
138 $depth--;
139 array_pop($open_nodes);
140 }
141
142 $string_offset = $offset + strlen($captured);
143 $chunk = substr($chunk, $string_offset);
144 }
145
146 if ($started && 0 === $depth) {
147 // We should be at the end of the xml
148 return substr($record, 0, strlen($chunk));
149 }
150 }
151
152 if (!empty($open_nodes)) {
153 // Add closing tags for open nodes
154 foreach ($open_nodes as $node) {
155 $record .= "</{$node}>";
156 }
157 }
158
159 return $record;
160 }
161
162 /**
163 * Get node name from xml tag
164 *
165 * @param $node
166 *
167 * @return bool|string
168 */
169 public function getNodeName($node)
170 {
171
172 preg_match("/<\/?([a-zA-Z0-9\-_\.:]+)[ ]*[^>]*>/", $node, $matches);
173 if (isset($matches[0]) && isset($matches[1])) {
174 $name = $matches[1];
175 } else {
176 $name = substr($node, 1, strlen($node) - 2);
177 }
178
179 return $name;
180 }
181
182 /**
183 * Generate file position index.
184 *
185 * Read through entire xml file generating record start and end positions
186 *
187 * @return mixed
188 */
189 public function generateIndex()
190 {
191 $this->record_counter = 0;
192 $this->record_start_index = 0;
193
194 $last_percent = 0;
195 $size = intval($this->get_file_size($this->getFileHandle()));
196 $this->config->set('process', 0);
197 $this->namespace_list = [];
198
199 rewind($this->getFileHandle());
200
201 while (!feof($this->getFileHandle())) {
202
203 $this->chunk .= $this->getChunk();
204 $this->chunk = $this->read_chunk_xml_nodes($this->chunk);
205
206 // only read the first 1mb of file
207 if ($this->is_processing && ($this->record_counter > 0 || ftell($this->getFileHandle()) > $this->process_max_size)) {
208 break;
209 }
210
211 $current_pos = intval(ftell($this->getFileHandle()));
212 if ($size > 0 && $current_pos > 0) {
213 $percent = round($current_pos / $size, 2);
214 if ($percent > $last_percent) {
215
216 do_action('iwp/importer/process', $last_percent * 100);
217 $this->config->set('process', $last_percent * 100);
218 $last_percent = $percent;
219
220 if (!is_null(iwp()->importer)) {
221 $state = ImporterState::get_state(iwp()->importer->getId());
222 if (isset($state['status']) && $state['status'] === 'cancelled') {
223 return;
224 }
225 }
226 }
227 }
228 }
229
230 $this->config->set('namespace_list', $this->namespace_list);
231
232 // if file is incomplete do we try and fix the record, or skip it?
233 if (!$this->is_processing && true === $this->record_opened) {
234 $this->record_opened = false;
235 $this->last_record_incomplete = true;
236 $this->setIndex(
237 $this->record_counter,
238 $this->record_start_index,
239 $this->chunk_offset
240 );
241 }
242 }
243
244 /**
245 * Read chunk from file
246 *
247 * @return bool|string
248 */
249 public function getChunk()
250 {
251 return fread($this->getFileHandle(), $this->chunk_size);
252 }
253
254 /**
255 * Read all xml nodes in chunk
256 */
257 public function readChunkXmlNodes()
258 {
259 $tags = [
260 ["<?", "?>", true, 0],
261 ["<!--", "-->", true, 0],
262 ["<![CDATA[", "]]>", true, 0],
263 ["<!", ">", true, 0],
264 ["</", ">", false, -1],
265 ["<", "/>", false, 0],
266 ["<", ">", false, 1],
267 ];
268
269 while (preg_match("/<[^>]+>/", $this->chunk, $matches, PREG_OFFSET_CAPTURE) !== 0) {
270 list($captured, $offset) = $matches[0];
271
272 $this->record_offset = $offset;
273
274 foreach ($tags as $tag) {
275
276 list($open_tag, $close_tag, $search_for_close, $depth_delta) = $tag;
277
278 if (strpos($captured, $open_tag) === 0) {
279
280 if (strlen($captured) - strlen($close_tag) === strpos($captured, $close_tag)) {
281 // Tag Found
282 if ($search_for_close === false) {
283 $this->record_node($captured, $depth_delta);
284 }
285 break;
286 } elseif ($search_for_close === true) {
287 $close_tag_position = strpos($this->chunk, $close_tag, $offset);
288 if ($close_tag_position !== false) {
289 // Close tag is found in current chunk
290 $captured = substr($this->chunk, $offset, $close_tag_position + strlen($close_tag) - $offset);
291 if ($search_for_close === false) {
292 $this->record_node($captured, $depth_delta);
293 }
294 } else {
295 // We need to load next chunk to find
296 return;
297 }
298 break;
299 }
300 }
301 }
302
303 $string_offset = $offset + strlen($captured);
304 $this->chunk_offset += $string_offset;
305 $this->chunk = substr($this->chunk, $string_offset);
306 }
307 }
308
309 /**
310 * Optimised version of readChunkXMLNodes
311 *
312 * @param string $chunk
313 * @return string
314 */
315 public function read_chunk_xml_nodes($chunk)
316 {
317 $tags = [
318 "<?" => ["<?", "?>", true, 0],
319 "<!--" => ["<!--", "-->", true, 0],
320 "<![CDATA[" => ["<![CDATA[", "]]>", true, 0],
321 "<!" => ["<!", ">", true, 0],
322 "</" => ["</", ">", false, -1],
323 "<" => ["<", "/>", false, 0],
324 "<" => ["<", ">", false, 1],
325 ];
326
327 // 1.Loop through the file, each node at a time
328 $chunk_offset = 0;
329
330 while (preg_match("/<[^>]+>/", $chunk, $matches, PREG_OFFSET_CAPTURE, $chunk_offset) !== 0) {
331 list($captured, $offset) = $matches[0];
332
333 // length of content between last and current tag
334 $content_length = ($offset - $chunk_offset);
335 $chunk_offset += $content_length + strlen($captured);
336
337 if (preg_match('/^(<\?|<!--|<!\[CDATA\[|<!|<\/|<)/', $captured, $tag_matches) !== 1) {
338 continue;
339 }
340
341 $tags_test = [];
342 if ($tag_matches[0] === '<') {
343 $tags_test[] = ["<", "/>", false, 0];
344 $tags_test[] = ["<", ">", false, 1];
345 } else {
346 $tags_test[] = $tags[$tag_matches[0]];
347 }
348
349 // print_r($tag_matches);
350
351 foreach ($tags_test as $tag) {
352 list($open_tag, $close_tag, $search_for_close, $depth_delta) = $tag;
353
354 // if (strpos($captured, $open_tag) !== 0) {
355 // continue;
356 // }
357
358 if (strlen($captured) - strlen($close_tag) === strpos($captured, $close_tag)) {
359 // Tag Found
360 if ($search_for_close === false) {
361 $this->record_node($captured, $depth_delta, $this->chunk_offset + $offset);
362 }
363 break;
364 } elseif ($search_for_close === true) {
365 $close_tag_position = strpos($chunk, $close_tag, $offset);
366 if ($close_tag_position !== false) {
367 // Close tag is found in current chunk
368 $captured = substr($chunk, $offset, $close_tag_position + strlen($close_tag) - $offset);
369 $chunk_offset = $close_tag_position + strlen($close_tag);
370 if ($search_for_close === false) {
371 $this->record_node($captured, $depth_delta, $this->chunk_offset + $offset);
372 }
373 } else {
374 // We need to load next chunk to find
375 $this->chunk_offset += $offset;
376 return substr($chunk, $offset);
377 }
378 break;
379 }
380 }
381 }
382
383 $this->chunk_offset += $chunk_offset;
384
385 return substr($chunk, $chunk_offset);
386 }
387
388 public function record_node($node, $depth_delta, $offset = null)
389 {
390 if (is_null($offset)) {
391 $offset = $this->chunk_offset + $this->record_offset;
392 }
393
394 $this->depth += $depth_delta;
395 $node_name = $this->getNodeName($node);
396
397 if ($depth_delta > 0) {
398
399 if ($this->isRecordStart($node_name)) {
400 $this->record_opened = true;
401 $this->record_start_index = $offset;
402 }
403
404 $this->open_nodes[] = $node_name;
405 $this->record_open_nodes();
406
407 // find namespaces
408 if (preg_match_all('/xmlns:([a-zA-Z0-9_-]+)="([^"]+)"/', $node, $matches) > 0) {
409 $this->namespace_list = array_merge($this->namespace_list, array_combine($matches[1], $matches[2]));
410 }
411 } elseif ($depth_delta < 0) {
412
413 array_pop($this->open_nodes);
414 if ($this->isRecordEnd($node_name)) {
415 $this->record_opened = false;
416
417 $this->setIndex(
418 $this->record_counter,
419 $this->record_start_index,
420 $offset + strlen($node)
421 );
422 $this->record_counter++;
423 }
424 }
425 }
426
427 /**
428 * Check for record start node
429 *
430 * @param $node_name
431 *
432 * @return bool
433 */
434 public function isRecordStart($node_name)
435 {
436
437 if ($this->base_path_segments === $this->open_nodes && $node_name === $this->base_path) {
438 return true;
439 }
440
441 return false;
442 }
443
444 /**
445 * Check for record end node
446 *
447 * @param $node_name
448 *
449 * @return bool
450 */
451 public function isRecordEnd($node_name)
452 {
453 if ($this->record_opened === false) {
454 return false;
455 }
456
457 if ($this->base_path_segments !== $this->open_nodes) {
458 return false;
459 }
460
461 if ($node_name !== $this->base_path) {
462 return false;
463 }
464
465 return true;
466 }
467
468 public function wrapWithRecordTag($record)
469 {
470 $namespace_attrs = '';
471 $unique_matches = array();
472
473 // clear sub namespaces
474 $record = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $record);
475
476 // No need to check within cdata tags
477 $temp = preg_replace('/<!\[CDATA\[((?:[^]]|\](?!\]>))*)\]\]>/', '', $record);
478
479 // $new_regex = '/<([^:!\/]+):[^ ]+[^>]*>/';
480 // $new_regex = '/<([^!\/>]+):[^>]+>/'; // breaking due to attr="http://", would match the :
481 $new_regex = '/<([^!\/">]+):[^>]+>/';
482
483 while (preg_match($new_regex, $temp, $matches, PREG_OFFSET_CAPTURE) !== 0) {
484 if (isset($matches[0]) && isset($matches[0][0]) && isset($matches[1]) && isset($matches[1][0])) {
485 list($match, $offset) = $matches[0];
486 $namespace = trim($matches[1][0]);
487
488 // TODO: update regex to not need this
489 $namespace_parts = explode(' ', $namespace);
490 $namespace = $namespace_parts[count($namespace_parts) - 1];
491
492 if (!in_array($namespace, $unique_matches)) {
493 $unique_matches[] = $namespace;
494
495 // load namespace list if needed
496 if (is_null($this->namespace_list) && !is_null($this->config)) {
497 $this->namespace_list = $this->config->get('namespace_list');
498 if (!is_array($this->namespace_list)) {
499 $this->namespace_list = array();
500 }
501 }
502
503 if (isset($this->namespace_list[$namespace])) {
504 $namespace_attrs .= ' xmlns:' . $namespace . '="' . $this->namespace_list[$namespace] . '"';
505 } else {
506 $namespace_attrs .= ' xmlns:' . $namespace . '="false"';
507 }
508 }
509
510 $temp = substr($temp, $offset + strlen($match));
511 }
512 }
513
514 $open_tag = '<record' . $namespace_attrs . '>';
515 $close_tag = '</record>';
516 return $open_tag . $record . $close_tag;
517 }
518
519 public function record_open_nodes()
520 {
521 $key = '/' . implode('/', $this->open_nodes);
522 if (!in_array($key, $this->node_list, true)) {
523 array_push($this->node_list, $key);
524 }
525 }
526
527 public function get_node_list()
528 {
529
530 $this->node_list = $this->config->get('node_list');
531 if (!is_array($this->node_list)) {
532 $this->node_list = array();
533 }
534
535 $loaded = $this->loadIndex();
536 if (empty($this->node_list) || !$loaded) {
537 $this->generateIndex();
538 $this->storeIndexes();
539
540 $this->config->set('node_list', $this->node_list);
541 }
542
543 return $this->node_list;
544 }
545
546 public final function getFileIndexKey()
547 {
548 $key = sprintf('file_index-%s', $this->record_patch_cache_key);
549
550 return $key;
551 }
552 }
553