xmlArray   F
last analyzed

Complexity

Total Complexity 129

Size/Duplication

Total Lines 722
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 270
dl 0
loc 722
rs 2
c 0
b 0
f 0
wmc 129

16 Methods

Rating   Name   Duplication   Size   Complexity  
A name() 0 3 2
A __construct() 0 25 4
B fetch() 0 27 7
A create_xml() 0 19 3
C _to_cdata() 0 30 12
A _array() 0 19 6
C path() 0 58 13
D _parse() 0 141 30
A exists() 0 28 4
A set() 0 20 4
C _xml() 0 45 12
A to_array() 0 18 3
A count() 0 14 3
A _fetch() 0 23 5
D _path() 0 57 19
A _from_cdata() 0 19 2

How to fix   Complexity   

Complex Class

Complex classes like xmlArray often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use xmlArray, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * The xmlArray class is an xml parser.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2022 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1.0
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * Class xmlArray
21
 * Represents an XML array
22
 */
23
class xmlArray
24
{
25
	/**
26
	 * @var array Holds parsed XML results
27
	 */
28
	public $array;
29
30
	/**
31
	 * @var int The debugging level
32
	 */
33
	public $debug_level;
34
35
	/**
36
	 * holds trim level textual data
37
	 *
38
	 * @var bool Holds trim level textual data
39
	 */
40
	public $trim;
41
42
	/**
43
	 * Constructor for the xml parser.
44
	 * Example use:
45
	 *  $xml = new xmlArray(file('data.xml'));
46
	 *
47
	 * @param string $data The xml data or an array of, unless is_clone is true.
48
	 * @param bool $auto_trim Used to automatically trim textual data.
49
	 * @param int $level The debug level. Specifies whether notices should be generated for missing elements and attributes.
50
	 * @param bool $is_clone default false. If is_clone is true, the  xmlArray is cloned from another - used internally only.
51
	 */
52
	public function __construct($data, $auto_trim = false, $level = null, $is_clone = false)
53
	{
54
		// If we're using this try to get some more memory.
55
		setMemoryLimit('32M');
56
57
		// Set the debug level.
58
		$this->debug_level = $level !== null ? $level : error_reporting();
59
		$this->trim = $auto_trim;
60
61
		// Is the data already parsed?
62
		if ($is_clone)
63
		{
64
			$this->array = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type string is incompatible with the declared type array of property $array.

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..

Loading history...
65
			return;
66
		}
67
68
		// Is the input an array? (ie. passed from file()?)
69
		if (is_array($data))
0 ignored issues
show
introduced by
The condition is_array($data) is always false.
Loading history...
70
			$data = implode('', $data);
71
72
		// Remove any xml declaration or doctype, and parse out comments and CDATA.
73
		$data = preg_replace('/<!--.*?-->/s', '', $this->_to_cdata(preg_replace(array('/^<\?xml.+?\?' . '>/is', '/<!DOCTYPE[^>]+?' . '>/s'), '', $data)));
74
75
		// Now parse the xml!
76
		$this->array = $this->_parse($data);
77
	}
78
79
	/**
80
	 * Get the root element's name.
81
	 * Example use:
82
	 *  echo $element->name();
83
	 *
84
	 * @return string The root element's name
85
	 */
86
	public function name()
87
	{
88
		return isset($this->array['name']) ? $this->array['name'] : '';
89
	}
90
91
	/**
92
	 * Get a specified element's value or attribute by path.
93
	 * Children are parsed for text, but only textual data is returned
94
	 * unless get_elements is true.
95
	 * Example use:
96
	 *  $data = $xml->fetch('html/head/title');
97
	 *
98
	 * @param string $path The path to the element to fetch
99
	 * @param bool $get_elements Whether to include elements
100
	 * @return string The value or attribute of the specified element
101
	 */
102
	public function fetch($path, $get_elements = false)
103
	{
104
		// Get the element, in array form.
105
		$array = $this->path($path);
106
107
		if ($array === false)
0 ignored issues
show
introduced by
The condition $array === false is always false.
Loading history...
108
			return false;
109
110
		// Getting elements into this is a bit complicated...
111
		if ($get_elements && !is_string($array))
112
		{
113
			$temp = '';
114
115
			// Use the _xml() function to get the xml data.
116
			foreach ($array->array as $val)
117
			{
118
				// Skip the name and any attributes.
119
				if (is_array($val))
120
					$temp .= $this->_xml($val, null);
121
			}
122
123
			// Just get the XML data and then take out the CDATAs.
124
			return $this->_to_cdata($temp);
125
		}
126
127
		// Return the value - taking care to pick out all the text values.
128
		return is_string($array) ? $array : $this->_fetch($array->array);
0 ignored issues
show
introduced by
The condition is_string($array) is always false.
Loading history...
129
	}
130
131
	/** Get an element, returns a new xmlArray.
132
	 * It finds any elements that match the path specified.
133
	 * It will always return a set if there is more than one of the element
134
	 * or return_set is true.
135
	 * Example use:
136
	 *  $element = $xml->path('html/body');
137
	 *
138
	 * @param $path string The path to the element to get
139
	 * @param $return_full bool Whether to return the full result set
140
	 * @return xmlArray a new xmlArray.
141
	 */
142
	public function path($path, $return_full = false)
143
	{
144
		global $txt;
145
146
		// Split up the path.
147
		$path = explode('/', $path);
148
149
		// Start with a base array.
150
		$array = $this->array;
151
152
		// For each element in the path.
153
		foreach ($path as $el)
154
		{
155
			// Deal with sets....
156
			if (strpos($el, '[') !== false)
157
			{
158
				$lvl = (int) substr($el, strpos($el, '[') + 1);
159
				$el = substr($el, 0, strpos($el, '['));
160
			}
161
			// Find an attribute.
162
			elseif (substr($el, 0, 1) == '@')
163
			{
164
				// It simplifies things if the attribute is already there ;).
165
				if (isset($array[$el]))
166
					return $array[$el];
167
				else
168
				{
169
					$trace = debug_backtrace();
170
					$i = 0;
171
					while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this))
172
						$i++;
173
					$debug = ' (from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'] . ')';
174
175
					// Cause an error.
176
					if ($this->debug_level & E_NOTICE)
177
					{
178
						loadLanguage('Errors');
179
						trigger_error(sprintf($txt['undefined_xml_attribute'], substr($el, 1) . $debug), E_USER_NOTICE);
180
					}
181
					return false;
182
				}
183
			}
