splitAttemptMove()   B
last analyzed

Complexity

Conditions 10
Paths 10

Size

Total Lines 73
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
eloc 30
nc 10
nop 2
dl 0
loc 73
ccs 0
cts 27
cp 0
crap 110
rs 7.6666
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 file contains functions for dealing with topics. Low-level functions,
5
 * i.e. database operations needed to perform.
6
 * These functions do NOT make permissions checks. (they assume those were
7
 * already made).
8
 *
9
 * @package   ElkArte Forum
10
 * @copyright ElkArte Forum contributors
11
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
12
 *
13
 * This file contains code covered by:
14
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
15
 *
16
 * @version 2.0 dev
17
 *
18
 */
19
20
use BBC\ParserWrapper;
21
use ElkArte\Cache\Cache;
22
use ElkArte\Helper\Util;
23
use ElkArte\Languages\Loader as LangLoader;
24
use ElkArte\MessagesDelete;
25
use ElkArte\Search\SearchApiWrapper;
26
use ElkArte\User;
27
28
/**
29
 * Removes the passed id_topic's checking for permissions.
30
 *
31
 * @param int[]|int $topics The topics to remove (can be an id or an array of ids).
32
 */
33
function removeTopicsPermissions($topics)
34
{
35
	global $board;
36
37
	// They can only delete their own topics. (we wouldn't be here if they couldn't do that..)
38
	$possible_remove = topicAttribute($topics, ['id_topic', 'id_board', 'id_member_started']);
39
40
	$removeCache = [];
41
	$removeCacheBoards = [];
42
	$test_owner = !empty($board) && !allowedTo('remove_any');
43
	foreach ($possible_remove as $row)
44
	{
45
		// Skip if we have to test the owner *and* the user is not the owner
46
		if ($test_owner && $row['id_member_started'] != User::$info->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...
47
		{
48
			continue;
49
		}
50
51
		$removeCache[] = $row['id_topic'];
52
		$removeCacheBoards[$row['id_topic']] = $row['id_board'];
53
	}
54
55
	// Maybe *none* were their own topics.
56
	if (!empty($removeCache))
57
	{
58
		removeTopics($removeCache, true, false, true, $removeCacheBoards);
59
	}
60
}
61
62
/**
63
 * Removes the passed id_topic's.
64
 *
65
 * - Permissions are NOT checked here because the function is used in a scheduled task
66
 *
67
 * @param int[]|int $topics The topics to remove (can be an id or an array of ids).
68
 * @param bool $decreasePostCount if true users' post count will be reduced
69
 * @param bool $ignoreRecycling if true topics are not moved to the recycle board (if it exists).
70
 * @param bool $log if true logs the action.
71
 * @param int[] $removeCacheBoards an array matching topics and boards.
72
 */
73
function removeTopics($topics, $decreasePostCount = true, $ignoreRecycling = false, $log = false, $removeCacheBoards = [])
74
{
75
	global $modSettings;
76 12
77
	// Nothing to do?
78
	if (empty($topics))
79 12
	{
80
		return;
81
	}
82
83
	$db = database();
84 12
	$cache = Cache::instance();
85 12
86
	// Only a single topic.
87
	if (!is_array($topics))
88 12
	{
89
		$topics = [$topics];
90 12
	}
91
92
	if ($log)
93 12
	{
94
		// Gotta send the notifications *first*!
95
		foreach ($topics as $topic)
96
		{
97
			// Only log the topic ID if it's not in the recycle board.
98
			logAction('remove', [(empty($modSettings['recycle_enable']) || $modSettings['recycle_board'] != $removeCacheBoards[$topic] ? 'topic' : 'old_topic_id') => $topic, 'board' => $removeCacheBoards[$topic]]);
99
			sendNotifications($topic, 'remove');
100
		}
101
	}
102
103
	// Decrease the post counts for members.
104
	if ($decreasePostCount)
105 12
	{
106
		$requestMembers = $db->query('', '
107 12
			SELECT 
108
				m.id_member, COUNT(*) AS posts
109
			FROM {db_prefix}messages AS m
110
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
111
			WHERE m.id_topic IN ({array_int:topics})
112
				AND m.icon != {string:recycled}
113
				AND b.count_posts = {int:do_count_posts}
114
				AND m.approved = {int:is_approved}
115
			GROUP BY m.id_member',
116
			[
117
				'do_count_posts' => 0,
118 12
				'recycled' => 'recycled',
119 12
				'topics' => $topics,
120 12
				'is_approved' => 1,
121 12
			]
122
		);
123
		if ($requestMembers->num_rows() > 0)
124 12
		{
125
			require_once(SUBSDIR . '/Members.subs.php');
126 12
			while (($rowMembers = $requestMembers->fetch_assoc()))
127 12
			{
128
				updateMemberData($rowMembers['id_member'], ['posts' => 'posts - ' . $rowMembers['posts']]);
129 12
			}
130
		}
131
		$requestMembers->free_result();
132 12
	}
133
134
	// Recycle topics that aren't in the recycle board...
135
	if (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 && !$ignoreRecycling)
136 12
	{
137
		$possible_recycle = topicAttribute($topics, ['id_topic', 'id_board', 'unapproved_posts', 'approved']);
138
139
		if (!empty($possible_recycle))
140
		{
141
			detectServer()->setTimeLimit(300);
142
143
			// Get topics that will be recycled.
144
			$recycleTopics = [];
145
			foreach ($possible_recycle as $row)
146
			{
147
				// If it's already in the recycle board do nothing
148
				if ($row['id_board'] == $modSettings['recycle_board'])
149
				{
150
					continue;
151
				}
152
153
				$recycleTopics[] = $row['id_topic'];
154
155
				// Set the id_previous_board for this topic - and make it not sticky.
156
				setTopicAttribute($row['id_topic'], [
157
					'id_previous_board' => $row['id_board'],
158
					'is_sticky' => 0,
159
				]);
160
			}
161
162
			if (!empty($recycleTopics))
163
			{
164
				// Mark recycled topics as recycled.
165
				$db->query('', '
166
				UPDATE {db_prefix}messages
167
				SET 
168
					icon = {string:recycled}
169
				WHERE id_topic IN ({array_int:recycle_topics})',
170
					[
171
						'recycle_topics' => $recycleTopics,
172
						'recycled' => 'recycled',
173
					]
174
				);
175
176
				// Move the topics to the recycle board.
177
				require_once(SUBSDIR . '/Topic.subs.php');
178
				moveTopics($recycleTopics, $modSettings['recycle_board']);
179
180
				// Close reports that are being recycled.
181
				require_once(SUBSDIR . '/Moderation.subs.php');
182
183
				$db->query('', '
184
				UPDATE {db_prefix}log_reported
185
				SET 
186
					closed = {int:is_closed}
187
				WHERE id_topic IN ({array_int:recycle_topics})',
188
					[
189
						'recycle_topics' => $recycleTopics,
190
						'is_closed' => 1,
191
					]
192
				);
193
194
				updateSettings(['last_mod_report_action' => time()]);
195
				recountOpenReports();
196
197
				// Make in-accessible any topic/message mentions
198
				$remover = new MessagesDelete($modSettings['recycle_enable'], $modSettings['recycle_board']);
199
				$remover->deleteMessageMentions(messagesInTopics($recycleTopics), true);
200
201
				// Topics that were recycled don't need to be deleted, so subtract them.
202
				$topics = array_diff($topics, $recycleTopics);
203
			}
204
		}
205 12
	}
206
207
	// Still topics left to delete?
208
	if (empty($topics))
209
	{
210 12
		return;
211
	}
212
213 12
	$adjustBoards = [];
214
215
	// Find out how many posts we are deleting.
216
	$db->fetchQuery('
217
		SELECT 
218
			id_board, approved, COUNT(*) AS num_topics, SUM(unapproved_posts) AS unapproved_posts,
219
			SUM(num_replies) AS num_replies
220
		FROM {db_prefix}topics
221 12
		WHERE id_topic IN ({array_int:topics})
222
		GROUP BY id_board, approved',
223 12
		[
224
			'topics' => $topics,
225 12
		]
226
	)->fetch_callback(
227 12
		function ($row) use (&$adjustBoards, $cache) {
228
			if (!isset($adjustBoards[$row['id_board']]['num_posts']))
229 12
			{
230 12
				$cache->remove('board-' . $row['id_board']);
231 12
232 12
				$adjustBoards[$row['id_board']] = [
233 12
					'num_posts' => 0,
234 12
					'num_topics' => 0,
235
					'unapproved_posts' => 0,
236
					'unapproved_topics' => 0,
237
					'id_board' => $row['id_board']
238 12
				];
239 12
			}
240
			// Posts = (num_replies + 1) for each approved topic.
241
			$adjustBoards[$row['id_board']]['num_posts'] += $row['num_replies'] + ($row['approved'] ? $row['num_topics'] : 0);
242 12
			$adjustBoards[$row['id_board']]['unapproved_posts'] += $row['unapproved_posts'];
243
244 12
			// Add the topics to the right type.
245
			if ($row['approved'])
246
			{
247
				$adjustBoards[$row['id_board']]['num_topics'] += $row['num_topics'];
248
			}
249
			else
250 12
			{
251
				$adjustBoards[$row['id_board']]['unapproved_topics'] += $row['num_topics'];
252
			}
253
		}
254 12
	);
255 12
256
	// Decrease number of posts and topics for each board.
257 12
	detectServer()->setTimeLimit(300);
258
	foreach ($adjustBoards as $stats)
259
	{
260
		$db->query('', '
261
			UPDATE {db_prefix}boards
262
			SET
263
				num_posts = CASE WHEN {int:num_posts} > num_posts THEN 0 ELSE num_posts - {int:num_posts} END,
264
				num_topics = CASE WHEN {int:num_topics} > num_topics THEN 0 ELSE num_topics - {int:num_topics} END,
265
				unapproved_posts = CASE WHEN {int:unapproved_posts} > unapproved_posts THEN 0 ELSE unapproved_posts - {int:unapproved_posts} END,
266 12
				unapproved_topics = CASE WHEN {int:unapproved_topics} > unapproved_topics THEN 0 ELSE unapproved_topics - {int:unapproved_topics} END
267 12
			WHERE id_board = {int:id_board}',
268 12
			[
269 12
				'id_board' => $stats['id_board'],
270 12
				'num_posts' => $stats['num_posts'],
271
				'num_topics' => $stats['num_topics'],
272
				'unapproved_posts' => $stats['unapproved_posts'],
273
				'unapproved_topics' => $stats['unapproved_topics'],
274
			]
275
		);
276 12
	}
277 12
278 12
	// Remove polls for these topics.
279
	$possible_polls = topicAttribute($topics, 'id_poll');
280 12
	$polls = [];
281
	foreach ($possible_polls as $row)
282 9
	{
283
		if (!empty($row['id_poll']))
284
		{
285
			$polls[] = $row['id_poll'];
286 12
		}
287
	}
288 6
289
	if (!empty($polls))
290
	{
291
		$db->query('', '
292 6
			DELETE FROM {db_prefix}polls
293
			WHERE id_poll IN ({array_int:polls})',
294
			[
295 6
				'polls' => $polls,
296
			]
297
		);
298
		$db->query('', '
299 6
			DELETE FROM {db_prefix}poll_choices
300
			WHERE id_poll IN ({array_int:polls})',
301
			[
302 6
				'polls' => $polls,
303
			]
304
		);
305
		$db->query('', '
306 6
			DELETE FROM {db_prefix}log_polls
307
			WHERE id_poll IN ({array_int:polls})',
308
			[
309
				'polls' => $polls,
310
			]
311
		);
312 12
	}
313
314 12
	// Get rid of the attachment(s).
315 12
	require_once(SUBSDIR . '/ManageAttachments.subs.php');
316
	$attachmentQuery = [
317 12
		'attachment_type' => 0,
318
		'id_topic' => $topics,
319
	];
320 12
	removeAttachments($attachmentQuery, 'messages');
321
322
	// Delete search index entries.
323
	if (!empty($modSettings['search_custom_index_config']))
324
	{
325
		$words = [];
326
		$messages = [];
327
		$db->fetchQuery('
328
			SELECT 
329
				id_msg, body
330
			FROM {db_prefix}messages
331
			WHERE id_topic IN ({array_int:topics})',
332
			[
333
				'topics' => $topics,
334
			]
335
		)->fetch_callback(
336
			function ($row) use (&$words, &$messages) {
337
				detectServer()->setTimeLimit(300);
338
339
				$words = array_merge($words, text2words($row['body'], true));
340
				$messages[] = $row['id_msg'];
341
			}
342
		);
343
		$words = array_unique($words);
344
345
		if (!empty($words) && !empty($messages))
346
		{
347
			$db->query('', '
348
				DELETE FROM {db_prefix}log_search_words
349
				WHERE id_word IN ({array_int:word_list})
350
					AND id_msg IN ({array_int:message_list})',
351
				[
352
					'word_list' => $words,
353
					'message_list' => $messages,
354
				]
355
			);
356
		}
357
	}
358
359 12
	// Reuse the message array if available
360
	if (empty($messages))
361 12
	{
362
		$messages = messagesInTopics($topics);
363
	}
364
365 12
	// If there are messages left in this topic
366
	if (!empty($messages))
367
	{
368 12
		// Decrease / Update the member like counts
369 12
		require_once(SUBSDIR . '/Likes.subs.php');
370
		decreaseLikeCounts($messages);
371
372 12
		// Remove all likes now that the topic is gone
373
		$db->query('', '
374
			DELETE FROM {db_prefix}message_likes
375
			WHERE id_msg IN ({array_int:messages})',
376 12
			[
377
				'messages' => $messages,
378
			]
379
		);
380
381 12
		// Remove all message mentions now that the topic is gone
382
		$remover = new MessagesDelete($modSettings['recycle_enable'], $modSettings['recycle_board']);
383
		$remover->deleteMessageMentions($messages);
384
	}
385
386 12
	// Delete messages in each topic.
387
	$db->query('', '
388
		DELETE FROM {db_prefix}messages
389
		WHERE id_topic IN ({array_int:topics})',
390
		[
391
			'topics' => $topics,
392
		]
393 12
	);
394
395
	// Remove linked calendar events.
396
	// @todo if unlinked events are enabled, wouldn't this be expected to keep them?
397 12
	$db->query('', '
398
		DELETE FROM {db_prefix}calendar
399
		WHERE id_topic IN ({array_int:topics})',
400
		[
401
			'topics' => $topics,
402
		]
403 12
	);
404
405
	// Delete log_topics data
406
	$db->query('', '
407 12
		DELETE FROM {db_prefix}log_topics
408
		WHERE id_topic IN ({array_int:topics})',
409
		[
410
			'topics' => $topics,
411
		]
412 12
	);
413
414
	// Delete notifications
415
	$db->query('', '
416 12
		DELETE FROM {db_prefix}log_notify
417
		WHERE id_topic IN ({array_int:topics})',
418
		[
419
			'topics' => $topics,
420
		]
421 12
	);
422
423
	// Delete the topics themselves
424
	$db->query('', '
425 12
		DELETE FROM {db_prefix}topics
426
		WHERE id_topic IN ({array_int:topics})',
427
		[
428
			'topics' => $topics,
429
		]
430 12
	);
431
432
	// Remove data from the subjects for search cache
433
	$db->query('', '
434 12
		DELETE FROM {db_prefix}log_search_subjects
435
		WHERE id_topic IN ({array_int:topics})',
436
		[
437
			'topics' => $topics,
438
		]
439 12
	);
440
	require_once(SUBSDIR . '/FollowUps.subs.php');
441
	removeFollowUpsByTopic($topics);
442
443 12
	foreach ($topics as $topic_id)
444
	{
445
		$cache->remove('topic_board-' . $topic_id);
446 12
	}
447 12
448
	// Maybe there's an addon that wants to delete topic related data of its own
449 12
	call_integration_hook('integrate_remove_topics', [$topics]);
450
451 12
	// Update the totals...
452
	require_once(SUBSDIR . '/Messages.subs.php');
453
	updateMessageStats();
454
	updateTopicStats();
455 12
	updateSettings([
456
		'calendar_updated' => time(),
457
	]);
458 12
459 12
	require_once(SUBSDIR . '/Post.subs.php');
460 12
	$updates = [];
461 12
	foreach ($adjustBoards as $stats)
462 12
	{
463
		$updates[] = $stats['id_board'];
464
	}
465 12
	updateLastMessages($updates);
466 12
}
467 12
468
/**
469 12
 * Moves lots of topics to a specific board and checks if the user can move them
470
 *
471 12
 * @param array $moveCache [0] => int[] is the topic, [1] => int[]  is the board to move to.
472 12
 */
473
function moveTopicsPermissions($moveCache)
474
{
475
	global $board;
476
477
	$db = database();
478
479
	$moveTos = [];
480
	$moveCache2 = [];
481
	$countPosts = [];
482
483
	// I know - I just KNOW you're trying to beat the system.  Too bad for you... we CHECK :P.
484
	$db->fetchQuery('
485
		SELECT 
486
			t.id_topic, t.id_board, b.count_posts
487
		FROM {db_prefix}topics AS t
488
			LEFT JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board)
489
		WHERE t.id_topic IN ({array_int:move_topic_ids})' . (!empty($board) && !allowedTo('move_any') ? '
490
			AND t.id_member_started = {int:current_member}' : '') . '
491
		LIMIT ' . count($moveCache[0]),
492
		[
493
			'current_member' => User::$info->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...
494
			'move_topic_ids' => $moveCache[0],
495
		]
496
	)->fetch_callback(
497
		function ($row) use (&$countPosts, &$moveCache2, &$moveTos, $moveCache) {
498
			$to = $moveCache[1][$row['id_topic']];
499
500
			if (empty($to))
501
			{
502
				return;
503
			}
504
505
			// Does this topic's board count the posts or not?
506
			$countPosts[$row['id_topic']] = empty($row['count_posts']);
507
508
			if (!isset($moveTos[$to]))
509
			{
510
				$moveTos[$to] = [];
511
			}
512
513
			$moveTos[$to][] = $row['id_topic'];
514
515
			// For reporting...
516
			$moveCache2[] = [$row['id_topic'], $row['id_board'], $to];
517
		}
518
	);
519
520
	// Do the actual moves...
521
	foreach ($moveTos as $to => $topics)
522
	{
523
		moveTopics($topics, $to, true);
524
	}
525
526
	// Does the post counts need to be updated?
527
	if (!empty($moveTos))
528
	{
529
		require_once(SUBSDIR . '/Boards.subs.php');
530
		$topicRecounts = [];
531
		$boards_info = fetchBoardsInfo(['boards' => array_keys($moveTos)], ['selects' => 'posts']);
532
533
		foreach ($boards_info as $row)
534
		{
535
			$cp = empty($row['count_posts']);
536
537
			// Go through all the topics that are being moved to this board.
538
			foreach ($moveTos[$row['id_board']] as $topic)
539
			{
540
				// If both boards have the same value for post counting then no adjustment needs to be made.
541
				if ($countPosts[$topic] != $cp)
542
				{
543
					// If the board being moved to does count the posts then the other one doesn't so add to their post count.
544
					$topicRecounts[$topic] = $cp ? 1 : -1;
545
				}
546
			}
547
		}
548
549
		if (!empty($topicRecounts))
550
		{
551
			require_once(SUBSDIR . '/Members.subs.php');
552
553
			// Get all the members who have posted in the moved topics.
554
			$posters = topicsPosters(array_keys($topicRecounts));
555
			foreach ($posters as $id_member => $topics)
556
			{
557
				$post_adj = 0;
558
				foreach ($topics as $id_topic)
559
				{
560
					$post_adj += $topicRecounts[$id_topic];
561
				}
562
563
				// And now update that member's post counts
564
				if (!empty($post_adj))
565
				{
566
					updateMemberData($id_member, ['posts' => 'posts + ' . $post_adj]);
567
				}
568
			}
569
		}
570
	}
571
}
572
573
/**
574
 * Moves one or more topics to a specific board.
575
 *
576
 * What it does:
577
 *
578
 * - Determines the source boards for the supplied topics
579
 * - Handles the moving of mark_read data
580
 * - Updates the posts count of the affected boards
581
 * - This function doesn't check permissions.
582
 *
583
 * @param int[]|int $topics
584
 * @param int $toBoard
585
 * @param bool $log if true logs the action.
586
 */
