Passed
Push — release-2.1 ( 0c2197...207d2d )
by Jeremy
05:47
created

ftp_connection::check_response()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
c 0
b 0
f 0
nop 1
dl 0
loc 10
rs 10
nc 2
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 http://www.simplemachines.org
10
 * @copyright 2018 Simple Machines and individual contributors
11
 * @license http://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 Beta 4
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
	 * @var bool Holds trim level textual data
38
	 */
39
	public $trim;
40
41
	/**
42
	 * Constructor for the xml parser.
43
	 * Example use:
44
	 *  $xml = new xmlArray(file('data.xml'));
45
	 *
46
	 * @param string $data The xml data or an array of, unless is_clone is true.
47
	 * @param bool $auto_trim Used to automatically trim textual data.
48
	 * @param int $level The debug level. Specifies whether notices should be generated for missing elements and attributes.
49
	 * @param bool $is_clone default false. If is_clone is true, the  xmlArray is cloned from another - used internally only.
50
	 */
51
	public function __construct($data, $auto_trim = false, $level = null, $is_clone = false)
52
	{
53
		// If we're using this try to get some more memory.
54
		setMemoryLimit('32M');
55
56
		// Set the debug level.
57
		$this->debug_level = $level !== null ? $level : error_reporting();
58
		$this->trim = $auto_trim;
59
60
		// Is the data already parsed?
61
		if ($is_clone)
62
		{
63
			$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...
64
			return;
65
		}
66
67
		// Is the input an array? (ie. passed from file()?)
68
		if (is_array($data))
0 ignored issues
show
introduced by
The condition is_array($data) is always false.
Loading history...
69
			$data = implode('', $data);
70
71
		// Remove any xml declaration or doctype, and parse out comments and CDATA.
72
		$data = preg_replace('/<!--.*?-->/s', '', $this->_to_cdata(preg_replace(array('/^<\?xml.+?\?' . '>/is', '/<!DOCTYPE[^>]+?' . '>/s'), '', $data)));
73
74
		// Now parse the xml!
75
		$this->array = $this->_parse($data);
76
	}
77
78
	/**
79
	 * Get the root element's name.
80
	 * Example use:
81
	 *  echo $element->name();
82
	 * @return string The root element's name
83
	 */
84
	public function name()
85
	{
86
		return isset($this->array['name']) ? $this->array['name'] : '';
87
	}
88
89
	/**
90
	 * Get a specified element's value or attribute by path.
91
	 * Children are parsed for text, but only textual data is returned
92
	 * unless get_elements is true.
93
	 * Example use:
94
	 *  $data = $xml->fetch('html/head/title');
95
	 * @param string $path The path to the element to fetch
96
	 * @param bool $get_elements Whether to include elements
97
	 * @return string The value or attribute of the specified element
98
	 */
99
	public function fetch($path, $get_elements = false)
100
	{
101
		// Get the element, in array form.
102
		$array = $this->path($path);
103
104
		if ($array === false)
0 ignored issues
show
introduced by
The condition $array === false is always false.
Loading history...
105
			return false;
106
107
		// Getting elements into this is a bit complicated...
108
		if ($get_elements && !is_string($array))
109
		{
110
			$temp = '';
111
112
			// Use the _xml() function to get the xml data.
113
			foreach ($array->array as $val)
114
			{
115
				// Skip the name and any attributes.
116
				if (is_array($val))
117
					$temp .= $this->_xml($val, null);
118
			}
119
120
			// Just get the XML data and then take out the CDATAs.
121
			return $this->_to_cdata($temp);
122
		}
123
124
		// Return the value - taking care to pick out all the text values.
125
		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...
126
	}
127
128
	/** Get an element, returns a new xmlArray.
129
	 * It finds any elements that match the path specified.
130
	 * It will always return a set if there is more than one of the element
131
	 * or return_set is true.
132
	 * Example use:
133
	 *  $element = $xml->path('html/body');
134
	 * @param $path string The path to the element to get
135
	 * @param $return_full bool Whether to return the full result set
136
	 * @return xmlArray, a new xmlArray.
137
	 */
138
	public function path($path, $return_full = false)
