1 | <?php |
||||
2 | |||||
3 | /** |
||||
4 | * The XmlArray class is an xml parser. |
||||
5 | * |
||||
6 | * @package ElkArte Forum |
||||
7 | * @copyright ElkArte Forum contributors |
||||
8 | * @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
||||
9 | * |
||||
10 | * This file contains code covered by: |
||||
11 | * copyright: 2011 Simple Machines (http://www.simplemachines.org) |
||||
12 | * |
||||
13 | * @version 2.0 dev |
||||
14 | * |
||||
15 | */ |
||||
16 | |||||
17 | namespace ElkArte; |
||||
18 | |||||
19 | /** |
||||
20 | * Class representing an xml array. |
||||
21 | * |
||||
22 | * Reads in xml, allows you to access it simply. |
||||
23 | * Version 2.0 dev. |
||||
24 | */ |
||||
25 | class XmlArray |
||||
26 | { |
||||
27 | /** @var array Holds xml parsed results */ |
||||
28 | public $array; |
||||
29 | |||||
30 | /** @var int|null Holds debugging level */ |
||||
31 | public $debug_level; |
||||
32 | |||||
33 | /** @var bool Holds trim level textual data */ |
||||
34 | public $trim; |
||||
35 | |||||
36 | /** |
||||
37 | * Constructor for the xml parser. |
||||
38 | * |
||||
39 | * Example use: |
||||
40 | * $xml = new \ElkArte\XmlArray(file('data.xml')); |
||||
41 | * |
||||
42 | * @param string $data the xml data or an array of, unless is_clone is true. |
||||
43 | * @param bool $auto_trim default false, used to automatically trim textual data. |
||||
44 | * @param int|null $level default null, the debug level, specifies whether notices should be generated for missing elements and attributes. |
||||
45 | * @param bool $is_clone default false. If is_clone is true, the \ElkArte\XmlArray is cloned from another - used internally only. |
||||
46 | */ |
||||
47 | public function __construct($data, $auto_trim = false, $level = null, $is_clone = false) |
||||
48 | { |
||||
49 | // If we're using this try to get some more memory. |
||||
50 | detectServer()->setMemoryLimit('128M'); |
||||
51 | |||||
52 | // Set the debug level. |
||||
53 | $this->debug_level = $level ?? error_reporting(); |
||||
54 | $this->trim = $auto_trim; |
||||
55 | |||||
56 | // Is the data already parsed? |
||||
57 | if ($is_clone) |
||||
58 | { |
||||
59 | 4 | $this->array = $data; |
|||
0 ignored issues
–
show
|
|||||
60 | |||||
61 | return; |
||||
62 | 4 | } |
|||
63 | |||||
64 | // Is the input an array? (ie. passed from file()?) |
||||
65 | 4 | if (is_array($data)) |
|||
0 ignored issues
–
show
|
|||||
66 | 4 | { |
|||
67 | $data = implode('', $data); |
||||
68 | } |
||||
69 | 4 | ||||
70 | // Remove any xml declaration or doctype, and parse out comments and CDATA. |
||||
71 | 4 | $data = preg_replace('/<!--.*?-->/s', '', $this->_to_cdata(preg_replace(['/^<\?xml.+?\?' . '>/is', '/<!DOCTYPE[^>]+?' . '>/s'], '', $data))); |
|||
72 | |||||
73 | 4 | // Now parse the xml! |
|||
74 | $this->array = $this->_parse($data); |
||||
75 | } |
||||
76 | |||||
77 | 4 | /** |
|||
78 | * Parse out CDATA tags. (htmlspecialchars them...) |
||||
79 | * |
||||
80 | * @param string $data The data with CDATA tags |
||||
81 | * |
||||
82 | * @return string |
||||
83 | 4 | */ |
|||
84 | protected function _to_cdata($data) |
||||
85 | { |
||||
86 | 4 | $inCdata = false; |
|||
87 | 4 | $inComment = false; |
|||
88 | $output = ''; |
||||
89 | |||||
90 | $parts = preg_split('~(<!\[CDATA\[|\]\]>|<!--|-->)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE); |
||||
91 | foreach ($parts as $part) |
||||
92 | { |
||||
93 | // Handle XML comments. |
||||
94 | if (!$inCdata && $part === '<!--') |
||||
95 | { |
||||
96 | 4 | $inComment = true; |
|||
97 | } |
||||
98 | 4 | if ($inComment && $part === '-->') |
|||
99 | 4 | { |
|||
100 | $inComment = false; |
||||
101 | 4 | } |
|||
102 | 4 | elseif ($inComment) |
|||
103 | { |
||||
104 | continue; |
||||
105 | 4 | } |
|||
106 | |||||
107 | // Handle Cdata blocks. |
||||
108 | elseif (!$inComment && $part === '<![CDATA[') |
||||
109 | 4 | { |
|||
110 | $inCdata = true; |
||||
111 | } |
||||
112 | elseif ($inCdata && $part === ']]>') |
||||
113 | 4 | { |
|||
114 | $inCdata = false; |
||||
115 | } |
||||
116 | elseif ($inCdata) |
||||
117 | { |
||||
118 | $output .= htmlentities($part, ENT_QUOTES); |
||||
119 | 4 | } |
|||
120 | |||||
121 | 4 | // Everything else is kept as is. |
|||
122 | else |
||||
123 | 4 | { |
|||
124 | $output .= $part; |
||||
125 | 4 | } |
|||
126 | } |
||||
127 | 4 | ||||
128 | return $output; |
||||
129 | 4 | } |
|||
130 | |||||
131 | /** |
||||
132 | * Parse data into an array. (privately used...) |
||||
133 | * |
||||
134 | * @param string $data to parse |
||||
135 | 4 | * |
|||
136 | * @return array |
||||
137 | */ |
||||
138 | protected function _parse($data) |
||||
139 | 4 | { |
|||
140 | // Start with an 'empty' array with no data. |
||||
141 | $current = array(); |
||||
142 | |||||
143 | // Loop until we're out of data. |
||||
144 | while ($data !== '') |
||||
145 | { |
||||
146 | // Find and remove the next tag. |
||||
147 | preg_match('/\A<([\w\-:]+)((?:\s+.+?)?)([\s]?\/)?' . '>/', $data, $match); |
||||
148 | if (isset($match[0])) |
||||
149 | 4 | { |
|||
150 | $data = preg_replace('/' . preg_quote($match[0], '/') . '/s', '', $data, 1); |
||||
151 | } |
||||
152 | 4 | ||||
153 | // Didn't find a tag? Keep looping.... |
||||
154 | if (!isset($match[1]) || $match[1] === '') |
||||
155 | 4 | { |
|||
156 | // If there's no <, the rest is data. |
||||
157 | $data_strpos = strpos($data, '<'); |
||||
158 | 4 | if ($data_strpos === false) |
|||
159 | 4 | { |
|||
160 | $text_value = $this->_from_cdata($data); |
||||
161 | 4 | $data = ''; |
|||
162 | |||||
163 | if ($text_value !== '') |
||||
164 | { |
||||
165 | 4 | $current[] = array( |
|||
166 | 'name' => '!', |
||||
167 | 'value' => $text_value |
||||
168 | 4 | ); |
|||
169 | 4 | } |
|||
170 | } |
||||
171 | 4 | // If the < isn't immediately next to the current position... more data. |
|||
172 | 4 | elseif ($data_strpos > 0) |
|||
173 | { |
||||
174 | 4 | $text_value = $this->_from_cdata(substr($data, 0, $data_strpos)); |
|||
175 | $data = substr($data, $data_strpos); |
||||
176 | 4 | ||||
177 | 4 | if ($text_value !== '') |
|||
178 | 4 | { |
|||
179 | $current[] = array( |
||||
180 | 'name' => '!', |
||||
181 | 'value' => $text_value |
||||
182 | ); |
||||
183 | 4 | } |
|||
184 | } |
||||
185 | 4 | // If we're looking at a </something> with no start, kill it. |
|||
186 | 4 | elseif ($data_strpos !== false && $data_strpos === 0) |
|||
187 | { |
||||
188 | 4 | $data_strpos = strpos($data, '<', 1); |
|||
189 | if ($data_strpos !== false) |
||||
190 | 4 | { |
|||
191 | 4 | $text_value = $this->_from_cdata(substr($data, 0, $data_strpos)); |
|||
192 | 4 | $data = substr($data, $data_strpos); |
|||
193 | |||||
194 | if ($text_value !== '') |
||||
195 | { |
||||
196 | $current[] = array( |
||||
197 | 'name' => '!', |
||||
198 | 'value' => $text_value |
||||
199 | ); |
||||
200 | } |
||||
201 | } |
||||
202 | else |
||||
203 | { |
||||
204 | $text_value = $this->_from_cdata($data); |
||||
205 | $data = ''; |
||||
206 | |||||
207 | if ($text_value !== '') |
||||
208 | { |
||||
209 | $current[] = array( |
||||
210 | 'name' => '!', |
||||
211 | 'value' => $text_value |
||||
212 | ); |
||||
213 | } |
||||
214 | } |
||||
215 | } |
||||
216 | |||||
217 | // Wait for an actual occurrence of an element. |
||||
218 | continue; |
||||
219 | } |
||||
220 | |||||
221 | // Create a new element in the array. |
||||
222 | $el = &$current[]; |
||||
223 | $el['name'] = $match[1]; |
||||
224 | |||||
225 | // If this ISN'T empty, remove the close tag and parse the inner data. |
||||
226 | if ((!isset($match[3]) || trim($match[3]) !== '/') && (!isset($match[2]) || trim($match[2]) !== '/')) |
||||
227 | { |
||||
228 | // Because PHP 5.2.0+ seems to croak using regex, we'll have to do this the less fun way. |
||||
229 | 4 | $last_tag_end = strpos($data, '</' . $match[1] . '>'); |
|||
230 | if ($last_tag_end === false) |
||||
231 | { |
||||
232 | continue; |
||||
233 | 4 | } |
|||
234 | 4 | ||||
235 | $offset = 0; |
||||
236 | while (true) |
||||
237 | 4 | { |
|||
238 | // Where is the next start tag? |
||||
239 | $next_tag_start = strpos($data, '<' . $match[1], $offset); |
||||
240 | 4 | ||||
241 | 4 | // If the next start tag is after the last end tag then we've found the right close. |
|||
242 | if ($next_tag_start === false || $next_tag_start > $last_tag_end) |
||||
243 | { |
||||
244 | break; |
||||
245 | } |
||||
246 | 4 | ||||
247 | 4 | // If not then find the next ending tag. |
|||
248 | $next_tag_end = strpos($data, '</' . $match[1] . '>', $offset); |
||||
249 | |||||
250 | 4 | // Didn't find one? Then just use the last and sod it. |
|||
251 | if ($next_tag_end === false) |
||||
252 | { |
||||
253 | 4 | break; |
|||
254 | } |
||||
255 | 4 | else |
|||
256 | { |
||||
257 | $last_tag_end = $next_tag_end; |
||||
258 | $offset = $next_tag_start + 1; |
||||
259 | } |
||||
260 | } |
||||
261 | |||||
262 | // Parse the insides. |
||||
263 | $inner_match = substr($data, 0, $last_tag_end); |
||||
264 | |||||
265 | // Data now starts from where this section ends. |
||||
266 | $data = substr($data, $last_tag_end + strlen('</' . $match[1] . '>')); |
||||
267 | |||||
268 | if (!empty($inner_match)) |
||||
269 | { |
||||
270 | // Parse the inner data. |
||||
271 | if (strpos($inner_match, '<') !== false) |
||||
272 | { |
||||
273 | $el += $this->_parse($inner_match); |
||||
274 | 4 | } |
|||
275 | elseif (trim($inner_match) !== '') |
||||
276 | { |
||||
277 | 4 | $text_value = $this->_from_cdata($inner_match); |
|||
278 | if (trim($text_value) !== '') |
||||
279 | 4 | { |
|||
280 | $el[] = array( |
||||
281 | 'name' => '!', |
||||
282 | 4 | 'value' => $text_value |
|||
283 | ); |
||||
284 | 4 | } |
|||
285 | } |
||||
286 | 4 | } |
|||
287 | } |
||||
288 | 4 | ||||
289 | 4 | // If we're dealing with attributes as well, parse them out. |
|||
290 | if (isset($match[2]) && $match[2] !== '') |
||||
291 | 4 | { |
|||
292 | 4 | // Find all the attribute pairs in the string. |
|||
293 | 4 | preg_match_all('/([\w:]+)="(.+?)"/', $match[2], $attr, PREG_SET_ORDER); |
|||
294 | |||||
295 | // Set them as @attribute-name. |
||||
296 | foreach ($attr as $match_attr) |
||||
297 | { |
||||
298 | $el['@' . $match_attr[1]] = $match_attr[2]; |
||||
299 | } |
||||
300 | } |
||||
301 | 4 | } |
|||
302 | |||||
303 | // Return the parsed array. |
||||
304 | 4 | return $current; |
|||
305 | } |
||||
306 | |||||
307 | 4 | /** |
|||
308 | * Turn the CDATAs back to normal text. |
||||
309 | 4 | * |
|||
310 | * @param string $data The data with CDATA tags |
||||
311 | * |
||||
312 | * @return string |
||||
313 | */ |
||||
314 | protected function _from_cdata($data) |
||||
315 | 4 | { |
|||
316 | // Get the HTML translation table and reverse it |
||||
317 | $trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)); |
||||
318 | |||||
319 | // Translate all the entities out. |
||||
320 | $data = preg_replace_callback('~&#(\d{1,4});~', fn($match) => $this->_from_cdata_callback($match), $data); |
||||
321 | $data = strtr($data, $trans_tbl); |
||||
322 | |||||
323 | return $this->trim ? trim($data) : $data; |
||||
324 | } |
||||
325 | 4 | ||||
326 | /** |
||||
327 | * Callback for the preg_replace in _from_cdata |
||||
328 | 4 | * |
|||
329 | * @param array $match An array of data |
||||
330 | * |
||||
331 | * @return string |
||||
332 | */ |
||||
333 | 4 | protected function _from_cdata_callback($match) |
|||
334 | 4 | { |
|||
335 | return chr($match[1]); |
||||
336 | 4 | } |
|||
337 | |||||
338 | /** |
||||
339 | * Get the root element's name. |
||||
340 | * |
||||
341 | * Example use: |
||||
342 | * echo $element->name(); |
||||
343 | */ |
||||
344 | public function name() |
||||
345 | { |
||||
346 | return $this->array['name'] ?? ''; |
||||
347 | } |
||||
348 | |||||
349 | /** |
||||
350 | * Get a specified element's value or attribute by path. |
||||
351 | * |
||||
352 | * - Children are parsed for text, but only textual data is returned |
||||
353 | * unless get_elements is true. |
||||
354 | * |
||||
355 | * Example use: |
||||
356 | * $data = $xml->fetch('html/head/title'); |
||||
357 | 2 | * |
|||
358 | * @param string $path - the path to the element to fetch |
||||
359 | 2 | * @param bool $get_elements - whether to include elements |
|||
360 | * |
||||
361 | * @return bool|string |
||||
362 | */ |
||||
363 | public function fetch($path, $get_elements = false) |
||||
364 | { |
||||
365 | // Get the element, in array form. |
||||
366 | $array = $this->path($path); |
||||
367 | |||||
368 | if ($array === false) |
||||
369 | { |
||||
370 | return false; |
||||
371 | } |
||||
372 | |||||
373 | // Getting elements into this is a bit complicated... |
||||
374 | if ($get_elements && !is_string($array)) |
||||
375 | { |
||||
376 | 2 | $temp = ''; |
|||
377 | |||||
378 | // Use the _xml() function to get the xml data. |
||||
379 | 2 | foreach ($array->array as $val) |
|||
380 | { |
||||
381 | 2 | // Skip the name and any attributes. |
|||
382 | if (is_array($val)) |
||||
383 | { |
||||
384 | $temp .= $this->_xml($val, null); |
||||
385 | } |
||||
386 | } |
||||
387 | 2 | ||||
388 | // Just get the XML data and then take out the CDATAs. |
||||
389 | return $this->_to_cdata($temp); |
||||
390 | } |
||||
391 | |||||
392 | // Return the value - taking care to pick out all the text values. |
||||
393 | return is_string($array) ? $array : $this->_fetch($array->array); |
||||
0 ignored issues
–
show
|
|||||
394 | } |
||||
395 | |||||
396 | /** |
||||
397 | * Get an element, returns a new \ElkArte\XmlArray. |
||||
398 | * |
||||
399 | * - It finds any elements that match the path specified. |
||||
400 | * - It will always return a set if there is more than one of the element |
||||
401 | * or return_set is true. |
||||
402 | * |
||||
403 | * Example use: |
||||
404 | * $element = $xml->path('html/body'); |
||||
405 | * |
||||
406 | 2 | * @param string $path - the path to the element to get |
|||
407 | * @param bool $return_full - always return full result set |
||||
408 | * @return \ElkArte\XmlArray|bool a new \ElkArte\XmlArray. |
||||
409 | */ |
||||
410 | public function path($path, $return_full = false) |
||||
411 | { |
||||
412 | // Split up the path. |
||||
413 | $path = explode('/', $path); |
||||
414 | |||||
415 | // Start with a base array. |
||||
416 | $array = $this->array; |
||||
417 | |||||
418 | // For each element in the path. |
||||
419 | foreach ($path as $el) |
||||
420 | { |
||||
421 | // Deal with sets.... |
||||
422 | if (strpos($el, '[') !== false) |
||||
423 | 4 | { |
|||
424 | $lvl = (int) substr($el, strpos($el, '[') + 1); |
||||
425 | $el = substr($el, 0, strpos($el, '[')); |
||||
426 | 4 | } |
|||
427 | // Find an attribute. |
||||
428 | elseif (substr($el, 0, 1) === '@') |
||||
429 | 4 | { |
|||
430 | // It simplifies things if the attribute is already there ;). |
||||
431 | if (isset($array[$el])) |
||||
432 | 4 | { |
|||
433 | return $array[$el]; |
||||
434 | } |
||||
435 | 4 | ||||
436 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
||||
437 | 4 | $i = 0; |
|||
438 | 4 | while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] === get_class($this)) |
|||
439 | { |
||||
440 | $i++; |
||||
441 | 2 | } |
|||
442 | $debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line']; |
||||
443 | |||||
444 | 2 | // Cause an error. |
|||
445 | if (($this->debug_level & E_NOTICE) !== 0) |
||||
446 | 2 | { |
|||
447 | trigger_error('Undefined XML attribute: ' . substr($el, 1) . $debug, E_USER_NOTICE); |
||||
448 | } |
||||
449 | |||||
450 | return false; |
||||
451 | } |
||||
452 | else |
||||
453 | { |
||||
454 | $lvl = null; |
||||
455 | } |
||||
456 | |||||
457 | // Find this element. |
||||
458 | $array = $this->_path($array, $el, $lvl); |
||||
459 | } |
||||
460 | |||||
461 | // Clean up after $lvl, for $return_full. |
||||
462 | if ($return_full && (!isset($array['name']) || substr($array['name'], -1) !== ']')) |
||||
463 | { |
||||
464 | $array = ['name' => $el . '[]', $array]; |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
465 | } |
||||
466 | |||||
467 | // Create the right type of class... |
||||
468 | $newClass = get_class($this); |
||||
469 | 2 | ||||
470 | // Return a new \ElkArte\XmlArray for the result. |
||||
471 | return $array === false ? false : new $newClass($array, $this->trim, $this->debug_level, true); |
||||
472 | } |
||||
473 | 4 | ||||
474 | /** |
||||
475 | * Get a specific array by path, one level down. (privately used...) |
||||
476 | * |
||||
477 | 4 | * @param array $array An array of data |
|||
478 | * @param string $path The path |
||||
479 | * @param int $level How far deep into the array we should go |
||||
480 | * @param bool $no_error Whether or not to ignore errors |
||||
481 | * |
||||
482 | * @return array|bool |
||||
483 | 4 | */ |
|||
484 | protected function _path($array, $path, $level, $no_error = false) |
||||
485 | { |
||||
486 | 4 | // Is $array even an array? It might be false! |
|||
487 | if (!is_array($array)) |
||||
0 ignored issues
–
show
|
|||||
488 | { |
||||
489 | return false; |
||||
490 | } |
||||
491 | |||||
492 | // Asking for *no* path? |
||||
493 | if ($path === '' || $path === '.') |
||||
494 | { |
||||
495 | return $array; |
||||
496 | } |
||||
497 | $paths = explode('|', $path); |
||||
498 | |||||
499 | 4 | // A * means all elements of any name. |
|||
500 | $show_all = in_array('*', $paths); |
||||
501 | |||||
502 | 4 | $results = []; |
|||
503 | |||||
504 | // Check each element. |
||||
505 | foreach ($array as $value) |
||||
506 | { |
||||
507 | if (!is_array($value) || $value['name'] === '!') |
||||
508 | 4 | { |
|||
509 | continue; |
||||
510 | 2 | } |
|||
511 | |||||
512 | 4 | if ($show_all || in_array($value['name'], $paths, true)) |
|||
513 | { |
||||
514 | // Skip elements before "the one". |
||||
515 | 4 | if ($level !== null && $level > 0) |
|||
516 | { |
||||
517 | 4 | $level--; |
|||
518 | } |
||||
519 | else |
||||
520 | 4 | { |
|||
521 | $results[] = $value; |
||||
522 | 4 | } |
|||
523 | } |
||||
524 | 4 | } |
|||
525 | |||||
526 | // No results found... |
||||
527 | 4 | if (empty($results)) |
|||
528 | { |
||||
529 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
||||
530 | 4 | $i = 0; |
|||
531 | while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] === get_class($this)) |
||||
532 | { |
||||
533 | $i++; |
||||
534 | } |
||||
535 | |||||
536 | 4 | $debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line']; |
|||
537 | |||||
538 | // Cause an error. |
||||
539 | if ($this->debug_level & E_NOTICE && !$no_error) |
||||
540 | { |
||||
541 | trigger_error('Undefined XML element: ' . $path . $debug, E_USER_NOTICE); |
||||
542 | 4 | } |
|||
543 | |||||
544 | return false; |
||||
545 | } |
||||
546 | |||||
547 | // Only one result. |
||||
548 | if ($level !== null || count($results) === 1) |
||||
0 ignored issues
–
show
|
|||||
549 | { |
||||
550 | return $results[0]; |
||||
551 | } |
||||
552 | |||||
553 | // Return the result set. |
||||
554 | return $results + ['name' => $path . '[]']; |
||||
555 | } |
||||
556 | |||||
557 | /** |
||||
558 | * Get a specific element's xml. (privately used...) |
||||
559 | * |
||||
560 | * @param array $array |
||||
561 | * @param null|int $indent |
||||
562 | 4 | * |
|||
563 | * @return string |
||||
564 | 4 | */ |
|||
565 | protected function _xml($array, $indent) |
||||
566 | { |
||||
567 | $indentation = $indent !== null ? ' |
||||
568 | ' . str_repeat(' ', $indent) : ''; |
||||
569 | 2 | ||||
570 | // This is a set of elements, with no name... |
||||
571 | if (is_array($array) && !isset($array['name'])) |
||||
572 | { |
||||
573 | $temp = ''; |
||||
574 | foreach ($array as $val) |
||||
575 | { |
||||
576 | $temp .= $this->_xml($val, $indent); |
||||
577 | } |
||||
578 | |||||
579 | return $temp; |
||||
580 | } |
||||
581 | |||||
582 | // This is just text! |
||||
583 | if ($array['name'] === '!') |
||||
584 | { |
||||
585 | return $indentation . '<![CDATA[' . $array['value'] . ']]>'; |
||||
586 | } |
||||
587 | |||||
588 | if (substr($array['name'], -2) === '[]') |
||||
589 | { |
||||
590 | $array['name'] = substr($array['name'], 0, -2); |
||||
591 | } |
||||
592 | |||||
593 | // Start the element. |
||||
594 | $output = $indentation . '<' . $array['name']; |
||||
595 | |||||
596 | $inside_elements = false; |
||||
597 | $output_el = ''; |
||||
598 | |||||
599 | // Run through and recursively output all the elements or attributes inside this. |
||||
600 | foreach ($array as $k => $v) |
||||
601 | { |
||||
602 | if (substr($k, 0, 1) === '@') |
||||
603 | { |
||||
604 | $output .= ' ' . substr($k, 1) . '="' . $v . '"'; |
||||
605 | } |
||||
606 | elseif (is_array($v)) |
||||
607 | { |
||||
608 | $output_el .= $this->_xml($v, $indent === null ? null : $indent + 1); |
||||
609 | $inside_elements = true; |
||||
610 | } |
||||
611 | } |
||||
612 | |||||
613 | // Indent, if necessary.... then close the tag. |
||||
614 | if ($inside_elements) |
||||
615 | { |
||||
616 | $output .= '>' . $output_el . $indentation . '</' . $array['name'] . '>'; |
||||
617 | } |
||||
618 | else |
||||
619 | { |
||||
620 | $output .= ' />'; |
||||
621 | } |
||||
622 | |||||
623 | return $output; |
||||
624 | } |
||||
625 | |||||
626 | /** |
||||
627 | * Given an array, return the text from that array. (recursive and privately used.) |
||||
628 | * |
||||
629 | * @param string[]|string $array An array of data |
||||
630 | * |
||||
631 | * @return string |
||||
632 | */ |
||||
633 | protected function _fetch($array) |
||||
634 | { |
||||
635 | // Don't return anything if this is just a string. |
||||
636 | if (is_string($array)) |
||||
637 | { |
||||
638 | return ''; |
||||
639 | } |
||||
640 | |||||
641 | $temp = ''; |
||||
642 | foreach ($array as $text) |
||||
643 | { |
||||
644 | // This means it's most likely an attribute or the name itself. |
||||
645 | if (!isset($text['name'])) |
||||
646 | { |
||||
647 | continue; |
||||
648 | 2 | } |
|||
649 | |||||
650 | // This is text! |
||||
651 | 2 | if ($text['name'] === '!') |
|||
652 | { |
||||
653 | $temp .= $text['value']; |
||||
654 | } |
||||
655 | // Another element - dive in ;). |
||||
656 | 2 | else |
|||
657 | 2 | { |
|||
658 | $temp .= $this->_fetch($text); |
||||
659 | } |
||||
660 | 2 | } |
|||
661 | |||||
662 | 2 | // Return all the bits and pieces we've put together. |
|||
663 | return $temp; |
||||
664 | } |
||||
665 | |||||
666 | 2 | /** |
|||
667 | * Check if an element exists. |
||||
668 | 2 | * |
|||
669 | * Example use, |
||||
670 | * echo $xml->exists('html/body') ? 'y' : 'n'; |
||||
671 | * |
||||
672 | * @param string $path - the path to the element to get. |
||||
673 | 1 | * @return bool |
|||
674 | */ |
||||
675 | public function exists($path) |
||||
676 | { |
||||
677 | // Split up the path. |
||||
678 | 2 | $path = explode('/', $path); |
|||
679 | |||||
680 | // Start with a base array. |
||||
681 | $array = $this->array; |
||||
682 | |||||
683 | // For each element in the path. |
||||
684 | foreach ($path as $el) |
||||
685 | { |
||||
686 | // Deal with sets.... |
||||
687 | $el_strpos = strpos($el, '['); |
||||
688 | |||||
689 | if ($el_strpos !== false) |
||||
690 | 4 | { |
|||
691 | $lvl = (int) substr($el, $el_strpos + 1); |
||||
692 | $el = substr($el, 0, $el_strpos); |
||||
693 | 4 | } |
|||
694 | // Find an attribute. |
||||
695 | elseif (substr($el, 0, 1) === '@') |
||||
696 | 4 | { |
|||
697 | return isset($array[$el]); |
||||
698 | } |
||||
699 | 4 | else |
|||
700 | { |
||||
701 | $lvl = null; |
||||
702 | 4 | } |
|||
703 | |||||
704 | 4 | // Find this element. |
|||
705 | $array = $this->_path($array, $el, $lvl, true); |
||||
0 ignored issues
–
show
It seems like
$array can also be of type boolean ; however, parameter $array of ElkArte\XmlArray::_path() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
706 | 4 | } |
|||
707 | 4 | ||||
708 | return $array !== false; |
||||
709 | } |
||||
710 | 2 | ||||
711 | /** |
||||
712 | 2 | * Count the number of occurrences of a path. |
|||
713 | * |
||||
714 | * Example use: |
||||
715 | * echo $xml->count('html/head/meta'); |
||||
716 | 2 | * |
|||
717 | * @param string $path - the path to search for. |
||||
718 | * @return int the number of elements the path matches. |
||||
719 | */ |
||||
720 | 4 | public function count($path) |
|||
721 | { |
||||
722 | // Get the element, always returning a full set. |
||||
723 | 4 | $temp = $this->path($path, true); |
|||
724 | |||||
725 | // Start at zero, then count up all the numeric keys. |
||||
726 | $i = 0; |
||||
727 | foreach ($temp->array as $item) |
||||
728 | { |
||||
729 | if (is_array($item)) |
||||
730 | { |
||||
731 | $i++; |
||||
732 | } |
||||
733 | } |
||||
734 | |||||
735 | return $i; |
||||
736 | } |
||||
737 | |||||
738 | /** |
||||
739 | * Get an array of \ElkArte\XmlArray's matching the specified path. |
||||
740 | * |
||||
741 | * - This differs from ->path(path, true) in that instead of an \ElkArte\XmlArray |
||||
742 | * of elements, an array of \ElkArte\XmlArray's is returned for use with foreach. |
||||
743 | * |
||||
744 | * Example use: |
||||
745 | * foreach ($xml->set('html/body/p') as $p) |
||||
746 | * |
||||
747 | * @param string $path - the path to search for. |
||||
748 | * @return array an array of \ElkArte\XmlArray objects |
||||
749 | */ |
||||
750 | public function set($path) |
||||
751 | { |
||||
752 | // None as yet, just get the path. |
||||
753 | $array = array(); |
||||
754 | $xml = $this->path($path, true); |
||||
755 | |||||
756 | foreach ($xml->array as $val) |
||||
757 | { |
||||
758 | // Skip these, they aren't elements. |
||||
759 | if (!is_array($val) || $val['name'] === '!') |
||||
760 | { |
||||
761 | continue; |
||||
762 | } |
||||
763 | |||||
764 | // Create the right type of class... |
||||
765 | 2 | $newClass = get_class($this); |
|||
766 | |||||
767 | // Create a new \ElkArte\XmlArray and stick it in the array. |
||||
768 | 2 | $array[] = new $newClass($val, $this->trim, $this->debug_level, true); |
|||
769 | 2 | } |
|||
770 | |||||
771 | 2 | return $array; |
|||
772 | } |
||||
773 | |||||
774 | 2 | /** |
|||
775 | * Create an xml file from an \ElkArte\XmlArray, the specified path if any. |
||||
776 | 2 | * |
|||
777 | * Example use: |
||||
778 | * echo $this->create_xml(); |
||||
779 | * |
||||
780 | 2 | * @param string|null $path - the path to the element. (optional) |
|||
781 | * @return string xml-formatted string. |
||||
782 | */ |
||||
783 | 2 | public function create_xml($path = null) |
|||
784 | { |
||||
785 | // Was a path specified? If so, use that array. |
||||
786 | 2 | if ($path !== null) |
|||
787 | { |
||||
788 | $path = $this->path($path); |
||||
789 | |||||
790 | // The path was not found |
||||
791 | if ($path === false) |
||||
792 | { |
||||
793 | return false; |
||||
0 ignored issues
–
show
|
|||||
794 | } |
||||
795 | |||||
796 | $path = $path->array; |
||||
797 | } |
||||
798 | // Just use the current array. |
||||
799 | else |
||||
800 | { |
||||
801 | $path = $this->array; |
||||
802 | } |
||||
803 | |||||
804 | // Add the xml declaration to the front. |
||||
805 | return '<?xml version="1.0"?' . '>' . $this->_xml($path, 0); |
||||
806 | } |
||||
807 | |||||
808 | /** |
||||
809 | * Output the xml in an array form. |
||||
810 | * |
||||
811 | * Example use: |
||||
812 | * print_r($xml->to_array()); |
||||
813 | * |
||||
814 | * @param string|null $path the path to output. |
||||
815 | * |
||||
816 | * @return array|bool|string |
||||
817 | */ |
||||
818 | public function to_array($path = null) |
||||
819 | { |
||||
820 | // Are we doing a specific path? |
||||
821 | if ($path !== null) |
||||
822 | { |
||||
823 | $path = $this->path($path); |
||||
824 | |||||
825 | // The path was not found |
||||
826 | if ($path === false) |
||||
827 | { |
||||
828 | return false; |
||||
829 | } |
||||
830 | |||||
831 | $path = $path->array; |
||||
832 | } |
||||
833 | 4 | // No, so just use the current array. |
|||
834 | else |
||||
835 | { |
||||
836 | 4 | $path = $this->array; |
|||
837 | } |
||||
838 | |||||
839 | return $this->_array($path); |
||||
840 | } |
||||
841 | |||||
842 | /** |
||||
843 | * Return an element as an array |
||||
844 | * |
||||
845 | * @param array $array An array of data |
||||
846 | * |
||||
847 | * @return array|string |
||||
848 | */ |
||||
849 | protected function _array($array) |
||||
850 | { |
||||
851 | 4 | $return = array(); |
|||
852 | $text = ''; |
||||
853 | foreach ($array as $value) |
||||
854 | 4 | { |
|||
855 | if (!is_array($value) || !isset($value['name'])) |
||||
856 | { |
||||
857 | continue; |
||||
858 | } |
||||
859 | |||||
860 | if ($value['name'] === '!') |
||||
861 | { |
||||
862 | $text .= $value['value']; |
||||
863 | } |
||||
864 | 4 | else |
|||
865 | { |
||||
866 | 4 | $return[$value['name']] = $this->_array($value); |
|||
867 | 4 | } |
|||
868 | 4 | } |
|||
869 | |||||
870 | 4 | if (empty($return)) |
|||
871 | { |
||||
872 | 4 | return $text; |
|||
873 | } |
||||
874 | |||||
875 | 4 | return $return; |
|||
876 | } |
||||
877 | } |
||||
878 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..