587
function moveTopics($topics, $toBoard, $log = false)
588
{
589
	global $modSettings;
590
591
	// No topics or no board?
592
	if (empty($topics) || empty($toBoard))
593
	{
594
		return;
595
	}
596
597
	$db = database();
598
599
	// Only a single topic.
600
	if (!is_array($topics))
601
	{
602
		$topics = [$topics];
603
	}
604
605
	$fromBoards = [];
606
	$fromCacheBoards = [];
607
608
	// Are we moving to the recycle board?
609
	$isRecycleDest = !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $toBoard;
610
611
	// Determine the source boards...
612
	$request = $db->query('', '
613
		SELECT 
614
			id_topic, id_board, approved, COUNT(*) AS num_topics, SUM(unapproved_posts) AS unapproved_posts,
615
			SUM(num_replies) AS num_replies
616
		FROM {db_prefix}topics
617
		WHERE id_topic IN ({array_int:topics})
618
		GROUP BY id_topic, id_board, approved',
619
		[
620
			'topics' => $topics,
621
		]
622
	);
623
	// Num of rows = 0 -> no topics found. Num of rows > 1 -> topics are on multiple boards.
624
	if ($request->num_rows() === 0)
625
	{
626
		return;
627
	}
628
629
	while (($row = $request->fetch_assoc()))
630
	{
631
		$fromCacheBoards[$row['id_topic']] = $row['id_board'];
632
		if (!isset($fromBoards[$row['id_board']]['num_posts']))
633
		{
634
			$fromBoards[$row['id_board']] = [
635
				'num_posts' => 0,
636
				'num_topics' => 0,
637
				'unapproved_posts' => 0,
638
				'unapproved_topics' => 0,
639
				'id_board' => $row['id_board']
640
			];
641
		}
642
		// Posts = (num_replies + 1) for each approved topic.
643
		$fromBoards[$row['id_board']]['num_posts'] += $row['num_replies'] + ($row['approved'] ? $row['num_topics'] : 0);
644
		$fromBoards[$row['id_board']]['unapproved_posts'] += $row['unapproved_posts'];
645
646
		// Add the topics to the right type.
647
		if ($row['approved'])
648
		{
649
			$fromBoards[$row['id_board']]['num_topics'] += $row['num_topics'];
650
		}
651
		else
652
		{
653
			$fromBoards[$row['id_board']]['unapproved_topics'] += $row['num_topics'];
654
		}
655
	}
656
	$request->free_result();
657
658
	// Move over the mark_read data. (because it may be read and now not by some!)
659
	$SaveAServer = max(0, $modSettings['maxMsgID'] - 50000);
660
	$log_topics = [];
661
	$db->fetchQuery('
662
		SELECT
663
		 	lmr.id_member, lmr.id_msg, t.id_topic, COALESCE(lt.unwatched, 0) as unwatched
664
		FROM {db_prefix}topics AS t
665
			INNER JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = t.id_board
666
				AND lmr.id_msg > t.id_first_msg AND lmr.id_msg > {int:protect_lmr_msg})
667
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = lmr.id_member)
668
		WHERE t.id_topic IN ({array_int:topics})
669
			AND lmr.id_msg > COALESCE(lt.id_msg, 0)',
670
		[
671
			'protect_lmr_msg' => $SaveAServer,
672
			'topics' => $topics,
673
		]
674
	)->fetch_callback(
675
		function ($row) use (&$log_topics) {
676
			$log_topics[] = [$row['id_member'], $row['id_topic'], $row['id_msg'], $row['unwatched']];
677
678
			// Prevent queries from getting too big. Taking some steam off.
679
			if (count($log_topics) > 500)
680
			{
681
				markTopicsRead($log_topics, true);
682
				$log_topics = [];
683
			}
684
		}
685
	);
686
687
	// Now that we have all the topics that *should* be marked read, and by which members...
688
	if (!empty($log_topics))
689
	{
690
		// Insert that information into the database!
691
		markTopicsRead($log_topics, true);
692
	}
693
694
	// Update the number of posts on each board.
695
	$totalTopics = 0;
696
	$totalPosts = 0;
697
	$totalUnapprovedTopics = 0;
698
	$totalUnapprovedPosts = 0;
699
	foreach ($fromBoards as $stats)
700
	{
701
		$db->query('', '
702
			UPDATE {db_prefix}boards
703
			SET
704
				num_posts = CASE WHEN {int:num_posts} > num_posts THEN 0 ELSE num_posts - {int:num_posts} END,
705
				num_topics = CASE WHEN {int:num_topics} > num_topics THEN 0 ELSE num_topics - {int:num_topics} END,
706
				unapproved_posts = CASE WHEN {int:unapproved_posts} > unapproved_posts THEN 0 ELSE unapproved_posts - {int:unapproved_posts} END,
707
				unapproved_topics = CASE WHEN {int:unapproved_topics} > unapproved_topics THEN 0 ELSE unapproved_topics - {int:unapproved_topics} END
708
			WHERE id_board = {int:id_board}',
709
			[
710
				'id_board' => $stats['id_board'],
711
				'num_posts' => $stats['num_posts'],
712
				'num_topics' => $stats['num_topics'],
713
				'unapproved_posts' => $stats['unapproved_posts'],
714
				'unapproved_topics' => $stats['unapproved_topics'],
715
			]
716
		);
717
		$totalTopics += $stats['num_topics'];
718
		$totalPosts += $stats['num_posts'];
719
		$totalUnapprovedTopics += $stats['unapproved_topics'];
720
		$totalUnapprovedPosts += $stats['unapproved_posts'];
721
	}
722
	$db->query('', '
723
		UPDATE {db_prefix}boards
724
		SET
725
			num_topics = num_topics + {int:total_topics},
726
			num_posts = num_posts + {int:total_posts},' . ($isRecycleDest ? '
727
			unapproved_posts = {int:no_unapproved}, unapproved_topics = {int:no_unapproved}' : '
728
			unapproved_posts = unapproved_posts + {int:total_unapproved_posts},
729
			unapproved_topics = unapproved_topics + {int:total_unapproved_topics}') . '
730
		WHERE id_board = {int:id_board}',
731
		[
732
			'id_board' => $toBoard,
733
			'total_topics' => $totalTopics,
734
			'total_posts' => $totalPosts,
735
			'total_unapproved_topics' => $totalUnapprovedTopics,
736
			'total_unapproved_posts' => $totalUnapprovedPosts,
737
			'no_unapproved' => 0,
738
		]
739
	);
740
741
	if ($isRecycleDest)
742
	{
743
		$attributes = [
744
			'id_board' => $toBoard,
745
			'approved' => 1,
746
			'unapproved_posts' => 0,
747
		];
748
	}
749
	else
750
	{
751
		$attributes = ['id_board' => $toBoard];
752
	}
753
754
	// Move the topic.  Done.  :P
755
	setTopicAttribute($topics, $attributes);
756
757
	// If this was going to the recycle bin, check what messages are being recycled, and remove them from the queue.
758
	if ($isRecycleDest && ($totalUnapprovedTopics || $totalUnapprovedPosts))
759
	{
760
		$approval_msgs = [];
761
		$db->fetchQuery('
762
			SELECT 
763
				id_msg
764
			FROM {db_prefix}messages
765
			WHERE id_topic IN ({array_int:topics})
766
				and approved = {int:not_approved}',
767
			[
768
				'topics' => $topics,
769
				'not_approved' => 0,
770
			]
771
		)->fetch_callback(
772
			function ($row) use (&$approval_msgs) {
773
				$approval_msgs[] = $row['id_msg'];
774
			}
775
		);
776
777
		// Empty the approval queue for these, as we're going to approve them next.
778
		if (!empty($approval_msgs))
779
		{
780
			$db->query('', '
781
				DELETE FROM {db_prefix}approval_queue
782
				WHERE id_msg IN ({array_int:message_list})
783
					AND id_attach = {int:id_attach}',
784
				[
785
					'message_list' => $approval_msgs,
786
					'id_attach' => 0,
787
				]
788
			);
789
		}
790
791
		// Get all the current max and mins.
792
		$topicAttribute = topicAttribute($topics, ['id_topic', 'id_first_msg', 'id_last_msg']);
793
		$topicMaxMin = [];
794
		foreach ($topicAttribute as $row)
795
		{
796
			$topicMaxMin[$row['id_topic']] = [
797
				'min' => $row['id_first_msg'],
798
				'max' => $row['id_last_msg'],
799
			];
800
		}
801
802
		// Check the MAX and MIN are correct.
803
		$db->fetchQuery('
804
			SELECT 
805
				id_topic, MIN(id_msg) AS first_msg, MAX(id_msg) AS last_msg
806
			FROM {db_prefix}messages
807
			WHERE id_topic IN ({array_int:topics})
808
			GROUP BY id_topic',
809
			[
810
				'topics' => $topics,
811
			]
812
		)->fetch_callback(
813
			function ($row) use ($topicMaxMin) {
814
				// If not, update.
815
				if ($row['first_msg'] != $topicMaxMin[$row['id_topic']]['min'] || $row['last_msg'] != $topicMaxMin[$row['id_topic']]['max'])
816
				{
817
					setTopicAttribute($row['id_topic'], [
818
						'id_first_msg' => $row['first_msg'],
819
						'id_last_msg' => $row['last_msg'],
820
					]);
821
				}
822
			}
823
		);
824
	}
825
826
	$db->query('', '
827
		UPDATE {db_prefix}messages
828
		SET 
829
			id_board = {int:id_board}' . ($isRecycleDest ? ',approved = {int:is_approved}' : '') . '
830
		WHERE id_topic IN ({array_int:topics})',
831
		[
832
			'id_board' => $toBoard,
833
			'topics' => $topics,
834
			'is_approved' => 1,
835
		]
836
	);
837
	$db->query('', '
838
		UPDATE {db_prefix}log_reported
839
		SET 
840
			id_board = {int:id_board}
841
		WHERE id_topic IN ({array_int:topics})',
842
		[
843
			'id_board' => $toBoard,
844
			'topics' => $topics,
845
		]
846
	);
847
	$db->query('', '
848
		UPDATE {db_prefix}calendar
849
		SET 
850
			id_board = {int:id_board}
851
		WHERE id_topic IN ({array_int:topics})',
852
		[
853
			'id_board' => $toBoard,
854
			'topics' => $topics,
855
		]
856
	);
857
858
	// Mark target board as seen, if it was already marked as seen before.
859
	$request = $db->query('', '
860
		SELECT 
861
			(COALESCE(lb.id_msg, 0) >= b.id_msg_updated) AS isSeen
862
		FROM {db_prefix}boards AS b
863
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
864
		WHERE b.id_board = {int:id_board}',
865
		[
866
			'current_member' => User::$info->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...
867
			'id_board' => $toBoard,
868
		]
869
	);
870
	list ($isSeen) = $request->fetch_row();
871
	$request->free_result();
872
873
	if (!empty($isSeen) && User::$info->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...
874
	{
875
		require_once(SUBSDIR . '/Boards.subs.php');
876
		markBoardsRead($toBoard);
877
	}
878
879
	// Update the cache?
880
	$cache = Cache::instance();
881
	foreach ($topics as $topic_id)
882
	{
883
		$cache->remove('topic_board-' . $topic_id);
884
	}
885
886
	require_once(SUBSDIR . '/Post.subs.php');
887
888
	$updates = array_keys($fromBoards);
889
	$updates[] = $toBoard;
890
891
	updateLastMessages(array_unique($updates));
892
893
	// Update 'em pesky stats.
894
	updateTopicStats();
895
	require_once(SUBSDIR . '/Messages.subs.php');
896
	updateMessageStats();
897
	updateSettings([
898
		'calendar_updated' => time(),
899
	]);
900
901
	if ($log)
902
	{
903
		foreach ($topics as $topic)
904
		{
905
			logAction('move', ['topic' => $topic, 'board_from' => $fromCacheBoards[$topic], 'board_to' => $toBoard]);
906
			sendNotifications($topic, 'move');
907
		}
908
	}
909
}
910
911
/**
912
 * Called after a topic is moved to update $board_link and $topic_link to point
913
 * to new location
914
 *
915
 * @param int $move_from The board the topic belongs to
916
 * @param int $id_board The "current" board
917
 * @param int $id_topic The topic id
918
 *
919
 * @return bool
920
 * @throws \ElkArte\Exceptions\Exception topic_already_moved
921
 */