139
	{
140
		// Split up the path.
141
		$path = explode('/', $path);
142
143
		// Start with a base array.
144
		$array = $this->array;
145
146
		// For each element in the path.
147
		foreach ($path as $el)
148
		{
149
			// Deal with sets....
150
			if (strpos($el, '[') !== false)
151
			{
152
				$lvl = (int) substr($el, strpos($el, '[') + 1);
153
				$el = substr($el, 0, strpos($el, '['));
154
			}
155
			// Find an attribute.
156
			elseif (substr($el, 0, 1) == '@')
157
			{
158
				// It simplifies things if the attribute is already there ;).
159
				if (isset($array[$el]))
160
					return $array[$el];
161
				else
162
				{
163
					$trace = debug_backtrace();
164
					$i = 0;
165
					while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this))
166
						$i++;
167
					$debug = ' (from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'] . ')';
168
169
					// Cause an error.
170
					if ($this->debug_level & E_NOTICE)
171
						trigger_error('Undefined XML attribute: ' . substr($el, 1) . $debug, E_USER_NOTICE);
172
					return false;
173
				}
174
			}
175
			else
176
				$lvl = null;
177
178
			// Find this element.
179
			$array = $this->_path($array, $el, $lvl);
180
		}
181
182
		// Clean up after $lvl, for $return_full.
183
		if ($return_full && (!isset($array['name']) || substr($array['name'], -1) != ']'))
184
			$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 147. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
185
186
		// Create the right type of class...
187
		$newClass = get_class($this);
188
189
		// Return a new xmlArray for the result.
190
		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...
191
	}
192
193
	/**
194
	 * Check if an element exists.
195
	 * Example use,
196
	 *  echo $xml->exists('html/body') ? 'y' : 'n';
197
	 *
198
	 * @param string $path The path to the element to get.
199
	 * @return boolean Whether the specified path exists
200
	 */
201
	public function exists($path)
202
	{
203
		// Split up the path.
204
		$path = explode('/', $path);
205
206
		// Start with a base array.
207
		$array = $this->array;
208
209
		// For each element in the path.
210
		foreach ($path as $el)
211
		{
212
			// Deal with sets....
213
			if (strpos($el, '[') !== false)
214
			{
215
				$lvl = (int) substr($el, strpos($el, '[') + 1);
216
				$el = substr($el, 0, strpos($el, '['));
217
			}
218
			// Find an attribute.
219
			elseif (substr($el, 0, 1) == '@')
220
				return isset($array[$el]);
221
			else
222
				$lvl = null;
223
224
			// Find this element.
225
			$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

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

623
			return chr(/** @scrutinizer ignore-type */ "$m[1]");
Loading history...
624
		}, $data), $trans_tbl);
625
626
		return $this->trim ? trim($data) : $data;
627
	}
628
629
	/**
630
	 * Given an array, return the text from that array. (recursive and privately used.)
631
	 *
632
	 * @param array $array An aray of data
633
	 * @return string The text from the array
634
	 */
635
	protected function _fetch($array)
636
	{
637
		// Don't return anything if this is just a string.
638
		if (is_string($array))
0 ignored issues
show
introduced by
The condition is_string($array) is always false.
Loading history...
639
			return '';
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
				continue;
647
648
			// This is text!
649
			if ($text['name'] == '!')
650
				$temp .= $text['value'];
651
			// Another element - dive in ;).
652
			else
653
				$temp .= $this->_fetch($text);
654
		}
655
656
		// Return all the bits and pieces we've put together.
657
		return $temp;
658
	}
659
660
	/**
661
	 * Get a specific array by path, one level down. (privately used...)
662
	 *
663
	 * @param array $array An array of data
664
	 * @param string $path The path
665
	 * @param int $level How far deep into the array we should go
666
	 * @param bool $no_error Whether or not to ignore errors
667
	 * @return string|array The specified array (or the contents of said array if there's only one result)
668
	 */
669
	protected function _path($array, $path, $level, $no_error = false)
670
	{
671
		// Is $array even an array?  It might be false!
672
		if (!is_array($array))
0 ignored issues
show
introduced by
The condition is_array($array) is always true.
Loading history...
673
			return false;
674
675
		// Asking for *no* path?
676
		if ($path == '' || $path == '.')
677
			return $array;
678
		$paths = explode('|', $path);
679
680
		// A * means all elements of any name.
681
		$show_all = in_array('*', $paths);
682
683
		$results = array();
684
685
		// Check each element.
686
		foreach ($array as $value)
687
		{
688
			if (!is_array($value) || $value['name'] === '!')
689
				continue;
690
691
			if ($show_all || in_array($value['name'], $paths))
692
			{
693
				// Skip elements before "the one".
694
				if ($level !== null && $level > 0)
695
					$level--;
696
				else
697
					$results[] = $value;
698
			}
699
		}
700
701
		// No results found...
702
		if (empty($results))
703
		{
704
			$trace = debug_backtrace();
705
			$i = 0;
706
			while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this))