184
			else
185
				$lvl = null;
186
187
			// Find this element.
188
			$array = $this->_path($array, $el, $lvl);
189
		}
190
191
		// Clean up after $lvl, for $return_full.
192
		if ($return_full && (!isset($array['name']) || substr($array['name'], -1) != ']'))
193
			$array = array('name' => $el . '[]', $array);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $el seems to be defined by a foreach iteration on line 153. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
194
195
		// Create the right type of class...
196
		$newClass = get_class($this);
197
198
		// Return a new xmlArray for the result.
199
		return $array === false ? false : new $newClass($array, $this->trim, $this->debug_level, true);
0 ignored issues
show
introduced by
The condition $array === false is always false.
Loading history...
200
	}
201
202
	/**
203
	 * Check if an element exists.
204
	 * Example use,
205
	 *  echo $xml->exists('html/body') ? 'y' : 'n';
206
	 *
207
	 * @param string $path The path to the element to get.
208
	 * @return boolean Whether the specified path exists
209
	 */
210
	public function exists($path)
211
	{
212
		// Split up the path.
213
		$path = explode('/', $path);
214
215
		// Start with a base array.
216
		$array = $this->array;
217
218
		// For each element in the path.
219
		foreach ($path as $el)
220
		{
221
			// Deal with sets....
222
			if (strpos($el, '[') !== false)
223
			{
224
				$lvl = (int) substr($el, strpos($el, '[') + 1);
225
				$el = substr($el, 0, strpos($el, '['));
226
			}
227
			// Find an attribute.
228
			elseif (substr($el, 0, 1) == '@')
229
				return isset($array[$el]);
230
			else
231
				$lvl = null;
232
233
			// Find this element.
234
			$array = $this->_path($array, $el, $lvl, true);
0 ignored issues
show
Bug introduced by
It seems like $array can also be of type string; however, parameter $array of 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 ignore-type  annotation

234
			$array = $this->_path(/** @scrutinizer ignore-type */ $array, $el, $lvl, true);
Loading history...
235
		}
236
237
		return $array !== false;
238
	}
239
240
	/**
241
	 * Count the number of occurrences of a path.
242
	 * Example use:
243
	 *  echo $xml->count('html/head/meta');
244
	 *
245
	 * @param string $path The path to search for.
246
	 * @return int The number of elements the path matches.
247
	 */
248
	public function count($path)
249
	{
250
		// Get the element, always returning a full set.
251
		$temp = $this->path($path, true);
252
253
		// Start at zero, then count up all the numeric keys.
254
		$i = 0;
255
		foreach ($temp->array as $item)
256
		{
257
			if (is_array($item))
258
				$i++;
259
		}
260
261
		return $i;
262
	}
263
264
	/**
265
	 * Get an array of xmlArray's matching the specified path.
266
	 * This differs from ->path(path, true) in that instead of an xmlArray
267
	 * of elements, an array of xmlArray's is returned for use with foreach.
268
	 * Example use:
269
	 *  foreach ($xml->set('html/body/p') as $p)
270
	 *
271
	 * @param $path string The path to search for.
272
	 * @return xmlArray[] An array of xmlArray objects
273
	 */
274
	public function set($path)
275
	{
276
		// None as yet, just get the path.
277
		$array = array();
278
		$xml = $this->path($path, true);
279
280
		foreach ($xml->array as $val)
281
		{
282
			// Skip these, they aren't elements.
283
			if (!is_array($val) || $val['name'] == '!')
284
				continue;
285
286
			// Create the right type of class...
287
			$newClass = get_class($this);
288
289
			// Create a new xmlArray and stick it in the array.
290
			$array[] = new $newClass($val, $this->trim, $this->debug_level, true);
291
		}
292
293
		return $array;
294
	}
295
296
	/**
297
	 * Create an xml file from an xmlArray, the specified path if any.
298
	 * Example use:
299
	 *  echo $this->create_xml();
300
	 *
301
	 * @param string $path The path to the element. (optional)
302
	 * @return string Xml-formatted string.
303
	 */
304
	public function create_xml($path = null)