922
function moveTopicConcurrence($move_from, $id_board, $id_topic)
923
{
924
	$db = database();
925
926
	if (empty($move_from) || empty($id_board) || empty($id_topic))
927
	{
928
		return true;
929
	}
930
931
	if ($move_from == $id_board)
932
	{
933
		return true;
934
	}
935
936
	$request = $db->query('', '
937
		SELECT 
938
			m.subject, b.name
939
		FROM {db_prefix}topics AS t
940
			LEFT JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board)
941
			LEFT JOIN {db_prefix}messages AS m ON (t.id_first_msg = m.id_msg)
942
		WHERE t.id_topic = {int:topic_id}
943
		LIMIT 1',
944
		[
945
			'topic_id' => $id_topic,
946
		]
947
	);
948
	list ($topic_subject, $board_name) = $request->fetch_row();
949
	$request->free_result();
950
951
	$board_link = '<a href="' . getUrl('board', ['board' => $id_board, 'start' => '0', 'name' => $board_name]) . '">' . $board_name . '</a>';
952
	$topic_link = '<a href="' . getUrl('topic', ['topic' => $id_topic, 'start' => '0', 'subject' => $topic_subject]) . '">' . $topic_subject . '</a>';
953
	throw new \ElkArte\Exceptions\Exception('topic_already_moved', false, [$topic_link, $board_link]);
954
}
955
956
/**
957
 * Determine if the topic has already been deleted by another user.
958
 *
959
 * What it does:
960
 *  - If the topic has been removed and resides in the recycle bin, present confirm dialog
961
 *  - If recycling is not enabled, or user confirms or topic is not in recycle simply returns
962
 *
963
 * @throws \ElkArte\Exceptions\Exception post_already_deleted
964
 */
965
function removeDeleteConcurrence()
966
{
967
	global $modSettings, $board, $context;
968
969
	$recycled_enabled = !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']);
970
971
	// Trying to remove from the recycle bin
972
	if ($recycled_enabled && !empty($board) && !isset($_GET['confirm_delete']) && $modSettings['recycle_board'] == $board)
973
	{
974
		if (isset($_REQUEST['msg']))
975
		{
976
			$confirm_url = getUrl('action', ['action' => 'deletemsg', 'confirm_delete', 'topic' => $context['current_topic'] . '.0', 'msg' => $_REQUEST['msg'], '{session_data}']);
977
		}
978
		else
979
		{
980
			$confirm_url = getUrl('action', ['action' => 'removetopic2', 'confirm_delete', 'topic' => $context['current_topic'] . '.0', '{session_data}']);
981
		}
982
983
		// Give them a prompt before we remove the message
984
		throw new \ElkArte\Exceptions\Exception('post_already_deleted', false, [$confirm_url]);
985
	}
986
}
987
988
/**
989
 * Increase the number of views of this topic.
990
 *
991
 * @param int $id_topic the topic being viewed or whatnot.
992
 */
993
function increaseViewCounter($id_topic)
994
{
995
	$db = database();
996
997
	$db->query('', '
998
		UPDATE {db_prefix}topics
999
		SET 
1000
			num_views = num_views + 1
1001
		WHERE id_topic = {int:current_topic}',
1002
		[
1003
			'current_topic' => $id_topic,
1004
		]
1005
	);
1006
}
1007 2
1008
/**
1009 2
 * Mark topic(s) as read by the given member, at the specified message.
1010
 *
1011
 * @param array $mark_topics array($id_member, $id_topic, $id_msg)
1012
 * @param bool $was_set = false - whether the topic has been previously read by the user
1013
 */
1014
function markTopicsRead($mark_topics, $was_set = false)
1015 2
{
1016
	$db = database();
1017
1018 2
	if (!is_array($mark_topics))
0 ignored issues
show
introduced by
The condition is_array($mark_topics) is always true.
Loading history...
1019
	{
1020
		return;
1021
	}
1022
1023
	$db->insert($was_set ? 'replace' : 'ignore',
1024
		'{db_prefix}log_topics',
1025
		[
1026
			'id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int', 'unwatched' => 'int',
1027
		],
1028
		$mark_topics,
1029 12
		['id_member', 'id_topic']
1030
	);
1031 12
}
1032
1033
/**
1034
 * Update user notifications for a topic... or the board it's in.
1035
 *
1036 12
 * @param int $id_topic
1037 12
 * @param int $id_board
1038
 * @todo look at board notification...
1039 12
 *
1040
 */
1041 6
function updateReadNotificationsFor($id_topic, $id_board)
1042 12
{
1043
	global $context;
1044 12
1045
	$db = database();
1046
1047
	// Check for notifications on this topic OR board.
1048
	$request = $db->query('', '
1049
		SELECT 
1050
			sent, id_topic
1051
		FROM {db_prefix}log_notify
1052
		WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
1053
			AND id_member = {int:current_member}
1054
		LIMIT 2',
1055
		[
1056
			'current_board' => $id_board,
1057 2
			'current_member' => User::$info->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...
1058
			'current_topic' => $id_topic,
1059 2
		]
1060
	);
1061
	while (($row = $request->fetch_assoc()))
1062 2
	{
1063
		// Find if this topic is marked for notification...
1064
		if (!empty($row['id_topic']))
1065
		{
1066
			$context['is_marked_notify'] = true;
1067
		}
1068
1069
		// Only do this once, but mark the notifications as "not sent yet" for next time.
1070 2
		if (!empty($row['sent']))
1071 2
		{
1072 2
			$db->query('', '
1073
				UPDATE {db_prefix}log_notify
1074
				SET 
1075 2
					sent = {int:is_not_sent}
1076
				WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
1077
					AND id_member = {int:current_member}',
1078
				[
1079
					'current_board' => $id_board,
1080
					'current_member' => User::$info->id,
1081
					'current_topic' => $id_topic,
1082
					'is_not_sent' => 0,
1083
				]
1084
			);
1085
1086
			break;
1087
		}
1088
	}
1089
	$request->free_result();
1090
}
1091
1092
/**
1093
 * How many topics are still unread since (last visit)
1094
 *
1095
 * @param int $id_board
1096
 * @param int $id_msg_last_visit
1097
 * @return int
1098
 */
1099
function getUnreadCountSince($id_board, $id_msg_last_visit)
1100
{
1101
	$db = database();
1102
1103 2
	$request = $db->query('', '
1104 2
		SELECT 
1105
			COUNT(*)
1106
		FROM {db_prefix}topics AS t
1107
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = {int:current_board} AND lb.id_member = {int:current_member})
1108
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
1109
		WHERE t.id_board = {int:current_board}
1110
			AND t.id_last_msg > COALESCE(lb.id_msg, 0)
1111
			AND t.id_last_msg > COALESCE(lt.id_msg, 0)' .
1112
		(empty($id_msg_last_visit) ? '' : '
1113
			AND t.id_last_msg > {int:id_msg_last_visit}'),
1114
		[
1115
			'current_board' => $id_board,
1116 2
			'current_member' => User::$info->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...
1117
			'id_msg_last_visit' => (int) $id_msg_last_visit,
1118 2
		]
1119
	);
1120
	list ($unread) = $request->fetch_row();
1121
	$request->free_result();
1122
1123
	return $unread;
1124
}
1125
1126
/**
1127 2
 * Returns whether this member has notification turned on for the specified topic.
1128
 *
1129
 * @param int $id_member
1130 2
 * @param int $id_topic
1131 2
 * @return bool
1132 2
 */
1133
function hasTopicNotification($id_member, $id_topic)
1134
{
1135 2
	$db = database();
1136 2
1137
	// Find out if they have notification set for this topic already.
1138 2
	return $db->fetchQuery('
1139
		SELECT 
1140
			id_member
1141
		FROM {db_prefix}log_notify
1142
		WHERE id_member = {int:current_member}
1143
			AND id_topic = {int:current_topic}
1144
		LIMIT 1',
1145
		[
1146
			'current_member' => $id_member,
1147
			'current_topic' => $id_topic,
1148
		]
1149
	)->num_rows() != 0;
1150
}
1151
1152
/**
1153
 * Set topic notification on or off for the given member.
1154
 *
1155
 * @param int $id_member
1156
 * @param int $id_topic
1157
 * @param bool $on
1158
 */
1159
function setTopicNotification($id_member, $id_topic, $on = false)
1160
{
1161
	$db = database();
1162
1163
	if ($on)
1164
	{
1165
		// Attempt to turn notifications on.
1166
		$db->insert('ignore',
1167
			'{db_prefix}log_notify',
1168
			['id_member' => 'int', 'id_topic' => 'int'],
1169
			[$id_member, $id_topic],
1170
			['id_member', 'id_topic']
1171
		);
1172
	}
1173
	else
1174
	{
1175
		// Just turn notifications off.
1176
		$db->query('', '
1177
			DELETE FROM {db_prefix}log_notify
1178 4
			WHERE id_member = {int:current_member}
1179
				AND id_topic = {int:current_topic}',
1180 4
			[
1181
				'current_member' => $id_member,
1182
				'current_topic' => $id_topic,
1183
			]
1184
		);
1185
	}
1186
}
1187
1188
/**
1189
 * Get the previous topic from where we are.
1190
 *
1191
 * @param int $id_topic origin topic id
1192
 * @param int $id_board board id
1193 4
 * @param int $id_member = 0 member id
1194
 * @param bool $includeUnapproved = false whether to include unapproved topics
1195
 * @param bool $includeStickies = true whether to include sticky topics
1196
 * @return int topic number
1197
 */
1198 4
function previousTopic($id_topic, $id_board, $id_member = 0, $includeUnapproved = false, $includeStickies = true)
1199 4
{
1200
	return topicPointer($id_topic, $id_board, false, $id_member, $includeUnapproved, $includeStickies);
1201
}
1202
1203 4
/**
1204
 * Get the next topic from where we are.
1205
 *
1206
 * @param int $id_topic origin topic id
1207
 * @param int $id_board board id
1208
 * @param int $id_member = 0 member id
1209
 * @param bool $includeUnapproved = false whether to include unapproved topics
1210
 * @param bool $includeStickies = true whether to include sticky topics
1211
 * @return int topic number
1212
 */
1213
function nextTopic($id_topic, $id_board, $id_member = 0, $includeUnapproved = false, $includeStickies = true)
1214
{
1215
	return topicPointer($id_topic, $id_board, true, $id_member, $includeUnapproved, $includeStickies);
1216
}
1217
1218
/**
1219
 * Advance topic pointer.
1220
 * (in either direction)
1221
 * This function is used by previousTopic() and nextTopic()
1222
 * The boolean parameter $next determines direction.
1223
 *
1224
 * @param int $id_topic origin topic id
1225
 * @param int $id_board board id
1226
 * @param bool $next = true whether to increase or decrease the pointer
1227
 * @param int $id_member = 0 member id
1228
 * @param bool $includeUnapproved = false whether to include unapproved topics
1229
 * @param bool $includeStickies = true whether to include sticky topics
1230
 * @return int the topic number
1231
 */
1232
function topicPointer($id_topic, $id_board, $next = true, $id_member = 0, $includeUnapproved = false, $includeStickies = true)
1233
{
1234
	$db = database();
1235
1236
	$request = $db->query('', '
1237
		SELECT 
1238
			t2.id_topic
1239
		FROM {db_prefix}topics AS t
1240
		INNER JOIN {db_prefix}topics AS t2 ON (' .
1241
		(empty($includeStickies) ? '
1242
				t2.id_last_msg {raw:strictly} t.id_last_msg' : '
1243
				(t2.id_last_msg {raw:strictly} t.id_last_msg AND t2.is_sticky {raw:strictly_equal} t.is_sticky) OR t2.is_sticky {raw:strictly} t.is_sticky')
1244
		. ')
1245
		WHERE t.id_topic = {int:current_topic}
1246
			AND t2.id_board = {int:current_board}' .
1247
		($includeUnapproved ? '' : '
1248
				AND (t2.approved = {int:is_approved} OR (t2.id_member_started != {int:id_member_started} AND t2.id_member_started = {int:current_member}))'
1249
		) . '
1250
		ORDER BY' . (
1251
		$includeStickies ? '
1252
				t2.is_sticky {raw:sorting},' :
1253
			'') .
1254
		' t2.id_last_msg {raw:sorting}
1255
		LIMIT 1',
1256
		[
1257
			'strictly' => $next ? '<' : '>',
1258
			'strictly_equal' => $next ? '<=' : '>=',
1259
			'sorting' => $next ? 'DESC' : '',
1260
			'current_board' => $id_board,
1261
			'current_member' => $id_member,
1262
			'current_topic' => $id_topic,
1263
			'is_approved' => 1,
1264
			'id_member_started' => 0,
1265
		]
1266
	);
1267
1268
	// Was there any?
1269
	if ($request->num_rows() === 0)
1270
	{
1271
		$request->free_result();
1272
1273
		// Roll over - if we're going prev, get the last - otherwise the first.
1274
		$request = $db->query('', '
1275
			SELECT 
1276
				id_topic
1277
			FROM {db_prefix}topics
1278
			WHERE id_board = {int:current_board}' .
1279
			($includeUnapproved ? '' : '
1280
				AND (approved = {int:is_approved} OR (id_member_started != {int:id_member_started} AND id_member_started = {int:current_member}))') . '
1281
			ORDER BY' . (
1282
			$includeStickies ? ' is_sticky {raw:sorting},' : '') .
1283
			' id_last_msg {raw:sorting}
1284
			LIMIT 1',
1285
			[
1286
				'sorting' => $next ? 'DESC' : '',
1287
				'current_board' => $id_board,
1288
				'current_member' => $id_member,
1289
				'is_approved' => 1,
1290
				'id_member_started' => 0,
1291
			]
1292
		);
1293
	}
1294
	// Now you can be sure $topic is the id_topic to view.
1295
	list ($topic) = $request->fetch_row();
1296
	$request->free_result();
1297
1298
	return $topic;
1299
}
1300
1301
/**
1302
 * Set off/on unread reply subscription for a topic
1303
 *
1304
 * @param int $id_member
1305
 * @param int $topic
1306
 * @param bool $on = false
1307
 */
1308
function setTopicWatch($id_member, $topic, $on = false)
1309
{
1310
	$db = database();
1311
1312
	// find the current entry if it exists that is
1313
	$was_set = getLoggedTopics(User::$info->id, [$topic]);
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...
1314
1315
	// Set topic unwatched on/off for this topic.
1316
	$db->insert(empty($was_set[$topic]) ? 'ignore' : 'replace',
1317
		'{db_prefix}log_topics',
1318
		['id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int', 'unwatched' => 'int'],
1319
		[$id_member, $topic, !empty($was_set[$topic]['id_msg']) ? $was_set[$topic]['id_msg'] : 0, $on ? 1 : 0],
1320
		['id_member', 'id_topic']
1321
	);
1322
}
1323
1324
/**
1325
 * Get all the details for a given topic
1326
 * - returns the basic topic information when $full is false
1327
 * - returns topic details, subject, last message read, etc when full is true
1328
 * - uses any integration information (value selects, tables and parameters) if passed and full is true
1329
 *
1330
 * @param array|int $topic_parameters can also accept a int value for a topic
1331
 * @param string $full defines the values returned by the function:
1332
 *    - if empty returns only the data from {db_prefix}topics
1333
 *    - if 'message' returns also information about the message (subject, body, etc.)
1334
 *    - if 'starter' returns also information about the topic starter (id_member and poster_name)
1335
 *    - if 'all' returns additional infos about the read/unwatched status
1336
 * @param string[] $selects (optional from integration)
1337
 * @param string[] $tables (optional from integration)
1338
 * @return array|bool to topic attributes
1339
 */
1340
function getTopicInfo($topic_parameters, $full = '', $selects = [], $tables = [])
1341
{
1342
	global $modSettings, $board;
1343
1344
	$db = database();
1345
1346
	// Nothing to do
1347
	if (empty($topic_parameters))
1348
	{
1349
		return false;
1350
	}
1351
1352
	// Build what we can with what we were given
1353
	if (!is_array($topic_parameters))
1354
	{
1355
		$topic_parameters = [
1356
			'topic' => $topic_parameters,
1357
			'member' => User::$info->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...
1358
			'board' => (int) $board,
1359
		];
1360
	}
1361
1362
	$messages_table = $full === 'message' || $full === 'all' || $full === 'starter';
1363
	$members_table = $full === 'starter' || $full === 'all';
1364 12
	$logs_table = $full === 'all';
1365
1366 12
	// Create the query, taking full and integration in to account
1367
	$request = $db->fetchQuery('
1368
		SELECT
1369 12
			t.id_topic, t.is_sticky, t.id_board, t.id_first_msg, t.id_last_msg,
1370
			t.id_member_started, t.id_member_updated, t.id_poll,
1371
			t.num_replies, t.num_views, t.num_likes, t.locked, t.redirect_expires,
1372
			t.id_redirect_topic, t.unapproved_posts, t.approved' . ($messages_table ? ',
1373
			ms.subject, ms.body, ms.id_member, ms.poster_time, ms.approved as msg_approved' : '') . ($members_table ? ',
1374
			COALESCE(mem.real_name, ms.poster_name) AS poster_name' : '') . ($logs_table ? ',
1375 12
			' . (User::$info->is_guest ? 't.id_last_msg + 1' : 'COALESCE(lt.id_msg, lmr.id_msg, -1) + 1') . ' AS new_from
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...
1376
			' . (!empty($modSettings['recycle_board']) && $modSettings['recycle_board'] == $board ? ', t.id_previous_board, t.id_previous_topic' : '') . '
1377
			' . (User::$info->is_guest === false ? ', COALESCE(lt.unwatched, 0) as unwatched' : '') : '') .
1378 10
		(!empty($selects) ? ', ' . implode(', ', $selects) : '') . '
1379 10
		FROM {db_prefix}topics AS t' . ($messages_table ? '
1380 10
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)' : '') . ($members_table ? '
1381
			LEFT JOIN {db_prefix}members as mem ON (mem.id_member = ms.id_member)' : '') . ($logs_table && User::$info->is_guest === false ? '
1382
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = {int:topic} AND lt.id_member = {int:member})
1383
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = {int:board} AND lmr.id_member = {int:member})' : '') . (!empty($tables) ? '
1384 12
			' . implode("\n\t\t\t", $tables) : '') . '
