AttachmentsDirectory   F
last analyzed

Complexity

Total Complexity 143

Size/Duplication

Total Lines 959
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 295
c 1
b 0
f 0
dl 0
loc 959
rs 2
wmc 143

34 Methods

Rating   Name   Duplication   Size   Complexity  
A getBaseDirs() 0 5 2
A isCurrentDirectoryId() 0 3 1
A getCurrent() 0 8 2
C delete() 0 87 17
B manageBySpace() 0 47 7
A directoryExists() 0 9 3
A autoManageEnabled() 0 8 2
A getAttachmentsTree() 0 30 4
A currentDirectoryId() 0 8 2
A checkNewDir() 0 15 3
A getPathById() 0 8 2
A isCurrentBaseDir() 0 8 2
A autoManageIsLevel() 0 3 1
A createDirectory() 0 30 4
B updateLastDirs() 0 32 11
A initLastDir() 0 5 2
A getPaths() 0 3 1
A clear() 0 5 1
A hasMultiPaths() 0 3 2
A hasNumFilesLimit() 0 3 1
B rename() 0 28 8
A countSubdirs() 0 12 3
C checkDirSpace() 0 35 16
A isBaseDir() 0 3 1
A hasSizeLimit() 0 3 1
A remainingFiles() 0 8 2
A hasFileTmpAttachments() 0 24 6
B checkDirSize() 0 12 7
A dirSpace() 0 9 2
F automanageCheckDirectory() 0 88 19
A remainingSpace() 0 8 2
A hasBaseDir() 0 3 1
A countDirs() 0 3 1
A __construct() 0 38 4

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
/**
4
 * Handles the job of attachment directory management.
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\Attachments;
15
16
use ElkArte\Database\QueryInterface;
17
use ElkArte\Errors\Errors;
0 ignored issues
show
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use ElkArte\Exceptions\Exception;
19
use ElkArte\Helper\FileFunctions;
20
use ElkArte\Helper\Util;
21
22
/**
23
 * Class AttachmentsDirectory
24
 *
25
 * The AttachmentsDirectory class is responsible for managing attachment directories and monitoring their health.
26
 */
27
class AttachmentsDirectory
28
{
29
	/** @var int Used to enable the auto sequence feature */
30
	public const AUTO_SEQUENCE = 1;
31
32
	/** @var int Used to enable the auto year feature */
33
	public const AUTO_YEAR = 2;
34
35
	/** @var int Used to enable the auto year-month feature */
36
	public const AUTO_YEAR_MONTH = 3;
37
38
	/** @var int Used to enable the auto random feature */
39
	public const AUTO_RAND = 4;
40
41
	/** @var int Used to enable the auto random feature */
42
	public const AUTO_RAND2 = 5;
43
44
	/** @var int Current size of data in a directory */
45
	protected static int $dir_size = 0;
46
47
	/** @var int Limits on the above */
48
	protected int $sizeLimit = 0;
49
50
	/** @var int Current number of files in a directory */
51
	protected static int $dir_files = 0;
52
53
	/** @var int Limits on the above */
54
	protected int $numFilesLimit = 0;
55
56
	/** @var int if auto manage attachment function is enabled and at what level
57
	 * 0 = normal/off, 1 = by space (#files/size), 2 = by years, 3 = by months 4 = random */
58
	protected int $automanage_attachments = 0;
59
60
	/** @var array Potential attachment directories */
61
	protected array $attachmentUploadDir = [];
62
63
	/** @var int Pointer to the above upload directory array */
64
	protected int $currentAttachmentUploadDir = 0;
65
66
	/** @var int|mixed If we are using subdirectories */
67
	protected mixed $useSubdirectories = 0;
68
69
	/** @var bool If to notify the admin when a directory is full */
70
	protected bool $attachment_full_notified = false;
71
72
	/** @var array|string Potential root/base directories to which we can add directories/files */
73
	protected array|string $baseDirectories = [];
74
75
	/** @var string Current base to use from the above array */
76
	protected string $basedirectory_for_attachments = '';
77
78
	/** @var array|mixed|string */
79
	protected mixed $last_dirs = [];
80
81
	/**
82
	 * The constructor for attachment directories, controls where to add files
83
	 * and monitors directory health
84
	 *
85
	 * @param array $options all the stuff
86
	 * @param QueryInterface $db
87
	 */
88
	public function __construct(array $options, protected QueryInterface $db)
89
	{
90
		$this->automanage_attachments = (int) ($options['automanage_attachments'] ?? $this->automanage_attachments);
91
		$this->sizeLimit = $options['attachmentDirSizeLimit'] ?? $this->sizeLimit;
92
		$this->numFilesLimit = $options['attachmentDirFileLimit'] ?? $this->numFilesLimit;
93
94
		$this->currentAttachmentUploadDir = $options['currentAttachmentUploadDir'] ?? $this->currentAttachmentUploadDir;
95
		$this->useSubdirectories = $options['use_subdirectories_for_attachments'] ?? $this->useSubdirectories;
96
97
		$this->last_dirs = $options['last_attachments_directory'] ?? serialize($this->last_dirs);
98
		$this->last_dirs = Util::unserialize($this->last_dirs);
99
100
		$this->baseDirectories = $options['attachment_basedirectories'] ?? serialize($this->baseDirectories);
101
		$this->baseDirectories = Util::unserialize($this->baseDirectories);
0 ignored issues
show
Documentation Bug introduced by
It seems like ElkArte\Helper\Util::uns...$this->baseDirectories) can also be of type false. However, the property $baseDirectories is declared as type array|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Bug introduced by
It seems like $this->baseDirectories can also be of type array; however, parameter $string of ElkArte\Helper\Util::unserialize() 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

101
		$this->baseDirectories = Util::unserialize(/** @scrutinizer ignore-type */ $this->baseDirectories);
Loading history...
102
103
		$this->basedirectory_for_attachments = $options['basedirectory_for_attachments'] ?? $this->basedirectory_for_attachments;
104
		$this->attachment_full_notified = !empty($options['attachment_full_notified'] ?? $this->basedirectory_for_attachments);
105
106
		if (empty($options['attachmentUploadDir']))
107
		{
108
			$options['attachmentUploadDir'] = serialize([1 => BOARDDIR . '/attachments']);
109
		}
110
111
		// It should be added to the installation and upgrade scripts.
112
		// But since the converters need to be updated also. This is easier.
113
		if (empty($options['currentAttachmentUploadDir']))
114
		{
115
			$this->currentAttachmentUploadDir = 1;
116
			$options['attachmentUploadDir'] = serialize([1 => $options['attachmentUploadDir']]);
117
118
			updateSettings([
119
				'attachmentUploadDir' => $options['attachmentUploadDir'],
120
				'currentAttachmentUploadDir' => 1,
121
			]);
122
		}
123
124
		$current = Util::unserialize($options['attachmentUploadDir']);
125
		$this->attachmentUploadDir = $current ?: $options['attachmentUploadDir'];
126
	}