305
	{
306
		// Was a path specified?  If so, use that array.
307
		if ($path !== null)
308
		{
309
			$path = $this->path($path);
310
311
			// The path was not found
312
			if ($path === false)
0 ignored issues
show
introduced by
The condition $path === false is always false.
Loading history...
313
				return false;
314
315
			$path = $path->array;
316
		}
317
		// Just use the current array.
318
		else
319
			$path = $this->array;
320
321
		// Add the xml declaration to the front.
322
		return '<?xml version="1.0"?' . '>' . $this->_xml($path, 0);
323
	}
324
325
	/**
326
	 * Output the xml in an array form.
327
	 * Example use:
328
	 *  print_r($xml->to_array());
329
	 *
330
	 * @param string $path The path to output.
331
	 * @return array An array of XML data
332
	 */
333
	public function to_array($path = null)
334
	{
335
		// Are we doing a specific path?
336
		if ($path !== null)
337
		{
338
			$path = $this->path($path);
339
340
			// The path was not found
341
			if ($path === false)
0 ignored issues
show
introduced by
The condition $path === false is always false.
Loading history...
342
				return false;
343
344
			$path = $path->array;
345
		}
346
		// No, so just use the current array.
347
		else
348
			$path = $this->array;
349
350
		return $this->_array($path);
351
	}
352
353
	/**
354
	 * Parse data into an array. (privately used...)
355
	 *
356
	 * @param string $data The data to parse
357
	 * @return array The parsed array
358
	 */
359
	protected function _parse($data)
360
	{
361
		// Start with an 'empty' array with no data.
362
		$current = array(
363
		);
364
365
		// Loop until we're out of data.
366
		while ($data != '')
367
		{
368
			// Find and remove the next tag.
369
			preg_match('/\A<([\w\-:]+)((?:\s+.+?)?)([\s]?\/)?' . '>/', $data, $match);
370
			if (isset($match[0]))
371
				$data = preg_replace('/' . preg_quote($match[0], '/') . '/s', '', $data, 1);
372
373
			// Didn't find a tag?  Keep looping....
374
			if (!isset($match[1]) || $match[1] == '')
375
			{
376
				// If there's no <, the rest is data.
377
				if (strpos($data, '<') === false)
378
				{
379
					$text_value = $this->_from_cdata($data);
380
					$data = '';
381
382
					if ($text_value != '')
383
						$current[] = array(
384
							'name' => '!',
385
							'value' => $text_value
386
						);
387
				}
388
				// If the < isn't immediately next to the current position... more data.
389
				elseif (strpos($data, '<') > 0)
390
				{
391
					$text_value = $this->_from_cdata(substr($data, 0, strpos($data, '<')));
392
					$data = substr($data, strpos($data, '<'));
393
394
					if ($text_value != '')
395
						$current[] = array(
396
							'name' => '!',
397
							'value' => $text_value
398
						);
399
				}
400
				// If we're looking at a </something> with no start, kill it.
401
				elseif (strpos($data, '<') !== false && strpos($data, '<') == 0)
402
				{
403
					if (strpos($data, '<', 1) !== false)
404
					{
405
						$text_value = $this->_from_cdata(substr($data, 0, strpos($data, '<', 1)));
406
						$data = substr($data, strpos($data, '<', 1));
407
408
						if ($text_value != '')
409
							$current[] = array(
410
								'name' => '!',
411
								'value' => $text_value
412
							);
413
					}
414
					else
415
					{
416
						$text_value = $this->_from_cdata($data);
417
						$data = '';
418
419
						if ($text_value != '')
420
							$current[] = array(
421
								'name' => '!',
422
								'value' => $text_value
423
							);
424
					}
425
				}
426
427
				// Wait for an actual occurance of an element.
428
				continue;
429
			}
430
431
			// Create a new element in the array.
432
			$el = &$current[];
433
			$el['name'] = $match[1];
434
435
			// If this ISN'T empty, remove the close tag and parse the inner data.
436
			if ((!isset($match[3]) || trim($match[3]) != '/') && (!isset($match[2]) || trim($match[2]) != '/'))
437
			{
438
				// Because PHP 5.2.0+ seems to croak using regex, we'll have to do this the less fun way.
439
				$last_tag_end = strpos($data, '</' . $match[1] . '>');
440
				if ($last_tag_end === false)
441
					continue;
442
443
				$offset = 0;
444
				while (1 == 1)
445
				{
446
					// Where is the next start tag?
447
					$next_tag_start = strpos($data, '<' . $match[1], $offset);
448
					// If the next start tag is after the last end tag then we've found the right close.
449
					if ($next_tag_start === false || $next_tag_start > $last_tag_end)
450
						break;
451
452
					// If not then find the next ending tag.
453
					$next_tag_end = strpos($data, '</' . $match[1] . '>', $offset);
454
455
					// Didn't find one? Then just use the last and sod it.
456
					if ($next_tag_end === false)
457
						break;
458
					else
459
					{
460
						$last_tag_end = $next_tag_end;
461
						$offset = $next_tag_start + 1;
462
					}
463
				}
464
				// Parse the insides.
465
				$inner_match = substr($data, 0, $last_tag_end);
466
				// Data now starts from where this section ends.
467
				$data = substr($data, $last_tag_end + strlen('</' . $match[1] . '>'));
468
469
				if (!empty($inner_match))
470
				{
471
					// Parse the inner data.
472
					if (strpos($inner_match, '<') !== false)
473
						$el += $this->_parse($inner_match);
474
					elseif (trim($inner_match) != '')
475
					{
476
						$text_value = $this->_from_cdata($inner_match);
477
						if ($text_value != '')
478
							$el[] = array(
479
								'name' => '!',
480
								'value' => $text_value
481
							);
482
					}
483
				}
484
			}
485
486
			// If we're dealing with attributes as well, parse them out.
487
			if (isset($match[2]) && $match[2] != '')
488
			{
489
				// Find all the attribute pairs in the string.
490
				preg_match_all('/([\w:]+)="(.+?)"/', $match[2], $attr, PREG_SET_ORDER);
491
492
				// Set them as @attribute-name.
493
				foreach ($attr as $match_attr)
494
					$el['@' . $match_attr[1]] = $match_attr[2];
495
			}
496
		}
497
498
		// Return the parsed array.
499
		return $current;
500
	}
