FileFunctions::_initDir()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 2
dl 0
loc 23
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This class provides many common file and directory functions such as creating directories, checking existence etc.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * @version 2.0 dev
11
 *
12
 */
13
14
namespace ElkArte\Helper;
15
16
use Exception;
17
18
class FileFunctions
19
{
20
	/** @var FileFunctions The instance of the class */
21
	private static $_instance;
22
23
	/**
24
	 * chmod control will attempt to make a file or directory writable
25
	 *
26
	 * - Progressively attempts various chmod values until item is writable or failure
27
	 *
28
	 * @param string $item file or directory
29
	 * @return bool
30
	 */
31
	public function chmod($item)
32
	{
33
		$fileChmod = [0644, 0666];
34
		$dirChmod = [0755, 0775, 0777];
35
36
		// Already writable?
37
		if ($this->isWritable($item))
38
		{
39
			return true;
40
		}
41
42
		$modes = $this->isDir($item) ? $dirChmod : $fileChmod;
43
		foreach ($modes as $mode)
44
		{
45
			$this->elk_chmod($item, $mode);
46
47
			if ($this->isWritable($item))
48
			{
49
				clearstatcache(false, $item);
50
51
				return true;
52
			}
53
		}
54
55
		return false;
56
	}
57
58
	/**
59
	 * Simple wrapper around chmod
60
	 *
61
	 * - Checks proper value for mode if one is supplied
62
	 * - Consolidates chmod error suppression to single function
63
	 *
64
	 * @param string $item
65
	 * @param string|int $mode
66
	 *
67
	 * @return bool
68
	 */
69
	public function elk_chmod($item, $mode = '')
70
	{
71
		$result = false;
72
		$mode = trim($mode);
73
74
		if (empty($mode) || !is_numeric($mode))
75
		{
76
			$mode = $this->isDir($item) ? 0755 : 0644;
77
		}
78
79
		// Make sure we have a form of 0777 or '777' or '0777' so its safe for intval '8'
80
		if (($mode % 10) >= 8)
81
		{
82
			$mode = decoct($mode);
0 ignored issues
show
Bug introduced by
It seems like $mode can also be of type string; however, parameter $num of decoct() does only seem to accept integer, 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

82
			$mode = decoct(/** @scrutinizer ignore-type */ $mode);
Loading history...
83
		}
84
85
		// All numbers and outside octal range, safely convert to octal
86
		if (ctype_digit((string) $mode) && preg_match('~[8-9]~', $mode))
87
		{
88
			$mode = decoct($mode);
89
		}
90
91
		// Happens when passed the octal value 0777 (not string) which is 511 decimal, we work on the
92
		// assumption no one is trying to do a chmod 511
93
		if (in_array($mode, [511, 420, 436], true))
94
		{
95
			$mode = decoct($mode);
96
		}
97
98
		if ($mode == decoct(octdec($mode)))
0 ignored issues
show
Bug introduced by
It seems like octdec($mode) can also be of type double; however, parameter $num of decoct() does only seem to accept integer, 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

98
		if ($mode == decoct(/** @scrutinizer ignore-type */ octdec($mode)))
Loading history...
99
		{
100
			return @chmod($item, intval($mode, 8));
101
		}
102
103
		return $result;
104
	}
105
106
	/**
107
	 * is_dir() helper using spl functions.  is_dir can throw an exception if open_basedir
108
	 * restrictions are in effect.
109
	 *
110
	 * @param string $dir
111
	 * @return bool
112
	 */
113
	public function isDir($dir)
114
	{
115
		try
116
		{
117
			$splDir = new \SplFileInfo($dir);
118
			if ($splDir->isDir() && $splDir->getType() === 'dir' && !$splDir->isLink())
119
			{
120
				return true;
121
			}
122
		}
123
		catch (\RuntimeException)
124
		{
125
			return false;
126
		}
127
128
		return false;
129
	}
130
131
	/**
132
	 * file_exists() helper.  file_exists can throw an E_WARNING on failure.
133
	 * Returns true if the filename (not a directory or link) exists.
134
	 *
135
	 * @param string $item a file or directory location
136
	 * @return bool
137
	 */
138
	public function fileExists($item)
139
	{
140
		try
141
		{
142
			$fileInfo = new \SplFileInfo($item);
143
			if ($fileInfo->isFile() && !$fileInfo->isLink())
144
			{
145
				return true;
146
			}
147
		}
148
		catch (\RuntimeException)
149
		{
150
			return false;
151
		}
152
153
		return false;
154
	}
155
156
	/**
157
	 * fileperms() helper using spl functions.  fileperms can throw an e-warning
158
	 *
159
	 * @param string $item
160
	 * @return int|bool
161
	 */
162
	public function filePerms($item)
163
	{
164
		try
165
		{
166
			$fileInfo = new \SplFileInfo($item);
167
			if ($perms = $fileInfo->getPerms())
168
			{
169
				return $perms;
170
			}
171
		}
172
		catch (\RuntimeException)
173
		{
174
			return false;
175
		}
176
177
		return false;
178
	}
179
180
	/**
181
	 * filesize() helper.  filesize can throw an E_WARNING on failure.
182
	 * Returns the filesize in bytes on success or false on failure.
183
	 *
184
	 * @param string $item a file location
185
	 * @return int|bool
186
	 */
187
	public function fileSize($item)
188
	{
189
		try
190
		{
191
			$fileInfo = new \SplFileInfo($item);
192
			$size = $fileInfo->getSize();
193
		}
194
		catch (\RuntimeException)
195
		{
196
			$size = false;
197
		}
198
199
		return $size;
200
	}
201
202
	/**
203
	 * is_writable() helper.  is_writable can throw an E_WARNING on failure.
204
	 * Returns true if the filename/directory exists and is writable.
205
	 *
206
	 * @param string $item a file or directory location
207
	 * @return bool
208
	 */
209
	public function isWritable($item)
210
	{
211
		try
212
		{
213
			$fileInfo = new \SplFileInfo($item);
214
			if ($fileInfo->isWritable())
215
			{
216
				return true;
217
			}
218
		}
219
		catch (\RuntimeException)
220
		{
221
			return false;
222
		}
223
224
		return false;
225
	}
226
227
	/**
228
	 * Creates a directory as defined by a supplied path
229
	 *
230
	 * What it does:
231
	 *
232
	 * - Attempts to make the directory writable
233
	 * - Will create a full tree structure
234
	 * - Optionally places an .htaccess in created directories for security
235
	 *
236
	 * @param string $path the path to fully create
237
	 * @param bool $makeSecure if to create .htaccess file in created directory
238
	 * @return bool
239
	 * @throws \Exception
240
	 */
241
	public function createDirectory($path, $makeSecure = true)
242
	{
243
		// Path already exists?
244
		if (file_exists($path))
245
		{
246
			if ($this->isDir($path))
247
			{
248
				return true;
249
			}
250
251
			// A file exists at this location with this name
252
			throw new Exception('attach_dir_duplicate_file');
253
		}
254
255
		// Normalize windows and linux path's
256
		$path = str_replace('\\', DIRECTORY_SEPARATOR, $path);
257
		$path = rtrim($path, DIRECTORY_SEPARATOR);
258
259
		$tree = explode(DIRECTORY_SEPARATOR, $path);
260
		$count = empty($tree) ? 0 : count($tree);
261
		$partialTree = '';
262
263
		// Make sure we have a valid path format
264
		$directory = empty($tree) ? false : $this->_initDir($tree, $count);
265
		if ($directory === false)
266
		{
267
			// Maybe it's just the folder name
268
			$tree = explode(DIRECTORY_SEPARATOR, BOARDDIR . DIRECTORY_SEPARATOR . $path);
269
			$count = empty($tree) ? 0 : count($tree);
270
271
			$directory = empty($tree) ? false : $this->_initDir($tree, $count);
272
			if ($directory === false)
273
			{
274
				throw new Exception('attachments_no_create');
275
			}
276
		}
277
278
		// Walk down the path until we find a part that exists
279
		for ($i = $count - 1; $i >= 0; $i--)
280
		{
281
			$partialTree = $directory . DIRECTORY_SEPARATOR . implode('/', array_slice($tree, 0, $i + 1));
282
			// If this exists, lets ensure it is a directory
283
			if (file_exists($partialTree))
284
			{
285
				if (!is_dir($partialTree))
286
				{
287
					throw new Exception('attach_dir_duplicate_file');
288
				}
289
290
				break;
291
			}
292
		}
293
294
		// Can't find this path anywhere
295
		if ($i < 0)
296
		{
297
			throw new Exception('attachments_no_create');
298
		}
299
300
		// Walk forward and create the missing parts
301
		for ($i++; $i < $count; $i++)
302
		{
303
			$partialTree .= '/' . $tree[$i];
304
			if (!mkdir($partialTree) && !$this->isDir($partialTree))
305
			{
306
				return false;
307
			}
308
309
			// Make it writable
310
			if (!$this->chmod($partialTree))
311
			{
312
				throw new Exception('attachments_no_write');
313
			}
314
315
			if ($makeSecure)
316
			{
317
				secureDirectory($partialTree, true);
318
			}
319
		}
320
321
		clearstatcache(false, $partialTree);
322
323
		return true;
324
	}
325
326
	/**
327
	 * Deletes a file (not a directory) at a given location
328
	 *
329
	 * @param $path
330
	 * @return bool
331
	 */
332
	public function delete($path)
333
	{
334
		if (!$this->fileExists($path) || !$this->isWritable($path))
335
		{
336
			return false;
337
		}
338
339
		error_clear_last();
340
		return @unlink($path);
341
	}
342
343
	/**
344
	 * Recursively removes a directory and all files and subdirectories contained within.
345
	 * Use with *caution*, it is thorough, destructive and irreversible.
346
	 *
347
	 * @param string $path
348
	 * @param bool $delete_dir if to remove the directory structure as well
349
	 * @return bool
350
	 */
351
	public function rmDir($path, $delete_dir = true)
352
	{
353
		// @todo build a list of excluded directories
354
		if (!$this->isDir($path))
355
		{
356
			return true;
357
		}
358
359
		$success = true;
360
		$iterator = new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS);
361
		$files = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST, \RecursiveIteratorIterator::CATCH_GET_CHILD);