707
				$i++;
708
			$debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'];
709
710
			// Cause an error.
711
			if ($this->debug_level & E_NOTICE && !$no_error)
712
				trigger_error('Undefined XML element: ' . $path . $debug, E_USER_NOTICE);
713
			return false;
714
		}
715
		// Only one result.
716
		elseif (count($results) == 1 || $level !== null)
0 ignored issues
show
introduced by
The condition $level !== null is always true.
Loading history...
717
			return $results[0];
718
		// Return the result set.
719
		else
720
			return $results + array('name' => $path . '[]');
721
	}
722
}
723
724
/**
725
 * Class ftp_connection
726
 * Simple FTP protocol implementation.
727
 *
728
 * @see https://tools.ietf.org/html/rfc959
729
 */
730
class ftp_connection
731
{
732
	/**
733
	 * @var string Holds the connection response
734
	 */
735
	public $connection;
736
737
	/**
738
	 * @var string Holds any errors
739
	 */
740
	public $error;
741
742
	/**
743
	 * @var string Holds the last message from the server
744
	 */
745
	public $last_message;
746
747
	/**
748
	 * @var boolean Whether or not this is a passive connection
749
	 */
750
	public $pasv;
751
752
	/**
753
	 * Create a new FTP connection...
754
	 *
755
	 * @param string $ftp_server The server to connect to
756
	 * @param int $ftp_port The port to connect to
757
	 * @param string $ftp_user The username
758
	 * @param string $ftp_pass The password
759
	 */
760
	public function __construct($ftp_server, $ftp_port = 21, $ftp_user = 'anonymous', $ftp_pass = '[email protected]')
761
	{
762
		// Initialize variables.
763
		$this->connection = 'no_connection';
764
		$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...
765
		$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...
766
767
		if ($ftp_server !== null)
0 ignored issues
show
introduced by
The condition $ftp_server !== null is always true.
Loading history...
768
			$this->connect($ftp_server, $ftp_port, $ftp_user, $ftp_pass);
769
	}
770
771
	/**
772
	 * Connects to a server
773
	 *
774
	 * @param string $ftp_server The address of the server
775
	 * @param int $ftp_port The port
776
	 * @param string $ftp_user The username
777
	 * @param string $ftp_pass The password
778
	 */
779
	public function connect($ftp_server, $ftp_port = 21, $ftp_user = 'anonymous', $ftp_pass = '[email protected]')
780
	{
781
		if (strpos($ftp_server, 'ftp://') === 0)
782
			$ftp_server = substr($ftp_server, 6);
783
		elseif (strpos($ftp_server, 'ftps://') === 0)
784
			$ftp_server = 'ssl://' . substr($ftp_server, 7);
785
		if (strpos($ftp_server, 'http://') === 0)
786
			$ftp_server = substr($ftp_server, 7);
787
		elseif (strpos($ftp_server, 'https://') === 0)
788
			$ftp_server = substr($ftp_server, 8);
789
		$ftp_server = strtr($ftp_server, array('/' => '', ':' => '', '@' => ''));
790
791
		// Connect to the FTP server.
792
		$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 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...
793
		if (!$this->connection)
794
		{
795
			$this->error = 'bad_server';
796
            		$this->last_message = 'Invalid Server';
797
			return;
798
		}
799
800
		// Get the welcome message...
801
		if (!$this->check_response(220))
802
		{
803
			$this->error = 'bad_response';
804
		        $this->last_message = 'Bad Response';
805
			return;
806
		}
807
808
		// Send the username, it should ask for a password.
809
		fwrite($this->connection, 'USER ' . $ftp_user . "\r\n");
810
811
		if (!$this->check_response(331))
812
		{
813
			$this->error = 'bad_username';
814
			$this->last_message = 'Invalid Username';
815
			return;
816
		}
817
818
		// Now send the password... and hope it goes okay.
819
820
		fwrite($this->connection, 'PASS ' . $ftp_pass . "\r\n");
821
		if (!$this->check_response(230))
822
		{
823
			$this->error = 'bad_password';
824
			$this->last_message = 'Invalid Password';
825
			return;
826
		}
827
	}
828
829
	/**
830
	 * Changes to a directory (chdir) via the ftp connection
831
	 *
832
	 * @param string $ftp_path The path to the directory we want to change to
833
	 * @return boolean Whether or not the operation was successful
834
	 */
835
	public function chdir($ftp_path)
836
	{
837
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
838
			return false;
839
840
		// No slash on the end, please...
841
		if ($ftp_path !== '/' && substr($ftp_path, -1) === '/')
842
			$ftp_path = substr($ftp_path, 0, -1);
843
844
		fwrite($this->connection, 'CWD ' . $ftp_path . "\r\n");
845
		if (!$this->check_response(250))
846
		{
847
			$this->error = 'bad_path';
848
			return false;
849
		}
850
851
		return true;
852
	}
853
854
	/**
855
	 * Changes a files atrributes (chmod)
856
	 *
857
	 * @param string $ftp_file The file to CHMOD
858
	 * @param int|string $chmod The value for the CHMOD operation
859
	 * @return boolean Whether or not the operation was successful
860
	 */
861
	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

861
	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...
862
	{
863
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
864
			return false;
865
866
		if ($ftp_file == '')
867
			$ftp_file = '.';
868
869
		// Do we have a file or a dir?
870
		$is_dir = is_dir($ftp_file);
871
		$is_writable = false;
872
873
		// Set different modes.
874
		$chmod_values = $is_dir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
875
876
		foreach ($chmod_values as $val)
877
		{
878
			// If it's writable, break out of the loop.
879
			if (is_writable($ftp_file))
880
			{
881
				$is_writable = true;
882
				break;
883
			}
884
885
			else
886
			{
887
				// Convert the chmod value from octal (0777) to text ("777").
888
				fwrite($this->connection, 'SITE CHMOD ' . decoct($val) . ' ' . $ftp_file . "\r\n");
889
				if (!$this->check_response(200))
890
				{
891
					$this->error = 'bad_file';
892
					break;
893
				}
894
			}
895
		}
896
		return $is_writable;
897
	}
898
899
	/**
900
	 * Deletes a file
901
	 *
902
	 * @param string $ftp_file The file to delete
903
	 * @return boolean Whether or not the operation was successful
904
	 */
905
	public function unlink($ftp_file)
906
	{
907
		// We are actually connected, right?
908
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
909
			return false;
910
911
		// Delete file X.
912
		fwrite($this->connection, 'DELE ' . $ftp_file . "\r\n");
913
		if (!$this->check_response(250))
914
		{
915
			fwrite($this->connection, 'RMD ' . $ftp_file . "\r\n");
916
917
			// Still no love?
918
			if (!$this->check_response(250))
919
			{
920
				$this->error = 'bad_file';
921
				return false;
922
			}
923
		}
924
925
		return true;
926
	}
927
928
	/**
929
	 * Reads the response to the command from the server
930
	 *
931
	 * @param string $desired The desired response
932
	 * @return boolean Whether or not we got the desired response
933
	 */
934
	public function check_response($desired)
935
	{
936
		// Wait for a response that isn't continued with -, but don't wait too long.
937
		$time = time();
938
		do
939
			$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 $handle 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

939
			$this->last_message = fgets(/** @scrutinizer ignore-type */ $this->connection, 1024);
Loading history...
940
		while ((strlen($this->last_message) < 4 || strpos($this->last_message, ' ') === 0 || strpos($this->last_message, ' ', 3) !== 3) && time() - $time < 5);
941
942
		// Was the desired response returned?
943
		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...
944
	}
945
946
	/**
947
	 * Used to create a passive connection
948
	 *
949
	 * @return boolean Whether the passive connection was created successfully
950
	 */
951
	public function passive()
952
	{
953
		// We can't create a passive data connection without a primary one first being there.
954
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
955
			return false;
956
957
		// Request a passive connection - this means, we'll talk to you, you don't talk to us.
958
		@fwrite($this->connection, 'PASV' . "\r\n");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fwrite(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

958
		/** @scrutinizer ignore-unhandled */ @fwrite($this->connection, 'PASV' . "\r\n");

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
959
		$time = time();
960
		do
961
			$response = fgets($this->connection, 1024);
962
		while (strpos($response, ' ', 3) !== 3 && time() - $time < 5);
963
964
		// If it's not 227, we weren't given an IP and port, which means it failed.
965
		if (strpos($response, '227 ') !== 0)
966
		{
967
			$this->error = 'bad_response';
968
			return false;
969
		}
970
971
		// Snatch the IP and port information, or die horribly trying...
972
		if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $response, $match) == 0)
973
		{
974
			$this->error = 'bad_response';
975
			return false;
976
		}
977
978
		// This is pretty simple - store it for later use ;).
979
		$this->pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]);