501
502
	/**
503
	 * Get a specific element's xml. (privately used...)
504
	 *
505
	 * @param array $array An array of element data
506
	 * @param null|int $indent How many levels to indent the elements (null = no indent)
507
	 * @return string The formatted XML
508
	 */
509
	protected function _xml($array, $indent)
510
	{
511
		$indentation = $indent !== null ? '
512
' . str_repeat('	', $indent) : '';
513
514
		// This is a set of elements, with no name...
515
		if (is_array($array) && !isset($array['name']))
516
		{
517
			$temp = '';
518
			foreach ($array as $val)
519
				$temp .= $this->_xml($val, $indent);
520
			return $temp;
521
		}
522
523
		// This is just text!
524
		if ($array['name'] == '!')
525
			return $indentation . '<![CDATA[' . $array['value'] . ']]>';
526
		elseif (substr($array['name'], -2) == '[]')
527
			$array['name'] = substr($array['name'], 0, -2);
528
529
		// Start the element.
530
		$output = $indentation . '<' . $array['name'];
531
532
		$inside_elements = false;
533
		$output_el = '';
534
535
		// Run through and recursively output all the elements or attrbutes inside this.
536
		foreach ($array as $k => $v)
537
		{
538
			if (substr($k, 0, 1) == '@')
539
				$output .= ' ' . substr($k, 1) . '="' . $v . '"';
540
			elseif (is_array($v))
541
			{
542
				$output_el .= $this->_xml($v, $indent === null ? null : $indent + 1);
543
				$inside_elements = true;
544
			}
545
		}
546
547
		// Indent, if necessary.... then close the tag.
548
		if ($inside_elements)
549
			$output .= '>' . $output_el . $indentation . '</' . $array['name'] . '>';
550
		else
551
			$output .= ' />';
552
553
		return $output;
554
	}
555
556
	/**
557
	 * Return an element as an array
558
	 *
559
	 * @param array $array An array of data
560
	 * @return string|array A string with the element's value or an array of element data
561
	 */
562
	protected function _array($array)
563
	{
564
		$return = array();
565
		$text = '';
566
		foreach ($array as $value)
567
		{
568
			if (!is_array($value) || !isset($value['name']))
569
				continue;
570
571
			if ($value['name'] == '!')
572
				$text .= $value['value'];
573
			else
574
				$return[$value['name']] = $this->_array($value);
575
		}
576
577
		if (empty($return))
578
			return $text;
579
		else
580
			return $return;
581
	}
582
583
	/**
584
	 * Parse out CDATA tags. (htmlspecialchars them...)
585
	 *
586
	 * @param string $data The data with CDATA tags included
587
	 * @return string The data contained within CDATA tags
588
	 */
589
	function _to_cdata($data)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
590
	{
591
		$inCdata = $inComment = false;
592
		$output = '';
593
594
		$parts = preg_split('~(<!\[CDATA\[|\]\]>|<!--|-->)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
595
		foreach ($parts as $part)
596
		{
597
			// Handle XML comments.
598
			if (!$inCdata && $part === '<!--')
599
				$inComment = true;
600
			if ($inComment && $part === '-->')
601
				$inComment = false;
602
			elseif ($inComment)
603
				continue;
604
605
			// Handle Cdata blocks.
606
			elseif (!$inComment && $part === '<![CDATA[')
607
				$inCdata = true;
608
			elseif ($inCdata && $part === ']]>')
609
				$inCdata = false;
610
			elseif ($inCdata)
611
				$output .= htmlentities($part, ENT_QUOTES);
612
613
			// Everything else is kept as is.
614
			else
615
				$output .= $part;
616
		}
617
618
		return $output;
619
	}
620
621
	/**
622
	 * Turn the CDATAs back to normal text.
623
	 *
624
	 * @param string $data The data with CDATA tags
625
	 * @return string The transformed data
626
	 */
627
	protected function _from_cdata($data)
628
	{
629
		// Get the HTML translation table and reverse it.
630
		$trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES));
631
632
		// Translate all the entities out.
633
		$data = strtr(
634
			preg_replace_callback(
635
				'~&#(\d{1,4});~',
636
				function($m)
637
				{
638
					return chr("$m[1]");
0 ignored issues
show
Bug introduced by
$m['1'] of type string is incompatible with the type integer expected by parameter $codepoint of chr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

638
					return chr(/** @scrutinizer ignore-type */ "$m[1]");
Loading history...
639
				},
640
				$data
641
			),
642
			$trans_tbl
643
		);
644
645
		return $this->trim ? trim($data) : $data;
646
	}
647
648
	/**
649
	 * Given an array, return the text from that array. (recursive and privately used.)
650
	 *
651
	 * @param array $array An aray of data
652
	 * @return string The text from the array
653
	 */