362
363
		/** @var \FilesystemIterator $file */
364
		foreach ($files as $file)
365
		{
366
			// If its not writable try to make it so or removal will fail
367
			if ($file->isWritable() || $this->chmod($file->getRealPath()))
368
			{
369
				if ($delete_dir && $file->isDir())
370
				{
371
					$success = $success && rmdir($file->getRealPath());
372
				}
373
				else
374
				{
375
					$success = $success && @unlink($file->getRealPath());
376
				}
377
			}
378
			else
379
			{
380
				$success = false;
381
			}
382
		}
383
384
		return $success && rmdir($path);
385
	}
386
387
	/**
388
	 * Helper function for createDirectory
389
	 *
390
	 * What it does:
391
	 *
392
	 * - Gets the directory w/o drive letter for windows
393
	 *
394
	 * @param string[] $tree
395
	 * @param int $count
396
	 * @return false|string|null
397
	 */
398
	private function _initDir(&$tree, &$count)
399
	{
400
		$directory = '';
401
402
		// If on Windows servers the first part of the path is the drive (e.g. "C:")
403
		if (strpos(PHP_OS_FAMILY, 'Win') === 0)
404
		{
405
			// Better be sure that the first part of the path is actually a drive letter...
406
			// ...even if, I should check this in the admin page...isn't it?
407
			// ...NHAAA Let's leave space for users' complains! :P
408
			if (preg_match('/^[a-z]:$/i', $tree[0]))
409
			{
410
				$directory = array_shift($tree);
411
			}
412
			else
413
			{
414
				return false;
415
			}
416
417
			$count--;
418
		}
419
420
		return $directory;
421
	}