127
128
	/**
129
	 * Return how many files will still "fit" in the directory
130
	 *
131
	 * @param int $current_files
132
	 * @return false|mixed
133
	 */
134
	public function remainingFiles(int $current_files): mixed
135
	{
136
		if ($this->hasNumFilesLimit())
137
		{
138
			return max($this->numFilesLimit - $current_files, 0);
139
		}
140
141
		return false;
142
	}
143
144
	/**
145
	 * Returns if a file limit (count) has been placed on a directory
146
	 *
147
	 * @return bool
148
	 */
149
	public function hasNumFilesLimit(): bool
150
	{
151
		return !empty($this->numFilesLimit);
152
	}
153
154
	/**
155
	 * Returns how much physical space is remaining for a directory
156
	 *
157
	 * @param $current_dir_size
158
	 * @return false|mixed
159
	 */
160
	public function remainingSpace($current_dir_size): mixed
161
	{
162
		if ($this->hasSizeLimit())
163
		{
164
			return max($this->sizeLimit - $current_dir_size, 0);
165
		}
166
167
		return false;
168
	}
169
170
	/**
171
	 * Return if a directory physical space limit has been set
172
	 *
173
	 * @return bool
174
	 */
175
	public function hasSizeLimit(): bool
176
	{
177
		return !empty($this->sizeLimit);
178
	}
179
180
	/**
181
	 * Little utility function for the $id_folder computation for attachments.
182
	 *
183
	 * What it does:
184
	 *
185
	 * - This returns the id of the folder where the attachment or avatar will be saved.
186
	 * - If multiple attachment directories are not enabled, this will be 1 by default.
187
	 *
188
	 * @return int 1 if multiple attachment directories are not enabled,
189
	 * or the id of the current attachment directory otherwise.
190
	 */
191
	public function currentDirectoryId(): int
192
	{
193
		if (!array_key_exists($this->currentAttachmentUploadDir, $this->attachmentUploadDir))
194
		{
195
			$this->currentAttachmentUploadDir = max(array_keys($this->attachmentUploadDir));
196
		}
197
198
		return $this->currentAttachmentUploadDir;
199
	}
200
201
	/**
202
	 * Checks if a given id or named directory has been defined.  Does not
203
	 * check if it exists on disk.
204
	 *
205
	 * @param int|string $id
206
	 * @return bool
207
	 */