654
	protected function _fetch($array)
655
	{
656
		// Don't return anything if this is just a string.
657
		if (is_string($array))
0 ignored issues
show
introduced by
The condition is_string($array) is always false.
Loading history...
658
			return '';
659
660
		$temp = '';
661
		foreach ($array as $text)
662
		{
663
			// This means it's most likely an attribute or the name itself.
664
			if (!isset($text['name']))
665
				continue;
666
667
			// This is text!
668
			if ($text['name'] == '!')
669
				$temp .= $text['value'];
670
			// Another element - dive in ;).
671
			else
672
				$temp .= $this->_fetch($text);
673
		}
674
675
		// Return all the bits and pieces we've put together.
676
		return $temp;
677
	}
678
679
	/**
680
	 * Get a specific array by path, one level down. (privately used...)
681
	 *
682
	 * @param array $array An array of data
683
	 * @param string $path The path
684
	 * @param int $level How far deep into the array we should go
685
	 * @param bool $no_error Whether or not to ignore errors
686
	 * @return string|array The specified array (or the contents of said array if there's only one result)
687
	 */
688
	protected function _path($array, $path, $level, $no_error = false)
689
	{
690
		global $txt;
691
692
		// Is $array even an array?  It might be false!
693
		if (!is_array($array))
0 ignored issues
show
introduced by
The condition is_array($array) is always true.
Loading history...
694
			return false;
695
696
		// Asking for *no* path?
697
		if ($path == '' || $path == '.')
698
			return $array;
699
		$paths = explode('|', $path);
700
701
		// A * means all elements of any name.
702
		$show_all = in_array('*', $paths);
703
704
		$results = array();
705
706
		// Check each element.
707
		foreach ($array as $value)
708
		{
709
			if (!is_array($value) || $value['name'] === '!')
710
				continue;
711
712
			if ($show_all || in_array($value['name'], $paths))
713
			{
714
				// Skip elements before "the one".
715
				if ($level !== null && $level > 0)
716
					$level--;
717
				else
718
					$results[] = $value;
719
			}
720
		}
721
722
		// No results found...
723
		if (empty($results))
724
		{
725
			$trace = debug_backtrace();
726
			$i = 0;
727
			while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this))
728
				$i++;
729
			$debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'];
730
731
			// Cause an error.
732
			if ($this->debug_level & E_NOTICE && !$no_error)
733
			{
734
				loadLanguage('Errors');
735
				trigger_error(sprintf($txt['undefined_xml_element'], $path . $debug), E_USER_NOTICE);
736
			}
737
			return false;
738
		}
739
		// Only one result.
740
		elseif (count($results) == 1 || $level !== null)
0 ignored issues
show
introduced by
The condition $level !== null is always true.
Loading history...
741
			return $results[0];
742
		// Return the result set.
743
		else
744
			return $results + array('name' => $path . '[]');
745
	}
746
}
747
748
/**
749
 * Class ftp_connection
750
 * Simple FTP protocol implementation.
751
 *
752
 * @see https://tools.ietf.org/html/rfc959
753
 */