422
423
	/**
424
	 * Create a full tree listing of files for a given directory path
425
	 *
426
	 * @param string $path
427
	 * @return array
428
	 */
429
	public function listTree($path)
430
	{
431
		$tree = [];
432
		if (!$this->isDir($path))
433
		{
434
			return $tree;
435
		}
436
437
		$iterator = new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS);
438
		$files = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST, \RecursiveIteratorIterator::CATCH_GET_CHILD);
439
		/** @var \SplFileInfo $file */
440
		foreach ($files as $file)
441
		{
442
			if ($file->isDir())
443
			{
444
				continue;
445
			}
446
447
			$sub_path = str_replace($path, '', $file->getPath());
448
449
			$tree[] = [
450
				'filename' => $sub_path === '' ? $file->getFilename() : $sub_path . '/' . $file->getFilename(),
451
				'size' => $file->getSize(),
452
				'skipped' => false,
453
			];
454
		}
455
456
		return $tree;
457
	}
458
459
	/**
460
	 * Being a singleton, use this static method to retrieve the instance of the class
461
	 *
462
	 * @return FileFunctions An instance of the class.
463
	 */
464
	public static function instance()
465
	{
466
		if (self::$_instance === null)
467
		{
468
			self::$_instance = new FileFunctions();
469
		}
470
471
		return self::$_instance;
472
	}
473
}
474