208
	public function directoryExists(int|string $id): bool
209
	{
210
		if (is_int($id))
211
		{
212
			return array_key_exists($id, $this->attachmentUploadDir);
213
		}
214
215
		return in_array($id, $this->attachmentUploadDir)
216
			|| in_array(BOARDDIR . DIRECTORY_SEPARATOR . $id, $this->attachmentUploadDir, true);
217
	}
218
219
	/**
220
	 * Loop through the attachment directory array to count any subdirectories
221
	 *
222
	 * @param string $dir
223
	 * @return int
224
	 */
225
	public function countSubdirs(string $dir): int
226
	{
227
		$expected_dirs = 0;
228
		foreach ($this->getPaths() as $sub)
229
		{
230
			if (strpos($sub, $dir . DIRECTORY_SEPARATOR) !== false)
231
			{
232
				$expected_dirs++;
233
			}
234
		}
235
236
		return $expected_dirs;
237
	}
238
239
	/**
240
	 * Returns the list of directories as an array.
241
	 *
242
	 * @return array the attachments directory/directories
243
	 */
244
	public function getPaths(): array
245
	{
246
		return $this->attachmentUploadDir;
247
	}
248
249
	/**
250
	 * Returns the directory name for a given key
251
	 *
252
	 * @param int $id key in our attachmentUploadDir array
253
	 * @return string
254
	 *
255
	 * @throws Exception
256
	 */
257
	public function getPathById(int $id): string
258
	{
259
		if (isset($this->attachmentUploadDir[$id]))
260
		{
261
			return $this->attachmentUploadDir[$id];
262
		}
263
264
		throw new Exception('dir_does_not_exists');
265
	}
266
267
	/**
268
	 * Return the base directory in use for attachments
269
	 *
270
	 * @return array
271
	 */
272
	public function getBaseDirs(): array
273
	{
274
		return is_array($this->baseDirectories)
275
			? $this->baseDirectories
276
			: [1 => $this->basedirectory_for_attachments];
277
	}
278
279
	/**
280
	 * Return if base directories have been defined
281
	 *
282
	 * @return bool
283
	 */
284
	public function hasBaseDir(): bool
285
	{
286
		return !empty($this->baseDirectories);
287
	}
288
289
	/**
290
	 * Return if a given directory is a defined base dir
291
	 *
292
	 * @param $dir
293
	 * @return bool
294
	 */
295
	public function isBaseDir($dir): bool
296
	{
297
		return in_array($dir, $this->baseDirectories, true);
0 ignored issues
show
Bug introduced by
It seems like $this->baseDirectories can also be of type string; however, parameter $haystack of in_array() 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

297
		return in_array($dir, /** @scrutinizer ignore-type */ $this->baseDirectories, true);
Loading history...
298
	}
299
300
	/**
301
	 * Updates the last directories information
302
	 *
303
	 * @param int $dir_id The key in the last_dirs array
304
	 * @return void
305
	 */
306
	public function updateLastDirs(int $dir_id): void
307
	{
308
		if (!empty($this->last_dirs) && (isset($this->last_dirs[$dir_id]) || isset($this->last_dirs[0])))
309
		{
310
			$num = substr(strrchr($this->attachmentUploadDir[$dir_id], '_'), 1);
311
			if (is_numeric($num))
312
			{
313
				// Need to find the base folder.
314
				$bid = -1;
315
				$use_subdirectories = 0;
316
				foreach ($this->attachmentUploadDir as $base)
317
				{
318
					if (strpos($this->attachmentUploadDir[$dir_id], $base . DIRECTORY_SEPARATOR) !== false)
319
					{
320
						$use_subdirectories = 1;
321
						break;
322
					}
323
				}
324
325
				if ($use_subdirectories == 0 && strpos($this->attachmentUploadDir[$dir_id], BOARDDIR . DIRECTORY_SEPARATOR) !== false)
326
				{
327
					$bid = 0;
328
				}
329
330
				$this->last_dirs[$bid] = (int) $num;
331
				$this->basedirectory_for_attachments = empty($this->basedirectory_for_attachments) ? '' : $this->basedirectory_for_attachments;
332
				$this->useSubdirectories = (int) $this->useSubdirectories;
333
334
				updateSettings([
335
					'last_attachments_directory' => serialize($this->last_dirs),
336
					'basedirectory_for_attachments' => $bid == 0 ? $this->basedirectory_for_attachments : $this->attachmentUploadDir[$bid],
337
					'use_subdirectories_for_attachments' => $use_subdirectories,
338
				]);
339
			}
340
		}
341
	}