1385 12
		WHERE t.id_topic = {int:topic}
1386 12
		LIMIT 1',
1387
		$topic_parameters
1388
	);
1389 12
	$topic_info = [];
1390
	if ($request !== false)
0 ignored issues
show
introduced by
The condition $request !== false is always true.
Loading history...
1391
	{
1392
		$topic_info = $request->fetch_assoc();
1393
	}
1394 12
	$request->free_result();
1395 12
1396 12
	return $topic_info;
1397 4
}
1398 4
1399 12
/**
1400 12
 * Get all the details for a given topic and message.
1401 12
 * Respects permissions and post moderation
1402 12
 *
1403 12
 * @param int $topic id of a topic
1404
 * @param int|null $msg the id of a message, if empty, t.id_first_msg is used
1405 12
 * @return array|bool to topic attributes
1406 12
 */
1407
function getTopicInfoByMsg($topic, $msg = null)
1408
{
1409 6
	global $modSettings;
1410
1411 12
	// Nothing to do
1412 12
	if (empty($topic))
1413
	{
1414 12
		return false;
1415
	}
1416 12
1417
	$db = database();
1418 12
1419
	$request = $db->query('', '
1420
		SELECT
1421
			t.locked, t.num_replies, t.id_member_started, t.id_first_msg,
1422
			m.id_msg, m.id_member, m.poster_time, m.subject, m.smileys_enabled, m.body, m.icon,
1423
			m.modified_time, m.modified_name, m.approved
1424
		FROM {db_prefix}messages AS m
1425
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:current_topic})
1426
		WHERE m.id_msg = {raw:id_msg}
1427
			AND m.id_topic = {int:current_topic}' . (allowedTo('modify_any') || allowedTo('approve_posts') ? '' : (!$modSettings['postmod_active'] ? '
1428
			AND (m.id_member != {int:guest_id} AND m.id_member = {int:current_member})' : '
1429
			AND (m.approved = {int:is_approved} OR (m.id_member != {int:guest_id} AND m.id_member = {int:current_member}))')),
1430
		[
1431
			'current_member' => User::$info->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...
1432
			'current_topic' => $topic,
1433
			'id_msg' => empty($msg) ? 't.id_first_msg' : $msg,
1434
			'is_approved' => 1,
1435
			'guest_id' => 0,
1436
		]
1437
	);
1438
	$topic_info = [];
1439
	if ($request !== false)
0 ignored issues
show
introduced by
The condition $request !== false is always true.
Loading history...
1440
	{
1441
		$topic_info = $request->fetch_assoc();
1442
	}
1443
	$request->free_result();
1444
1445
	return $topic_info;
1446
}
1447
1448
/**
1449
 * So long as you are sure... all old posts will be gone.
1450
 * Used in Maintenance.controller.php to prune old topics.
1451
 *
1452
 * @param int[] $boards
1453
 * @param string $delete_type
1454
 * @param bool $exclude_stickies
1455
 * @param int $older_than
1456
 */
1457
function removeOldTopics(array $boards, $delete_type, $exclude_stickies, $older_than)
1458
{
1459
	$db = database();
1460
1461
	// Custom conditions.
1462
	$condition = '';
1463
	$condition_params = [
1464
		'boards' => $boards,
1465
		'poster_time' => $older_than,
1466
	];
1467
1468
	// Just moved notice topics?
1469
	if ($delete_type == 'moved')
1470
	{
1471
		$condition .= '
1472
			AND m.icon = {string:icon}
1473
			AND t.locked = {int:locked}';
1474
		$condition_params['icon'] = 'moved';
1475
		$condition_params['locked'] = 1;
1476
	}
1477
	// Otherwise, maybe locked topics only?
1478
	elseif ($delete_type == 'locked')
1479
	{
1480
		$condition .= '
1481
			AND t.locked = {int:locked}';
1482
		$condition_params['locked'] = 1;
1483
	}
1484
1485
	// Exclude stickies?
1486
	if ($exclude_stickies)
1487
	{
1488
		$condition .= '
1489
			AND t.is_sticky = {int:is_sticky}';
1490
		$condition_params['is_sticky'] = 0;
1491
	}
1492
1493
	// All we're gonna do here is grab the id_topic's and send them to removeTopics().
1494
	$topics = [];
1495
	$db->fetchQuery('
1496
		SELECT 
1497
			t.id_topic
1498
		FROM {db_prefix}topics AS t
1499
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)
1500
		WHERE
1501
			m.poster_time < {int:poster_time}' . $condition . '
1502
			AND t.id_board IN ({array_int:boards})',
1503
		$condition_params
1504
	)->fetch_callback(
1505
		function ($row) use (&$topics) {
1506
			$topics[] = $row['id_topic'];
1507
		}
1508
	);
1509
1510
	removeTopics($topics, false, true);
1511
}
1512
1513
/**
1514
 * Retrieve all topics started by the given member.
1515
 *
1516
 * @param int $memberID
1517
 *
1518
 * @return array
1519
 */
1520
function topicsStartedBy($memberID)
1521
{
1522
	$db = database();
1523
1524
	// Fetch all topics started by this user.
1525
	$topicIDs = [];
1526
	$db->fetchQuery('
1527
		SELECT 
1528
			t.id_topic
1529
		FROM {db_prefix}topics AS t
1530
		WHERE t.id_member_started = {int:selected_member}',
1531
		[
1532
			'selected_member' => $memberID,
1533
		]
1534
	)->fetch_callback(
1535
		function ($row) use (&$topicIDs) {
1536
			$topicIDs[] = $row['id_topic'];
1537
		}
1538
	);
1539
1540
	return $topicIDs;
1541
}
1542
1543
/**
1544
 * Retrieve the messages of the given topic, that are at or after
1545
 * a message.
1546
 * Used by split topics actions.
1547
 *
1548
 * @param int $id_topic
1549
 * @param int $id_msg
1550
 * @param bool $include_current = false
1551
 * @param bool $only_approved = false
1552
 *
1553
 * @return array message ids
1554
 */
1555
function messagesSince($id_topic, $id_msg, $include_current = false, $only_approved = false)
1556
{
1557
	$db = database();
1558
1559
	// Fetch the message IDs of the topic that are at or after the message.
1560
	$messages = [];
1561
	$db->fetchQuery('
1562
		SELECT 
1563
			id_msg
1564
		FROM {db_prefix}messages
1565
		WHERE id_topic = {int:current_topic}
1566
			AND id_msg ' . ($include_current ? '>=' : '>') . ' {int:last_msg}' . ($only_approved ? '
1567
			AND approved = {int:approved}' : ''),
1568
		[
1569
			'current_topic' => $id_topic,
1570
			'last_msg' => $id_msg,
1571
			'approved' => 1,
1572
		]
1573
	)->fetch_callback(
1574
		function ($row) use (&$messages) {
1575
			$messages[] = $row['id_msg'];
1576
		}
1577
	);
1578
1579
	return $messages;
1580
}
1581
1582
/**
1583
 * This function returns the number of messages in a topic,
1584
 * posted after $id_msg.
1585
 *
1586
 * @param int $id_topic
1587
 * @param int $id_msg
1588
 * @param bool $include_current = false
1589
 * @param bool $only_approved = false
1590
 *
1591
 * @return int
1592
 */
1593
function countMessagesSince($id_topic, $id_msg, $include_current = false, $only_approved = false)
1594
{
1595
	$db = database();
1596
1597
	// Give us something to work with
1598
	if (empty($id_topic) || empty($id_msg))
1599
	{
1600
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
1601
	}
1602
1603
	$request = $db->query('', '
1604
		SELECT 
1605
			COUNT(*)
1606
		FROM {db_prefix}messages
1607
		WHERE id_topic = {int:current_topic}
1608
			AND id_msg ' . ($include_current ? '>=' : '>') . ' {int:last_msg}' . ($only_approved ? '
1609
			AND approved = {int:approved}' : '') . '
1610
		LIMIT 1',
1611
		[
1612
			'current_topic' => $id_topic,
1613
			'last_msg' => $id_msg,
1614
			'approved' => 1,
1615
		]
1616
	);
1617
	list ($count) = $request->fetch_row();
1618
	$request->free_result();
1619
1620
	return $count;
1621
}
1622
1623
/**
1624
 * Returns how many messages are in a topic before the specified message id.
1625
 * Used in display to compute the start value for a specific message.
1626
 *
1627
 * @param int $id_topic
1628
 * @param int $id_msg
1629
 * @param bool $include_current = false
1630
 * @param bool $only_approved = false
1631
 * @param bool $include_own = false
1632
 * @return int
1633
 */
1634
function countMessagesBefore($id_topic, $id_msg, $include_current = false, $only_approved = false, $include_own = false)
1635
{
1636
	$db = database();
1637
1638
	$request = $db->query('', '
1639
		SELECT 
1640
			COUNT(*)
1641
		FROM {db_prefix}messages
1642
		WHERE id_msg ' . ($include_current ? '<=' : '<') . ' {int:id_msg}
1643
			AND id_topic = {int:current_topic}' . ($only_approved ? '
1644
			AND (approved = {int:is_approved}' . ($include_own ? '
1645
			OR id_member = {int:current_member}' : '') . ')' : ''),
1646
		[
1647
			'current_member' => User::$info->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...
1648
			'current_topic' => $id_topic,
1649
			'id_msg' => $id_msg,
1650
			'is_approved' => 1,
1651
		]
1652
	);
1653
	list ($count) = $request->fetch_row();
1654
	$request->free_result();
1655
1656
	return $count;
1657
}
1658
1659
/**
1660
 * Select a part of the messages in a topic.
1661
 *
1662
 * @param int $topic
1663
 * @param int $start The item to start with (for pagination purposes)
1664
 * @param int $items_per_page The number of items to show per page
1665
 * @param array $messages
1666
 * @param bool $only_approved
1667
 *
1668
 * @return array
1669
 */
1670
function selectMessages($topic, $start, $items_per_page, $messages = [], $only_approved = false)
1671
{
1672
	$db = database();
1673
1674
	$returnMessages = [];
1675
	$parser = ParserWrapper::instance();
1676
1677
	// Get the messages and stick them into an array.
1678
	$db->fetchQuery('
1679
		SELECT 
1680
			m.subject, COALESCE(mem.real_name, m.poster_name) AS real_name, 
1681
			m.poster_time, m.body, m.id_msg, m.smileys_enabled, m.id_member
1682
		FROM (
1683
			SELECT 
1684
				m.id_msg 
1685
			FROM {db_prefix}messages AS m
1686
			WHERE m.id_topic = {int:current_topic}' . (empty($messages['before']) ? '' : '
1687
				AND m.id_msg < {int:msg_before}') . (empty($messages['after']) ? '' : '
1688
				AND m.id_msg > {int:msg_after}') . (empty($messages['excluded']) ? '' : '
1689
				AND m.id_msg NOT IN ({array_int:no_split_msgs})') . (empty($messages['included']) ? '' : '
1690
				AND m.id_msg IN ({array_int:split_msgs})') . (!$only_approved ? '' : '
1691
				AND approved = {int:is_approved}') . '
1692
			ORDER BY m.id_msg DESC
1693
			LIMIT {int:start}, {int:messages_per_page}
1694
		) AS o 
1695
		JOIN {db_prefix}messages as m ON o.id_msg=m.id_msg 
1696
		LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1697
		ORDER BY m.id_msg DESC',
1698
		[
1699
			'current_topic' => $topic,
1700
			'no_split_msgs' => !empty($messages['excluded']) ? $messages['excluded'] : [],
1701
			'split_msgs' => !empty($messages['included']) ? $messages['included'] : [],
1702
			'is_approved' => 1,
1703
			'start' => $start,
1704
			'messages_per_page' => $items_per_page,
1705
			'msg_before' => !empty($messages['before']) ? (int) $messages['before'] : 0,
1706
			'msg_after' => !empty($messages['after']) ? (int) $messages['after'] : 0,
1707
		]
1708
	)->fetch_callback(
1709
		function ($row) use (&$returnMessages, $parser) {
1710
			$row['subject'] = censor($row['subject']);
1711
			$row['body'] = censor($row['body']);
1712
1713
			$row['body'] = $parser->parseMessage($row['body'], (bool) $row['smileys_enabled']);
1714
1715
			$returnMessages[$row['id_msg']] = [
1716
				'id' => $row['id_msg'],
1717
				'subject' => $row['subject'],
1718
				'time' => standardTime($row['poster_time']),
1719
				'html_time' => htmlTime($row['poster_time']),
1720
				'timestamp' => forum_time(true, $row['poster_time']),
1721
				'body' => $row['body'],
1722
				'poster' => $row['real_name'],
1723
				'id_poster' => $row['id_member'],
1724
			];
1725
		}
1726
	);
1727
1728
	return $returnMessages;
1729
}
1730
1731
/**
1732
 * Loads all the messages of a topic
1733
 * Used when printing or other functions that require a topic listing
1734
 *
1735
 * @param int $topic
1736
 * @param string $render defaults to print style rendering for parse_bbc
1737
 *
1738
 * @return array
1739
 */
1740
function topicMessages($topic, $render = 'print')
1741
{
1742
	global $modSettings;
1743
1744
	$db = database();
1745
1746
	$posts = [];
1747
	$parser = ParserWrapper::instance();
1748
	if ($render === 'print')
1749
	{
1750
		$parser->getCodes()->setForPrinting();
1751
	}
1752
1753
	$db->fetchQuery('
1754
		SELECT 
1755
			subject, poster_time, body, COALESCE(mem.real_name, poster_name) AS poster_name, id_msg
1756
		FROM {db_prefix}messages AS m
1757
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1758
		WHERE m.id_topic = {int:current_topic}' . ($modSettings['postmod_active'] && !allowedTo('approve_posts') ? '
1759
			AND (m.approved = {int:is_approved}' . (User::$info->is_guest ? '' : ' OR m.id_member = {int:current_member}') . ')' : '') . '
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...
1760
		ORDER BY m.id_msg',
1761
		[
1762
			'current_topic' => $topic,
1763
			'is_approved' => 1,
1764
			'current_member' => User::$info->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...
1765
		]
1766
	)->fetch_callback(
1767
		function ($row) use (&$posts, $parser, $render) {
1768
			// Censor the subject and message.
1769
			$row['subject'] = censor($row['subject']);
1770
			$row['body'] = censor($row['body']);
1771
1772
			$posts[$row['id_msg']] = [
1773
				'subject' => $row['subject'],
1774
				'member' => $row['poster_name'],
1775
				'time' => standardTime($row['poster_time'], false),
1776
				'html_time' => htmlTime($row['poster_time']),
1777
				'timestamp' => forum_time(true, $row['poster_time']),
1778
				'body' => $parser->parseMessage($row['body'], $render !== 'print'),
1779
				'id_msg' => $row['id_msg'],
1780
			];
1781
		}
1782
	);
1783
1784
	return $posts;
1785
}
1786
1787
/**
1788
 * Load message image attachments for use in the print page function
1789
 * Returns array of file attachment name along with width/height properties
1790
 * Will only return approved attachments
1791
 *
1792
 * @param int[] $id_messages
1793
 *
1794
 * @return array
1795
 */
