1<?php
2/**
3 * Joomla! Content Management System
4 *
5 * @copyright  Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
6 * @license    GNU General Public License version 2 or later; see LICENSE.txt
7 */
8
9namespace Joomla\CMS\Filesystem;
10
11defined('JPATH_PLATFORM') or die;
12
13/**
14 * File system helper
15 *
16 * Holds support functions for the filesystem, particularly the stream
17 *
18 * @since  1.7.0
19 */
20class FilesystemHelper
21{
22	/**
23	 * Remote file size function for streams that don't support it
24	 *
25	 * @param   string  $url  TODO Add text
26	 *
27	 * @return  mixed
28	 *
29	 * @link    https://www.php.net/manual/en/function.filesize.php
30	 * @since   1.7.0
31	 */
32	public static function remotefsize($url)
33	{
34		$sch = parse_url($url, PHP_URL_SCHEME);
35
36		if (($sch != 'http') && ($sch != 'https') && ($sch != 'ftp') && ($sch != 'ftps'))
37		{
38			return false;
39		}
40
41		if (($sch == 'http') || ($sch == 'https'))
42		{
43			$headers = get_headers($url, 1);
44
45			if ((!array_key_exists('Content-Length', $headers)))
46			{
47				return false;
48			}
49
50			return $headers['Content-Length'];
51		}
52
53		if (($sch == 'ftp') || ($sch == 'ftps'))
54		{
55			$server = parse_url($url, PHP_URL_HOST);
56			$port = parse_url($url, PHP_URL_PORT);
57			$path = parse_url($url, PHP_URL_PATH);
58			$user = parse_url($url, PHP_URL_USER);
59			$pass = parse_url($url, PHP_URL_PASS);
60
61			if ((!$server) || (!$path))
62			{
63				return false;
64			}
65
66			if (!$port)
67			{
68				$port = 21;
69			}
70
71			if (!$user)
72			{
73				$user = 'anonymous';
74			}
75
76			if (!$pass)
77			{
78				$pass = '';
79			}
80
81			switch ($sch)
82			{
83				case 'ftp':
84					$ftpid = ftp_connect($server, $port);
85					break;
86
87				case 'ftps':
88					$ftpid = ftp_ssl_connect($server, $port);
89					break;
90			}
91
92			if (!$ftpid)
93			{
94				return false;
95			}
96
97			$login = ftp_login($ftpid, $user, $pass);
98
99			if (!$login)
100			{
101				return false;
102			}
103
104			$ftpsize = ftp_size($ftpid, $path);
105			ftp_close($ftpid);
106
107			if ($ftpsize == -1)
108			{
109				return false;
110			}
111
112			return $ftpsize;
113		}
114	}
115
116	/**
117	 * Quick FTP chmod
118	 *
119	 * @param   string   $url   Link identifier
120	 * @param   integer  $mode  The new permissions, given as an octal value.
121	 *
122	 * @return  mixed
123	 *
124	 * @link    https://www.php.net/manual/en/function.ftp-chmod.php
125	 * @since   1.7.0
126	 */
127	public static function ftpChmod($url, $mode)
128	{
129		$sch = parse_url($url, PHP_URL_SCHEME);
130
131		if (($sch != 'ftp') && ($sch != 'ftps'))
132		{
133			return false;
134		}
135
136		$server = parse_url($url, PHP_URL_HOST);
137		$port = parse_url($url, PHP_URL_PORT);
138		$path = parse_url($url, PHP_URL_PATH);
139		$user = parse_url($url, PHP_URL_USER);
140		$pass = parse_url($url, PHP_URL_PASS);
141
142		if ((!$server) || (!$path))
143		{
144			return false;
145		}
146
147		if (!$port)
148		{
149			$port = 21;
150		}
151
152		if (!$user)
153		{
154			$user = 'anonymous';
155		}
156
157		if (!$pass)
158		{
159			$pass = '';
160		}
161
162		switch ($sch)
163		{
164			case 'ftp':
165				$ftpid = ftp_connect($server, $port);
166				break;
167
168			case 'ftps':
169				$ftpid = ftp_ssl_connect($server, $port);
170				break;
171		}
172
173		if (!$ftpid)
174		{
175			return false;
176		}
177
178		$login = ftp_login($ftpid, $user, $pass);
179
180		if (!$login)
181		{
182			return false;
183		}
184
185		$res = ftp_chmod($ftpid, $mode, $path);
186		ftp_close($ftpid);
187
188		return $res;
189	}
190
191	/**
192	 * Modes that require a write operation
193	 *
194	 * @return  array
195	 *
196	 * @since   1.7.0
197	 */
198	public static function getWriteModes()
199	{
200		return array('w', 'w+', 'a', 'a+', 'r+', 'x', 'x+');
201	}
202
203	/**
204	 * Stream and Filter Support Operations
205	 *
206	 * Returns the supported streams, in addition to direct file access
207	 * Also includes Joomla! streams as well as PHP streams
208	 *
209	 * @return  array  Streams
210	 *
211	 * @since   1.7.0
212	 */
213	public static function getSupported()
214	{
215		// Really quite cool what php can do with arrays when you let it...
216		static $streams;
217
218		if (!$streams)
219		{
220			$streams = array_merge(stream_get_wrappers(), self::getJStreams());
221		}
222
223		return $streams;
224	}
225
226	/**
227	 * Returns a list of transports
228	 *
229	 * @return  array
230	 *
231	 * @since   1.7.0
232	 */
233	public static function getTransports()
234	{
235		// Is this overkill?
236		return stream_get_transports();
237	}
238
239	/**
240	 * Returns a list of filters
241	 *
242	 * @return  array
243	 *
244	 * @since   1.7.0
245	 */
246	public static function getFilters()
247	{
248		// Note: This will look like the getSupported() function with J! filters.
249		// TODO: add user space filter loading like user space stream loading
250		return stream_get_filters();
251	}
252
253	/**
254	 * Returns a list of J! streams
255	 *
256	 * @return  array
257	 *
258	 * @since   1.7.0
259	 */
260	public static function getJStreams()
261	{
262		static $streams = array();
263
264		if (!$streams)
265		{
266			$files = new \DirectoryIterator(__DIR__ . '/Streams');
267
268			/* @type  $file  DirectoryIterator */
269			foreach ($files as $file)
270			{
271				// Only load for php files.
272				if (!$file->isFile() || $file->getExtension() !== 'php')
273				{
274					continue;
275				}
276
277				$streams[] = str_replace('stream', '', strtolower($file->getBasename('.php')));
278			}
279		}
280
281		return $streams;
282	}
283
284	/**
285	 * Determine if a stream is a Joomla stream.
286	 *
287	 * @param   string  $streamname  The name of a stream
288	 *
289	 * @return  boolean  True for a Joomla Stream
290	 *
291	 * @since   1.7.0
292	 */
293	public static function isJoomlaStream($streamname)
294	{
295		return in_array($streamname, self::getJStreams());
296	}
297
298	/**
299	 * Calculates the maximum upload file size and returns string with unit or the size in bytes
300	 *
301	 * Call it with JFilesystemHelper::fileUploadMaxSize();
302	 *
303	 * @param   bool  $unitOutput  This parameter determines whether the return value should be a string with a unit
304	 *
305	 * @return  float|string The maximum upload size of files with the appropriate unit or in bytes
306	 *
307	 * @since   3.4
308	 */
309	public static function fileUploadMaxSize($unitOutput = true)
310	{
311		static $max_size = false;
312		static $output_type = true;
313
314		if ($max_size === false || $output_type != $unitOutput)
315		{
316			$max_size   = self::parseSize(ini_get('post_max_size'));
317			$upload_max = self::parseSize(ini_get('upload_max_filesize'));
318
319			if ($upload_max > 0 && ($upload_max < $max_size || $max_size == 0))
320			{
321				$max_size = $upload_max;
322			}
323
324			if ($unitOutput == true)
325			{
326				$max_size = self::parseSizeUnit($max_size);
327			}
328
329			$output_type = $unitOutput;
330		}
331
332		return $max_size;
333	}
334
335	/**
336	 * Returns the size in bytes without the unit for the comparison
337	 *
338	 * @param   string  $size  The size which is received from the PHP settings
339	 *
340	 * @return  float The size in bytes without the unit
341	 *
342	 * @since   3.4
343	 */
344	private static function parseSize($size)
345	{
346		$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
347		$size = preg_replace('/[^0-9\.]/', '', $size);
348
349		$return = round($size);
350
351		if ($unit)
352		{
353			$return = round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
354		}
355
356		return $return;
357	}
358
359	/**
360	 * Creates the rounded size of the size with the appropriate unit
361	 *
362	 * @param   float  $maxSize  The maximum size which is allowed for the uploads
363	 *
364	 * @return  string String with the size and the appropriate unit
365	 *
366	 * @since   3.4
367	 */
368	private static function parseSizeUnit($maxSize)
369	{
370		$base     = log($maxSize) / log(1024);
371		$suffixes = array('', 'k', 'M', 'G', 'T');
372
373		return round(pow(1024, $base - floor($base)), 0) . $suffixes[floor($base)];
374	}
375}
376