342
343
	/**
344
	 * If size/count limits are enabled, validates a given file will still fit
345
	 * within the given constraints.  If not will attempt to create new directories
346
	 * based on ACP options
347
	 *
348
	 * @param $thumb_size
349
	 */
350
	public function checkDirSize($thumb_size): void
351
	{
352
		if ($this->autoManageIsLevel(self::AUTO_SEQUENCE) && (!empty($this->sizeLimit) || !empty($this->numFilesLimit)))
353
		{
354
			self::$dir_size += $thumb_size;
355
			self::$dir_files++;
356
357
			// If the folder is full, try to create a new one and move the thumb to it.
358
			if ((self::$dir_size > $this->sizeLimit * 1024 || self::$dir_files + 2 > $this->numFilesLimit) && $this->manageBySpace())
359
			{
360
				self::$dir_size = 0;
361
				self::$dir_files = 0;
362
			}
363
		}
364
	}
365
366
	/**
367
	 * Returns if the current management level is a level
368
	 *
369
	 * Default = 1, By Year = 2, By Year and Month = 3, Rand = 4;
370
	 *
371
	 * @param $level
372
	 * @return bool
373
	 */
374
	public function autoManageIsLevel($level): bool
375
	{
376
		return $this->automanage_attachments === (int) $level;
377
	}
378
379
	/**
380
	 * Determines the current base directory and attachment directory
381
	 *
382
	 * What it does:
383
	 *
384
	 * - Increments the above directory to the next available slot
385
	 * - Uses createDirectory to create the incremental directory
386
	 *
387
	 */
388
	public function manageBySpace(): ?bool
389
	{
390
		if ($this->autoManageEnabled(self::AUTO_SEQUENCE))
391
		{
392
			return true;
393
		}
394
395
		$baseDirectory = empty($this->useSubdirectories) ? BOARDDIR : $this->basedirectory_for_attachments;
396
397
		// Just to be sure: I don't want directory separators at the end
398
		$sep = stripos(PHP_OS_FAMILY, 'WIN') === 0 ? '\/' : DIRECTORY_SEPARATOR;
399
		$baseDirectory = rtrim($baseDirectory, $sep);
400
401
		// Get the current base directory
402
		if (!empty($this->useSubdirectories) && !empty($this->baseDirectories))
403
		{
404
			$base_dir = array_search($this->basedirectory_for_attachments, $this->baseDirectories, true);
0 ignored issues
show
Bug introduced by
It seems like $this->baseDirectories can also be of type string; however, parameter $haystack of array_search() 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

404
			$base_dir = array_search($this->basedirectory_for_attachments, /** @scrutinizer ignore-type */ $this->baseDirectories, true);
Loading history...
405
		}
406
		else
407
		{
408
			$base_dir = 0;
409
		}
410
411
		// Get the last attachment directory for that base directory
412
		$this->initLastDir($base_dir);
413
414
		// And increment it.
415
		$this->last_dirs[$base_dir]++;
416
417
		$uploadDirectory = $baseDirectory . DIRECTORY_SEPARATOR . 'attachments_' . $this->last_dirs[$base_dir];
418
419
		// make sure it exists and is writable
420
		try
421
		{
422
			$this->createDirectory($uploadDirectory);
423
424
			$this->currentAttachmentUploadDir = array_search($uploadDirectory, $this->attachmentUploadDir, true);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_search($uploadDire...achmentUploadDir, true) can also be of type string. However, the property $currentAttachmentUploadDir is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
425
			updateSettings([
426
				'last_attachments_directory' => serialize($this->last_dirs),
427
				'currentAttachmentUploadDir' => $this->currentAttachmentUploadDir,
428
			]);
429
430
			return true;
431
		}
432
		catch (Exception)
433
		{
434
			return false;
435
		}
436
	}
437
438
	/**
439
	 * If the auto manage function is enabled
440
	 *
441
	 * @param int|null $minLevel
442
	 * @return bool
443
	 */
444
	public function autoManageEnabled(int $minLevel = null): bool
445
	{
446
		if ($minLevel === null)
447
		{
448
			return !empty($this->automanage_attachments);
449
		}
450
451
		return $this->automanage_attachments > $minLevel;
452
	}
453
454
	/**
455
	 * Initializes the last directory for the given base directory
456
	 *
457
	 * @param string $base_dir the base directory
458
	 * @return void
459
	 */
460
	protected function initLastDir(string $base_dir): void
461
	{
462
		if (!isset($this->last_dirs[$base_dir]))
463
		{
464
			$this->last_dirs[$base_dir] = 0;
465
		}
466
	}