1796
function messagesAttachments($id_messages)
1797
{
1798
	global $modSettings;
1799
1800
	require_once(SUBSDIR . '/Attachments.subs.php');
1801
1802
	$db = database();
1803
1804
	$temp = [];
1805
	$printattach = [];
1806
	$db->fetchQuery('
1807
		SELECT
1808
			a.id_attach, a.id_msg, a.approved, a.width, a.height, a.file_hash, a.filename, a.id_folder, a.mime_type
1809
		FROM {db_prefix}attachments AS a
1810
		WHERE a.id_msg IN ({array_int:message_list})
1811
			AND a.attachment_type = {int:attachment_type}',
1812
		[
1813
			'message_list' => $id_messages,
1814
			'attachment_type' => 0,
1815
			'is_approved' => 1,
1816
		]
1817
	)->fetch_callback(
1818
		function ($row) use (&$temp, &$printattach) {
1819
			$temp[$row['id_attach']] = $row;
1820
			if (!isset($printattach[$row['id_msg']]))
1821
			{
1822
				$printattach[$row['id_msg']] = [];
1823
			}
1824
		}
1825
	);
1826
1827
	ksort($temp);
1828
1829
	// Load them into $context so the template can use them
1830
	foreach ($temp as $row)
1831
	{
1832
		if (!empty($row['width']) && !empty($row['height']))
1833
		{
1834
			if (!empty($modSettings['max_image_width']) && (empty($modSettings['max_image_height']) || $row['height'] * ($modSettings['max_image_width'] / $row['width']) <= $modSettings['max_image_height']))
1835
			{
1836
				if ($row['width'] > $modSettings['max_image_width'])
1837
				{
1838
					$row['height'] = floor($row['height'] * ($modSettings['max_image_width'] / $row['width']));
1839
					$row['width'] = $modSettings['max_image_width'];
1840
				}
1841
			}
1842
			elseif (!empty($modSettings['max_image_width']))
1843
			{
1844
				if ($row['height'] > $modSettings['max_image_height'])
1845
				{
1846
					$row['width'] = floor($row['width'] * $modSettings['max_image_height'] / $row['height']);
1847
					$row['height'] = $modSettings['max_image_height'];
1848
				}
1849
			}
1850
1851
			$row['filename'] = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
1852
1853
			// save for the template
1854
			$printattach[$row['id_msg']][] = $row;
1855
		}
1856
	}
1857
1858
	return $printattach;
1859
}
1860
1861
/**
1862
 * Retrieve unapproved posts of the member
1863
 * in a specific topic
1864
 *
1865
 * @param int $id_topic topic id
1866
 * @param int $id_member member id
1867
 * @return array|int empty array if no member supplied, otherwise number of posts
1868
 */
1869
function unapprovedPosts($id_topic, $id_member)
1870
{
1871
	$db = database();
1872
1873
	// not all guests are the same!
1874
	if (empty($id_member))
1875
	{
1876
		return [];
1877
	}
1878
1879
	$request = $db->query('', '
1880
		SELECT 
1881
			COUNT(id_member) AS my_unapproved_posts
1882
		FROM {db_prefix}messages
1883
		WHERE id_topic = {int:current_topic}
1884
			AND id_member = {int:current_member}
1885
			AND approved = 0',
1886
		[
1887
			'current_topic' => $id_topic,
1888
			'current_member' => $id_member,
1889
		]
1890
	);
1891
	list ($myUnapprovedPosts) = $request->fetch_row();
1892
	$request->free_result();
1893
1894
	return $myUnapprovedPosts;
1895
}
1896
1897
/**
1898
 * Update topic info after a successful split of a topic.
1899
 *
1900
 * @param array $options
1901
 * @param int $id_board
1902
 */
1903
function updateSplitTopics($options, $id_board)
1904
{
1905
	$db = database();
1906
1907
	// Any associated reported posts better follow...
1908
	$db->query('', '
1909
		UPDATE {db_prefix}log_reported
1910
		SET 
1911
			id_topic = {int:id_topic}
1912
		WHERE id_msg IN ({array_int:split_msgs})
1913
			AND type = {string:a_message}',
1914
		[
1915
			'split_msgs' => $options['splitMessages'],
1916
			'id_topic' => $options['split2_ID_TOPIC'],
1917
			'a_message' => 'msg',
1918
		]
1919
	);
1920
1921
	// Mess with the old topic's first, last, and number of messages.
1922
	setTopicAttribute($options['split1_ID_TOPIC'], [
1923
		'num_replies' => $options['split1_replies'],
1924
		'id_first_msg' => $options['split1_first_msg'],
1925
		'id_last_msg' => $options['split1_last_msg'],
1926
		'id_member_started' => $options['split1_firstMem'],
1927
		'id_member_updated' => $options['split1_lastMem'],
1928
		'unapproved_posts' => $options['split1_unapprovedposts'],
1929
	]);
1930
1931
	// Now, put the first/last message back to what they should be.
1932
	setTopicAttribute($options['split2_ID_TOPIC'], [
1933
		'id_first_msg' => $options['split2_first_msg'],
1934
		'id_last_msg' => $options['split2_last_msg'],
1935
	]);
1936
1937
	// If the new topic isn't approved ensure the first message flags
1938
	// this just in case.
1939
	if (!$options['split2_approved'])
1940
	{
1941
		$db->query('', '
1942
			UPDATE {db_prefix}messages
1943
			SET 
1944
				approved = {int:approved}
1945
			WHERE id_msg = {int:id_msg}
1946
				AND id_topic = {int:id_topic}',
1947
			[
1948
				'approved' => 0,
1949
				'id_msg' => $options['split2_first_msg'],
1950
				'id_topic' => $options['split2_ID_TOPIC'],
1951
			]
1952
		);
1953
	}
1954
1955
	// The board has more topics now (Or more unapproved ones!).
1956
	$db->query('', '
1957
		UPDATE {db_prefix}boards
1958
		SET 
1959
			' . ($options['split2_approved']
1960
			? ' num_topics = num_topics + 1'
1961
			: ' unapproved_topics = unapproved_topics + 1') . '
1962
		WHERE id_board = {int:id_board}',
1963
		[
1964
			'id_board' => $id_board,
1965
		]
1966
	);
1967
}
1968
1969
/**
1970
 * Find out who started a topic, and the lock status
1971
 *
1972
 * @param int $topic
1973
 * @return array with id_member_started and locked
1974
 */
1975
function topicStatus($topic)
1976
{
1977
	// Find out who started the topic, and the lock status.
1978
	$starter = topicAttribute($topic, ['id_member_started', 'locked']);
1979
1980
	return [$starter['id_member_started'], $starter['locked']];
1981
}
1982
1983
/**
1984
 * Set attributes for a topic, i.e. locked, sticky.
1985
 * Parameter $attributes is an array where the key is the column name of the
1986
 * attribute to change, and the value is... the new value of the attribute.
1987
 * It sets the new value for the attribute as passed to it.
1988
 * <b>It is currently limited to integer values only</b>
1989
 *
1990
 * @param int|int[] $topic
1991
 * @param array $attributes
1992
 * @return int number of row affected
1993
 * @todo limited to integer attributes
1994
 */
1995
function setTopicAttribute($topic, $attributes)
1996
{
1997
	$db = database();
1998
1999
	$update = [];
2000
	foreach ($attributes as $key => $attr)
2001
	{
2002
		$attributes[$key] = (int) $attr;
2003
		$update[] = '
2004
				' . $key . ' = {int:' . $key . '}';
2005
	}
2006
2007
	if (empty($update))
2008
	{
2009
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
2010
	}
2011
2012
	$attributes['current_topic'] = (array) $topic;
2013
2014
	return $db->query('', '
2015
		UPDATE {db_prefix}topics
2016
		SET 
2017
			' . implode(',', $update) . '
2018
		WHERE id_topic IN ({array_int:current_topic})',
2019
		$attributes
2020
	)->affected_rows();
2021
}
2022
2023
/**
2024
 * Retrieve the locked or sticky status of a topic.
2025
 *
2026 10
 * @param int|int[] $id_topic topic to get the status for
2027
 * @param string|string[] $attributes Basically the column names
2028 10
 * @return array named array based on attributes requested
2029 10
 */
2030
function topicAttribute($id_topic, $attributes)
2031 10
{
2032 10
	$db = database();
2033 10
2034
	// @todo maybe add a filer for known attributes... or not
2035
// 	$attributes = array(
2036 10
// 		'locked' => 'locked',
2037
// 		'sticky' => 'is_sticky',
2038
// 	);
2039
2040
	// check the lock status
2041 10
	$request = $db->query('', '
2042
		SELECT 
2043 10
			{raw:attribute}
2044
		FROM {db_prefix}topics
2045
		WHERE id_topic IN ({array_int:current_topic})',
2046 10
		[
2047
			'current_topic' => (array) $id_topic,
2048 5
			'attribute' => implode(',', (array) $attributes),
2049 10
		]
2050
	);
2051
2052
	if (is_array($id_topic))
2053
	{
2054
		$status = [];
2055
		while (($row = $request->fetch_assoc()))
2056
		{
2057
			$status[] = $row;
2058
		}
2059
	}
2060
	else
2061
	{
2062 12
		$status = $request->fetch_assoc();
2063
	}
2064
2065
	$request->free_result();
2066
2067
	return $status;
2068
}
2069
2070
/**
2071 12
 * Retrieve some topic attributes based on the user:
2072
 *   - locked
2073
 *   - notify
2074
 *   - is_sticky
2075
 *   - id_poll
2076
 *   - id_last_msg
2077 12
 *   - id_member of the first message in the topic
2078 12
 *   - id_first_msg
2079
 *   - subject of the first message in the topic
2080
 *   - last_post_time that is poster_time if poster_time > modified_time, or
2081
 *       modified_time otherwise
2082 12
 *
2083
 * @param int $id_topic topic to get the status for
2084 12
 * @param int $user a user id
2085 12
 * @return array
2086
 */
2087 12
function topicUserAttributes($id_topic, $user)
2088
{
2089
	$db = database();
2090
2091
	$request = $db->query('', '
2092 6
		SELECT
2093
			t.locked, COALESCE(ln.id_topic, 0) AS notify, t.is_sticky, t.id_poll,
2094
			t.id_last_msg, mf.id_member, t.id_first_msg, mf.subject,
2095 12
			CASE WHEN ml.poster_time > ml.modified_time THEN ml.poster_time ELSE ml.modified_time END AS last_post_time
2096
		FROM {db_prefix}topics AS t
2097 12
			LEFT JOIN {db_prefix}log_notify AS ln ON (ln.id_topic = t.id_topic AND ln.id_member = {int:current_member})
2098
			LEFT JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
2099
			LEFT JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2100
		WHERE t.id_topic = {int:current_topic}
2101
		LIMIT 1',
2102
		[
2103
			'current_member' => $user,
2104
			'current_topic' => $id_topic,
2105
		]
2106
	);
2107
	$return = $request->fetch_assoc();
2108
	$request->free_result();
2109
2110
	return $return;
2111
}
2112
2113
/**
2114
 * Retrieve some details about the topic
2115
 *
2116
 * @param int[] $topics an array of topic id
2117
 *
2118
 * @return array
2119
 */
2120
function topicsDetails($topics)
2121
{
2122
	return topicAttribute($topics, ['id_topic', 'id_member_started', 'id_board', 'locked', 'approved', 'unapproved_posts']);
2123
}
2124
2125
/**
2126
 * Toggle sticky status for the passed topics and logs the action.
2127
 *
2128
 * @param int[] $topics
2129
 * @param bool $log If true the action is logged
2130
 * @return int Number of topics toggled
2131
 */
2132
function toggleTopicSticky($topics, $log = false)
2133
{
2134
	$db = database();
2135
2136
	$topics = is_array($topics) ? $topics : [$topics];
0 ignored issues
show
introduced by
The condition is_array($topics) is always true.
Loading history...
2137
2138
	$toggled = $db->query('', '
2139
		UPDATE {db_prefix}topics
2140
		SET 
2141
			is_sticky = CASE WHEN is_sticky = 1 THEN 0 ELSE 1 END
2142
		WHERE id_topic IN ({array_int:sticky_topic_ids})',
2143
		[
2144
			'sticky_topic_ids' => $topics,
2145
		]
2146
	)->affected_rows();
2147
2148
	if ($log)
2149
	{
2150
		// Get the board IDs and Sticky status
2151
		$topicAttributes = topicAttribute($topics, ['id_topic', 'id_board', 'is_sticky']);
2152
		$stickyCacheBoards = [];
2153
		$stickyCacheStatus = [];
2154
		foreach ($topicAttributes as $row)
2155
		{
2156
			$stickyCacheBoards[$row['id_topic']] = $row['id_board'];
2157
			$stickyCacheStatus[$row['id_topic']] = empty($row['is_sticky']);
2158
		}
2159
2160
		foreach ($topics as $topic)
2161
		{
2162
			logAction($stickyCacheStatus[$topic] ? 'unsticky' : 'sticky', ['topic' => $topic, 'board' => $stickyCacheBoards[$topic]]);
2163
			sendNotifications($topic, 'sticky');
2164
		}
2165
	}
2166
2167
	return $toggled;
2168
}
2169
2170
/**
2171
 * Get topics from the log_topics table belonging to a certain user
2172
 *
2173
 * @param int $member a member id
2174
 * @param int[] $topics an array of topics
2175
 * @return array an array of topics in the table (key) and its unwatched status (value)
2176
 *
2177
 * @todo find a better name
2178
 */
2179
function getLoggedTopics($member, $topics)
2180
{
2181
	$db = database();
2182
2183
	$logged_topics = [];
2184
	$db->query('', '
2185
		SELECT 
2186
			id_topic, id_msg, unwatched
2187
		FROM {db_prefix}log_topics
2188
		WHERE id_topic IN ({array_int:selected_topics})
2189
			AND id_member = {int:current_user}',
2190
		[
2191
			'selected_topics' => $topics,
2192
			'current_user' => $member,
2193
		]
2194
	)->fetch_callback(
2195
		function ($row) use (&$logged_topics) {
2196
			$logged_topics[$row['id_topic']] = $row;
2197
		}
2198
	);
2199
2200
	return $logged_topics;
2201
}
2202
2203
/**
2204
 * Returns a list of topics ids and their subjects
2205
 *
2206
 * @param int[] $topic_ids
2207
 *
2208
 * @return array
2209
 */
2210
function topicsList($topic_ids)
2211
{
2212
	global $modSettings;
2213
2214
	// you have to want *something* from this function
2215 2
	if (empty($topic_ids))
2216
	{
2217 2
		return [];
2218 2
	}
2219
2220
	$db = database();
2221
2222
	$topics = [];
2223
2224
	$db->fetchQuery('
2225 2
		SELECT 
2226 2
			t.id_topic, m.subject
2227
		FROM {db_prefix}topics AS t
2228 2
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
2229
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
2230
		WHERE {query_see_board}
2231 2
			AND t.id_topic IN ({array_int:topic_list})' . ($modSettings['postmod_active'] ? '
2232
			AND t.approved = {int:is_approved}' : '') . '
2233
		LIMIT {int:limit}',
2234 2
		[
2235
			'topic_list' => $topic_ids,
2236
			'is_approved' => 1,
2237
			'limit' => count($topic_ids),
2238
		]
2239
	)->fetch_callback(
2240
		function ($row) use (&$topics) {
2241
			$topics[$row['id_topic']] = [
2242
				'id_topic' => $row['id_topic'],
2243
				'subject' => censor($row['subject']),
2244
			];
2245
		}
2246
	);
2247
2248
	return $topics;
2249
}
2250
2251
/**
2252
 * Get each post and poster in this topic and take care of user settings such as
2253
 * limit or sort direction.
2254
 *
2255
 * @param int $topic
2256
 * @param array $limit
2257
 * @param bool $sort set to false for a desc sort
2258
 * @return array
2259
 */