754
class ftp_connection
755
{
756
	/**
757
	 * @var string Holds the connection response
758
	 */
759
	public $connection;
760
761
	/**
762
	 * @var string Holds any errors
763
	 */
764
	public $error;
765
766
	/**
767
	 * @var string Holds the last message from the server
768
	 */
769
	public $last_message;
770
771
	/**
772
	 * @var boolean Whether or not this is a passive connection
773
	 */
774
	public $pasv;
775
776
	/**
777
	 * Create a new FTP connection...
778
	 *
779
	 * @param string $ftp_server The server to connect to
780
	 * @param int $ftp_port The port to connect to
781
	 * @param string $ftp_user The username
782
	 * @param string $ftp_pass The password
783
	 */
784
	public function __construct($ftp_server, $ftp_port = 21, $ftp_user = 'anonymous', $ftp_pass = '[email protected]')
785
	{
786
		// Initialize variables.
787
		$this->connection = 'no_connection';
788
		$this->error = false;
0 ignored issues
show
Documentation Bug introduced by
The property $error was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
789
		$this->pasv = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $pasv.

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..

Loading history...
790
791
		if ($ftp_server !== null)
0 ignored issues
show
introduced by
The condition $ftp_server !== null is always true.
Loading history...
792
			$this->connect($ftp_server, $ftp_port, $ftp_user, $ftp_pass);
793
	}
794
795
	/**
796
	 * Connects to a server
797
	 *
798
	 * @param string $ftp_server The address of the server
799
	 * @param int $ftp_port The port
800
	 * @param string $ftp_user The username
801
	 * @param string $ftp_pass The password
802
	 */
803
	public function connect($ftp_server, $ftp_port = 21, $ftp_user = 'anonymous', $ftp_pass = '[email protected]')
804
	{
805
		if (strpos($ftp_server, 'ftp://') === 0)
806
			$ftp_server = substr($ftp_server, 6);
807
		elseif (strpos($ftp_server, 'ftps://') === 0)
808
			$ftp_server = 'ssl://' . substr($ftp_server, 7);
809
		if (strpos($ftp_server, 'http://') === 0)
810
			$ftp_server = substr($ftp_server, 7);
811
		elseif (strpos($ftp_server, 'https://') === 0)
812
			$ftp_server = substr($ftp_server, 8);
813
		$ftp_server = strtr($ftp_server, array('/' => '', ':' => '', '@' => ''));
814
815
		// Connect to the FTP server.
816
		$this->connection = @fsockopen($ftp_server, $ftp_port, $err, $err, 5);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $err seems to be never defined.
Loading history...
Documentation Bug introduced by
It seems like @fsockopen($ftp_server, $ftp_port, $err, $err, 5) of type false or resource is incompatible with the declared type string of property $connection.

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..

Loading history...
817
		if (!$this->connection)
818
		{
819
			$this->error = 'bad_server';
820
			$this->last_message = 'Invalid Server';
821
			return;
822
		}
823
824
		// Get the welcome message...
825
		if (!$this->check_response(220))
826
		{
827
			$this->error = 'bad_response';
828
			$this->last_message = 'Bad Response';
829
			return;
830
		}
831
832
		// Send the username, it should ask for a password.
833
		fwrite($this->connection, 'USER ' . $ftp_user . "\r\n");
834
835
		if (!$this->check_response(331))
836
		{
837
			$this->error = 'bad_username';
838
			$this->last_message = 'Invalid Username';
839
			return;
840
		}
841
842
		// Now send the password... and hope it goes okay.
843
844
		fwrite($this->connection, 'PASS ' . $ftp_pass . "\r\n");
845
		if (!$this->check_response(230))
846
		{
847
			$this->error = 'bad_password';
848
			$this->last_message = 'Invalid Password';
849
			return;
850
		}
851
	}
852
853
	/**
854
	 * Changes to a directory (chdir) via the ftp connection
855
	 *
856
	 * @param string $ftp_path The path to the directory we want to change to
857
	 * @return boolean Whether or not the operation was successful
858
	 */
859
	public function chdir($ftp_path)
860
	{
861
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
862
			return false;
863
864
		// No slash on the end, please...
865
		if ($ftp_path !== '/' && substr($ftp_path, -1) === '/')
866
			$ftp_path = substr($ftp_path, 0, -1);
867
868
		fwrite($this->connection, 'CWD ' . $ftp_path . "\r\n");
869
		if (!$this->check_response(250))
870
		{
871
			$this->error = 'bad_path';
872
			return false;
873
		}
874
875
		return true;
876
	}
877
878
	/**
879
	 * Changes a files atrributes (chmod)
880
	 *
881
	 * @param string $ftp_file The file to CHMOD
882
	 * @param int|string $chmod The value for the CHMOD operation
883
	 * @return boolean Whether or not the operation was successful
884
	 */
885
	public function chmod($ftp_file, $chmod)
0 ignored issues
show
Unused Code introduced by
The parameter $chmod is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

885
	public function chmod($ftp_file, /** @scrutinizer ignore-unused */ $chmod)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
886
	{
887
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
888
			return false;
889
890
		if ($ftp_file == '')
891
			$ftp_file = '.';
892
893
		// Do we have a file or a dir?
894
		$is_dir = is_dir($ftp_file);
895
		$is_writable = false;
896
897
		// Set different modes.
898
		$chmod_values = $is_dir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
899
900
		foreach ($chmod_values as $val)
901
		{
902
			// If it's writable, break out of the loop.
903
			if (is_writable($ftp_file))
904
			{
905
				$is_writable = true;
906
				break;
907
			}
908
909
			else
910
			{
911
				// Convert the chmod value from octal (0777) to text ("777").
912
				fwrite($this->connection, 'SITE CHMOD ' . decoct($val) . ' ' . $ftp_file . "\r\n");
913
				if (!$this->check_response(200))
914
				{
915
					$this->error = 'bad_file';
916
					break;
917
				}
918
			}
919
		}
920
		return $is_writable;
921
	}
922
923
	/**
924
	 * Deletes a file
925
	 *
926
	 * @param string $ftp_file The file to delete
927
	 * @return boolean Whether or not the operation was successful
928
	 */
929
	public function unlink($ftp_file)
930
	{
931
		// We are actually connected, right?
932
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
933
			return false;
934
935
		// Delete file X.
936
		fwrite($this->connection, 'DELE ' . $ftp_file . "\r\n");
937
		if (!$this->check_response(250))
938
		{
939
			fwrite($this->connection, 'RMD ' . $ftp_file . "\r\n");
940
941
			// Still no love?
942
			if (!$this->check_response(250))
943
			{
944
				$this->error = 'bad_file';
945
				return false;
946
			}
947
		}
948
949
		return true;
950
	}
951
952
	/**
953
	 * Reads the response to the command from the server
954
	 *
955
	 * @param string $desired The desired response
956
	 * @return boolean Whether or not we got the desired response
957
	 */
958
	public function check_response($desired)
959
	{
960
		// Wait for a response that isn't continued with -, but don't wait too long.
961
		$time = time();
962
		do
963
			$this->last_message = fgets($this->connection, 1024);
0 ignored issues
show
Bug introduced by
$this->connection of type string is incompatible with the type resource expected by parameter $stream of fgets(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

963
			$this->last_message = fgets(/** @scrutinizer ignore-type */ $this->connection, 1024);
Loading history...
964
		while ((strlen($this->last_message) < 4 || strpos($this->last_message, ' ') === 0 || strpos($this->last_message, ' ', 3) !== 3) && time() - $time < 5);
965
966
		// Was the desired response returned?
967
		return is_array($desired) ? in_array(substr($this->last_message, 0, 3), $desired) : substr($this->last_message, 0, 3) == $desired;
0 ignored issues
show
introduced by
The condition is_array($desired) is always false.
Loading history...
968
	}
969
970
	/**
971
	 * Used to create a passive connection
972
	 *
973
	 * @return boolean Whether the passive connection was created successfully
974
	 */
975
	public function passive()
976
	{
977
		// We can't create a passive data connection without a primary one first being there.
978
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
979
			return false;
980
981
		// Request a passive connection - this means, we'll talk to you, you don't talk to us.
982
		@fwrite($this->connection, 'PASV' . "\r\n");
983
		$time = time();
984
		do
985
			$response = fgets($this->connection, 1024);
986
		while (strpos($response, ' ', 3) !== 3 && time() - $time < 5);
987
988
		// If it's not 227, we weren't given an IP and port, which means it failed.
989
		if (strpos($response, '227 ') !== 0)
990
		{
991
			$this->error = 'bad_response';
992
			return false;
993
		}
994
995
		// Snatch the IP and port information, or die horribly trying...
996
		if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $response, $match) == 0)
997
		{
998
			$this->error = 'bad_response';
999
			return false;
1000
		}
1001
1002
		// This is pretty simple - store it for later use ;).
1003
		$this->pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]);
