Issues (1061)

Sources/Class-Package.php (37 issues)

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 2020 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 RC2
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
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
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
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
		// Split up the path.
145
		$path = explode('/', $path);
146
147
		// Start with a base array.
148
		$array = $this->array;
149
150
		// For each element in the path.
151
		foreach ($path as $el)
152
		{
153
			// Deal with sets....
154
			if (strpos($el, '[') !== false)
155
			{
156
				$lvl = (int) substr($el, strpos($el, '[') + 1);
157
				$el = substr($el, 0, strpos($el, '['));
158
			}
159
			// Find an attribute.
160
			elseif (substr($el, 0, 1) == '@')
161
			{
162
				// It simplifies things if the attribute is already there ;).
163
				if (isset($array[$el]))
164
					return $array[$el];
165
				else
166
				{
167
					$trace = debug_backtrace();
168
					$i = 0;
169
					while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this))
170
						$i++;
171
					$debug = ' (from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'] . ')';
172
173
					// Cause an error.
174
					if ($this->debug_level & E_NOTICE)
175
						trigger_error('Undefined XML attribute: ' . substr($el, 1) . $debug, E_USER_NOTICE);
176
					return false;
177
				}
178
			}
179
			else
180
				$lvl = null;
181
182
			// Find this element.
183
			$array = $this->_path($array, $el, $lvl);
184
		}
185
186
		// Clean up after $lvl, for $return_full.
187
		if ($return_full && (!isset($array['name']) || substr($array['name'], -1) != ']'))
188
			$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 151. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
189
190
		// Create the right type of class...
191
		$newClass = get_class($this);
192
193
		// Return a new xmlArray for the result.
194
		return $array === false ? false : new $newClass($array, $this->trim, $this->debug_level, true);
0 ignored issues
show
The condition $array === false is always false.
Loading history...
195
	}
196
197
	/**
198
	 * Check if an element exists.
199
	 * Example use,
200
	 *  echo $xml->exists('html/body') ? 'y' : 'n';
201
	 *
202
	 * @param string $path The path to the element to get.
203
	 * @return boolean Whether the specified path exists
204
	 */
205
	public function exists($path)
206
	{
207
		// Split up the path.
208
		$path = explode('/', $path);
209
210
		// Start with a base array.
211
		$array = $this->array;
212
213
		// For each element in the path.
214
		foreach ($path as $el)
215
		{
216
			// Deal with sets....
217
			if (strpos($el, '[') !== false)
218
			{
219
				$lvl = (int) substr($el, strpos($el, '[') + 1);
220
				$el = substr($el, 0, strpos($el, '['));
221
			}
222
			// Find an attribute.
223
			elseif (substr($el, 0, 1) == '@')
224
				return isset($array[$el]);
225
			else
226
				$lvl = null;
227
228
			// Find this element.
229
			$array = $this->_path($array, $el, $lvl, true);
0 ignored issues
show
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

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

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

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

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

1086
		$listing = explode("\n", /** @scrutinizer ignore-type */ $listing);
Loading history...
1087
1088
		@fwrite($this->connection, 'PWD' . "\r\n");
0 ignored issues
show
$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

1088
		@fwrite(/** @scrutinizer ignore-type */ $this->connection, 'PWD' . "\r\n");
Loading history...
1089
		$time = time();
1090
		do
1091
			$response = fgets($this->connection, 1024);
0 ignored issues
show
$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

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

1205
		fwrite(/** @scrutinizer ignore-type */ $this->connection, 'QUIT' . "\r\n");
Loading history...
1206
		fclose($this->connection);
0 ignored issues
show
$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

1206
		fclose(/** @scrutinizer ignore-type */ $this->connection);
Loading history...
1207
1208
		return true;
1209
	}
1210
}
1211
1212
?>