TemporaryAttachmentsList::removeAll()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 7
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 13
rs 9.6111
1
<?php
2
3
/**
4
 * Represents a list of temporary attachments for managing attachments in a session,
5
 * including operations to add, remove, and validate them.
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * @version 2.0 Beta 1
12
 */
13
14
namespace ElkArte\Attachments;
15
16
use Elkarte\Exceptions\Exception;
17
use ElkArte\Helper\FileFunctions;
18
use ElkArte\Helper\ValuesContainer;
19
20
/**
21
 * Overall List bag for interfacing/finding individual TemporaryAttachment bags
22
 */
23
class TemporaryAttachmentsList extends ValuesContainer
24
{
25
	public const ID = 'temp_attachments';
26
27
	/** @var string name we store temporary attachments under */
28
	public const TMPNAME_TPL = 'post_tmp_{user}_{hash}';
29
30
	/** @var string System level error, such as permissions issue to a folder */
31
	protected string $sysError = '';
32
33
	/**
34
	 * Constructor
35
	 */
36
	public function __construct()
37
	{
38
		if (!isset($_SESSION[static::ID]))
39
		{
40
			$_SESSION[static::ID] = [];
41
		}
42
43
		$this->data = &$_SESSION[static::ID];
44
45
		parent::__construct($this->data);
46
	}
47
48
	/**
49
	 * Removes all the temporary attachments of the user
50
	 *
51
	 * @param int|null $userId
52
	 */
53
	public function removeAll(int $userId = null): void
54
	{
55
		$prefix = $userId === null ? $this->getTplName('', '')[0] : $this->getTplName($userId, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $userId of ElkArte\Attachments\Temp...mentsList::getTplName(). ( Ignorable by Annotation )

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

55
		$prefix = $userId === null ? $this->getTplName(/** @scrutinizer ignore-type */ '', '')[0] : $this->getTplName($userId, '');
Loading history...
56
57
		foreach ($this->data as $attachID => $attachment)
58
		{
59
			if (str_contains($attachID, (string) $prefix))
60
			{
61
				$path = $attachment['tmp_name'] ?? '';
62
				if ($path !== '')
63
				{
64
					$this->remove($path);
65
					$this->remove($path . '_thumb');
66
				}
67
			}
68
		}
69
	}
70
71
	/**
72
	 * Deletes a temporary attachment from the filesystem
73
	 *
74
	 * @param string $file
75
	 * @return bool
76
	 */
77
	public function remove(string $file): bool
78
	{
79
		// Must exist and have edit permissions
80
		if ($file === '')
81
		{
82
			return false;
83
		}
84
85
		return FileFunctions::instance()->delete($file);
86
	}
87
88
	/**
89
	 * Sets the error message of a problem that prevents any attachment to be uploaded or saved
90
	 *
91
	 * @param string $msg
92
	 */
93
	public function setSystemError(string $msg): void
94
	{
95
		$this->sysError = $msg;
96
	}
97
98
	/**
99
	 * Returns the error message of the problem that stops attachments
100
	 *
101
	 * @return string
102
	 */
103
	public function getSystemError(): string
104
	{
105
		return $this->sysError;
106
	}
107
108
	/**
109
	 * Is there any error that prevents the system to upload any attachment?
110
	 *
111
	 * @return bool
112
	 */
113
	public function hasSystemError(): bool
114
	{
115
		return !empty($this->sysError);
116
	}
117
118
	/**
119
	 * Deletes a temporary attachment from the TemporaryAttachment array (and the filesystem)
120
	 *
121
	 * @param string $attachID the temporary name generated when a file is uploaded
122
	 *               and used in $_SESSION to help identify the attachment itself
123
	 * @param bool $fatal
124
	 * @throws Exception if fatal is true
125
	 */
126
	public function removeById(string $attachID, bool $fatal = true): void
127
	{
128
		if ($fatal && !isset($this->data[$attachID]))
129
		{
130
			throw new Exception('attachment_not_found');
131
		}
132
133
		if ($fatal && !$this->data[$attachID]->fileExists())
134
		{
135
			throw new Exception('attachment_not_found');
136
		}
137
138
		$this->data[$attachID]->remove($fatal);
139
		unset($this->data[$attachID]);
140
	}
141
142
	/**
143
	 * Validates that a new message is bound to a given board
144
	 *
145
	 * @param int $board
146
	 * @return bool
147
	 */
148
	public function belongToBoard(int $board): bool
149
	{
150
		return empty($this->data['post']['msg']) && (int) $this->data['post']['board'] === (int) $board;
151
	}
152
153
	/**
154
	 * Checks if at least one temporary file for a certain user exists in the file system.
155
	 *
156
	 * @param int $userId
157
	 * @return bool
158
	 */
159
	public function filesExist(int $userId): bool
160
	{
161
		$prefix = $this->getTplName($userId, '');
162
		/** @var TemporaryAttachment $attachment */
163
		foreach ($this->data as $attachID => $attachment)
164
		{
165
			if (!str_contains($attachID, (string) $prefix))
166
			{
167
				continue;
168
			}
169
170
			if ($attachment->fileExists())
171
			{
172
				unset($this->data['post']['files']);
173
174
				return true;
175
			}
176
		}
177
178
		return false;
179
	}
180
181
	/**
182
	 * Remove attachment files that we do not want to keep
183
	 *
184
	 * @param string[] $keep
185
	 * @param int $userId
186
	 */
187
	public function removeExcept(array $keep, int $userId): void
188
	{
189
		$prefix = $this->getTplName($userId, '');
190
191
		foreach ($this->data as $attachID => $attachment)
192
		{
193
			if ((isset($this->data['post']['files'], $attachment['name']) && in_array($attachment['name'], $this->data['post']['files'], true))
194
				|| in_array($attachID, $keep)
195
				|| !str_contains($attachID, (string) $prefix))
196
			{
197
				continue;
198
			}
199
200
			// Remove this one from our data array and the filesystem
201
			$attachment->remove(false);
202
			unset($this->data[$attachID]);
203
		}
204
	}
205
206
	/**
207
	 * Returns an array of names of temporary attachments for the specified user.
208
	 *
209
	 * @param int $userId
210
	 * @return mixed
211
	 */
212
	public function getFileNames(int $userId): mixed
213
	{
214
		$prefix = $this->getTplName($userId, '');
215
216
		foreach ($this->data as $attachID => $attachment)
217
		{
218
			if (str_contains($attachID, (string) $prefix))
219
			{
220
				$this->data['post']['files'][] = $attachment->getName();
221
			}
222
		}
223
224
		return $this->data['post']['files'];
225
	}
226
227
	/**
228
	 * Returns a single file name.
229
	 *
230
	 * @param int $userId
231
	 * @param string $hash
232
	 * @return string
233
	 */
234
	public function getTplName(int $userId, string $hash = ''): string
235
	{
236
		return str_replace(['{user}', '{hash}'], [$userId, $hash], static::TMPNAME_TPL);
237
	}
238
239
	/**
240
	 * If there is any post data available
241
	 *
242
	 * @return bool
243
	 */
244
	public function hasPostData(): bool
245
	{
246
		return isset($this->data['post']);
247
	}
248
249
	/**
250
	 * Add file data, for those that passed upload tests, to the data attachid key
251
	 *
252
	 * @param TemporaryAttachment $data
253
	 */
254
	public function addAttachment(TemporaryAttachment $data): void
255
	{
256
		$this->data[$data['attachid']] = $data;
257
	}
258
259
	/**
260
	 * Retrieves the attachment data.
261
	 *
262
	 * @return array
263
	 */
264
	public function getAttachment(): array
265
	{
266
		return $this->data;
267
	}
268
269
	/**
270
	 * Checks if there are any lost attachments
271
	 *
272
	 * @return bool
273
	 */
274
	public function areLostAttachments(): bool
275
	{
276
		return empty($this->data['post']['msg']);
277
	}
278
279
	/**
280
	 * Return a post parameter like files, last_msg, topic, msg
281
	 *
282
	 * @param $idx
283
	 * @return mixed|null
284
	 */
285
	public function getPostParam($idx): mixed
286
	{
287
		return $this->data['post'][$idx] ?? null;
288
	}
289
290
	/**
291
	 * Add post values to the data array in the post key
292
	 *
293
	 * @param array $vals
294
	 */
295
	public function setPostParam(array $vals): void
296
	{
297
		if (!isset($this->data['post']))
298
		{
299
			$this->data['post'] = [];
300
		}
301
302
		$this->data['post'] = array_merge($this->data['post'], $vals);
303
	}
304
305
	/**
306
	 * If a temporary attachment is for this specific message
307
	 *
308
	 * @param int $msg
309
	 * @return bool
310
	 */
311
	public function belongToMsg(int $msg): bool
312
	{
313
		return (int) $this->data['post']['msg'] === (int) $msg;
314
	}
315
316
	/**
317
	 * Checks if there is any attachment that has been processed
318
	 */
319
	public function hasAttachments(): bool
320
	{
321
		return $this->count() > 0;
322
	}
323
324
	/**
325
	 * Finds a temporary attachment by id
326
	 *
327
	 * @param string $attach_id the temporary name generated when a file is uploaded
328
	 *  and used in $_SESSION to help identify the attachment itself
329
	 * @param string $attachmentsDir
330
	 * @param int $userId
331
	 * @return mixed
332
	 * @throws Exception
333
	 */
334
	public function getTempAttachById(string $attach_id, $attachmentsDir, $userId): mixed
335
	{
336
		$attach_real_id = null;
337
338
		if ($this->hasAttachments() === false)
339
		{
340
			throw new Exception('no_access');
341
		}
342
343
		foreach ($this->data as $attachID => $val)
344
		{
345
			if ($attachID === 'post')
346
			{
347
				continue;
348
			}
349
350
			if ($val['public_attachid'] === $attach_id)
351
			{
352
				$attach_real_id = $attachID;
353
				break;
354
			}
355
		}
356
357
		if (empty($attach_real_id))
358
		{
359
			throw new Exception('no_access');
360
		}
361
362
		// The common name form is "post_tmp_123_0ac9a0b1fc18604e8704084656ed5f09"
363
		$id_attach = preg_replace('~[^0-9a-zA-Z_]~', '', $attach_real_id);
364
365
		// Permissions: only temporary attachments
366
		if (!str_starts_with($id_attach, 'post_tmp'))
367
		{
368
			throw new Exception('no_access');
369
		}
370
371
		// Permissions: only author is allowed.
372
		$pieces = explode('_', substr($id_attach, 9));
373
374
		if (!isset($pieces[0]) || $pieces[0] != $userId)
375
		{
376
			throw new Exception('no_access');
377
		}
378
379
		$attach_dir = $attachmentsDir->getCurrent();
380
381
		if (isset($this->data[$attach_real_id]) && file_exists($attach_dir . '/' . $attach_real_id))
382
		{
383
			return $this->data[$attach_real_id];
384
		}
385
386
		throw new Exception('no_access');
387
	}
388
389
	/**
390
	 * Finds our private attachment id from its public id
391
	 *
392
	 * @param string $public_attachid
393
	 *
394
	 * @return string
395
	 */
396
	public function getIdFromPublic(string $public_attachid): string
397
	{
398
		if ($this->hasAttachments() === false)
399
		{
400
			return $public_attachid;
401
		}
402
403
		foreach ($this->data as $key => $val)
404
		{
405
			if ($key === 'post')
406
			{
407
				continue;
408
			}
409
410
			$val = $val->toArray();
411
			if (!isset($val['public_attachid']))
412
			{
413
				continue;
414
			}
415
416
			if ($val['public_attachid'] !== $public_attachid)
417
			{
418
				continue;
419
			}
420
421
			return $key;
422
		}
423
424
		return $public_attachid;
425
	}
426
427
	/**
428
	 * Destroy all the attachment data in $_SESSION
429
	 * Maybe it should also do some cleanup?
430
	 */
431
	public function unset(): void
432
	{
433
		$this->data = [];
434
	}
435
}
436