1004
1005
		return true;
1006
	}
1007
1008
	/**
1009
	 * Creates a new file on the server
1010
	 *
1011
	 * @param string $ftp_file The file to create
1012
	 * @return boolean Whether or not the file was created successfully
1013
	 */
1014
	public function create_file($ftp_file)
1015
	{
1016
		// First, we have to be connected... very important.
1017
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
1018
			return false;
1019
1020
		// I'd like one passive mode, please!
1021
		if (!$this->passive())
1022
			return false;
1023
1024
		// Seems logical enough, so far...
1025
		fwrite($this->connection, 'STOR ' . $ftp_file . "\r\n");
1026
1027
		// Okay, now we connect to the data port.  If it doesn't work out, it's probably "file already exists", etc.
1028
		$fp = @fsockopen($this->pasv['ip'], $this->pasv['port'], $err, $err, 5);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $err seems to be never defined.
Loading history...
1029
		if (!$fp || !$this->check_response(150))
1030
		{
1031
			$this->error = 'bad_file';
1032
			@fclose($fp);
1033
			return false;
1034
		}
1035
1036
		// This may look strange, but we're just closing it to indicate a zero-byte upload.
1037
		fclose($fp);
1038
		if (!$this->check_response(226))
1039
		{
1040
			$this->error = 'bad_response';
1041
			return false;
1042
		}
1043
1044
		return true;
1045
	}
1046
1047
	/**
1048
	 * Generates a directory listing for the current directory
1049
	 *
1050
	 * @param string $ftp_path The path to the directory
1051
	 * @param bool $search Whether or not to get a recursive directory listing
1052
	 * @return string|boolean The results of the command or false if unsuccessful
1053
	 */
1054
	public function list_dir($ftp_path = '', $search = false)
1055
	{
1056
		// Are we even connected...?
1057
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
1058
			return false;
1059
1060
		// Passive... non-agressive...
1061
		if (!$this->passive())
1062
			return false;
1063
1064
		// Get the listing!
1065
		fwrite($this->connection, 'LIST -1' . ($search ? 'R' : '') . ($ftp_path == '' ? '' : ' ' . $ftp_path) . "\r\n");
1066
1067
		// Connect, assuming we've got a connection.
1068
		$fp = @fsockopen($this->pasv['ip'], $this->pasv['port'], $err, $err, 5);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $err seems to be never defined.
Loading history...
1069
		if (!$fp || !$this->check_response(array(150, 125)))
1070
		{
1071
			$this->error = 'bad_response';
1072
			@fclose($fp);
1073
			return false;
1074
		}
1075
1076
		// Read in the file listing.
1077
		$data = '';
1078
		while (!feof($fp))
1079
			$data .= fread($fp, 4096);
1080
		fclose($fp);
1081
1082
		// Everything go okay?
1083
		if (!$this->check_response(226))
1084
		{
1085
			$this->error = 'bad_response';
1086
			return false;
1087
		}
1088
1089
		return $data;
1090
	}
1091
1092
	/**
1093
	 * Determines the current directory we are in
1094
	 *
1095
	 * @param string $file The name of a file
1096
	 * @param string $listing A directory listing or null to generate one
1097
	 * @return string|boolean The name of the file or false if it wasn't found
1098
	 */
1099
	public function locate($file, $listing = null)