467
468
	/**
469
	 * Creates a directory as defined by the admin attach options
470
	 *
471
	 * What it does:
472
	 *
473
	 * - Attempts to make the directory writable
474
	 * - Places an .htaccess in new directories for security
475
	 *
476
	 * @param string $uploadDirectory
477
	 * @return bool
478
	 * @throws Exception
479
	 *
480
	 */
481
	public function createDirectory(string $uploadDirectory): bool
482
	{
483
		$fileFunctions = FileFunctions::instance();
484
485
		$uploadDirectory = str_replace('\\', DIRECTORY_SEPARATOR, $uploadDirectory);
486
		$uploadDirectory = rtrim($uploadDirectory, DIRECTORY_SEPARATOR);
487
488
		try
489
		{
490
			$result = $fileFunctions->createDirectory($uploadDirectory, true);
491
		}
492
		catch (Exception $exception)
493
		{
494
			Errors::instance()->log_error($exception->getMessage());
495
			throw $exception;
496
		}
497
498
		// Only update if it's a new directory
499
		if ($result && !in_array($uploadDirectory, $this->attachmentUploadDir, true))
500
		{
501
			$this->currentAttachmentUploadDir = max(array_keys($this->attachmentUploadDir)) + 1;
502
			$this->attachmentUploadDir[$this->currentAttachmentUploadDir] = $uploadDirectory;
503
504
			updateSettings([
505
				'attachmentUploadDir' => serialize($this->attachmentUploadDir),
506
				'currentAttachmentUploadDir' => $this->currentAttachmentUploadDir,
507
			], true);
508
		}
509
510
		return true;
511
	}
512
513
	/**
514
	 * Returns the number of attachment directories we have
515
	 *
516
	 * @return int
517
	 */
518
	public function countDirs(): int
519
	{
520
		return count($this->attachmentUploadDir);
521
	}
522
523
	/**
524
	 * Returns the attachment tree with modified paths
525
	 *
526
	 * @param array $file_tree the original attachment tree
527
	 * @return array the modified attachment tree
528
	 */
529
	public function getAttachmentsTree(array $file_tree): array
530
	{
531
		// Are we using multiple attachment directories?
532
		if ($this->hasMultiPaths())
533
		{
534
			unset($file_tree[strtr(BOARDDIR, ['\\' => '/'])]['contents']['attachments']);
535
536
			// @todo Should we suggest non-current directories be read only?
537
			foreach ($this->attachmentUploadDir as $dir)
538
			{
539
				$file_tree[strtr($dir, ['\\' => '/'])] = [
540
					'type' => 'dir',
541
					'writable_on' => 'restrictive',
542
				];
543
			}
544
		}
545
		else
546
		{
547
			if (substr($this->attachmentUploadDir[1], 0, strlen(BOARDDIR)) != BOARDDIR)
548
			{
549
				unset($file_tree[strtr(BOARDDIR, ['\\' => '/'])]['contents']['attachments']);
550
			}
551
552
			$file_tree[strtr($this->attachmentUploadDir[1], ['\\' => '/'])] = [
553
				'type' => 'dir',
554
				'writable_on' => 'restrictive',
555
			];
556
		}
557
558
		return $file_tree;
559
	}
560
561
	/**
562
	 * Checks if we have multiple paths for attachments
563
	 *
564
	 * @return bool
565
	 */
566
	public function hasMultiPaths(): bool
567
	{
568
		return $this->autoManageEnabled() && count($this->attachmentUploadDir) > 1;
569
	}
570
571
	/**
572
	 * Check and create a directory automatically.
573
	 *
574
	 * @param bool $is_admin_interface
575
	 * @return bool
576
	 */
577
	public function automanageCheckDirectory(bool $is_admin_interface = false): bool