980
981
		return true;
982
	}
983
984
	/**
985
	 * Creates a new file on the server
986
	 *
987
	 * @param string $ftp_file The file to create
988
	 * @return boolean Whether or not the file was created successfully
989
	 */
990
	public function create_file($ftp_file)
991
	{
992
		// First, we have to be connected... very important.
993
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
994
			return false;
995
996
		// I'd like one passive mode, please!
997
		if (!$this->passive())
998
			return false;
999
1000
		// Seems logical enough, so far...
1001
		fwrite($this->connection, 'STOR ' . $ftp_file . "\r\n");
1002
1003
		// Okay, now we connect to the data port.  If it doesn't work out, it's probably "file already exists", etc.
1004
		$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...
1005
		if (!$fp || !$this->check_response(150))
1006
		{
1007
			$this->error = 'bad_file';
1008
			@fclose($fp);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1008
			/** @scrutinizer ignore-unhandled */ @fclose($fp);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1009
			return false;
1010
		}
1011
1012
		// This may look strange, but we're just closing it to indicate a zero-byte upload.
1013
		fclose($fp);
1014
		if (!$this->check_response(226))
1015
		{
1016
			$this->error = 'bad_response';
1017
			return false;
1018
		}
1019
1020
		return true;
1021
	}
1022
1023
	/**
1024
	 * Generates a directory listing for the current directory
1025
	 *
1026
	 * @param string $ftp_path The path to the directory
1027
	 * @param bool $search Whether or not to get a recursive directory listing
1028
	 * @return string|boolean The results of the command or false if unsuccessful
1029
	 */
