MessagesDelete::restoreMessages()   F
last analyzed

Complexity

Conditions 19
Paths 183

Size

Total Lines 151
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 380

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 19
eloc 63
c 1
b 1
f 0
nc 183
nop 1
dl 0
loc 151
rs 3.825
ccs 0
cts 116
cp 0
crap 380

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

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