578
	{
579
		if ($this->autoManageEnabled() === false)
580
		{
581
			return false;
582
		}
583
584
		if ($this->checkNewDir($is_admin_interface) === false)
585
		{
586
			return false;
587
		}
588
589
		// Get our date and random numbers for the directory choices
590
		$year = date('Y');
591
		$month = date('m');
592
593
		$rand = md5(mt_rand());
594
		$rand1 = $rand[1];
595
		$rand = $rand[0];
596
597
		if (!empty($this->baseDirectories) && !empty($this->useSubdirectories))
598
		{
599
			$base_dir = array_search($this->basedirectory_for_attachments, $this->baseDirectories, true);
0 ignored issues
show
Bug introduced by
It seems like $this->baseDirectories can also be of type string; however, parameter $haystack of array_search() 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

599
			$base_dir = array_search($this->basedirectory_for_attachments, /** @scrutinizer ignore-type */ $this->baseDirectories, true);
Loading history...
600
		}
601
		else
602
		{
603
			$base_dir = 0;
604
		}
605
606
		$baseDirectory = empty($this->useSubdirectories) ? BOARDDIR : $this->basedirectory_for_attachments;
607
608
		// Just to be sure: I don't want directory separators at the end
609
		$sep = stripos(PHP_OS_FAMILY, 'WIN') === 0 ? '\/' : DIRECTORY_SEPARATOR;
610
		$baseDirectory = rtrim($baseDirectory, $sep);
611
612
		switch ($this->automanage_attachments)
613
		{
614
			case self::AUTO_SEQUENCE:
615
				$this->initLastDir($base_dir);
616
				$uploadDirectory = $baseDirectory . DIRECTORY_SEPARATOR . 'attachments_' . $this->last_dirs[$base_dir];
617
				break;
618
			case self::AUTO_YEAR:
619
				$uploadDirectory = $baseDirectory . DIRECTORY_SEPARATOR . $year;
620
				break;
621
			case self::AUTO_YEAR_MONTH:
622
				$uploadDirectory = $baseDirectory . DIRECTORY_SEPARATOR . $year . DIRECTORY_SEPARATOR . $month;
623
				break;
624
			case self::AUTO_RAND:
625
				$uploadDirectory = $baseDirectory . DIRECTORY_SEPARATOR . (empty($this->useSubdirectories) ? 'attachments-' : 'random_') . $rand;
626
				break;
627
			case self::AUTO_RAND2:
628
				$uploadDirectory = $baseDirectory . DIRECTORY_SEPARATOR . (empty($this->useSubdirectories) ? 'attachments-' : 'random_') . $rand . DIRECTORY_SEPARATOR . $rand1;
629
				break;
630
			default:
631
				$uploadDirectory = '';
632
		}
633
634
		if (!empty($uploadDirectory) && !in_array($uploadDirectory, $this->attachmentUploadDir, true))
635
		{
636
			try
637
			{
638
				$this->createDirectory($uploadDirectory);
639
				$outputCreation = true;
640
			}
641
			catch (Exception)
642
			{
643
				$outputCreation = false;
644
			}
645
		}
646
		elseif (in_array($uploadDirectory, $this->attachmentUploadDir, true))
647
		{
648
			$outputCreation = true;
649
		}
650
		else
651
		{
652
			$outputCreation = false;
653
		}
654
655
		if ($outputCreation)
656
		{
657
			$this->currentAttachmentUploadDir = array_search($uploadDirectory, $this->attachmentUploadDir, true);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_search($uploadDire...achmentUploadDir, true) can also be of type string. However, the property $currentAttachmentUploadDir is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
658
659
			updateSettings([
660
				'currentAttachmentUploadDir' => $this->currentAttachmentUploadDir,
661
			]);
662
		}
663
664
		return $outputCreation;
665
	}
666
667
	/**
668
	 * Should we try to create a new directory or not?
669
	 *
670
	 * False if Auto manage is off or No files were uploaded
671
	 * True if from the ACP or if files were uploaded
672
	 *
673
	 * @param bool $is_admin_interface
674
	 * @return bool
675
	 */
676
	protected function checkNewDir(bool $is_admin_interface): bool
677
	{
678
		// Not pretty, but since we don't want folders created for every post.
679
		// It'll do unless a better solution can be found.
680
		if ($is_admin_interface === true)
681
		{
682
			return true;
683
		}
684
685
		if ($this->autoManageEnabled() === false)
686
		{
687
			return false;
688
		}
689
690
		return $this->hasFileTmpAttachments();
691
	}
692
693
	/**
694
	 * Checks if there are any temporary file attachments
695
	 *
696
	 * @param bool $strict Whether to perform strict check on uploaded files
697
	 * @return bool Returns true if there are temporary file attachments, false otherwise
698
	 */
699
	public function hasFileTmpAttachments(bool $strict = true): bool
700
	{
701
		if (isset($_FILES['attachment']['tmp_name']))
702
		{
703
			foreach ($_FILES['attachment']['tmp_name'] as $tmp_name)
704
			{
705
				if (empty($tmp_name))
706
				{
707
					continue;
708
				}
709
710
				if (!$strict)
711
				{
712
					return true;
713
				}
714
715
				if (is_uploaded_file($tmp_name))
716
				{
717
					return true;
718
				}
719
			}
720
		}
721
722
		return false;
723
	}
724
725
	/**
726
	 * Checks if the current active directory has space allowed for a new attachment file
727
	 *
728
	 * @param TemporaryAttachment $sess_attach
729
	 * @throws Exception
730
	 */
731
	public function checkDirSpace(TemporaryAttachment $sess_attach): void
