MessagesDelete::__construct()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 3
ccs 0
cts 0
cp 0
crap 6
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 dev
15
 *
16
 */
17
18
namespace ElkArte;
19
20
use ElkArte\Exceptions\Exception;
21
use ElkArte\Helper\ValuesContainer;
22
23
/**
24
 * Class MessagesDelete
25
 *
26
 * Methods for deleting and restoring messages in boards
27
 */
28
class MessagesDelete
29
{
30
	/** @var ValuesContainer The current user deleting something */
31
	protected $user;
32
33
	/** @var int[] Id of the messages not found. */
34
	private $_unfound_messages = [];
35
36
	/** @var int[] Id of the topics that should be restored */
37
	private $_topics_to_restore = [];
38
39
	/** @var int The board id of the recycle board */
40
	private $_recycle_board;
41
42
	/**
43
	 * Initialize the class! :P
44
	 *
45
	 * @param int|bool $recycle_enabled if the recycling is enabled.
46
	 * @param int|null $recycle_board the id of the recycle board (if any)
47
	 * @param User $user the user::info making the request
48
	 */
49
	public function __construct($recycle_enabled, $recycle_board, $user = null)
50
	{
51
		$this->_recycle_board = $recycle_enabled ? (int) $recycle_board : null;
52
53
		$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\User. However, the property $user is declared as type ElkArte\Helper\ValuesContainer. 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...
54
	}
55
56
	/**
57
	 * Restores a bunch of messages the recycle bin to the appropriate board.
58
	 * If any "first message" is within the array, it is added to the list of
59
	 * topics to restore (see MessagesDelete::restoreTopics)
60
	 *
61
	 * @param int[] $msgs_id Messages to restore
62
	 *
63
	 * @return array|void
64
	 */
65
	public function restoreMessages($msgs_id)
66
	{
67
		$msgs = array();
68
		foreach ($msgs_id as $msg)
69
		{
70
			$msg = (int) $msg;
71
			if (!empty($msg))
72
			{
73
				$msgs[] = $msg;
74
			}
75
		}
76
77
		if (empty($msgs))
78
		{
79
			return;
80
		}
81
82
		$db = database();
83
84
		// Get the id_previous_board and id_previous_topic.
85
		$actioned_messages = [];
86
		$previous_topics = [];
87
		$db->fetchQuery('
88
			SELECT 
89
				m.id_topic, m.id_msg, m.id_board, m.subject, m.id_member, t.id_previous_board, t.id_previous_topic,
90
				t.id_first_msg, b.count_posts, COALESCE(pt.id_board, 0) AS possible_prev_board
91
			FROM {db_prefix}messages AS m
92
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
93
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
94
				LEFT JOIN {db_prefix}topics AS pt ON (pt.id_topic = t.id_previous_topic)
95
			WHERE m.id_msg IN ({array_int:messages})',
96
			array(
97
				'messages' => $msgs,
98
			)
99
		)->fetch_callback(
100
			function ($row) use (&$actioned_messages, &$previous_topics) {
101
				// Restoring the first post means topic.
102
				if ((int) $row['id_msg'] === (int) $row['id_first_msg'] && (int) $row['id_previous_topic'] === (int) $row['id_topic'])
103
				{
104
					$this->_topics_to_restore[] = $row['id_topic'];
105
					return;
106
				}
107
108
				// Don't know where it's going?
109
				if (empty($row['id_previous_topic']))
110
				{
111
					$this->_unfound_messages[$row['id_msg']] = $row['subject'];
112
					return;
113
				}
114
115
				$previous_topics[] = $row['id_previous_topic'];
116
				if (empty($actioned_messages[$row['id_previous_topic']]))
117
				{
118
					$actioned_messages[$row['id_previous_topic']] = array(
119
						'msgs' => array(),
120
						'count_posts' => (int) $row['count_posts'],
121
						'subject' => $row['subject'],
122
						'previous_board' => $row['id_previous_board'],
123
						'possible_prev_board' => $row['possible_prev_board'],
124
						'current_topic' => (int) $row['id_topic'],
125
						'current_board' => (int) $row['id_board'],
126
						'members' => array(),
127
					);
128
				}
129
130
				$actioned_messages[$row['id_previous_topic']]['msgs'][$row['id_msg']] = $row['subject'];
131
				if ($row['id_member'])
132
				{
133
					$actioned_messages[$row['id_previous_topic']]['members'][] = (int) $row['id_member'];
134
				}
135
			}
136
		);
137
138
		// Check for topics we are going to fully restore.
139
		foreach (array_keys($actioned_messages) as $topic)
140
		{
141
			if (in_array($topic, $this->_topics_to_restore))
142
			{
143
				unset($actioned_messages[$topic]);
144
			}
145
		}
146
147
		// Load any previous topics to check they exist.
148
		if (!empty($previous_topics))
149
		{
150
			$previous_topics = array();
151
			$db->fetchQuery('
152
				SELECT 
153
					t.id_topic, t.id_board, m.subject
154
				FROM {db_prefix}topics AS t
155
					INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
156
				WHERE t.id_topic IN ({array_int:previous_topics})',
157
				array(
158
					'previous_topics' => $previous_topics,
159
				)
160
			)->fetch_callback(
161
				static function ($row) use (&$previous_topics) {
162
					$previous_topics[$row['id_topic']] = array(
163
						'board' => $row['id_board'],
164
						'subject' => $row['subject'],
165
					);
166
				}
167
			);
168
		}
169
170
		// Restore each topic.
171
		$messages = array();
172
		foreach ($actioned_messages as $topic => $data)
173
		{
174
			// If we have topics we are going to restore the whole lot ignore them.
175
			if (in_array($topic, $this->_topics_to_restore))
176
			{
177
				unset($actioned_messages[$topic]);
178
				continue;
179
			}
180
181
			// Move the posts back then!
182
			if (isset($previous_topics[$topic]))
183
			{
184
				$this->_mergePosts(array_keys($data['msgs']), $data['current_topic'], $topic);
185
186
				// Log em.
187
				logAction('restore_posts', array('topic' => $topic, 'subject' => $previous_topics[$topic]['subject'], 'board' => empty($data['previous_board']) ? $data['possible_prev_board'] : $data['previous_board']));
188
				$messages = array_merge(array_keys($data['msgs']), $messages);
189
			}
190
			else
191
			{
192
				foreach ($data['msgs'] as $msg)
193
				{
194
					$this->_unfound_messages[$msg['id']] = $msg['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
				array(
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)
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 = array($msgs);
233
		}
234
235
		// Lets 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
			array(
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
			array(
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
		// Lets see if the board that we are returning to has post count enabled.
267
		if (empty($count_posts))
268
		{
269
			// Lets 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
				array(
277
					'messages' => $msgs,
278
					'is_approved' => 1,
279
				)
280
			)->fetch_callback(
281
				static function ($row) {
282
					updateMemberData($row['id_member'], array('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
			array(
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 = array(
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
			array(
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, array(
342
			'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.
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
			array(
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 = array(
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
				array(
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, array(
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, array(
418
				'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.
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, array(
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 = array();
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
				array(
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(array($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)
479
	{
480
		foreach ($topics_id as $topic)
481
		{
482
			$topic = (int) $topic;
483
			if (!empty($topic))
484
			{
485
				$this->_topics_to_restore[] = $topic;
486
			}
487
		}
488
	}
489
490
	/**
491
	 * Actually restore the topics previously "collected"
492
	 */
493
	public function doRestore()
494
	{
495
		if (empty($this->_topics_to_restore))
496
		{
497
			return;
498
		}
499
500
		$db = database();
501
502
		require_once(SUBSDIR . '/Boards.subs.php');
503
504
		// Lets get the data for these topics.
505
		$request = $db->fetchQuery('
506
			SELECT 
507
				t.id_topic, t.id_previous_board, t.id_board, t.id_first_msg, m.subject
508
			FROM {db_prefix}topics AS t
509
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
510
			WHERE t.id_topic IN ({array_int:topics})',
511
			array(
512
				'topics' => $this->_topics_to_restore,
513
			)
514
		);
515
		while (($row = $request->fetch_assoc()))
516
		{
517
			// We can only restore if the previous board is set.
518
			if (empty($row['id_previous_board']))
519
			{
520
				$this->_unfound_messages[$row['id_first_msg']] = $row['subject'];
521
				continue;
522
			}
523
524
			// Ok we got here so me move them from here to there.
525
			moveTopics($row['id_topic'], $row['id_previous_board']);
526
527
			// Lets remove the recycled icon.
528
			$db->query('', '
529
				UPDATE {db_prefix}messages
530
				SET icon = {string:icon}
531
				WHERE id_topic = {int:id_topic}',
532
				array(
533
					'icon' => 'xx',
534
					'id_topic' => $row['id_topic'],
535
				)
536
			);
537
538
			// Lets see if the board that we are returning to has post count enabled.
539
			$board_data = boardInfo($row['id_previous_board']);
540
541
			if (empty($board_data['count_posts']))
542
			{
543
				require_once(SUBSDIR . '/Members.subs.php');
544
545
				// Lets get the members that need their post count restored.
546
				$db->fetchQuery('
547
					SELECT
548
						id_member, COUNT(id_msg) AS post_count
549
					FROM {db_prefix}messages
550
					WHERE id_topic = {int:topic}
551
						AND approved = {int:is_approved}
552
					GROUP BY id_member',
553
					array(
554
						'topic' => $row['id_topic'],
555
						'is_approved' => 1,
556
					)
557
				)->fetch_callback(
558
					static function ($member) {
559
						updateMemberData($member['id_member'], array('posts' => 'posts + ' . $member['post_count']));
560
					}
561
				);
562
			}
563
564
			// Log it.
565
			logAction('restore_topic', array('topic' => $row['id_topic'], 'board' => $row['id_board'], 'board_to' => $row['id_previous_board']));
566
		}
567
568
		$request->free_result();
569
	}
570
571
	/**
572
	 * Returns either the list of messages not found, or the number
573
	 *
574
	 * @param bool $return_msg If true returns the array of not found messages,
575
	 *                         if false their number
576
	 * @return bool|int[]
577
	 */
578
	public function unfoundRestoreMessages($return_msg = false)
579
	{
580
		if ($return_msg)
581
		{
582
			return $this->_unfound_messages;
583
		}
584
585
		return !empty($this->_unfound_messages);
586
	}
587
588
	/**
589
	 * Remove a message from the forum.
590
	 *
591
	 * - If $check_permissions is true, and it is the first and only message in a topic, removes the topic
592
	 *
593
	 * @param int $message The ID of the message to be removed
594
	 * @param bool $decreasePostCount Whether to decrease the post count or not (default: true)
595
	 * @param bool $check_permissions Whether to check permissions or not (default: true) (may result in fatal
596
	 *             errors or login screens)
597
	 *
598
	 * @return bool Whether the message was successfully removed or not
599
	 */
600
	public function removeMessage($message, $decreasePostCount = true, $check_permissions = true)
601
	{
602
		global $board, $modSettings;
603
604
		$db = database();
605
606
		$message = (int) $message;
607
608
		if (empty($message))
609
		{
610
			return false;
611
		}
612
613
		$row = $db->fetchQuery('
614
			SELECT
615
				m.id_msg, m.id_member, m.icon, m.poster_time, m.subject,' . (empty($modSettings['search_custom_index_config']) ? '' : ' m.body,') . '
616
				m.approved, t.id_topic, t.id_first_msg, t.id_last_msg, t.num_replies, t.id_board,
617
				t.id_member_started AS id_member_poster,
618
				b.count_posts
619
			FROM {db_prefix}messages AS m
620
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
621
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
622
			WHERE m.id_msg = {int:id_msg}
623
			LIMIT 1',
624
			array(
625
				'id_msg' => $message,
626
			)
627
		)->fetch_assoc();
628
629
		if (empty($row))
630
		{
631
			return false;
632
		}
633
634
		if ($check_permissions)
635
		{
636
			$check = $this->_checkDeletePermissions($row, $board);
637
			if ($check === 'topic')
638
			{
639
				// This needs to be included for topic functions
640
				require_once(SUBSDIR . '/Topic.subs.php');
641
				removeTopics($row['id_topic']);
642
643
				return true;
644
			}
645
646
			if ($check === 'exit')
647
			{
648
				return false;
649
			}
650
		}
651
652
		// Deleting a recycled message can not lower anyone's post count.
653
		if ($row['icon'] === 'recycled')
654
		{
655
			$decreasePostCount = false;
656
		}
657
658
		// This is the last post, update the last post on the board.
659
		if ((int) $row['id_last_msg'] === $message)
660
		{
661
			// Find the last message, set it, and decrease the post count.
662
			$row2 = $db->fetchQuery('
663
				SELECT 
664
					id_msg, id_member
665
				FROM {db_prefix}messages
666
				WHERE id_topic = {int:id_topic}
667
					AND id_msg != {int:id_msg}
668
				ORDER BY ' . ($modSettings['postmod_active'] ? 'approved DESC, ' : '') . 'id_msg DESC
669
				LIMIT 1',
670
				array(
671
					'id_topic' => $row['id_topic'],
672
					'id_msg' => $message,
673
				)
674
			)->fetch_assoc();
675
676
			$db->query('', '
677
				UPDATE {db_prefix}topics
678
				SET
679
					id_last_msg = {int:id_last_msg},
680
					id_member_updated = {int:id_member_updated}' . (!$modSettings['postmod_active'] || $row['approved'] ? ',
681
					num_replies = CASE WHEN num_replies = {int:no_replies} THEN 0 ELSE num_replies - 1 END' : ',
682
					unapproved_posts = CASE WHEN unapproved_posts = {int:no_unapproved} THEN 0 ELSE unapproved_posts - 1 END') . '
683
				WHERE id_topic = {int:id_topic}',
684
				array(
685
					'id_last_msg' => (int) $row2['id_msg'],
686
					'id_member_updated' => (int) $row2['id_member'],
687
					'no_replies' => 0,
688
					'no_unapproved' => 0,
689
					'id_topic' => (int) $row['id_topic'],
690
				)
691
			);
692
		}
693
		// Only decrease post counts.
694
		else
695
		{
696
			$db->query('', '
697
				UPDATE {db_prefix}topics
698
				SET ' . ($row['approved'] ? '
699
					num_replies = CASE WHEN num_replies = {int:no_replies} THEN 0 ELSE num_replies - 1 END' : '
700
					unapproved_posts = CASE WHEN unapproved_posts = {int:no_unapproved} THEN 0 ELSE unapproved_posts - 1 END') . '
701
				WHERE id_topic = {int:id_topic}',
702
				array(
703
					'no_replies' => 0,
704
					'no_unapproved' => 0,
705
					'id_topic' => (int) $row['id_topic'],
706
				)
707
			);
708
		}
709
710
		// Default recycle to false.
711
		$recycle = false;
712
713
		// If recycle topics has been set, make a copy of this message in the recycle board.
714
		// Make sure we're not recycling messages that are already on the recycle board.
715
		if (!empty($this->_recycle_board) && $row['id_board'] != $this->_recycle_board && $row['icon'] !== 'recycled')
716
		{
717
			// Check if the recycle board exists and if so get the read status.
718
			$request = $db->query('', '
719
				SELECT 
720
					(COALESCE(lb.id_msg, 0) >= b.id_msg_updated) AS is_seen, id_last_msg
721
				FROM {db_prefix}boards AS b
722
					LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
723
				WHERE b.id_board = {int:recycle_board}',
724
				array(
725
					'current_member' => $this->user->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
726
					'recycle_board' => $this->_recycle_board,
727
				)
728
			);
729
730
			if ($request->num_rows() === 0)
731
			{
732
				throw new Exceptions\Exception('recycle_no_valid_board');
733
			}
734
735
			[$isRead, $last_board_msg] = $request->fetch_row();
736
			$request->free_result();
737
738
			// Is there an existing topic in the recycle board to group this post with?
739
			$request = $db->query('', '
740
				SELECT 
741
					id_topic, id_first_msg, id_last_msg
742
				FROM {db_prefix}topics
743
				WHERE id_previous_topic = {int:id_previous_topic}
744
					AND id_board = {int:recycle_board}',
745
				array(
746
					'id_previous_topic' => $row['id_topic'],
747
					'recycle_board' => $this->_recycle_board,
748
				)
749
			);
750
			[$id_recycle_topic, $first_topic_msg, $last_topic_msg] = $request->fetch_row();
751
			$request->free_result();
752
753
			// Insert a new topic in the recycle board if $id_recycle_topic is empty.
754
			if (empty($id_recycle_topic))
755
			{
756
				$insert_res = $db->insert('',
757
					'{db_prefix}topics',
758
					array(
759
						'id_board' => 'int', 'id_member_started' => 'int', 'id_member_updated' => 'int', 'id_first_msg' => 'int',
760
						'id_last_msg' => 'int', 'unapproved_posts' => 'int', 'approved' => 'int', 'id_previous_topic' => 'int',
761
					),
762
					array(
763
						$this->_recycle_board, $row['id_member'], $row['id_member'], $message,
764
						$message, 0, 1, $row['id_topic'],
765
					),
766
					array('id_topic')
767
				);
768
				$topicID = $insert_res->insert_id();
769
			}
770
			else
771
			{
772
				// Capture the ID of the new topic...
773
				$topicID = $id_recycle_topic;
774
			}
775
776
			// If the topic creation went successful, move the message.
777
			if ($topicID > 0)
778
			{
779
				$db->query('', '
780
					UPDATE {db_prefix}messages
781
					SET
782
						id_topic = {int:id_topic},
783
						id_board = {int:recycle_board},
784
						icon = {string:recycled},
785
						approved = {int:is_approved}
786
					WHERE id_msg = {int:id_msg}',
787
					array(
788
						'id_topic' => $topicID,
789
						'recycle_board' => $this->_recycle_board,
790
						'id_msg' => $message,
791
						'recycled' => 'recycled',
792
						'is_approved' => 1,
793
					)
794
				);
795
796
				// Take any reported posts with us...
797
				$db->query('', '
798
					UPDATE {db_prefix}log_reported
799
					SET
800
						id_topic = {int:id_topic},
801
						id_board = {int:recycle_board}
802
					WHERE id_msg = {int:id_msg}
803
						AND type = {string:msg}',
804
					array(
805
						'id_topic' => $topicID,
806
						'recycle_board' => $this->_recycle_board,
807
						'id_msg' => $message,
808
						'msg' => 'msg',
809
					)
810
				);
811
812
				// Mark recycled topic as read.
813
				if ($this->user->is_guest === false)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
814
				{
815
					require_once(SUBSDIR . '/Topic.subs.php');
816
					markTopicsRead(array($this->user->id, $topicID, $modSettings['maxMsgID'], 0), true);
817
				}
818
819
				// Mark recycle board as seen, if it was marked as seen before.
820
				if (!empty($isRead) && $this->user->is_guest === false)
821
				{
822
					require_once(SUBSDIR . '/Boards.subs.php');
823
					markBoardsRead($this->_recycle_board);
824
				}
825
826
				// Add one topic and post to the recycle bin board.
827
				$db->query('', '
828
					UPDATE {db_prefix}boards
829
					SET
830
						num_topics = num_topics + {int:num_topics_inc},
831
						num_posts = num_posts + 1' .
832
					($message > $last_board_msg ? ', id_last_msg = {int:id_merged_msg}' : '') . '
833
					WHERE id_board = {int:recycle_board}',
834
					array(
835
						'num_topics_inc' => empty($id_recycle_topic) ? 1 : 0,
836
						'recycle_board' => $this->_recycle_board,
837
						'id_merged_msg' => $message,
838
					)
839
				);
840
841
				// Lets increase the num_replies, and the first/last message ID as appropriate.
842
				if (!empty($id_recycle_topic))
843
				{
844
					$db->query('', '
845
						UPDATE {db_prefix}topics
846
						SET num_replies = num_replies + 1' .
847
						($message > $last_topic_msg ? ', id_last_msg = {int:id_merged_msg}' : '') .
848
						($message < $first_topic_msg ? ', id_first_msg = {int:id_merged_msg}' : '') . '
849
						WHERE id_topic = {int:id_recycle_topic}',
850
						array(
851
							'id_recycle_topic' => $id_recycle_topic,
852
							'id_merged_msg' => $message,
853
						)
854
					);
855
				}
856
857
				// Update mentions accessibility
858
				$this->deleteMessageMentions(messagesInTopics($topicID), true);
859
860
				// Make sure this message isn't getting deleted later on.
861
				$recycle = true;
862
863
				// Make sure we update the search subject index.
864
				require_once(SUBSDIR . '/Messages.subs.php');
865
				updateSubjectStats($topicID, $row['subject']);
866
			}
867
868
			// If it wasn't approved don't keep it in the queue.
869
			if (!$row['approved'])
870
			{
871
				$db->query('', '
872
					DELETE FROM {db_prefix}approval_queue
873
					WHERE id_msg = {int:id_msg}
874
						AND id_attach = {int:id_attach}',
875
					array(
876
						'id_msg' => $message,
877
						'id_attach' => 0,
878
					)
879
				);
880
			}
881
		}
882
883
		$db->query('', '
884
			UPDATE {db_prefix}boards
885
			SET ' . ($row['approved'] ? '
886
				num_posts = CASE WHEN num_posts = {int:no_posts} THEN 0 ELSE num_posts - 1 END' : '
887
				unapproved_posts = CASE WHEN unapproved_posts = {int:no_unapproved} THEN 0 ELSE unapproved_posts - 1 END') . '
888
			WHERE id_board = {int:id_board}',
889
			array(
890
				'no_posts' => 0,
891
				'no_unapproved' => 0,
892
				'id_board' => $row['id_board'],
893
			)
894
		);
895
896
		// If the poster was registered and the board this message was on incremented
897
		// the member's posts when it was posted, decrease his or her post count.
898
		if (!empty($row['id_member']) && $decreasePostCount && empty($row['count_posts']) && $row['approved'])
899
		{
900
			require_once(SUBSDIR . '/Members.subs.php');
901
			updateMemberData($row['id_member'], array('posts' => '-'));
902
		}
903
904
		// Only remove posts if they're not recycled.
905
		if (!$recycle)
906
		{
907
			// Update the like counts
908
			require_once(SUBSDIR . '/Likes.subs.php');
909
			decreaseLikeCounts($message);
910
911
			// Remove the likes!
912
			$db->query('', '
913
				DELETE FROM {db_prefix}message_likes
914
				WHERE id_msg = {int:id_msg}',
915
				array(
916
					'id_msg' => $message,
917
				)
918
			);
919
920
			// Remove the mentions!
921
			$this->deleteMessageMentions($message);
922
923
			// Remove the message!
924
			$db->query('', '
925
				DELETE FROM {db_prefix}messages
926
				WHERE id_msg = {int:id_msg}',
927
				array(
928
					'id_msg' => $message,
929
				)
930
			);
931
932
			if (!empty($modSettings['search_custom_index_config']))
933
			{
934
				$words = text2words($row['body'], true);
935
				if (!empty($words))
936
				{
937
					$db->query('', '
938
						DELETE FROM {db_prefix}log_search_words
939
						WHERE id_word IN ({array_int:word_list})
940
							AND id_msg = {int:id_msg}',
941
						array(
942
							'word_list' => $words,
943
							'id_msg' => $message,
944
						)
945
					);
946
				}
947
			}
948
949
			// Delete attachment(s) if they exist.
950
			require_once(SUBSDIR . '/ManageAttachments.subs.php');
951
			$attachmentQuery = array(
952
				'attachment_type' => 0,
953
				'id_msg' => $message,
954
			);
955
			removeAttachments($attachmentQuery);
956
957
			// Delete follow-ups too
958
			require_once(SUBSDIR . '/FollowUps.subs.php');
959
960
			// If it is an entire topic
961
			if ((int) $row['id_first_msg'] === (int) $message)
962
			{
963
				$db->query('', '
964
					DELETE FROM {db_prefix}follow_ups
965
					WHERE follow_ups IN ({array_int:topics})',
966
					array(
967
						'topics' => $row['id_topic'],
968
					)
969
				);
970
			}
971
972
			// Allow mods to remove message related data of their own (likes, maybe?)
973
			call_integration_hook('integrate_remove_message', array($message));
974
		}
975
976
		// Update the pesky statistics.
977
		updateMessageStats();
978
		require_once(SUBSDIR . '/Topic.subs.php');
979
		updateTopicStats();
980
		updateSettings(['calendar_updated' => time(),]);
981
982
		// And now to update the last message of each board we messed with.
983
		require_once(SUBSDIR . '/Post.subs.php');
984
		if ($recycle)
985
		{
986
			updateLastMessages([$row['id_board'], $this->_recycle_board]);
987
		}
988
		else
989
		{
990
			updateLastMessages($row['id_board']);
991
		}
992
993
		// Close any moderation reports for this message.
994
		require_once(SUBSDIR . '/Moderation.subs.php');
995
		$updated_reports = updateReportsStatus($message, 'close', 1);
996
		if ($updated_reports != 0)
997
		{
998
			updateSettings(array('last_mod_report_action' => time()));
999
			recountOpenReports(true);
1000
		}
1001
1002
		// Add it to the mod log.
1003
		if (!allowedTo('delete_any'))
1004
		{
1005
			return false;
1006
		}
1007
1008
		if (allowedTo('delete_own') && $row['id_member'] == $this->user->id)
1009
		{
1010
			return false;
1011
		}
1012
1013
		logAction('delete', array('topic' => $row['id_topic'], 'subject' => $row['subject'], 'member' => $row['id_member'], 'board' => $row['id_board']));
1014
		return false;
1015
	}
1016
1017
	/**
1018
	 * When a message is removed, we need to remove associated mentions and updated the member
1019
	 * mention count for anyone was mentioned in that message (like, quote, @, etc)
1020
	 *
1021
	 * @param int|int[] $messages
1022
	 * @param bool $recycle If recycle board is enabled, sets mentions as in_accessible, otherwise hard delete
1023
	 */
1024
	public function deleteMessageMentions($messages, $recycle = false)
1025
	{
1026
		$db = database();
1027
1028
		$mentionTypes = ['mentionmem', 'likemsg', 'rlikemsg', 'quotedmem', 'watchedtopic', 'watchedboard'];
1029
		$messages = is_array($messages) ? $messages : [$messages];
1030
		$changeMe = [];
1031
1032
		// Who was mentioned in these messages
1033
		$db->fetchQuery('
1034
			SELECT 
1035
				DISTINCT(id_member) as id_member
1036
			FROM {db_prefix}log_mentions
1037
			WHERE id_target IN ({array_int:messages})
1038
				AND mention_type IN ({array_string:mention_types})',
1039
			[
1040
				'messages' => $messages,
1041
				'mention_types' => $mentionTypes,
1042
			]
1043
		)->fetch_callback(
1044
			function ($row) use (&$changeMe) {
1045
				$changeMe[] = (int) $row['id_member'];
1046
			}
1047
		);
1048
1049
		if ($recycle === true)
1050
		{
1051
			// Set them as not accessible when they are going to the trashcan
1052
			$db->query('', '
1053
				UPDATE {db_prefix}log_mentions
1054
				SET
1055
					is_accessible = 0
1056
				WHERE id_target IN ({array_int:messages})
1057
					AND is_accessible = 1',
1058
				[
1059
					'messages' => $messages,
1060
					'mention_types' => $mentionTypes,
1061
				]
1062
			);
1063
		}
1064
		else
1065
		{
1066
			// This message is really gone, so remove the mentions!
1067
			$db->query('', '
1068
				DELETE FROM {db_prefix}log_mentions
1069
				WHERE id_target IN ({array_int:messages})
1070
					AND mention_type IN ({array_string:mention_types})',
1071
				[
1072
					'messages' => $messages,
1073
					'mention_types' => $mentionTypes,
1074
				]
1075
			);
1076
		}
1077
1078
		// Update the mention count for this group
1079
		require_once(SUBSDIR . '/Mentions.subs.php');
1080
		foreach ($changeMe as $member)
1081
		{
1082
			countUserMentions(false, '', $member);
1083
		}
1084
	}
1085
1086
	/**
1087
	 * Check if the user has the necessary permissions to delete a post.
1088
	 *
1089
	 * @param array $row Details on the message
1090
	 * @param int $board The board ID
1091
	 *
1092
	 * @return bool|string 'topic' if the user has the necessary permissions and this is the only message in a topic
1093
	 *                     'exit' if the user cannot delete an unapproved message,
1094
	 *                      true if they have permission,
1095
	 *                      exception otherwise.
1096
	 * @throws Exception cannot_delete_replies, cannot_delete_own, modify_post_time_passed, cannot_delete_any, delFirstPost
1097
	 */
1098
	protected function _checkDeletePermissions($row, $board)
1099
	{
1100
		global $modSettings;
1101
1102
		$row['id_board'] = (int) $row['id_board'];
1103
		$row['id_member_poster'] = (int) $row['id_member_poster'];
1104
		$row['id_member'] = (int) $row['id_member'];
1105
		$row['id_first_msg'] = (int) $row['id_first_msg'];
1106
		$row['id_msg'] = (int) $row['id_msg'];
1107
		$board = (int) $board;
1108
1109
		if (empty($board) || $row['id_board'] !== $board)
1110
		{
1111
			$delete_any = boardsAllowedTo('delete_any');
1112
1113
			if (!in_array(0, $delete_any) && !in_array($row['id_board'], $delete_any))
1114
			{
1115
				$delete_own = boardsAllowedTo('delete_own');
1116
				$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

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