MessagesDelete::_checkDeletePermissions()   F
last analyzed

Complexity

Conditions 54
Paths 1498

Size

Total Lines 148
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2970

Importance

Changes 0
Metric Value
cc 54
eloc 67
nc 1498
nop 2
dl 0
loc 148
ccs 0
cts 82
cp 0
crap 2970
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This class takes care of deleting and restoring messages in boards
5
 * that means posts and topics
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
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 Beta 1
15
 *
16
 */
17
18
namespace ElkArte;
19
20
use ElkArte\Exceptions\Exception;
21
22
/**
23
 * Class MessagesDelete
24
 *
25
 * Methods for deleting and restoring messages in boards
26
 */
27
class MessagesDelete
28
{
29
	/** @var User The current user deleting something */
30
	protected $user;
31
32
	/** @var int[] Id of the messages not found. */
33
	private $_unfound_messages = [];
34
35
	/** @var int[] Id of the topics that should be restored */
36
	private $_topics_to_restore = [];
37
38
	/** @var int The board id of the recycle board */
39
	private $_recycle_board;
40
41
	/**
42
	 * Initialize the class! :P
43
	 *
44
	 * @param int|bool $recycle_enabled if the recycling is enabled.
45
	 * @param int|null $recycle_board the id of the recycle board (if any)
46
	 * @param User $user the user::info making the request
47
	 */
48
	public function __construct($recycle_enabled, $recycle_board, $user = null)
49
	{
50
		$this->_recycle_board = $recycle_enabled ? (int) $recycle_board : null;
51
52
		$this->user = $user ?? User::$info;
0 ignored issues
show
Documentation Bug introduced by
It seems like $user ?? ElkArte\User::info can also be of type ElkArte\Helper\ValuesContainer. However, the property $user is declared as type ElkArte\User. 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...
53
	}
54
55
	/**
56
	 * Restores a bunch of messages in the recycle bin to the appropriate board.
57
	 * If any "first message" is within the array, it is added to the list of
58
	 * topics to restore (see MessagesDelete::restoreTopics)
59
	 *
60
	 * @param int[] $msgs_id Messages to restore
61
	 *
62
	 * @return array|void
63
	 */
64
	public function restoreMessages($msgs_id): ?array
65
	{
66
		$msgs = [];
67
		foreach ($msgs_id as $msg)
68
		{
69
			if (!empty($msg))
70
			{
71
				$msgs[] = $msg;
72
			}
73
		}
74
75
		if (empty($msgs))
76
		{
77
			return null;
78
		}
79
80
		$db = database();
81
82
		// Get the id_previous_board and id_previous_topic.
83
		$actioned_messages = [];
84
		$previous_topics = [];
85
		$db->fetchQuery('
86
			SELECT 
87
				m.id_topic, m.id_msg, m.id_board, m.subject, m.id_member, t.id_previous_board, t.id_previous_topic,
88
				t.id_first_msg, b.count_posts, COALESCE(pt.id_board, 0) AS possible_prev_board
89
			FROM {db_prefix}messages AS m
90
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
91
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
92
				LEFT JOIN {db_prefix}topics AS pt ON (pt.id_topic = t.id_previous_topic)
93
			WHERE m.id_msg IN ({array_int:messages})',
94
			[
95
				'messages' => $msgs,
96
			]
97
		)->fetch_callback(
98
			function ($row) use (&$actioned_messages, &$previous_topics) {
99
				// Restoring the first post means topic.
100
				if ((int) $row['id_msg'] === (int) $row['id_first_msg'] && (int) $row['id_previous_topic'] === (int) $row['id_topic'])
101
				{
102
					$this->_topics_to_restore[] = $row['id_topic'];
103
					return;
104
				}
105
106
				// Don't know where it's going?
107
				if (empty($row['id_previous_topic']))
108
				{
109
					$this->_unfound_messages[$row['id_msg']] = $row['subject'];
110
					return;
111
				}
112
113
				$previous_topics[] = $row['id_previous_topic'];
114
				if (empty($actioned_messages[$row['id_previous_topic']]))
115
				{
116
					$actioned_messages[$row['id_previous_topic']] = [
117
						'msgs' => [],
118
						'count_posts' => (int) $row['count_posts'],
119
						'subject' => $row['subject'],
120
						'previous_board' => $row['id_previous_board'],
121
						'possible_prev_board' => $row['possible_prev_board'],
122
						'current_topic' => (int) $row['id_topic'],
123
						'current_board' => (int) $row['id_board'],
124
						'members' => [],
125
					];
126
				}
127
128
				$actioned_messages[$row['id_previous_topic']]['msgs'][$row['id_msg']] = $row['subject'];
129
				if ($row['id_member'])
130
				{
131
					$actioned_messages[$row['id_previous_topic']]['members'][] = (int) $row['id_member'];
132
				}
133
			}
134
		);
135
136
		// Check for topics we are going to fully restore.
137
		foreach (array_keys($actioned_messages) as $topic)
138
		{
139
			if (in_array($topic, $this->_topics_to_restore))
140
			{
141
				unset($actioned_messages[$topic]);
142
			}
143
		}
144
145
		// Load any previous topics to check they exist.
146
		if (!empty($previous_topics))
147
		{
148
			$db->fetchQuery('
149
				SELECT 
150
					t.id_topic, t.id_board, m.subject
151
				FROM {db_prefix}topics AS t
152
					INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
153
				WHERE t.id_topic IN ({array_int:previous_topics})',
154
				[
155
					'previous_topics' => $previous_topics,
156
				]
157
			)->fetch_callback(
158
				static function ($row) use (&$previous_topics) {
159
					$previous_topics[$row['id_topic']] = [
160
						'board' => $row['id_board'],
161
						'subject' => $row['subject'],
162
					];
163
				}
164
			);
165
		}
166
167
		// Restore each topic.
168
		$messages = [];
169
		foreach ($actioned_messages as $topic => $data)
170
		{
171
			// If we have topics, we are going to restore the whole lot ignore them.
172
			if (in_array($topic, $this->_topics_to_restore))
173
			{
174
				unset($actioned_messages[$topic]);
175
				continue;
176
			}
177
178
			// Move the posts back then!
179
			if (isset($previous_topics[$topic]))
180
			{
181
				$this->_mergePosts(array_keys($data['msgs']), $data['current_topic'], $topic);
182
183
				// Log em.
184
				logAction('restore_posts', ['topic' => $topic, 'subject' => $previous_topics[$topic]['subject'], 'board' => empty($data['previous_board']) ? $data['possible_prev_board'] : $data['previous_board']]);
185
				foreach (array_keys($data['msgs']) as $m)
186
				{
187
					$messages[] = $m;
188
				}
189
			}
190
			else
191
			{
192
				foreach ($data['msgs'] as $id => $subject)
193
				{
194
					$this->_unfound_messages[$id] = $subject;
195
				}
196
			}
197
		}
198
199
		// Put the icons back.
200
		if (!empty($messages))
201
		{
202
			$db->query('', '
203
				UPDATE {db_prefix}messages
204
				SET icon = {string:icon}
205
				WHERE id_msg IN ({array_int:messages})',
206
				[
207
					'icon' => 'xx',
208
					'messages' => $messages,
209
				]
210
			);
211
		}
212
213
		return $actioned_messages;
214
	}
215
216
	/**
217
	 * Take a load of messages from one place and stick them in a topic
218
	 *
219
	 * @param int[] $msgs
220
	 * @param int $from_topic
221
	 * @param int $target_topic
222
	 */
223
	private function _mergePosts($msgs, $from_topic, $target_topic): void
224
	{
225
		$db = database();
226
227
		// @todo This really needs to be rewritten to take a load of messages from ANY topic, it's also inefficient.
228
229
		// Is it an array?
230
		if (!is_array($msgs))
0 ignored issues
show
introduced by
The condition is_array($msgs) is always true.
Loading history...
231
		{
232
			$msgs = [$msgs];
233
		}
234
235
		// Let's make sure they are int.
236
		$msgs = array_map(static fn($value) => (int) $value, $msgs);
237
238
		// Get the source information.
239
		$request = $db->query('', '
240
			SELECT 
241
				t.id_board, t.id_first_msg, t.num_replies, t.unapproved_posts
242
			FROM {db_prefix}topics AS t
243
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
244
			WHERE t.id_topic = {int:from_topic}',
245
			[
246
				'from_topic' => $from_topic,
247
			]
248
		);
249
		[$from_board, $from_first_msg, $from_replies, $from_unapproved_posts] = $request->fetch_row();
250
		$request->free_result();
251
252
		// Get some target topic and board stats.
253
		$request = $db->query('', '
254
			SELECT 
255
				t.id_board, t.id_first_msg, t.num_replies, t.unapproved_posts, b.count_posts
256
			FROM {db_prefix}topics AS t
257
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
258
			WHERE t.id_topic = {int:target_topic}',
259
			[
260
				'target_topic' => $target_topic,
261
			]
262
		);
263
		[$target_board, $target_first_msg, $target_replies, $target_unapproved_posts, $count_posts] = $request->fetch_row();
264
		$request->free_result();
265
266
		// Let's see if the board that we are returning to has post-count enabled.
267
		if (empty($count_posts))
268
		{
269
			// Let's get the members that need their post-count restored.
270
			require_once(SUBSDIR . '/Members.subs.php');
271
			$db->fetchQuery('
272
				SELECT id_member
273
				FROM {db_prefix}messages
274
				WHERE id_msg IN ({array_int:messages})
275
					AND approved = {int:is_approved}',
276
				[
277
					'messages' => $msgs,
278
					'is_approved' => 1,
279
				]
280
			)->fetch_callback(
281
				static function ($row) {
282
					updateMemberData($row['id_member'], ['posts' => '+']);
283
				}
284
			);
285
		}
286
287
		// Time to move the messages.
288
		$db->query('', '
289
			UPDATE {db_prefix}messages
290
			SET
291
				id_topic = {int:target_topic},
292
				id_board = {int:target_board},
293
				icon = {string:icon}
294
			WHERE id_msg IN({array_int:msgs})',
295
			[
296
				'target_topic' => $target_topic,
297
				'target_board' => $target_board,
298
				'icon' => $target_board == $this->_recycle_board ? 'recycled' : 'xx',
299
				'msgs' => $msgs,
300
			]
301
		);
302
303
		// Fix the id_first_msg and id_last_msg for the target topic.
304
		$target_topic_data = [
305
			'num_replies' => 0,
306
			'unapproved_posts' => 0,
307
			'id_first_msg' => 9999999999,
308
		];
309
		$db->fetchQuery('
310
			SELECT 
311
				MIN(id_msg) AS id_first_msg, MAX(id_msg) AS id_last_msg, COUNT(*) AS message_count, approved
312
			FROM {db_prefix}messages
313
			WHERE id_topic = {int:target_topic}
314
			GROUP BY id_topic, approved
315
			ORDER BY approved ASC
316
			LIMIT 2',
317
			[
318
				'target_topic' => $target_topic,
319
			]
320
		)->fetch_callback(
321
			static function ($row) use (&$target_topic_data) {
322
				if ($row['id_first_msg'] < $target_topic_data['id_first_msg'])
323
				{
324
					$target_topic_data['id_first_msg'] = $row['id_first_msg'];
325
				}
326
327
				$target_topic_data['id_last_msg'] = $row['id_last_msg'];
328
				if (!$row['approved'])
329
				{
330
					$target_topic_data['unapproved_posts'] = $row['message_count'];
331
				}
332
				else
333
				{
334
					$target_topic_data['num_replies'] = max(0, $row['message_count'] - 1);
335
				}
336
			}
337
		);
338
339
		// We have a new post-count for the board.
340
		require_once(SUBSDIR . '/Boards.subs.php');
341
		incrementBoard($target_board, [
342
			'num_posts' => $target_topic_data['num_replies'] - $target_replies, // Let's keep in mind that the first message in a topic counts towards num_replies in a board.
343
			'unapproved_posts' => $target_topic_data['unapproved_posts'] - $target_unapproved_posts,
344
		]);
345
346
		// In some cases we merged the only post in a topic, so the topic data is left behind in the topic table.
347
		$request = $db->query('', '
348
			SELECT 
349
				id_topic
350
			FROM {db_prefix}messages
351
			WHERE id_topic = {int:from_topic}',
352
			[
353
				'from_topic' => $from_topic,
354
			]
355
		);
356
357
		// Remove the topic if it doesn't have any messages.
358
		$topic_exists = true;
359
		if ($request->num_rows() === 0)
360
		{
361
			require_once(SUBSDIR . '/Topic.subs.php');
362
			removeTopics($from_topic, false, true);
363
			$topic_exists = false;
364
		}
365
366
		$request->free_result();
367
368
		// Recycled topic.
369
		if ($topic_exists)
370
		{
371
			// Fix the id_first_msg and id_last_msg for the source topic.
372
			$source_topic_data = [
373
				'num_replies' => 0,
374
				'unapproved_posts' => 0,
375
				'id_first_msg' => 9999999999,
376
			];
377
378
			$db->fetchQuery('
379
				SELECT 
380
					MIN(id_msg) AS id_first_msg, MAX(id_msg) AS id_last_msg, COUNT(*) AS message_count, approved, subject
381
				FROM {db_prefix}messages
382
				WHERE id_topic = {int:from_topic}
383
				GROUP BY id_topic, approved
384
				ORDER BY approved ASC
385
				LIMIT 2',
386
				[
387
					'from_topic' => $from_topic,
388
				]
389
			)->fetch_callback(
390
				static function ($row) use (&$source_topic_data) {
391
					if ($row['id_first_msg'] < $source_topic_data['id_first_msg'])
392
					{
393
						$source_topic_data['id_first_msg'] = $row['id_first_msg'];
394
					}
395
396
					$source_topic_data['id_last_msg'] = $row['id_last_msg'];
397
					if (!$row['approved'])
398
					{
399
						$source_topic_data['unapproved_posts'] = $row['message_count'];
400
					}
401
					else
402
					{
403
						$source_topic_data['num_replies'] = max(0, $row['message_count'] - 1);
404
					}
405
				}
406
			);
407
408
			// Update the topic details for the source topic.
409
			setTopicAttribute($from_topic, [
410
				'id_first_msg' => $source_topic_data['id_first_msg'],
411
				'id_last_msg' => $source_topic_data['id_last_msg'],
412
				'num_replies' => $source_topic_data['num_replies'],
413
				'unapproved_posts' => $source_topic_data['unapproved_posts'],
414
			]);
415
416
			// We have a new post-count for the source board.
417
			incrementBoard($target_board, [
418
				'num_posts' => $source_topic_data['num_replies'] - $from_replies, // Let's keep in mind that the first message in a topic counts towards num_replies in a board.
419
				'unapproved_posts' => $source_topic_data['unapproved_posts'] - $from_unapproved_posts,
420
			]);
421
		}
422
423
		// Finally, get around to updating the destination topic, now all indexes etc. on the source are fixed.
424
		setTopicAttribute($target_topic, [
425
			'id_first_msg' => $target_topic_data['id_first_msg'],
426
			'id_last_msg' => $target_topic_data['id_last_msg'],
427
			'num_replies' => $target_topic_data['num_replies'],
428
			'unapproved_posts' => $target_topic_data['unapproved_posts'],
429
		]);
430
431
		// Need it to update some stats.
432
		require_once(SUBSDIR . '/Post.subs.php');
433
		require_once(SUBSDIR . '/Topic.subs.php');
434
		require_once(SUBSDIR . '/Messages.subs.php');
435
436
		// Update stats.
437
		updateTopicStats();
438
		updateMessageStats();
439
440
		// Subject cache?
441
		$cache_updates = [];
442
		if ($target_first_msg != $target_topic_data['id_first_msg'])
443
		{
444
			$cache_updates[] = $target_topic_data['id_first_msg'];
445
		}
446
447
		if (!empty($source_topic_data['id_first_msg']) && $from_first_msg != $source_topic_data['id_first_msg'])
448
		{
449
			$cache_updates[] = $source_topic_data['id_first_msg'];
450
		}
451
452
		if (!empty($cache_updates))
453
		{
454
			require_once(SUBSDIR . '/Messages.subs.php');
455
			$db->fetchQuery('
456
				SELECT 
457
					id_topic, subject
458
				FROM {db_prefix}messages
459
				WHERE id_msg IN ({array_int:first_messages})',
460
				[
461
					'first_messages' => $cache_updates,
462
				]
463
			)->fetch_callback(
464
				static function ($row) {
465
					updateSubjectStats($row['id_topic'], $row['subject']);
466
				}
467
			);
468
		}
469
470
		updateLastMessages([$from_board, $target_board]);
471
	}
472
473
	/**
474
	 * Prepares topics to be restored from the recycle bin to the appropriate board
475
	 *
476
	 * @param int[] $topics_id Topics to restore
477
	 */
478
	public function restoreTopics($topics_id): void
479
	{
480
		foreach ($topics_id as $topic)
481
		{
482
			if (!empty($topic))
483
			{
484
				$this->_topics_to_restore[] = $topic;
485
			}
486
		}
487
	}
488
489
	/**
490
	 * Actually restore the topics previously "collected"
491
	 */
492
	public function doRestore(): void
493
	{
494
		if (empty($this->_topics_to_restore))
495
		{
496
			return;
497
		}
498
499
		$db = database();
500
501
		require_once(SUBSDIR . '/Boards.subs.php');
502
503
		// Let's get the data for these topics.
504
		$request = $db->fetchQuery('
505
			SELECT 
506
				t.id_topic, t.id_previous_board, t.id_board, t.id_first_msg, m.subject
507
			FROM {db_prefix}topics AS t
508
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
509
			WHERE t.id_topic IN ({array_int:topics})',
510
			[
511
				'topics' => $this->_topics_to_restore,
512
			]
513
		);
514
		while (($row = $request->fetch_assoc()))
515
		{
516
			// We can only restore if the previous board is set.
517
			if (empty($row['id_previous_board']))
518
			{
519
				$this->_unfound_messages[$row['id_first_msg']] = $row['subject'];
520
				continue;
521
			}
522
523
			// Ok, we got here, so me move them from here to there.
524
			moveTopics($row['id_topic'], $row['id_previous_board']);
525
526
			// Let's remove the recycled icon.
527
			$db->query('', '
528
				UPDATE {db_prefix}messages
529
				SET icon = {string:icon}
530
				WHERE id_topic = {int:id_topic}',
531
				[
532
					'icon' => 'xx',
533
					'id_topic' => $row['id_topic'],
534
				]
535
			);
536
537
			// Let's see if the board that we are returning to has post-count enabled.
538
			$board_data = boardInfo($row['id_previous_board']);
539
540
			if (empty($board_data['count_posts']))
541
			{
542
				require_once(SUBSDIR . '/Members.subs.php');
543
544
				// Let's get the members that need their post-count restored.
545
				$db->fetchQuery('
546
					SELECT
547
						id_member, COUNT(id_msg) AS post_count
548
					FROM {db_prefix}messages
549
					WHERE id_topic = {int:topic}
550
						AND approved = {int:is_approved}
551
					GROUP BY id_member',
552
					[
553
						'topic' => $row['id_topic'],
554
						'is_approved' => 1,
555
					]
556
				)->fetch_callback(
557
					static function ($member) {
558
						updateMemberData($member['id_member'], ['posts' => 'posts + ' . $member['post_count']]);
559
					}
560
				);
561
			}
562
563
			// Log it.
564
			logAction('restore_topic', ['topic' => $row['id_topic'], 'board' => $row['id_board'], 'board_to' => $row['id_previous_board']]);
565
		}
566
567
		$request->free_result();
568
	}
569
570
	/**
571
	 * Returns either the list of messages not found, or the number
572
	 *
573
	 * @param bool $return_msg If true returns the array of not found messages,
574
	 *                         if false their number
575
	 * @return bool|int[]
576
	 */
577
	public function unfoundRestoreMessages($return_msg = false)
578
	{
579
		if ($return_msg)
580
		{
581
			return $this->_unfound_messages;
582
		}
583
584
		return !empty($this->_unfound_messages);
585
	}
586
587
	/**
588
	 * Remove a message from the forum.
589
	 *
590
	 * - If $check_permissions is true, and it is the first and only message in a topic, removes the topic
591
	 *
592
	 * @param int $message The ID of the message to be removed
593
	 * @param bool $decreasePostCount Whether to decrease the post-count or not (default: true)
594
	 * @param bool $check_permissions Whether to check permissions or not (default: true) (may result in fatal
595
	 *             errors or login screens)
596
	 *
597
	 * @return bool Whether the message was successfully removed or not
598
	 */
599
	public function removeMessage($message, $decreasePostCount = true, $check_permissions = true): bool
600
	{
601
		global $board, $modSettings;
602
603
		$db = database();
604
605
		$message = (int) $message;
606
607
		if (empty($message))
608
		{
609
			return false;
610
		}
611
612
		$row = $db->fetchQuery('
613
			SELECT
614
				m.id_msg, m.id_member, m.icon, m.poster_time, m.subject,' . (empty($modSettings['search_custom_index_config']) ? '' : ' m.body,') . '
615
				m.approved, t.id_topic, t.id_first_msg, t.id_last_msg, t.num_replies, t.id_board,
616
				t.id_member_started AS id_member_poster,
617
				b.count_posts
618
			FROM {db_prefix}messages AS m
619
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
620
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
621
			WHERE m.id_msg = {int:id_msg}
622
			LIMIT 1',
623
			[
624
				'id_msg' => $message,
625
			]
626
		)->fetch_assoc();
627
628
		if (empty($row))
629
		{
630
			return false;
631
		}
632
633
		if ($check_permissions)
634
		{
635
			$check = $this->_checkDeletePermissions($row, $board);
636
			if ($check === 'topic')
637
			{
638
				// This needs to be included for topic functions
639
				require_once(SUBSDIR . '/Topic.subs.php');
640
				removeTopics($row['id_topic']);
641
642
				return true;
643
			}
644
645
			if ($check === 'exit')
646
			{
647
				return false;
648
			}
649
		}
650
651
		// Deleting a recycled message cannot lower anyone's post-count.
652
		if ($row['icon'] === 'recycled')
653
		{
654
			$decreasePostCount = false;
655
		}
656
657
		// This is the last post, update the last post on the board.
658
		if ((int) $row['id_last_msg'] === $message)
659
		{
660
			// Find the last message, set it, and decrease the post-count.
661
			$row2 = $db->fetchQuery('
662
				SELECT 
663
					id_msg, id_member
664
				FROM {db_prefix}messages
665
				WHERE id_topic = {int:id_topic}
666
					AND id_msg != {int:id_msg}
667
				ORDER BY ' . ($modSettings['postmod_active'] ? 'approved DESC, ' : '') . 'id_msg DESC
668
				LIMIT 1',
669
				[
670
					'id_topic' => $row['id_topic'],
671
					'id_msg' => $message,
672
				]
673
			)->fetch_assoc();
674
675
			$db->query('', '
676
				UPDATE {db_prefix}topics
677
				SET
678
					id_last_msg = {int:id_last_msg},
679
					id_member_updated = {int:id_member_updated}' . (!$modSettings['postmod_active'] || $row['approved'] ? ',
680
					num_replies = CASE WHEN num_replies = {int:no_replies} THEN 0 ELSE num_replies - 1 END' : ',
681
					unapproved_posts = CASE WHEN unapproved_posts = {int:no_unapproved} THEN 0 ELSE unapproved_posts - 1 END') . '
682
				WHERE id_topic = {int:id_topic}',
683
				[
684
					'id_last_msg' => (int) $row2['id_msg'],
685
					'id_member_updated' => (int) $row2['id_member'],
686
					'no_replies' => 0,
687
					'no_unapproved' => 0,
688
					'id_topic' => (int) $row['id_topic'],
689
				]
690
			);
691
		}
692
		// Only decrease post-counts.
693
		else
694
		{
695
			$db->query('', '
696
				UPDATE {db_prefix}topics
697
				SET ' . ($row['approved'] ? '
698
					num_replies = CASE WHEN num_replies = {int:no_replies} THEN 0 ELSE num_replies - 1 END' : '
699
					unapproved_posts = CASE WHEN unapproved_posts = {int:no_unapproved} THEN 0 ELSE unapproved_posts - 1 END') . '
700
				WHERE id_topic = {int:id_topic}',
701
				[
702
					'no_replies' => 0,
703
					'no_unapproved' => 0,
704
					'id_topic' => (int) $row['id_topic'],
705
				]
706
			);
707
		}
708
709
		// Default recycle to false.
710
		$recycle = false;
711
712
		// If recycle topics have been set, make a copy of this message in the recycle board.
713
		// Make sure we're not recycling messages that are already on the recycle board.
714
		if (!empty($this->_recycle_board) && $row['id_board'] != $this->_recycle_board && $row['icon'] !== 'recycled')
715
		{
716
			// Check if the recycle board exists, and if so, get the read status.
717
			$request = $db->query('', '
718
				SELECT 
719
					(COALESCE(lb.id_msg, 0) >= b.id_msg_updated) AS is_seen, id_last_msg
720
				FROM {db_prefix}boards AS b
721
					LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
722
				WHERE b.id_board = {int:recycle_board}',
723
				[
724
					'current_member' => $this->user->id,
0 ignored issues
show
Bug introduced by
The property id is declared protected in ElkArte\User and cannot be accessed from this context.
Loading history...
725
					'recycle_board' => $this->_recycle_board,
726
				]
727
			);
728
729
			if ($request->num_rows() === 0)
730
			{
731
				throw new Exceptions\Exception('recycle_no_valid_board');
732
			}
733
734
			[$isRead, $last_board_msg] = $request->fetch_row();
735
			$request->free_result();
736
737
			// Is there an existing topic in the recycle board to group this post with?
738
			$request = $db->query('', '
739
				SELECT 
740
					id_topic, id_first_msg, id_last_msg
741
				FROM {db_prefix}topics
742
				WHERE id_previous_topic = {int:id_previous_topic}
743
					AND id_board = {int:recycle_board}',
744
				[
745
					'id_previous_topic' => $row['id_topic'],
746
					'recycle_board' => $this->_recycle_board,
747
				]
748
			);
749
			[$id_recycle_topic, $first_topic_msg, $last_topic_msg] = $request->fetch_row();
750
			$request->free_result();
751
752
			// Insert a new topic in the recycle board if $id_recycle_topic is empty.
753
			if (empty($id_recycle_topic))
754
			{
755
				$insert_res = $db->insert('',
756
					'{db_prefix}topics',
757
					[
758
						'id_board' => 'int', 'id_member_started' => 'int', 'id_member_updated' => 'int', 'id_first_msg' => 'int',
759
						'id_last_msg' => 'int', 'unapproved_posts' => 'int', 'approved' => 'int', 'id_previous_topic' => 'int',
760
					],
761
					[
762
						$this->_recycle_board, $row['id_member'], $row['id_member'], $message,
763
						$message, 0, 1, $row['id_topic'],
764
					],
765
					['id_topic']
766
				);
767
				$topicID = $insert_res->insert_id();
768
			}
769
			else
770
			{
771
				// Capture the ID of the new topic...
772
				$topicID = $id_recycle_topic;
773
			}
774
775
			// If the topic creation went successful, move the message.
776
			if ($topicID > 0)
777
			{
778
				$db->query('', '
779
					UPDATE {db_prefix}messages
780
					SET
781
						id_topic = {int:id_topic},
782
						id_board = {int:recycle_board},
783
						icon = {string:recycled},
784
						approved = {int:is_approved}
785
					WHERE id_msg = {int:id_msg}',
786
					[
787
						'id_topic' => $topicID,
788
						'recycle_board' => $this->_recycle_board,
789
						'id_msg' => $message,
790
						'recycled' => 'recycled',
791
						'is_approved' => 1,
792
					]
793
				);
794
795
				// Take any reported posts with us...
796
				$db->query('', '
797
					UPDATE {db_prefix}log_reported
798
					SET
799
						id_topic = {int:id_topic},
800
						id_board = {int:recycle_board}
801
					WHERE id_msg = {int:id_msg}
802
						AND type = {string:msg}',
803
					[
804
						'id_topic' => $topicID,
805
						'recycle_board' => $this->_recycle_board,
806
						'id_msg' => $message,
807
						'msg' => 'msg',
808
					]
809
				);
810
811
				// Mark recycled topic as read.
812
				if ($this->user->is_guest === false)
0 ignored issues
show
Bug introduced by
The property is_guest does not seem to exist on ElkArte\User.
Loading history...
813
				{
814
					require_once(SUBSDIR . '/Topic.subs.php');
815
					markTopicsRead([$this->user->id, $topicID, $modSettings['maxMsgID'], 0], true);
816
				}
817
818
				// Mark the recycle board as seen, if it was marked as seen before.
819
				if (!empty($isRead) && $this->user->is_guest === false)
820
				{
821
					require_once(SUBSDIR . '/Boards.subs.php');
822
					markBoardsRead($this->_recycle_board);
823
				}
824
825
				// Add one topic and post to the recycle bin board.
826
				$db->query('', '
827
					UPDATE {db_prefix}boards
828
					SET
829
						num_topics = num_topics + {int:num_topics_inc},
830
						num_posts = num_posts + 1' .
831
					($message > $last_board_msg ? ', id_last_msg = {int:id_merged_msg}' : '') . '
832
					WHERE id_board = {int:recycle_board}',
833
					[
834
						'num_topics_inc' => empty($id_recycle_topic) ? 1 : 0,
835
						'recycle_board' => $this->_recycle_board,
836
						'id_merged_msg' => $message,
837
					]
838
				);
839
840
				// Let's increase the num_replies, and the first/last message ID as appropriate.
841
				if (!empty($id_recycle_topic))
842
				{
843
					$db->query('', '
844
						UPDATE {db_prefix}topics
845
						SET num_replies = num_replies + 1' .
846
						($message > $last_topic_msg ? ', id_last_msg = {int:id_merged_msg}' : '') .
847
						($message < $first_topic_msg ? ', id_first_msg = {int:id_merged_msg}' : '') . '
848
						WHERE id_topic = {int:id_recycle_topic}',
849
						[
850
							'id_recycle_topic' => $id_recycle_topic,
851
							'id_merged_msg' => $message,
852
						]
853
					);
854
				}
855
856
				// Update mentions accessibility
857
				$this->deleteMessageMentions(messagesInTopics($topicID), true);
858
859
				// Make sure this message isn't getting deleted later on.
860
				$recycle = true;
861
862
				// Make sure we update the search subject index.
863
				require_once(SUBSDIR . '/Messages.subs.php');
864
				updateSubjectStats($topicID, $row['subject']);
865
			}
866
867
			// If it wasn't approved, don't keep it in the queue.
868
			if (!$row['approved'])
869
			{
870
				$db->query('', '
871
					DELETE FROM {db_prefix}approval_queue
872
					WHERE id_msg = {int:id_msg}
873
						AND id_attach = {int:id_attach}',
874
					[
875
						'id_msg' => $message,
876
						'id_attach' => 0,
877
					]
878
				);
879
			}
880
		}
881
882
		$db->query('', '
883
			UPDATE {db_prefix}boards
884
			SET ' . ($row['approved'] ? '
885
				num_posts = CASE WHEN num_posts = {int:no_posts} THEN 0 ELSE num_posts - 1 END' : '
886
				unapproved_posts = CASE WHEN unapproved_posts = {int:no_unapproved} THEN 0 ELSE unapproved_posts - 1 END') . '
887
			WHERE id_board = {int:id_board}',
888
			[
889
				'no_posts' => 0,
890
				'no_unapproved' => 0,
891
				'id_board' => $row['id_board'],
892
			]
893
		);
894
895
		// If the poster was registered and the board, this message was on increased
896
		// the member's posts when it was posted, decrease his or her post-count.
897
		if (!empty($row['id_member']) && $decreasePostCount && empty($row['count_posts']) && $row['approved'])
898
		{
899
			require_once(SUBSDIR . '/Members.subs.php');
900
			updateMemberData($row['id_member'], ['posts' => '-']);
901
		}
902
903
		// Only remove posts if they're not recycled.
904
		if (!$recycle)
905
		{
906
			// Update the like counts
907
			require_once(SUBSDIR . '/Likes.subs.php');
908
			decreaseLikeCounts($message);
909
910
			// Remove the likes!
911
			$db->query('', '
912
				DELETE FROM {db_prefix}message_likes
913
				WHERE id_msg = {int:id_msg}',
914
				[
915
					'id_msg' => $message,
916
				]
917
			);
918
919
			// Remove the mentions!
920
			$this->deleteMessageMentions($message);
921
922
			// Remove the message!
923
			$db->query('', '
924
				DELETE FROM {db_prefix}messages
925
				WHERE id_msg = {int:id_msg}',
926
				[
927
					'id_msg' => $message,
928
				]
929
			);
930
931
			if (!empty($modSettings['search_custom_index_config']))
932
			{
933
				$words = text2words($row['body'], true);
934
				if (!empty($words))
935
				{
936
					$db->query('', '
937
						DELETE FROM {db_prefix}log_search_words
938
						WHERE id_word IN ({array_int:word_list})
939
							AND id_msg = {int:id_msg}',
940
						[
941
							'word_list' => $words,
942
							'id_msg' => $message,
943
						]
944
					);
945
				}
946
			}
947
948
			// Delete attachment(s) if they exist.
949
			require_once(SUBSDIR . '/ManageAttachments.subs.php');
950
			$attachmentQuery = [
951
				'attachment_type' => 0,
952
				'id_msg' => $message,
953
			];
954
			removeAttachments($attachmentQuery);
955
956
			// Delete follow-ups too
957
			require_once(SUBSDIR . '/FollowUps.subs.php');
958
959
			// If it is an entire topic
960
			if ((int) $row['id_first_msg'] === (int) $message)
961
			{
962
				$db->query('', '
963
					DELETE FROM {db_prefix}follow_ups
964
					WHERE follow_ups IN ({array_int:topics})',
965
					[
966
						'topics' => $row['id_topic'],
967
					]
968
				);
969
			}
970
971
			// Allow mods to remove message-related data of their own (likes, maybe?)
972
			call_integration_hook('integrate_remove_message', [$message]);
973
		}
974
975
		// Update the pesky statistics.
976
		updateMessageStats();
977
		require_once(SUBSDIR . '/Topic.subs.php');
978
		updateTopicStats();
979
		updateSettings(['calendar_updated' => time(),]);
980
981
		// And now to update the last message of each board we messed with.
982
		require_once(SUBSDIR . '/Post.subs.php');
983
		if ($recycle)
984
		{
985
			updateLastMessages([$row['id_board'], $this->_recycle_board]);
986
		}
987
		else
988
		{
989
			updateLastMessages($row['id_board']);
990
		}
991
992
		// Close any moderation reports for this message.
993
		require_once(SUBSDIR . '/Moderation.subs.php');
994
		$updated_reports = updateReportsStatus($message, 'close', 1);
995
		if ($updated_reports != 0)
996
		{
997
			updateSettings(['last_mod_report_action' => time()]);
998
			recountOpenReports();
999
		}
1000
1001
		// Add it to the mod log.
1002
		if (!allowedTo('delete_any'))
1003
		{
1004
			return false;
1005
		}
1006
1007
		if (allowedTo('delete_own') && $row['id_member'] == $this->user->id)
1008
		{
1009
			return false;
1010
		}
1011
1012
		logAction('delete', ['topic' => $row['id_topic'], 'subject' => $row['subject'], 'member' => $row['id_member'], 'board' => $row['id_board']]);
1013
		return false;
1014
	}
1015
1016
	/**
1017
	 * When a message is removed, we need to remove associated mentions and updated the member
1018
	 * mention count for anyone was mentioned in that message (like, quote, @, etc.)
1019
	 *
1020
	 * @param int|int[] $messages
1021
	 * @param bool $recycle If recycle board is enabled, sets mentions as in_accessible, otherwise hard delete
1022
	 */
1023
	public function deleteMessageMentions($messages, $recycle = false): void
1024
	{
1025
		$db = database();
1026
1027
		$mentionTypes = ['mentionmem', 'likemsg', 'rlikemsg', 'quotedmem', 'watchedtopic', 'watchedboard'];
1028
		$messages = is_array($messages) ? $messages : [$messages];
1029
		$changeMe = [];
1030
1031
		// Who was mentioned in these messages
1032
		$db->fetchQuery('
1033
			SELECT 
1034
				DISTINCT(id_member) as id_member
1035
			FROM {db_prefix}log_mentions
1036
			WHERE id_target IN ({array_int:messages})
1037
				AND mention_type IN ({array_string:mention_types})',
1038
			[
1039
				'messages' => $messages,
1040
				'mention_types' => $mentionTypes,
1041
			]
1042
		)->fetch_callback(
1043
			function ($row) use (&$changeMe) {
1044
				$changeMe[] = (int) $row['id_member'];
1045
			}
1046
		);
1047
1048
		if ($recycle === true)
1049
		{
1050
			// Set them as not accessible when they are going to the trashcan
1051
			$db->query('', '
1052
				UPDATE {db_prefix}log_mentions
1053
				SET
1054
					is_accessible = 0
1055
				WHERE id_target IN ({array_int:messages})
1056
					AND is_accessible = 1',
1057
				[
1058
					'messages' => $messages,
1059
					'mention_types' => $mentionTypes,
1060
				]
1061
			);
1062
		}
1063
		else
1064
		{
1065
			// This message is really gone, so remove the mentions!
1066
			$db->query('', '
1067
				DELETE FROM {db_prefix}log_mentions
1068
				WHERE id_target IN ({array_int:messages})
1069
					AND mention_type IN ({array_string:mention_types})',
1070
				[
1071
					'messages' => $messages,
1072
					'mention_types' => $mentionTypes,
1073
				]
1074
			);
1075
		}
1076
1077
		// Update the mention count for this group
1078
		require_once(SUBSDIR . '/Mentions.subs.php');
1079
		foreach ($changeMe as $member)
1080
		{
1081
			countUserMentions(false, '', $member);
1082
		}
1083
	}
1084
1085
	/**
1086
	 * Check if the user has the necessary permissions to delete a post.
1087
	 *
1088
	 * @param array $row Details on the message
1089
	 * @param int $board The board ID
1090
	 *
1091
	 * @return bool|string 'topic' if the user has the necessary permissions and this is the only message in a topic
1092
	 *                     'exit' if the user cannot delete an unapproved message,
1093
	 *                      true if they have permission,
1094
	 *                      exception otherwise.
1095
	 * @throws Exception cannot_delete_replies, cannot_delete_own, modify_post_time_passed, cannot_delete_any, delFirstPost
1096
	 */
1097
	protected function _checkDeletePermissions($row, $board)
1098
	{
1099
		global $modSettings;
1100
1101
		$row['id_board'] = (int) $row['id_board'];
1102
		$row['id_member_poster'] = (int) $row['id_member_poster'];
1103
		$row['id_member'] = (int) $row['id_member'];
1104
		$row['id_first_msg'] = (int) $row['id_first_msg'];
1105
		$row['id_msg'] = (int) $row['id_msg'];
1106
		$board = (int) $board;
1107
1108
		if (empty($board) || $row['id_board'] !== $board)
1109
		{
1110
			$delete_any = boardsAllowedTo('delete_any');
1111
1112
			if (!in_array(0, $delete_any) && !in_array($row['id_board'], $delete_any))
1113
			{
1114
				$delete_own = boardsAllowedTo('delete_own');
1115
				$delete_own = in_array(0, $delete_own) || in_array($row['id_board'], $delete_own);
0 ignored issues
show
Bug introduced by
$delete_own of type true is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

1115
				$delete_own = in_array(0, /** @scrutinizer ignore-type */ $delete_own) || in_array($row['id_board'], $delete_own);
Loading history...
1116
				$delete_replies = boardsAllowedTo('delete_replies');
1117
				$delete_replies = in_array(0, $delete_replies) || in_array($row['id_board'], $delete_replies);
1118
1119
				// Their own checks
1120
				if ($row['id_member'] === $this->user->id)
0 ignored issues
show
Bug introduced by
The property id is declared protected in ElkArte\User and cannot be accessed from this context.
Loading history...
1121
				{
1122
					if (!$delete_own)
1123
					{
1124
						if ($row['id_member_poster'] === $this->user->id)
1125
						{
1126
							if (!$delete_replies)
1127
							{
1128
								throw new Exceptions\Exception('cannot_delete_replies', 'permission');
1129
							}
1130
						}
1131
						else
1132
						{
1133
							throw new Exceptions\Exception('cannot_delete_own', 'permission');
1134
						}
1135
					}
1136
					elseif (($row['id_member_poster'] !== $this->user->id || !$delete_replies) && !empty($modSettings['edit_disable_time']) && $row['poster_time'] + $modSettings['edit_disable_time'] * 60 < time())
1137
					{
1138
						throw new Exceptions\Exception('modify_post_time_passed', false);
1139
					}
1140
				}
1141
				elseif ($row['id_member_poster'] === $this->user->id)
1142
				{
1143
					if (!$delete_replies)
1144
					{
1145
						throw new Exceptions\Exception('cannot_delete_replies', 'permission');
1146
					}
1147
				}
1148
				else
1149
				{
1150
					throw new Exceptions\Exception('cannot_delete_any', 'permission');
1151
				}
1152
			}
1153
1154
			// Can't delete an unapproved message if you can't see it!
1155
			if ($modSettings['postmod_active'] && !$row['approved'] && $row['id_member'] != $this->user->id && (!in_array(0, $delete_any) && !in_array($row['id_board'], $delete_any)))
1156
			{
1157
				$approve_posts = empty($this->user->mod_cache['ap']) ? boardsAllowedTo('approve_posts') : $this->user->mod_cache['ap'];
0 ignored issues
show
Bug introduced by
The property mod_cache does not seem to exist on ElkArte\User.
Loading history...
1158
				if (!in_array(0, $approve_posts, true) && !in_array($row['id_board'], $approve_posts, true))
1159
				{
1160
					return 'exit';
1161
				}
1162
			}
1163
		}
1164
		else
1165
		{
1166
			// Check permissions to delete this message.
1167
			if ($row['id_member'] === $this->user->id)
1168
			{
1169
				if (!allowedTo('delete_own'))
1170
				{
1171
					if ($row['id_member_poster'] === $this->user->id && !allowedTo('delete_any'))
1172
					{
1173
						isAllowedTo('delete_replies');
1174
					}
1175
					elseif (!allowedTo('delete_any'))
1176
					{
1177
						isAllowedTo('delete_own');
1178
					}
1179
				}
1180
				elseif (!allowedTo('delete_any') && ($row['id_member_poster'] !== $this->user->id || !allowedTo('delete_replies')) && !empty($modSettings['edit_disable_time']) && $row['poster_time'] + $modSettings['edit_disable_time'] * 60 < time())
1181
				{
1182
					throw new Exceptions\Exception('modify_post_time_passed', false);
1183
				}
1184
			}
1185
			elseif ($row['id_member_poster'] === $this->user->id && !allowedTo('delete_any'))
1186
			{
1187
				isAllowedTo('delete_replies');
1188
			}
1189
			else
1190
			{
1191
				isAllowedTo('delete_any');
1192
			}
1193
1194
			if ($modSettings['postmod_active'] && !$row['approved'] && $row['id_member'] !== $this->user->id && !allowedTo('delete_own'))
1195
			{
1196
				isAllowedTo('approve_posts');
1197
			}
1198
		}
1199
1200
		// Delete the *whole* topic, but only if the topic consists of one message.
1201
		if ($row['id_first_msg'] === $row['id_msg'])
1202
		{
1203
			if (empty($board) || $row['id_board'] !== $board)
1204
			{
1205
				$remove_own = false;
1206
				$remove_any = boardsAllowedTo('remove_any');
1207
				$remove_any = in_array(0, $remove_any) || in_array($row['id_board'], $remove_any, true);
1208
1209
				if (!$remove_any)
1210
				{
1211
					$remove_own = boardsAllowedTo('remove_own');
1212
					$remove_own = in_array(0, $remove_own) || in_array($row['id_board'], $remove_own, true);
1213
				}
1214
1215
				if ($row['id_member'] !== $this->user->id && !$remove_any)
1216
				{
1217
					throw new Exceptions\Exception('cannot_remove_any', 'permission');
1218
				}
1219
1220
				if (!$remove_any && !$remove_own)
1221
				{
1222
					throw new Exceptions\Exception('cannot_remove_own', 'permission');
1223
				}
1224
			}
1225
			elseif ($row['id_member'] !== $this->user->id)
1226
			{
1227
				// Check permissions to delete a whole topic.
1228
				isAllowedTo('remove_any');
1229
			}
1230
			elseif (!allowedTo('remove_any'))
1231
			{
1232
				isAllowedTo('remove_own');
1233
			}
1234
1235
			// ...if there is only one post.
1236
			if (!empty($row['num_replies']))
1237
			{
1238
				throw new Exceptions\Exception('delFirstPost', false);
1239
			}
1240
1241
			return 'topic';
1242
		}
1243
1244
		return true;
1245
	}
1246
}
1247