732
	{
733
		if (empty(self::$dir_size) || empty(self::$dir_files))
734
		{
735
			$this->dirSpace($sess_attach->getSize());
736
		}
737
738
		// Are we about to run out of room? Let's notify the admin then.
739
		if (($this->attachment_full_notified === false && !empty($this->sizeLimit) && $this->sizeLimit > 4000 && self::$dir_size > ($this->sizeLimit - 2000) * 1024)
740
			|| (!empty($this->numFilesLimit) && $this->numFilesLimit * .95 < self::$dir_files && $this->numFilesLimit > 500))
741
		{
742
			require_once(SUBSDIR . '/Admin.subs.php');
743
			emailAdmins('admin_attachments_full');
744
			updateSettings(['attachment_full_notified' => 1]);
745
		}
746
747
		// No room left.... What to do now???
748
		if ((!empty($this->numFilesLimit) && self::$dir_files + 2 > $this->numFilesLimit)
749
			|| (!empty($this->sizeLimit) && self::$dir_size > $this->sizeLimit * 1024))
750
		{
751
			// If we are managing the directories space automatically, lets get to it
752
			if ($this->autoManageIsLevel(self::AUTO_SEQUENCE))
753
			{
754
				// Move it to the new folder if we can. (Throws Exception if it fails)
755
				if ($this->manageBySpace())
756
				{
757
					$sess_attach->moveTo($this->getCurrent());
758
					$sess_attach->setIdFolder($this->currentAttachmentUploadDir);
759
					self::$dir_size = 0;
760
					self::$dir_files = 0;
761
				}
762
			}
763
			else
764
			{
765
				throw new Exception('ran_out_of_space');
766
			}
767
		}
768
	}
769
770
	/**
771
	 * Current space consumed by the files in a directory plus what a new file will add
772
	 *
773
	 * @param int $tmp_attach_size
774
	 * @return void
775
	 */
776
	protected function dirSpace(int $tmp_attach_size = 0): void
777
	{
778
		require_once(SUBSDIR . '/ManageAttachments.subs.php');
779
		$current_dir = attachDirProperties($this->currentAttachmentUploadDir);
780
781
		// Add 1 to file count only when a new file will be added
782
		self::$dir_files = $current_dir['files'] + (empty($tmp_attach_size) ? 0 : 1);
783
784
		self::$dir_size = $current_dir['size'] + $tmp_attach_size;
785
	}
786
787
	/**
788
	 * The current attachment path:
789
	 *
790
	 * What it does:
791
	 *
792
	 * @return string
793
	 * @todo not really true at the moment
794
	 *  - BOARDDIR . '/attachments', if nothing is set yet.
795
	 *  - if the forum is using multiple attachments directories,
796
	 *    then the current path is stored as unserialize($modSettings['attachmentUploadDir'])[$modSettings['currentAttachmentUploadDir']]
797
	 *  - otherwise, the current path is $modSettings['attachmentUploadDir'].
798
	 *
799
	 */
800
	public function getCurrent(): string
801
	{
802
		if (empty($this->attachmentUploadDir))
803
		{
804
			return BOARDDIR . '/attachments';
805
		}
806
807
		return $this->attachmentUploadDir[$this->currentAttachmentUploadDir];
808
	}
809
810
	/**
811
	 * Renames the directory for a given key and updates base directory path if necessary
812
	 *
813
	 * @param int $id Key in the attachmentUploadDir array
814
	 * @param string &$real_path Reference to the current directory path to be renamed
815
	 *
816
	 * @throws Exception When the directory cannot be renamed or already exists
817
	 */
818
	public function rename(int $id, string &$real_path): void
819
	{
820
		$fileFunctions = FileFunctions::instance();
821
		if (!empty($this->attachmentUploadDir[$id]) && $real_path !== $this->attachmentUploadDir[$id])
822
		{
823
			if (!$fileFunctions->isDir($real_path))
824
			{
825
				if (!@rename($this->attachmentUploadDir[$id], $real_path))
826
				{
827
					$real_path = $this->attachmentUploadDir[$id];
828
					throw new Exception('attach_dir_no_rename');
829
				}
830
			}
831
			else
832
			{
833
				$real_path = $this->attachmentUploadDir[$id];
834
				throw new Exception('attach_dir_exists_msg');
835
			}
836
837
			// Update the base directory path
838
			if (!empty($this->baseDirectories) && array_key_exists($id, $this->baseDirectories))
0 ignored issues
show
Bug introduced by
It seems like $this->baseDirectories can also be of type string; however, parameter $array of array_key_exists() does only seem to accept ArrayObject|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

838
			if (!empty($this->baseDirectories) && array_key_exists($id, /** @scrutinizer ignore-type */ $this->baseDirectories))
Loading history...
839
			{
840
				$base = $this->basedirectory_for_attachments === $this->attachmentUploadDir[$id] ? $real_path : $this->basedirectory_for_attachments;
841
842
				$this->baseDirectories[$id] = $real_path;
843
				updateSettings([
844
					'attachment_basedirectories' => serialize($this->baseDirectories),
845
					'basedirectory_for_attachments' => $base,
846
				]);
847
			}
848
		}
849
	}