1030
	public function list_dir($ftp_path = '', $search = false)
1031
	{
1032
		// Are we even connected...?
1033
		if (!is_resource($this->connection))
0 ignored issues
show
introduced by
The condition is_resource($this->connection) is always false.
Loading history...
1034
			return false;
1035
1036
		// Passive... non-agressive...
1037
		if (!$this->passive())
1038
			return false;
1039
1040
		// Get the listing!
1041
		fwrite($this->connection, 'LIST -1' . ($search ? 'R' : '') . ($ftp_path == '' ? '' : ' ' . $ftp_path) . "\r\n");
1042
1043
		// Connect, assuming we've got a connection.
1044
		$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...
1045
		if (!$fp || !$this->check_response(array(150, 125)))
1046
		{
1047
			$this->error = 'bad_response';
1048
			@fclose($fp);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1048
			/** @scrutinizer ignore-unhandled */ @fclose($fp);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1049
			return false;
1050
		}
1051
1052
		// Read in the file listing.
1053
		$data = '';
1054
		while (!feof($fp))
1055
			$data .= fread($fp, 4096);
1056
		fclose($fp);
1057
1058
		// Everything go okay?
1059
		if (!$this->check_response(226))
1060
		{
1061
			$this->error = 'bad_response';
1062
			return false;
1063
		}
1064
1065
		return $data;
1066
	}
1067
1068
	/**
1069
	 * Determines the current directory we are in
1070
	 *
1071
	 * @param string $file The name of a file
1072
	 * @param string $listing A directory listing or null to generate one
1073
	 * @return string|boolean The name of the file or false if it wasn't found
1074
	 */
1075
	public function locate($file, $listing = null)
1076
	{
1077
		if ($listing === null)
1078
			$listing = $this->list_dir('', true);
1079
		$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

1079
		$listing = explode("\n", /** @scrutinizer ignore-type */ $listing);
Loading history...
1080
1081
		@fwrite($this->connection, 'PWD' . "\r\n");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fwrite(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1081
		/** @scrutinizer ignore-unhandled */ @fwrite($this->connection, 'PWD' . "\r\n");

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
Bug introduced by
$this->connection of type string is incompatible with the type resource expected by parameter $handle 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

1081
		@fwrite(/** @scrutinizer ignore-type */ $this->connection, 'PWD' . "\r\n");
Loading history...
1082
		$time = time();
1083
		do
1084
			$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 $handle 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

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

1198
		fwrite(/** @scrutinizer ignore-type */ $this->connection, 'QUIT' . "\r\n");
Loading history...
1199
		fclose($this->connection);
0 ignored issues
show
Bug introduced by
$this->connection of type string is incompatible with the type resource expected by parameter $handle 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

1199
		fclose(/** @scrutinizer ignore-type */ $this->connection);
Loading history...
1200
1201
		return true;
1202
	}
1203
}
1204
1205
?>