Post::_getCurrentSize()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 9
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 19
rs 9.9666
ccs 0
cts 13
cp 0
crap 12
1
<?php
2
3
/**
4
 * Integration system for attachments into Post controller
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
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Modules\Attachments;
18
19
use ElkArte\Attachments\TemporaryAttachment;
20
use ElkArte\Attachments\TemporaryAttachmentProcess;
21
use ElkArte\Attachments\TemporaryAttachmentsList;
22
use ElkArte\Errors\AttachmentErrorContext;
23
use ElkArte\Errors\ErrorContext;
24
use ElkArte\EventManager;
25
use ElkArte\Helper\FileFunctions;
26
use ElkArte\Modules\AbstractModule;
27
28
/**
29
 * Class Attachments_Post_Module
30
 */
31
class Post extends AbstractModule
32
{
33
	/** @var int The mode of attachments (disabled/enabled/show only). */
34
	protected static $_attach_level = 0;
35
36
	/** @var AttachmentErrorContext The objects that keeps track of errors. */
37
	protected $_attach_errors;
38
39
	/** @var int[] List of attachments ID already saved. */
40
	protected $_saved_attach_id = [];
41
42
	/** @var bool If it is a new message or if it is an existing one edited. */
43
	protected $_is_new_message = false;
44
45
	/** @var bool If temporary attachments should be ignored or not */
46
	protected $ignore_temp = false;
47
48
	/**
49
	 * {@inheritDoc}
50
	 */
51
	public static function hooks(EventManager $eventsManager)
52
	{
53
		global $modSettings;
54
55
		if (!empty($modSettings['attachmentEnable']))
56
		{
57
			self::$_attach_level = (int) $modSettings['attachmentEnable'];
58
59
			return [
60
				['prepare_post', [Post::class, 'prepare_post'], []],
61
				['prepare_context', [Post::class, 'prepare_context'], ['post_errors']],
62
				['finalize_post_form', [Post::class, 'finalize_post_form'], ['show_additional_options', 'board', 'topic']],
63
				['prepare_save_post', [Post::class, 'prepare_save_post'], ['post_errors']],
64
				['pre_save_post', [Post::class, 'pre_save_post'], ['msgOptions']],
65
				['after_save_post', [Post::class, 'after_save_post'], ['msgOptions']],
66
			];
67
		}
68
69
		return [];
70
	}
71
72
	/**
73
	 * Get the error handler ready for post attachments
74
	 */
75
	public function prepare_post()
76
	{
77
		$this->_initErrors();
78
	}
79
80
	/**
81
	 * Set and activate the attachment error instance
82
	 */
83
	protected function _initErrors()
84
	{
85
		if ($this->_attach_errors === null)
86
		{
87
			$this->_attach_errors = AttachmentErrorContext::context();
88
89
			$this->_attach_errors->activate();
90
		}
91
	}
92
93
	/**
94
	 * Set up the errors for the template etc
95
	 *
96
	 * @param ErrorContext $post_errors
97
	 */
98
	public function prepare_context($post_errors)
99
	{
100
		global $context;
101
102
		// An array to hold all the attachments for this topic.
103
		$context['attachments']['current'] = [];
104
105
		if ($this->_attach_errors->hasErrors())
106
		{
107
			$post_errors->addError(['attachments_errors' => $this->_attach_errors]);
108
		}
109
	}
110
111
	/**
112
	 * This does lots of stuff, yes it does, in fact so much that trying to document a method like this
113
	 * would be insane.  What needs to be done, is fix this bowl of spaghetti.
114
	 *
115
	 * What it does:
116
	 *
117
	 * - Infuriates anyone trying to read the code or follow the execution path
118
	 * - Causes hallucinations and sleepless nights
119
	 * - Known to induce binge drinking
120
	 *
121
	 * @param bool $show_additional_options
122
	 * @param int $board
123
	 * @param int $topic
124
	 */
125
	public function finalize_post_form(&$show_additional_options, $board, $topic)
126
	{
127
		global $txt, $context, $modSettings;
128
129
		$context['attachments']['can']['post'] = self::$_attach_level === 1 && (allowedTo('post_attachment') || ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments')));
130
		$context['attachments']['ila_enabled'] = !empty($modSettings['attachment_inline_enabled']);
131
132
		// If you can't use attachments, this is easy
133
		if ($context['attachments']['can']['post'] === false)
134
		{
135
			return;
136
		}
137
138
		// Calculate the total size and number of attachments.
139
		$attachments = $this->_getCurrentSize();
140
141
		// A bit of housekeeping first.
142
		$tmp_attachments = new TemporaryAttachmentsList();
143
		if ($tmp_attachments->count() === 1)
144
		{
145
			$tmp_attachments->unset();
146
		}
147
148
		// Adding/removing attachments to a new or existing post
149
		if ($tmp_attachments->hasAttachments())
150
		{
151
			// Is this a request to delete them?
152
			if (isset($_GET['delete_temp']))
153
			{
154
				$tmp_attachments->removeAll($this->user->id);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\UserInfo. Since you implemented __get, consider adding a @property annotation.
Loading history...
155
				$tmp_attachments->unset();
156
				$this->_attach_errors->addError('temp_attachments_gone');
157
			}
158
			// Hmm, coming in fresh and there are files in session.
159
			elseif ($context['current_action'] !== 'post2' || !empty($this->_req->getPost('from_qr')))
0 ignored issues
show
Bug introduced by
The method getPost() does not exist on null. ( Ignorable by Annotation )

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

159
			elseif ($context['current_action'] !== 'post2' || !empty($this->_req->/** @scrutinizer ignore-call */ getPost('from_qr')))

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
160
			{
161
				$this->_determineExistingFate($tmp_attachments, $board, $topic);
162
			}
163
164
			// Process new attachments, skipping over existing
165
			if (!isset($context['ignore_temp_attachments']) && $tmp_attachments->getPostParam('files') === null)
166
			{
167
				if ($tmp_attachments->hasSystemError())
168
				{
169
					if ($context['current_action'] !== 'post2')
170
					{
171
						$txt['error_attach_initial_error'] = $txt['attach_no_upload'] . '<div class="attachmenterrors">' . $tmp_attachments->getSystemError() . '</div>';
172
						$this->_attach_errors->addError('attach_initial_error');
173
					}
174
175
					$tmp_attachments->unset();
176
				}
177
178
				$prefix = $tmp_attachments->getTplName($this->user->id, '');
179
180
				/** @var TemporaryAttachment $attachment */
181
				foreach ($tmp_attachments as $attachID => $attachment)
182
				{
183
					// Initial errors (such as missing directory), we can recover
184
					if ($attachID !== 'initial_error' && strpos($attachID, (string) $prefix) === false)
185
					{
186
						continue;
187
					}
188
189
					// Show any errors which might have occurred.
190
					if ($attachment->hasErrors())
191
					{
192
						if ($context['current_action'] !== 'post2')
193
						{
194
							$txt['error_attach_errors'] = empty($txt['error_attach_errors']) ? '<br />' : '';
195
							$txt['error_attach_errors'] .= sprintf($txt['attach_warning'], $attachment->getName()) . '<div class="attachmenterrors">';
196
							foreach ($attachment->getErrors() as $error)
197
							{
198
								$txt['error_attach_errors'] .= (is_array($error) ? vsprintf($txt[$error[0]], $error[1]) : $txt[$error]) . '<br  />';
199
							}
200
201
							$txt['error_attach_errors'] .= '</div>';
202
							$this->_attach_errors->addError('attach_errors');
203
						}
204
205
						// Take out the trash.
206
						$tmp_attachments->removeById($attachID, false);
207
208
						continue;
209
					}
210
211
					// More housekeeping.
212
					if ($attachment->fileExists() === false)
213
					{
214
						$tmp_attachments->removeById($attachID, false);
215
						continue;
216
					}
217
218
					$attachments['quantity']++;
219
					$attachments['total_size'] += $attachment['size'];
220
221
					if (!isset($context['files_in_session_warning']))
222
					{
223
						$context['files_in_session_warning'] = $txt['attached_files_in_session'];
224
					}
225
226
					$context['attachments']['current'][] = [
227
						'name' => '<span class="underline">' . htmlspecialchars($attachment['name'], ENT_COMPAT, 'UTF-8') . '</span>',
228
						'size' => $attachment['size'],
229
						'id' => $attachment['public_attachid'],
230
						'unchecked' => false,
231
						'approved' => 1,
232
					];
233
				}
234
			}
235
		}
236
237
		// If there are attachment errors. Let's show a list to the user.
238
		if ($this->_attach_errors->hasErrors())
239
		{
240
			theme()->getTemplates()->load('Errors');
241
242
			$errors = $this->_attach_errors->prepareErrors();
243
244
			foreach ($errors as $key => $error)
245
			{
246
				$context['attachment_error_keys'][] = $key . '_error';
247
				$context[$key . '_error'] = $error;
248
			}
249
		}
250
251
		// If they've unchecked an attachment, they may still want to attach that many
252
		// more files, but don't allow more than num_allowed_attachments.
253
		$context['attachments']['num_allowed'] = empty($modSettings['attachmentNumPerPostLimit']) ? 50 : min($modSettings['attachmentNumPerPostLimit'] - count($context['attachments']['current']), $modSettings['attachmentNumPerPostLimit']);
254
		$context['attachments']['can']['post_unapproved'] = allowedTo('post_attachment');
255
		$context['attachments']['total_size'] = $attachments['total_size'] ?? 0;
256
		$context['attachments']['quantity'] = $attachments['quantity'] ?? 0;
257
		$context['attachments']['restrictions'] = [];
258
		if (!empty($modSettings['attachmentCheckExtensions']))
259
		{
260
			$context['attachments']['allowed_extensions'] = strtr(strtolower($modSettings['attachmentExtensions']), [',' => ', ']);
261
		}
262
		else
263
		{
264
			$context['attachments']['allowed_extensions'] = '';
265
		}
266
267
		$context['attachments']['template'] = 'template_add_new_attachments';
268
269
		$attachmentRestrictionTypes = ['attachmentNumPerPostLimit', 'attachmentPostLimit', 'attachmentSizeLimit'];
270
		foreach ($attachmentRestrictionTypes as $type)
271
		{
272
			if (!empty($modSettings[$type]))
273
			{
274
				$context['attachments']['restrictions'][] = $type === 'attachmentNumPerPostLimit'
275
					? sprintf($txt['attach_restrict_' . $type], comma_format($modSettings[$type], 0))
276
					: sprintf($txt['attach_restrict_' . $type], byte_format($modSettings[$type] * 1024));
277
278
				// Show some numbers.
279
				if ($type === 'attachmentNumPerPostLimit')
280
				{
281
					$context['attachments']['restrictions'][] = sprintf($txt['attach_remaining'], '<span id="' . $type . '">' . ($modSettings['attachmentNumPerPostLimit'] - $attachments['quantity']) . '</span>');
282
				}
283
284
				if ($type === 'attachmentPostLimit')
285
				{
286
					$context['attachments']['restrictions'][] = sprintf($txt['attach_available'], '<span id="' . $type . '">' . byte_format(max(($modSettings['attachmentPostLimit'] * 1024) - $attachments['total_size'], 0)) . '</span>');
287
				}
288
			}
289
		}
290
291
		$show_additional_options = $show_additional_options || $tmp_attachments->hasPostData();
292
	}
293
294
	/**
295
	 * Sets up the current number of attachment files and size.  Will account
296
	 * for any that are currently in session
297
	 *
298
	 * @return array
299
	 */
300
	private function _getCurrentSize()
301
	{
302
		global $context;
303
304
		$attachments = [];
305
		$attachments['total_size'] = 0;
306
		$attachments['quantity'] = 0;
307
308
		// If this isn't a new post, check the current attachments.
309
		if (isset($_REQUEST['msg']))
310
		{
311
			$attachments['quantity'] = count($context['attachments']['current']);
312
			foreach ($context['attachments']['current'] as $attachment)
313
			{
314
				$attachments['total_size'] += $attachment['size'];
315
			}
316
		}
317
318
		return $attachments;
319
	}
320
321
	/**
322
	 * If we have loose attachments, this will determine where they best belong and
323
	 * provides the proper action for the user to deal with them.
324
	 *
325
	 * @param TemporaryAttachmentsList $tmp_attachments
326
	 * @param int $board
327
	 * @param int $topic
328
	 */
329
	private function _determineExistingFate($tmp_attachments, $board, $topic)
330
	{
331
		global $context, $txt, $scripturl;
332
333
		$msg = $this->_req->getRequest('msg', 'intval', '');
334
		$last_msg = $this->_req->getRequest('last_msg', 'intval', '');
335
336
		// Let's be nice and see if they belong here first.
337
		if ((empty($msg) && $tmp_attachments->belongToBoard($board))
338
			|| (!empty($msg) && $tmp_attachments->belongToMsg($msg)))
339
		{
340
			// See if any files still exist before showing the warning message and the files attached.
341
			if ($tmp_attachments->filesExist($this->user->id))
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\UserInfo. Since you implemented __get, consider adding a @property annotation.
Loading history...
342
			{
343
				$this->_attach_errors->addError('temp_attachments_new');
344
				$context['files_in_session_warning'] = $txt['attached_files_in_session'];
345
			}
346
		}
347
		else
348
		{
349
			// Since, they don't belong here. Let's inform the user that they exist..
350
			if (!empty($topic))
351
			{
352
				$delete_url = $scripturl . '?action=post' . (empty($msg) ? ('') : ';msg=' . $msg) . (empty($last_msg) ? ('') : ';last_msg=' . $last_msg) . ';topic=' . $topic . ';delete_temp';
353
			}
354
			else
355
			{
356
				$delete_url = $scripturl . '?action=post;board=' . $board . ';delete_temp';
357
			}
358
359
			// Compile a list of the files to show the user.
360
			$file_list = $tmp_attachments->getFileNames($this->user->id);
361
			$file_list = '<div class="attachments">' . implode('<br />', $file_list) . '</div>';
362
363
			$context['ignore_temp_attachments'] = true;
364
			if ($tmp_attachments->areLostAttachments())
365
			{
366
				$this->_attach_errors->addError(['temp_attachments_lost', [$delete_url, $file_list]]);
367
			}
368
			else
369
			{
370
				// We have a message id, so we can link back to the old topic they were trying to edit..
371
				$goback_url = $scripturl . '?action=post;msg=' . $tmp_attachments->getPostParam('msg') . ($tmp_attachments->getPostParam('last_msg') === null ? '' : ';last_msg=' . $tmp_attachments->getPostParam('last_msg')) . ';topic=' . $tmp_attachments->getPostParam('topic') . ';additionalOptions';
372
				$this->_attach_errors->addError(['temp_attachments_found', [$delete_url, $goback_url, $file_list]]);
373
			}
374
		}
375
	}
376
377
	/**
378
	 * Save attachments when the post is saved
379
	 *
380
	 * @param ErrorContext $post_errors
381
	 */
382
	public function prepare_save_post($post_errors)
383
	{
384
		$this->_initErrors();
385
386
		$msg = isset($_REQUEST['msg']) ? (int) $_REQUEST['msg'] : 0;
387
		$this->saveAttachments($msg);
388
389
		if ($this->_attach_errors->hasErrors())
390
		{
391
			$post_errors->addError(['attachments_errors' => $this->_attach_errors]);
392
		}
393
	}
394
395
	/**
396
	 * Handles both the saving and removing of attachments on post save
397
	 *
398
	 * @param int $msg
399
	 */
400
	protected function saveAttachments($msg)
401
	{
402
		global $context, $modSettings;
403
404
		// First check to see if they are trying to delete any current attachments.
405
		if (isset($_POST['attach_del']))
406
		{
407
			require_once(SUBSDIR . '/Attachments.subs.php');
408
			$keep_temp = [];
409
			$keep_ids = [];
410
			$tmp_attachments = new TemporaryAttachmentsList();
411
			$prefix = $tmp_attachments->getTplName($this->user->id, '');
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\UserInfo. Since you implemented __get, consider adding a @property annotation.
Loading history...
412
413
			foreach ($_POST['attach_del'] as $public_id)
414
			{
415
				$attachID = $tmp_attachments->getIdFromPublic($public_id);
416
417
				if (strpos($attachID, (string) $prefix) !== false)
418
				{
419
					$keep_temp[] = $attachID;
420
				}
421
				else
422
				{
423
					$keep_ids[] = (int) $attachID;
424
				}
425
			}
426
427
			$tmp_attachments->removeExcept($keep_temp, $this->user->id);
428
429
			// Editing a message, remove attachments they no longer wanted to keep
430
			if (!empty($msg))
431
			{
432
				require_once(SUBSDIR . '/ManageAttachments.subs.php');
433
				$attachmentQuery = [
434
					'attachment_type' => 0,
435
					'id_msg' => (int) $msg,
436
					'not_id_attach' => $keep_ids,
437
				];
438
				removeAttachments($attachmentQuery);
439
			}
440
		}
441
442
		// Then try to upload any attachments.
443
		$context['attachments']['can']['post'] = self::$_attach_level === 1 && (allowedTo('post_attachment') || ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments')));
444
		if ($context['attachments']['can']['post'] && empty($this->_req->getPost('from_qr')))
445
		{
446
			require_once(SUBSDIR . '/Attachments.subs.php');
447
			$processAttachments = new TemporaryAttachmentProcess();
448
			$this->ignore_temp = $processAttachments->processAttachments((int) $msg);
449
		}
450
	}
451
452
	/**
453
	 * Saves all valid attachments that were uploaded
454
	 *
455
	 * @param array $msgOptions
456
	 */
457
	public function pre_save_post(&$msgOptions)
458
	{
459
		global $context, $modSettings;
460
461
		$this->_is_new_message = empty($msgOptions['id']);
462
		$tmp_attachments = new TemporaryAttachmentsList();
463
		$prefix = $tmp_attachments->getTplName($this->user->id, '');
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\UserInfo. Since you implemented __get, consider adding a @property annotation.
Loading history...
464
465
		// ...or attach a new file...
466
		if (empty($this->ignore_temp)
467
			&& $context['attachments']['can']['post']
468
			&& $tmp_attachments->hasAttachments()
469
			&& empty($this->_req->getPost('from_qr')))
470
		{
471
			$this->_saved_attach_id = [];
472
473
			foreach ($tmp_attachments->toArray() as $attachID => $attachment)
474
			{
475
				if ($attachID !== 'initial_error' && strpos($attachID, (string) $prefix) === false)
476
				{
477
					continue;
478
				}
479
480
				// If there was an initial error just show that message.
481
				if ($attachID === 'initial_error')
482
				{
483
					$tmp_attachments->unset();
484
					break;
485
				}
486
487
				// No errors, then try to create the attachment
488
				if (empty($attachment['errors']))
489
				{
490
					// Load the attachmentOptions array with the data needed to create an attachment
491
					$attachmentOptions = [
492
						'post' => $this->_req->getRequest('msg', 'intval', 0),
493
						'poster' => $this->user->id,
494
						'name' => $attachment['name'],
495
						'tmp_name' => $attachment['tmp_name'],
496
						'size' => $attachment['size'] ?? 0,
497
						'mime_type' => $attachment['type'] ?? '',
498
						'id_folder' => $attachment['id_folder'] ?? 0,
499
						'approved' => !$modSettings['postmod_active'] || allowedTo('post_attachment'),
500
						'errors' => [],
501
					];
502
503
					if (createAttachment($attachmentOptions))
504
					{
505
						$this->_saved_attach_id[] = $attachmentOptions['id'];
506
						if (!empty($attachmentOptions['thumb']))
507
						{
508
							$this->_saved_attach_id[] = $attachmentOptions['thumb'];
509
						}
510
511
						$msgOptions['body'] = preg_replace('~\[attachurl(?:.*?)]' . $attachment['public_attachid'] . '\[/attachurl]~', '[attachurl]' . $attachmentOptions['id'] . '[/attachurl]', $msgOptions['body']);
512
						$msgOptions['body'] = preg_replace('~\[attach(.*?)]' . $attachment['public_attachid'] . '\[/attach]~', '[attach$1]' . $attachmentOptions['id'] . '[/attach]', $msgOptions['body']);
513
					}
514
				}
515
				// We have errors on this file, build out the issues for display to the user
516
				else
517
				{
518
					FileFunctions::instance()->delete($attachment['tmp_name']);
519
				}
520
			}
521
522
			// Clean up any post_tmp_.*?(_thumb)? items
523
			$tmp_attachments->removeAll($this->user->id);
524
			$tmp_attachments->unset();
525
		}
526
527
		if (empty($this->_saved_attach_id))
528
		{
529
			return;
530
		}
531
532
		if ($msgOptions['icon'] !== 'xx')
533
		{
534
			return;
535
		}
536
537
		$msgOptions['icon'] = 'clip';
538
	}
539
540
	/**
541
	 * Assigns saved attachments to the message id they were saved with
542
	 *
543
	 * @param array $msgOptions
544
	 */
545
	public function after_save_post($msgOptions)
546
	{
547
		if (!$this->_is_new_message)
548
		{
549
			return;
550
		}
551
552
		if (empty($this->_saved_attach_id))
553
		{
554
			return;
555
		}
556
557
		bindMessageAttachments($msgOptions['id'], $this->_saved_attach_id);
558
	}
559
}
560