850
851
	/**
852
	 * Remove a directory if its empty (not counting .htaccess or index.php)
853
	 *
854
	 * @param $id
855
	 * @param $real_path
856
	 * @return bool|null
857
	 * @throws Exception
858
	 */
859
	public function delete($id, &$real_path): ?bool
860
	{
861
		$real_path = $this->attachmentUploadDir[$id];
862
863
		// It's not a good idea to delete the current directory.
864
		if ($this->isCurrentDirectoryId($id))
865
		{
866
			throw new Exception('attach_dir_is_current');
867
		}
868
869
		// Or the current base directory
870
		if ($this->isCurrentBaseDir($id))
871
		{
872
			throw new Exception('attach_dir_is_current_bd');
873
		}
874
875
		// Or the board directory
876
		if ($real_path === realpath(BOARDDIR))
877
		{
878
			throw new Exception('attach_dir_no_delete');
879
		}
880
881
		// Let's not try to delete a path with files in it.
882
		$num_attach = countAttachmentsInFolders($id);
883
884
		// A check to see if it's a used base dir.
885
		if ($num_attach === 0 && !empty($this->baseDirectories))
886
		{
887
			// Count any sub-folders.
888
			foreach ($this->attachmentUploadDir as $sub)
889
			{
890
				if (strpos($sub, $real_path . DIRECTORY_SEPARATOR) !== false)
891
				{
892
					$num_attach++;
893
				}
894
			}
895
		}
896
897
		// It's safe to delete. So try to delete the folder also
898
		if ($num_attach === 0)
899
		{
900
			$fileFunctions = FileFunctions::instance();
901
			$doit = false;
902
903
			if ($fileFunctions->isDir($real_path))
904
			{
905
				$doit = true;
906
			}
907
			elseif ($fileFunctions->isDir(BOARDDIR . DIRECTORY_SEPARATOR . $real_path))
908
			{
909
				$doit = true;
910
				$real_path = BOARDDIR . DIRECTORY_SEPARATOR . $real_path;
911
			}
912
913
			// They have a path in the system that does not exist
914
			if ($doit === false && !$fileFunctions->fileExists($real_path))
915
			{
916
				$this->clear($id);
917
				return true;
918
			}
919
920
			if ($doit)
921
			{
922
				$fileFunctions->delete($real_path . '/.htaccess');
923
				$fileFunctions->delete($real_path . '/index.php');
924
				$result = $fileFunctions->rmDir($real_path);
925
926
				if (!$result)
927
				{
928
					throw new Exception('attach_dir_no_delete');
929
				}
930
			}
931
932
			// Remove it from the base directory list.
933
			if (!empty($result) && !empty($this->baseDirectories))
934
			{
935
				$this->clear($id);
936
				return true;
937
			}
938
		}
939
		else
940
		{
941
			throw new Exception('attach_dir_no_remove');
942
		}
943
944
		// Default: nothing changed
945
		return null;
946
	}
947
948
	/**
949
	 * Remove a directory from modSettings 'attachment_basedirectories'
950
	 *
951
	 * @param $id
952
	 */
953
	public function clear($id): void
954
	{
955
		unset($this->baseDirectories[$id]);
956
		updateSettings([
957
			'attachment_basedirectories' => serialize($this->baseDirectories)
958
		]);
959
	}
960
961
	/**
962
	 * Checks if the given ID is the current directory ID
963
	 *
964
	 * @param int $id The ID to check against the current directory ID
965
	 * @return bool Returns true if the given ID is the same as the current directory ID, otherwise returns false
966
	 */
967
	public function isCurrentDirectoryId(int $id): bool
968
	{
969
		return $this->currentAttachmentUploadDir == $id;
970
	}
971
972
	/**
973
	 * Returns if a given directory is the current base directory used for attachments
974
	 *
975
	 * @param int|string $id
976
	 * @return bool
977
	 */
978
	public function isCurrentBaseDir(int|string $id): bool
979
	{
980
		if (is_int($id))
981
		{
982
			return $this->basedirectory_for_attachments === $this->attachmentUploadDir[$id];
983
		}
984
985
		return $this->basedirectory_for_attachments === $id;
986
	}
987
}
988