2260
function getTopicsPostsAndPoster($topic, $limit, $sort)
2261
{
2262
	global $modSettings;
2263
2264
	$db = database();
2265
2266
	$topic_details = [
2267
		'messages' => [],
2268
		'all_posters' => [],
2269
	];
2270
2271
	// When evaluating potentially huge offsets, grab the ids only, first.
2272
	// The performance impact is still significant going from three columns to one.
2273
	$postMod = $modSettings['postmod_active'] && allowedTo('approve_posts');
2274
	$request = $db->fetchQuery('
2275
		SELECT 
2276
			m.id_msg, m.id_member
2277
		FROM (
2278
			SELECT 
2279
				id_msg 
2280
			FROM {db_prefix}messages
2281
			WHERE id_topic = {int:current_topic}' . ($postMod ? '' : '
2282
			AND (approved = {int:is_approved}' . (User::$info->is_guest ? '' : ' OR id_member = {int:current_member}') . ')') . '
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...
2283
			ORDER BY id_msg ' . ($sort ? '' : 'DESC') . ($limit['messages_per_page'] == -1 ? '' : '
2284
			LIMIT ' . $limit['start'] . ', ' . $limit['offset']) . '
2285
		) AS o 
2286
		JOIN {db_prefix}messages as m ON o.id_msg=m.id_msg
2287
		ORDER BY m.id_msg ' . ($sort ? '' : 'DESC'),
2288
		[
2289
			'current_member' => User::$info->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...
2290
			'current_topic' => $topic,
2291
			'is_approved' => 1,
2292
			'blank_id_member' => 0,
2293
		]
2294
	);
2295
	while ($row = $request->fetch_assoc())
2296
	{
2297
		if (!empty($row['id_member']))
2298 2
		{
2299
			$topic_details['all_posters'][$row['id_msg']] = $row['id_member'];
2300 2
		}
2301
2302
		$topic_details['messages'][] = $row['id_msg'];
2303 2
	}
2304
	$request->free_result();
2305
2306
	return $topic_details;
2307 2
}
2308
2309
/**
2310
 * Remove a batch of messages (or topics)
2311 2
 *
2312 2
 * @param int[] $messages
2313 2
 * @param array $messageDetails
2314 2
 * @param string $type = replies
2315
 */
2316 2
function removeMessages($messages, $messageDetails, $type = 'replies')
2317 2
{
2318 2
	global $modSettings;
2319 2
2320
	// @todo something's not right, removeMessage() does check permissions,
2321 2
	// removeTopics() doesn't
2322
	if ($type === 'topics')
2323 2
	{
2324
		removeTopics($messages);
2325
2326
		// and tell the world about it
2327
		foreach ($messages as $topic)
2328 2
		{
2329 2
			// Note, only log topic ID in native form if it's not gone forever.
2330
			logAction('remove', [
2331
				(empty($modSettings['recycle_enable']) || $modSettings['recycle_board'] != $messageDetails[$topic]['board'] ? 'topic' : 'old_topic_id') => $topic, 'subject' => $messageDetails[$topic]['subject'], 'member' => $messageDetails[$topic]['member'], 'board' => $messageDetails[$topic]['board']]);
2332 2
		}
2333
	}
2334
	else
2335
	{
2336
		$remover = new MessagesDelete($modSettings['recycle_enable'], $modSettings['recycle_board']);
2337
		foreach ($messages as $post)
2338
		{
2339
			$remover->removeMessage($post);
2340
		}
2341
	}
2342
}
2343
2344
/**
2345
 * Approve a batch of posts (or topics in their own right)
2346
 *
2347
 * @param int[] $messages
2348
 * @param array $messageDetails
2349
 * @param string $type = replies
2350
 */
2351
function approveMessages($messages, $messageDetails, $type = 'replies')
2352
{
2353
	if ($type === 'topics')
2354
	{
2355
		approveTopics($messages, true, true);
2356
	}
2357
	else
2358
	{
2359
		require_once(SUBSDIR . '/Post.subs.php');
2360
		approvePosts($messages);
2361
2362
		// and tell the world about it again
2363
		foreach ($messages as $post)
2364
		{
2365
			logAction('approve', ['topic' => $messageDetails[$post]['topic'], 'subject' => $messageDetails[$post]['subject'], 'member' => $messageDetails[$post]['member'], 'board' => $messageDetails[$post]['board']]);
2366
		}
2367
	}
2368
}
2369
2370
/**
2371
 * Approve topics, all we got.
2372
 *
2373
 * @param int[] $topics array of topics ids
2374
 * @param bool $approve = true
2375
 * @param bool $log if true logs the action.
2376
 *
2377
 * @return bool|void
2378
 */
2379
function approveTopics($topics, $approve = true, $log = false)
2380
{
2381
	if (!is_array($topics))
0 ignored issues
show
introduced by
The condition is_array($topics) is always true.
Loading history...
2382
	{
2383
		$topics = [$topics];
2384
	}
2385
2386
	if (empty($topics))
2387
	{
2388
		return false;
2389
	}
2390
2391
	$db = database();
2392
2393
	$approve_type = $approve ? 0 : 1;
2394
2395
	if ($log)
2396
	{
2397
		$log_action = $approve ? 'approve_topic' : 'unapprove_topic';
2398
2399
		// We need unapproved topic ids, their authors and the subjects!
2400
		$db->fetchQuery('
2401
			SELECT 
2402
				t.id_topic, t.id_member_started, m.subject
2403
			FROM {db_prefix}topics as t
2404
				LEFT JOIN {db_prefix}messages AS m ON (t.id_first_msg = m.id_msg)
2405
			WHERE t.id_topic IN ({array_int:approve_topic_ids})
2406
				AND t.approved = {int:approve_type}
2407
			LIMIT ' . count($topics),
2408
			[
2409
				'approve_topic_ids' => $topics,
2410
				'approve_type' => $approve_type,
2411
			]
2412
		)->fetch_callback(
2413
			function ($row) use (&$log_action) {
2414
				global $board;
2415
2416
				logAction($log_action, [
2417
					'topic' => $row['id_topic'],
2418
					'subject' => $row['subject'],
2419
					'member' => $row['id_member_started'],
2420
					'board' => $board]
2421
				);
2422
			}
2423
		);
2424
	}
2425
2426
	// Just get the messages to be approved and pass through...
2427
	$msgs = [];
2428
	$db->fetchQuery('
2429
		SELECT 
2430
			id_msg
2431
		FROM {db_prefix}messages
2432
		WHERE id_topic IN ({array_int:topic_list})
2433
			AND approved = {int:approve_type}',
2434
		[
2435
			'topic_list' => $topics,
2436
			'approve_type' => $approve_type,
2437
		]
2438
	)->fetch_callback(
2439
		function ($row) use (&$msgs) {
2440
			$msgs[] = $row['id_msg'];
2441
		}
2442
	);
2443
2444
	require_once(SUBSDIR . '/Post.subs.php');
2445
2446
	return approvePosts($msgs, $approve);
2447
}
2448
2449
/**
2450
 * Post a message at the end of the original topic
2451
 *
2452
 * @param string $reason the text that will become the message body
2453
 * @param string $subject the text that will become the message subject
2454
 * @param array $board_info some board information (at least id, name, if posts are counted)
2455
 * @param string $new_topic used to build the url for moving to a new topic
2456
 */
2457
function postSplitRedirect($reason, $subject, $board_info, $new_topic)
2458
{
2459
	global $language, $txt, $topic, $board;
2460
2461
	// Should be in the boardwide language.
2462
	if (User::$info->language != $language)
0 ignored issues
show
Bug Best Practice introduced by
The property language does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2463
	{
2464
		$lang_loader = new LangLoader($language, $txt, database());
2465
		$lang_loader->load('index');
2466
	}
2467
2468
	preparsecode($reason);
2469
2470
	// Add a URL onto the message.
2471
	$reason = strtr($reason, [
2472
		$txt['movetopic_auto_board'] => '[url=' . getUrl('board', ['board' => $board_info['id'], 'start' => '0', 'name' => $board_info['name']]) . ']' . $board_info['name'] . '[/url]',
2473
		$txt['movetopic_auto_topic'] => '[iurl]' . getUrl('topic', ['topic' => $new_topic, 'start' => '0', 'subject' => $subject]) . '[/iurl]'
2474
	]);
2475
2476
	$msgOptions = [
2477
		'subject' => $txt['split'] . ': ' . strtr(Util::htmltrim(Util::htmlspecialchars($subject)), ["\r" => '', "\n" => '', "\t" => '']),
2478
		'body' => $reason,
2479
		'icon' => 'moved',
2480
		'smileys_enabled' => 1,
2481
	];
2482
2483
	$topicOptions = [
2484
		'id' => $topic,
2485
		'board' => $board,
2486
		'mark_as_read' => true,
2487
	];
2488
2489
	$posterOptions = [
2490
		'id' => User::$info->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...
2491
		'update_post_count' => empty($board_info['count_posts']),
2492
	];
2493
2494
	createPost($msgOptions, $topicOptions, $posterOptions);
2495
}
2496
2497
/**
2498
 * General function to split off a topic.
2499
 * creates a new topic and moves the messages with the IDs in
2500
 * array messagesToBeSplit to the new topic.
2501
 * the subject of the newly created topic is set to 'newSubject'.
2502
 * marks the newly created message as read for the user splitting it.
2503
 * updates the statistics to reflect a newly created topic.
2504
 * logs the action in the moderation log.
2505
 * a notification is sent to all users monitoring this topic.
2506
 *
2507
 * @param int $split1_ID_TOPIC
2508
 * @param int[] $splitMessages
2509
 * @param string $new_subject
2510
 *
2511
 * @return int the topic ID of the new split topic.
2512
 * @throws \ElkArte\Exceptions\Exception no_posts_selected, selected_all_posts, cant_find_message
2513
 */
2514
function splitTopic($split1_ID_TOPIC, $splitMessages, $new_subject)
2515
{
2516
	global $txt, $modSettings;
2517
2518
	$db = database();
2519
2520
	// Nothing to split?
2521
	if (empty($splitMessages))
2522
	{
2523
		throw new \ElkArte\Exceptions\Exception('no_posts_selected', false);
2524
	}
2525
2526
	// Get some board info.
2527
	$topicAttribute = topicAttribute($split1_ID_TOPIC, ['id_board', 'approved']);
2528
	$id_board = $topicAttribute['id_board'];
2529
	$split1_approved = $topicAttribute['approved'];
2530
2531
	// Find the new first and last not in the list. (old topic)
2532
	$request = $db->query('', '
2533
		SELECT
2534
			MIN(m.id_msg) AS myid_first_msg, MAX(m.id_msg) AS myid_last_msg, COUNT(*) AS message_count, m.approved
2535
		FROM {db_prefix}messages AS m
2536
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:id_topic})
2537
		WHERE m.id_msg NOT IN ({array_int:no_msg_list})
2538
			AND m.id_topic = {int:id_topic}
2539
		GROUP BY m.approved
2540
		ORDER BY m.approved DESC
2541
		LIMIT 2',
2542
		[
2543
			'id_topic' => $split1_ID_TOPIC,
2544
			'no_msg_list' => $splitMessages,
2545
		]
2546
	);
2547
	// You can't select ALL the messages!
2548
	if ($request->num_rows() === 0)
2549
	{
2550
		throw new \ElkArte\Exceptions\Exception('selected_all_posts', false);
2551
	}
2552
2553
	$split1_first_msg = null;
2554
	$split1_last_msg = null;
2555
2556
	while (($row = $request->fetch_assoc()))
2557
	{
2558
		// Get the right first and last message dependant on approved state...
2559
		if (empty($split1_first_msg) || $row['myid_first_msg'] < $split1_first_msg)
2560
		{
2561
			$split1_first_msg = $row['myid_first_msg'];
2562
		}
2563
2564
		if (empty($split1_last_msg) || $row['approved'])
2565
		{
2566
			$split1_last_msg = $row['myid_last_msg'];
2567
		}
2568
2569
		// Get the counts correct...
2570
		if ($row['approved'])
2571
		{
2572
			$split1_replies = $row['message_count'] - 1;
2573
			$split1_unapprovedposts = 0;
2574
		}
2575
		else
2576
		{
2577
			if (!isset($split1_replies))
2578
			{
2579
				$split1_replies = 0;
2580
			}
2581
			// If the topic isn't approved then num replies must go up by one... as first post wouldn't be counted.
2582
			elseif (!$split1_approved)
2583
			{
2584
				$split1_replies++;
2585
			}
2586
2587
			$split1_unapprovedposts = $row['message_count'];
2588
		}
2589
	}
2590
	$request->free_result();
2591
	$split1_firstMem = getMsgMemberID($split1_first_msg);
2592
	$split1_lastMem = getMsgMemberID($split1_last_msg);
2593
2594
	// Find the first and last in the list. (new topic)
2595
	$request = $db->query('', '
2596
		SELECT 
2597
			MIN(id_msg) AS myid_first_msg, MAX(id_msg) AS myid_last_msg, COUNT(*) AS message_count, approved
2598
		FROM {db_prefix}messages
2599
		WHERE id_msg IN ({array_int:msg_list})
2600
			AND id_topic = {int:id_topic}
2601
		GROUP BY id_topic, approved
2602
		ORDER BY approved DESC
2603
		LIMIT 2',
2604
		[
2605
			'msg_list' => $splitMessages,
2606
			'id_topic' => $split1_ID_TOPIC,
2607
		]
2608
	);
2609
	while (($row = $request->fetch_assoc()))
2610
	{
2611
		// As before get the right first and last message dependant on approved state...
2612
		if (empty($split2_first_msg) || $row['myid_first_msg'] < $split2_first_msg)
2613
		{
2614
			$split2_first_msg = $row['myid_first_msg'];
2615
		}
2616
2617
		if (empty($split2_last_msg) || $row['approved'])
2618
		{
2619
			$split2_last_msg = $row['myid_last_msg'];
2620
		}
2621
2622
		// Then do the counts again...
2623
		if ($row['approved'])
2624
		{
2625
			$split2_approved = true;
2626
			$split2_replies = $row['message_count'] - 1;
2627
			$split2_unapprovedposts = 0;
2628
		}
2629
		else
2630
		{
2631
			// Should this one be approved??
2632
			if ($split2_first_msg == $row['myid_first_msg'])
2633
			{
2634
				$split2_approved = false;
2635
			}
2636
2637
			if (!isset($split2_replies))
2638
			{
2639
				$split2_replies = 0;
2640
			}
2641
			// As before, fix number of replies.
2642
			elseif (!$split2_approved)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $split2_approved does not seem to be defined for all execution paths leading up to this point.
Loading history...
2643
			{
2644
				$split2_replies++;
2645
			}
2646
2647
			$split2_unapprovedposts = $row['message_count'];
2648
		}
2649
	}
2650
	$request->free_result();
2651
	$split2_firstMem = getMsgMemberID($split2_first_msg);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $split2_first_msg does not seem to be defined for all execution paths leading up to this point.
Loading history...
2652
	$split2_lastMem = getMsgMemberID($split2_last_msg);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $split2_last_msg does not seem to be defined for all execution paths leading up to this point.
Loading history...
2653
2654
	// No database changes yet, so let's double check to see if everything makes at least a little sense.
2655
	if (!isset($split1_approved, $split2_approved) || $split1_first_msg <= 0 || $split1_last_msg <= 0 || $split2_first_msg <= 0 || $split2_last_msg <= 0 || $split1_replies < 0 || $split2_replies < 0 || $split1_unapprovedposts < 0 || $split2_unapprovedposts < 0)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $split1_replies does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $split2_unapprovedposts does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $split1_unapprovedposts does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $split2_replies does not seem to be defined for all execution paths leading up to this point.
Loading history...
2656
	{
2657
		throw new \ElkArte\Exceptions\Exception('cant_find_messages');
2658
	}
