moveTopics()   F
last analyzed

Complexity

Conditions 28
Paths 10373

Size

Total Lines 320
Code Lines 148

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 812

Importance

Changes 0
Metric Value
eloc 148
dl 0
loc 320
rs 0
c 0
b 0
f 0
cc 28
nc 10373
nop 3
ccs 0
cts 129
cp 0
crap 812

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, array('id_topic', 'id_board', 'id_member_started'));
39
40
	$removeCache = array();
41
	$removeCacheBoards = array();
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 = array())
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 = array($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', array((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
			array(
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'], array('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, array('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 = array();
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'], array(
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
					array(
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
					array(
189
						'recycle_topics' => $recycleTopics,
190
						'is_closed' => 1,
191
					)
192
				);
193
194
				updateSettings(array('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 = array();
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
		array(
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']] = array(
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
			array(
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 = array();
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
			array(
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
			array(
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
			array(
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 = array(
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 = array();
326
		$messages = array();
327
		$db->fetchQuery('
328
			SELECT 
329
				id_msg, body
330
			FROM {db_prefix}messages
331
			WHERE id_topic IN ({array_int:topics})',
332
			array(
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
				array(
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
			array(
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
		array(
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
		array(
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
		array(
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
		array(
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
		array(
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
		array(
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', array($topics));
450
451 12
	// Update the totals...
452
	require_once(SUBSDIR . '/Messages.subs.php');
453
	updateMessageStats();
454
	updateTopicStats();
455 12
	updateSettings(array(
456
		'calendar_updated' => time(),
457
	));
458 12
459 12
	require_once(SUBSDIR . '/Post.subs.php');
460 12
	$updates = array();
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 = array();
480
	$moveCache2 = array();
481
	$countPosts = array();
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
		array(
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] = array();
511
			}
512
513
			$moveTos[$to][] = $row['id_topic'];
514
515
			// For reporting...
516
			$moveCache2[] = array($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 = array();
531
		$boards_info = fetchBoardsInfo(array('boards' => array_keys($moveTos)), array('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, array('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 = array($topics);
603
	}
604
605
	$fromBoards = array();
606
	$fromCacheBoards = array();
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
		array(
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']] = array(
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 = array();
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
		array(
671
			'protect_lmr_msg' => $SaveAServer,
672
			'topics' => $topics,
673
		)
674
	)->fetch_callback(
675
		function ($row) use (&$log_topics) {
676
			$log_topics[] = array($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 = array();
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
			array(
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
		array(
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 = array(
744
			'id_board' => $toBoard,
745
			'approved' => 1,
746
			'unapproved_posts' => 0,
747
		);
748
	}
749
	else
750
	{
751
		$attributes = array('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 = array();
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
			array(
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
				array(
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, array('id_topic', 'id_first_msg', 'id_last_msg'));
793
		$topicMaxMin = array();
794
		foreach ($topicAttribute as $row)
795
		{
796
			$topicMaxMin[$row['id_topic']] = array(
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
			array(
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'], array(
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
		array(
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
		array(
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
		array(
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
		array(
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(array(
898
		'calendar_updated' => time(),
899
	));
900
901
	if ($log)
902
	{
903
		foreach ($topics as $topic)
904
		{
905
			logAction('move', array('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
	else
936
	{
937
		$request = $db->query('', '
938
			SELECT 
939
				m.subject, b.name
940
			FROM {db_prefix}topics AS t
941
				LEFT JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board)
942
				LEFT JOIN {db_prefix}messages AS m ON (t.id_first_msg = m.id_msg)
943
			WHERE t.id_topic = {int:topic_id}
944
			LIMIT 1',
945
			array(
946
				'topic_id' => $id_topic,
947
			)
948
		);
949
		list ($topic_subject, $board_name) = $request->fetch_row();
950
		$request->free_result();
951
952
		$board_link = '<a href="' . getUrl('board', ['board' => $id_board, 'start' => '0', 'name' => $board_name]) . '">' . $board_name . '</a>';
953
		$topic_link = '<a href="' . getUrl('topic', ['topic' => $id_topic, 'start' => '0', 'subject' => $topic_subject]) . '">' . $topic_subject . '</a>';
954
		throw new \ElkArte\Exceptions\Exception('topic_already_moved', false, array($topic_link, $board_link));
955
	}
956
}
957
958
/**
959
 * Determine if the topic has already been deleted by another user.
960
 *
961
 * What it does:
962
 *  - If the topic has been removed and resides in the recycle bin, present confirm dialog
963
 *  - If recycling is not enabled, or user confirms or topic is not in recycle simply returns
964
 *
965
 * @throws \ElkArte\Exceptions\Exception post_already_deleted
966
 */
967
function removeDeleteConcurrence()
968
{
969
	global $modSettings, $board, $context;
970
971
	$recycled_enabled = !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']);
972
973
	// Trying to remove from the recycle bin
974
	if ($recycled_enabled && !empty($board) && !isset($_GET['confirm_delete']) && $modSettings['recycle_board'] == $board)
975
	{
976
		if (isset($_REQUEST['msg']))
977
		{
978
			$confirm_url = getUrl('action', ['action' => 'deletemsg', 'confirm_delete', 'topic' => $context['current_topic'] . '.0', 'msg' => $_REQUEST['msg'], '{session_data}']);
979
		}
980
		else
981
		{
982
			$confirm_url = getUrl('action', ['action' => 'removetopic2', 'confirm_delete', 'topic' => $context['current_topic'] . '.0', '{session_data}']);
983
		}
984
985
		// Give them a prompt before we remove the message
986
		throw new \ElkArte\Exceptions\Exception('post_already_deleted', false, array($confirm_url));
987
	}
988
}
989
990
/**
991
 * Increase the number of views of this topic.
992
 *
993
 * @param int $id_topic the topic being viewed or whatnot.
994
 */
995
function increaseViewCounter($id_topic)
996
{
997
	$db = database();
998
999
	$db->query('', '
1000
		UPDATE {db_prefix}topics
1001
		SET 
1002
			num_views = num_views + 1
1003
		WHERE id_topic = {int:current_topic}',
1004
		array(
1005
			'current_topic' => $id_topic,
1006
		)
1007 2
	);
1008
}
1009 2
1010
/**
1011
 * Mark topic(s) as read by the given member, at the specified message.
1012
 *
1013
 * @param array $mark_topics array($id_member, $id_topic, $id_msg)
1014
 * @param bool $was_set = false - whether the topic has been previously read by the user
1015 2
 */
1016
function markTopicsRead($mark_topics, $was_set = false)
1017
{
1018 2
	$db = database();
1019
1020
	if (!is_array($mark_topics))
0 ignored issues
show
introduced by
The condition is_array($mark_topics) is always true.
Loading history...
1021
	{
1022
		return;
1023
	}
1024
1025
	$db->insert($was_set ? 'replace' : 'ignore',
1026
		'{db_prefix}log_topics',
1027
		array(
1028
			'id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int', 'unwatched' => 'int',
1029 12
		),
1030
		$mark_topics,
1031 12
		array('id_member', 'id_topic')
1032
	);
1033
}
1034
1035
/**
1036 12
 * Update user notifications for a topic... or the board it's in.
1037 12
 *
1038
 * @param int $id_topic
1039 12
 * @param int $id_board
1040
 * @todo look at board notification...
1041 6
 *
1042 12
 */
1043
function updateReadNotificationsFor($id_topic, $id_board)
1044 12
{
1045
	global $context;
1046
1047
	$db = database();
1048
1049
	// Check for notifications on this topic OR board.
1050
	$request = $db->query('', '
1051
		SELECT 
1052
			sent, id_topic
1053
		FROM {db_prefix}log_notify
1054
		WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
1055
			AND id_member = {int:current_member}
1056
		LIMIT 2',
1057 2
		array(
1058
			'current_board' => $id_board,
1059 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...
1060
			'current_topic' => $id_topic,
1061
		)
1062 2
	);
1063
	while (($row = $request->fetch_assoc()))
1064
	{
1065
		// Find if this topic is marked for notification...
1066
		if (!empty($row['id_topic']))
1067
		{
1068
			$context['is_marked_notify'] = true;
1069
		}
1070 2
1071 2
		// Only do this once, but mark the notifications as "not sent yet" for next time.
1072 2
		if (!empty($row['sent']))
1073
		{
1074
			$db->query('', '
1075 2
				UPDATE {db_prefix}log_notify
1076
				SET 
1077
					sent = {int:is_not_sent}
1078
				WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
1079
					AND id_member = {int:current_member}',
1080
				array(
1081
					'current_board' => $id_board,
1082
					'current_member' => User::$info->id,
1083
					'current_topic' => $id_topic,
1084
					'is_not_sent' => 0,
1085
				)
1086
			);
1087
1088
			break;
1089
		}
1090
	}
1091
	$request->free_result();
1092
}
1093
1094
/**
1095
 * How many topics are still unread since (last visit)
1096
 *
1097
 * @param int $id_board
1098
 * @param int $id_msg_last_visit
1099
 * @return int
1100
 */
1101
function getUnreadCountSince($id_board, $id_msg_last_visit)
1102
{
1103 2
	$db = database();
1104 2
1105
	$request = $db->query('', '
1106
		SELECT 
1107
			COUNT(*)
1108
		FROM {db_prefix}topics AS t
1109
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = {int:current_board} AND lb.id_member = {int:current_member})
1110
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
1111
		WHERE t.id_board = {int:current_board}
1112
			AND t.id_last_msg > COALESCE(lb.id_msg, 0)
1113
			AND t.id_last_msg > COALESCE(lt.id_msg, 0)' .
1114
		(empty($id_msg_last_visit) ? '' : '
1115
			AND t.id_last_msg > {int:id_msg_last_visit}'),
1116 2
		array(
1117
			'current_board' => $id_board,
1118 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...
1119
			'id_msg_last_visit' => (int) $id_msg_last_visit,
1120
		)
1121
	);
1122
	list ($unread) = $request->fetch_row();
1123
	$request->free_result();
1124
1125
	return $unread;
1126
}
1127 2
1128
/**
1129
 * Returns whether this member has notification turned on for the specified topic.
1130 2
 *
1131 2
 * @param int $id_member
1132 2
 * @param int $id_topic
1133
 * @return bool
1134
 */
1135 2
function hasTopicNotification($id_member, $id_topic)
1136 2
{
1137
	$db = database();
1138 2
1139
	// Find out if they have notification set for this topic already.
1140
	return $db->fetchQuery('
1141
		SELECT 
1142
			id_member
1143
		FROM {db_prefix}log_notify
1144
		WHERE id_member = {int:current_member}
1145
			AND id_topic = {int:current_topic}
1146
		LIMIT 1',
1147
		array(
1148
			'current_member' => $id_member,
1149
			'current_topic' => $id_topic,
1150
		)
1151
	)->num_rows() != 0;
1152
}
1153
1154
/**
1155
 * Set topic notification on or off for the given member.
1156
 *
1157
 * @param int $id_member
1158
 * @param int $id_topic
1159
 * @param bool $on
1160
 */
1161
function setTopicNotification($id_member, $id_topic, $on = false)
1162
{
1163
	$db = database();
1164
1165
	if ($on)
1166
	{
1167
		// Attempt to turn notifications on.
1168
		$db->insert('ignore',
1169
			'{db_prefix}log_notify',
1170
			array('id_member' => 'int', 'id_topic' => 'int'),
1171
			array($id_member, $id_topic),
1172
			array('id_member', 'id_topic')
1173
		);
1174
	}
1175
	else
1176
	{
1177
		// Just turn notifications off.
1178 4
		$db->query('', '
1179
			DELETE FROM {db_prefix}log_notify
1180 4
			WHERE id_member = {int:current_member}
1181
				AND id_topic = {int:current_topic}',
1182
			array(
1183
				'current_member' => $id_member,
1184
				'current_topic' => $id_topic,
1185
			)
1186
		);
1187
	}
1188
}
1189
1190
/**
1191
 * Get the previous topic from where we are.
1192
 *
1193 4
 * @param int $id_topic origin topic id
1194
 * @param int $id_board board id
1195
 * @param int $id_member = 0 member id
1196
 * @param bool $includeUnapproved = false whether to include unapproved topics
1197
 * @param bool $includeStickies = true whether to include sticky topics
1198 4
 * @return int topic number
1199 4
 */
1200
function previousTopic($id_topic, $id_board, $id_member = 0, $includeUnapproved = false, $includeStickies = true)
1201
{
1202
	return topicPointer($id_topic, $id_board, false, $id_member, $includeUnapproved, $includeStickies);
1203 4
}
1204
1205
/**
1206
 * Get the next topic from where we are.
1207
 *
1208
 * @param int $id_topic origin topic id
1209
 * @param int $id_board board id
1210
 * @param int $id_member = 0 member id
1211
 * @param bool $includeUnapproved = false whether to include unapproved topics
1212
 * @param bool $includeStickies = true whether to include sticky topics
1213
 * @return int topic number
1214
 */
1215
function nextTopic($id_topic, $id_board, $id_member = 0, $includeUnapproved = false, $includeStickies = true)
1216
{
1217
	return topicPointer($id_topic, $id_board, true, $id_member, $includeUnapproved, $includeStickies);
1218
}
1219
1220
/**
1221
 * Advance topic pointer.
1222
 * (in either direction)
1223
 * This function is used by previousTopic() and nextTopic()
1224
 * The boolean parameter $next determines direction.
1225
 *
1226
 * @param int $id_topic origin topic id
1227
 * @param int $id_board board id
1228
 * @param bool $next = true whether to increase or decrease the pointer
1229
 * @param int $id_member = 0 member id
1230
 * @param bool $includeUnapproved = false whether to include unapproved topics
1231
 * @param bool $includeStickies = true whether to include sticky topics
1232
 * @return int the topic number
1233
 */
1234
function topicPointer($id_topic, $id_board, $next = true, $id_member = 0, $includeUnapproved = false, $includeStickies = true)
1235
{
1236
	$db = database();
1237
1238
	$request = $db->query('', '
1239
		SELECT 
1240
			t2.id_topic
1241
		FROM {db_prefix}topics AS t
1242
		INNER JOIN {db_prefix}topics AS t2 ON (' .
1243
		(empty($includeStickies) ? '
1244
				t2.id_last_msg {raw:strictly} t.id_last_msg' : '
1245
				(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')
1246
		. ')
1247
		WHERE t.id_topic = {int:current_topic}
1248
			AND t2.id_board = {int:current_board}' .
1249
		($includeUnapproved ? '' : '
1250
				AND (t2.approved = {int:is_approved} OR (t2.id_member_started != {int:id_member_started} AND t2.id_member_started = {int:current_member}))'
1251
		) . '
1252
		ORDER BY' . (
1253
		$includeStickies ? '
1254
				t2.is_sticky {raw:sorting},' :
1255
			'') .
1256
		' t2.id_last_msg {raw:sorting}
1257
		LIMIT 1',
1258
		array(
1259
			'strictly' => $next ? '<' : '>',
1260
			'strictly_equal' => $next ? '<=' : '>=',
1261
			'sorting' => $next ? 'DESC' : '',
1262
			'current_board' => $id_board,
1263
			'current_member' => $id_member,
1264
			'current_topic' => $id_topic,
1265
			'is_approved' => 1,
1266
			'id_member_started' => 0,
1267
		)
1268
	);
1269
1270
	// Was there any?
1271
	if ($request->num_rows() === 0)
1272
	{
1273
		$request->free_result();
1274
1275
		// Roll over - if we're going prev, get the last - otherwise the first.
1276
		$request = $db->query('', '
1277
			SELECT 
1278
				id_topic
1279
			FROM {db_prefix}topics
1280
			WHERE id_board = {int:current_board}' .
1281
			($includeUnapproved ? '' : '
1282
				AND (approved = {int:is_approved} OR (id_member_started != {int:id_member_started} AND id_member_started = {int:current_member}))') . '
1283
			ORDER BY' . (
1284
			$includeStickies ? ' is_sticky {raw:sorting},' : '') .
1285
			' id_last_msg {raw:sorting}
1286
			LIMIT 1',
1287
			array(
1288
				'sorting' => $next ? 'DESC' : '',
1289
				'current_board' => $id_board,
1290
				'current_member' => $id_member,
1291
				'is_approved' => 1,
1292
				'id_member_started' => 0,
1293
			)
1294
		);
1295
	}
1296
	// Now you can be sure $topic is the id_topic to view.
1297
	list ($topic) = $request->fetch_row();
1298
	$request->free_result();
1299
1300
	return $topic;
1301
}
1302
1303
/**
1304
 * Set off/on unread reply subscription for a topic
1305
 *
1306
 * @param int $id_member
1307
 * @param int $topic
1308
 * @param bool $on = false
1309
 */
1310
function setTopicWatch($id_member, $topic, $on = false)
1311
{
1312
	$db = database();
1313
1314
	// find the current entry if it exists that is
1315
	$was_set = getLoggedTopics(User::$info->id, array($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...
1316
1317
	// Set topic unwatched on/off for this topic.
1318
	$db->insert(empty($was_set[$topic]) ? 'ignore' : 'replace',
1319
		'{db_prefix}log_topics',
1320
		array('id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int', 'unwatched' => 'int'),
1321
		array($id_member, $topic, !empty($was_set[$topic]['id_msg']) ? $was_set[$topic]['id_msg'] : 0, $on ? 1 : 0),
1322
		array('id_member', 'id_topic')
1323
	);
1324
}
1325
1326
/**
1327
 * Get all the details for a given topic
1328
 * - returns the basic topic information when $full is false
1329
 * - returns topic details, subject, last message read, etc when full is true
1330
 * - uses any integration information (value selects, tables and parameters) if passed and full is true
1331
 *
1332
 * @param mixed[]|int $topic_parameters can also accept a int value for a topic
1333
 * @param string $full defines the values returned by the function:
1334
 *    - if empty returns only the data from {db_prefix}topics
1335
 *    - if 'message' returns also information about the message (subject, body, etc.)
1336
 *    - if 'starter' returns also information about the topic starter (id_member and poster_name)
1337
 *    - if 'all' returns additional infos about the read/unwatched status
1338
 * @param string[] $selects (optional from integration)
1339
 * @param string[] $tables (optional from integration)
1340
 * @return mixed[]|bool to topic attributes
1341
 */
1342
function getTopicInfo($topic_parameters, $full = '', $selects = array(), $tables = array())
1343
{
1344
	global $modSettings, $board;
1345
1346
	$db = database();
1347
1348
	// Nothing to do
1349
	if (empty($topic_parameters))
1350
	{
1351
		return false;
1352
	}
1353
1354
	// Build what we can with what we were given
1355
	if (!is_array($topic_parameters))
1356
	{
1357
		$topic_parameters = array(
1358
			'topic' => $topic_parameters,
1359
			'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...
1360
			'board' => (int) $board,
1361
		);
1362
	}
1363
1364 12
	$messages_table = $full === 'message' || $full === 'all' || $full === 'starter';
1365
	$members_table = $full === 'starter' || $full === 'all';
1366 12
	$logs_table = $full === 'all';
1367
1368
	// Create the query, taking full and integration in to account
1369 12
	$request = $db->fetchQuery('
1370
		SELECT
1371
			t.id_topic, t.is_sticky, t.id_board, t.id_first_msg, t.id_last_msg,
1372
			t.id_member_started, t.id_member_updated, t.id_poll,
1373
			t.num_replies, t.num_views, t.num_likes, t.locked, t.redirect_expires,
1374
			t.id_redirect_topic, t.unapproved_posts, t.approved' . ($messages_table ? ',
1375 12
			ms.subject, ms.body, ms.id_member, ms.poster_time, ms.approved as msg_approved' : '') . ($members_table ? ',
1376
			COALESCE(mem.real_name, ms.poster_name) AS poster_name' : '') . ($logs_table ? ',
1377
			' . (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...
1378 10
			' . (!empty($modSettings['recycle_board']) && $modSettings['recycle_board'] == $board ? ', t.id_previous_board, t.id_previous_topic' : '') . '
1379 10
			' . (User::$info->is_guest === false ? ', COALESCE(lt.unwatched, 0) as unwatched' : '') : '') .
1380 10
		(!empty($selects) ? ', ' . implode(', ', $selects) : '') . '
1381
		FROM {db_prefix}topics AS t' . ($messages_table ? '
1382
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)' : '') . ($members_table ? '
1383
			LEFT JOIN {db_prefix}members as mem ON (mem.id_member = ms.id_member)' : '') . ($logs_table && User::$info->is_guest === false ? '
1384 12
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = {int:topic} AND lt.id_member = {int:member})
1385 12
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = {int:board} AND lmr.id_member = {int:member})' : '') . (!empty($tables) ? '
1386 12
			' . implode("\n\t\t\t", $tables) : '') . '
1387
		WHERE t.id_topic = {int:topic}
1388
		LIMIT 1',
1389 12
		$topic_parameters
1390
	);
1391
	$topic_info = array();
1392
	if ($request !== false)
1393
	{
1394 12
		$topic_info = $request->fetch_assoc();
1395 12
	}
1396 12
	$request->free_result();
1397 4
1398 4
	return $topic_info;
1399 12
}
1400 12
1401 12
/**
1402 12
 * Get all the details for a given topic and message.
1403 12
 * Respects permissions and post moderation
1404
 *
1405 12
 * @param int $topic id of a topic
1406 12
 * @param int|null $msg the id of a message, if empty, t.id_first_msg is used
1407
 * @return mixed[]|bool to topic attributes
1408
 */
1409 6
function getTopicInfoByMsg($topic, $msg = null)
1410
{
1411 12
	global $modSettings;
1412 12
1413
	// Nothing to do
1414 12
	if (empty($topic))
1415
	{
1416 12
		return false;
1417
	}
1418 12
1419
	$db = database();
1420
1421
	$request = $db->query('', '
1422
		SELECT
1423
			t.locked, t.num_replies, t.id_member_started, t.id_first_msg,
1424
			m.id_msg, m.id_member, m.poster_time, m.subject, m.smileys_enabled, m.body, m.icon,
1425
			m.modified_time, m.modified_name, m.approved
1426
		FROM {db_prefix}messages AS m
1427
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:current_topic})
1428
		WHERE m.id_msg = {raw:id_msg}
1429
			AND m.id_topic = {int:current_topic}' . (allowedTo('modify_any') || allowedTo('approve_posts') ? '' : (!$modSettings['postmod_active'] ? '
1430
			AND (m.id_member != {int:guest_id} AND m.id_member = {int:current_member})' : '
1431
			AND (m.approved = {int:is_approved} OR (m.id_member != {int:guest_id} AND m.id_member = {int:current_member}))')),
1432
		array(
1433
			'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...
1434
			'current_topic' => $topic,
1435
			'id_msg' => empty($msg) ? 't.id_first_msg' : $msg,
1436
			'is_approved' => 1,
1437
			'guest_id' => 0,
1438
		)
1439
	);
1440
	$topic_info = array();
1441
	if ($request !== false)
1442
	{
1443
		$topic_info = $request->fetch_assoc();
1444
	}
1445
	$request->free_result();
1446
1447
	return $topic_info;
1448
}
1449
1450
/**
1451
 * So long as you are sure... all old posts will be gone.
1452
 * Used in Maintenance.controller.php to prune old topics.
1453
 *
1454
 * @param int[] $boards
1455
 * @param string $delete_type
1456
 * @param bool $exclude_stickies
1457
 * @param int $older_than
1458
 */
1459
function removeOldTopics(array $boards, $delete_type, $exclude_stickies, $older_than)
1460
{
1461
	$db = database();
1462
1463
	// Custom conditions.
1464
	$condition = '';
1465
	$condition_params = array(
1466
		'boards' => $boards,
1467
		'poster_time' => $older_than,
1468
	);
1469
1470
	// Just moved notice topics?
1471
	if ($delete_type == 'moved')
1472
	{
1473
		$condition .= '
1474
			AND m.icon = {string:icon}
1475
			AND t.locked = {int:locked}';
1476
		$condition_params['icon'] = 'moved';
1477
		$condition_params['locked'] = 1;
1478
	}
1479
	// Otherwise, maybe locked topics only?
1480
	elseif ($delete_type == 'locked')
1481
	{
1482
		$condition .= '
1483
			AND t.locked = {int:locked}';
1484
		$condition_params['locked'] = 1;
1485
	}
1486
1487
	// Exclude stickies?
1488
	if ($exclude_stickies)
1489
	{
1490
		$condition .= '
1491
			AND t.is_sticky = {int:is_sticky}';
1492
		$condition_params['is_sticky'] = 0;
1493
	}
1494
1495
	// All we're gonna do here is grab the id_topic's and send them to removeTopics().
1496
	$topics = array();
1497
	$db->fetchQuery('
1498
		SELECT 
1499
			t.id_topic
1500
		FROM {db_prefix}topics AS t
1501
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)
1502
		WHERE
1503
			m.poster_time < {int:poster_time}' . $condition . '
1504
			AND t.id_board IN ({array_int:boards})',
1505
		$condition_params
1506
	)->fetch_callback(
1507
		function ($row) use (&$topics) {
1508
			$topics[] = $row['id_topic'];
1509
		}
1510
	);
1511
1512
	removeTopics($topics, false, true);
1513
}
1514
1515
/**
1516
 * Retrieve all topics started by the given member.
1517
 *
1518
 * @param int $memberID
1519
 *
1520
 * @return array
1521
 */
1522
function topicsStartedBy($memberID)
1523
{
1524
	$db = database();
1525
1526
	// Fetch all topics started by this user.
1527
	$topicIDs = array();
1528
	$db->fetchQuery('
1529
		SELECT 
1530
			t.id_topic
1531
		FROM {db_prefix}topics AS t
1532
		WHERE t.id_member_started = {int:selected_member}',
1533
		array(
1534
			'selected_member' => $memberID,
1535
		)
1536
	)->fetch_callback(
1537
		function ($row) use (&$topicIDs) {
1538
			$topicIDs[] = $row['id_topic'];
1539
		}
1540
	);
1541
1542
	return $topicIDs;
1543
}
1544
1545
/**
1546
 * Retrieve the messages of the given topic, that are at or after
1547
 * a message.
1548
 * Used by split topics actions.
1549
 *
1550
 * @param int $id_topic
1551
 * @param int $id_msg
1552
 * @param bool $include_current = false
1553
 * @param bool $only_approved = false
1554
 *
1555
 * @return array message ids
1556
 */
1557
function messagesSince($id_topic, $id_msg, $include_current = false, $only_approved = false)
1558
{
1559
	$db = database();
1560
1561
	// Fetch the message IDs of the topic that are at or after the message.
1562
	$messages = array();
1563
	$db->fetchQuery('
1564
		SELECT 
1565
			id_msg
1566
		FROM {db_prefix}messages
1567
		WHERE id_topic = {int:current_topic}
1568
			AND id_msg ' . ($include_current ? '>=' : '>') . ' {int:last_msg}' . ($only_approved ? '
1569
			AND approved = {int:approved}' : ''),
1570
		array(
1571
			'current_topic' => $id_topic,
1572
			'last_msg' => $id_msg,
1573
			'approved' => 1,
1574
		)
1575
	)->fetch_callback(
1576
		function ($row) use (&$messages) {
1577
			$messages[] = $row['id_msg'];
1578
		}
1579
	);
1580
1581
	return $messages;
1582
}
1583
1584
/**
1585
 * This function returns the number of messages in a topic,
1586
 * posted after $id_msg.
1587
 *
1588
 * @param int $id_topic
1589
 * @param int $id_msg
1590
 * @param bool $include_current = false
1591
 * @param bool $only_approved = false
1592
 *
1593
 * @return int
1594
 */
1595
function countMessagesSince($id_topic, $id_msg, $include_current = false, $only_approved = false)
1596
{
1597
	$db = database();
1598
1599
	// Give us something to work with
1600
	if (empty($id_topic) || empty($id_msg))
1601
	{
1602
		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...
1603
	}
1604
1605
	$request = $db->query('', '
1606
		SELECT 
1607
			COUNT(*)
1608
		FROM {db_prefix}messages
1609
		WHERE id_topic = {int:current_topic}
1610
			AND id_msg ' . ($include_current ? '>=' : '>') . ' {int:last_msg}' . ($only_approved ? '
1611
			AND approved = {int:approved}' : '') . '
1612
		LIMIT 1',
1613
		array(
1614
			'current_topic' => $id_topic,
1615
			'last_msg' => $id_msg,
1616
			'approved' => 1,
1617
		)
1618
	);
1619
	list ($count) = $request->fetch_row();
1620
	$request->free_result();
1621
1622
	return $count;
1623
}
1624
1625
/**
1626
 * Returns how many messages are in a topic before the specified message id.
1627
 * Used in display to compute the start value for a specific message.
1628
 *
1629
 * @param int $id_topic
1630
 * @param int $id_msg
1631
 * @param bool $include_current = false
1632
 * @param bool $only_approved = false
1633
 * @param bool $include_own = false
1634
 * @return int
1635
 */
1636
function countMessagesBefore($id_topic, $id_msg, $include_current = false, $only_approved = false, $include_own = false)
1637
{
1638
	$db = database();
1639
1640
	$request = $db->query('', '
1641
		SELECT 
1642
			COUNT(*)
1643
		FROM {db_prefix}messages
1644
		WHERE id_msg ' . ($include_current ? '<=' : '<') . ' {int:id_msg}
1645
			AND id_topic = {int:current_topic}' . ($only_approved ? '
1646
			AND (approved = {int:is_approved}' . ($include_own ? '
1647
			OR id_member = {int:current_member}' : '') . ')' : ''),
1648
		array(
1649
			'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...
1650
			'current_topic' => $id_topic,
1651
			'id_msg' => $id_msg,
1652
			'is_approved' => 1,
1653
		)
1654
	);
1655
	list ($count) = $request->fetch_row();
1656
	$request->free_result();
1657
1658
	return $count;
1659
}
1660
1661
/**
1662
 * Select a part of the messages in a topic.
1663
 *
1664
 * @param int $topic
1665
 * @param int $start The item to start with (for pagination purposes)
1666
 * @param int $items_per_page The number of items to show per page
1667
 * @param mixed[] $messages
1668
 * @param bool $only_approved
1669
 *
1670
 * @return array|mixed[]
1671
 */
1672
function selectMessages($topic, $start, $items_per_page, $messages = array(), $only_approved = false)
1673
{
1674
	$db = database();
1675
1676
	$returnMessages = array();
1677
	$parser = ParserWrapper::instance();
1678
1679
	// Get the messages and stick them into an array.
1680
	$db->fetchQuery('
1681
		SELECT 
1682
			m.subject, COALESCE(mem.real_name, m.poster_name) AS real_name, 
1683
			m.poster_time, m.body, m.id_msg, m.smileys_enabled, m.id_member
1684
		FROM (
1685
			SELECT 
1686
				m.id_msg 
1687
			FROM {db_prefix}messages AS m
1688
			WHERE m.id_topic = {int:current_topic}' . (empty($messages['before']) ? '' : '
1689
				AND m.id_msg < {int:msg_before}') . (empty($messages['after']) ? '' : '
1690
				AND m.id_msg > {int:msg_after}') . (empty($messages['excluded']) ? '' : '
1691
				AND m.id_msg NOT IN ({array_int:no_split_msgs})') . (empty($messages['included']) ? '' : '
1692
				AND m.id_msg IN ({array_int:split_msgs})') . (!$only_approved ? '' : '
1693
				AND approved = {int:is_approved}') . '
1694
			ORDER BY m.id_msg DESC
1695
			LIMIT {int:start}, {int:messages_per_page}
1696
		) AS o 
1697
		JOIN {db_prefix}messages as m ON o.id_msg=m.id_msg 
1698
		LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1699
		ORDER BY m.id_msg DESC',
1700
		array(
1701
			'current_topic' => $topic,
1702
			'no_split_msgs' => !empty($messages['excluded']) ? $messages['excluded'] : array(),
1703
			'split_msgs' => !empty($messages['included']) ? $messages['included'] : array(),
1704
			'is_approved' => 1,
1705
			'start' => $start,
1706
			'messages_per_page' => $items_per_page,
1707
			'msg_before' => !empty($messages['before']) ? (int) $messages['before'] : 0,
1708
			'msg_after' => !empty($messages['after']) ? (int) $messages['after'] : 0,
1709
		)
1710
	)->fetch_callback(
1711
		function ($row) use (&$returnMessages, $parser) {
1712
			$row['subject'] = censor($row['subject']);
1713
			$row['body'] = censor($row['body']);
1714
1715
			$row['body'] = $parser->parseMessage($row['body'], (bool) $row['smileys_enabled']);
1716
1717
			$returnMessages[$row['id_msg']] = array(
1718
				'id' => $row['id_msg'],
1719
				'subject' => $row['subject'],
1720
				'time' => standardTime($row['poster_time']),
1721
				'html_time' => htmlTime($row['poster_time']),
1722
				'timestamp' => forum_time(true, $row['poster_time']),
1723
				'body' => $row['body'],
1724
				'poster' => $row['real_name'],
1725
				'id_poster' => $row['id_member'],
1726
			);
1727
		}
1728
	);
1729
1730
	return $returnMessages;
1731
}
1732
1733
/**
1734
 * Loads all the messages of a topic
1735
 * Used when printing or other functions that require a topic listing
1736
 *
1737
 * @param int $topic
1738
 * @param string $render defaults to print style rendering for parse_bbc
1739
 *
1740
 * @return array
1741
 */
1742
function topicMessages($topic, $render = 'print')
1743
{
1744
	global $modSettings;
1745
1746
	$db = database();
1747
1748
	$posts = array();
1749
	$parser = ParserWrapper::instance();
1750
	if ($render === 'print')
1751
	{
1752
		$parser->getCodes()->setForPrinting();
1753
	}
1754
1755
	$db->fetchQuery('
1756
		SELECT 
1757
			subject, poster_time, body, COALESCE(mem.real_name, poster_name) AS poster_name, id_msg
1758
		FROM {db_prefix}messages AS m
1759
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1760
		WHERE m.id_topic = {int:current_topic}' . ($modSettings['postmod_active'] && !allowedTo('approve_posts') ? '
1761
			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...
1762
		ORDER BY m.id_msg',
1763
		array(
1764
			'current_topic' => $topic,
1765
			'is_approved' => 1,
1766
			'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...
1767
		)
1768
	)->fetch_callback(
1769
		function ($row) use (&$posts, $parser, $render) {
1770
			// Censor the subject and message.
1771
			$row['subject'] = censor($row['subject']);
1772
			$row['body'] = censor($row['body']);
1773
1774
			$posts[$row['id_msg']] = array(
1775
				'subject' => $row['subject'],
1776
				'member' => $row['poster_name'],
1777
				'time' => standardTime($row['poster_time'], false),
1778
				'html_time' => htmlTime($row['poster_time']),
1779
				'timestamp' => forum_time(true, $row['poster_time']),
1780
				'body' => $parser->parseMessage($row['body'], $render !== 'print'),
1781
				'id_msg' => $row['id_msg'],
1782
			);
1783
		}
1784
	);
1785
1786
	return $posts;
1787
}
1788
1789
/**
1790
 * Load message image attachments for use in the print page function
1791
 * Returns array of file attachment name along with width/height properties
1792
 * Will only return approved attachments
1793
 *
1794
 * @param int[] $id_messages
1795
 *
1796
 * @return array
1797
 */
1798
function messagesAttachments($id_messages)
1799
{
1800
	global $modSettings;
1801
1802
	require_once(SUBSDIR . '/Attachments.subs.php');
1803
1804
	$db = database();
1805
1806
	$temp = array();
1807
	$printattach = array();
1808
	$db->fetchQuery('
1809
		SELECT
1810
			a.id_attach, a.id_msg, a.approved, a.width, a.height, a.file_hash, a.filename, a.id_folder, a.mime_type
1811
		FROM {db_prefix}attachments AS a
1812
		WHERE a.id_msg IN ({array_int:message_list})
1813
			AND a.attachment_type = {int:attachment_type}',
1814
		array(
1815
			'message_list' => $id_messages,
1816
			'attachment_type' => 0,
1817
			'is_approved' => 1,
1818
		)
1819
	)->fetch_callback(
1820
		function ($row) use (&$temp, &$printattach) {
1821
			$temp[$row['id_attach']] = $row;
1822
			if (!isset($printattach[$row['id_msg']]))
1823
			{
1824
				$printattach[$row['id_msg']] = array();
1825
			}
1826
		}
1827
	);
1828
1829
	ksort($temp);
1830
1831
	// Load them into $context so the template can use them
1832
	foreach ($temp as $row)
1833
	{
1834
		if (!empty($row['width']) && !empty($row['height']))
1835
		{
1836
			if (!empty($modSettings['max_image_width']) && (empty($modSettings['max_image_height']) || $row['height'] * ($modSettings['max_image_width'] / $row['width']) <= $modSettings['max_image_height']))
1837
			{
1838
				if ($row['width'] > $modSettings['max_image_width'])
1839
				{
1840
					$row['height'] = floor($row['height'] * ($modSettings['max_image_width'] / $row['width']));
1841
					$row['width'] = $modSettings['max_image_width'];
1842
				}
1843
			}
1844
			elseif (!empty($modSettings['max_image_width']))
1845
			{
1846
				if ($row['height'] > $modSettings['max_image_height'])
1847
				{
1848
					$row['width'] = floor($row['width'] * $modSettings['max_image_height'] / $row['height']);
1849
					$row['height'] = $modSettings['max_image_height'];
1850
				}
1851
			}
1852
1853
			$row['filename'] = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
1854
1855
			// save for the template
1856
			$printattach[$row['id_msg']][] = $row;
1857
		}
1858
	}
1859
1860
	return $printattach;
1861
}
1862
1863
/**
1864
 * Retrieve unapproved posts of the member
1865
 * in a specific topic
1866
 *
1867
 * @param int $id_topic topic id
1868
 * @param int $id_member member id
1869
 * @return array|int empty array if no member supplied, otherwise number of posts
1870
 */
1871
function unapprovedPosts($id_topic, $id_member)
1872
{
1873
	$db = database();
1874
1875
	// not all guests are the same!
1876
	if (empty($id_member))
1877
	{
1878
		return array();
1879
	}
1880
1881
	$request = $db->query('', '
1882
		SELECT 
1883
			COUNT(id_member) AS my_unapproved_posts
1884
		FROM {db_prefix}messages
1885
		WHERE id_topic = {int:current_topic}
1886
			AND id_member = {int:current_member}
1887
			AND approved = 0',
1888
		array(
1889
			'current_topic' => $id_topic,
1890
			'current_member' => $id_member,
1891
		)
1892
	);
1893
	list ($myUnapprovedPosts) = $request->fetch_row();
1894
	$request->free_result();
1895
1896
	return $myUnapprovedPosts;
1897
}
1898
1899
/**
1900
 * Update topic info after a successful split of a topic.
1901
 *
1902
 * @param mixed[] $options
1903
 * @param int $id_board
1904
 */
1905
function updateSplitTopics($options, $id_board)
1906
{
1907
	$db = database();
1908
1909
	// Any associated reported posts better follow...
1910
	$db->query('', '
1911
		UPDATE {db_prefix}log_reported
1912
		SET 
1913
			id_topic = {int:id_topic}
1914
		WHERE id_msg IN ({array_int:split_msgs})
1915
			AND type = {string:a_message}',
1916
		array(
1917
			'split_msgs' => $options['splitMessages'],
1918
			'id_topic' => $options['split2_ID_TOPIC'],
1919
			'a_message' => 'msg',
1920
		)
1921
	);
1922
1923
	// Mess with the old topic's first, last, and number of messages.
1924
	setTopicAttribute($options['split1_ID_TOPIC'], array(
1925
		'num_replies' => $options['split1_replies'],
1926
		'id_first_msg' => $options['split1_first_msg'],
1927
		'id_last_msg' => $options['split1_last_msg'],
1928
		'id_member_started' => $options['split1_firstMem'],
1929
		'id_member_updated' => $options['split1_lastMem'],
1930
		'unapproved_posts' => $options['split1_unapprovedposts'],
1931
	));
1932
1933
	// Now, put the first/last message back to what they should be.
1934
	setTopicAttribute($options['split2_ID_TOPIC'], array(
1935
		'id_first_msg' => $options['split2_first_msg'],
1936
		'id_last_msg' => $options['split2_last_msg'],
1937
	));
1938
1939
	// If the new topic isn't approved ensure the first message flags
1940
	// this just in case.
1941
	if (!$options['split2_approved'])
1942
	{
1943
		$db->query('', '
1944
			UPDATE {db_prefix}messages
1945
			SET 
1946
				approved = {int:approved}
1947
			WHERE id_msg = {int:id_msg}
1948
				AND id_topic = {int:id_topic}',
1949
			array(
1950
				'approved' => 0,
1951
				'id_msg' => $options['split2_first_msg'],
1952
				'id_topic' => $options['split2_ID_TOPIC'],
1953
			)
1954
		);
1955
	}
1956
1957
	// The board has more topics now (Or more unapproved ones!).
1958
	$db->query('', '
1959
		UPDATE {db_prefix}boards
1960
		SET 
1961
			' . ($options['split2_approved']
1962
			? ' num_topics = num_topics + 1'
1963
			: ' unapproved_topics = unapproved_topics + 1') . '
1964
		WHERE id_board = {int:id_board}',
1965
		array(
1966
			'id_board' => $id_board,
1967
		)
1968
	);
1969
}
1970
1971
/**
1972
 * Find out who started a topic, and the lock status
1973
 *
1974
 * @param int $topic
1975
 * @return array with id_member_started and locked
1976
 */
1977
function topicStatus($topic)
1978
{
1979
	// Find out who started the topic, and the lock status.
1980
	$starter = topicAttribute($topic, array('id_member_started', 'locked'));
1981
1982
	return array($starter['id_member_started'], $starter['locked']);
1983
}
1984
1985
/**
1986
 * Set attributes for a topic, i.e. locked, sticky.
1987
 * Parameter $attributes is an array where the key is the column name of the
1988
 * attribute to change, and the value is... the new value of the attribute.
1989
 * It sets the new value for the attribute as passed to it.
1990
 * <b>It is currently limited to integer values only</b>
1991
 *
1992
 * @param int|int[] $topic
1993
 * @param mixed[] $attributes
1994
 * @return int number of row affected
1995
 * @todo limited to integer attributes
1996
 */
1997
function setTopicAttribute($topic, $attributes)
1998
{
1999
	$db = database();
2000
2001
	$update = array();
2002
	foreach ($attributes as $key => $attr)
2003
	{
2004
		$attributes[$key] = (int) $attr;
2005
		$update[] = '
2006
				' . $key . ' = {int:' . $key . '}';
2007
	}
2008
2009
	if (empty($update))
2010
	{
2011
		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...
2012
	}
2013
2014
	$attributes['current_topic'] = (array) $topic;
2015
2016
	return $db->query('', '
2017
		UPDATE {db_prefix}topics
2018
		SET 
2019
			' . implode(',', $update) . '
2020
		WHERE id_topic IN ({array_int:current_topic})',
2021
		$attributes
2022
	)->affected_rows();
2023
}
2024
2025
/**
2026 10
 * Retrieve the locked or sticky status of a topic.
2027
 *
2028 10
 * @param int|int[] $id_topic topic to get the status for
2029 10
 * @param string|string[] $attributes Basically the column names
2030
 * @return array named array based on attributes requested
2031 10
 */
2032 10
function topicAttribute($id_topic, $attributes)
2033 10
{
2034
	$db = database();
2035
2036 10
	// @todo maybe add a filer for known attributes... or not
2037
// 	$attributes = array(
2038
// 		'locked' => 'locked',
2039
// 		'sticky' => 'is_sticky',
2040
// 	);
2041 10
2042
	// check the lock status
2043 10
	$request = $db->query('', '
2044
		SELECT 
2045
			{raw:attribute}
2046 10
		FROM {db_prefix}topics
2047
		WHERE id_topic IN ({array_int:current_topic})',
2048 5
		array(
2049 10
			'current_topic' => (array) $id_topic,
2050
			'attribute' => implode(',', (array) $attributes),
2051
		)
2052
	);
2053
2054
	if (is_array($id_topic))
2055
	{
2056
		$status = array();
2057
		while (($row = $request->fetch_assoc()))
2058
		{
2059
			$status[] = $row;
2060
		}
2061
	}
2062 12
	else
2063
	{
2064
		$status = $request->fetch_assoc();
2065
	}
2066
2067
	$request->free_result();
2068
2069
	return $status;
2070
}
2071 12
2072
/**
2073
 * Retrieve some topic attributes based on the user:
2074
 *   - locked
2075
 *   - notify
2076
 *   - is_sticky
2077 12
 *   - id_poll
2078 12
 *   - id_last_msg
2079
 *   - id_member of the first message in the topic
2080
 *   - id_first_msg
2081
 *   - subject of the first message in the topic
2082 12
 *   - last_post_time that is poster_time if poster_time > modified_time, or
2083
 *       modified_time otherwise
2084 12
 *
2085 12
 * @param int $id_topic topic to get the status for
2086
 * @param int $user a user id
2087 12
 * @return mixed[]
2088
 */
2089
function topicUserAttributes($id_topic, $user)
2090
{
2091
	$db = database();
2092 6
2093
	$request = $db->query('', '
2094
		SELECT
2095 12
			t.locked, COALESCE(ln.id_topic, 0) AS notify, t.is_sticky, t.id_poll,
2096
			t.id_last_msg, mf.id_member, t.id_first_msg, mf.subject,
2097 12
			CASE WHEN ml.poster_time > ml.modified_time THEN ml.poster_time ELSE ml.modified_time END AS last_post_time
2098
		FROM {db_prefix}topics AS t
2099
			LEFT JOIN {db_prefix}log_notify AS ln ON (ln.id_topic = t.id_topic AND ln.id_member = {int:current_member})
2100
			LEFT JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
2101
			LEFT JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2102
		WHERE t.id_topic = {int:current_topic}
2103
		LIMIT 1',
2104
		array(
2105
			'current_member' => $user,
2106
			'current_topic' => $id_topic,
2107
		)
2108
	);
2109
	$return = $request->fetch_assoc();
2110
	$request->free_result();
2111
2112
	return $return;
2113
}
2114
2115
/**
2116
 * Retrieve some details about the topic
2117
 *
2118
 * @param int[] $topics an array of topic id
2119
 *
2120
 * @return array
2121
 */
2122
function topicsDetails($topics)
2123
{
2124
	return topicAttribute($topics, array('id_topic', 'id_member_started', 'id_board', 'locked', 'approved', 'unapproved_posts'));
2125
}
2126
2127
/**
2128
 * Toggle sticky status for the passed topics and logs the action.
2129
 *
2130
 * @param int[] $topics
2131
 * @param bool $log If true the action is logged
2132
 * @return int Number of topics toggled
2133
 */
2134
function toggleTopicSticky($topics, $log = false)
2135
{
2136
	$db = database();
2137
2138
	$topics = is_array($topics) ? $topics : array($topics);
0 ignored issues
show
introduced by
The condition is_array($topics) is always true.
Loading history...
2139
2140
	$toggled = $db->query('', '
2141
		UPDATE {db_prefix}topics
2142
		SET 
2143
			is_sticky = CASE WHEN is_sticky = 1 THEN 0 ELSE 1 END
2144
		WHERE id_topic IN ({array_int:sticky_topic_ids})',
2145
		array(
2146
			'sticky_topic_ids' => $topics,
2147
		)
2148
	)->affected_rows();
2149
2150
	if ($log)
2151
	{
2152
		// Get the board IDs and Sticky status
2153
		$topicAttributes = topicAttribute($topics, array('id_topic', 'id_board', 'is_sticky'));
2154
		$stickyCacheBoards = array();
2155
		$stickyCacheStatus = array();
2156
		foreach ($topicAttributes as $row)
2157
		{
2158
			$stickyCacheBoards[$row['id_topic']] = $row['id_board'];
2159
			$stickyCacheStatus[$row['id_topic']] = empty($row['is_sticky']);
2160
		}
2161
2162
		foreach ($topics as $topic)
2163
		{
2164
			logAction($stickyCacheStatus[$topic] ? 'unsticky' : 'sticky', array('topic' => $topic, 'board' => $stickyCacheBoards[$topic]));
2165
			sendNotifications($topic, 'sticky');
2166
		}
2167
	}
2168
2169
	return $toggled;
2170
}
2171
2172
/**
2173
 * Get topics from the log_topics table belonging to a certain user
2174
 *
2175
 * @param int $member a member id
2176
 * @param int[] $topics an array of topics
2177
 * @return array an array of topics in the table (key) and its unwatched status (value)
2178
 *
2179
 * @todo find a better name
2180
 */
2181
function getLoggedTopics($member, $topics)
2182
{
2183
	$db = database();
2184
2185
	$logged_topics = array();
2186
	$db->query('', '
2187
		SELECT 
2188
			id_topic, id_msg, unwatched
2189
		FROM {db_prefix}log_topics
2190
		WHERE id_topic IN ({array_int:selected_topics})
2191
			AND id_member = {int:current_user}',
2192
		array(
2193
			'selected_topics' => $topics,
2194
			'current_user' => $member,
2195
		)
2196
	)->fetch_callback(
2197
		function ($row) use (&$logged_topics) {
2198
			$logged_topics[$row['id_topic']] = $row;
2199
		}
2200
	);
2201
2202
	return $logged_topics;
2203
}
2204
2205
/**
2206
 * Returns a list of topics ids and their subjects
2207
 *
2208
 * @param int[] $topic_ids
2209
 *
2210
 * @return array
2211
 */
2212
function topicsList($topic_ids)
2213
{
2214
	global $modSettings;
2215 2
2216
	// you have to want *something* from this function
2217 2
	if (empty($topic_ids))
2218 2
	{
2219
		return array();
2220
	}
2221
2222
	$db = database();
2223
2224
	$topics = array();
2225 2
2226 2
	$db->fetchQuery('
2227
		SELECT 
2228 2
			t.id_topic, m.subject
2229
		FROM {db_prefix}topics AS t
2230
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
2231 2
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
2232
		WHERE {query_see_board}
2233
			AND t.id_topic IN ({array_int:topic_list})' . ($modSettings['postmod_active'] ? '
2234 2
			AND t.approved = {int:is_approved}' : '') . '
2235
		LIMIT {int:limit}',
2236
		array(
2237
			'topic_list' => $topic_ids,
2238
			'is_approved' => 1,
2239
			'limit' => count($topic_ids),
2240
		)
2241
	)->fetch_callback(
2242
		function ($row) use (&$topics) {
2243
			$topics[$row['id_topic']] = array(
2244
				'id_topic' => $row['id_topic'],
2245
				'subject' => censor($row['subject']),
2246
			);
2247
		}
2248
	);
2249
2250
	return $topics;
2251
}
2252
2253
/**
2254
 * Get each post and poster in this topic and take care of user settings such as
2255
 * limit or sort direction.
2256
 *
2257
 * @param int $topic
2258
 * @param mixed[] $limit
2259
 * @param bool $sort set to false for a desc sort
2260
 * @return array
2261
 */
2262
function getTopicsPostsAndPoster($topic, $limit, $sort)
2263
{
2264
	global $modSettings;
2265
2266
	$db = database();
2267
2268
	$topic_details = array(
2269
		'messages' => array(),
2270
		'all_posters' => array(),
2271
	);
2272
2273
	// When evaluating potentially huge offsets, grab the ids only, first.
2274
	// The performance impact is still significant going from three columns to one.
2275
	$postMod = $modSettings['postmod_active'] && allowedTo('approve_posts');
2276
	$request = $db->fetchQuery('
2277
		SELECT 
2278
			m.id_msg, m.id_member
2279
		FROM (
2280
			SELECT 
2281
				id_msg 
2282
			FROM {db_prefix}messages
2283
			WHERE id_topic = {int:current_topic}' . ($postMod ? '' : '
2284
			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...
2285
			ORDER BY id_msg ' . ($sort ? '' : 'DESC') . ($limit['messages_per_page'] == -1 ? '' : '
2286
			LIMIT ' . $limit['start'] . ', ' . $limit['offset']) . '
2287
		) AS o 
2288
		JOIN {db_prefix}messages as m ON o.id_msg=m.id_msg
2289
		ORDER BY m.id_msg ' . ($sort ? '' : 'DESC'),
2290
		array(
2291
			'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...
2292
			'current_topic' => $topic,
2293
			'is_approved' => 1,
2294
			'blank_id_member' => 0,
2295
		)
2296
	);
2297
	while ($row = $db->fetch_assoc($request))
0 ignored issues
show
Bug introduced by
The method fetch_assoc() does not exist on ElkArte\Database\QueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to ElkArte\Database\QueryInterface. ( Ignorable by Annotation )

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

2297
	while ($row = $db->/** @scrutinizer ignore-call */ fetch_assoc($request))
Loading history...
2298 2
	{
2299
		if (!empty($row['id_member']))
2300 2
		{
2301
			$topic_details['all_posters'][$row['id_msg']] = $row['id_member'];
2302
		}
2303 2
2304
		$topic_details['messages'][] = $row['id_msg'];
2305
	}
2306
	$db->free_result($request);
0 ignored issues
show
Bug introduced by
The method free_result() does not exist on ElkArte\Database\QueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to ElkArte\Database\QueryInterface. ( Ignorable by Annotation )

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

2306
	$db->/** @scrutinizer ignore-call */ 
2307
      free_result($request);
Loading history...
2307 2
2308
	return $topic_details;
2309
}
2310
2311 2
/**
2312 2
 * Remove a batch of messages (or topics)
2313 2
 *
2314 2
 * @param int[] $messages
2315
 * @param array $messageDetails
2316 2
 * @param string $type = replies
2317 2
 */
2318 2
function removeMessages($messages, $messageDetails, $type = 'replies')
2319 2
{
2320
	global $modSettings;
2321 2
2322
	// @todo something's not right, removeMessage() does check permissions,
2323 2
	// removeTopics() doesn't
2324
	if ($type === 'topics')
2325
	{
2326
		removeTopics($messages);
2327
2328 2
		// and tell the world about it
2329 2
		foreach ($messages as $topic)
2330
		{
2331
			// Note, only log topic ID in native form if it's not gone forever.
2332 2
			logAction('remove', array(
2333
				(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']));
2334
		}
2335
	}
2336
	else
2337
	{
2338
		$remover = new MessagesDelete($modSettings['recycle_enable'], $modSettings['recycle_board']);
2339
		foreach ($messages as $post)
2340
		{
2341
			$remover->removeMessage($post);
2342
		}
2343
	}
2344
}
2345
2346
/**
2347
 * Approve a batch of posts (or topics in their own right)
2348
 *
2349
 * @param int[] $messages
2350
 * @param mixed[] $messageDetails
2351
 * @param string $type = replies
2352
 */
2353
function approveMessages($messages, $messageDetails, $type = 'replies')
2354
{
2355
	if ($type === 'topics')
2356
	{
2357
		approveTopics($messages, true, true);
2358
	}
2359
	else
2360
	{
2361
		require_once(SUBSDIR . '/Post.subs.php');
2362
		approvePosts($messages);
2363
2364
		// and tell the world about it again
2365
		foreach ($messages as $post)
2366
		{
2367
			logAction('approve', array('topic' => $messageDetails[$post]['topic'], 'subject' => $messageDetails[$post]['subject'], 'member' => $messageDetails[$post]['member'], 'board' => $messageDetails[$post]['board']));
2368
		}
2369
	}
2370
}
2371
2372
/**
2373
 * Approve topics, all we got.
2374
 *
2375
 * @param int[] $topics array of topics ids
2376
 * @param bool $approve = true
2377
 * @param bool $log if true logs the action.
2378
 *
2379
 * @return bool|void
2380
 */
2381
function approveTopics($topics, $approve = true, $log = false)
2382
{
2383
	if (!is_array($topics))
0 ignored issues
show
introduced by
The condition is_array($topics) is always true.
Loading history...
2384
	{
2385
		$topics = array($topics);
2386
	}
2387
2388
	if (empty($topics))
2389
	{
2390
		return false;
2391
	}
2392
2393
	$db = database();
2394
2395
	$approve_type = $approve ? 0 : 1;
2396
2397
	if ($log)
2398
	{
2399
		$log_action = $approve ? 'approve_topic' : 'unapprove_topic';
2400
2401
		// We need unapproved topic ids, their authors and the subjects!
2402
		$db->fetchQuery('
2403
			SELECT 
2404
				t.id_topic, t.id_member_started, m.subject
2405
			FROM {db_prefix}topics as t
2406
				LEFT JOIN {db_prefix}messages AS m ON (t.id_first_msg = m.id_msg)
2407
			WHERE t.id_topic IN ({array_int:approve_topic_ids})
2408
				AND t.approved = {int:approve_type}
2409
			LIMIT ' . count($topics),
2410
			array(
2411
				'approve_topic_ids' => $topics,
2412
				'approve_type' => $approve_type,
2413
			)
2414
		)->fetch_callback(
2415
			function ($row) use (&$log_action) {
2416
				global $board;
2417
2418
				logAction($log_action, array(
2419
					'topic' => $row['id_topic'],
2420
					'subject' => $row['subject'],
2421
					'member' => $row['id_member_started'],
2422
					'board' => $board)
2423
				);
2424
			}
2425
		);
2426
	}
2427
2428
	// Just get the messages to be approved and pass through...
2429
	$msgs = array();
2430
	$db->fetchQuery('
2431
		SELECT 
2432
			id_msg
2433
		FROM {db_prefix}messages
2434
		WHERE id_topic IN ({array_int:topic_list})
2435
			AND approved = {int:approve_type}',
2436
		array(
2437
			'topic_list' => $topics,
2438
			'approve_type' => $approve_type,
2439
		)
2440
	)->fetch_callback(
2441
		function ($row) use (&$msgs) {
2442
			$msgs[] = $row['id_msg'];
2443
		}
2444
	);
2445
2446
	require_once(SUBSDIR . '/Post.subs.php');
2447
2448
	return approvePosts($msgs, $approve);
2449
}
2450
2451
/**
2452
 * Post a message at the end of the original topic
2453
 *
2454
 * @param string $reason the text that will become the message body
2455
 * @param string $subject the text that will become the message subject
2456
 * @param mixed[] $board_info some board information (at least id, name, if posts are counted)
2457
 * @param string $new_topic used to build the url for moving to a new topic
2458
 */
2459
function postSplitRedirect($reason, $subject, $board_info, $new_topic)
2460
{
2461
	global $language, $txt, $topic, $board;
2462
2463
	// Should be in the boardwide language.
2464
	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...
2465
	{
2466
		$lang_loader = new LangLoader($language, $txt, database());
2467
		$lang_loader->load('index');
2468
	}
2469
2470
	preparsecode($reason);
2471
2472
	// Add a URL onto the message.
2473
	$reason = strtr($reason, array(
2474
		$txt['movetopic_auto_board'] => '[url=' . getUrl('board', ['board' => $board_info['id'], 'start' => '0', 'name' => $board_info['name']]) . ']' . $board_info['name'] . '[/url]',
2475
		$txt['movetopic_auto_topic'] => '[iurl]' . getUrl('topic', ['topic' => $new_topic, 'start' => '0', 'subject' => $subject]) . '[/iurl]'
2476
	));
2477
2478
	$msgOptions = array(
2479
		'subject' => $txt['split'] . ': ' . strtr(Util::htmltrim(Util::htmlspecialchars($subject)), array("\r" => '', "\n" => '', "\t" => '')),
2480
		'body' => $reason,
2481
		'icon' => 'moved',
2482
		'smileys_enabled' => 1,
2483
	);
2484
2485
	$topicOptions = array(
2486
		'id' => $topic,
2487
		'board' => $board,
2488
		'mark_as_read' => true,
2489
	);
2490
2491
	$posterOptions = array(
2492
		'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...
2493
		'update_post_count' => empty($board_info['count_posts']),
2494
	);
2495
2496
	createPost($msgOptions, $topicOptions, $posterOptions);
2497
}
2498
2499
/**
2500
 * General function to split off a topic.
2501
 * creates a new topic and moves the messages with the IDs in
2502
 * array messagesToBeSplit to the new topic.
2503
 * the subject of the newly created topic is set to 'newSubject'.
2504
 * marks the newly created message as read for the user splitting it.
2505
 * updates the statistics to reflect a newly created topic.
2506
 * logs the action in the moderation log.
2507
 * a notification is sent to all users monitoring this topic.
2508
 *
2509
 * @param int $split1_ID_TOPIC
2510
 * @param int[] $splitMessages
2511
 * @param string $new_subject
2512
 *
2513
 * @return int the topic ID of the new split topic.
2514
 * @throws \ElkArte\Exceptions\Exception no_posts_selected, selected_all_posts, cant_find_message
2515
 */
2516
function splitTopic($split1_ID_TOPIC, $splitMessages, $new_subject)
2517
{
2518
	global $txt, $modSettings;
2519
2520
	$db = database();
2521
2522
	// Nothing to split?
2523
	if (empty($splitMessages))
2524
	{
2525
		throw new \ElkArte\Exceptions\Exception('no_posts_selected', false);
2526
	}
2527
2528
	// Get some board info.
2529
	$topicAttribute = topicAttribute($split1_ID_TOPIC, array('id_board', 'approved'));
2530
	$id_board = $topicAttribute['id_board'];
2531
	$split1_approved = $topicAttribute['approved'];
2532
2533
	// Find the new first and last not in the list. (old topic)
2534
	$request = $db->query('', '
2535
		SELECT
2536
			MIN(m.id_msg) AS myid_first_msg, MAX(m.id_msg) AS myid_last_msg, COUNT(*) AS message_count, m.approved
2537
		FROM {db_prefix}messages AS m
2538
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:id_topic})
2539
		WHERE m.id_msg NOT IN ({array_int:no_msg_list})
2540
			AND m.id_topic = {int:id_topic}
2541
		GROUP BY m.approved
2542
		ORDER BY m.approved DESC
2543
		LIMIT 2',
2544
		array(
2545
			'id_topic' => $split1_ID_TOPIC,
2546
			'no_msg_list' => $splitMessages,
2547
		)
2548
	);
2549
	// You can't select ALL the messages!
2550
	if ($request->num_rows() === 0)
2551
	{
2552
		throw new \ElkArte\Exceptions\Exception('selected_all_posts', false);
2553
	}
2554
2555
	$split1_first_msg = null;
2556
	$split1_last_msg = null;
2557
2558
	while (($row = $request->fetch_assoc()))
2559
	{
2560
		// Get the right first and last message dependant on approved state...
2561
		if (empty($split1_first_msg) || $row['myid_first_msg'] < $split1_first_msg)
2562
		{
2563
			$split1_first_msg = $row['myid_first_msg'];
2564
		}
2565
2566
		if (empty($split1_last_msg) || $row['approved'])
2567
		{
2568
			$split1_last_msg = $row['myid_last_msg'];
2569
		}
2570
2571
		// Get the counts correct...
2572
		if ($row['approved'])
2573
		{
2574
			$split1_replies = $row['message_count'] - 1;
2575
			$split1_unapprovedposts = 0;
2576
		}
2577
		else
2578
		{
2579
			if (!isset($split1_replies))
2580
			{
2581
				$split1_replies = 0;
2582
			}
2583
			// If the topic isn't approved then num replies must go up by one... as first post wouldn't be counted.
2584
			elseif (!$split1_approved)
2585
			{
2586
				$split1_replies++;
2587
			}
2588
2589
			$split1_unapprovedposts = $row['message_count'];
2590
		}
2591
	}
2592
	$request->free_result();
2593
	$split1_firstMem = getMsgMemberID($split1_first_msg);
2594
	$split1_lastMem = getMsgMemberID($split1_last_msg);
2595
2596
	// Find the first and last in the list. (new topic)
2597
	$request = $db->query('', '
2598
		SELECT 
2599
			MIN(id_msg) AS myid_first_msg, MAX(id_msg) AS myid_last_msg, COUNT(*) AS message_count, approved
2600
		FROM {db_prefix}messages
2601
		WHERE id_msg IN ({array_int:msg_list})
2602
			AND id_topic = {int:id_topic}
2603
		GROUP BY id_topic, approved
2604
		ORDER BY approved DESC
2605
		LIMIT 2',
2606
		array(
2607
			'msg_list' => $splitMessages,
2608
			'id_topic' => $split1_ID_TOPIC,
2609
		)
2610
	);
2611
	while (($row = $request->fetch_assoc()))
2612
	{
2613
		// As before get the right first and last message dependant on approved state...
2614
		if (empty($split2_first_msg) || $row['myid_first_msg'] < $split2_first_msg)
2615
		{
2616
			$split2_first_msg = $row['myid_first_msg'];
2617
		}
2618
2619
		if (empty($split2_last_msg) || $row['approved'])
2620
		{
2621
			$split2_last_msg = $row['myid_last_msg'];
2622
		}
2623
2624
		// Then do the counts again...
2625
		if ($row['approved'])
2626
		{
2627
			$split2_approved = true;
2628
			$split2_replies = $row['message_count'] - 1;
2629
			$split2_unapprovedposts = 0;
2630
		}
2631
		else
2632
		{
2633
			// Should this one be approved??
2634
			if ($split2_first_msg == $row['myid_first_msg'])
2635
			{
2636
				$split2_approved = false;
2637
			}
2638
2639
			if (!isset($split2_replies))
2640
			{
2641
				$split2_replies = 0;
2642
			}
2643
			// As before, fix number of replies.
2644
			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...
2645
			{
2646
				$split2_replies++;
2647
			}
2648
2649
			$split2_unapprovedposts = $row['message_count'];
2650
		}
2651
	}
2652
	$request->free_result();
2653
	$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...
2654
	$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...
2655
2656
	// No database changes yet, so let's double check to see if everything makes at least a little sense.
2657
	if ($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 || !isset($split1_approved) || !isset($split2_approved))
0 ignored issues
show
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...
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 $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_unapprovedposts does not seem to be defined for all execution paths leading up to this point.
Loading history...
2658
	{
2659
		throw new \ElkArte\Exceptions\Exception('cant_find_messages');
2660
	}
2661
2662
	// You cannot split off the first message of a topic.
2663
	if ($split1_first_msg > $split2_first_msg)
2664
	{
2665
		throw new \ElkArte\Exceptions\Exception('split_first_post', false);
2666
	}
2667
2668
	// The message that is starting the new topic may have likes, these become topic likes
2669
	require_once(SUBSDIR . '/Likes.subs.php');
2670
	$split2_first_msg_likes = messageLikeCount($split2_first_msg);
2671
2672
	// We're off to insert the new topic!  Use 0 for now to avoid UNIQUE errors.
2673
	$db->insert('',
2674
		'{db_prefix}topics',
2675
		array(
2676
			'id_board' => 'int',
2677
			'id_member_started' => 'int',
2678
			'id_member_updated' => 'int',
2679
			'id_first_msg' => 'int',
2680
			'id_last_msg' => 'int',
2681
			'num_replies' => 'int',
2682
			'unapproved_posts' => 'int',
2683
			'approved' => 'int',
2684
			'is_sticky' => 'int',
2685
			'num_likes' => 'int',
2686
		),
2687
		array(
2688
			(int) $id_board, $split2_firstMem, $split2_lastMem, 0,
2689
			0, $split2_replies, $split2_unapprovedposts, (int) $split2_approved, 0, $split2_first_msg_likes,
2690
		),
2691
		array('id_topic')
2692
	);
2693
	$split2_ID_TOPIC = $db->insert_id('{db_prefix}topics');
2694
	if ($split2_ID_TOPIC <= 0)
2695
	{
2696
		throw new \ElkArte\Exceptions\Exception('cant_insert_topic');
2697
	}
2698
2699
	// Move the messages over to the other topic.
2700
	$new_subject = strtr(Util::htmltrim(Util::htmlspecialchars($new_subject)), array("\r" => '', "\n" => '', "\t" => ''));
2701
2702
	// Check the subject length.
2703
	if (Util::strlen($new_subject) > 100)
2704
	{
2705
		$new_subject = Util::substr($new_subject, 0, 100);
2706
	}
2707
2708
	// Valid subject?
2709
	if ($new_subject != '')
2710
	{
2711
		$db->query('', '
2712
			UPDATE {db_prefix}messages
2713
			SET
2714
				id_topic = {int:id_topic},
2715
				subject = CASE WHEN id_msg = {int:split_first_msg} THEN {string:new_subject} ELSE {string:new_subject_replies} END
2716
			WHERE id_msg IN ({array_int:split_msgs})',
2717
			array(
2718
				'split_msgs' => $splitMessages,
2719
				'id_topic' => $split2_ID_TOPIC,
2720
				'new_subject' => $new_subject,
2721
				'split_first_msg' => $split2_first_msg,
2722
				'new_subject_replies' => $txt['response_prefix'] . $new_subject,
2723
			)
2724
		);
2725
2726
		// Cache the new topics subject... we can do it now as all the subjects are the same!
2727
		require_once(SUBSDIR . '/Messages.subs.php');
2728
		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

2728
		updateSubjectStats(/** @scrutinizer ignore-type */ $split2_ID_TOPIC, $new_subject);
Loading history...
2729
	}
2730
2731
	// Any associated reported posts better follow...
2732
	require_once(SUBSDIR . '/Topic.subs.php');
2733
	updateSplitTopics(array(
2734
		'splitMessages' => $splitMessages,
2735
		'split1_replies' => $split1_replies,
2736
		'split1_first_msg' => $split1_first_msg,
2737
		'split1_last_msg' => $split1_last_msg,
2738
		'split1_firstMem' => $split1_firstMem,
2739
		'split1_lastMem' => $split1_lastMem,
2740
		'split1_unapprovedposts' => $split1_unapprovedposts,
2741
		'split1_ID_TOPIC' => $split1_ID_TOPIC,
2742
		'split2_first_msg' => $split2_first_msg,
2743
		'split2_last_msg' => $split2_last_msg,
2744
		'split2_ID_TOPIC' => $split2_ID_TOPIC,
2745
		'split2_approved' => $split2_approved,
2746
	), $id_board);
2747
2748
	require_once(SUBSDIR . '/FollowUps.subs.php');
2749
2750
	// Let's see if we can create a stronger bridge between the two topics
2751
	// @todo not sure what message from the oldest topic I should link to the new one, so I'll go with the first
2752
	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

2752
	linkMessages($split1_first_msg, /** @scrutinizer ignore-type */ $split2_ID_TOPIC);
Loading history...
2753
2754
	// Copy log topic entries.
2755
	// @todo This should really be chunked.
2756
	$replaceEntries = array();
2757
	$db->fetchQuery('
2758
		SELECT 
2759
			id_member, id_msg, unwatched
2760
		FROM {db_prefix}log_topics
2761
		WHERE id_topic = {int:id_topic}',
2762
		array(
2763
			'id_topic' => (int) $split1_ID_TOPIC,
2764
		)
2765
	)->fetch_callback(
2766
		function ($row) use (&$replaceEntries, $split2_ID_TOPIC) {
2767
			$replaceEntries[] = array($row['id_member'], $split2_ID_TOPIC, $row['id_msg'], $row['unwatched']);
2768
		}
2769
	);
2770
2771
	if (!empty($replaceEntries))
2772
	{
2773
		require_once(SUBSDIR . '/Topic.subs.php');
2774
		markTopicsRead($replaceEntries, false);
2775
		unset($replaceEntries);
2776
	}
2777
2778
	// Housekeeping.
2779
	updateTopicStats();
2780
	updateLastMessages($id_board);
2781
2782
	logAction('split', array('topic' => $split1_ID_TOPIC, 'new_topic' => $split2_ID_TOPIC, 'board' => $id_board));
2783
2784
	// Notify people that this topic has been split?
2785
	require_once(SUBSDIR . '/Notification.subs.php');
2786
	sendNotifications($split1_ID_TOPIC, 'split');
2787
2788
	// If there's a search index that needs updating, update it...
2789
	$searchAPI = new SearchApiWrapper(!empty($modSettings['search_index']) ? $modSettings['search_index'] : '');
2790
	$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

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