PluginProbe
Import WP – CSV & XML Import Export for WordPress / 2.9.0
Import WP – CSV & XML Import Export for WordPress v2.9.0
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 / File / XMLFile.php

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

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