2659
2660
	// You cannot split off the first message of a topic.
2661
	if ($split1_first_msg > $split2_first_msg)
2662
	{
2663
		throw new \ElkArte\Exceptions\Exception('split_first_post', false);
2664
	}
2665
2666
	// The message that is starting the new topic may have likes, these become topic likes
2667
	require_once(SUBSDIR . '/Likes.subs.php');
2668
	$split2_first_msg_likes = messageLikeCount($split2_first_msg);
2669
2670
	// We're off to insert the new topic!  Use 0 for now to avoid UNIQUE errors.
2671
	$db->insert('',
2672
		'{db_prefix}topics',
2673
		[
2674
			'id_board' => 'int',
2675
			'id_member_started' => 'int',
2676
			'id_member_updated' => 'int',
2677
			'id_first_msg' => 'int',
2678
			'id_last_msg' => 'int',
2679
			'num_replies' => 'int',
2680
			'unapproved_posts' => 'int',
2681
			'approved' => 'int',
2682
			'is_sticky' => 'int',
2683
			'num_likes' => 'int',
2684
		],
2685
		[
2686
			(int) $id_board, $split2_firstMem, $split2_lastMem, 0,
2687
			0, $split2_replies, $split2_unapprovedposts, (int) $split2_approved, 0, $split2_first_msg_likes,
2688
		],
2689
		['id_topic']
2690
	);
2691
	$split2_ID_TOPIC = $db->insert_id('{db_prefix}topics');
2692
	if ($split2_ID_TOPIC <= 0)
2693
	{
2694
		throw new \ElkArte\Exceptions\Exception('cant_insert_topic');
2695
	}
2696
2697
	// Move the messages over to the other topic.
2698
	$new_subject = strtr(Util::htmltrim(Util::htmlspecialchars($new_subject)), ["\r" => '', "\n" => '', "\t" => '']);
2699
2700
	// Check the subject length.
2701
	if (Util::strlen($new_subject) > 100)
2702
	{
2703
		$new_subject = Util::substr($new_subject, 0, 100);
2704
	}
2705
2706
	// Valid subject?
2707
	if ($new_subject != '')
2708
	{
2709
		$db->query('', '
2710
			UPDATE {db_prefix}messages
2711
			SET
2712
				id_topic = {int:id_topic},
2713
				subject = CASE WHEN id_msg = {int:split_first_msg} THEN {string:new_subject} ELSE {string:new_subject_replies} END
2714
			WHERE id_msg IN ({array_int:split_msgs})',
2715
			[
2716
				'split_msgs' => $splitMessages,
2717
				'id_topic' => $split2_ID_TOPIC,
2718
				'new_subject' => $new_subject,
2719
				'split_first_msg' => $split2_first_msg,
2720
				'new_subject_replies' => $txt['response_prefix'] . $new_subject,
2721
			]
2722
		);
2723
2724
		// Cache the new topics subject... we can do it now as all the subjects are the same!
2725
		require_once(SUBSDIR . '/Messages.subs.php');
2726
		updateSubjectStats($split2_ID_TOPIC, $new_subject);
0 ignored issues
show
Bug introduced by
It seems like $split2_ID_TOPIC can also be of type boolean; however, parameter $id_topic of updateSubjectStats() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

2726
		updateSubjectStats(/** @scrutinizer ignore-type */ $split2_ID_TOPIC, $new_subject);
Loading history...
2727
	}
2728
2729
	// Any associated reported posts better follow...
2730
	require_once(SUBSDIR . '/Topic.subs.php');
2731
	updateSplitTopics([
2732
		'splitMessages' => $splitMessages,
2733
		'split1_replies' => $split1_replies,
2734
		'split1_first_msg' => $split1_first_msg,
2735
		'split1_last_msg' => $split1_last_msg,
2736
		'split1_firstMem' => $split1_firstMem,
2737
		'split1_lastMem' => $split1_lastMem,
2738
		'split1_unapprovedposts' => $split1_unapprovedposts,
2739
		'split1_ID_TOPIC' => $split1_ID_TOPIC,
2740
		'split2_first_msg' => $split2_first_msg,
2741
		'split2_last_msg' => $split2_last_msg,
2742
		'split2_ID_TOPIC' => $split2_ID_TOPIC,
2743
		'split2_approved' => $split2_approved,
2744
	], $id_board);
2745
2746
	require_once(SUBSDIR . '/FollowUps.subs.php');
2747
2748
	// Let's see if we can create a stronger bridge between the two topics
2749
	// @todo not sure what message from the oldest topic I should link to the new one, so I'll go with the first
2750
	linkMessages($split1_first_msg, $split2_ID_TOPIC);
0 ignored issues
show
Bug introduced by
It seems like $split2_ID_TOPIC can also be of type boolean; however, parameter $topic of linkMessages() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

2750
	linkMessages($split1_first_msg, /** @scrutinizer ignore-type */ $split2_ID_TOPIC);
Loading history...
2751
2752
	// Copy log topic entries.
2753
	// @todo This should really be chunked.
2754
	$replaceEntries = [];
2755
	$db->fetchQuery('
2756
		SELECT 
2757
			id_member, id_msg, unwatched
2758
		FROM {db_prefix}log_topics
2759
		WHERE id_topic = {int:id_topic}',
2760
		[
2761
			'id_topic' => (int) $split1_ID_TOPIC,
2762
		]
2763
	)->fetch_callback(
2764
		function ($row) use (&$replaceEntries, $split2_ID_TOPIC) {
2765
			$replaceEntries[] = [$row['id_member'], $split2_ID_TOPIC, $row['id_msg'], $row['unwatched']];
2766
		}
2767
	);
2768
2769
	if (!empty($replaceEntries))
2770
	{
2771
		require_once(SUBSDIR . '/Topic.subs.php');
2772
		markTopicsRead($replaceEntries, false);
2773
		unset($replaceEntries);
2774
	}
2775
2776
	// Housekeeping.
2777
	updateTopicStats();
2778
	updateLastMessages($id_board);
2779
2780
	logAction('split', ['topic' => $split1_ID_TOPIC, 'new_topic' => $split2_ID_TOPIC, 'board' => $id_board]);
2781
2782
	// Notify people that this topic has been split?
2783
	require_once(SUBSDIR . '/Notification.subs.php');
2784
	sendNotifications($split1_ID_TOPIC, 'split');
2785
2786
	// If there's a search index that needs updating, update it...
2787
	$searchAPI = new SearchApiWrapper(!empty($modSettings['search_index']) ? $modSettings['search_index'] : '');
2788
	$searchAPI->topicSplit($split2_ID_TOPIC, $splitMessages);
0 ignored issues
show
Bug introduced by
It seems like $split2_ID_TOPIC can also be of type boolean; however, parameter $split2_ID_TOPIC of ElkArte\Search\SearchApiWrapper::topicSplit() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

2788
	$searchAPI->topicSplit(/** @scrutinizer ignore-type */ $split2_ID_TOPIC, $splitMessages);
Loading history...
2789
2790
	// Return the ID of the newly created topic.
2791
	return $split2_ID_TOPIC;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $split2_ID_TOPIC also could return the type boolean which is incompatible with the documented return type integer.
Loading history...
2792
}
2793
2794
/**
2795
 * If we are also moving the topic somewhere else, let's try do to it
2796
 * Includes checks for permissions move_own/any, etc.
2797
 *
2798
 * @param array $boards an array containing basic info of the origin and destination boards (from splitDestinationBoard)
2799
 * @param int $totopic id of the destination topic
2800
 */
2801
function splitAttemptMove($boards, $totopic)
2802
{
2803
	global $board;
2804
2805
	$db = database();
2806
2807
	// If the starting and final boards are different we have to check some permissions and stuff
2808
	if ($boards['destination']['id'] != $board)
2809
	{
2810
		$doMove = false;
2811
		if (allowedTo('move_any'))
2812
		{
2813
			$doMove = true;
2814
		}
2815
		else
2816
		{
2817
			$new_topic = getTopicInfo($totopic);
2818
			if ($new_topic['id_member_started'] == User::$info->id && allowedTo('move_own'))
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...
2819
			{
2820
				$doMove = true;
2821
			}
2822
		}
2823
2824
		if ($doMove)
2825
		{
2826
			// Update member statistics if needed
2827
			// @todo this should probably go into a function...
2828
			if ($boards['destination']['count_posts'] != $boards['current']['count_posts'])
2829
			{
2830
				$posters = [];
2831
				$db->fetchQuery('
2832
					SELECT 
2833
						id_member
2834
					FROM {db_prefix}messages
2835
					WHERE id_topic = {int:current_topic}
2836
						AND approved = {int:is_approved}',
2837
					[
2838
						'current_topic' => $totopic,
2839
						'is_approved' => 1,
2840
					]
2841
				)->fetch_callback(
2842
					function ($row) use (&$posters) {
2843
						if (!isset($posters[$row['id_member']]))
2844
						{
2845
							$posters[$row['id_member']] = 0;
2846
						}
2847
2848
						$posters[$row['id_member']]++;
2849
					}
2850
				);
2851
2852
				require_once(SUBSDIR . '/Members.subs.php');
2853
				foreach ($posters as $id_member => $posts)
2854
				{
2855
					// The board we're moving from counted posts, but not to.
2856
					if (empty($boards['current']['count_posts']))
2857
					{
2858
						updateMemberData($id_member, ['posts' => 'posts - ' . $posts]);
2859
					}
2860
					// The reverse: from didn't, to did.
2861
					else
2862
					{
2863
						updateMemberData($id_member, ['posts' => 'posts + ' . $posts]);
2864
					}
2865
				}
2866
			}
2867
2868
			// And finally move it!
2869
			moveTopics($totopic, $boards['destination']['id']);
2870
		}
2871
		else
2872
		{
2873
			$boards['destination'] = $boards['current'];
2874
		}
2875
	}
2876
}
2877
2878
/**
2879
 * Retrieves information of the current and destination board of a split topic
2880
 *
2881
 * @param int $toboard
2882
 *
2883
 * @return array
2884
 * @throws \ElkArte\Exceptions\Exception no_board
2885
 */
2886
function splitDestinationBoard($toboard = 0)
2887
{
2888
	global $board, $topic;
2889
2890
	$current_board = boardInfo($board, $topic);
2891
	if (empty($current_board))
2892
	{
2893
		throw new \ElkArte\Exceptions\Exception('no_board');
2894
	}
2895
2896
	if (!empty($toboard) && $board !== $toboard)
2897
	{
2898
		$destination_board = boardInfo($toboard);
2899
		if (empty($destination_board))
2900
		{
2901
			throw new \ElkArte\Exceptions\Exception('no_board');
2902
		}
2903
	}
2904
2905
	if (!isset($destination_board))
2906
	{
2907
		$destination_board = array_merge($current_board, ['id' => $board]);
2908
	}
2909
	else
2910
	{
2911
		$destination_board['id'] = $toboard;
2912
	}
2913
2914
	return ['current' => $current_board, 'destination' => $destination_board];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $destination_board does not seem to be defined for all execution paths leading up to this point.
Loading history...
2915
}
2916
2917
/**
2918
 * Retrieve topic notifications count.
2919
 * (used by createList() callbacks, amongst others.)
2920
 *
2921
 * @param int $memID id_member
2922
 * @return int
2923
 */
2924
function topicNotificationCount($memID)
2925
{
2926
	global $modSettings;
2927
2928
	$db = database();
2929
2930
	$request = $db->query('', '
2931
		SELECT 
2932
			COUNT(*)
2933
		FROM {db_prefix}log_notify AS ln' . (!$modSettings['postmod_active'] && User::$info->query_see_board === '1=1' ? '' : '
0 ignored issues
show
Bug Best Practice introduced by
The property query_see_board does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2934
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)') . (User::$info->query_see_board === '1=1' ? '' : '
2935
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)') . '
2936
		WHERE ln.id_member = {int:selected_member}' . (User::$info->query_see_board === '1=1' ? '' : '
2937
			AND {query_see_board}') . ($modSettings['postmod_active'] ? '
2938
			AND t.approved = {int:is_approved}' : ''),
2939
		[
2940
			'selected_member' => $memID,
2941
			'is_approved' => 1,
2942
		]
2943
	);
2944
	list ($totalNotifications) = $request->fetch_row();
2945
	$request->free_result();
2946
2947
	return (int) $totalNotifications;
2948
}
2949
2950
/**
2951
 * Retrieve all topic notifications for the given user.
2952
 * (used by createList() callbacks)
2953
 *
2954
 * @param int $start The item to start with (for pagination purposes)
2955
 * @param int $items_per_page The number of items to show per page
2956
 * @param string $sort A string indicating how to sort the results
2957 2
 * @param int $memID id_member
2958
 * @return array
2959 2
 */
2960
function topicNotifications($start, $items_per_page, $sort, $memID)
2961 2
{
2962
	global $modSettings;
2963
2964 2
	$db = database();
2965 2
2966 2
	// All the topics with notification on...
2967 2
	$notification_topics = [];
2968 2
	$db->query('', '
2969 2
		SELECT
2970
			COALESCE(lt.id_msg, lmr.id_msg, -1) + 1 AS new_from, b.id_board, b.name,
2971 2
			t.id_topic, ms.subject, ms.id_member, COALESCE(mem.real_name, ms.poster_name) AS real_name_col,
2972 2
			ml.id_msg_modified, ml.poster_time, ml.id_member AS id_member_updated,
2973
			COALESCE(mem2.real_name, ml.poster_name) AS last_real_name
2974
		FROM {db_prefix}log_notify AS ln
2975 2
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
2976 2
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})
2977
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
2978 2
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2979
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ms.id_member)
2980
			LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = ml.id_member)
2981
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
2982
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})
2983
		WHERE ln.id_member = {int:selected_member}
2984
		ORDER BY {raw:sort}
2985
		LIMIT {int:offset}, {int:items_per_page}',
2986
		[
2987
			'current_member' => User::$info->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...
2988
			'is_approved' => 1,
2989
			'selected_member' => $memID,
2990
			'sort' => $sort,
2991
			'offset' => $start,
2992
			'items_per_page' => $items_per_page,
2993
		]
2994 2
	)->fetch_callback(
2995
		function ($row) use (&$notification_topics) {
2996 2
			$row['subject'] = censor($row['subject']);
2997
			$topic_href = getUrl('topic', ['topic' => $row['id_topic'], 'start' => '0', 'subject' => $row['subject']]);
2998
			$topic_new_href = getUrl('topic', ['topic' => $row['id_topic'], 'start' => 'msg' . $row['new_from'], 'subject' => $row['subject']]);
2999 2
3000 2
			$notification_topics[] = [
3001
				'id' => $row['id_topic'],
3002
				'poster_link' => empty($row['id_member']) ? $row['real_name_col'] : '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row['id_member'], 'name' => $row['real_name_col']]) . '">' . $row['real_name_col'] . '</a>',
3003
				'poster_updated_link' => empty($row['id_member_updated']) ? $row['last_real_name'] : '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row['id_member_updated'], 'name' => $row['last_real_name']]) . '">' . $row['last_real_name'] . '</a>',
3004
				'subject' => $row['subject'],
3005
				'href' => $topic_href,
3006
				'link' => '<a href="' . $topic_href . '">' . $row['subject'] . '</a>',
3007 2
				'new' => $row['new_from'] <= $row['id_msg_modified'],
3008
				'new_from' => $row['new_from'],
3009
				'updated' => standardTime($row['poster_time']),
3010
				'new_href' => $topic_new_href . '#new',
3011
				'new_link' => '<a href="' . $topic_new_href . '#new">' . $row['subject'] . '</a>',
3012
				'board_link' => '<a href="' . getUrl('board', ['board' => $row['id_board'], 'start' => '0', 'name' => $row['name']]) . '">' . $row['name'] . '</a>',
3013
			];
3014
		}
3015
	);
3016
3017
	return $notification_topics;
3018
}
3019 2
3020 2
/**
3021 2
 * Get a list of posters in this topic, and their posts counts in the topic.
3022 2
 * Used to update users posts counts when topics are moved or are deleted.
3023 2
 *
3024 2
 * @param int $id_topic topic id to work with
3025
 *
3026 2
 * @return array
3027
 */