1100
	{
1101
		if ($listing === null)
1102
			$listing = $this->list_dir('', true);
1103
		$listing = explode("\n", $listing);
0 ignored issues
show
Bug introduced by
It seems like $listing can also be of type false; however, parameter $string of explode() does only seem to accept string, 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 ignore-type  annotation

1103
		$listing = explode("\n", /** @scrutinizer ignore-type */ $listing);
Loading history...
1104
1105
		@fwrite($this->connection, 'PWD' . "\r\n");
0 ignored issues
show
Bug introduced by
$this->connection of type string is incompatible with the type resource expected by parameter $stream of fwrite(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1105
		@fwrite(/** @scrutinizer ignore-type */ $this->connection, 'PWD' . "\r\n");
Loading history...
1106
		$time = time();
1107
		do
1108
			$response = fgets($this->connection, 1024);
0 ignored issues
show
Bug introduced by
$this->connection of type string is incompatible with the type resource expected by parameter $stream of fgets(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1108
			$response = fgets(/** @scrutinizer ignore-type */ $this->connection, 1024);
Loading history...
1109
		while ($response[3] != ' ' && time() - $time < 5);
1110
1111
		// Check for 257!
1112
		if (preg_match('~^257 "(.+?)" ~', $response, $match) != 0)
1113
			$current_dir = strtr($match[1], array('""' => '"'));
1114
		else
1115
			$current_dir = '';
1116
1117
		for ($i = 0, $n = count($listing); $i < $n; $i++)
1118
		{
1119
			if (trim($listing[$i]) == '' && isset($listing[$i + 1]))
1120
			{
1121
				$current_dir = substr(trim($listing[++$i]), 0, -1);
1122
				$i++;
1123
			}
1124
1125
			// Okay, this file's name is:
1126
			$listing[$i] = $current_dir . '/' . trim(strlen($listing[$i]) > 30 ? strrchr($listing[$i], ' ') : $listing[$i]);
1127
1128
			if ($file[0] == '*' && substr($listing[$i], -(strlen($file) - 1)) == substr($file, 1))
1129
				return $listing[$i];
1130
			if (substr($file, -1) == '*' && substr($listing[$i], 0, strlen($file) - 1) == substr($file, 0, -1))
1131
				return $listing[$i];
1132
			if (basename($listing[$i]) == $file || $listing[$i] == $file)
1133
				return $listing[$i];
1134
		}
1135
1136
		return false;
1137
	}
1138
1139
	/**
1140
	 * Creates a new directory on the server
1141
	 *
1142
	 * @param string $ftp_dir The name of the directory to create
1143
	 * @return boolean Whether or not the operation was successful
1144
	 */
1145
	public function create_dir($ftp_dir)
1146
	{
1147
		// We must be connected to the server to do something.
1148
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
1149
			return false;
1150
1151
		// Make this new beautiful directory!
1152
		fwrite($this->connection, 'MKD ' . $ftp_dir . "\r\n");
1153
		if (!$this->check_response(257))
1154
		{
1155
			$this->error = 'bad_file';
1156
			return false;
1157
		}
1158
1159
		return true;
1160
	}
1161
1162
	/**
1163
	 * Detects the current path
1164
	 *
1165
	 * @param string $filesystem_path The full path from the filesystem
1166
	 * @param string $lookup_file The name of a file in the specified path
1167
	 * @return array An array of detected info - username, path from FTP root and whether or not the current path was found
1168
	 */
1169
	public function detect_path($filesystem_path, $lookup_file = null)
1170
	{
1171
		$username = '';
1172
1173
		if (isset($_SERVER['DOCUMENT_ROOT']))
1174
		{
1175
			if (preg_match('~^/home[2]?/([^/]+?)/public_html~', $_SERVER['DOCUMENT_ROOT'], $match))
1176
			{
1177
				$username = $match[1];
1178
1179
				$path = strtr($_SERVER['DOCUMENT_ROOT'], array('/home/' . $match[1] . '/' => '', '/home2/' . $match[1] . '/' => ''));
1180
1181
				if (substr($path, -1) == '/')
1182
					$path = substr($path, 0, -1);
1183
1184
				if (strlen(dirname($_SERVER['PHP_SELF'])) > 1)
1185
					$path .= dirname($_SERVER['PHP_SELF']);
1186
			}
1187
			elseif (strpos($filesystem_path, '/var/www/') === 0)
1188
				$path = substr($filesystem_path, 8);
1189
			else
1190
				$path = strtr(strtr($filesystem_path, array('\\' => '/')), array($_SERVER['DOCUMENT_ROOT'] => ''));
1191
		}
1192
		else
1193
			$path = '';
1194
1195
		if (is_resource($this->connection) && $this->list_dir($path) == '')
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
1196
		{
1197
			$data = $this->list_dir('', true);
1198
1199
			if ($lookup_file === null)
1200
				$lookup_file = $_SERVER['PHP_SELF'];
1201
1202
			$found_path = dirname($this->locate('*' . basename(dirname($lookup_file)) . '/' . basename($lookup_file), $data));
1203
			if ($found_path == false)
1204
				$found_path = dirname($this->locate(basename($lookup_file)));
1205
			if ($found_path != false)
1206
				$path = $found_path;
1207
		}
1208
		elseif (is_resource($this->connection))
1209
			$found_path = true;
1210
1211
		return array($username, $path, isset($found_path));
1212
	}
1213
1214
	/**
1215
	 * Close the ftp connection
1216
	 *
1217
	 * @return boolean Always returns true
1218
	 */
1219
	public function close()
1220
	{
1221
		// Goodbye!
1222
		fwrite($this->connection, 'QUIT' . "\r\n");
0 ignored issues
show
Bug introduced by
$this->connection of type string is incompatible with the type resource expected by parameter $stream of fwrite(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1222
		fwrite(/** @scrutinizer ignore-type */ $this->connection, 'QUIT' . "\r\n");
Loading history...
1223
		fclose($this->connection);
0 ignored issues
show
Bug introduced by
$this->connection of type string is incompatible with the type resource expected by parameter $stream of fclose(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1223
		fclose(/** @scrutinizer ignore-type */ $this->connection);
Loading history...
1224
1225
		return true;
1226
	}
1227
}
1228
1229
?>