3028
function postersCount($id_topic)
3029
{
3030
	$db = database();
3031
3032
	// We only care about approved topics, the rest don't count.
3033
	$posters = [];
3034
	$db->query('', '
3035
		SELECT 
3036
			id_member
3037
		FROM {db_prefix}messages
3038
		WHERE id_topic = {int:current_topic}
3039
			AND approved = {int:is_approved}',
3040
		[
3041
			'current_topic' => $id_topic,
3042
			'is_approved' => 1,
3043
		]
3044
	)->fetch_callback(
3045
		function ($row) use (&$posters) {
3046 2
			if (!isset($posters[$row['id_member']]))
3047
			{
3048
				$posters[$row['id_member']] = 0;
3049 2
			}
3050
3051
			$posters[$row['id_member']]++;
3052
		}
3053
	);
3054
3055
	return $posters;
3056
}
3057
3058
/**
3059
 * Counts topics from the given id_board.
3060
 *
3061
 * @param int $board
3062
 * @param bool $approved
3063
 * @return int
3064
 */
3065
function countTopicsByBoard($board, $approved = false)
3066
{
3067
	$db = database();
3068
3069
	// How many topics are on this board?  (used for paging.)
3070
	$request = $db->query('', '
3071
		SELECT 
3072
			COUNT(*)
3073
		FROM {db_prefix}topics AS t
3074
		WHERE t.id_board = {int:id_board}' . (empty($approved) ? '
3075
			AND t.approved = {int:is_approved}' : ''),
3076
		[
3077
			'id_board' => $board,
3078
			'is_approved' => 1,
3079
		]
3080
	);
3081
	list ($topics) = $request->fetch_row();
3082
	$request->free_result();
3083
3084
	return $topics;
3085
}
3086
3087
/**
3088
 * Determines topics which can be merged from a specific board.
3089
 *
3090
 * @param int $id_board
3091
 * @param int $id_topic
3092
 * @param bool $approved
3093
 * @param int $offset
3094
 * @return array
3095
 */
3096
function mergeableTopics($id_board, $id_topic, $approved, $offset)
3097
{
3098
	global $modSettings;
3099
3100
	$db = database();
3101
3102
	// Get some topics to merge it with.
3103
	$topics = [];
3104
	$db->fetchQuery('
3105
		SELECT 
3106
			t.id_topic, m.subject, m.id_member, COALESCE(mem.real_name, m.poster_name) AS poster_name
3107
		FROM (
3108
			SELECT t.id_topic 
3109
			FROM {db_prefix}topics AS t
3110
			WHERE t.id_board = {int:id_board}
3111
				AND t.id_topic != {int:id_topic}' . (empty($approved) ? '
3112
				AND t.approved = {int:is_approved}' : '') . '
3113
			ORDER BY t.is_sticky DESC, t.id_last_msg DESC
3114
			LIMIT {int:limit} OFFSET {int:offset} 
3115
		) AS o 
3116
	    INNER JOIN {db_prefix}topics AS t ON (o.id_topic = t.id_topic)
3117
	    INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
3118
		LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
3119
		ORDER BY t.is_sticky DESC, t.id_last_msg DESC',
3120
		[
3121
			'id_board' => $id_board,
3122
			'id_topic' => $id_topic,
3123
			'offset' => $offset,
3124
			'limit' => $modSettings['defaultMaxTopics'],
3125
			'is_approved' => 1,
3126
		]
3127
	)->fetch_callback(
3128
		function ($row) use (&$topics) {
3129
			$row['subject'] = censor($row['subject']);
3130
3131
			$href = getUrl('profile', ['action' => 'profile', 'u' => $row['id_member'], 'name' => $row['poster_name']]);
3132
			$topics[] = [
3133
				'id' => $row['id_topic'],
3134
				'poster' => [
3135
					'id' => $row['id_member'],
3136
					'name' => $row['poster_name'],
3137
					'href' => empty($row['id_member']) ? '' : $href,
3138
					'link' => empty($row['id_member']) ? $row['poster_name'] : '<a href="' . $href . '" target="_blank" class="new_win">' . $row['poster_name'] . '</a>'
3139
				],
3140
				'subject' => $row['subject'],
3141
				'js_subject' => addcslashes(addslashes($row['subject']), '/')
3142
			];
3143
		}
3144
	);
3145
3146
	return $topics;
3147
}
3148
3149
/**
3150
 * Determines all messages from a given array of topics.
3151
 *
3152
 * @param int[] $topics integer array of topics to work with
3153
 * @return array
3154
 */
3155
function messagesInTopics($topics)
3156
{
3157
	$db = database();
3158
3159
	$topics = is_array($topics) ? $topics : [$topics];
0 ignored issues
show
introduced by
The condition is_array($topics) is always true.
Loading history...
3160
3161
	// Obtain all the message ids we are going to affect.
3162
	$messages = [];
3163
	$db->fetchQuery('
3164
		SELECT 
3165
			id_msg
3166
		FROM {db_prefix}messages
3167
		WHERE id_topic IN ({array_int:topic_list})',
3168
		[
3169
			'topic_list' => $topics,
3170
		]
3171
	)->fetch_callback(
3172
		function ($row) use (&$messages) {
3173
			$messages[] = $row['id_msg'];
3174
		}
3175
	);
3176
3177
	return $messages;
3178
}
3179
3180
/**
3181
 * Retrieves the members that posted in a group of topics.
3182
 *
3183
 * @param int[] $topics integer array of topics to work with
3184
 * @return array of topics each member posted in (grouped by members)
3185
 */
3186
function topicsPosters($topics)
3187
{
3188 12
	$db = database();
3189
3190
	// Obtain all the member ids
3191 12
	$members = [];
3192 12
	$db->fetchQuery('
3193
		SELECT 
3194
			id_member, id_topic
3195
		FROM {db_prefix}messages
3196
		WHERE id_topic IN ({array_int:topic_list})',
3197
		[
3198 12
			'topic_list' => $topics,
3199
		]
3200 12
	)->fetch_callback(
3201
		function ($row) use (&$members) {
3202 12
			$members[$row['id_member']][] = $row['id_topic'];
3203 12
		}
3204
	);
3205
3206 12
	return $members;
3207
}
3208
3209
/**
3210
 * Updates all the tables involved when two or more topics are merged
3211
 *
3212
 * @param int $first_msg the first message of the new topic
3213
 * @param int[] $topics ids of all the topics merged
3214
 * @param int $id_topic id of the merged topic
3215
 * @param int $target_board id of the target board where the topic will resides
3216
 * @param string $target_subject subject of the new topic
3217
 * @param string $enforce_subject if not empty all the messages will be set to the same subject
3218
 * @param int[] $notifications array of topics with active notifications
3219
 */
3220
function fixMergedTopics($first_msg, $topics, $id_topic, $target_board, $target_subject, $enforce_subject, $notifications)
3221
{
3222
	$db = database();
3223
3224
	// Delete the remaining topics.
3225
	$deleted_topics = array_diff($topics, [$id_topic]);
3226
	$db->query('', '
3227
		DELETE FROM {db_prefix}topics
3228
		WHERE id_topic IN ({array_int:deleted_topics})',
3229
		[
3230
			'deleted_topics' => $deleted_topics,
3231
		]
3232
	);
3233
3234
	$db->query('', '
3235
		DELETE FROM {db_prefix}log_search_subjects
3236
		WHERE id_topic IN ({array_int:deleted_topics})',
3237
		[
3238
			'deleted_topics' => $deleted_topics,
3239
		]
3240
	);
3241
3242
	// Change the topic IDs of all messages that will be merged.  Also adjust subjects if 'enforce subject' was checked.
3243
	$db->query('', '
3244
		UPDATE {db_prefix}messages
3245
		SET
3246
			id_topic = {int:id_topic},
3247
			id_board = {int:target_board}' . (empty($enforce_subject) ? '' : ',
3248
			subject = {string:subject}') . '
3249
		WHERE id_topic IN ({array_int:topic_list})',
3250
		[
3251
			'topic_list' => $topics,
3252
			'id_topic' => $id_topic,
3253
			'target_board' => $target_board,
3254
			'subject' => response_prefix() . $target_subject,
3255
		]
3256
	);
3257
3258
	// Any reported posts should reflect the new board.
3259
	$db->query('', '
3260
		UPDATE {db_prefix}log_reported
3261
		SET
3262
			id_topic = {int:id_topic},
3263
			id_board = {int:target_board}
3264
		WHERE id_topic IN ({array_int:topics_list})',
3265
		[
3266
			'topics_list' => $topics,
3267
			'id_topic' => $id_topic,
3268
			'target_board' => $target_board,
3269
		]
3270
	);
3271
3272
	// Change the subject of the first message...
3273
	$db->query('', '
3274
		UPDATE {db_prefix}messages
3275
		SET 
3276
			subject = {string:target_subject}
3277
		WHERE id_msg = {int:first_msg}',
3278
		[
3279
			'first_msg' => $first_msg,
3280
			'target_subject' => $target_subject,
3281
		]
3282
	);
3283
3284
	// Adjust all calendar events to point to the new topic.
3285
	$db->query('', '
3286
		UPDATE {db_prefix}calendar
3287
		SET
3288
			id_topic = {int:id_topic},
3289
			id_board = {int:target_board}
3290
		WHERE id_topic IN ({array_int:deleted_topics})',
3291
		[
3292
			'deleted_topics' => $deleted_topics,
3293
			'id_topic' => $id_topic,
3294
			'target_board' => $target_board,
3295
		]
3296
	);
3297
3298
	// Merge log topic entries.
3299
	// The unwatched setting comes from the oldest topic
3300
	$request = $db->query('', '
3301
		SELECT 
3302
			id_member, unwatched, MIN(id_msg) AS new_id_msg
3303
		FROM {db_prefix}log_topics
3304
		WHERE id_topic IN ({array_int:topics})
3305
		GROUP BY id_member, unwatched',
3306
		[
3307
			'topics' => $topics,
3308
		]
3309
	);
3310
	if ($request->num_rows() > 0)
3311
	{
3312
		$replaceEntries = [];
3313
		while (($row = $request->fetch_assoc()))
3314
		{
3315
			$replaceEntries[] = [$row['id_member'], $id_topic, $row['new_id_msg'], $row['unwatched']];
3316
		}
3317
3318
		markTopicsRead($replaceEntries, true);
3319
		unset($replaceEntries);
3320
3321
		// Get rid of the old log entries.
3322
		$db->query('', '
3323
			DELETE FROM {db_prefix}log_topics
3324
			WHERE id_topic IN ({array_int:deleted_topics})',
3325
			[
3326
				'deleted_topics' => $deleted_topics,
3327
			]
3328
		);
3329
	}
3330
	$request->free_result();
3331
3332
	if (!empty($notifications))
3333
	{
3334
		$request = $db->query('', '
3335
			SELECT 
3336
				id_member, MAX(sent) AS sent
3337
			FROM {db_prefix}log_notify
3338
			WHERE id_topic IN ({array_int:topics_list})
3339
			GROUP BY id_member',
3340
			[
3341
				'topics_list' => $notifications,
3342
			]
3343
		);
3344
		if ($request->num_rows() > 0)
3345
		{
3346
			$replaceEntries = [];
3347
			while (($row = $request->fetch_assoc()))
3348
			{
3349
				$replaceEntries[] = [$row['id_member'], $id_topic, 0, $row['sent']];
3350
			}
3351
3352
			$db->replace(
3353
				'{db_prefix}log_notify',
3354
				['id_member' => 'int', 'id_topic' => 'int', 'id_board' => 'int', 'sent' => 'int'],
3355
				$replaceEntries,
3356
				['id_member', 'id_topic', 'id_board']
3357
			);
3358
			unset($replaceEntries);
3359
3360
			$db->query('', '
3361
				DELETE FROM {db_prefix}log_topics
3362
				WHERE id_topic IN ({array_int:deleted_topics})',
3363
				[
3364
					'deleted_topics' => $deleted_topics,
3365
				]
3366
			);
3367
		}
3368
		$request->free_result();
3369
	}
3370
}
3371
3372
/**
3373
 * Load the subject from a given topic id.
3374
 *
3375
 * @param int $id_topic
3376
 *
3377
 * @return string
3378
 * @throws \ElkArte\Exceptions\Exception topic_gone
3379
 */
3380
function getSubject($id_topic)
3381
{
3382
	global $modSettings;
3383
3384
	$db = database();
3385
3386
	$request = $db->query('', '
3387
		SELECT 
3388
			ms.subject
3389
		FROM {db_prefix}topics AS t
3390
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
3391
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
3392
		WHERE t.id_topic = {int:search_topic_id}
3393
			AND {query_see_board}' . ($modSettings['postmod_active'] ? '
3394
			AND t.approved = {int:is_approved_true}' : '') . '
3395
		LIMIT 1',
3396
		[
3397
			'is_approved_true' => 1,
3398
			'search_topic_id' => $id_topic,
3399
		]
3400
	);
3401
	if ($request->num_rows() === 0)
3402
	{
3403
		throw new \ElkArte\Exceptions\Exception('topic_gone', false);
3404
	}
3405
	list ($subject) = $request->fetch_row();
3406
	$request->free_result();
3407
3408
	return $subject;
3409
}
3410
3411
/**
3412
 * This function updates the total number of topics,
3413
 * or if parameter $increment is true it simply increments them.
3414
 *
3415
 * @param bool|null $increment = null if true, increment + 1 the total topics, otherwise recount all topics
3416
 */
3417
function updateTopicStats($increment = null)
3418
{
3419
	global $modSettings;
3420
3421
	$db = database();
3422
3423
	if ($increment === true)
3424
	{
3425
		updateSettings(['totalTopics' => true], true);
3426
	}
3427
	else
3428
	{
3429
		// Get the number of topics - a SUM is better for InnoDB tables.
3430
		// We also ignore the recycle bin here because there will probably be a bunch of one-post topics there.
3431
		$request = $db->query('', '
3432
			SELECT 
3433
				SUM(num_topics + unapproved_topics) AS total_topics
3434
			FROM {db_prefix}boards' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
3435
			WHERE id_board != {int:recycle_board}' : ''),
3436
			[
3437
				'recycle_board' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
3438
			]
3439
		);
3440
		$row = $request->fetch_assoc();
3441
		$request->free_result();
3442
3443
		updateSettings(['totalTopics' => $row['total_topics'] ?? 0]);
3444
	}
3445
}
3446
3447
/**
3448
 * Toggles the locked status of the passed id_topic's checking for permissions.
3449
 *
3450
 * @param int[] $topics The topics to lock (can be an id or an array of ids).
3451 24
 * @param bool $log if true logs the action.
3452
 */
3453 24
function toggleTopicsLock($topics, $log = false)
3454
{
3455 24
	global $board;
3456
3457 24
	$db = database();
3458
3459
	$needs_check = !empty($board) && !allowedTo('lock_any');
3460
	$lockCache = [];
3461
3462
	$topicAttribute = topicAttribute($topics, ['id_topic', 'locked', 'id_board', 'id_member_started']);
3463 12
3464
	foreach ($topicAttribute as $row)
3465
	{
3466 12
		// Skip the entry if it needs to be checked and the user is not the owen and
3467 12
		// the topic was not locked or locked by someone with more permissions
3468
		if ($needs_check && (User::$info->id != $row['id_member_started'] || !in_array($row['locked'], [0, 2])))
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...
3469 12
		{
3470
			continue;
3471
		}
3472 12
3473 12
		$lockCache[] = $row['id_topic'];
3474
3475 12
		if ($log)
3476
		{
3477 24
			$lockStatus = empty($row['locked']) ? 'lock' : 'unlock';
3478
3479
			logAction($lockStatus, ['topic' => $row['id_topic'], 'board' => $row['id_board']]);
3480
			sendNotifications($row['id_topic'], $lockStatus);
3481
		}
3482
	}
3483
3484
	// It could just be that *none* were their own topics...
3485
	if (!empty($lockCache))
3486
	{
3487
		// Alternate the locked value.
3488
		$db->query('', '
3489
			UPDATE {db_prefix}topics
3490
			SET 
3491
				locked = CASE WHEN locked = {int:is_locked} THEN ' . (allowedTo('lock_any') ? '1' : '2') . ' ELSE 0 END
3492
			WHERE id_topic IN ({array_int:locked_topic_ids})',
3493
			[
3494
				'locked_topic_ids' => $lockCache,
3495
				'is_locked' => 0,
3496
			]
3497
		);
3498
	}
3499
}
3500